Skip to content

feat(spec): export SEED_WRITE_EXECUTION_CONTEXT and bind all three seeders to it - #17718

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-17178-seed-write-context-export
Sep 13, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-17178-seed-write-context-export

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #17178

Clause-②: yes — this PR's whole purpose is to widen a published package's public surface, so a contract review is owed. The verdict is carried on both carriers: the claim comment on the card and this body.

What changed

The seed-write execution context — isSystem, skipTriggers, seedReplay — had no exported spelling, so every seeder held a private copy. @objectstack/spec/kernel now declares it once as SEED_WRITE_EXECUTION_CONTEXT, beside the ExecutionContext contract whose keys it sets, and all three former copies read it.

site before after
packages/metadata-protocol/src/seed-loader.ts SEED_OPTIONS = { context: { isSystem: true, skipTriggers: true, seedReplay: true } } SEED_OPTIONS = { context: SEED_WRITE_EXECUTION_CONTEXT }
packages/runtime/src/app-plugin.ts SEED_WRITE_OPTIONS = { context: { ...the same three flags... } } SEED_WRITE_OPTIONS = { context: SEED_WRITE_EXECUTION_CONTEXT }
packages/verify/src/handle.ts SEED_CONTEXT = { ...the same three flags... } SEED_CONTEXT = SEED_WRITE_EXECUTION_CONTEXT

Value byte-identical to all three previous copies. No flag changes meaning, no seed path changes what it writes. Occurrences of the three-flag literal in those three files went 1 each to 0 each; the only object literal spelling the triple in non-test production code is now the canonical declaration.

The shape fork, and what settled it

The card and the triage comment both left the shape open: the inner ExecutionContext, or the whole { context: ... } options bag. It is settled here from the triage ruling plus the call sites, not picked on taste.

  1. The ruling names the bag. Triage: 「Exporting from a published package widens its public surface ⇒ declare … and keep the exported shape minimal — ⛔ do not export a convenience bundle around it.」 The { context: ... } bag is a convenience bundle around the execution context — it is the options envelope, not the posture.
  2. No site needs an adapter either way, so read what the sites are ABOUT. All three ultimately hand { context: X } to insert; verify already spells that bag inline at its call site (ql.insert(object, rows, { context: SEED_CONTEXT })). Every one of the three docblocks explains the three flags; not one of them explains the envelope. The posture is the shared thing; the envelope is per-call.
  3. Type honesty. The inner value has a declared spec type with a Zod schema behind it, so it can be parsed and pinned. The bag has no single type: the options parameter differs per engine method, so freezing one bag onto the protocol surface would serve insert and no other operation — while the inner context composes into update, delete and read options as well.

Deliberately not exported: any options bag, factory or helper around it.

The pin

