Repository navigation
Commit 6c17a50
fix(lint): the runtime gate's object-write baseline keeps the written item's stored self (#22133)
Fixes #22118
Clause-②: no (the fix restores the gate's published contract:
`runtime-gate.ts` "the gate blocks new writes, never stored rows" and
`reference-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 builds `stored`: 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.
- `runRuntimeAuthoringRules` subtracts `baseline` findings, as before.
It also subtracts `stored` findings, 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.
- `isLocatedOnAnotherEntry` reads a location off the finding's path
spelling, never off its rule. It reads the three spellings the door's
rules use: positional `objects[3]…`, name-keyed `objects.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.
- `buildRuntimeWriteSnapshots` is on both package entries, so its
signature is byte-identical to `main`. It returns the baseline/candidate
pair read off the new builder. A pin asserts it still returns exactly
those two keys.
- A create, and a write of a type that is not a context collection, run
exactly the two passes they ran before.
`.changeset/22118-gate-baseline-stored-self.md`: patch,
`@objectstack/lint`, with `Clause-②: no`.
## Readings (Zone 2)
All readings are taken at the door's own snapshot shape on this branch,
unless they say otherwise.
- **H1: holds.** On `main` `51290bca`, the gate charged a label-only
`fx_master` save with `object-field-ref-unknown @
objects.fx_detail2.fields.m.lookupColumns[0]` and with
`expression-invalid @ object 'fx_detail' · field 'qty' readonlyWhen`.
The baseline printed as `[fx_account, fx_detail2]`, with the master
absent. The fingerprint is `rule · where · path · message`. Sibling
slots are identical across the passes. What differs is presence:
`lookupColumns` against an absent target is unknowable
(`validate-object-field-refs`, #20432), and the `parent` traversal
cannot resolve. So the finding is new against a baseline without the
master.
- **H2: holds.** On a create there is no stored self and no `stored`
snapshot, 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.
- **H3: holds.** Two writes that newly break a sibling are still
refused, one per spelling:
- Positional: the write removes `code`, which the detail's
`lookupColumns` names.
- Prose: the write turns `status` into a lookup, so the detail's
`readonlyWhen: "parent.status.name == 'x'"` now reads through a
reference.
- At the door, the positional control answers `{ code:
'INVALID_METADATA', status: 422 }`.
- **H4, by type:**
- **permission: the same defect, at advisory tier.**
`security-master-detail-ungranted` is 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.
- **book and dataset: no instance.** The book door runs
`validateSecurityPosture` and `validateSecurityRoleWord`. The dataset
door runs `validateDatasetMeasureAggregates` and
`validateReferenceIntegrity`. Each judges the written entry against
`permissions` or `objects` and 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:
- a finding located on another entry is the write's only when neither
the universe without the item nor the stored universe holds it;
- a finding located on the written item itself is the write's whenever
the universe without the item does not hold it, whatever the stored row
held ("re-saving a row is writing it");
- a finding whose path names no locatable entry is judged as one on the
written item.
Other changed lines:
- "runs the rules TWICE" became "on the context alone and again with the
item grafted in".
- The cost line now says "two passes … three on an update into a context
collection".
- The `restoredCredentialPaths` comment states that the stored pass can
match the item's slot, and why that is inert.
The `reference-integrity-suite.ts` sentence ("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 off `runtime-gate.object-writes.test.ts`, which PR #22103 edits):
25 cases.
- The two measured cases resolve. Each has a non-vacuity check: the
finding is absent from the baseline and present in the candidate and in
`stored`, at the same raw path.
- Both H3 controls.
- The written-object control in all three spellings. Each check confirms
the stored self carries the identical finding.
- Create, and permission relabel/create.
- Snapshot shape, and that the published builder returns only baseline
and candidate.
- Twelve location-reader cases, including five unlocatable spellings,
each answering `false`.
-
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
new block `#22118`: 4 cases through the real `saveMetaItem`, with the
registry holding the stored universe.
- Both measured cases save, and the row lands.
- The sibling-breaking write and the written object's own finding are
each refused with `{ 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, blob `625a165b` to the mutated blob).
Each leg ran `pnpm --filter @objectstack/lint build` and
`scripts/ablation-dist-preflight.mjs` before measuring: the marker was
hit in `dist/index.{js,cjs}` and `dist/runtime.{js,cjs}`. The door suite
reads `@objectstack/lint` through `dist/`.
- **Leg 1, the reversal** (stored pass disabled, which is `main`'s
behaviour):
- Lint: 3 failed / 22 passed. The failures are the two measured cases
and the permission relabel.
- Door: 2 failed / 2 passed. Both cases answered the card's own
refusals: `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]`.
- Both controls stayed green.
- **Leg 2, the location reader ablated** (every stored-pass finding
cancels):
- Lint: 3 failed / 22 passed. The failures are the written-object
control in all three spellings.
- Door: 1 failed / 3 passed. The failure is the written-object control.
- **Restore, after each leg:** blob equal to HEAD, `git diff HEAD`
empty, `dist/` rebuilt with the marker absent from all 14 built files,
and the tree clean.
## Local verification at `8d2a3c3d`
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`: 124
files, 5705 tests passed.
- `pnpm --filter @objectstack/lint typecheck` (`tsc --noEmit` plus 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.
`--listFiles` includes the edited test file (1 hit; 224 test files in
the program).
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 63 commands from the merge base.
Reconciled with `--ran`: 63 derived, 62 run green, 1 NOT MEASURED.
- `check-plugin-teardown-shape --self-test` and
`check:lean-entry-closure` first answered PREREQUISITE NOT MET: a
fixture commit was outside the shallow clone, and `objectql` had no
`dist/`. Both were re-measured green after fetching the commit and
building `objectql`.
- **NOT MEASURED:** `check:dual-build-cjs-loads`, reason: it reads the
built output of every workspace package (about 68 had no `dist/` here).
That is CI's run. A narrowed direct reading:
`packages/lint/dist/runtime.cjs` and `index.cjs` load, `runtime.cjs`
exports the same six names, and neither entry exposes
`buildRuntimeWriteSnapshotSet` or `isLocatedOnAnotherEntry`.
- Lint, narrowed to the 3 touched TS files with `eslint
--no-inline-config --format json`: 3 files, 0 errors, 0 warnings.
`eslint.config.mjs` never enables type-aware linting (0 hits for
`parserOptions.project` or `projectService`, and the config says so in
prose). So this diff cannot move the verdict for any untouched file. The
full `pnpm lint` run is CI's.
## Acceptance notes
- **book / dataset (H4):** they share the construction because it is one
line. A per-type exception would be a second policy beside the one
differential, and the next rule that judged a sibling book or dataset
against the written one would re-create this defect silently. On those
updates the stored pass costs one more rule pass, with no measured
effect today.
- **Unlocatable spellings keep the previous verdict.** No door rule
emits a double-quoted `object "x"` path today (the double-quoted form
appears only in `where` beside a positional path). A rule that did would
not get this relief until the reader learns that spelling.
- **Observation, not filed:** in this branch's probe, a detail's
`readonlyWhen: "parent.status == 'x'"` drew no door finding when the
master lacked `status`. Whether any surface judges field existence
through `parent` was not measured. Carrier: none.
- **Overlap:** PR #22103 edits `runtime-gate.object-writes.test.ts`.
This PR does not touch that file.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ec8f37c commit 6c17a50
4 files changed
Lines changed: 646 additions & 31 deletions
File tree
- .changeset
- packages
- lint/src
- metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
0 commit comments