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
Conversation
… 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
…gine-insert-readonly-strip
…, 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
…gine-insert-readonly-strip
… not by an issue id Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…gine-insert-readonly-strip
…g origin/main Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift CheckThis PR changes 8 package(s): 35 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
…gine-insert-readonly-strip
…eleased package list Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
|
Contract review at Short form: Stays draft, 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
Coordination — the engine seat is now at
|
⛔ Tier correction — the engine seat is no longer at
|
| 01:43Z | now (~02:0xZ) | |
|---|---|---|
external_metadata.last_served_model |
claude-fable-5-1 |
claude-opus-5 |
session_context.model |
claude-fable-5-1 |
claude-opus-5 |
vs CONTRACT_REVIEW_TIER (dispatch-gates.mjs:8659 = claude-fable-5-1) |
at tier | ⛔ below tier |
Per the rule this seat must apply to itself: 「读数 ≠ CONTRACT_REVIEW_TIER ⇒ 本席 ⛔ 不自判清标,改走转录
核验的 fable 复核子代理 —— 标签在复核完成前原样留置,卡在队列外等待是安全态」. So:
- ⛔ This seat will not judge or clear
needs:contract-reviewon this PR, on feat(spec): register the ADR-0030 notification cut-over in sys_migration's well-known migration ids #15450, or on any
clause-② carrier, and ⛔ will not flip or arm on the strength of its own reading. - ✅ The context-isolated fable review subagent dispatched at 01:5xZ keeps running and its verdict is
adopted verbatim after its transcript's per-messagemodelstamps are verified as
claude-fable-5-1throughout — or voided whole. Those are the only two legal moves; ⛔ never
rewritten, abridged or polished. ⚠️ It has been told it is now the review of record rather than a second opinion, and told ⛔ not to
defer any row back to this seat.- ⛔ The quota-exemption downgrade does not apply here: it covers dispatch, and contract review exists
precisely to compensate for a lower-tier dispatch.
One measured finding while re-deriving, worth recording because it corrects a common shorthand.
「fable 卡」 is not a path property in this repo. Derived just now — ⛔ not recalled, and in a
throwaway worktree cut at origin/main because the tool resolves its root from its own file location
and the primary checkout printed 「
CHANGED … a well-formed answer about a tree nobody is on」:
node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/data/field.zod.ts
→ Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)
Same answer for metadata-protocol/src/protocol.ts, objectql/src/engine.ts, objectql/src/summary-backfill.ts,
spec/src/data/aggregation-policy.ts, drivers/driver-sql/src/sql-driver.ts and a generated
translations path. ⇒ The mandatory-tier globs do not cover this PR's surface at all; what puts this
PR at CONTRACT_REVIEW_TIER is the content limb — the card's own clause-② standing — carried by the
needs:contract-review label, not by any path. A seat that reasons 「it touches packages/spec, so the
path forces fable」 has the right conclusion for the wrong reason, and would get the opposite case wrong.
Nothing else changes: this PR stays draft, both carriers stay hung, and the merge round still sequences
after the engine.ts p0 (#15225).
Generated by Claude Code
…-create, retire every surviving "INSERT is exempt" statement, fix five census cross-references Contract review 5548671173 (FAIL, patch round), items 1-4: 1. Every surviving statement of the superseded "INSERT is engine-exempt / the ingress strips on create" premise is corrected to the 2026-09-03 ruling (option C): the verdict's five sites, plus the same statement found by sweep in validate-flow-node-writes.test.ts, the kernel contracts page, fields.mdx, the strictReadonlyWrites contract docblock, the readonly liveness verdict (evidence pointed at the deleted ingress strip), the authz conformance matrix row and two test headers. The verdicts of the green control cases are unchanged; their justifications now name the scan gap (#15394) instead of an exemption. 2. system-context.mdx: the five prose row references the 22→21 … 65→64 renumbering left behind (50→49, 30→29 twice, 22→21, 34→33), each checked by eye against the renumbered table; the census gate reads none of them. 3. batchData's two upsert-create engine.insert calls forward onFieldsDropped and hang the merged events on the row result exactly as case 'create' does; pinned for both arms in protocol.readonly-insert.test.ts, the firing control enumerates six calls, and cloneData's deliberate absence is pinned against CloneDataResponseSchema (no droppedFields member). 4. Changeset body: the metadata-protocol bullet names which faces report droppedFields and why cloneData does not; the lint bullet is true of all three rules. Levels, BREAKING banner and ADR-0087 disposition unchanged. origin/main is deliberately not merged this round; the p0 on engine.ts (#15225) lands first and a separate merge round follows it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…e superseded insert exemption The runtime-owned helper's "leaves author-declared readonly fields alone" case kept its verdict but stated as its reason that the engine is not where the static-readonly insert strip lives. Since the 2026-09-03 ruling it is; the case now says why the verdict still holds (a separate pass with its own gate, and preserveAudit must not leak across). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ts note grew; close a string literal in the authz matrix check-system-context-census --fix rewrote 9 line anchors (pure line rot: the insert() standing note gained seven lines); the gate reads OK — 105 / 19 / 44 afterwards. The authz conformance matrix row gained an apostrophe inside a single-quoted string in the previous commit; rephrased. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Contract re-review at
|
… no longer stated as live in schema text, code notes, test titles, the census gate's row annotations or the authz matrix Patch round R3 on the contract re-review (5549250610), items 1-3 and 5. Prose only; no behaviour change, no test deleted or skipped. - N2 (a)-(e): the `preserveAudit`, `CreateDataResponseSchema.droppedFields`, `CreateManyDataResponseSchema.droppedFields` and `BatchOperationResultSchema.droppedFields` `.describe()` strings and the `CloneDataResponseSchema` TSDoc now state the ruled state: the static `readonly` strip runs inside `engine.insert`, after `beforeInsert`, `isSystem`-gated; the DataProtocol ingress copy is deleted. The generated `content/docs/references/**` rows follow in the next commit via `check:generated --fix`. - N2 (f)-(k): `rest-server.ts` batch-route note keeps the routing reason on what the ingress still owns (object-existence gate, #7823 response strip, `droppedFields` relay) and attributes the platform-object carve-out to the engine, where it lives (`staticReadonlyInsertSubject`); the #3431 header note; `rule-validator.ts`'s second-consumer sentence names `staticReadonlyInsertSubject`; the mcp stdio bridge's divergence list drops the readonly strip and says why it is closed; two test comments/titles in `engine-autonumber-runtime-owned.test.ts` and one title in `rest-dropped-fields.test.ts`. - N3: the seven `why` rows in `check-system-context-census.mjs` re-derived against the head table (24->23 x2, 22->21 x2, 25->24, 34->33, 60->59). The gate never parses them. - N4: the authz matrix row states the two mechanisms - a system context for identity provisioning; the `sys_`/`managedBy` carve-out for the metadata repository's `sys_metadata_history.recorded_by` provenance row - and re-derives "event-log cursors" by name as `sys_metadata_history.event_seq` (same row, same mechanism). Issue-id multiset of the row is byte-identical for the prose-id ledger. 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
Fixes #14147
readonlyis now enforced in-engine on INSERT for non-system callers, exactly asit 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-zhuangandhotlong, human merge).Patch round R4 — the E3 ruling 5550450281 (「已发布必修,其余立卡」) applied to contract re-review 5550434842 (FAIL) → head
a73dd85dbOne 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/mainis still NOT merged thisround (F2: #15225 / PR #15687 land first), so
mergeable_state: dirtystaysexpected; the both-sides set grows from six to seven files —
settings-service.tshas onemaincommit 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; items2–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 thatships in
.d.ts, the generatedcontent/docs/references/**, customer-facingdocs pages) is fixed in the round; non-published residue (test headers, in-source
notes that reach no published artefact,
scripts/provenance text) becomes onefindingcard 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 eachN5 site grep its sentence as a fixed string in
dist/**/*.d.ts(the shippeddeclaration surface), in
dist/**/*.{js,mjs,cjs}, and in the.mapfiles, pluscontent/docs/**(the generatedreferences/**projections and the customerpages); beside each, a firing control — the exported identifier the sentence
sits next to, grepped the same way. Before =
485a2d525; after =a73dd85dbrebuilt. One boundary stated so nobody re-derives it: every shipped
.js.mapembeds its whole source (
sourcesContent), so by that reading no source commentis ever non-published — that reading contradicts the ruling's own examples, so
the
.d.ts/ docs measurement is the one applied and the.mapcolumn isreported, not scored.
.d.ts/.js/.mapspec/src/security/public-form.ts:21-22(«insert may seedreadonlycolumns») — and the same docblock's:10-11(«the static-readonlystrip (#2948) only covers UPDATE — so nothing downstream guards these columns»), the same claim in the spelling the reviewer's patterns did not carrydist/security/index.d.ts:278,.d.mts) / none / 2 files —:10-11likewise hit at:266PUBLIC_FORM_SERVER_MANAGED_FIELDSin.d.ts6 linesobjectql/src/readonly-strict-errors.ts:21-22— the module docblock («a create is deliberately exempt from the author-declared strips (#3413)»)ReadonlyFieldRejectedErrorin.d.ts4 linesREADONLY_CLASS_REASONS; declaration emit drops it, esbuild drops it from the JS:21and:170; that is what the ruling prescribes and the card says soobjectql/src/readonly-strict-errors.ts:170-172— theoperationproperty TSDoc of the exported class («an INSERT refusal can only ever be about a runtime-owned value»)dist/index.d.ts:362,:373,.d.mts×2) / none / 4 filesdist/index.d.ts:365,:379service-settings/src/settings-service.ts:2279— theupsertRowdocblock («The INSERT path is deliberately exempt from that strip (#3413)»)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.d.ts2 linesdist/index.d.ts:1156settings-secret-rotation.test.ts:13,:189;sys-secret-orphan-report.test.ts:190wrapEngineAsSettingsEnginein.d.ts4 linesrest/src/rest-batch-endpoint.test.ts:308-312droppedFieldsindist/index.cjs34 linesservice-automation/src/builtin/crud-dropped-fields.test.ts:144-146create_recordin.d.ts4 linesspec/scripts/liveness/proof-registry.mts:156dist,liveness/,json-schema/,prompts/,api-surface/,content/docsreadonly-static-writeinliveness/field.json,liveness/README.mdscripts/is outsidepackages/spec'sfiles; the:150summary is true but names UPDATE only)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.tsand even in the.js. The residual#3413in the rebuilt spec.d.ts(analytics.zod-*.d.ts:1318) iscontracts/data-engine.ts:145, the R2-corrected historical sentence («true when written, SUPERSEDED since») — class H, published, true.a73dd85dbpublic-form.tsTSDoc: the anonymous-surface rationale no longer rests on «the strip only covers UPDATE» — it states the strip runs insideengine.insertfor a non-system caller since the 2026-09-03 ruling and is a policy overreadonly: truedeclarations, 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 (preserveAuditis UPDATE-only, #6640). Added to the cross-lane table below;@objectstack/specstayspatchcheck:api-surface«unchanged» (no surface moved);check:generatedfixed point (no projection regenerates — TSDoc on a const is not a.describe()); the prose itself by item 2's list, pattern p6check:generated0 «All 15 generated artifacts are up to date» ·check:api-surface0 «public API surface + factory signatures unchanged ✓» ·check:docs0 «230 generated files in sync» ·check:liveness0 ·git statusempty after bothreadonly-strict-errors.ts:166-175(operationTSDoc only): an INSERT refusal is about a runtime-owned value (exempt writersisSystem+preserveAudit) or, since the ruling, a staticreadonlyvalue from a non-system caller (only exempt writerisSystem;preserveAuditis UPDATE-only for that strip);readonlyWhenstill locks nothing on a create. ⛔buildRefusalMessageuntouched (byte-pinned #5126/#5503). The module docblock:5-60is deliberately left — non-published, cardedengine-insert-static-readonly-strip.test.ts«strictReadonlyWrites refuses before any driver dispatch» (a staticcompleted_atrefused withERR_READONLY_FIELD_REJECTED, 0 creates) — the refusal the old sentence deniedcheck:test-typecheckOK 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 passedsettings-service.ts:2274-2285(upsertRowdocblock): «deliberately exempt (#3413)» → on THIS object the insert is outside the strip becausesys_settingissys_-prefixed andmanagedBy: 'engine-owned', whichstaticReadonlyInsertSubjectleaves 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': patchso the corrected.d.tsships (prose only; the package's behaviour is unchanged)check-adr-0087-registration/check-changeset-no-major/check-empty-changeset--base origin/main0 / 0 / 0Zone 2 — one of three falsified. (1) ⭐ Falsified: N5(a) is not the only published site — (b2) and (c) reach a
.d.tstoo (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:generatedafter the rebuild reports «All 15 generated artifacts are up to date» andgit statusis empty — a TSDoc on a const regenerates nocontent/docs/references/**projection (nothing to commit from the generator). (3) Held:check:api-surfaceon the rebuiltdistanswers «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
readonlystrip, 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 at485a2d525for the before-reading): scopepackages content docs skills scriptswith:!**/CHANGELOG.md :!content/docs/releases/** :!content/docs/references/**, allgit grep -n -iE: A the twelve premise spellings of R3; Bingress.*(readonly|strip|seed)|(readonly|strip|seed).*ingress— content-only, one regex over the line, ⛔ never a filter overgrep -noutput; p1insert[^|]{0,80}exempt|exempt[^|]{0,80}insert; p2readonly-exempt; p3may seed|can seed; p4seed[^|]{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 byfile:line, every line classified below.485a2d525(positive control — must fire)a73dd85db:21,:170; (c):2279; (d) ×3; (e):308; (f):145; (g):156:145:22:22:22, (c), (d) ×3, (e), (g)'s neighbours:11skills/objectstack-data/SKILL.md:274, the governed half, #15382)file:linepublic-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 -noutput, 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 theinternal: truewrite-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):
docs/automation/hook-bodies.mdx:264ctx.api.object('x').insert({ FIELD })¦ Silently dropped — unless the ho…docs/data-modeling/fields.mdx:319readonly¦boolean¦false¦ Prevent editing — hidden from create/edit f… — customer page: «server-enforced on both write paths»docs/kernel/contracts/data-engine.mdx:311readonlyWhenis insert-exempt at this…docs/kernel/contracts/data-engine.mdx:371docs/kernel/contracts/data-engine.mdx:372readonly: truefield's initial value… — continuation of the :371 quotation; :373 «That row is superseded»docs/kernel/contracts/data-engine.mdx:377docs/permissions/system-context.mdx:114readonlystrip bypassed — INSERT ¦ objectql ¦ Same, on create — one…docs/protocol/objectql/security.mdx:276preserveAuditis an UPDATE-path exemption. It does not apply on INSERT (#66…adr/0066-unified-authorization-model.md:63adr/0131-total-organization-ownership-no-null-organization-id.md:849singleand created before appli…qa/access-security.json:129qa/cli.json:1099packages/client/src/index.ts:415readonly—@objectstack/clientTSDoc, published; cites #3043 as origin, the statement holds under Cpackages/cli/src/commands/meta/resync.ts:82packages/cli/src/commands/migrate/duplicates.ts:884readOnlyProberefuses to bring alint/system-fields-consumers.test.ts:136lint/validate-flow-node-writes.test.ts:412engine.insert(#14147), not an exemption. This rule asks alint/validate-readonly-action-writes.test.ts:29lint/validate-readonly-action-writes.test.ts:289lint/validate-readonly-action-writes.test.ts:299readonlyAND areadonlyWhen-locked column — a fact about elevation, not INSERTlint/validate-readonly-action-writes.test.ts:313lint/validate-readonly-action-writes.ts:59lint/validate-readonly-action-writes.ts:61lint/validate-readonly-action-writes.ts:64lint/validate-readonly-action-writes.ts:70lint/validate-readonly-action-writes.ts:73insertseeding areadonlyAND a — samelint/validate-readonly-action-writes.ts:174lint/validate-readonly-action-writes.ts:179lint/validate-readonly-action-writes.ts:190engine.tsstates it at the bulk strip: "INSERT stays exempt"…lint/validate-readonly-flow-writes.test.ts:330lint/validate-readonly-flow-writes.ts:7success. #3407/#3413 made that strip observable at — #3407/#3413 made the strip observable — true historylint/validate-readonly-flow-writes.ts:15readonlylint/validate-readonly-flow-writes.ts:16create_recordmay legitimately seed readonlylint/validate-readonly-flow-writes.ts:20lint/validate-readonly-flow-writes.ts:221lint/validate-readonly-hook-writes.test.ts:215lint/validate-readonly-hook-writes.test.ts:216lint/validate-readonly-hook-writes.test.ts:217lint/validate-readonly-hook-writes.test.ts:218lint/validate-readonly-hook-writes.ts:43lint/validate-readonly-hook-writes.ts:44readonlystrip (#3043/#3413: "a create may legitimately seedlint/validate-readonly-hook-writes.ts:210lint/validate-readonly-hook-writes.ts:211readonlystrip so a create may legitimately seedlint/validate-readonly-hook-writes.ts:212lint/validate-readonly-hook-writes.ts:339mp/protocol.dropped-fields.bulk.test.ts:108mp/protocol.dropped-fields.test.ts:4onFieldsDroppedchannel and wired the flowmp/protocol.dropped-fields.test.ts:126mp/protocol.readonly-insert.test.ts:6mp/protocol.readonly-insert.test.ts:7mp/protocol.readonly-insert.test.ts:13stripReadonlyFieldsinsidemp/protocol.readonly-insert.test.ts:106mp/protocol.ts:10393stripReadonlyForInsert, #3043) is deletedmp/protocol.ts:10395mp/protocol.ts:10491createData— a clone's 201 body ismp/protocol.ts:10542update_recordwiring (#3413). A faulty — theupdate_recordwiring #3413 builtmp/protocol.ts:11952mp/protocol.ts:12020datapayload — the bulkmp/protocol.write-response-internal-fields.tripwire.test.ts:9internal: truewrite-response strip lives at the protocol ingress, notmp/protocol.write-response-internal-fields.tripwire.test.ts:244internal: truef…mp/seed-loader-state-machine-exempt.test.ts:50initialStatesexemptionmp/seed-loader.ts:201mp/seed-loader.ts:222mp/seed-loader.ts:2032objectql/engine-autonumber-resync.test.ts:381objectql/engine-autonumber-runtime-owned.test.ts:362objectql/engine-autonumber-runtime-owned.test.ts:550stripReadonlyForInsert, #3043), andobjectql/engine-autonumber-runtime-owned.test.ts:552objectql/engine-autonumber-runtime-owned.test.ts:561objectql/engine-autonumber-runtime-owned.test.ts:634readonlyfield of an ORDINARY type is still stripped…objectql/engine-capability-provenance.test.ts:158objectql/engine-hook-provenance-sibling-seams.test.ts:219objectql/engine-hook-provenance-sibling-seams.test.ts:360readonlyWhenentirely, so no lockobjectql/engine-insert-static-readonly-strip.test.ts:17objectql/engine-insert-static-readonly-strip.test.ts:31objectql/engine-insert-static-readonly-strip.test.ts:34objectql/engine-insert-static-readonly-strip.test.ts:203objectql/engine-insert-static-readonly-strip.test.ts:276objectql/engine-insert-static-readonly-strip.test.ts:301objectql/engine-strict-readonly-warning-truthful.test.ts:22account_number— a caller-seeded autonumber refused under strict — runtime-ownedobjectql/engine-update-addressing-id-not-dropped.test.ts:158idreadonly'…objectql/engine-update-addressing-id-no-warn.test.ts:54{value,id}+where.id, readonlyid¦ NONE…objectql/engine-update-addressing-id-no-warn.test.ts:177objectql/engine.ts:4610objectql/engine.ts:9632objectql/engine.ts:9634objectql/engine.ts:10521objectql/engine.ts:11687stripReadonlyWhenFields; INSERT stays exempt.objectql/internal-fields.test.ts:245objectql/readonly-strict-errors.ts:21objectql/readonly-strict-errors.ts:22objectql/validation/record-validator.test.ts:618objectql/validation/rule-validator.test.ts:1355objectql/validation/rule-validator.test.ts:1357objectql/validation/rule-validator.ts:643readonlyWhendocblockobjectql/validation/rule-validator.ts:872objectql/validation/rule-validator.ts:1021stripReadonlyFieldsdocblock names UPDATE only; runs on both verbs since #14147 — #15819 secondaryobjectql/validation/rule-validator.ts:1318objectql/validation/rule-validator.ts:1328objectql/validation/rule-validator.ts:1484objectql/validation/rule-validator.ts:1486objectql/validation/rule-validator.ts:1509objectql/validation/rule-validator.ts:1540objectql/validation/rule-validator.ts:1567engine.insertcalls {@linkobjectql/validation/rule-validator.ts:1602write from a system context (\context.isSystem`) — a non-system create may…plugins/plugin-security/src/authz-matrix-gate.test.ts:442plugins/plugin-security/src/walled-platform-bucket-diagnostic.test.ts:364acme_readonlydogfood/authz-conformance.matrix.ts:281readonly: truestripped from…dogfood/authz-conformance.matrix.ts:282dogfood/authz-conformance.matrix.ts:284readonly: trueused to be UI-only, so a logge…dogfood/field-zoo.matrix.ts:55dogfood/showcase-static-readonly.dogfood.test.ts:24stripReadonlyForInsert, metadata-protocol) — the seam everyrest/import-runner-historical-readonly-insert.test.ts:15rest/rest-batch-endpoint.test.ts:308readonly-exempt by design (#3413), so — N5 (e) — #15819 row 5rest/rest-server.ts:12359readonlystrip lived at that ingress andrest/rest-write-response-internal-fields.tripwire.test.ts:330POST /data/:objectuses, stripped there.',packages/runtime/src/app-plugin.ts:138packages/runtime/src/http-dispatcher.test.ts:688packages/runtime/src/seed-datasets.ts:72sa/builtin/create-record-readonly-drop.test.ts:118sa/builtin/create-record-readonly-drop.test.ts:136sa/builtin/crud-dropped-fields.test.ts:145sa/builtin/crud-nodes.ts:323sa/builtin/crud-nodes.ts:328ss/settings-secret-rotation.test.ts:13ss/settings-secret-rotation.test.ts:189context.isSystem), while the INSERT path is exempt (#3413). It is a — N5 (d) — row 3ss/settings-service.ts:2280ss/sys-secret-orphan-report.test.ts:190context.isSystem), while the INSERT path is exempt (#3413). It is also — N5 (d) — row 4spec/liveness/field.json:87spec/liveness/field.json:102spec/scripts/build-schemas-check-mode.test.ts:2442spec/scripts/build-schemas-check-mode.test.ts:2740spec/scripts/build-schemas-check-mode.test.ts:3100spec/scripts/liveness/evidence.test.ts:33spec/scripts/liveness/proof-registry.mts:156spec/src/api/protocol.zod.ts:1975rea… — the R3-corrected.describe()`spec/src/contracts/data-engine.ts:145spec/src/contracts/data-engine.ts:146readonly: truefield's initial value") — true whenspec/src/contracts/seed-settlement.ts:92spec/src/contracts/share-link-service.ts:185spec/src/data/field-autonumber-readonly.test.ts:89summaryis a stored roll-up a caller MAY legitimately seed (#6014).spec/src/data/field.zod.ts:1581spec/src/data/temporal-conformance.ts:90create()—spec/src/kernel/execution-context.test.ts:179spec/src/kernel/execution-context.zod.ts:392spec/src/stack.zod.ts:1427config.objects: an app legitimately seeds theskills/objectstack-data/SKILL.md:274Not 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-filecontent/docs/**control),content/docs/references/**(generator-owned;git grep -iE 'insert exempt|may seed' -- content/docs/referencesreturns nothing).Item 3 — the residue card
#15819 —
findingonly, ungraded, unrouted, unassigned: the seven non-published live-class sites (rows 1–7 = (b1), (d) ×3, (e), (f), (g)) each byfile:linewith what it says and why it is the class, the by-omissionrule-validator.ts:1021as a marked secondary entry, the governedSKILL.md:274excluded 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 MCPsearch_issues(27 results; nearest #15394 = the lint rules' scan gap, #15703 =cloneData's missingdroppedFields— neither is this), plus the openfindinglist (one page of 100) grepped locally.GET /commits/a73dd85db/check-runs→total_count: 0; the PR readsmergeable: false,mergeable_state: dirty. GitHub runs nopull_requestworkflow on a conflicted PR, so — as on485a2d525,e78c95cb4,66c580b0c— no CI has run on any head sincebd598e803, 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, ⛔ neverbd598e803's.R4 measurements — all at
a73dd85db, builds/tests throughscripts/pm/os-verify-lock.sh(slotdev-14147-r4,--statusread first: free), every exit captured before any pipe, VERDICT lines readspec build·objectql build·service-settings build·git status.d.tsemitted;gen:schemarewrote nothingcheck:generated·check:api-surface·check:docs·check:liveness·git statustypecheck· 6 suites · service-settingstypecheck·testturbo build --filter='@objectstack/client-react...' --filter='@objectstack/driver-turso...'(35 tasks) ·check:skill-examples·check:type-check-debt· rm the gitignored.examples-build→check:docs-audit-scope·turbo build --filter='./packages/**'(71/71 cached) ·check:dual-build-cjs-loads.d.tshit → 0; (b1) (d) (e) (f) (g): 0 throughoutnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom this worktree, no paths (52-path change set from the merge-base), under its own STALE TREE warning (139 commits behindorigin/main, 60 gate files changed — F4)check:cross-package-test-inputs1 → 0 — red only under my runner'sNODE_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-scope1 → 0 with the gitignoredpackages/spec/.examples-buildleft bycheck:skill-examplesremoved (#15328, pre-existing);check:skill-examples1,check:type-check-debt3,check:dual-build-cjs-loads3 = PREREQUISITE (client-react.d.ts/ driver-turso / everydist) → 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-bytes7497 files;check:engine-double-contract;check:corpus-claim-drift--base origin/mainno-migration-prescription· nomajor· 1 declaring changesetNOT 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
485a2d525Two commits appended on
66c580b0c(⛔ no rebase, no amend, no force-push):e78c95cb4items 1–3 and 5 at the source ·485a2d525item 1's regeneratedreference rows plus one census re-anchor. ⛔
origin/mainis still NOT mergedthis round (F2 upheld: 0 commits on
engine.tssince the merge-base, #15225lands first), so
mergeable_state: dirtystays expected. ⛔ No behaviour change,no test deleted or skipped; E1 and E2 are not touched.
.describe()strings + the Clone TSDocexecution-context.zod.tspreserveAudit: «the create-side staticreadonlystrip runs insideengine.insertitself (after thebeforeInserthooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads onlycontext.isSystem»; (b)CreateDataResponseSchema.droppedFields: «the strip runs insideengine.insert, after thebeforeInserthooks,isSystem-gated»; (c)CloneDataResponseSchemaTSDoc: «the staticreadonlystrip applied insideengine.insertfor 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 staticreadonlystrip 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 withcheck:generated --fix(it reported exactly one stale artifact,content/docs/references/**;gen:docsrewrote 12 rows ofreferences/data/data-engine.mdx, 1 ofreferences/kernel/execution-context.mdx, and thedroppedFieldsrows ofreferences/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 filecheck:generated(a describe edit without regeneration);check:api-surface(no surface moved); the prose itself by item 4's listcheck:generated --fix0 ·check:generated0 «All 15 generated artifacts are up to date» ·check:api-surface0 «public API surface + factory signatures unchanged»rest-server.tsbatch-route note: the exemption sentence is deleted; the note says the strip lived at the ingress when written and runs insideengine.insertsince the ruling, and stands the routing on what the ingress still owns — the #3770 object-existence gate, the #7823internal: trueresponse strip and thedroppedFieldsrelay (one create ingress, one response contract).staticReadonlyInsertSubject,rule-validator.ts:1515-1525, returns null onmanagedBy/sys_;createDatainprotocol.tshas 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-readonlystrip dropped (engine.insert, relayed bycreateDataasdroppedFields)»; (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-616comment and:634title; (k)rest-dropped-fields.test.ts:97titlemcp/resttypecheck for the comment-only editsmcptypecheck 0 ·resttypecheck 0whyrowsbypassTenantAuditthreading — head row 23 «Tenant-audit warning silenced»), 22→21 ×2 (strictReadonlyWritesrefusal;READONLY_CLASS_REASONS— head row 21 «Strict-drop refusal never fires»), 25→24 (system-write-guard.tshelper — 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 theowner_idstamp»);:318«row 2» unchanged and right. No mechanical pin exists: the gate only interpolateswhyinto 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)--self-test0 · gate 0ingressco-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 aboutreadonlyWhen» was false at66c580b0c(eleven live sites) and is withdrawn — the list replaces itenforcementandnotenow state the two mechanisms: (1) a system context — identity provisioning (plugin-auth/src/objectql-adapter.ts:738,:1123wrap the engine inwithSystemContext); (2) thesys_/managedBycarve-out instaticReadonlyInsertSubject— the metadata repository (sys-metadata-repository.ts:661-683,:804-824;isSystemhas 0 hits in that file) seedssys_metadata_history.recorded_by(readonly: truelookup) 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 listauthz-conformance47/47 ·check:doc-authoring0 «sibling-package prose ids hold the baseline — no growth, no burn-down unrecorded»Zone 2 — falsified: none of the four. (1)
check:generated --fixregenerated every row the describe edits moved, and a secondcheck:generatedreports nothing stale — a fixed point. (2)check:api-surfaceanswers «unchanged» on the rebuiltdist;@objectstack/specstayspatch. (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/mcpoutside the bridge hits only comments about other divergences (ExecutionContext assembly in__tests__/plugin-execution-context.test.ts:10, the exposure gate instdio-data-bridge.exposure.test.ts), and «Known divergences» occurs nowhere but the bridge (control: 5divergencehits inside it) — prose-only,mcptypecheck 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 throughscripts/pm/os-verify-lock.sh(slotdev-14147-r3), every exit captured before any pipepnpm --filter @objectstack/spec build·check:generated --fixcontent/docs/references/**) →gen:docs; held 187s, waited 301scheck:generated(fixed point) ·check:api-surface·check:livenesspnpm --filter @objectstack/lint testmetadata-protocol typecheck·testobjectql typecheck(incl.check:test-typecheck, 44 files / 69 pinned) · 5 suitesengine-insert-static-readonly-strip,engine-autonumber-runtime-owned,validation/rule-validator,engine-lookup-referential-integrity,engine-repo-execute-elevationcreate-record-readonly-drop· restimport-runner-historical-readonly-insert+rest-dropped-fieldstest/authz-conformance.test.ts·pnpm --filter @objectstack/example-todo testmcp typecheck·rest typecheck(+ itscheck:test-typecheck)check-system-context-census --self-test· gate ·--fix· gate ·--fixagainstdio-data-bridge.ts:246→:250after 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.describe()strings clean; 829 pinned prose ids hold; 7497 files, no control bytescheck-adr-0087-registration·check-changeset-no-major·check-empty-changeset, each--base origin/mainno-migration-prescription· nomajor· 1 declaring changesetgit diff --name-only a06faebbe...HEAD -- 'content/docs/releases/**'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 behindorigin/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: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, speccheck:api-surface/check:authorable-surface/check:docs/check:generated/check:liveness/check:export-origins/check:strictness-ledger, and the rest of the derived listcheck:skill-examples(needsclient-reactbuilt; then 257 examples type-check) ·check:docs-audit-scope(itsaffected-docsself-test reds whilepackages/spec/.examples-build/**, left behind bycheck:skill-examples, exists — #15328, pre-existing; 0 with the gitignored artefact removed, anddocs-audit/check-affected-docs.mjs+check-drift-comment.mjsre-measured 0 the same way)check:dual-build-cjs-loads(PREREQUISITE NOT MET until everypackages/*was built under the lock, 71 turbo tasks; then 103/66/619/1 against floors 90/58/520/1) ·check:type-check-debt(untildriver-tursowas built; then «13 ledger entries re-measured, 143 raw tsc errors, none above its recorded number»)--base origin/main(all 0, above); the other 10 read$RUNNER_TEMP/matrix.shardinputs (check-shard-attestation,check-test-completeness,check-cross-package-test-inputs --union-into …) — NOT MEASURED, CI-only by constructioncheck-partof-closing-keyword.mjs/check-single-claim-paths.mjsanswer 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)» — needsNODE_USE_ENV_PROXY=1here: Node's own fetch bypasses the container proxy that injects the token and gets 401 without it ·check:react-declaration-parityexit 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 cardcheck-platform-object-tenancy-census.mjs,check-registry-log-declared.mjsdo not exist on this branch (no merge this round) — F3, run at the merge roundItem 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'— thengit grep -n -iE ingressfiltered to lines also matchingreadonly|strip|seed, both overpackages content docs skills scriptswith:!**/CHANGELOG.md :!content/docs/releases/** :!content/docs/references/**. Positive control: the literalat the DataProtocol ingressfires onexecution-context.zod.tsat66c580b0c(1 hit) and on nothing inpackages/spec/srcat485a2d525. 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 filteredgrep -noutput, 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 aboutreadonlyWhen, still insert-exempt · #7823 theinternal: truewrite-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 seamprotocol.readonly-insert.test.tspins) · 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/.docs/automation/hook-bodies.mdx:264insertrow states the in-engine strip;…docs/kernel/contracts/data-engine.mdx:311readonlyWhenis insert-exempt at this…docs/kernel/contracts/data-engine.mdx:371lint/validate-readonly-action-writes.test.ts:29…/validate-readonly-action-writes.test.ts:289…/validate-readonly-action-writes.test.ts:313lint/validate-readonly-action-writes.ts:59…/validate-readonly-action-writes.ts:61…/validate-readonly-action-writes.ts:64…/validate-readonly-action-writes.ts:174…/validate-readonly-action-writes.ts:179lint/validate-readonly-flow-writes.test.ts:330lint/validate-readonly-flow-writes.ts:15…/validate-readonly-flow-writes.ts:16lint/validate-readonly-hook-writes.test.ts:216…/validate-readonly-hook-writes.test.ts:217lint/validate-readonly-hook-writes.ts:43…/validate-readonly-hook-writes.ts:44…/validate-readonly-hook-writes.ts:211mp/protocol.readonly-insert.test.ts:13mp/protocol.ts:10395objectql/engine-hook-provenance-sibling-seams.test.ts:360readonlyWhenentirely»objectql/engine-insert-static-readonly-strip.test.ts:17…/engine-insert-static-readonly-strip.test.ts:31objectql/engine.ts:9634objectql/validation/rule-validator.ts:872readonlyWhenpredicate note: «INSERT is exe……/rule-validator.ts:1484…/rule-validator.ts:1486…/rule-validator.ts:1509spec/src/data/field-autonumber-readonly.test.ts:89summaryis not in `RUNTIME_OWNED_FIELD_TYPE…spec/src/stack.zod.ts:1427sys_business_unitrows in `con…docs/permissions/system-context.mdx:114lint/validate-readonly-flow-writes.ts:17…/validate-readonly-flow-writes.ts:20mp/protocol.readonly-insert.test.ts:4…/protocol.readonly-insert.test.ts:6…/protocol.readonly-insert.test.ts:7…/protocol.readonly-insert.test.ts:15…/protocol.readonly-insert.test.ts:23onFieldsDroppedthere, which……/protocol.readonly-insert.test.ts:55…/protocol.readonly-insert.test.ts:106…/protocol.readonly-insert.test.ts:130mp/protocol.ts:10393…/protocol.ts:10491cloneData's 201-body internal-field strip…/protocol.ts:11952…/protocol.ts:12020…/protocol.write-response-internal-fields.tripwire.test.ts:9internal: truewrite-response strip li……/protocol.write-response-internal-fields.tripwire.test.ts:244objectql/engine-autonumber-runtime-owned.test.ts:550…/engine-autonumber-runtime-owned.test.ts:552…/engine-autonumber-runtime-owned.test.ts:561…/engine-autonumber-runtime-owned.test.ts:634objectql/engine-insert-static-readonly-strip.test.ts:17objectql/engine-update-addressing-id-no-warn.test.ts:54{value,id}+where.idRES……/engine-update-addressing-id-no-warn.test.ts:177objectql/engine-update-addressing-id-not-dropped.test.ts:158objectql/engine.ts:10521objectql/internal-fields.test.ts:245objectql/validation/rule-validator.test.ts:1357dogfood/authz-conformance.matrix.ts:281…/authz-conformance.matrix.ts:282enforcement— rewritten this round (N4: two……/authz-conformance.matrix.ts:284note— «first enforced at the DATA-WRITE IN…dogfood/showcase-static-readonly.dogfood.test.ts:24rest/import-runner-historical-readonly-insert.test.ts:15…/import-runner-historical-readonly-insert.test.ts:23…/import-runner-historical-readonly-insert.test.ts:101createDatais on the path so the s……/import-runner-historical-readonly-insert.test.ts:127…/import-runner-historical-readonly-insert.test.ts:136rest/rest-server.ts:12359rest/rest-write-response-internal-fields.tripwire.test.ts:330spec/liveness/field.json:102spec/scripts/liveness/evidence.test.ts:33spec/src/kernel/execution-context.zod.ts:392preserveAuditdescribe, 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-filecontent/docs/**control), andcontent/docs/references/**(regenerated this round;git grep -F 'at the DataProtocol ingress' -- content/docs/referencesanswers 0 hits aftergen: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
66c580b0cThree commits appended on
bd598e803(⛔ no rebase, no amend, no force-push):b5fb6d728items 1–4 ·da62e0e93one more sweep-found test justification ·66c580b0ccensus re-anchor plus a string-literal fix. ⛔origin/mainis NOTmerged this round, deliberately:
packages/objectql/src/engine.tsis also thelanding site of #15225 (p0, in flight); the p0 lands first and a separate merge
round follows it, so
mergeable_state: dirtyis expected and untouched here.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 blocksvalidate-readonly-hook-writes.ts:39-52as the template. (a): theinsertrow now reads "Silently dropped — unless the hook declaresrunAs: 'system'", the "one channel … plus INSERT" thesis is gone, the "On UPDATE" framing says every non-system write, and the rule-scope sentence namesinsert/createas the #15394 scan gap. (b): the standing note now says two strips shareinsertDroppedand onlyreadonlyWhenstays 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 five —git grep -iEover 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) andgit grep stripReadonlyForInsert, both excluding CHANGELOGs andcontent/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 saidupdateonly, 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(thestrictReadonlyWritesTSDoc — a second cross-lane docblock-only touch ondomain:spec, same shape as the accepted B1;check:generatedconfirms no artefact moved);packages/spec/liveness/field.jsonreadonly(itsevidencepointer named the deleted ingress strip —check:livenessis green on the new pointer);packages/qa/dogfood/test/authz-conformance.matrix.tsreadonly-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— withdrawn in R3: false atreadonlyWhen66c580b0c(eleven live sites); R3 item 4 carries the list.a06faebbe, where "row 50's doors" sat under row 51 (themanage_metadatadoors, now 49), row 30 was theafterDeleterevocation skip (now 29), row 22 strict-drop (now 21), row 34revoke()(now 33).:16150→49 ·:23730→29 ·:24622→21 ·:25034→33 ·:42230→29. No other prose reference sits above the deleted row (row 2twice, androws 1–60/61–64, were already right).check:system-context-censusdoes not read prose references: its green (OK — 105 elevation read sites in 19 packages across 44 files;--fixre-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.batchData's upsert-create arms forward noonFieldsDroppedrunBatchDataLoop— the id-less row, and the id-named row whoseprobeRecordanswers null — now build the same options object ascase 'create'and hangmergeDroppedFieldEvents(rowDropped)on the row result; the arg-forminsertCtxis retired. Pinned inprotocol.readonly-insert.test.ts, one case per arm (the id-named case also assertsfindOnewas consulted exactly once, so it cannot go green through the update arm five lines up); the firing control now enumerates sixengine.insertcalls and is retitled "every create face whose response carriesdroppedFieldspasses anonFieldsDroppedlistener".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 }, andsearch-clone-schema-conformance.test.tsasserts in so many words that the body carries nodroppedFields; wiring a listener there means a new response key on a@objectstack/specsurface 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 logswarn, nothing is reported on the wire; a new pin holdscloneDatato passing no listener with that reason..changeset/tidy-cups-smile.md, levels / BREAKING banner / ADR-0087 marker untouched —check:adr-0087-registration --base origin/main0,check:changeset-no-major0): the metadata-protocol bullet names which faces reportdroppedFieldsand whycloneDatadoes 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 throughscripts/pm/os-verify-lock.sh, every exit code captured before any pipepnpm --filter @objectstack/lint testpnpm --filter @objectstack/metadata-protocol typecheck·testbd598e803: the two upsert-create cases and the clone pinpnpm --filter @objectstack/objectql typecheck(runscheck:test-typechecktoo)engine-autonumber-runtime-owned,validation/rule-validator,engine-insert-static-readonly-strip@objectstack/dogfoodtest/authz-conformance.test.ts66c580b0c)pnpm --filter @objectstack/spec check:liveness·check:generatedGate union, derived from a detached worktree at fresh
origin/main(fa125f3bf— ⛔ not the ~183-commit-stale primary checkout) withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackover the 38 paths ofgit 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/maingates were then re-run strictly after the last commit (all 0).check:system-context-census(+ self-test),check:type-check-coverage,check:type-check-debt(exit 3 untildriver-tursowas 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 untilclient-reactwas 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 listscripts/docs-audit/check-affected-docs.mjs— its self-test went red once, in a chunk that ran concurrently withcheck:skill-examples(which leaves the gitignoredpackages/spec/.examples-build/**that the walk admits askind=contractsources — the #15328 root cause the previous round already posted); exit 0 on re-run with that artefact absentcheck-platform-object-tenancy-census.mjs(+ self-test),check-registry-log-declared.mjs(+ self-test): both landed onmainon 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 roundcheck:dual-build-cjs-loads— PREREQUISITE NOT MET (unbuiltdist/) until everypackages/*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/1session_01ARYe3yQTQCUFm5qPYNgKaJ.What the change is
engine.insertgains a static-readonlypass beside the runtime-owned one italready had, inside the same
if (!opCtx.context?.isSystem)branch, calling theSAME
stripReadonlyFieldsthatengine.updatecalls, and reporting through thesame channels:
readonlyStripWarningatwarn,onFieldsDroppedunder reasonreadonly,strictReadonlyWritesrefusing withERR_READONLY_FIELD_REJECTEDbefore any driver dispatch. The boundary copy is deleted, not kept as a
second implementation.
Three consequences the card asked for, all discharged:
engine.insertdirectly no longer writes theread-only column;
create_record'sonFieldsDroppedwiring starts receiving readonly drops —driven end to end, evidence below;
assertReferencesResolve's doc sentence is true again.A2.2 — every
stripReadonlyForInsertcall site, enumerated before deletingThe definition was at
packages/metadata-protocol/src/protocol.ts:1794. It hadfive call sites, all in that one file — the card's "at least six" counts the
definition and/or the prose references:
protocol.ts:10526createDataonFieldsDropped(already wired here) carries the dropprotocol.ts:10623cloneDataoverridesare still applied BEFORE the insert, so a smuggled readonly key is still judgedprotocol.ts:11746batchData(createrow)onFieldsDroppedper row (it forwarded none before)protocol.ts:12004createManyDataprotocol.ts:12062insertManyDataThree further mentions in the same file were prose (
:1902,:12069) or thedefinition itself. Deleted with it:
warnPreserveAuditIgnoredOnInsert(movedinto the engine as
preserveAuditIgnoredOnInsertWarning) anddiffDroppedFields, which existed only to reconstruct the ingress strip from abefore/after payload diff and is now dead.
Command and output:
At
bd598e803this paragraph claimed that every non-CHANGELOG prose referenceoutside
protocol.tshad been corrected and that zero references to the deletedsymbol 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 ofstripReadonlyForInsertoutside CHANGELOGsnames it as deleted or as history.
Zone 2, item by item
A2.1 — anchors: CONFIRMED.
engine.insertatengine.ts:9724;stripReadonlyFieldsimported at:196; the doc sentence at:5939-5940("like every other write-path guard in this engine (
stripReadonlyFields,stripReadonlyForInsert)"); update-path call sites at:11389and:11569;the lint premise at
validate-readonly-action-writes.ts:57and:154. All fivelocated by symbol and all five matched. One addition the table did not have: a
SECOND false sentence in
assertReferencesResolveat:5983-5984("and thecreate 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, aswarned). 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:
ctx.apiisql.createContext(buildActionExecutionContext(ec))and that is
{ ...ec, isSystem: true }— the rule's own header measures thisand
packages/objectql/src/engine-repo-execute-elevation.test.tspins it. Sothe 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;readonlyWhenstill 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
insertstill keeps both values, andflagging 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 thatneither 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.
validate-readonly-flow-writes.ts(create_record) andvalidate-readonly-hook-writes.ts(ctx.api.insert) — which run NON-elevatedand where a create of a readonly column IS now a silent no-op. At
bd598e803only 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'sSTRIP_SUBJECT_METHODSdocblock (:209-212) still carried the premise, and thisparagraph 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 isposted 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_recordDOES fire now: CONFIRMED end to end. Not a unit teston the strip — a real
ObjectQLover a recording driver, registered as thedataservice of a realAutomationEngine, running a real flow(
packages/services/service-automation/src/builtin/create-record-readonly-drop.test.ts):The first case asserts
creates[0]reaches the driver withoutcompleted_at,and that
listRuns('seed')[0].steps.find(nodeId === 'mk')carriesstatus: 'success'with exactly one warning containingcreate_record(duly_task)andcompleted_at. The second asserts arunAs: '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:
engine.insertalreadystrips
autonumberviastripRuntimeOwnedFieldsunder the WIDERpreserveAuditwhitelist a historical import needs. The new static pass runsover
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 injectsreadonly: trueonto every one, sostripReadonlyFieldswould call it an author-declared lock).preserveAuditis NOT forwarded on the create path. The 2026-08-08ruling narrowed that exemption to UPDATE and left
isSystemas the createside's only one. Honouring it here — which reusing the update call shape
verbatim would have done — would hand a non-system
treatAsHistoricalimportthe 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).managedBy, thesys_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_bywrite working — a direct, non-system
engine.insertcaller.Two behaviours the move changed on purpose, both stated in the changeset:
beforeInserthooks, so it inherits the engine'sguards: 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;
defaultValueis re-derived, so a forgedapproval_statusstill becomesdraftrather than NULL — the ingress copygot that for free by running before
applyFieldDefaults; running after thehooks means asking for it explicitly.
The
warnline is also now verb-aware: on a create it says "the create is beingCOMMITTED WITHOUT IT", names
beforeInsert, and drops thepreserveAuditremedy, which cannot work there. Offering a remedy that would not have worked
is the defect already removed once from that message.
packages/specfile in this diffExtended in patch round R3 per its item 1 (the original
field.zod.tsparagraphfollows the table). Every entry is docblock /
.describe()/ ledger text — thetext face under
lanes/spec.md(「changeset 恒 patch」, 「产物随源走」): noexported symbol, authorable key or accepted value moves, proven rather than
asserted by
check:api-surfaceat485a2d525and again ata73dd85db(«public API surface + factorysignatures unchanged»).
@objectstack/specstays patch. Disposition follows theaccepted B1 shape: declared here, flagged for the spec lane, which may revert or take
any of them.
packages/spec/src/data/field.zod.tsRUNTIME_OWNED_FIELD_TYPESdocblock sentence — namesstaticReadonlyInsertSubjectpackages/spec/src/contracts/data-engine.tsstrictReadonlyWritesTSDoc, the INSERT paragraphpackages/spec/liveness/field.jsonreadonlyrow'sevidencepointer andnotepackages/spec/src/kernel/execution-context.zod.tspreserveAudit.describe(), the INSERT sentencepackages/spec/src/api/protocol.zod.tsCreateDataResponseSchema.droppedFieldsandCreateManyDataResponseSchema.droppedFields.describe();CloneDataResponseSchemaTSDocpackages/spec/src/api/batch.zod.tsBatchOperationResultSchema.droppedFields.describe()packages/spec/src/security/public-form.tsPUBLIC_FORM_SERVER_MANAGED_FIELDS— the anonymous-surface rationale sentence and the authenticated-write example (ships indist/security/index.d.ts)content/docs/references/{api/protocol,api/batch,data/data-engine,kernel/execution-context}.mdxos-regen,gen:docs)The original
field.zod.tsparagraphThis diff edits one file on
domain:spec's single-owner surface, and thechangeset releases that package (
'@objectstack/spec': patch), so it is calledout here rather than left as a diff line.
RUNTIME_OWNED_FIELD_TYPES. It describedthe 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 namesstaticReadonlyInsertSubject, whichis where that exclusion lives after this PR.
schema, no behaviour. The
patchlevel follows from that under thebump-level rule ("a
fix(that changes no public surface stayspatch").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.
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 tooktest/task-recurrence.test.tswent red on484cec193: 5 failed / 101 passed.The card's own consequence (1) is the cause — a non-system caller reaching
engine.insertno longer writes a read-only column — and the suite asserted theold contract. Two independent causes, not one:
engine.update) were NOT downstreamof the insert failure. The fixture seeded
completed_date—readonly: true,server-owned — on CREATE as a non-system caller; that seed is now stripped, so
the stored row held NULL and the object's
completed_date_requiredrulerefused the later completion. ⭐ And the seed was load-bearing only in this
file, for a reason worth stating: the harness claims to boot "the same
harness
test/task-completion-trigger.test.tsboots" and bound no hooks atall, so the app's own
beforeUpdatecompletion stamp (task.hook.ts, theexamples/app-todo: a normal user can never mark a task complete —
completed_dateisreadonly(stripped on update) andcompleted_date_requiredthen refuses the write, so the app's owncompleteTaskaction always fails #7036 remedy) never ran here. Its sibling deleted the identical create-seedwhen that stamp shipped, and drives this exact write shape green today.
engine.insert) is a fixture that must STARTfrom an already-completed row — there is no transition to stamp on.
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:
bootTodoKernelnow binds the app's owntaskHook, which its own docblockalready claimed it did. Both completion cases therefore travel the app's real
user path instead of around it.
completed_dateCREATE-seed is dropped from those two cases.context.isSystem— the remedythis PR's changeset names, and the answer the ruling gives for seeding a
server-owned column at create time.
completed_dateisreadonly(stripped on update) andcompleted_date_requiredthen refuses the write, so the app's owncompleteTaskaction always fails #7036measurement table keeps its historical fourth row (
insert already-completed (user ctx): OK) with a note that it is dated evidence and no longer a liveescape.
CHANGELOG.mdis left alone — past tense, correct as history.pnpm --filter @objectstack/example-todo test→ 4 files, 106 tests, 0 failed.Composition with #15363, which landed on
mainmid-flight65846bc46(#15363) also editspackages/metadata-protocol/src/protocol.ts. Aclean text merge is not evidence that two changes compose, so this was read
rather than assumed. They compose, and the argument is structural:
toRowApiErrorand a newisEngineDuplicateRecordEnvelopehelper (post-merge lines 1879–1925) — thefailure arm, mapping a caught
DuplicateRecordErrorto theUNIQUE_VIOLATIONwire spelling on a failed row;try(case 'create':),where the engine's
onFieldsDroppedpopulatesdroppedFieldson a row thatwrote. 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 noprotocol create face passes it:
Even had one,
ReadonlyFieldRejectedErrorfails their two-part gate(
code === 'DUPLICATE_RECORD' && name === 'DuplicateRecordError'). Measured, notinferred:
@objectstack/metadata-protocoland@objectstack/restare both greenon 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 purelyadditive 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"):
@objectstack/objectqlengine.insertdoes something new with a caller-supplied readonly field — published behaviour@objectstack/metadata-protocol@objectstack/service-automationcrud-nodes.ts; theonFieldsDroppedchannel is unchanged, only its traffic is new@objectstack/lintindex.tsdeliberately does not re-export — so no public surface widens@objectstack/specexamples/app-todoisprivate: true, so it releases nothing and takes no entry;packages/rest's only change is a test file.majorstays refused during the launch window, so breaking-ness is carried by theBREAKING banner plus the ADR-0087 disposition:
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.jsonor the upgrade guide; the remedy is application code, not a metadata migration),
and separately disposes of the retirement candidate on the measurement
(
stripReadonlyForInsertwas a bare module-privatefunction, absent from thatpackage's
index.ts, which itsexportsmap makes the only path in).node scripts/check-adr-0087-registration.mjsexits 0.Tests
(The numbers in this section are from
67d6144c5, before the merge ofmainandthe patch round; the patch round's own measurements at
66c580b0care in thePatch round section at the top.)
Union run after the final commit, at⚠️ The suite list is now
67d6144c5.derived from
turbo ls --affectedwithout droppingexamples/**andqa/**— that narrowing is what let theexample-todoregression above reachCI instead of this worktree, and the contract review named it. Every row below is
a number; ⛔ nothing is reported as "not reached".
@objectstack/objectql@objectstack/metadata-protocol@objectstack/lint@objectstack/rest@objectstack/service-automation@objectstack/runtime@objectstack/core@objectstack/spec@objectstack/metadata@objectstack/metadata-core@objectstack/platform-objects@objectstack/driver-memory@objectstack/plugin-security@objectstack/plugin-auth@objectstack/plugin-approvals@objectstack/plugin-audit@objectstack/plugin-sharing@objectstack/dogfood@objectstack/downstream-contract@objectstack/example-todo@objectstack/example-showcase@objectstack/example-crm@objectstack/example-embed-objectql@objectstack/example-multi-packageand@objectstack/refd-timer-testkitdeclareno
testscript — stated because a--filterthat matches no script exits 0having 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 casesagainst a real
ObjectQL: the card's exact repro inverted (no context,explicit
isSystem: false),onFieldsDropped, the warn line's threecreate-shaped claims,
defaultValuere-derivation, the three exemptions(
isSystem, abeforeInsertstamp, a hook stamp the caller echoed, platformobjects), the neighbouring rules (
preserveAuditrefused-and-warned on createbut still reinstating an autonumber;
readonlyWhenstill INSERT-exempt),strictReadonlyWritesrefusing with zero driver creates and a deliberatelysilent 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.ts— replacedentirely. 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— thecreate-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— mockengine swapped for a real
ObjectQL. A mock cannot strip, so the oldharness 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— thereadonly-lookup narrowing split into its two halves: the platform-object casekeeps its value and reaches the check (fixture renamed
sys_-prefixed, as thereal
sys_metadata_historyis — now load-bearing, not cosmetic), and a newauthor-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 runas its own command with its exit code captured before any pipe. 70 exit 0.
Named:
check:api-surface0 ·check:authorable-surface0 ·check:browser-reachable-entries0 ·check:changeset-gate-self-tests0 ·check:corpus-claim-drift0 ·check:cross-package-test-inputs0 ·check:dispatcher-error-vocabulary0 ·check:doc-anchors0 ·check:doc-authoring0 ·check:doc-security-posture0 ·check:docs-audit-scope0 ·check:docs-redirects0 ·check:docs-single-h10 ·check:dual-build-cjs-loads0 ·check:dual-source-exports0 ·check:durability-log-level0 ·check:empty-state0 ·check:engine-double-contract0 ·check:entry-nameability0 ·check:error-code-casing0 ·check:error-code-provenance0 ·check:error-status-conformance0 ·check:exported-any0 ·check:filter-alias-parity0 ·check:liveness0 ·check:llms-txt0 ·check:nul-bytes0 ·check:partof-closing-keyword0 ·check:pm-skill-ratchet0 ·check:published-files0 ·check:published-readme-exports0 ·check:skill-examples0 ·check:spec-changes0 ·check:strictness-ledger0 ·check:test-source-alias0 ·check:type-source-resolution0 (plus theremainder of the derived list).
Convention-scoped, invisible to the deriver by construction, asked separately
and run:
check:system-context-census--fixrepaired the line-anchor rot but not the rest: deletingstripReadonlyForInsertremoved 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-coveragecheck:type-check-debtNOT MEASURED (exit 3 or a prerequisite refusal) — reported as such, never as a pass:
check:dual-build-cjs-loadscheck:skill-examplespackages/client-reactbuilt; built and re-ran to 0check:published-readme-exportscheck:api-surface,check:dual-source-exports,check:entry-nameability,check:exported-any,check:browser-reachable-entriespackages/spec/dist; built spec and re-ran to 0check:react-declaration-paritycheck:docs-audit-scopepackages/spec/.examples-build/**artefacts left bycheck:skill-examplesaskind=contractroute sources. Removing that directory greens it. Root cause measured and posted on the existing card #15328 rather than filed againThe system-context census, in detail
content/docs/permissions/system-context.mdxis the one artefact this PR and#15319 both move, so it is spelled out.
node scripts/check-system-context-census.mjs --fixwas 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:
That is a real, ruled consequence: row 21 ("
readonlystrip bypassed — INSERT(protocol ingress)", metadata-protocol) cited the
if (context?.isSystem)lineof 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").
--fix"and nothing else" and never to hand-edit it.
--fixcannot express a censusthat 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 thislane is fencing, so it is flagged here rather than buried. Nothing near
engine.ts:5298was touched;origin/mainwas merged twice (dc46c4ec1, then638ea042d) 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(threeprotocol.tsprose ids the deletion removed) andscripts/engine-double-contract.pinned.json(the rewrittenprotocol.readonly-insert.test.tsfake, whosefindOnenow routes throughassertEngineFindOnePredicate). New prose in this diff carries no issue ids, perthe maintainer ruling of 2026-08-12 the prose-id ratchet enforces.
Out of scope, filed not fixed
premise as a scan gap (see A2.3).
check:docs-audit-scopelocal failure, postedas a comment on the existing card rather than as a duplicate.
contract, carded rather than fixed by the 2026-09-05 E3 ruling (R4 item 3).
content/docs/releases/**untouched.