Commit bdb247d
feat(spec): export SEED_WRITE_EXECUTION_CONTEXT and bind all three seeders to it (#17718)
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 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: 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
- **`SYSTEM_CTX` is 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`'s `SYSTEM_CONTEXT`, `service-knowledge`'s
`adminCtx`). 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.ts` is
the module that already owns the "one assembly of an `ExecutionContext`"
principle and names this exact defect family (#6071 / #6206 / #6551).
- **The shared singleton is not frozen at runtime, deliberately.**
`Object.freeze` would 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 is `as const satisfies
ExecutionContext`, so every consumer is compile-blocked from mutating
through the readonly properties. Noted as a boundary, not decided here.
- **The fourth-copy gate** described under "The pin" is a costed
proposal, not an omission: a repo-root `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.com/claude-code)
https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
---
_Generated by [Claude
Code](https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bd25e89 commit bdb247d
8 files changed
Lines changed: 181 additions & 40 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- runtime/src
- spec
- api-surface
- export-origins
- src/kernel
- verify/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
2095 | 2096 | | |
2096 | 2097 | | |
2097 | 2098 | | |
2098 | | - | |
2099 | | - | |
2100 | | - | |
2101 | | - | |
2102 | | - | |
2103 | | - | |
2104 | | - | |
2105 | | - | |
2106 | | - | |
2107 | | - | |
2108 | | - | |
2109 | | - | |
2110 | | - | |
| 2099 | + | |
| 2100 | + | |
| 2101 | + | |
2111 | 2102 | | |
2112 | | - | |
2113 | | - | |
2114 | | - | |
2115 | | - | |
2116 | | - | |
2117 | | - | |
2118 | | - | |
2119 | | - | |
2120 | | - | |
2121 | | - | |
| 2103 | + | |
| 2104 | + | |
| 2105 | + | |
| 2106 | + | |
| 2107 | + | |
| 2108 | + | |
| 2109 | + | |
| 2110 | + | |
2122 | 2111 | | |
2123 | | - | |
| 2112 | + | |
2124 | 2113 | | |
2125 | 2114 | | |
2126 | 2115 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
43 | 48 | | |
44 | | - | |
| 49 | + | |
45 | 50 | | |
46 | 51 | | |
47 | 52 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
376 | 376 | | |
377 | 377 | | |
378 | 378 | | |
| 379 | + | |
379 | 380 | | |
380 | 381 | | |
381 | 382 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
374 | 374 | | |
375 | 375 | | |
376 | 376 | | |
| 377 | + | |
377 | 378 | | |
378 | 379 | | |
379 | 380 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
199 | 200 | | |
200 | 201 | | |
201 | 202 | | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
433 | 433 | | |
434 | 434 | | |
435 | 435 | | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| |||
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
245 | 245 | | |
246 | | - | |
| 246 | + | |
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
| |||
0 commit comments