feat(spec): export SEED_WRITE_EXECUTION_CONTEXT and bind all three seeders to it - #17718
feat(spec): export SEED_WRITE_EXECUTION_CONTEXT and bind all three seeders to it#17718claude[bot] wants to merge 2 commits into
Conversation
…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
📓 Docs Drift CheckThis PR changes 4 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
⛔ HOLD — this PR is not for the
|
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/kernelnow declares it once asSEED_WRITE_EXECUTION_CONTEXT, beside theExecutionContextcontract whose keys it sets, and all three former copies read it.packages/metadata-protocol/src/seed-loader.tsSEED_OPTIONS = { context: { isSystem: true, skipTriggers: true, seedReplay: true } }SEED_OPTIONS = { context: SEED_WRITE_EXECUTION_CONTEXT }packages/runtime/src/app-plugin.tsSEED_WRITE_OPTIONS = { context: { ...the same three flags... } }SEED_WRITE_OPTIONS = { context: SEED_WRITE_EXECUTION_CONTEXT }packages/verify/src/handle.tsSEED_CONTEXT = { ...the same three flags... }SEED_CONTEXT = SEED_WRITE_EXECUTION_CONTEXTValue 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.{ context: ... }bag is a convenience bundle around the execution context — it is the options envelope, not the posture.{ context: X }toinsert;verifyalready 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.insertand 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.tsgains six cases ([#17178]):ExecutionContextSchema.safeParse— a valid context, not a dialect;./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--rancarrying an exit code per family (captured before any pipe, percmd > log 2>&1; EXIT=$?). Measured on5a307947.Build, tests and typechecks, each through
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0on every hold):check:api-surfaceread the widening as0 breaking (removed/narrowed), 1 addedandapi-surface/kernel.jsonplusexport-origins/kernel.jsoneach moved by exactly one line.pnpm --filter @objectstack/spec check:generatedis green on all 15 artifacts.eslint: the FULL population, not a narrowing.
eslint . --no-inline-config --format json— 6635 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: truefrom the canonical (anchor occurrence count 1 to 0,git diff --statshowing the single deletion) turned the pin red on exactly the two cases that should move, and on nothing else:Restored with
git checkout HEAD -- path(never the bare form, which restores from the index):git hash-objectequals the HEAD blob hashedc468ad, andgit diff HEADis 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.tsand.d.mts(mentions 1 to 0 in each) turned the consumer typecheck red naming it:Both files restored byte-exact (sha256 match,
73a3133f…and8f9cd586…), after which the same typecheck exits 0. The ablation script carriedtrap restore EXIT INT TERMwith absolute paths resolved fromgit rev-parse --show-toplevel; no temporary file or mutation survives in the diff.Changeset
minoron@objectstack/spec,patchon the three consumers.minoris not a choice here: the standing ruling in.github/workflows/pr-automation.ymlreads 「A purely additive widening of a published package's public surface takes at leastminor. The commit type may raise a bump but never lower it below what the act requires.」 andcheck:api-surfacemeasured this as purely additive. The three consumers ship changeddistwith unchanged public surfaces, sopatch. Not breaking, so no ADR-0087 disposition marker is owed.Acceptance notes
SYSTEM_CTXis the same duplication class one level out, and is NOT touched here.{ isSystem: true, positions: [], permissions: [] }is a private constant in three files (plugins/plugin-approvals/src/approval-node.ts,plugins/plugin-approvals/src/approval-service.ts,plugins/plugin-sharing/src/sharing-rule-service.ts), with two more shapes nearby (verify'sSYSTEM_CONTEXT,service-knowledge'sadminCtx). All measured identical today — no divergence, no contract violated, no authoring trap — so it is noted, not filed. The successor who would carry it is a follow-up on single-sourcing the plain system context;packages/core/src/security/assemble-execution-context.tsis the module that already owns the "one assembly of anExecutionContext" principle and names this exact defect family (两处手写的 ExecutionContext 组装已漂移:REST 传输不带principalKind/onBehalfOf,而 explain / security 会读它 #6071 / 同族第三处组装:share-link 路由把授权信封裁成 4 个字段后直接当 enforcement context 喂给 engine.find ——group租户姿态下 Layer 0 墙恒判否 #6206 / dispatcher 面的 /share-links 把已解析完整的 ExecutionContext 重新裁成两个字段再喂给 enforcement —— 与 #6206 同一条 enforcement 路径的另一张脸 #6551).Object.freezewould be a real improvement against aliasing, and there is precedent for it in this package, but it is a behaviour change on the seed write path if any layer ever mutates a context in place — and the card forbids changes to seed behaviour. The type isas const satisfies ExecutionContext, so every consumer is compile-blocked from mutating through the readonly properties. Noted as a boundary, not decided here.check:*script plus its workflow step, all outside this card's declared file surface.维护者速读(草稿)
改了什么 —— 种子写入用的执行上下文(
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