Skip to content

feat(lint): key collection-resident publish-gate finding paths by name, not the private snapshot index - #10296

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-10064-name-keyed-issue-paths
Aug 20, 2026
Merged

feat(lint): key collection-resident publish-gate finding paths by name, not the private snapshot index#10296
qq9340100 merged 2 commits into
mainfrom
claude/issue-10064-name-keyed-issue-paths

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes #10064

Implements the maintainer ruling on #10064 (2026-08-20, Option A): a runtime publish refusal's issues[].path (and the 2xx advisories[].path) for collection-resident write types (object / permission / book) now keys the top-level collection entry by NAMEobjects.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-sha pin (9a3daf8d37ad973a621e5edd276fe32467f90684, via git grep <sha> — no checkout switch): no Studio/objectui consumer parses the path string, so Option A lands one-step with no compat window.

  • packages/data-objectstack/src/metadata-client.ts:107typeof c.path === 'string' presence guard only.
  • packages/app-shell/src/providers/saveAdvisoryToast.ts:70-78formatFinding deliberately does NOT render path (its docblock: "a document pointer meant for tooling").
  • packages/app-shell/src/preview/capabilityLint.ts — formats `${f.where}: ${f.message}`; path unused (the objects[0] literal in its test is inert fixture data).
  • packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx:127-140 parses 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 in packages/lint/src/data-model-rules.ts. Measured against origin/main, that assumption under-covers and mis-locates:

  • The observed objects[417].sharingModel finding is produced by validate-security-posture.ts, not data-model-rules.ts; the objects[${i}] spelling spans ~15 rule files (plus permissions[${i}] / books[${i}]).
  • Rules also serve the CLI (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: runRuntimeAuthoringRules in packages/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 and packages/lint/src/validate-security-posture.test.ts is untouched (its :434 pin included).

Design points, each pinned by a test:

  • Applied AFTER the differential. The fingerprint set-diff still keys on raw positional paths, where two entries can never collide — two stored items that (illegitimately) share a name must not have their distinct findings merged or cancelled by the rewrite.
  • Nested positions stay positional (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.
  • Single-member write types keep their trivially-stable form (flows[0].nodes[1]…) — existing pins in runtime-gate.test.ts (flows[0], views[0], dashboards[0]) pass unchanged and double as the proof.
  • Fallback, never a hole: an entry that is missing, unnamed, or whose name will not splice into a dotted path keeps the positional spelling (pinned).
  • Context-resident findings name-key too: a permission write's finding attributed to a context object carries an equally unresolvable snapshot offset, and is rewritten by the same mechanism (pinned).

Also in this PR (per the ruling)

  • RuntimeAuthoringIssueSchema.path describe now states the convention (packages/spec/src/api/protocol.zod.ts; content/docs/references/api/protocol.mdx regenerated via check:generated --fixgen:docs; gen:schema artifacts proved not stale by the wrapper).
  • The feat(lint,metadata-protocol): judge a package write against its own closure #10058 bench's differential comparison reads the semantic tuple by default (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, lint dist rebuilt with the fix and marker nameKeyFindingPath confirmed present in dist/runtime.js before the run): differential verdict: UNCHANGED on both shapes — the index-only +1 / -1 phantom is gone from the shape rather than normalised away.
  • Prose that narrated the indexed form updated: data-model-rules.ts docblock, AuthoringFinding.path TSDoc.
  • Changeset: @objectstack/lint minor (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/spec patch (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's issues[].path carries an array index into the gate's private snapshot (objects[417].sharingModel) that no caller can resolve #10064 pins + 1 updated pin in validate-security-posture.runtime-surface.test.ts).
  • Reverse verification (fix committed first, 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 positional objects[2].sharingModel / objects[1].fields.invoice / objects[0].sharingModel — 2128 others green; fix restored via git checkout HEAD -- (index + worktree), tree clean.
  • pnpm --filter @objectstack/lint typecheck, pnpm --filter @objectstack/spec typecheck: pass.
  • Downstream consumers (closure built first): @objectstack/metadata-protocol 124 files / 1708 passed, @objectstack/objectql full suite 221 files / 3921 passed.
  • node scripts/pm/dispatch-gates.mjs (no paths — the script derives the 9-path change set itself) union at 784693d9a: 29/29 local gates green (TOTAL-FAILURES=0), including the convention-triggered test-file set (query-options-erasure, type-check-coverage, type-check-debt on the built closure, engine-double-contract, where-matcher) and check:nul-bytes. CI runs the full farm regardless.

Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 20, 2026 14:31
… 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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

7 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/data-model-rules.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 115 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bf7cc13025fc7c7e53da09488f61106141b98af7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 90644d3f6b61c2784ce3653861d957d7fcf7e54a — the merge of head 784693d9ac5d2b9fdd8bab47f52cd3af9b301119 into base bf7cc13025fc7c7e53da09488f61106141b98af7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@qq9340100
qq9340100 marked this pull request as ready for review August 20, 2026 15:28
@qq9340100
qq9340100 enabled auto-merge August 20, 2026 15:28
@qq9340100
qq9340100 added this pull request to the merge queue Aug 20, 2026
Merged via the queue into main with commit def0d3e Aug 20, 2026
26 checks passed
@qq9340100
qq9340100 deleted the claude/issue-10064-name-keyed-issue-paths branch August 20, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants