Repository navigation
fix(lint): the runtime gate's object-write baseline keeps the written item's stored self - #22133
Conversation
… item's stored self On an update into a context collection the gate now also judges the stored universe (the baseline with the written item's stored self at the slot the item takes in the candidate). A finding located on another entry that the stored universe already holds is no longer charged to the write; findings on the written item itself, and findings whose path names no locatable entry, are judged against the baseline alone, as before. Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
…ontrols, create and permission Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
…ave door Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
…stored universe is the gate's own `buildRuntimeWriteSnapshots` is on both package entries, so its return type stays the baseline/candidate pair. The construction moves to the module-level `buildRuntimeWriteSnapshotSet`, which the gate and the pins read. Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
Conflict in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts only: #22118's stored-self block and #22042's nested-predicate block were both appended after the pass-2 block. Both are kept whole, #22118's first. Claude-Session: https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 4 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 7cff3624ed980190fd7f5b1c9eb8d9bc3ad4efd8 && git checkout 7cff3624ed980190fd7f5b1c9eb8d9bc3ad4efd8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8fc50b7647d30db2a0837877cf251c1163a3239e 11234a7e55ea008e4b77179d14ef6f8edff82c22 && git checkout -B drift-repro 8fc50b7647d30db2a0837877cf251c1163a3239e && git merge --no-ff 11234a7e55ea008e4b77179d14ef6f8edff82c22
node scripts/docs-audit/affected-docs.mjs --json 8fc50b7647d30db2a0837877cf251c1163a3239e |
Fixes #22118
Clause-②: no (the fix restores the gate's published contract:
runtime-gate.ts"the gate blocks new writes, never stored rows" andreference-integrity-suite.ts"a stored object already in violation is never charged to someone else's write"; the refusal it removes is one that text already denies)What changed
The runtime publish gate judged a stored sibling's finding against a baseline that had dropped the written object's stored self. A label-only save of a master was refused (422) for a stored detail the author never touched. The gate now also judges the stored universe on an update into a context collection.
packages/lint/src/runtime-gate.ts:buildRuntimeWriteSnapshotSet(module-level, on neither package entry) builds the baseline and candidate as before. On an UPDATE into a context collection it also buildsstored: the baseline with the written item's stored self put back, at the slot the item takes in the candidate. Every sibling sits at the same index in all three snapshots.runRuntimeAuthoringRulessubtractsbaselinefindings, as before. It also subtractsstoredfindings, but only those whose path positively names another entry (isLocatedOnAnotherEntry). Findings located on the written item are never read from that pass, so they are judged as before.isLocatedOnAnotherEntryreads a location off the finding's path spelling, never off its rule. It reads the three spellings the door's rules use: positionalobjects[3]…, name-keyedobjects.acme_invoice…, and an object named in prose (object 'fx_detail' · …, the path the expression and autonumber rules emit). A path it cannot locate keeps the previous verdict. That direction can leave a sibling's finding charged to a write; it can never wave the written item's own finding through.buildRuntimeWriteSnapshotsis on both package entries, so its signature is byte-identical tomain. It returns the baseline/candidate pair read off the new builder. A pin asserts it still returns exactly those two keys..changeset/22118-gate-baseline-stored-self.md: patch,@objectstack/lint, withClause-②: no.Readings (Zone 2)
All readings are taken at the door's own snapshot shape on this branch, unless they say otherwise.
main51290bca, the gate charged a label-onlyfx_mastersave withobject-field-ref-unknown @ objects.fx_detail2.fields.m.lookupColumns[0]and withexpression-invalid @ object 'fx_detail' · field 'qty' readonlyWhen. The baseline printed as[fx_account, fx_detail2], with the master absent. The fingerprint isrule · where · path · message. Sibling slots are identical across the passes. What differs is presence:lookupColumnsagainst an absent target is unknowable (validate-object-field-refs, [finding] a misspelt field name in a field'srelatedListColumns,lookupColumns,lookupFilters[].fieldordependsOnpasses every authoring door, and fails only at view or picker time #20432), and theparenttraversal cannot resolve. So the finding is new against a baseline without the master.storedsnapshot, so the verdict is unchanged. It is pinned: creating the master beside the stored detail is still charged with the detail's finding, because the stored universe never held it.code, which the detail'slookupColumnsnames.statusinto a lookup, so the detail'sreadonlyWhen: "parent.status.name == 'x'"now reads through a reference.{ code: 'INVALID_METADATA', status: 422 }.security-master-detail-ungrantedis silent while no permission set is authored. A label-only re-save of a tenant's only set was therefore charged a stored detail's warning:objects.fx_line.fields.hdr, measured red under the reversal below. With this change it carries none; creating the same set still reports it. Covered, because the construction is the one shared line.validateSecurityPostureandvalidateSecurityRoleWord. The dataset door runsvalidateDatasetMeasureAggregatesandvalidateReferenceIntegrity. Each judges the written entry againstpermissionsorobjectsand resolves nothing into a sibling of its own collection, so the stored pass cancels nothing there today. See the Acceptance notes.The contract sentence (narrowed to what holds)
The module header now states what "added" means. The bare sentence "the gate blocks new writes, never stored rows" used to sit in the builder docblock. It now heads a precise list, every item of which the code does:
Other changed lines:
restoredCredentialPathscomment states that the stored pass can match the item's slot, and why that is inert.The
reference-integrity-suite.tssentence ("a stored object already in violation is never charged to someone else's write") sits in a paragraph about the FLOW snapshot, where it was and remains true. It is untouched.Tests
packages/lint/src/runtime-gate.stored-self-baseline.test.ts(new; it keeps offruntime-gate.object-writes.test.ts, which PR feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103 edits): 25 cases.stored, at the same raw path.false.packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, new block#22118: 4 cases through the realsaveMetaItem, with the registry holding the stored universe.{ code: 'INVALID_METADATA', status: 422 }and the issue's path, and nothing lands.Reverse verification (one-off, at
8d2a3c3d, no permanent ablation file)Both legs went through
scripts/ablation-replace.mjs, with a literal anchor that must hit (x1 to x0, blob625a165bto the mutated blob). Each leg ranpnpm --filter @objectstack/lint buildandscripts/ablation-dist-preflight.mjsbefore measuring: the marker was hit indist/index.{js,cjs}anddist/runtime.{js,cjs}. The door suite reads@objectstack/lintthroughdist/.main's behaviour):object/fx_master failed author-time validation: 1 issue — objects.fx_detail2.fields.m.lookupColumns[0] [object-field-ref-unknown]and… object 'fx_detail' · field 'qty' readonlyWhen [expression-invalid].git diff HEADempty,dist/rebuilt with the marker absent from all 14 built files, and the tree clean.Local verification at
8d2a3c3dpnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 124 files, 5705 tests passed.pnpm --filter @objectstack/lint typecheck(tsc --noEmitplus the test layer): OK. No new test-typecheck signature.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 221 files passed, 3 skipped; 28243 tests passed, 19 skipped.pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.--listFilesincludes the edited test file (1 hit; 224 test files in the program).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands from the merge base. Reconciled with--ran: 63 derived, 62 run green, 1 NOT MEASURED.check-plugin-teardown-shape --self-testandcheck:lean-entry-closurefirst answered PREREQUISITE NOT MET: a fixture commit was outside the shallow clone, andobjectqlhad nodist/. Both were re-measured green after fetching the commit and buildingobjectql.check:dual-build-cjs-loads, reason: it reads the built output of every workspace package (about 68 had nodist/here). That is CI's run. A narrowed direct reading:packages/lint/dist/runtime.cjsandindex.cjsload,runtime.cjsexports the same six names, and neither entry exposesbuildRuntimeWriteSnapshotSetorisLocatedOnAnotherEntry.eslint --no-inline-config --format json: 3 files, 0 errors, 0 warnings.eslint.config.mjsnever enables type-aware linting (0 hits forparserOptions.projectorprojectService, and the config says so in prose). So this diff cannot move the verdict for any untouched file. The fullpnpm lintrun is CI's.Acceptance notes
object "x"path today (the double-quoted form appears only inwherebeside a positional path). A rule that did would not get this relief until the reader learns that spelling.readonlyWhen: "parent.status == 'x'"drew no door finding when the master lackedstatus. Whether any surface judges field existence throughparentwas not measured. Carrier: none.runtime-gate.object-writes.test.ts. This PR does not touch that file.Generated by Claude Code