Skip to content

feat(lint): the two list-view field rules reach the flattened view overlay at the runtime publish gate (#9313) - #10234

Merged
os-elon merged 4 commits into
mainfrom
claude/issue-9313-reference-integrity-view-surface
Aug 21, 2026
Merged

feat(lint): the two list-view field rules reach the flattened view overlay at the runtime publish gate (#9313)#10234
os-elon merged 4 commits into
mainfrom
claude/issue-9313-reference-integrity-view-surface

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9313

A standalone list view written through PUT /api/v1/meta/view — the only door a Studio tenant or an MCP/AI author has — is now judged by validateSearchableFields and validateSortableFields. Both halves the card names, because either alone is a silent no-op:

  • dispatch: the reference-integrity suite's registry entry gains runtimeTypes: ['flow', 'view'], and the suite dispatches its members by the gate-passed ctx.runtimeWriteType (ReferenceIntegrityRule.runtimeTypes, default ['flow']);
  • walk: both rules gain the SELF rung — a views[] entry that IS a flattened list overlay (viewKind: 'list', no nested config), the shape ViewMetadataSchema's list-overlay member takes on the wire and the shape the gate snapshots as views: [item]. Recogniser verified against the wire schema's own member discrimination in packages/spec/src/ui/view.zod.ts (record = viewKind AND nested config; container is strict and cannot carry viewKind).

Provenance — a resumption, not a fresh authorship

The core of this PR is f2e1822ce, the original dev's own commit, rescued unmodified by the domain:engine PM seat after that dev died on the Fable-5 quota mid-wrap-up (issue comment 5342539755). This session (devx seat, maintainer instruction 「9313 插队」, running the required claude-fable-5 tier) merged origin/main (8d1fa008), re-created the untracked changeset verbatim from the rescue record, then audited and verified everything below. Amendments of my own are confined to 2c119f2c and are labelled as such.

The granularity decision, argued explicitly (a domain:engine judgement — hence draft)

The card reserved "widen the whole suite vs split the entry" as the engine seat's decision and sanctioned the split path only if argued. The rescued commit takes the split: the entry's runtimeTypes says which writes dispatch the suite; a new per-member runtimeTypes axis (default ['flow'], i.e. the frozen #4463 P1 surface) says which members judge that per-write snapshot. A view write reaches exactly the two list-view field rules, which resolve only against stack.objects — a collection RuntimeStackContext carries.

Why the whole suite is the wrong granularity for this door — measured, and the failure differs per body shape (both ways wrong):

  • On a containerview write, validateActionNameRefs (error-tier) resolves views[].list / views[].listViews.* action names against stack.actions, which no per-write snapshot carries. Measured: a container naming a stack-level action yields action-name-undefined on the snapshot shape and is clean against the full stack — a phantom 422 on a legitimate write. (Latent in the shipped corpus: 0 of 11 shipped containers name a stack-level action today.)
  • On a flattened overlay, the same rule has no rung at all, so crossing it would be a silent no-op that reads as coverage — the very shape this card was filed about. Measured by ablation: crossing it left the flattened-overlay control green.

The rescued commit's comments claimed only the first channel; my audit measured both and corrected the comments (authoring-rules.ts, reference-integrity-suite.ts) and the test controls to match.

Wiring ratchet: authoring-rule-wiring.test.ts is entry-level and holds — the rescued commit updates its runtimeAuthoringRulesFor('view') census to ['validatePresetComparands', 'validateReferenceIntegrity', ...FAMILY], and its invariants (surface question, declared types, snapshot mapping, same-array identity) all pass. The member-level axis is pinned by runtime-gate.view-writes.test.ts ("exactly the two list-view field rules declare view"; "every member still judges flow snapshots") plus the behavioural container control below. The #7220 family invariant is untouched: the two list-view field rules cross together.

Proof obligation — the walk rung discriminates (ablation, both legs confirmed on disk)

Resolution path stated: every test in packages/lint/src imports the rules relatively in-package (./validate-sortable-fields.js), so vitest executes the mutated source directly — no exports map, no dist, no alias in the path; neither leg needs a package build. Each leg's mutation/restoration was proven by grep counts on the anchored text (injected spelling and original spelling, both directions), not by editor exit codes.

  1. Walk rung removed, dispatch widening kept (guard short-circuited to false in both walk files; disk proof 1/1 injected, 0/0 original): 8 tests red — the three gate refusal tests + the data.object retarget test in runtime-gate.view-writes.test.ts, plus 2 rung unit tests in each walk file. Everything else stayed green. "the rule now dispatches for view" alone is therefore measurably NOT this card — the refusal tests fail without the rung.
  2. Restore (disk proof 0/0 injected, 1/1 original): full package suite green — Tests 2153 passed (2153), TESTEXIT=0.
  3. Granularity wall ablated (validateActionNameRefs member widened onto view; disk proof 1/0): 2 tests red — the member-surface pin AND the new container-shaped behavioural control. The rescued commit's original flattened-overlay control stayed green under this crossing (its comment claimed it was "THE control" — measured vacuous as a discriminator, which is why 2c119f2c adds the container control and re-labels the flattened one as a regression pin).
  4. Restore (disk proof 0/1): suite green.

False-positive budget (#4716 discipline; the #9313 claim re-derived)

Population stated: every shipped view-door body derivable from this repo — (A) all 11 defineView containers registered in the four shipped stacks (examples/app-crm, examples/app-showcase, examples/app-todo, packages/qa/downstream-contract), replayed verbatim as view writes against each stack's own objects; (B) 39 console-shaped personalization overlays — one per shipped list surface (object listViews.*, container list, container listViews.*): stored config flattened to the top level, identity bound, sort[].id carrying crypto.randomUUID() row ids (#5074), isPinned/sortOrder riding along — i.e. the shape saveMetaItem really stores, not the authoring shape.

  • A+B: 0 refusals, 0 advisories over 50 shipped view-door bodies. A lower bound, as every authored corpus is.
  • Anti-vacuity: a known-bad body through the same harness refuses (sort-field-unknown), and the six-refusal shape is pinned permanently in runtime-gate.view-writes.test.ts.
  • ⚠️ The rescued changeset claimed 102 bodies (10 containers + 92 overlays); that population is not reproducible from this tree and the measurement script died with the original worktree. The changeset now carries the re-derived, reproducible numbers (50 = 11 + 39) — the claim shrank, the verdict (0/0) is unchanged.

Lane C — reported separately, not part of the shipped-body budget: a synthetic console column-sort PUT for every displayed column of every shipped list surface (180 PUTs) yields 3 refusals, all sort-field-unsortable on formula columns shipped in grids (crm_opportunity.expected_revenue ×2, showcase_project.budget_remaining). Pre-widening, those PUTs persisted and the sort silently returned unordered rows under a success (the rule's own measured message); post-widening the PUT is refused loudly. No stored shipped body sorts by a formula field, so the rollback clause is not triggered — but this is the hot-path behaviour change quantified, and whether objectui's grid should stop offering the click on a formula column is recorded as an open question on the issue.

Verification (all at 2c119f2c, after the final commit)

  • pnpm exec vitest run (packages/lint): Tests 2153 passed (2153); pnpm exec tsc --noEmit: clean (TESTEXIT=0 TSCEXIT=0, captured before any pipe).
  • Gate union from node scripts/pm/dispatch-gates.mjs (no path args — variable set derived by the script from the merge base), all 15 exit 0, each verdict quoted from the gate's own output, including: check-cross-package-test-inputs "OK: 12 package(s) read outside themselves, all declared"; check:slot-lookup "ratchet holds: 107 unswept site(s) …, none new"; check:type-check-debt "33 ledger entr(ies) re-measured …, none above its recorded number / surplus: none"; check:engine-double-contract "OK — 331 pinned, 133 in the DEBT ledger, 2 exempt"; check:where-matcher "0 silently-wrong and 0 unjudged …; none new"; check:nul-bytes "OK (scanned 6076 text file(s) …)". Baselines: none raised; every ratchet sits at its recorded number.
  • CLI behaviour unchanged: CLI callers pass no runtimeWriteType, so whole-stack runs keep the full suite (pinned by the flow-door control and the "every member still judges flow" assertion).

Draft status

⛔ Deliberately draft and not to be flipped ready by this seat: the per-member granularity split is a domain:engine judgement (issue comment 5342067628); it ships only with that seat's or the maintainer's acknowledgement.

Generated by Claude Code


Generated by Claude Code

…erlay at the runtime publish gate (#9313)
Both halves the card names, because either alone is a silent no-op:
- dispatch: the reference-integrity suite's registry entry gains
runtimeTypes view; the suite dispatches its MEMBERS by the gate-passed
ctx.runtimeWriteType (default flow), so a view snapshot reaches exactly
validateSearchableFields + validateSortableFields — never
validateActionNameRefs, whose stack.actions universe no per-write
snapshot carries (the measured phantom-refusal channel that makes
whole-suite granularity wrong for this door);
- walk: both rules gain the SELF rung — a views[] entry that IS a
flattened list overlay (viewKind 'list', no nested config), the
PUT /api/v1/meta/view shape — judging top-level sort/searchableFields
with the same binding order and skips as every other list-view surface.
CLI commands pass no runtimeWriteType and keep the full suite; flow
snapshots keep every member (default declaration). The ViewItem RECORD
shape (config.sort one level down) is recorded scope, not walked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
The changeset was untracked when the original dev died on the fable quota;
its text was preserved verbatim in issue comment 5342539755. Re-created
from that block, not re-derived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…ped phantom-channel pin (#9313)
Measured by ablation: crossing validateActionNameRefs onto view left the
flattened-overlay rides-along control GREEN, because that rule walks
views[].list / views[].listViews.* and has no flattened-overlay rung — on
that shape the crossing is a silent no-op, not a phantom refusal. The
channel is real on CONTAINER view writes (measured: action-name-undefined
on the per-write snapshot, clean on the full stack), so the behavioural
control now uses a container body naming stack-level actions, and the
source comments state the two measured failure shapes instead of one.
Changeset FP numbers re-derived on this tree: 0 refusals / 0 advisories
over 50 shipped view-door bodies (11 containers + 39 console-shaped
personalization overlays) across four authoring lineages — the rescued
text claimed 102 (10+92); the measured population replaces it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 15 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/plugin-endpoints.mdx(via /api/v1/meta/view (route))
  • content/docs/concepts/metadata-lifecycle.mdx(via /api/v1/meta/view (route))
  • content/docs/deployment/validating-metadata.mdx(via AUTHORING_RULES (symbol), runtimeTypes (symbol))
  • content/docs/kernel/services-checklist.mdx(via /api/v1/meta/view (route))
  • content/docs/protocol/objectui/index.mdx(via /api/v1/meta/view (route))
  • content/docs/ui/forms.mdx(via /api/v1/meta/view (route))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via AUTHORING_RULES (symbol), runtimeTypes (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 — 4 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 68f65ff6048dba9c28bdbc708b789ae529eb8fa3packageMentionDocs.

Which tree this was computed on

This run read content/docs from f9832f51354ed1e4761038abc78c3f696dc079ce — the merge of head 2c119f2ccde372125c795e345c00b66481164ee7 into base 68f65ff6048dba9c28bdbc708b789ae529eb8fa3, 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 f9832f51354ed1e4761038abc78c3f696dc079ce && git checkout f9832f51354ed1e4761038abc78c3f696dc079ce
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 68f65ff6048dba9c28bdbc708b789ae529eb8fa3 2c119f2ccde372125c795e345c00b66481164ee7 && git checkout -B drift-repro 68f65ff6048dba9c28bdbc708b789ae529eb8fa3 && git merge --no-ff 2c119f2ccde372125c795e345c00b66481164ee7
node scripts/docs-audit/affected-docs.mjs --json 68f65ff6048dba9c28bdbc708b789ae529eb8fa3

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 68f65ff6048dba9c28bdbc708b789ae529eb8fa3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

3 participants

@os-zhuang@os-elon@claude