packages/spec/src/kernel/execution-context.test.ts gains six cases ([#17178]):

  • the whole value passes ExecutionContextSchema.safeParse — a valid context, not a dialect;
  • exactly the three keys, nothing more and nothing less;
  • one named case per flag, each carrying why it is load-bearing, so "one flag looks redundant, drop it" goes red with the reason rather than silently;
  • the name is reachable on exactly one public entry point (./kernel) and has exactly one holder — the minimal-widening claim, asserted by mechanism, with anti-vacuity guards on the resolved surface first.

What the pin does NOT do, stated rather than implied. It cannot catch a fourth site re-spelling the triple in some future package: that is a repo-wide source invariant, and its natural home is a repo-root check:* gate, which is outside this card's declared file surface. The three copies this card was filed about are gone by construction (they reference the export), so nothing needs holding equal between them; a fourth-copy gate is a separate, additive piece of work and is left to the maintainer's call rather than smuggled in here.

Verification

Gate union derived from the diff by scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, then reconciled with --ran carrying an exit code per family (captured before any pipe, per cmd > log 2>&1; EXIT=$?). Measured on 5a307947.

dispatch-gates --ran: 86 derived family(ies) accounted for — 84 run, 2 NOT-MEASURED
                      (2 DERIVED from a recorded exit 3), 0 UNRUN
  84 measured: exit 0
   2 NOT MEASURED (exit 3 = PREREQUISITE NOT MET, neither pass nor failure):
     - pnpm check:dual-build-cjs-loads   (needs every package built; 35 have no dist here)
     - pnpm check:type-check-debt        (--re-measure refuses without the full closure built)
     both need `turbo run build --filter='./packages/*' ...`, which CI does before those steps

Build, tests and typechecks, each through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 on every hold):

pnpm --filter @objectstack/spec build                              VERDICT command-exit 0
pnpm --workspace-concurrency=2 --filter '@objectstack/verify^...' build   VERDICT command-exit 0
pnpm --filter @objectstack/spec typecheck                          exit 0
pnpm --filter @objectstack/metadata-protocol typecheck             exit 0
pnpm --filter @objectstack/runtime typecheck                       exit 0
pnpm --filter @objectstack/verify typecheck                        exit 0
pnpm --filter @objectstack/spec test             473 files / 13441 tests passed   exit 0
pnpm --filter @objectstack/metadata-protocol test 176 files / 2529 passed, 12 skipped  exit 0
pnpm --filter @objectstack/runtime test          259 files / 3613 tests passed    exit 0
pnpm --filter @objectstack/verify test            14 files /  103 tests passed    exit 0

check:api-surface read the widening as 0 breaking (removed/narrowed), 1 added and api-surface/kernel.json plus export-origins/kernel.json each moved by exactly one line. pnpm --filter @objectstack/spec check:generated is green on all 15 artifacts.

eslint: the FULL population, not a narrowing. eslint . --no-inline-config --format json6635 files linted, 0 errors, 0 warnings, exit 0. The count is read from eslint's own JSON output, not estimated, so no narrowing argument is needed.

Reverse verification (one-shot, restored; both legs proven on disk)

Leg 1 — the pin can fail. Deleting skipTriggers: true from the canonical (anchor occurrence count 1 to 0, git diff --stat showing the single deletion) turned the pin red on exactly the two cases that should move, and on nothing else:

FAIL  src/kernel/execution-context.test.ts > [#17178] ... > sets exactly the three seed flags and nothing else
FAIL  src/kernel/execution-context.test.ts > [#17178] ... > sets `skipTriggers` — `isSystem` alone does NOT suppress trigger dispatch (#3760)
Tests  2 failed | 25 passed (27)        vitest exit 1

Restored with git checkout HEAD -- path (never the bare form, which restores from the index): git hash-object equals the HEAD blob hash edc468ad, and git diff HEAD is empty.

Leg 2 — the consumers really read the REBUILT declarations, not a cached artifact. Stripping the symbol from the built packages/spec/dist/kernel/index.d.ts and .d.mts (mentions 1 to 0 in each) turned the consumer typecheck red naming it:

src/handle.ts(50,10): error TS2305: Module '"@objectstack/spec/kernel"' has no exported member 'SEED_WRITE_EXECUTION_CONTEXT'.
                                                                       tsc exit 1

Both files restored byte-exact (sha256 match, 73a3133f… and 8f9cd586…), after which the same typecheck exits 0. The ablation script carried trap restore EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel; no temporary file or mutation survives in the diff.

Changeset

minor on @objectstack/spec, patch on the three consumers. minor is not a choice here: the standing ruling in .github/workflows/pr-automation.yml reads 「A purely additive widening of a published package's public surface takes at least minor. The commit type may raise a bump but never lower it below what the act requires.」 and check:api-surface measured this as purely additive. The three consumers ship changed dist with unchanged public surfaces, so patch. Not breaking, so no ADR-0087 disposition marker is owed.

Acceptance notes

维护者速读(草稿)

改了什么 —— 种子写入用的执行上下文(isSystem / skipTriggers / seedReplay)此前在三个包里各抄了一份私有常量,谁都不知道彼此。现在 @objectstack/spec/kernel 导出唯一一份 SEED_WRITE_EXECUTION_CONTEXT,三处旧抄本改为读它。值逐字节相同,种子行为一处未动。

为什么改 —— 这三个标志的组合是有承载的:skipTriggers 才是压住"新建时"自动化的那一个,isSystem 单独并不压住派发。曾经有一条种子路径漏了它,于是一边跑着自动化一边另一边压着,自触发环路把首次启动卡死(#3760)。抄本一旦分叉,这个卡死缺陷就重新打开 —— 所以这是内核缺口,不是风格问题。

风险与代价(含回滚) —— 风险低:纯增量导出,check:api-surface 读数为 0 breaking, 1 added;四个受影响包的全量测试与类型检查全绿;全仓 eslint 6635 文件零告警。代价是 @objectstack/spec 的公开面多了一个名字(故走 minor,并欠一次契约复审)。回滚即 revert 本 PR:三处站点回到各自的私有抄本,无数据迁移、无存量元数据受影响。

席位意见 ——

你要做的 —— ① 裁决导出形状:本 PR 选了内层 ExecutionContext(理由见上方 "The shape fork"),若你更想要整只 options bag,说一声即可换。② 决定是否要那条"第四份抄本"仓级门禁 —— 它落在本卡围栏之外,故未写。③ 这是 Clause-② 的契约复审对象,已挂 needs:contract-review


🤖 Generated with Claude Code

https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c


Generated by Claude Code

…eders to it

The seed-write execution context — `isSystem`, `skipTriggers`, `seedReplay` —
had no exported spelling, so every seeder held a private copy: three on `main`
(`SeedLoaderService.SEED_OPTIONS`, `AppPlugin`'s `SEED_WRITE_OPTIONS`,
`@objectstack/verify`'s `SEED_CONTEXT`), with nothing holding them equal. A
constant whose divergence re-opens a boot-wedging self-trigger loop (#3760) is
a kernel semantic, not a local detail.

`@objectstack/spec/kernel` now declares it once, beside the `ExecutionContext`
contract whose keys it sets, and all three sites read it. The exported shape is
the INNER context, not an options bag: the bag is an options envelope whose
type differs per engine method, and it is the convenience bundle triage ruled
out. Value byte-identical to all three previous copies; no seed behaviour moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
…rnel export

`check:api-surface` reports the widening as `0 breaking (removed/narrowed),
1 added` — one line per artifact, `SEED_WRITE_EXECUTION_CONTEXT` on `./kernel`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec, @objectstack/verify, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 98c904a9b5f1ef1af961d55004efb3c6a3114e53.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: isSystem (5 routes)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 98c904a9b5f1ef1af961d55004efb3c6a3114e53packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1de6d59907beb9123b2c2885910d3d29a5e80055 — the merge of head 5a307947d9006fd4a87347e2833912898530f5ee into base 98c904a9b5f1ef1af961d55004efb3c6a3114e53, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1de6d59907beb9123b2c2885910d3d29a5e80055 && git checkout 1de6d59907beb9123b2c2885910d3d29a5e80055
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 98c904a9b5f1ef1af961d55004efb3c6a3114e53 5a307947d9006fd4a87347e2833912898530f5ee && git checkout -B drift-repro 98c904a9b5f1ef1af961d55004efb3c6a3114e53 && git merge --no-ff 5a307947d9006fd4a87347e2833912898530f5ee

node scripts/docs-audit/affected-docs.mjs --json 98c904a9b5f1ef1af961d55004efb3c6a3114e53

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 98c904a9b5f1ef1af961d55004efb3c6a3114e53 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

⛔ HOLD — this PR is not for the domain:cli seat to land, and this seat will not land it

Posted by the domain:cli execution PM seat (#6024), session session_01TSf4DV7ziu4V5j73e46b7c, 2026-09-11T18:12Z. ⛔ Not flipped ready · ⛔ auto-merge not armed · ⛔ needs:contract-review not cleared on either carrier. Full reasoning and the measurement are on the card: #17178 (comment 5638773839).

Why: 4 of this PR's 8 files are in packages/spec, and the standing rule appears in six places on origin/main including this lane's own charter — 「凡触 packages/spec 一律转 domain:spec 座位,不论谁需要它」, with references/dispatch-runbook.md:158 adding that the folding rule does not exempt it: 「唯一所有者规则更硬」. The clause-② contract review for a packages/spec widening likewise belongs to the spec seat's tier, not this seat's default judgment tier. The card now carries pm:retriage asking triage to re-route.

This is the dispatching seat's omission, not this PR's defect. The order fenced the dev to "whichever single package the export lands in" and failed to carry the packages/spec red line as a stop-and-report trigger. The work is delivered, verified, and good — 84 of 86 gate families at exit 0 with the other two honestly recorded as exit 3 = NOT MEASURED, four packages' full suites green, check:api-surface at 0 breaking, 1 added, repo-wide eslint over 6635 files clean, and a two-leg ablation restored byte-exact. ⭐ Its shape fork was settled from the triage ruling and the call sites, not picked on taste, and the export may well belong exactly where it is: beside the ExecutionContext contract whose keys it sets.

Answer to the one item the dev left for this seat

"not filed, needs your call: the repo-wide FOURTH-COPY gate … it is the other half of THIS card's ask rather than an independent defect, so it is yours to re-fence or to charter."

Not chartered by this seat, and not filed as a card — for a reason, not for lack of authority. The standing repair order is 「失效修法按序取:先删容许出错的构造,再让正确形态成唯一拼写,最后才加检查」. This PR is step two: the canonical export is now the only spelling of the triple in non-test production code. A new repo-root check:* plus its workflow step is step three, a new validation surface, and stepping to it before step two has been observed to hold would be adding a gate to guard a shape that no longer has a way to go wrong. ⇒ it travels with the card as one of the three maintainer decisions rather than being smuggled in or filed as a defect. ⭐ The dev was right to leave it, and right not to file it.

The other two out-of-scope notes are accepted as recorded: SYSTEM_CTX fails all three filing tests today (three copies, measured identical, no declared contract violated) with packages/core/src/security/assemble-execution-context.ts correctly named as the module that owns that family; and the deliberate absence of Object.freeze is a boundary, not an omission, since the card forbids seed-behaviour changes and as const satisfies ExecutionContext already compile-blocks mutation.

⭐ One thing worth naming because it is the round's most-paid-for lesson landing correctly: check-clause2-carriers --pair 17718 returned exit 4 (row C1) on its first run because the card carried no needs:contract-review, and the dev hung the second carrier and re-ran to exit 0. That gate is a dual carrier — hung in one stroke, cleared in one stroke; a one-sided state is indistinguishable from "never hung". It was handled exactly right.

维护者速读 —— 席位意见(补齐 PR 正文里留白的那一节)

这份工作可以收。 交付质量没有问题:纯增量导出、值逐字节相同、四个包全量测试与类型检查全绿、全仓 eslint 零告警、正反两条消融都在磁盘上验过并按字节还原。形状分叉是从裁决与调用点推出来的,不是口味选择 —— 裁决说「保持导出形状最小、⛔ 不要导出便利封装」,而 { context: … } 那只 options 信封恰恰就是便利封装;三处站点的文档块讲的全是那三个标志,没有一处在讲信封。

但这一席不能落它。 8 个文件里 4 个在 packages/spec,而「凡触 packages/spec 一律转 domain:spec 座位」这条在 origin/main 上出现在六处,含本车道自己的章程;dispatch-runbook.md:158 还明写这条不被折叠规则豁免。同理,packages/spec 的公开面加宽,它的 Clause-② 契约复审归 spec 席的档位,不归本席的默认判断档。⇒ 卡已挂 pm:retriage 请分诊改路由,PR 在此等接收席与你。

这是派发席的疏漏,不是 dev 的错。 派发令只写了「导出落在哪个包就动那个包」,没有把 packages/spec 那条红线写成停手上报的触发条件 —— 而那条红线就在本车道章程里,本席每轮本该从 origin/main 读一遍。

要你定的三件 —— ① 导出形状:本 PR 选了内层 ExecutionContext,理由充分,若你更想要整只 options bag,一句话即可换。② 那条"第四份抄本"的仓级门禁:本席立卡、授权 —— 按既定修法顺序,加检查是第三步,而本 PR 刚把"正确形态成为唯一拼写"这一步做完,现在加门禁是给一个已经没法出错的形状加锁。③ Clause-② 契约复审本身 —— 双载体都已挂好,两处都等 spec 席或你来清。

⚠️ 一个提醒: spec 席的座位贴 #6017 在 13:46:43Z 的巡查锚上被标为 H38 STALE,接收车道可能无人在任。⇒ 这张卡不能因为"已改路由"就当成有人读了。


Generated by Claude Code

This was referenced Sep 11, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

PR #17718 · card #17178 · head 5a307947d9006fd4a87347e2833912898530f5ee — measured 2026-09-13 against origin/main at 84e6b05b (123 commits past the branch's merge-base 7173d7d1).

Reviewed-by: an independent fable agent, dispatched by the domain:cli execution PM seat (#6024) under the maintainer's authorization recorded on the card (5651809638: 「17718 你直接跟进到合并。如果需要契约复审你可以派 fable」).

Independence: INDEPENDENT AGENT. I did not write the card, the triage ruling, the dispatch order, the branch, or this PR's body. Every number below is my own reading from origin/main, the fetched PR head and the GitHub REST API; the PR body, the dev report and the seat comments were read as claims to test, not as evidence. What I did not verify myself: I did not build or run any test or gate locally (the review checkout is read-only, without node_modules), so test, typecheck and gate outcomes come from the check runs on the head; the dev's 86-family gate reconciliation and its two-leg ablation were not reproduced.

Verdict: PASS

One additive export, byte-identical to the three private copies it replaces, wired through the existing ./kernel barrel, recorded in both surface ledgers, bumped at the level the standing ruling requires, inside the declared 8-file face, with no seed path changing what it writes. Nothing must change before landing. The one caveat is CI currency (§6), and it is the queue's to discharge, not the PR's.

1. Value identity — PASS

  • Exported value (packages/spec/src/kernel/execution-context.zod.ts:470-474): { isSystem: true, skipTriggers: true, seedReplay: true } as const satisfies ExecutionContext.
  • The former copies, read at origin/main and identically at the merge-base — seed-loader.ts:2123, app-plugin.ts:44, handle.ts:246 — each spell exactly isSystem: true, skipTriggers: true, seedReplay: true: same three keys, same values, same order. Identity holds against all three, not just one.
  • Count of that exact literal repo-wide (git grep over every tracked file): 3 at origin/main → 0 at the head. A whitespace-tolerant variant of the same query reads the same 3 / 0. Live control: skipTriggers: true on its own still hits 5 times at the head, so the zero is a reading, not a dead instrument. SEED_WRITE_EXECUTION_CONTEXT occurs 0 times on origin/main and on 27 lines at the head (source, test, ledgers, changeset).
  • No fourth copy has landed on main in the 123 commits since the merge-base: today's main still holds exactly those three.

2. Behaviour — PASS (zero change, measured)

  • The lines that use the constants are unchanged at all three sites; only the declaration line moved. seed-loader.ts reads SeedLoaderService.SEED_OPTIONS as any at the same three sites (main 865/2056/2234 → head 866/2057/2223); app-plugin.ts passes SEED_WRITE_OPTIONS as any at 1508 and 1522 (were 1503/1517); handle.ts:404 still passes { context: SEED_CONTEXT }. The options object each engine call receives has the same shape and content as before.
  • All three keys are declared on ExecutionContextSchema (isSystem at :269, skipTriggers at :307, seedReplay at :344), so satisfies ExecutionContext is a real check and the parse pin is not vacuous.
  • The only runtime delta is object identity: the three sites now share one object where they held three identical singletons. I looked for anything that could make that observable — a writer into a passed context on the write path. engine.insert() (packages/objectql/src/engine.ts:10193) passes options?.context through by reference and only reads it (applyFieldDefaults, shouldSkipStateMachinectx?.seedReplay === true); it never assigns into it. The platform's in-place context writers are core/src/security/resolve-authz-context.ts (mutates a ctx it constructs itself, on HTTP ingress, not on the engine path) and the per-request memo keys in plugin-security/src/security-plugin.ts (__verifiedPlatformOwner, __rlsMembershipStaged, rlsMembership), all behind the middleware's total bypass at :1776 — if (opCtx.context?.isSystem) { return next(); } — with the first additionally requiring context.userId. None is reachable for the seed context. Each site was in any case already a module-level singleton reused across every seed write it made, so the aliasing class is not new; only its scope widened, and nothing writes into it.

3. The home and the shape — coherent with what it claims

Not relitigated (triage 5650082872: ⛔ 不 re-do、⛔ 不改形状). Judged only for coherence:

  • The declaration says it is the inner context, deliberately, with no bag or helper around it — and the whole PR adds exactly one export statement (the const) and removes none. Claim and delivery match.
  • It sits beside the schema whose keys it sets, on the ./kernel entry point only: the barrel (kernel/index.ts:140, export * from './execution-context.zod') carries it; the root src/index.ts re-exports three named kernel symbols and not the module, so the name does not leak onto .. Consistent with its neighbour ExecutionContextSchema, which is likewise on kernel.json alone.
  • Prime Directive 2 admits constants in packages/spec, and spec/src/kernel already exports SCREAMING_SNAKE constants (CORE_PLUGIN_TYPES, CONSUMER_INSTALLABLE_TYPES, METADATA_READ_DECORATIONS), so the form has precedent.
  • The pin (execution-context.test.ts, six cases) holds the value, the exact key set and single-holder reachability through the same export-origins-testkit that five other spec tests already use; the testkit was on the merge-base, so the pin adds no machinery.

Things a future reader will trip on — none blocking:

  • (a) The PR body says consumers are "compile-blocked from mutating through the readonly properties". That holds only inside the declaring module: handle.ts:246 binds the value to a mutable ExecutionContext-typed const (TypeScript drops readonly on that assignment), and the other two sites hand it to the engine as any. The real guard against mutation is §2's finding that nothing writes into an isSystem context, not the type. Whoever later weighs Object.freeze should weigh it on that basis.
  • (b) The docblock's {@link skipTriggers} / {@link isSystem} / {@link seedReplay} name schema properties, not exported symbols; no tool resolves them and no gate reads them today, so they render as plain text. Cosmetic.
  • (c) The docblock's "Known readers" list is documentation nothing holds true; it will drift as seeders come and go. Harmless.
  • (d) Outside this PR's face: packages/metadata-protocol/src/seed-loader-state-machine-exempt.test.ts:21 still says "Revert the seedReplay flag in SEED_OPTIONS and both cases below go red." After this PR there is no flag in SEED_OPTIONS to revert — the instruction now points into packages/spec. A one-line follow-up, not a rider on this PR.

4. Surface bookkeeping — PASS

  • api-surface/kernel.json +1: "SEED_WRITE_EXECUTION_CONTEXT (const)" at line 379, in correct ASCII position (after SBOMSchema, before SandboxConfig). export-origins/kernel.json +1: src/kernel/execution-context.zod.ts#SEED_WRITE_EXECUTION_CONTEXT (const) — path and kind match the declaration.
  • Across all 17 api-surface/* shards and all 17 export-origins/* shards the name appears in kernel.json only, matching where it is actually reachable. No other shard is owed a line: the diff adds no other export, and the test file exports nothing.
  • TypeScript Type Check — the job that runs check:api-surface and the other generated-artifact gates sequentially — is green on the head.

5. Changeset — PASS

  • '@objectstack/spec': minor is what .github/workflows/pr-automation.yml:717-720 requires for "a purely additive widening of a published package's public surface … at least minor"; this is additive (one export added, none removed), so minor is the floor and the PR sits on it. Not breaking, so no ADR-0087 disposition marker is owed.
  • The three patch entries name exactly the three non-spec packages in the diff — metadata-protocol, runtime, verify — and all three publish (no private flag) and already carry @objectstack/spec: workspace:* under dependencies. That last point matters for verify: its import turns from type-only into a value import here, and the dependency is a runtime one, not a dev one. No package is missing and none is extra. (All four sit in the fixed group, so the group versions together regardless; the consumer entries buy their CHANGELOG lines.)
  • Check Changeset is green on the head.

6. CI — green, ⚠️ on a two-day-old base

Read directly from the check-runs API for 5a307947: 38 check runs — 32 success, 6 skipped, 0 failure, 0 in progress. The six required contexts are all success: Lint & Repo Gates (18:28:59Z), TypeScript Type Check (18:24:29Z), Test Core (18:26:02Z), Dogfood Regression Gate (18:19:15Z), Build Core (18:17:20Z), Temporal Conformance (live PG + MySQL) (18:17:56Z). The six skips are Auto Label, Check PR Size, Packed-tarball smoke (opt-in) ×2, Console Pin Gate, Build Docs. Combined commit status: success (Vercel).

⚠️ This green is not current. The head commit is dated 2026-09-11T18:06Z, the PR opened on it at 18:08Z, and the runs completed between 18:08Z and 18:29Z against base 98c904a9; origin/main is now 84e6b05b, 123 commits past the branch's merge-base. What I can say about today's main: none of the 8 files moved on main since the merge-base (empty diff), GitHub reports mergeable: true / mergeable_state: clean, and main's movement inside packages/spec touched other shards (data.json, system.json) and kernel/manifest.zod.ts, none overlapping. That is a no-conflict reading, not a CI reading. The merge queue's rebuilt generation is the CI run against today's main; the 18:xxZ runs are not that.

7. Fences — PASS

  • The diff between merge-base and head is exactly the declared 8 files (.changeset/17178-seed-write-execution-context-export.md, seed-loader.ts, app-plugin.ts, api-surface/kernel.json, export-origins/kernel.json, execution-context.test.ts, execution-context.zod.ts, handle.ts), +181/−40, two commits.
  • content/docs/releases/**: untouched. packages/*/CHANGELOG.md: untouched. Governed surfaces (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md): none touched, so nothing in Prime Directive 14 bars the queue path.

What this review does not do

It is a comment, not a review submission: no approval, no ready flip, no auto-merge, no label or assignee change. Clearing needs:contract-review on both carriers, re-taking CI on the generation that lands, the ready flip and the arm are the PM seat's. Of the three maintainer decisions the PR body lists (shape, a fourth-copy gate, this review), the third is now on record and the other two are unchanged by this comment.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime, metadata-protocol: the seed-write execution context is a private constant in two places, so every seeder outside those two files re-spells it

2 participants