Skip to content

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

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-17178-seed-write-context-export
Draft

feat(spec): export SEED_WRITE_EXECUTION_CONTEXT and bind all three seeders to it#17718
claude[bot] wants to merge 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

1 participant