Skip to content

fix(objectql,metadata-protocol): a static readonly field is stripped from a non-system INSERT inside engine.insert, and the boundary copy is deleted - #15395

Merged
zhuangjianguo merged 28 commits into
mainfrom
claude/issue-14147-engine-insert-readonly-strip
Sep 5, 2026
Merged

fix(objectql,metadata-protocol): a static readonly field is stripped from a non-system INSERT inside engine.insert, and the boundary copy is deleted#15395
zhuangjianguo merged 28 commits into
mainfrom
claude/issue-14147-engine-insert-readonly-strip

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14147

readonly is now enforced in-engine on INSERT for non-system callers, exactly as
it already was on UPDATE. Maintainer ruling of 2026-09-03 (option C, comment
5522734749, verbatim 「同意」), presented as overturning their own 2026-07-24
"INSERT (all callers) exempt" row: one semantics, one enforcement point.

⛔ Draft, needs:contract-review, not enqueued, no auto-merge.
Governed-prose half lands separately: #15382 (draft, review requested from
os-zhuang and hotlong, human merge).

Patch round R4 — the E3 ruling 5550450281 (「已发布必修,其余立卡」) applied to contract re-review 5550434842 (FAIL) → head a73dd85db

One commit appended on 485a2d525 (⛔ no rebase, no amend, no force-push):
a73dd85db — the three sites the measurement below shows reach a PUBLISHED
.d.ts, plus one changeset line. ⛔ origin/main is still NOT merged this
round
(F2: #15225 / PR #15687 land first), so mergeable_state: dirty stays
expected; the both-sides set grows from six to seven files —
settings-service.ts has one main commit since the merge-base (6b8c67778,
#15434; hunks at :33-38, :1816, :2056-2100, disjoint from R4's
:2274-2285). ⛔ No behaviour change, no test touched, E1/E2 untouched; items
2–6 of the third verdict are not re-opened except where the measurement moved
one into item 1 (it moved two halves — below).

The ruling, and what it narrowed

The maintainer, answering E3 (comment 5550450281, 2026-09-05, verbatim):
已发布必修,其余立卡」 — for the class 「a superseded architecture still
stated as live」, a published surface (packages/spec .describe()/TSDoc that
ships in .d.ts, the generated content/docs/references/**, customer-facing
docs pages) is fixed in the round; non-published residue (test headers, in-source
notes that reach no published artefact, scripts/ provenance text) becomes one
finding card and the PR lands. ⇒ The seven-item patch list became two:
item 1 every site that reaches a published artefact, measured per site;
item 7 the sweep re-bound by claim. The residue is #15819.

Item 1 — 「published」 measured per site (built dist, exit captured before any pipe)

Method: rebuild @objectstack/spec, @objectstack/objectql,
@objectstack/service-settings (and their closure) at the head, then for each
N5 site grep its sentence as a fixed string in dist/**/*.d.ts (the shipped
declaration surface), in dist/**/*.{js,mjs,cjs}, and in the .map files, plus
content/docs/** (the generated references/** projections and the customer
pages); beside each, a firing control — the exported identifier the sentence
sits next to, grepped the same way. Before = 485a2d525; after = a73dd85db
rebuilt. One boundary stated so nobody re-derives it: every shipped .js.map
embeds its whole source (sourcesContent), so by that reading no source comment
is ever non-published — that reading contradicts the ruling's own examples, so
the .d.ts / docs measurement is the one applied and the .map column is
reported, not scored.

site before: .d.ts / .js / .map control verdict R4
(a) spec/src/security/public-form.ts:21-22 («insert may seed readonly columns») — and the same docblock's :10-11 («the static-readonly strip (#2948) only covers UPDATE — so nothing downstream guards these columns»), the same claim in the spelling the reviewer's patterns did not carry hit (dist/security/index.d.ts:278, .d.mts) / none / 2 files — :10-11 likewise hit at :266 PUBLIC_FORM_SERVER_MANAGED_FIELDS in .d.ts 6 lines published fixed — both sentences; after rebuild: 0 / 0 / 0, control still 6
(b1) objectql/src/readonly-strict-errors.ts:21-22 — the module docblock («a create is deliberately exempt from the author-declared strips (#3413)») none / none / 4 files ReadonlyFieldRejectedError in .d.ts 4 lines not published — it is a floating docblock ahead of the non-exported READONLY_CLASS_REASONS; declaration emit drops it, esbuild drops it from the JS carded (#15819 row 1) — ⚠️ so the file now disagrees with itself between :21 and :170; that is what the ruling prescribes and the card says so
(b2) objectql/src/readonly-strict-errors.ts:170-172 — the operation property TSDoc of the exported class («an INSERT refusal can only ever be about a runtime-owned value») hit (dist/index.d.ts:362, :373, .d.mts ×2) / none / 4 files same published — ⭐ Zone 2 (1) falsified fixed; after: 0 / 0 / 0, the new sentence at dist/index.d.ts:365, :379
(c) service-settings/src/settings-service.ts:2279 — the upsertRow docblock («The INSERT path is deliberately exempt from that strip (#3413)») hit (dist/index.d.ts:1150, .d.cts) / hit (dist/index.js:1722, .cjs:1803 — the root tsup config keeps this comment in the JS) / 2 files «Why the update is a SYSTEM write» in .d.ts 2 lines published — ⭐ Zone 2 (1) falsified again fixed; after: 0 / 0 / 0, the new sentence at dist/index.d.ts:1156
(d) settings-secret-rotation.test.ts:13, :189; sys-secret-orphan-report.test.ts:190 none / none / none wrapEngineAsSettingsEngine in .d.ts 4 lines not published (test files are outside the entry) carded (rows 2–4)
(e) rest/src/rest-batch-endpoint.test.ts:308-312 none / none / none droppedFields in dist/index.cjs 34 lines not published carded (row 5)
(f) service-automation/src/builtin/crud-dropped-fields.test.ts:144-146 none / none / none create_record in .d.ts 4 lines not published carded (row 6)
(g) spec/scripts/liveness/proof-registry.mts:156 none in dist, liveness/, json-schema/, prompts/, api-surface/, content/docs readonly-static-write in liveness/field.json, liveness/README.md not published (scripts/ is outside packages/spec's files; the :150 summary is true but names UPDATE only) carded (row 7)

So the reviewer's three-way distinction had three different answers: (a) published; (b) half — the module docblock is not, the property TSDoc is; (c) published, in the .d.ts and even in the .js. The residual #3413 in the rebuilt spec .d.ts (analytics.zod-*.d.ts:1318) is contracts/data-engine.ts:145, the R2-corrected historical sentence («true when written, SUPERSEDED since») — class H, published, true.

# what changed at a73dd85db pin that goes red if lost exit measured
1a public-form.ts TSDoc: the anonymous-surface rationale no longer rests on «the strip only covers UPDATE» — it states the strip runs inside engine.insert for a non-system caller since the 2026-09-03 ruling and is a policy over readonly: true declarations, never over a column by name (which is why this surface still denies the anchors by name); the authenticated-write example now says a non-system insert is stripped in-engine and an import seeds read-only columns only under a system context (preserveAudit is UPDATE-only, #6640). Added to the cross-lane table below; @objectstack/spec stays patch check:api-surface «unchanged» (no surface moved); check:generated fixed point (no projection regenerates — TSDoc on a const is not a .describe()); the prose itself by item 2's list, pattern p6 spec build 0 · check:generated 0 «All 15 generated artifacts are up to date» · check:api-surface 0 «public API surface + factory signatures unchanged ✓» · check:docs 0 «230 generated files in sync» · check:liveness 0 · git status empty after both
1b readonly-strict-errors.ts:166-175 (operation TSDoc only): an INSERT refusal is about a runtime-owned value (exempt writers isSystem + preserveAudit) or, since the ruling, a static readonly value from a non-system caller (only exempt writer isSystem; preserveAudit is UPDATE-only for that strip); readonlyWhen still locks nothing on a create. ⛔ buildRefusalMessage untouched (byte-pinned #5126/#5503). The module docblock :5-60 is deliberately left — non-published, carded engine-insert-static-readonly-strip.test.ts «strictReadonlyWrites refuses before any driver dispatch» (a static completed_at refused with ERR_READONLY_FIELD_REJECTED, 0 creates) — the refusal the old sentence denied objectql typecheck 0 (incl. check:test-typecheck OK 44 files / 69 pinned) · 6 suites (engine-insert-static-readonly-strip, engine-autonumber-runtime-owned, validation/rule-validator, engine-lookup-referential-integrity, engine-repo-execute-elevation, engine-strict-readonly-warning-truthful) 266 passed
1c settings-service.ts:2274-2285 (upsertRow docblock): «deliberately exempt (#3413)» → on THIS object the insert is outside the strip because sys_setting is sys_-prefixed and managedBy: 'engine-owned', which staticReadonlyInsertSubject leaves to the platform object's own guards while UPDATE applies no such carve-out (#15719) — the superseded row named as superseded, not as the reason. The changeset adds '@objectstack/service-settings': patch so the corrected .d.ts ships (prose only; the package's behaviour is unchanged) the package's own suites (behaviour unchanged); prose by item 2's list service-settings typecheck 0 · test 29 files / 519 passed · check-adr-0087-registration / check-changeset-no-major / check-empty-changeset --base origin/main 0 / 0 / 0

Zone 2 — one of three falsified. (1) ⭐ Falsified: N5(a) is not the only published site — (b2) and (c) reach a .d.ts too (the table's greps, before/after, with controls), so both moved into item 1 and the card is smaller than the verdict's b–g. (2) Held: check:generated after the rebuild reports «All 15 generated artifacts are up to date» and git status is empty — a TSDoc on a const regenerates no content/docs/references/** projection (nothing to commit from the generator). (3) Held: check:api-surface on the rebuilt dist answers «public API surface + factory signatures unchanged ✓».

Item 2 — the sweep re-bound by CLAIM, not spelling

The class: any present-tense statement that a non-system INSERT is exempt from, or seeds past, the static readonly strip, or that the strip lives at the DataProtocol ingress.

Command (sweep-r4.sh, run from the worktree at the head named, then from a detached tree at 485a2d525 for the before-reading): scope packages content docs skills scripts with :!**/CHANGELOG.md :!content/docs/releases/** :!content/docs/references/**, all git grep -n -iE: A the twelve premise spellings of R3; B ingress.*(readonly|strip|seed)|(readonly|strip|seed).*ingresscontent-only, one regex over the line, ⛔ never a filter over grep -n output; p1 insert[^|]{0,80}exempt|exempt[^|]{0,80}insert; p2 readonly-exempt; p3 may seed|can seed; p4 seed[^|]{0,40}readonly|readonly[^|]{0,40}seed; p5 #3413; p6 (R4-added, the spelling N5(a)'s first sentence used and none of the above carries) covers UPDATE|strips? (readonly|read-only)[^|]{0,60}UPDATE payload|from (an|the) UPDATE payload. Union by file:line, every line classified below.

pattern at 485a2d525 (positive control — must fire) at a73dd85db live-class hits after
A 31 lines 31 0
B (content-only) 32 — the 32 the reviewer measured; the R3 naive form returns 42, the 10 extra admitted by file name 32 0
p1 60 — fires on N5 (b) :21, :170; (c) :2279; (d) ×3; (e) :308; (f) :145; (g) :156 59 7 non-published sites, all on #15819; 0 published
p2 3 — fires on (f) :145 3 1 (the same (f) line)
p3 8 — fires on (a) :22 7 0
p4 41 — fires on (a) :22 40 0
p5 17 — fires on (b) :22, (c), (d) ×3, (e), (g)'s neighbours 16 the carded (b1)/(d)/(e) lines
p6 6 — fires on (a) :11 5 0 (the remaining 5: four true UPDATE statements + skills/objectstack-data/SKILL.md:274, the governed half, #15382)
union 150 unique file:line 147 7 sites / 8 lines, every one on #15819 — removed by R4: public-form.ts:11, :22, readonly-strict-errors.ts:170, settings-service.ts:2279; added: settings-service.ts:2280 (the superseded row named as superseded — H + R)

The two descriptive imprecisions of the R3 list, corrected (the R3 text below is struck in place): (i) R3's second pass filtered grep -n output, so 10 of its 72 sites were admitted by their file name, not the line — protocol.readonly-insert.test.ts :4 :15 :23 :55 :130, import-runner-historical-readonly-insert.test.ts :23 :101 :127 :136, validate-readonly-flow-writes.ts:17; they keep their classes in the R3 list (over-included, not laundered) and are absent from pass B here by construction; (ii) «five file:lines matched by both passes are listed once per match» was wrong — exactly one line (R3 #23/#53, engine-insert-static-readonly-strip.test.ts:17) was listed twice; 72 unique sites.

Classes — H historical (the superseded architecture stated as past) · R ruled state (true today) · RW about readonlyWhen, still insert-exempt · #7823 the internal: true write-response strip, a different strip at the protocol ingress by the 2026-08-13 ruling · D delegation prose (the ingress forwards whole and relays the engine verdict) · P a pin that forbids the spelling · U an unrelated subject sharing the spelling · F a parser fixture · L the live class — non-published, carded on #15819 · S stale by omission, not false (carded on #15819 as secondary) · G the governed half, #15382. Path prefixes: docs/=content/docs/, adr/=docs/adr/, qa/=docs/qa/platform-checklist/areas/, lint/=packages/lint/src/, mp/=packages/metadata-protocol/src/, objectql/=packages/objectql/src/, rest/=packages/rest/src/, spec/=packages/spec/, dogfood/=packages/qa/dogfood/test/, sa/=packages/services/service-automation/src/, ss/=packages/services/service-settings/src/, plugins/=packages/plugins/.

147 lines (H 42 · R 33 · U 33 · RW 11 · #7823 8 · L 8 · H + R 6 · P 2 · D 1 · S 1 · F 1 · G 1):

# site passes class says
1 docs/automation/hook-bodies.mdx:264 A+p1 H + R ctx.api.object('x').insert({ FIELD }) ¦ Silently dropped — unless the ho…
2 docs/data-modeling/fields.mdx:319 p4 R readonly ¦ boolean ¦ false ¦ Prevent editing — hidden from create/edit f… — customer page: «server-enforced on both write paths»
3 docs/kernel/contracts/data-engine.mdx:311 A+p1 RW Of the two AUTHOR-DECLARED strips only readonlyWhen is insert-exempt at this…
4 docs/kernel/contracts/data-engine.mdx:371 A+p1 H paragraph said the two AUTHOR-DECLARED strips were insert-exempt at this seam
5 docs/kernel/contracts/data-engine.mdx:372 p3+p4+p5 H (#3413: an in-process create may seed a readonly: true field's initial value… — continuation of the :371 quotation; :373 «That row is superseded»
6 docs/kernel/contracts/data-engine.mdx:377 p1 RW stays insert-exempt (a conditional lock has no prior record on a create). So a…
7 docs/permissions/system-context.mdx:114 B R 20 ¦ readonly strip bypassed — INSERT ¦ objectql ¦ Same, on create — one…
8 docs/protocol/objectql/security.mdx:276 p1 R *preserveAudit is an UPDATE-path exemption. It does not apply on INSERT (#66
9 adr/0066-unified-authorization-model.md:63 p3 U A package may seed permission-set policies (incl. per-object grants for **its…
10 adr/0131-total-organization-ownership-no-null-organization-id.md:849 p1 U C1 ¦ Default Organization load-bearing under single and created before appli…
11 qa/access-security.json:129 p4 U "showcase_contact.lead_score is the seeded readonly:true field (no defaultValu…
12 qa/cli.json:1099 p4 U "clause": "read-only in fact, not just in intent: the DB file is byte-identica…
13 packages/client/src/index.ts:415 p4 R record was written — e.g. a non-system create cannot seed a static readonly@objectstack/client TSDoc, published; cites #3043 as origin, the statement holds under C
14 packages/cli/src/commands/meta/resync.ts:82 p4 U viewer_readonly …) are seeded INSERT-ONCE at boot: an existing row is never
15 packages/cli/src/commands/migrate/duplicates.ts:884 p4 U and suppresses the artifact seed, readOnlyProbe refuses to bring a
16 lint/system-fields-consumers.test.ts:136 p4 U readonly seeded: boolean;
17 lint/validate-flow-node-writes.test.ts:412 p1 R inside engine.insert (#14147), not an exemption. This rule asks a
18 lint/validate-readonly-action-writes.test.ts:29 A+p1 H what changed is that "INSERT is exempt" stopped being a true reason for
19 lint/validate-readonly-action-writes.test.ts:289 A+p1 H [#14147] This block used to be titled "INSERT is exempt from both strips"
20 lint/validate-readonly-action-writes.test.ts:299 p4 R an ELEVATED insert seeding a readonly AND a readonlyWhen-locked column — a fact about elevation, not INSERT
21 lint/validate-readonly-action-writes.test.ts:313 A+p1 P expect(reason).not.toMatch(/INSERT is exempt/i);
22 lint/validate-readonly-action-writes.ts:59 A+p1 H two. The premise this bullet used to carry - "INSERT is exempt from BOTH
23 lint/validate-readonly-action-writes.ts:61 A+p1 H overturning their own 2026-07-24 "INSERT (all callers) exempt" row) put
24 lint/validate-readonly-action-writes.ts:64 A+p1 R this bullet cited. So INSERT is no longer engine-exempt from anything.
25 lint/validate-readonly-action-writes.ts:70 p1 RW engine.ts: "INSERT stays exempt"), and the STATIC one is skipped on THIS
26 lint/validate-readonly-action-writes.ts:73 p4 R ruling landed: an elevated insert seeding a readonly AND a — same
27 lint/validate-readonly-action-writes.ts:174 A+p1 H the 2026-09-03 ruling the reason was a one-liner - "INSERT is exempt from both
28 lint/validate-readonly-action-writes.ts:179 A+p1 P alone. ⛔ Neither reason may be restated as "INSERT is exempt": that sentence
29 lint/validate-readonly-action-writes.ts:190 p1 RW 'does not have. engine.ts states it at the bulk strip: "INSERT stays exempt"…
30 lint/validate-readonly-flow-writes.test.ts:330 A H [#14147] This case used to be justified by "create_record is engine-exempt
31 lint/validate-readonly-flow-writes.ts:7 p5 H the step still reports success. #3407/#3413 made that strip observable at — #3407/#3413 made the strip observable — true history
32 lint/validate-readonly-flow-writes.ts:15 A+p1 H be that INSERT was engine-exempt from the author-declared static-readonly
33 lint/validate-readonly-flow-writes.ts:16 A+p4+p5 H strip (#3043/#3413: "a create_record may legitimately seed readonly
34 lint/validate-readonly-flow-writes.ts:20 B R the static strip for a non-system caller, the ingress copy is deleted,
35 lint/validate-readonly-flow-writes.ts:221 p6 R `runAs:'${runAs}' the engine silently strips readonly fields from the UPDATE p…
36 lint/validate-readonly-hook-writes.test.ts:215 p1 R describe('validateReadonlyHookWrites - GREEN today: insert()/create() are a SC…
37 lint/validate-readonly-hook-writes.test.ts:216 A+p1 H [#14147] This block used to be titled "INSERT is engine-exempt" and rested
38 lint/validate-readonly-hook-writes.test.ts:217 A H on exactly that sentence ("a create may legitimately seed read-only
39 lint/validate-readonly-hook-writes.test.ts:218 p5 H columns", #3043/#3413). The maintainer ruling of 2026-09-03 (option C)
40 lint/validate-readonly-hook-writes.ts:43 A+p1 H used to be that INSERT was engine-exempt from the author-declared
41 lint/validate-readonly-hook-writes.ts:44 A+p5 H static-readonly strip (#3043/#3413: "a create may legitimately seed
42 lint/validate-readonly-hook-writes.ts:210 p1 H used to say the opposite — that the engine exempts INSERT from the
43 lint/validate-readonly-hook-writes.ts:211 A+p4 H author-declared static-readonly strip so a create may legitimately seed
44 lint/validate-readonly-hook-writes.ts:212 p5 H read-only columns (#3043/#3413). The maintainer ruling of 2026-09-03
45 lint/validate-readonly-hook-writes.ts:339 p6 R `on every non-system trigger the engine strips readonly keys from that UPDATE…
46 mp/protocol.dropped-fields.bulk.test.ts:108 p4 R it('omits droppedFields when no row seeds a readonly field', async () = {
47 mp/protocol.dropped-fields.test.ts:4 p5 H #3413 built the engine-level onFieldsDropped channel and wired the flow
48 mp/protocol.dropped-fields.test.ts:126 p4 R it('omits droppedFields when the create seeds no readonly field', async () = {
49 mp/protocol.readonly-insert.test.ts:6 B H #3043 put the strip at this DataProtocol ingress because the engine was
50 mp/protocol.readonly-insert.test.ts:7 B+p1+p2+p5 H INSERT-readonly-exempt (#3413), and the ingress is the seam every external
51 mp/protocol.readonly-insert.test.ts:13 A+p1 H "INSERT (all callers) exempt" row: the strip is stripReadonlyFields inside
52 mp/protocol.readonly-insert.test.ts:106 B D describe('#14147 — the create ingress DELEGATES the readonly strip to engine.i…
53 mp/protocol.ts:10393 B H ingress copy it replaces (stripReadonlyForInsert, #3043) is deleted
54 mp/protocol.ts:10395 A+p1 H 2026-09-03, superseding the 2026-07-24 "INSERT (all callers) exempt"
55 mp/protocol.ts:10491 B #7823 [#7823] Same ingress strip as createData — a clone's 201 body is
56 mp/protocol.ts:10542 p5 U mirroring service-automation's update_record wiring (#3413). A faulty — the update_record wiring #3413 built
57 mp/protocol.ts:11952 B #7823 [#7823] Per-outcome ingress strip — the partial-success face hands
58 mp/protocol.ts:12020 B #7823 [#7823] Ingress strip over each row's data payload — the bulk
59 mp/protocol.write-response-internal-fields.tripwire.test.ts:9 B #7823 The internal: true write-response strip lives at the protocol ingress, not
60 mp/protocol.write-response-internal-fields.tripwire.test.ts:244 B #7823 describe('#7823 tripwire: every generic data ingress strips internal: true f…
61 mp/seed-loader-state-machine-exempt.test.ts:50 p1 U Reproduces the objectql insert-time initialStates guard, honoring the #3433 ex… — the #3433 initialStates exemption
62 mp/seed-loader.ts:201 p4 U const ALL_SEED_ENVS: readonly SeedEnv[] = ['prod', 'dev', 'test'];
63 mp/seed-loader.ts:222 p4 U const NODE_ENV_TO_SEED_ENV: ReadonlyRecordstring, SeedEnv = {
64 mp/seed-loader.ts:2032 p4 U private static readonly SEED_OPTIONS = { context: { isSystem: true, skipTrigge…
65 objectql/engine-autonumber-resync.test.ts:381 p1 U it('a beforeInsert hook stamp — the third exempt writer (#6339)', async () = { — runtime-owned exempt writer
66 objectql/engine-autonumber-runtime-owned.test.ts:362 p1 R it('INSERT: the exemptions survive strict — strict refuses only what the strip… — exempt writers survive strict — true
67 objectql/engine-autonumber-runtime-owned.test.ts:550 B H ingress when this note was written (stripReadonlyForInsert, #3043), and
68 objectql/engine-autonumber-runtime-owned.test.ts:552 B H (option C, #14147), which deleted that ingress copy. Either way it is a strip
69 objectql/engine-autonumber-runtime-owned.test.ts:561 B H So the static strip skips runtime-owned types outright — the ingress copy did
70 objectql/engine-autonumber-runtime-owned.test.ts:634 B R it('an author-declared readonly field of an ORDINARY type is still stripped…
71 objectql/engine-capability-provenance.test.ts:158 p4 U const SEEDED_SECURITY_COLLECTIONS: ReadonlyArrayreadonly [string, string] = [
72 objectql/engine-hook-provenance-sibling-seams.test.ts:219 p4 R it('⛔ NEGATIVE CONTROL: strictReadonlyWrites still REFUSES the unauthorised se…
73 objectql/engine-hook-provenance-sibling-seams.test.ts:360 A+p1 RW TRUE predicate. INSERT is exempt from readonlyWhen entirely, so no lock
74 objectql/engine-insert-static-readonly-strip.test.ts:17 A+B H The create-side strip lived at the DataProtocol ingress
75 objectql/engine-insert-static-readonly-strip.test.ts:31 A+p1 H own 2026-07-24 "INSERT (all callers) exempt" row, verbatim 「同意」: one
76 objectql/engine-insert-static-readonly-strip.test.ts:34 p4 R seeding a readonly column at create time is done under system context, the
77 objectql/engine-insert-static-readonly-strip.test.ts:203 p4 R it('isSystem seeds the readonly column — the exemption the ruling names', asyn…
78 objectql/engine-insert-static-readonly-strip.test.ts:276 p4 R .toContain('may not seed a readonly column');
79 objectql/engine-insert-static-readonly-strip.test.ts:301 p1 RW it('readonlyWhen stays INSERT-exempt — a conditional lock has no prior record…
80 objectql/engine-strict-readonly-warning-truthful.test.ts:22 p4 R INSERT { strictReadonlyWrites: true }, caller seeds account_number — a caller-seeded autonumber refused under strict — runtime-owned
81 objectql/engine-update-addressing-id-not-dropped.test.ts:158 B U it('the REST ingress shape reports NOTHING: {…, id} + where.id, id readonly'…
82 objectql/engine-update-addressing-id-no-warn.test.ts:54 B U ¦ REST ingress shape: {value,id} + where.id, readonly id ¦ NONE…
83 objectql/engine-update-addressing-id-no-warn.test.ts:177 B U it('THE REPRO: the REST ingress shape logs NOTHING, and the strip is unchanged…
84 objectql/engine.ts:4610 p1 U Adoption runs BEFORE the driver write, so an exempt insert that then fails — autonumber adoption, «exempt insert» = adopted under an exemption
85 objectql/engine.ts:9632 p1 RW "INSERT stays exempt" for exactly that strip), and the FLS write gate throws
86 objectql/engine.ts:9634 A H what this path actually strips. The 2026-07-24 row "INSERT (all callers)
87 objectql/engine.ts:10521 B #7823 write-response strip lives at the GENERIC-DATA-PATH INGRESS —
88 objectql/engine.ts:11687 p1 RW single-id stripReadonlyWhenFields; INSERT stays exempt.
89 objectql/internal-fields.test.ts:245 B #7823 201/200 bodies are stripped at the generic-data-path ingress
90 objectql/readonly-strict-errors.ts:21 p1 L INSERT only the runtime-owned ones, because a create is deliberately exempt — N5 (b1) — #15819 row 1
91 objectql/readonly-strict-errors.ts:22 p5 L from the author-declared strips (#3413). One error names everything wrong — same sentence
92 objectql/validation/record-validator.test.ts:618 p1 U it('autonumber stays exempt on update as on insert', () = {
93 objectql/validation/rule-validator.test.ts:1355 p1+p5 H [#14147] This case used to say "insert keeps its #3413 exemption" and
94 objectql/validation/rule-validator.test.ts:1357 B H insert strip lives (that is the #3043 protocol ingress)". The maintainer
95 objectql/validation/rule-validator.ts:643 p6 RW record from an UPDATE payload — the field is locked, so an incoming change is — the readonlyWhen docblock
96 objectql/validation/rule-validator.ts:872 A+p1 RW predicate is fail-open for that row. INSERT is exempt (update path only),
97 objectql/validation/rule-validator.ts:1021 p6 S Strip CALLER-SUPPLIED writes to read-only fields from an UPDATE payload — stripReadonlyFields docblock names UPDATE only; runs on both verbs since #14147#15819 secondary
98 objectql/validation/rule-validator.ts:1318 p1 H originally because INSERT was exempt from the author-declared static-`readonly…
99 objectql/validation/rule-validator.ts:1328 p3 R Runtime-owned fields carry none of that ambiguity: nobody may seed a record — nobody may seed a record number — true
100 objectql/validation/rule-validator.ts:1484 A+p1 H and superseded the 2026-07-24 row "INSERT (all callers) exempt". The
101 objectql/validation/rule-validator.ts:1486 A H semantics — and the boundary copy that used to run at the DataProtocol ingress
102 objectql/validation/rule-validator.ts:1509 A+p1 H ruled on its own merits, NOT on the "INSERT is exempt" row that ruling C
103 objectql/validation/rule-validator.ts:1540 p1 R non-system INSERT that asks for the historical-import exemption is TOLD it
104 objectql/validation/rule-validator.ts:1567 p1 R runs, not WHAT exempts it. So engine.insert calls {@link
105 objectql/validation/rule-validator.ts:1602 p4 R write from a system context (\context.isSystem`) — a non-system create may…
106 plugins/plugin-security/src/authz-matrix-gate.test.ts:442 p1 U it('platform admin may insert a cross-org row on a PRIVATE object (posture exe…
107 plugins/plugin-security/src/walled-platform-bucket-diagnostic.test.ts:364 p4 U the organization-less writer seeds a DIFFERENT list, so acme_readonly
108 dogfood/authz-conformance.matrix.ts:281 B H + R { id: 'readonly-static-write', summary: 'static readonly: true stripped from…
109 dogfood/authz-conformance.matrix.ts:282 B+p4 R enforcement: 'UPDATE: objectql/engine.ts stripReadonlyFields on the single-id…
110 dogfood/authz-conformance.matrix.ts:284 B+p1+p4 H + R note: 'The #3003 field report: readonly: true used to be UI-only, so a logge…
111 dogfood/field-zoo.matrix.ts:55 p4 U body: (seeded: ReadonlyRecordstring, string) = Recordstring, unknown;
112 dogfood/showcase-static-readonly.dogfood.test.ts:24 B H create INGRESS (stripReadonlyForInsert, metadata-protocol) — the seam every
113 rest/import-runner-historical-readonly-insert.test.ts:15 B H + R [#14147] WHERE that strip lives moved — from the DataProtocol ingress into
114 rest/rest-batch-endpoint.test.ts:308 p1+p5 L The engine's INSERT path is static-readonly-exempt by design (#3413), so — N5 (e) — #15819 row 5
115 rest/rest-server.ts:12359 B H #3043 static-readonly strip lived at that ingress and
116 rest/rest-write-response-internal-fields.tripwire.test.ts:330 B #7823 + 'same ingress POST /data/:object uses, stripped there.',
117 packages/runtime/src/app-plugin.ts:138 p4 U private readonly skipSeedData: boolean;
118 packages/runtime/src/http-dispatcher.test.ts:688 p3 U in domain-handler-registry.test.ts, which can seed an
119 packages/runtime/src/seed-datasets.ts:72 p4 U export function mergeSeedDatasets(ctx: unknown, datasets: readonly unknown[]):…
120 sa/builtin/create-record-readonly-drop.test.ts:118 p4 R expect(creates[0], 'a non-system flow may not seed a readonly column').not.toH…
121 sa/builtin/create-record-readonly-drop.test.ts:136 p4 R expect(creates[0].completed_at, 'seeding a readonly column at create time is a…
122 sa/builtin/crud-dropped-fields.test.ts:145 p1+p2 L Today ObjectQL's insert path strips nothing (INSERT is readonly-exempt, — N5 (f) — #15819 row 6
123 sa/builtin/crud-nodes.ts:323 p1+p2 H stripped nothing (INSERT was readonly-exempt; FLS write
124 sa/builtin/crud-nodes.ts:328 p4 R seeds a readonly column now lands here: `output.dropped-
125 ss/settings-secret-rotation.test.ts:13 p1+p5 L INSERT path is deliberately exempt (#3413). — N5 (d) — #15819 row 2
126 ss/settings-secret-rotation.test.ts:189 p1+p5 L context.isSystem), while the INSERT path is exempt (#3413). It is a — N5 (d) — row 3
127 ss/settings-service.ts:2280 p1 H + R "INSERT exempt" row (superseded by the 2026-09-03 ruling, #14147: — R4: the superseded row named as superseded
128 ss/sys-secret-orphan-report.test.ts:190 p1+p5 L context.isSystem), while the INSERT path is exempt (#3413). It is also — N5 (d) — row 4
129 spec/liveness/field.json:87 p1 RW "note": "renderer CEL + server write path: stripReadonlyWhenFields drops UPDAT…
130 spec/liveness/field.json:102 B+p1+p4 H + R "note": "renderer + server write path: a non-system write to the field is sile…
131 spec/scripts/build-schemas-check-mode.test.ts:2442 p4 U const seedRetiredKeys = (table: Recordnumber, readonly string[]): void = {
132 spec/scripts/build-schemas-check-mode.test.ts:2740 p4 U const seedRetiredDefs = (table: Recordnumber, readonly string[]): void = {
133 spec/scripts/build-schemas-check-mode.test.ts:3100 p4 U const seedRetiredKeys = (table: Recordnumber, readonly string[]): void = {
134 spec/scripts/liveness/evidence.test.ts:33 B F 'packages/objectql/src/validation/rule-validator.ts (UPDATE strip); packages/m…
135 spec/scripts/liveness/proof-registry.mts:156 p1 L strip (forge dropped, sibling edit lands, insert exempt) over real HTTP. — N5 (g) — #15819 row 7
136 spec/src/api/protocol.zod.ts:1975 p4 R 'before the record was written — a non-system create cannot seed a static rea… — the R3-corrected .describe()`
137 spec/src/contracts/data-engine.ts:145 p1+p5 H insert exempt from the two AUTHOR-DECLARED strips (#3413: "an in-process
138 spec/src/contracts/data-engine.ts:146 p3+p4 H create may seed a readonly: true field's initial value") — true when
139 spec/src/contracts/seed-settlement.ts:92 p4 U readonly suppressed: readonly SeedSuppressionReason[];
140 spec/src/contracts/share-link-service.ts:185 p3 U (skip the per-object opt-in check) so platform bootstrappers can seed
141 spec/src/data/field-autonumber-readonly.test.ts:89 A U summary is a stored roll-up a caller MAY legitimately seed (#6014).
142 spec/src/data/field.zod.ts:1581 p1 R readonly: z.boolean().default(false).describe('Read-only — never editable in f…
143 spec/src/data/temporal-conformance.ts:90 p3 U can seed a genuinely mixed writer population through its own create()
144 spec/src/kernel/execution-context.test.ts:179 p1 R it('states that the exemption does NOT reach INSERT', () = {
145 spec/src/kernel/execution-context.zod.ts:392 B+p1 R preserveAudit: z.boolean().optional().describe('Historical import: preserve th…
146 spec/src/stack.zod.ts:1427 A U demand they appear in config.objects: an app legitimately seeds the
147 skills/objectstack-data/SKILL.md:274 p6 G the field silently stripped from an UPDATE payload; the write reports — #15382, governed, ⛔ not this PR

Not in the sweep's scope by its own exclusions: **/CHANGELOG.md (history), content/docs/releases/** (release-owned, untouched — git diff --name-only a06faebbe...HEAD -- 'content/docs/releases/**' returns 0 files against a 10-file content/docs/** control), content/docs/references/** (generator-owned; git grep -iE 'insert exempt|may seed' -- content/docs/references returns nothing).

Item 3 — the residue card

#15819finding only, ungraded, unrouted, unassigned: the seven non-published live-class sites (rows 1–7 = (b1), (d) ×3, (e), (f), (g)) each by file:line with what it says and why it is the class, the by-omission rule-validator.ts:1021 as a marked secondary entry, the governed SKILL.md:274 excluded by name, the measurement method, the templates to close it, and the statement that the maintainer ruled these carded rather than fixed. Cross-links #14147, this PR, ruling C, the three verdicts and the E3 ruling. Dedupe before filing: one targeted MCP search_issues (27 results; nearest #15394 = the lint rules' scan gap, #15703 = cloneData's missing droppedFields — neither is this), plus the open finding list (one page of 100) grepped locally.

⚠️ CI reality — NOT MEASURED, stated rather than implied

GET /commits/a73dd85db/check-runstotal_count: 0; the PR reads mergeable: false, mergeable_state: dirty. GitHub runs no pull_request workflow on a conflicted PR, so — as on 485a2d525, e78c95cb4, 66c580b0cno CI has run on any head since bd598e803, which predates R2's one behaviour change. The local farm below is the only evidence for the last four commits, and the landing must read the merged head's checks, ⛔ never bd598e803's.

R4 measurements — all at a73dd85db, builds/tests through scripts/pm/os-verify-lock.sh (slot dev-14147-r4, --status read first: free), every exit captured before any pipe, VERDICT lines read

run exit result
lock A (waited 284s, held 165s): spec build · objectql build · service-settings build · git status 0 · 0 · 0 · empty 34/34 .d.ts emitted; gen:schema rewrote nothing
lock A: check:generated · check:api-surface · check:docs · check:liveness · git status 0 · 0 · 0 · 0 · empty «All 15 generated artifacts are up to date» · «unchanged ✓» · «230 generated files in sync» · 36 rows current
lock B (waited 332s, held 53s): objectql typecheck · 6 suites · service-settings typecheck · test 0 · 0 · 0 · 0 test-typecheck OK 44/69 · 266 passed · tsc clean · 29 files, 519 passed
lock C (waited 0s, held 424s): turbo build --filter='@objectstack/client-react...' --filter='@objectstack/driver-turso...' (35 tasks) · check:skill-examples · check:type-check-debt · rm the gitignored .examples-buildcheck:docs-audit-scope · turbo build --filter='./packages/**' (71/71 cached) · check:dual-build-cjs-loads 0 ×6 257 examples type-check · 13 entries, 143 raw, none above record · in sync, 190 docs · 103/66/619/1 vs floors 90/58/520/1
dist measurement (the table above), before and after, controls firing grep exits captured (a) (b2) (c): .d.ts hit → 0; (b1) (d) (e) (f) (g): 0 throughout
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from this worktree, no paths (52-path change set from the merge-base), under its own STALE TREE warning (139 commits behind origin/main, 60 gate files changed — F4) 0 112 commands, all executed sequentially unlocked with per-command capture: 107 at 0 first run; check:cross-package-test-inputs 1 → 0 — red only under my runner's NODE_USE_ENV_PROXY=1 (Node prints the experimental-undici warning on import, failing its own «importing this module prints NOTHING» self-test) — an artefact of the runner, not the tree, 0 with the env unset (declared: the first 84 commands ran with that env; only that self-test is sensitive to it); check:docs-audit-scope 1 → 0 with the gitignored packages/spec/.examples-build left by check:skill-examples removed (#15328, pre-existing); check:skill-examples 1, check:type-check-debt 3, check:dual-build-cjs-loads 3 = PREREQUISITE (client-react .d.ts / driver-turso / every dist) → 0 after lock C's builds. Among the 0s: check:system-context-census (+ self-test) «105 sites / 19 packages / 44 files; 139 anchors resolve» — no re-anchor needed (the one anchor in an edited file, readonly-strict-errors.ts:66, is above the edit); check:doc-authoring «14753 customer-facing strings clean; 829 prose ids hold»; check:nul-bytes 7497 files; check:engine-double-contract; check:corpus-claim-drift
the 3 workflow-valued changeset gates --base origin/main 0 · 0 · 0 valid no-migration-prescription · no major · 1 declaring changeset

NOT MEASURED, declared: CI on this head (above, structural); check:react-declaration-parity (objectui's manifest — the on-demand pin-bump gate, as on every card); the 10 workflow-valued families reading $RUNNER_TEMP / matrix.shard; the two main-only families absent from this branch (the merge round's); pnpm --filter @objectstack/spec typecheck (comment-only edit; the build's DTS pass compiled it — a declared narrowing); the lint / metadata-protocol / rest / service-automation / dogfood / example-todo suites of R3 (no file in those packages changed this round; R3's readings at the parent commit stand — a declared narrowing).

Attribution: this patch round was produced in Claude Code session session_01ARYe3yQTQCUFm5qPYNgKaJ.

Patch round R3 — contract re-review 5549250610 (FAIL) → head 485a2d525

Two commits appended on 66c580b0c (⛔ no rebase, no amend, no force-push):
e78c95cb4 items 1–3 and 5 at the source · 485a2d525 item 1's regenerated
reference rows plus one census re-anchor. ⛔ origin/main is still NOT merged
this round
(F2 upheld: 0 commits on engine.ts since the merge-base, #15225
lands first), so mergeable_state: dirty stays expected. ⛔ No behaviour change,
no test deleted or skipped; E1 and E2 are not touched.

# Verdict item What changed Pin that goes red if lost Exit measured
1 N2 (a)–(e): four .describe() strings + the Clone TSDoc (a) execution-context.zod.ts preserveAudit: «the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem»; (b) CreateDataResponseSchema.droppedFields: «the strip runs inside engine.insert, after the beforeInsert hooks, isSystem-gated»; (c) CloneDataResponseSchema TSDoc: «the static readonly strip applied inside engine.insert for a non-system caller — the 2026-09-03 ruling, #14147; the #3043 ingress copy is deleted»; (d) CreateManyDataResponseSchema.droppedFields: «the in-engine create-side strip (engine.insert, isSystem-gated)»; (e) BatchOperationResultSchema.droppedFields: «the in-engine static readonly strip on create». ⛔ No issue id inside any .describe()check:doc-authoring's customer-facing-spec-text rule forbids one there, so those cite the ruling by date. Regenerated with check:generated --fix (it reported exactly one stale artifact, content/docs/references/**; gen:docs rewrote 12 rows of references/data/data-engine.mdx, 1 of references/kernel/execution-context.mdx, and the droppedFields rows of references/api/protocol.mdx / references/api/batch.mdx — ⛔ nothing by hand). git grep -F 'at the DataProtocol ingress' -- content/docs/references: 13 rows before, 0 after. The cross-lane section below now lists every spec file check:generated (a describe edit without regeneration); check:api-surface (no surface moved); the prose itself by item 4's list spec build 0 · check:generated --fix 0 · check:generated 0 «All 15 generated artifacts are up to date» · check:api-surface 0 «public API surface + factory signatures unchanged»
2 N2 (f)–(k) (f) rest-server.ts batch-route note: the exemption sentence is deleted; the note says the strip lived at the ingress when written and runs inside engine.insert since the ruling, and stands the routing on what the ingress still owns — the #3770 object-existence gate, the #7823 internal: true response strip and the droppedFields relay (one create ingress, one response contract). ⚠️ One deliberate deviation from the verdict's phrase «surviving grounds — platform carve-out, one ingress»: the carve-out is kept as a stated fact but attributed to the engine, because that is where it lives (staticReadonlyInsertSubject, rule-validator.ts:1515-1525, returns null on managedBy / sys_; createData in protocol.ts has 0 mentions of either) — as a routing ground it would be a false fact of exactly the class this PR keeps failing on; (g) :8008: «the engine's create-side static-readonly strip dropped (engine.insert, relayed by createData as droppedFields)»; (h) rule-validator.ts:998-1008: the second consumer is {@link staticReadonlyInsertSubject} in this module, the #5628-time consumer named as history; (i) stdio-data-bridge.ts:48-54: the readonly strip leaves the divergence list, with a parenthetical stating it is closed since C on both transports; (j) engine-autonumber-runtime-owned.test.ts:611-616 comment and :634 title; (k) rest-dropped-fields.test.ts:97 title item 4's list; mcp / rest typecheck for the comment-only edits rest 2 suites 7/7 · objectql 5 suites 255/255 · mcp typecheck 0 · rest typecheck 0
3 N3: the seven why rows Re-derived by construct against the head table: 24→23 ×2 (the tenant-audit early return; bypassTenantAudit threading — head row 23 «Tenant-audit warning silenced»), 22→21 ×2 (strictReadonlyWrites refusal; READONLY_CLASS_REASONS — head row 21 «Strict-drop refusal never fires»), 25→24 (system-write-guard.ts helper — head row 24 «Engine-owned / append-only write guard bypassed»), 34→33 (revoke()'s CONFLICT guard — head row 33), 60→59 (stampSystemInsertOwner — head row 59 «Automation flow data nodes re-add the owner_id stamp»); :318 «row 2» unchanged and right. No mechanical pin exists: the gate only interpolates why into its error text (:698, :828-869) and parses the two count-row patterns; the eye check against the head table is the whole evidence, and no test outside the script names it (control: 13 self-mentions) none — stated --self-test 0 · gate 0
4 Make the sweep claim true Re-run over the twelve spellings plus the ingress co-occurrence pass, before and after editing; every remaining hit is listed and classified below (73 lines). R2's sentence «Every remaining hit is a historical quotation or is about readonlyWhen» was false at 66c580b0c (eleven live sites) and is withdrawn — the list replaces it the list itself; the reviewer re-runs the grep against it sweep 0 with a firing positive control
5 N4: the matrix row enforcement and note now state the two mechanisms: (1) a system context — identity provisioning (plugin-auth/src/objectql-adapter.ts:738, :1123 wrap the engine in withSystemContext); (2) the sys_/managedBy carve-out in staticReadonlyInsertSubject — the metadata repository (sys-metadata-repository.ts:661-683, :804-824; isSystem has 0 hits in that file) seeds sys_metadata_history.recorded_by (readonly: true lookup) under the caller's context. «Event-log cursors» re-derived by name: sys_metadata_history.event_seq (readonly: true, «Per-organization monotonic event log cursor», sys-metadata-history.object.ts:61-66), written by those same two inserts — the second mechanism, not a system context; no other object declares a cursor column. The row's issue-id multiset is byte-identical (#2948 ×2, #3003 ×3, #3043 ×2, #5591 ×1) for the prose-id ledger; both strings carry no unescaped apostrophe (the R2 defect) test/authz-conformance.test.ts (47) holds the row's ids and shape, not its prose — the prose is held only by item 4's list dogfood authz-conformance 47/47 · check:doc-authoring 0 «sibling-package prose ids hold the baseline — no growth, no burn-down unrecorded»

Zone 2 — falsified: none of the four. (1) check:generated --fix regenerated every row the describe edits moved, and a second check:generated reports nothing stale — a fixed point. (2) check:api-surface answers «unchanged» on the rebuilt dist; @objectstack/spec stays patch. (3) All seven row numbers re-derived by construct give the verdict's seven. (4) No test pins the bridge's divergence list: git grep -iE 'divergence|ingress' -- packages/mcp outside the bridge hits only comments about other divergences (ExecutionContext assembly in __tests__/plugin-execution-context.test.ts:10, the exposure gate in stdio-data-bridge.exposure.test.ts), and «Known divergences» occurs nowhere but the bridge (control: 5 divergence hits inside it) — prose-only, mcp typecheck 0. Beyond the four: the verdict's four classes did not cover every sweep hit, so three further honest classes (ruled state · a pin that forbids the spelling · an unrelated subject sharing the spelling) are declared in the list rather than forced into the four.

R3 measurements — all at 485a2d525, every build/test through scripts/pm/os-verify-lock.sh (slot dev-14147-r3), every exit captured before any pipe

run exit result
pnpm --filter @objectstack/spec build · check:generated --fix 0 · 0 1 of 15 stale (content/docs/references/**) → gen:docs; held 187s, waited 301s
check:generated (fixed point) · check:api-surface · check:liveness 0 · 0 · 0 «All 15 generated artifacts are up to date» · «unchanged ✓» · 36 ledger rows current
pnpm --filter @objectstack/lint test 0 94 files, 2909 passed
metadata-protocol typecheck · test 0 · 0 tsc clean · 161 files (+2 skipped), 2374 passed (+10 skipped)
objectql typecheck (incl. check:test-typecheck, 44 files / 69 pinned) · 5 suites engine-insert-static-readonly-strip, engine-autonumber-runtime-owned, validation/rule-validator, engine-lookup-referential-integrity, engine-repo-execute-elevation 0 · 0 OK · 5 files, 255 passed
service-automation create-record-readonly-drop · rest import-runner-historical-readonly-insert + rest-dropped-fields 0 · 0 2 passed · 2 files, 7 passed
dogfood test/authz-conformance.test.ts · pnpm --filter @objectstack/example-todo test 0 · 0 47 passed · 4 files, 106 passed
mcp typecheck · rest typecheck (+ its check:test-typecheck) 0 · 0 comment-only edits, tsc clean
check-system-context-census --self-test · gate · --fix · gate · --fix again 0 · 1 · 0 · 0 · 0 the 1 was pure line rot — stdio-data-bridge.ts:246:250 after my four header lines; 1 anchor rewritten, then 0 rewritten (fixed point); «105 sites / 19 packages / 44 files; 139 anchors resolve, 27 declared non-read»
check:doc-authoring (self-test + gate) · check:nul-bytes 0 · 0 8745 .describe() strings clean; 829 pinned prose ids hold; 7497 files, no control bytes
check-adr-0087-registration · check-changeset-no-major · check-empty-changeset, each --base origin/main 0 · 0 · 0 valid no-migration-prescription · no major · 1 declaring changeset
git diff --name-only a06faebbe...HEAD -- 'content/docs/releases/**' 0 files control: the same filter on content/docs/** returns 10 (6 hand-written + 4 regenerated)

Gate union derived from this worktree at 485a2d525 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths — the tool took the 49-path change set from the merge-base itself), printed under its own STALE TREE warning (96 commits behind origin/main, 39 gate files changed there — F4, the deriver's drift, not a missing run): 112 commands (72 pnpm, 40 node; 99 by path, 9 by change kind, 7 whole-tree). All 112 executed sequentially with the capture idiom, unlocked as the lock's own coverage statement says they are; per exit:

exit families
0 108 of 112 on first run — among them check:system-context-census (+ self-test), check:type-check-coverage, check:doc-authoring, check:engine-double-contract, check:corpus-claim-drift, check:nul-bytes, check:cross-package-test-inputs, check:pm-dispatch-gates, spec check:api-surface / check:authorable-surface / check:docs / check:generated / check:liveness / check:export-origins / check:strictness-ledger, and the rest of the derived list
1 → 0 check:skill-examples (needs client-react built; then 257 examples type-check) · check:docs-audit-scope (its affected-docs self-test reds while packages/spec/.examples-build/**, left behind by check:skill-examples, exists — #15328, pre-existing; 0 with the gitignored artefact removed, and docs-audit/check-affected-docs.mjs + check-drift-comment.mjs re-measured 0 the same way)
3 → 0 check:dual-build-cjs-loads (PREREQUISITE NOT MET until every packages/* was built under the lock, 71 turbo tasks; then 103/66/619/1 against floors 90/58/520/1) · check:type-check-debt (until driver-turso was built; then «13 ledger entries re-measured, 143 raw tsc errors, none above its recorded number»)
13 workflow-valued families (not in the 112) the 3 changeset gates run locally with --base origin/main (all 0, above); the other 10 read $RUNNER_TEMP / matrix.shard inputs (check-shard-attestation, check-test-completeness, check-cross-package-test-inputs --union-into …) — NOT MEASURED, CI-only by construction
31 artifact rosters the deriver scores silent by construction (9 under my paths) run anyway: 28 at 0; check-partof-closing-keyword.mjs / check-single-claim-paths.mjs answer exit 2 «NOT WIRED» with no PR context (a usage verdict) — re-run against THIS PR: PR_BODY=THIS-BODY node scripts/check-partof-closing-keyword.mjs → exit 0 «no Part-of/closing-keyword contradiction»; PR_NUMBER=15395 … check-single-claim-paths.mjs → exit 0 «modifies none of the 1 declared at-most-one-writer path(s)» — needs NODE_USE_ENV_PROXY=1 here: Node's own fetch bypasses the container proxy that injects the token and gets 401 without it · check:react-declaration-parity exit 1 «MANIFEST is not set — this gate did NOT run» — objectui's manifest, the on-demand pin-bump gate (maintainer ruling 2026-08-07), never a CI family; NOT MEASURED here as on every card
main-only check-platform-object-tenancy-census.mjs, check-registry-log-declared.mjs do not exist on this branch (no merge this round) — F3, run at the merge round

Item 4 — the sweep, re-run, every remaining hit classified

Command (sweep.sh, from the worktree at the head named above): the nine spellings R2 declared plus the reviewer's three — git grep -n -iE 'INSERT is exempt|engine-exempt|legitimately seed|exempt from the readonly strip|INSERT \(all callers\)|plus INSERT|absent BY DECISION|insert-exempt at this seam|at the DataProtocol ingress|INSERT is engine-exempt|create-side strip lives|ingress strip covers' — then git grep -n -iE ingress filtered to lines also matching readonly|strip|seed, both over packages content docs skills scripts with :!**/CHANGELOG.md :!content/docs/releases/** :!content/docs/references/**. Positive control: the literal at the DataProtocol ingress fires on execution-context.zod.ts at 66c580b0c (1 hit) and on nothing in packages/spec/src at 485a2d525. 73 lines remain (five file:lines matched by both passes are listed once per match)corrected in R4: 72 unique sites; one line (#23/#53) is listed twice; and because the second pass filtered grep -n output, 10 of the 72 were admitted by their FILE NAME rather than the line (#33, #35, #38#42, #65#68) — kept with their classes, over-included not laundered; R4's pass B is content-only. Every one is here. Classes — H historical quotation (the superseded architecture stated as past) · R ruled state (the current architecture, true) · RW about readonlyWhen, still insert-exempt · #7823 the internal: true write-response strip, a different strip at the protocol ingress by the 2026-08-13 ruling · D delegation prose (the ingress forwards whole and relays the engine verdict — the seam protocol.readonly-insert.test.ts pins) · P a pin that forbids the spelling · U an unrelated subject sharing the spelling · F a parser fixture string. Path prefixes abbreviated: lint/=packages/lint/src/, objectql/=packages/objectql/src/, mp/=packages/metadata-protocol/src/, rest/=packages/rest/src/, spec/=packages/spec/, dogfood/=packages/qa/dogfood/test/, docs/=content/docs/.

# site class says
1 docs/automation/hook-bodies.mdx:264 R + H the insert row states the in-engine strip;…
2 docs/kernel/contracts/data-engine.mdx:311 RW «only readonlyWhen is insert-exempt at this…
3 docs/kernel/contracts/data-engine.mdx:371 H «Until the maintainer ruling … this paragraph…
4 lint/validate-readonly-action-writes.test.ts:29 H «"INSERT is exempt" stopped being a true reason»
5 …/validate-readonly-action-writes.test.ts:289 H «This block used to be titled …»
6 …/validate-readonly-action-writes.test.ts:313 P `expect(reason).not.toMatch(/INSERT is exempt…
7 lint/validate-readonly-action-writes.ts:59 H «The premise this bullet used to carry»
8 …/validate-readonly-action-writes.ts:61 H names the overturned 2026-07-24 row
9 …/validate-readonly-action-writes.ts:64 R «INSERT is no longer engine-exempt from anyth…
10 …/validate-readonly-action-writes.ts:174 H «the reason was a one-liner - "INSERT is exem…
11 …/validate-readonly-action-writes.ts:179 P «Neither reason may be restated as "INSERT is…
12 lint/validate-readonly-flow-writes.test.ts:330 H «This case used to be justified by …»
13 lint/validate-readonly-flow-writes.ts:15 H «used to be that INSERT was engine-exempt»
14 …/validate-readonly-flow-writes.ts:16 H the #3043/#3413 quotation inside that sentence
15 lint/validate-readonly-hook-writes.test.ts:216 H «This block used to be titled "INSERT is engi…
16 …/validate-readonly-hook-writes.test.ts:217 H the quotation it rested on
17 lint/validate-readonly-hook-writes.ts:43 H «used to be that INSERT was engine-exempt»
18 …/validate-readonly-hook-writes.ts:44 H the quotation inside it
19 …/validate-readonly-hook-writes.ts:211 H «This docblock used to say the opposite — tha…
20 mp/protocol.readonly-insert.test.ts:13 H names the superseded row; the next clause sta…
21 mp/protocol.ts:10395 H «superseding the 2026-07-24 "INSERT (all call…
22 objectql/engine-hook-provenance-sibling-seams.test.ts:360 RW «INSERT is exempt from readonlyWhen entirely»
23 objectql/engine-insert-static-readonly-strip.test.ts:17 H «The create-side strip lived at the DataProto…
24 …/engine-insert-static-readonly-strip.test.ts:31 H the overturned row, verbatim 「同意」
25 objectql/engine.ts:9634 H «The 2026-07-24 row "INSERT (all callers) …»…
26 objectql/validation/rule-validator.ts:872 RW readonlyWhen predicate note: «INSERT is exe…
27 …/rule-validator.ts:1484 H «superseded the 2026-07-24 row»
28 …/rule-validator.ts:1486 H «the boundary copy that used to run at the Da…
29 …/rule-validator.ts:1509 H «NOT on the "INSERT is exempt" row that rulin…
30 spec/src/data/field-autonumber-readonly.test.ts:89 U summary is not in `RUNTIME_OWNED_FIELD_TYPE…
31 spec/src/stack.zod.ts:1427 U an app seeds sys_business_unit rows in `con…
32 docs/permissions/system-context.mdx:114 R census row 20: one gate over both create-side…
33 lint/validate-readonly-flow-writes.ts:17 H «with an ingress copy in metadata-protocol th…
34 …/validate-readonly-flow-writes.ts:20 R «the ingress copy is deleted»
35 mp/protocol.readonly-insert.test.ts:4 D the file pins the seam «from the ingress side»
36 …/protocol.readonly-insert.test.ts:6 H «#3043 put the strip at this DataProtocol ing…
37 …/protocol.readonly-insert.test.ts:7 H «INSERT-readonly-exempt (#3413)» — the contin…
38 …/protocol.readonly-insert.test.ts:15 R «the ingress copy is deleted»
39 …/protocol.readonly-insert.test.ts:23 D «the ENGINE's onFieldsDropped there, which…
40 …/protocol.readonly-insert.test.ts:55 D «what the ingress FORWARDS and what it …»
41 …/protocol.readonly-insert.test.ts:106 D `describe('#14147 — the create ingress DELEGA…
42 …/protocol.readonly-insert.test.ts:130 D «the ingress never pre-empts the isSystem gate»
43 mp/protocol.ts:10393 H «The ingress copy it replaces (`stripReadonly…
44 …/protocol.ts:10491 #7823 cloneData's 201-body internal-field strip
45 …/protocol.ts:11952 #7823 the partial-success face's per-outcome respon…
46 …/protocol.ts:12020 #7823 the bulk face's per-row response strip
47 …/protocol.write-response-internal-fields.tripwire.test.ts:9 #7823 «The internal: true write-response strip li…
48 …/protocol.write-response-internal-fields.tripwire.test.ts:244 #7823 the tripwire's describe title
49 objectql/engine-autonumber-runtime-owned.test.ts:550 H «at the DataProtocol create ingress when this…
50 …/engine-autonumber-runtime-owned.test.ts:552 H «which deleted that ingress copy»
51 …/engine-autonumber-runtime-owned.test.ts:561 H «the ingress copy did by rule, and the in-eng…
52 …/engine-autonumber-runtime-owned.test.ts:634 R title corrected this round: «stripped on a cr…
53 objectql/engine-insert-static-readonly-strip.test.ts:17 H same line as #23, matched by the second pass — the one duplicate (R4 correction)
54 objectql/engine-update-addressing-id-no-warn.test.ts:54 U the UPDATE-path {value,id} + where.id RES…
55 …/engine-update-addressing-id-no-warn.test.ts:177 U same subject, the repro title
56 objectql/engine-update-addressing-id-not-dropped.test.ts:158 U same subject
57 objectql/engine.ts:10521 #7823 «write-response strip lives at the GENERIC-DA…
58 objectql/internal-fields.test.ts:245 #7823 «201/200 bodies are stripped at the generic-d…
59 objectql/validation/rule-validator.test.ts:1357 H «This case used to say … "(that is the #3043
60 dogfood/authz-conformance.matrix.ts:281 H + R «#3043 at the ingress; in-engine for every ca…
61 …/authz-conformance.matrix.ts:282 R enforcement — rewritten this round (N4: two…
62 …/authz-conformance.matrix.ts:284 H + R note — «first enforced at the DATA-WRITE IN…
63 dogfood/showcase-static-readonly.dogfood.test.ts:24 H «#3043 first closed the INSERT face at the…
64 rest/import-runner-historical-readonly-insert.test.ts:15 H + R «WHERE that strip lives moved — from the Data…
65 …/import-runner-historical-readonly-insert.test.ts:23 H quotes the ruling's own test note («cannot se…
66 …/import-runner-historical-readonly-insert.test.ts:101 D the real createData is on the path so the s…
67 …/import-runner-historical-readonly-insert.test.ts:127 D describe title: the run goes through the REAL…
68 …/import-runner-historical-readonly-insert.test.ts:136 D where a refusal surfacing through that face w…
69 rest/rest-server.ts:12359 H rewritten this round: «When this was written…
70 rest/rest-write-response-internal-fields.tripwire.test.ts:330 #7823 the internal-field response strip on the batc…
71 spec/liveness/field.json:102 H + R the ledger note: «first (#3043) at the DataPr…
72 spec/scripts/liveness/evidence.test.ts:33 F a parser fixture string (the evidence-pointer…
73 spec/src/kernel/execution-context.zod.ts:392 R the preserveAudit describe, rewritten this…

Not in the sweep's scope by its own exclusions, stated so the reader does not re-find them: **/CHANGELOG.md (past tense, correct as history), content/docs/releases/** (release-owned, untouched — 0 files in this PR's diff against a 6-file content/docs/** control), and content/docs/references/** (regenerated this round; git grep -F 'at the DataProtocol ingress' -- content/docs/references answers 0 hits after gen:docs, 13 before). skills/objectstack-data/SKILL.md:274 («from an UPDATE payload») is the governed half and lands in #15382, as ruled — it matches none of the twelve spellings and is listed here only because the previous verdict named it.

Attribution: this patch round was produced in Claude Code session session_01ARYe3yQTQCUFm5qPYNgKaJ.

Patch round R2 — contract review 5548671173 (FAIL) → head 66c580b0c

Three commits appended on bd598e803 (⛔ no rebase, no amend, no force-push):
b5fb6d728 items 1–4 · da62e0e93 one more sweep-found test justification ·
66c580b0c census re-anchor plus a string-literal fix. ⛔ origin/main is NOT
merged this round, deliberately:
packages/objectql/src/engine.ts is also the
landing site of #15225 (p0, in flight); the p0 lands first and a separate merge
round follows it, so mergeable_state: dirty is expected and untouched here.

# Verdict item Disposition
1 Superseded premise stated as live — (a) hook-bodies.mdx:255 / :264, (b) engine.ts:9626-9631, (c) validate-readonly-flow-writes.ts:14-18, (d) validate-readonly-hook-writes.ts:209-212, (e) the two GREEN blocks Done. All five re-measured exactly as the verdict read them, all corrected with validate-readonly-hook-writes.ts:39-52 as the template. (a): the insert row now reads "Silently dropped — unless the hook declares runAs: 'system'", the "one channel … plus INSERT" thesis is gone, the "On UPDATE" framing says every non-system write, and the rule-scope sentence names insert/create as the #15394 scan gap. (b): the standing note now says two strips share insertDropped and only readonlyWhen stays insert-exempt. (e): verdicts unchanged, justifications now "conditional lock has no prior record" + "static half is a scan gap, #15394", the way the action test was done. ⛔ No test deleted or skipped. Sweep beyond the fivegit grep -iE over the premise's spellings (INSERT is exempt, engine-exempt, legitimately seed, exempt from the readonly strip, INSERT (all callers), plus INSERT, absent BY DECISION, insert-exempt at this seam, at the DataProtocol ingress) and git grep stripReadonlyForInsert, both excluding CHANGELOGs and content/docs/releases/: the same statement was live in eight more places, all corrected, all prose, no behaviour: validate-flow-node-writes.test.ts:409 (a third GREEN title of class (e)); content/docs/kernel/contracts/data-engine.mdx:306, :311, :369-386 (a published page whose strip table said update only, whose insert paragraph said "insert-exempt at this seam by design", and whose Callout said the ingress enforces its own policy); content/docs/data-modeling/fields.mdx:319 ("on INSERT … at the DataProtocol ingress"); packages/spec/src/contracts/data-engine.ts:139-160 (the strictReadonlyWrites TSDoc — a second cross-lane docblock-only touch on domain:spec, same shape as the accepted B1; check:generated confirms no artefact moved); packages/spec/liveness/field.json readonly (its evidence pointer named the deleted ingress strip — check:liveness is green on the new pointer); packages/qa/dogfood/test/authz-conformance.matrix.ts readonly-static-write (enforcement and note said "enforced at the ingress, not the engine"; the row's issue-id multiset is byte-identical for the prose-id ratchet); rule-validator.test.ts:1354 ("insert keeps its #3413 exemption"); and the two test headers named in A2.2 below. Every remaining hit is a historical quotation or is about readonlyWhenwithdrawn in R3: false at 66c580b0c (eleven live sites); R3 item 4 carries the list.
2 Five census cross-references Done, each checked by eye — against the renumbered table, and against the pre-renumbering table at merge-base a06faebbe, where "row 50's doors" sat under row 51 (the manage_metadata doors, now 49), row 30 was the afterDelete revocation skip (now 29), row 22 strict-drop (now 21), row 34 revoke() (now 33). :161 50→49 · :237 30→29 · :246 22→21 · :250 34→33 · :422 30→29. No other prose reference sits above the deleted row (row 2 twice, and rows 1–60 / 61–64, were already right). ⚠️ check:system-context-census does not read prose references: its green (OK — 105 elevation read sites in 19 packages across 44 files; --fix re-anchored 9 lines that moved when the engine.ts note grew and is a fixed point after that) says nothing about these five. The eye check is the only evidence, and it is stated here as such.
3 batchData's upsert-create arms forward no onFieldsDropped Done. Both arms in runBatchDataLoop — the id-less row, and the id-named row whose probeRecord answers null — now build the same options object as case 'create' and hang mergeDroppedFieldEvents(rowDropped) on the row result; the arg-form insertCtx is retired. Pinned in protocol.readonly-insert.test.ts, one case per arm (the id-named case also asserts findOne was consulted exactly once, so it cannot go green through the update arm five lines up); the firing control now enumerates six engine.insert calls and is retitled "every create face whose response carries droppedFields passes an onFieldsDropped listener". cloneData: the changeset sentence is narrowed, not the code widened — and the absence is pinned. CloneDataResponseSchema (#11924, declared AS PRODUCED by maintainer ruling) is exactly { object, id, sourceId, record }, and search-clone-schema-conformance.test.ts asserts in so many words that the body carries no droppedFields; wiring a listener there means a new response key on a @objectstack/spec surface plus two conformance pins — a spec change with its own card — filed unassigned as #15703 — not a patch-round delegation detail. Code and changeset agree: the clone forwards whole, the engine strips and logs warn, nothing is reported on the wire; a new pin holds cloneData to passing no listener with that reason.
4 Two false changeset sentences Done (.changeset/tidy-cups-smile.md, levels / BREAKING banner / ADR-0087 marker untouched — check:adr-0087-registration --base origin/main 0, check:changeset-no-major 0): the metadata-protocol bullet names which faces report droppedFields and why cloneData does not; the lint bullet is true of all three rules; the spec bullet now also names the contract docblock and the ledger verdict.

Zone 2 readings falsified: none — all four re-measured exactly as the reviewer stated them. What was falsified is one of this PR's own earlier claims (A2.2's "zero references remain"), corrected in place below.

Patch-round measurements — all at 66c580b0c, every build/test through scripts/pm/os-verify-lock.sh, every exit code captured before any pipe

run result
pnpm --filter @objectstack/lint test 94 files, 2909 passed, 0 failed
pnpm --filter @objectstack/metadata-protocol typecheck · test tsc clean · 161 files (+2 skipped), 2374 passed (+10 skipped), 0 failed — +3 over bd598e803: the two upsert-create cases and the clone pin
pnpm --filter @objectstack/objectql typecheck (runs check:test-typecheck too) OK
objectql targeted vitest: engine-autonumber-runtime-owned, validation/rule-validator, engine-insert-static-readonly-strip 3 files, 236 passed
@objectstack/dogfood test/authz-conformance.test.ts 47 passed (its first run was red on my own apostrophe inside the matrix's single-quoted string — fixed in 66c580b0c)
pnpm --filter @objectstack/spec check:liveness · check:generated 0 · 0 — "All 15 generated artifacts are up to date"

Gate union, derived from a detached worktree at fresh origin/main (fa125f3bf — ⛔ not the ~183-commit-stale primary checkout) with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack over the 38 paths of git diff --name-only a06faebbe...HEAD: the tool's own Reconciliation — 108 families. Executed with the header's capture idiom (each printed command recorded as it ran) and reconciled with --ran: "108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN" — that line counts execution, not passing, so the per-exit table follows. The union ran on the final tree; the six --base origin/main gates were then re-run strictly after the last commit (all 0).

exit families
0 104 of 108 — among them check:system-context-census (+ self-test), check:type-check-coverage, check:type-check-debt (exit 3 until driver-turso was built, then re-measured: 13 ledger entries, 143 raw tsc errors, none above its recorded number), check:doc-authoring, check:engine-double-contract, check:corpus-claim-drift, check:docs-audit-scope, check:skill-examples (exit 1 until client-react was built, then 257 examples type-check), check:nul-bytes, check:liveness, check:api-surface, check:docs, check:adr-0087-registration, check:changeset-no-major, check:empty-changeset, check:objectui-changeset, and the rest of the derived list
1 → 0 scripts/docs-audit/check-affected-docs.mjs — its self-test went red once, in a chunk that ran concurrently with check:skill-examples (which leaves the gitignored packages/spec/.examples-build/** that the walk admits as kind=contract sources — the #15328 root cause the previous round already posted); exit 0 on re-run with that artefact absent
MODULE_NOT_FOUND — NOT MEASURED check-platform-object-tenancy-census.mjs (+ self-test), check-registry-log-declared.mjs (+ self-test): both landed on main on 2026-09-04 and do not exist on this branch (no merge this round); they run on CI's merge ref and at the deferred merge round
3 → 0 check:dual-build-cjs-loads — PREREQUISITE NOT MET (unbuilt dist/) until every packages/* was built under the lock exactly as lint.yml does (turbo, 71 tasks, all cached); then 0: entries/packages/cjsFiles/probes 103/66/619/1 against floors 90/58/520/1

⚠️ Stated plainly: the only gate the verdict named for item 2 is green and reads none of the prose references item 2 fixes. Attribution: this patch round was produced in Claude Code session session_01ARYe3yQTQCUFm5qPYNgKaJ.

What the change is

engine.insert gains a static-readonly pass beside the runtime-owned one it
already had, inside the same if (!opCtx.context?.isSystem) branch, calling the
SAME stripReadonlyFields that engine.update calls, and reporting through the
same channels: readonlyStripWarning at warn, onFieldsDropped under reason
readonly, strictReadonlyWrites refusing with ERR_READONLY_FIELD_REJECTED
before any driver dispatch. The boundary copy is deleted, not kept as a
second implementation.

Three consequences the card asked for, all discharged:

  1. a non-system caller reaching engine.insert directly no longer writes the
    read-only column;
  2. create_record's onFieldsDropped wiring starts receiving readonly drops —
    driven end to end, evidence below;
  3. assertReferencesResolve's doc sentence is true again.

A2.2 — every stripReadonlyForInsert call site, enumerated before deleting

The definition was at packages/metadata-protocol/src/protocol.ts:1794. It had
five call sites, all in that one file — the card's "at least six" counts the
definition and/or the prose references:

# site method what replaces it
1 protocol.ts:10526 createData payload forwarded whole; the engine's onFieldsDropped (already wired here) carries the drop
2 protocol.ts:10623 cloneData payload forwarded whole; overrides are still applied BEFORE the insert, so a smuggled readonly key is still judged
3 protocol.ts:11746 batchData (create row) now builds an options object and forwards onFieldsDropped per row (it forwarded none before)
4 protocol.ts:12004 createManyData payload forwarded whole; the aggregated listener already existed
5 protocol.ts:12062 insertManyData payload forwarded whole; per-row precision recovered from the batch union by asking which row SUPPLIED each name

Three further mentions in the same file were prose (:1902, :12069) or the
definition itself. Deleted with it: warnPreserveAuditIgnoredOnInsert (moved
into the engine as preserveAuditIgnoredOnInsertWarning) and
diffDroppedFields, which existed only to reconstruct the ingress strip from a
before/after payload diff and is now dead.

Command and output:

$ git grep -n "stripReadonlyForInsert" -- packages | grep -v CHANGELOG
packages/metadata-protocol/src/protocol.ts:1794:function stripReadonlyForInsert(schema: any, data: any, context: any): any {
packages/metadata-protocol/src/protocol.ts:1902: * (`stripReadonlyForInsert`, #3043) — BEFORE the engine — so the engine listener
packages/metadata-protocol/src/protocol.ts:10526:        const data = stripReadonlyForInsert(
packages/metadata-protocol/src/protocol.ts:10623:        const insertData = stripReadonlyForInsert(schema, data, ctx);
packages/metadata-protocol/src/protocol.ts:11746:  const stripped = stripReadonlyForInsert(batchSchema, record.data || record, context);
packages/metadata-protocol/src/protocol.ts:12004:        const rows = stripReadonlyForInsert(
packages/metadata-protocol/src/protocol.ts:12062:        const rows = stripReadonlyForInsert(
... plus test/doc/CHANGELOG prose in objectql, rest, spec, qa/dogfood, content/docs, docs/qa

At bd598e803 this paragraph claimed that every non-CHANGELOG prose reference
outside protocol.ts had been corrected and that zero references to the deleted
symbol remained except two naming it as deleted. That was false. The patch
round's sweep (see Patch round above) found the superseded premise still stated
as live in the sites the verdict lists and in eight more, and two test headers
(engine-autonumber-runtime-owned.test.ts, showcase-static-readonly.dogfood.test.ts)
still citing the deleted function as the live INSERT strip. All are corrected at
66c580b0c; every remaining mention of stripReadonlyForInsert outside CHANGELOGs
names it as deleted or as history.

Zone 2, item by item

A2.1 — anchors: CONFIRMED. engine.insert at engine.ts:9724;
stripReadonlyFields imported at :196; the doc sentence at :5939-5940
("like every other write-path guard in this engine (stripReadonlyFields,
stripReadonlyForInsert)"); update-path call sites at :11389 and :11569;
the lint premise at validate-readonly-action-writes.ts:57 and :154. All five
located by symbol and all five matched. One addition the table did not have: a
SECOND false sentence in assertReferencesResolve at :5983-5984 ("and the
create ingress does the same"), corrected too.

A2.2 — CONFIRMED with a correction: five call sites, not six; the definition
is at protocol.ts:1794 (the PM's grep surfaced only CHANGELOGs and a test, as
warned). Table above.

A2.3 — the lint GREEN control: PARTLY FALSIFIED, and this is the one item that
needs a reviewer's eye.
The premise was dropped and the scan gap's stated
reason replaced, but the control case was NOT flipped to a finding, because
measurement says a finding there would be false:

  • an action body's ctx.api is ql.createContext(buildActionExecutionContext(ec))
    and that is { ...ec, isSystem: true } — the rule's own header measures this
    and packages/objectql/src/engine-repo-execute-elevation.test.ts pins it. So
    the new create-side static strip, which runs under if (!opCtx.context?.isSystem),
    is skipped on the action surface for exactly the reason it is skipped there on
    update;
  • readonlyWhen still has no create-side strip at all (engine.ts:11515:
    "INSERT stays exempt"), and that rule reports only the conditional shape.

So on the ACTION surface an elevated insert still keeps both values, and
flagging it would tell an author their write never lands when it does — the
failure the file was written to avoid. What landed instead is a reasoned
refusal, pinned
: the silence is now exported as data
(READONLY_ACTION_INSERT_SILENCE, two named reasons), and the test asserts that
neither reason may ever be spelled "INSERT is exempt" / "exempt from both
strips" again, plus that one of them still names the surviving engine fact. That
is the coverage the ruling wanted (the superseded premise can no longer hide
inside a green case) without encoding a falsehood.

⚠️ The genuinely-widenable gap is on the two SIBLINGS the ruling did not name —
validate-readonly-flow-writes.ts (create_record) and
validate-readonly-hook-writes.ts (ctx.api.insert) — which run NON-elevated
and where a create of a readonly column IS now a silent no-op. At bd598e803 only the hook rule's header (validate-readonly-hook-writes.ts:39-52)
had been corrected — the flow rule's header (:14-18) and the hook rule's
STRIP_SUBJECT_METHODS docblock (:209-212) still carried the premise, and this
paragraph claimed otherwise. The patch round corrects both, plus the two GREEN test
blocks and a third in validate-flow-node-writes.test.ts; the same correction is
posted on #15394. Widening their scan sets adds a new error-severity build finding,
so that half stays filed rather than ridden in: #15394.
If the reviewer reads the ruling as requiring the flip on the action surface
anyway, say so and it goes in — this is a measurement, not a preference.

A2.4 — create_record DOES fire now: CONFIRMED end to end. Not a unit test
on the strip — a real ObjectQL over a recording driver, registered as the
data service of a real AutomationEngine, running a real flow
(packages/services/service-automation/src/builtin/create-record-readonly-drop.test.ts):

✓ the column does not land, and the run says so — a node warning naming the field
✓ runAs:system still seeds it, with no drop and no warning — the intended channel
  Test Files  1 passed (1)
       Tests  2 passed (2)

The first case asserts creates[0] reaches the driver without completed_at,
and that listRuns('seed')[0].steps.find(nodeId === 'mk') carries
status: 'success' with exactly one warning containing
create_record(duly_task) and completed_at. The second asserts a
runAs: 'system' flow still seeds the column and produces no warning.

A2.5 — gate families: below, per family, with exit codes.

The three narrowings carried across, each argued

The deleted copy was not a plain subset of the engine strip. Three of its scope
rules are preserved deliberately, and all three are OUTSIDE what ruling C
superseded — a reviewer disagreeing with any of them is disagreeing with this
PR, not with the ruling:

  1. runtime-owned types stay with their own pass. engine.insert already
    strips autonumber via stripRuntimeOwnedFields under the WIDER
    preserveAudit whitelist a historical import needs. The new static pass runs
    over staticReadonlyInsertSubject(schema), a view with those types removed,
    so the second pass cannot delete what the first legitimately kept — and the
    log line keeps stating the runtime-owned reason, which is the true one for an
    autonumber (the spec injects readonly: true onto every one, so
    stripReadonlyFields would call it an author-declared lock).
  2. preserveAudit is NOT forwarded on the create path. The 2026-08-08
    ruling narrowed that exemption to UPDATE and left isSystem as the create
    side's only one. Honouring it here — which reusing the update call shape
    verbatim would have done — would hand a non-system treatAsHistorical import
    the ability to seed the approval/status columns the strip protects. Ruling C
    moved WHERE the strip runs, not WHAT exempts it. The loud line moved with it
    (preserveAuditIgnoredOnInsertWarning).
  3. platform objects (managedBy, the sys_ namespace) keep their carve-out.
    ADR-0086 / 安全:owner_id(属主锚点)客户端可写、服务端无守卫 → 非属主可伪造/转移记录属主 #3004: those columns have their own 403 guards, and a silent strip
    must not swallow the payload the guard exists to reject. That boundary was
    ruled on its own merits, never as part of the "INSERT is exempt" row. It is
    also what keeps the metadata repository's sys_metadata_history.recorded_by
    write working — a direct, non-system engine.insert caller.

Two behaviours the move changed on purpose, both stated in the changeset:

  • the strip now runs AFTER the beforeInsert hooks, so it inherits the engine's
    guards: a hook's own stamp is not caller-supplied, and a key a hook ASSIGNED
    is the hook's write even when the caller echoed the same value. The ingress
    copy ran before the hooks and could judge neither;
  • a stripped key's defaultValue is re-derived, so a forged
    approval_status still becomes draft rather than NULL — the ingress copy
    got that for free by running before applyFieldDefaults; running after the
    hooks means asking for it explicitly.

The warn line is also now verb-aware: on a create it says "the create is being
COMMITTED WITHOUT IT", names beforeInsert, and drops the preserveAudit
remedy
, which cannot work there. Offering a remedy that would not have worked
is the defect already removed once from that message.

⚠️ Declared cross-lane touches — every packages/spec file in this diff

Extended in patch round R3 per its item 1 (the original field.zod.ts paragraph
follows the table). Every entry is docblock / .describe() / ledger text — the
text face under lanes/spec.md (「changeset 恒 patch」, 「产物随源走」): no
exported symbol, authorable key or accepted value moves, proven rather than
asserted by check:api-surface at 485a2d525 and again at a73dd85db («public API surface + factory
signatures unchanged»). @objectstack/spec stays patch. Disposition follows the
accepted B1 shape: declared here, flagged for the spec lane, which may revert or take
any of them.

file what round
packages/spec/src/data/field.zod.ts RUNTIME_OWNED_FIELD_TYPES docblock sentence — names staticReadonlyInsertSubject original
packages/spec/src/contracts/data-engine.ts strictReadonlyWrites TSDoc, the INSERT paragraph R2
packages/spec/liveness/field.json the readonly row's evidence pointer and note R2
packages/spec/src/kernel/execution-context.zod.ts preserveAudit .describe(), the INSERT sentence R3
packages/spec/src/api/protocol.zod.ts CreateDataResponseSchema.droppedFields and CreateManyDataResponseSchema.droppedFields .describe(); CloneDataResponseSchema TSDoc R3
packages/spec/src/api/batch.zod.ts BatchOperationResultSchema.droppedFields .describe() R3
packages/spec/src/security/public-form.ts TSDoc on the exported PUBLIC_FORM_SERVER_MANAGED_FIELDS — the anonymous-surface rationale sentence and the authenticated-write example (ships in dist/security/index.d.ts) R4
content/docs/references/{api/protocol,api/batch,data/data-engine,kernel/execution-context}.mdx the regenerated projections of the rows above (os-regen, gen:docs) R3

The original field.zod.ts paragraph

This diff edits one file on domain:spec's single-owner surface, and the
changeset releases that package ('@objectstack/spec': patch), so it is called
out here rather than left as a diff line.

  • What: one docblock sentence on RUNTIME_OWNED_FIELD_TYPES. It described
    the DataProtocol create ingress as deferring to the engine's runtime-owned
    strips "rather than pre-empting them with its own narrower exemption set
    (stripReadonlyForInsert)". It now names staticReadonlyInsertSubject, which
    is where that exclusion lives after this PR.
  • Scope: prose only. No exported symbol, no accepted key or value, no
    schema, no behaviour. The patch level follows from that under the
    bump-level rule ("a fix( that changes no public surface stays patch").
  • Why not leave it: the sentence names a function this PR deletes. Leaving
    it would ship a comment my own diff falsifies, on the file that is the
    protocol's statement of runtime-ownership — the one place a reader goes to
    learn which strip owns which field type.
  • Disposition: flagged for the spec lane rather than absorbed. Reverting it
    is a one-line change if that lane would rather correct it themselves.

examples/app-todo — what this PR broke, and which of the two answers it took

test/task-recurrence.test.ts went red on 484cec193: 5 failed / 101 passed.
The card's own consequence (1) is the cause — a non-system caller reaching
engine.insert no longer writes a read-only column — and the suite asserted the
old contract. Two independent causes, not one:

Both answers are fixture/harness changes; the example's behaviour is
unchanged and the engine change is untouched.
⛔ Nothing skipped, disabled or
quarantined, and no assertion deleted — the diff adds coverage:

  1. bootTodoKernel now binds the app's own taskHook, which its own docblock
    already claimed it did. Both completion cases therefore travel the app's real
    user path instead of around it.
  2. The stale completed_date CREATE-seed is dropped from those two cases.
  3. The already-completed fixture seeds under context.isSystem — the remedy
    this PR's changeset names, and the answer the ruling gives for seeding a
    server-owned column at create time.
  4. Prose: the recurrence rationale is rewritten, and the sibling's examples/app-todo: a normal user can never mark a task complete — completed_date is readonly (stripped on update) and completed_date_required then refuses the write, so the app's own completeTask action always fails #7036
    measurement table keeps its historical fourth row (insert already-completed (user ctx): OK) with a note that it is dated evidence and no longer a live
    escape. CHANGELOG.md is left alone — past tense, correct as history.

pnpm --filter @objectstack/example-todo test4 files, 106 tests, 0 failed.

Composition with #15363, which landed on main mid-flight

65846bc46 (#15363) also edits packages/metadata-protocol/src/protocol.ts. A
clean text merge is not evidence that two changes compose, so this was read
rather than assumed. They compose, and the argument is structural:

  • its change is confined to toRowApiError and a new
    isEngineDuplicateRecordEnvelope helper (post-merge lines 1879–1925) — the
    failure arm, mapping a caught DuplicateRecordError to the
    UNIQUE_VIOLATION wire spelling on a failed row;
  • mine is in the success arm of the same per-row try (case 'create':),
    where the engine's onFieldsDropped populates droppedFields on a row that
    wrote. One row cannot be in both arms, and neither reads state the other
    writes.

The one way they could have met is if this PR made a create THROW where it did
not before. It does, in exactly one shape — strictReadonlyWrites — and no
protocol create face passes it:

$ grep -c strictReadonlyWrites packages/metadata-protocol/src/protocol.ts
0
$ grep -c strictReadonlyWrites packages/objectql/src/engine.ts      # control: the grep fires
16

Even had one, ReadonlyFieldRejectedError fails their two-part gate
(code === 'DUPLICATE_RECORD' && name === 'DuplicateRecordError'). Measured, not
inferred: @objectstack/metadata-protocol and @objectstack/rest are both green
on the merged head, including that PR's own new row pins.

Changeset derivation

.changeset/tidy-cups-smile.md, re-derived against the diff rather than recalled,
under the bump-level rule that landed mid-flight (b337a1308, #15380: "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"):

package level the act
@objectstack/objectql minor engine.insert does something new with a caller-supplied readonly field — published behaviour
@objectstack/metadata-protocol minor five create faces stop stripping and start delegating — published behaviour
@objectstack/service-automation patch prose only in crud-nodes.ts; the onFieldsDropped channel is unchanged, only its traffic is new
@objectstack/lint patch prose, plus one module-local constant its index.ts deliberately does not re-export — so no public surface widens
@objectstack/spec patch one docblock sentence (see the cross-lane section above)

examples/app-todo is private: true, so it releases nothing and takes no entry;
packages/rest's only change is a test file.

major stays refused during the launch window, so breaking-ness is carried by the
BREAKING banner plus the ADR-0087 disposition:

<!-- adr-0087: not-required (no-migration-prescription) … -->

which answers both questions rather than one — the BREAKING is a write-path
behaviour change (no spec property, metadata key, accepted value or exported
symbol disappears; nothing reaches objectstack migrate meta, spec-changes.json
or the upgrade guide; the remedy is application code, not a metadata migration),
and separately disposes of the retirement candidate on the measurement
(stripReadonlyForInsert was a bare module-private function, absent from that
package's index.ts, which its exports map makes the only path in).
node scripts/check-adr-0087-registration.mjs exits 0.

Tests

(The numbers in this section are from 67d6144c5, before the merge of main and
the patch round; the patch round's own measurements at 66c580b0c are in the
Patch round section at the top.)

Union run after the final commit, at 67d6144c5. ⚠️ The suite list is now
derived from turbo ls --affected without dropping examples/** and
qa/**
— that narrowing is what let the example-todo regression above reach
CI instead of this worktree, and the contract review named it. Every row below is
a number; ⛔ nothing is reported as "not reached".

package files tests failed
@objectstack/objectql 271 4651 0
@objectstack/metadata-protocol 161 (+2 skipped) 2371 (+10 skipped) 0
@objectstack/lint 94 2906 0
@objectstack/rest 178 3023 0
@objectstack/service-automation 106 1244 0
@objectstack/runtime 224 3208 0
@objectstack/core 49 1189 0
@objectstack/spec 471 12634 0
@objectstack/metadata 46 705 0
@objectstack/metadata-core 15 261 0
@objectstack/platform-objects 33 518 0
@objectstack/driver-memory 40 1036 0
@objectstack/plugin-security 96 1816 0
@objectstack/plugin-auth 94 1986 0
@objectstack/plugin-approvals 36 662 0
@objectstack/plugin-audit 22 323 0
@objectstack/plugin-sharing 33 811 0
@objectstack/dogfood 131 (+1 skipped) 1019 (+3 skipped) 0
@objectstack/downstream-contract 2 19 0
@objectstack/example-todo 4 106 0
@objectstack/example-showcase 27 375 0
@objectstack/example-crm 5 45 0
@objectstack/example-embed-objectql 1 2 0

@objectstack/example-multi-package and @objectstack/refd-timer-testkit declare
no test script — stated because a --filter that matches no script exits 0
having run nothing
, which reads exactly like a pass. The remaining affected
packages (drivers other than memory, the remaining services/triggers/connectors,
cli, client*, console) were left to CI, which runs the farm exactly once;
that is a declared narrowing, not a silent one.

Every heavy run went through bash scripts/pm/os-verify-lock.sh -c '…'.

New pins:

  • packages/objectql/src/engine-insert-static-readonly-strip.test.ts — 16 cases
    against a real ObjectQL: the card's exact repro inverted (no context,
    explicit isSystem: false), onFieldsDropped, the warn line's three
    create-shaped claims, defaultValue re-derivation, the three exemptions
    (isSystem, a beforeInsert stamp, a hook stamp the caller echoed, platform
    objects), the neighbouring rules (preserveAudit refused-and-warned on create
    but still reinstating an autonumber; readonlyWhen still INSERT-exempt),
    strictReadonlyWrites refusing with zero driver creates and a deliberately
    silent listener, and the batch path judged per row.
  • packages/services/service-automation/src/builtin/create-record-readonly-drop.test.ts
    — A2.4, above.

Fixtures triaged rather than mass-edited (each of the four kinds appeared):

  • packages/metadata-protocol/src/protocol.readonly-insert.test.tsreplaced
    entirely.
    It pinned the deleted branch through a mock engine, so under the
    new architecture it could only ever re-measure a mock. It now pins DELEGATION
    on all five create faces (payload forwarded whole, engine verdict surfaced)
    plus a firing control that every face passes a listener at all. The
    enforcement is pinned where it now runs.
  • packages/metadata-protocol/src/protocol.dropped-fields{,.bulk}.test.ts — the
    create-side stand-ins now play the engine's part (strip + report), which is the
    shape the update-side stand-ins in the same files always had.
  • packages/rest/src/import-runner-historical-readonly-insert.test.ts — mock
    engine swapped for a real ObjectQL. A mock cannot strip, so the old
    harness would have reported the historical column landing on a create and
    called it green — the same blind spot its own header was written against.
  • packages/objectql/src/engine-lookup-referential-integrity.test.ts — the
    readonly-lookup narrowing split into its two halves: the platform-object case
    keeps its value and reaches the check (fixture renamed sys_-prefixed, as the
    real sys_metadata_history is — now load-bearing, not cosmetic), and a new
    author-object sibling pins that the same value is STRIPPED before the check
    ever sees it. They fail differently: a lost narrowing REJECTS a platform write,
    a lost strip ACCEPTS a forged one.
  • examples/app-todo/test/task-recurrence.test.ts — see its own section above.

Gates — per family, exit codes, never an aggregate

73 families derived from the ACTUAL change set at the merged head with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, each run
as its own command with its exit code captured before any pipe. 70 exit 0.
Named: check:api-surface 0 · check:authorable-surface 0 ·
check:browser-reachable-entries 0 · check:changeset-gate-self-tests 0 ·
check:corpus-claim-drift 0 · check:cross-package-test-inputs 0 ·
check:dispatcher-error-vocabulary 0 · check:doc-anchors 0 ·
check:doc-authoring 0 · check:doc-security-posture 0 ·
check:docs-audit-scope 0 · check:docs-redirects 0 · check:docs-single-h1 0 ·
check:dual-build-cjs-loads 0 · check:dual-source-exports 0 ·
check:durability-log-level 0 · check:empty-state 0 ·
check:engine-double-contract 0 · check:entry-nameability 0 ·
check:error-code-casing 0 · check:error-code-provenance 0 ·
check:error-status-conformance 0 · check:exported-any 0 ·
check:filter-alias-parity 0 · check:liveness 0 · check:llms-txt 0 ·
check:nul-bytes 0 · check:partof-closing-keyword 0 ·
check:pm-skill-ratchet 0 · check:published-files 0 ·
check:published-readme-exports 0 · check:skill-examples 0 ·
check:spec-changes 0 · check:strictness-ledger 0 ·
check:test-source-alias 0 · check:type-source-resolution 0 (plus the
remainder of the derived list).

Convention-scoped, invisible to the deriver by construction, asked separately
and run:

family exit note
check:system-context-census 0 RED first, exactly as the co-tenancy note predicted. --fix repaired the line-anchor rot but not the rest: deleting stripReadonlyForInsert removed metadata-protocol's ONLY elevation read, so row 21 of the page cited a line that is no longer a read site and ten declared counts drifted. Detail below.
check:type-check-coverage 0 ratchet
check:type-check-debt 0 ratchet

NOT MEASURED (exit 3 or a prerequisite refusal) — reported as such, never as a pass:

family exit why, and what was done
check:dual-build-cjs-loads 3 → 0 PREREQUISITE NOT MET on seven unbuilt packages; built them and re-ran to 0
check:skill-examples 1 → 0 needed packages/client-react built; built and re-ran to 0
check:published-readme-exports 1 → 0 same class; built and re-ran to 0
check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any, check:browser-reachable-entries 1 → 0 all refused on a stale packages/spec/dist; built spec and re-ran to 0
check:react-declaration-parity 1, still NOT MEASURED "MANIFEST is not set — there is no registry side to compare against. This gate did NOT run." Needs an objectui checkout plus a browser dump; out of reach here and untouched by this diff
check:docs-audit-scope 1 → 0 NOT this diff: the walk admits gitignored packages/spec/.examples-build/** artefacts left by check:skill-examples as kind=contract route sources. Removing that directory greens it. Root cause measured and posted on the existing card #15328 rather than filed again

The system-context census, in detail

content/docs/permissions/system-context.mdx is the one artefact this PR and
#15319 both move, so it is spelled out. node scripts/check-system-context-census.mjs --fix
was run first and repaired 22 line anchors (11 +/-). It could not repair the
rest, and its own message says why — it fixes "pure line rot" only:

::error::[anchor-is-not-a-read-site] the page anchors packages/metadata-protocol/src/protocol.ts:1795,
  which the census does not call an elevation read and NON_READ_ANCHORS does not declare.
::error::[declared-count] `headline-sites` says 106, the census says 105
... 9 more declared-count drifts

That is a real, ruled consequence: row 21 ("readonly strip bypassed — INSERT
(protocol ingress)", metadata-protocol) cited the if (context?.isSystem) line
of the function this PR deletes, and it was metadata-protocol's ONLY elevation
read — so the package leaves the census. Row 20 already covers "INSERT (engine
pass)" at the very branch the new static strip lives under, so the behaviour is
not lost, it is folded. The page was therefore edited: row 21 deleted, rows
22–65 renumbered, row 20's description widened to say it now gates both
create-side passes, and the ten declared counts brought to the measured census
(106→105 sites, 20→19 packages, 112→111 reads, 102→101 behaviour-bearing,
45→44 files, "rows 1–61"→"rows 1–60", "rows 62–65"→"rows 61–64").

⚠️ Declared deviation: the dispatch said to repair this file with --fix
"and nothing else" and never to hand-edit it. --fix cannot express a census
that legitimately SHRANK; the counts are prose the gate reads back, and the gate
itself demands they be brought to the measurement ("it is quoted as a live count,
so it must stay one"). The edit is mechanical and gate-verified — final state
OK — 105 elevation read sites in 19 packages across 44 files, all anchored; 139 anchors resolve, 27 declared non-read — but it is a hand edit on the file this
lane is fencing, so it is flagged here rather than buried. Nothing near
engine.ts:5298 was touched; origin/main was merged twice (dc46c4ec1, then
638ea042d) and the ratchet families were re-run after each.

Two ratchets also recorded burn-down, both mechanical remedies the gates
prescribe by name: scripts/doc-authoring-prose-id.baseline.json (three
protocol.ts prose ids the deletion removed) and
scripts/engine-double-contract.pinned.json (the rewritten
protocol.readonly-insert.test.ts fake, whose findOne now routes through
assertEngineFindOnePredicate). New prose in this diff carries no issue ids, per
the maintainer ruling of 2026-08-12 the prose-id ratchet enforces.

Out of scope, filed not fixed

… strip into engine.insert

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…nly strip and the ingress delegation

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…latform and author halves

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…e; pin the reasoned refusal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…prose; changeset for the create-side move

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ue of a create

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…, doc-authoring baseline burn-down

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
… no longer has

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
… not by an issue id

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

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 8 package(s): @objectstack/lint, @objectstack/mcp, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/rest, @objectstack/service-automation, @objectstack/service-settings, @objectstack/spec, touching 37 documentable anchor(s). ⚠️ 8 changed file(s) yielded no anchor (packages/lint/src/validate-readonly-flow-writes.ts, packages/lint/src/validate-readonly-hook-writes.ts, packages/mcp/src/stdio-data-bridge.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

35 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 b2375921b1a7e68a78eece9964fead59157ada24.

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

What this run could not see
  • 8 changed file(s) yielded no anchor (packages/lint/src/validate-readonly-flow-writes.ts, packages/lint/src/validate-readonly-hook-writes.ts, packages/mcp/src/stdio-data-bridge.ts, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 66 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 138 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 b2375921b1a7e68a78eece9964fead59157ada24packageMentionDocs.

Which tree this was computed on

This run read content/docs from 26c3ba18b15bd441905889e2fb66a8ce702924c7 — the merge of head 2f0262ca14e2dcccebda6ba0a9abba68e911f69d into base b2375921b1a7e68a78eece9964fead59157ada24, 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 26c3ba18b15bd441905889e2fb66a8ce702924c7 && git checkout 26c3ba18b15bd441905889e2fb66a8ce702924c7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b2375921b1a7e68a78eece9964fead59157ada24 2f0262ca14e2dcccebda6ba0a9abba68e911f69d && git checkout -B drift-repro b2375921b1a7e68a78eece9964fead59157ada24 && git merge --no-ff 2f0262ca14e2dcccebda6ba0a9abba68e911f69d

node scripts/docs-audit/affected-docs.mjs --json b2375921b1a7e68a78eece9964fead59157ada24

⚠️ 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 b2375921b1a7e68a78eece9964fead59157ada24 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERFAIL (patch round) on 484cec193. Verdict and the owed round are on the card: #14147 (director seat, summon #14).

Short form: Test Core (4/6) is red on this head in @objectstack/example-todo (test/task-recurrence.test.ts, 5 × Completed date is required when status is Completed). examples/app-todo seeds the readonly: true completed_date on a non-system create, which is the superseded #3425 premise; the new in-engine strip removes it, as ruled. Migrate the example (system-context seeds or the hook path; rewrite the two prose rationales), re-run the affected set including examples/** and qa/** with @objectstack/runtime reported, and get all six shards green before re-parking. Everything else in the diff passed at tier and is recorded on the card so the next round does not re-argue it.

Stays draft, needs:contract-review stays on.


Generated by Claude Code

… and drop the stale completed_date create-seed

The harness claimed to boot the same stack as task-completion-trigger.test.ts
while binding no hooks, so task.hook.ts's beforeUpdate completion stamp never
ran in this file -- which is why it still carried a completed_date CREATE-seed
its sibling deleted when that stamp shipped. The seed was also a non-system
caller writing a readonly, server-owned column on create, which the engine now
strips. Binding the app's hook lets both completion cases travel the app's real
user path; the one fixture that must START completed seeds under isSystem,
the documented remedy. No assertion changed, nothing skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
… live create-side escape

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…re-anchor the system-context census after the mcp bridge note grew

`pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated --fix`
reported exactly one stale artifact (`content/docs/references/**`) and regenerated it with
`gen:docs`: 12 rows of `references/data/data-engine.mdx`, one of
`references/kernel/execution-context.mdx`, and the `droppedFields` rows of
`references/api/protocol.mdx` and `references/api/batch.mdx`. A second `check:generated`
answers "All 15 generated artifacts are up to date" (fixed point); `check:api-surface`
answers "public API surface + factory signatures unchanged".

`check-system-context-census --fix` re-anchored one line (`stdio-data-bridge.ts:246` -> `:250`,
the header note in that file grew by four lines); the gate is 0 and a second `--fix`
rewrites nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…still stated the superseded readonly-on-INSERT contract now state the ruled one

Maintainer ruling on E3 (2026-09-05, 「已发布必修,其余立卡」): published surfaces
are fixed in the round, non-published residue is carded. Measured against the
built dist, three of the third verdict's nine N5 sites reach a published
`.d.ts` and are corrected here:

- packages/spec/src/security/public-form.ts — TSDoc on the exported
  PUBLIC_FORM_SERVER_MANAGED_FIELDS (dist/security/index.d.ts): the anonymous
  surface no longer rests on "the static-readonly strip only covers UPDATE", and
  the authenticated-write example no longer says an insert may seed readonly
  columns; a non-system insert is stripped inside engine.insert since the
  2026-09-03 ruling, an import seeds read-only columns only under a system
  context (preserveAudit is UPDATE-only).
- packages/objectql/src/readonly-strict-errors.ts — the `operation` property
  TSDoc of the exported ReadonlyFieldRejectedError (dist/index.d.ts): an INSERT
  refusal is about a runtime-owned value OR, since the ruling, a static readonly
  value from a non-system caller. buildRefusalMessage is untouched; the module
  docblock at the top of the file reaches no published artefact and is carded.
- packages/services/service-settings/src/settings-service.ts — the upsertRow
  docblock (dist/index.d.ts and dist/index.js): a sys_setting insert stays
  outside the strip because the object is sys_-prefixed and engine-owned
  (staticReadonlyInsertSubject's carve-out, #15719), not by the superseded row.

The changeset adds '@objectstack/service-settings': patch so the corrected
.d.ts ships. No behaviour change; no test touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Seven of the eight both-sides files auto-merged with every hunk of both
sides preserved (verified line-for-line against each parent's diff from
the merge base). One conflicted and was resolved by hand:

- content/docs/permissions/system-context.mdx (merge=os-regen, MIXED):
  the driver refused to defer because both sides carry prose, text-merged,
  and conflicted. Resolution keeps the branch's merged INSERT row 20 (the
  DataProtocol ingress row is gone) AND main's new row (#15225 bulk event
  organizationId), so the numbering from Sharing onward returns to the
  merge base's. Counts follow: 106 sites / 19 packages / 44 files. The
  anchor line numbers are placeholders here; the next commit regenerates
  them with `pnpm gen:system-context-census`.
- scripts/check-system-context-census.mjs: two NON_READ_ANCHORS `why`
  strings the branch renumbered (row 33, row 59) are row 34 / row 60 under
  the merged numbering; the other five keep the branch's values.

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

`pnpm gen:system-context-census` on the merge commit. The first run refused
engine.ts (16 distinct page anchors vs 15 anchorable lines) because row 20
carried the branch tree's coordinate for the INSERT `isSystem` site while the
validation-row range carried main's coordinate for the same line; putting
row 20 on main's coordinate (the driver's "take either side") made the page
one coordinate system, after which the rerun rewrote 11 engine.ts anchors and
refused zero files. `pnpm check:system-context-census` is green: 106 sites in
19 packages across 44 files, 140 anchors resolve, 27 declared non-read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 5, 2026 11:54
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit b398ad2 Sep 5, 2026
39 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14147-engine-insert-readonly-strip branch September 5, 2026 12:24
baozhoutao pushed a commit that referenced this pull request Sep 5, 2026
… SEAM, not by number

`check-system-context-census` held every `file:line` anchor on the page to the
tree, but nothing held the two other hand-maintained claims on the same page: the
page's own prose row references and the `why:` strings in `NON_READ_ANCHORS` that
name a row by number. A row inserted into the behaviour table renumbers every row
below it and silently falsifies every reference past the insertion point, with the
gate green — measured on #15687 (three references went stale) and #15395 (all
three became true again by coincidence, observed by nothing).

Each reference now declares the SEAM it is about — a literal occurring in exactly
one numbered row — and the gate resolves the seam, reads the number the reference
carries, and refuses when they disagree, naming the reference, its number and the
row the key resolves to. A ledger keyed by number would rot in the same edit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
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 protocol:data size/xl tests tooling

Projects

None yet

3 participants