feat(lint): key collection-resident publish-gate finding paths by name, not the private snapshot index - #10296
Conversation
… gate The 422/advisory wire path for object/permission/book writes now keys the top-level collection entry by NAME (objects.acme_invoice.sharingModel) instead of the gate's private per-write snapshot index (objects[417].~), which no caller can resolve. Rules keep emitting positional paths; the rewrite happens once, after the differential, in runtime-gate.ts. Fixes #10064 (maintainer ruling 2026-08-20: Option A) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gcKVsiywU9CcS96S5t9qD
…ple; changeset + regenerated docs With the wire path name-keyed (#10064), the raw (rule, where, path, message) tuple IS the semantic tuple, so the bench's differential-verdict line reads it by default; an index-insensitive canary prints only when a positional-fallback path moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gcKVsiywU9CcS96S5t9qD
📓 Docs Drift Check7 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 115 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 90644d3f6b61c2784ce3653861d957d7fcf7e54a && git checkout 90644d3f6b61c2784ce3653861d957d7fcf7e54a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bf7cc13025fc7c7e53da09488f61106141b98af7 784693d9ac5d2b9fdd8bab47f52cd3af9b301119 && git checkout -B drift-repro bf7cc13025fc7c7e53da09488f61106141b98af7 && git merge --no-ff 784693d9ac5d2b9fdd8bab47f52cd3af9b301119
node scripts/docs-audit/affected-docs.mjs --json bf7cc13025fc7c7e53da09488f61106141b98af7 |
Fixes #10064
Implements the maintainer ruling on #10064 (2026-08-20, Option A): a runtime publish refusal's
issues[].path(and the 2xxadvisories[].path) for collection-resident write types (object/permission/book) now keys the top-level collection entry by NAME —objects.acme_invoice.sharingModel— instead of by the gate's private per-write snapshot index (objects[417].sharingModel), which no caller can resolve.First measurement — the ruling's stated confidence gap
Read-only sweep of the objectui checkout at this repo's
.objectui-shapin (9a3daf8d37ad973a621e5edd276fe32467f90684, viagit grep <sha>— no checkout switch): no Studio/objectui consumer parses thepathstring, so Option A lands one-step with no compat window.packages/data-objectstack/src/metadata-client.ts:107—typeof c.path === 'string'presence guard only.packages/app-shell/src/providers/saveAdvisoryToast.ts:70-78—formatFindingdeliberately does NOT renderpath(its docblock: "a document pointer meant for tooling").packages/app-shell/src/preview/capabilityLint.ts— formats`${f.where}: ${f.message}`;pathunused (theobjects[0]literal in its test is inert fixture data).packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx:127-140parses the metadata-save 422's Zod document paths (array-join + prefix trim against the saved parent document), not gate snapshot paths; a gate-shaped path falls through the prefix trim unchanged under either spelling.Where the fix lives — and why not in the rules (PM mechanism assumption refuted in part)
The dispatch assumed the fix edits the
objects[${i}]construction sites inpackages/lint/src/data-model-rules.ts. Measured againstorigin/main, that assumption under-covers and mis-locates:objects[417].sharingModelfinding is produced byvalidate-security-posture.ts, notdata-model-rules.ts; theobjects[${i}]spelling spans ~15 rule files (pluspermissions[${i}]/books[${i}]).os validate/os lint), where the positional index resolves against the author's own config file and is legitimate; the unresolvable index exists only at the runtime gate, whose snapshot the caller never sees.So the rewrite lives at the one seam that owns the wire shape:
runRuntimeAuthoringRulesinpackages/lint/src/runtime-gate.ts(nameKeyFindingPath). Rules keep emitting positional paths; the CLI is byte-identical; every gate-fed rule — security-posture included — is covered by one mechanism. This also keeps the H17 fence intact: no rule severity is touched andpackages/lint/src/validate-security-posture.test.tsis untouched (its:434pin included).Design points, each pinned by a test:
objects.acme_invoice.indexes[1]): within one named item they index the author's own document, which the receiver holds — the ruling targets only the top-level collection index no caller can resolve.flows[0].nodes[1]…) — existing pins inruntime-gate.test.ts(flows[0],views[0],dashboards[0]) pass unchanged and double as the proof.Also in this PR (per the ruling)
RuntimeAuthoringIssueSchema.pathdescribe now states the convention (packages/spec/src/api/protocol.zod.ts;content/docs/references/api/protocol.mdxregenerated viacheck:generated --fix→gen:docs;gen:schemaartifacts proved not stale by the wrapper).scripts/bench/runtime-publish-gate.bench.mts): with the wire path name-keyed, the raw(rule, where, path, message)tuple IS the semantic tuple, so one default reading replaces the raw/semantic pair; an index-insensitive canary prints only when a positional-fallback path moved. Re-check (--type object --mode package --objects 420, lintdistrebuilt with the fix and markernameKeyFindingPathconfirmed present indist/runtime.jsbefore the run):differential verdict: UNCHANGEDon both shapes — the index-only+1 / -1phantom is gone from the shape rather than normalised away.data-model-rules.tsdocblock,AuthoringFinding.pathTSDoc.@objectstack/lintminor (wire-visible change in the emitted finding-path spelling; the accepted metadata set is unchanged, nothing an author writes is removed or renamed — not breaking, and the objectui sweep above confirms no consumer parses the old spelling),@objectstack/specpatch (describe text only). No ADR-0087 marker needed: the changeset is not declared-breaking.Verification (at
784693d9a)pnpm --filter @objectstack/lint test: 77 files, 2132 passed | 5 skipped (includes 4 new finding: a publish refusal'sissues[].pathcarries an array index into the gate's private snapshot (objects[417].sharingModel) that no caller can resolve #10064 pins + 1 updated pin invalidate-security-posture.runtime-surface.test.ts).git restore --source=origin/main -- packages/lint/src/runtime-gate.ts, rerun the two pinned files): exactly the 4 name-keying pins fail in the predicted direction — observed positionalobjects[2].sharingModel/objects[1].fields.invoice/objects[0].sharingModel— 2128 others green; fix restored viagit checkout HEAD --(index + worktree), tree clean.pnpm --filter @objectstack/lint typecheck,pnpm --filter @objectstack/spec typecheck: pass.@objectstack/metadata-protocol124 files / 1708 passed,@objectstack/objectqlfull suite 221 files / 3921 passed.node scripts/pm/dispatch-gates.mjs(no paths — the script derives the 9-path change set itself) union at784693d9a: 29/29 local gates green (TOTAL-FAILURES=0), including the convention-triggered test-file set (query-options-erasure,type-check-coverage,type-check-debton the built closure,engine-double-contract,where-matcher) andcheck:nul-bytes. CI runs the full farm regardless.Generated by Claude Code
Generated by Claude Code