Skip to content

feat(lint): a standalone ViewItem record's nested config.sort / config.searchableFields reach the runtime publish gate (#10001) - #10558

Merged
os-elon merged 4 commits into
mainfrom
claude/issue-10001-viewitem-record-config-rung
Aug 21, 2026
Merged

feat(lint): a standalone ViewItem record's nested config.sort / config.searchableFields reach the runtime publish gate (#10001)#10558
os-elon merged 4 commits into
mainfrom
claude/issue-10001-viewitem-record-config-rung

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10001

A standalone ViewItem RECORD — ViewMetadataSchema's member 1 (ViewItemWireSchema, { name, object, viewKind: 'list', config }), the shape a Studio-saved view takes through PUT /api/v1/meta/view and the shape objectui's updateView round-trips on every pin/reorder toggle ({ ...current, ...partial }, the #5074 trace) — is now judged by validateSortableFields and validateSearchableFields on its nested config.sort / config.searchableFields. Before this rung, a record write carrying config.sort: [{ field: 'amout' }] published through the runtime gate in silence and answered 400 INVALID_SORT (#6994/#7095) on the view's first fetch, every load.

Walk-only, as the card scoped it: #9313 (PR #10234, squash adbcbfd86) already widened the reference-integrity suite entry and exactly these two members onto view writes, so the dispatch existed and the record rung was the missing half. The per-member granularity wall is untouched: no further member crosses onto view (the member-surface pin and the container behavioural control both still pass unchanged).

The rung (both twin walks, mirrored)

In each walk's views[] loop, the structural complement of #9313's self rung:

  • recogniser: view.viewKind === 'list' && isRec(view.config) — the wire union's own record/overlay member discrimination (the overlay rung keeps its !isRec(view.config) guard; a strict container carries neither key; a form record's FormViewSchema config declares no list-field surface);
  • binding: listViewObject(view.config) ?? viewObject — the config's own data.object (ADR-0047 explicit retarget; a record's data binding lives on its config) ahead of the record's required top-level object;
  • path: views[i].config.sort[…] / views[i].config.searchableFields[…], where-label view "…" (ViewItem record);
  • the record's top LEVEL is deliberately still not read: a stray sort riding beside config in the stored body (saveMetaItem persists the original) is a key the wire schema strips and no runtime read path serves — judging it would be a false-positive channel. Pinned by the rung-split controls.

The boundary marker, flipped (the anti-vacuity anchor)

runtime-gate.view-writes.test.ts carried the #9313 marker — "a ViewItem RECORD's nested config.sort is not judged here — recorded scope, not a rung that fell off" — asserting errors == [] on a record with a bad config.sort. That test is replaced by the record-rung block; the first commit of this branch (3c07cebfd, tests only) is the walks at origin/main state plus the flipped assertions, and the run at that commit is the required before-state:

Tests 10 failed | 86 passed (96)
FAIL src/runtime-gate.view-writes.test.ts > … > REFUSES a record's `config.sort` naming
an unknown field — the flipped boundary marker (#10001)
AssertionError: []: expected undefined to be defined

The [] in the assertion message is the gate's own answer: the bad record published clean on origin/main. All 10 reds are #10001 tests; all 86 pre-existing tests (including the positive controls below) passed on the origin/main walks.

The three controls, with before-states

  1. Defect control (red before, green after) — the flipped marker above plus the record refusal tests (unknown field / formula sort / stale searchable entry, exact views[0].config.* paths, retarget honored). Before-state: fail on origin/main walks (verbatim output above). After dc2d43620: green.
  2. Positive control over preserved behaviour (green before AND after) — "the flattened overlay is judged exactly once, on its top-level path — no record-rung leak": exactly one sort-field-unknown at views[0].sort[0] on a bad overlay (a config-rung leak moves the path; a double judgment adds a finding). Unit-level census twin: one bad sort per rung in a single stack (object listViews, container list, overlay, record) must yield exactly the four declared paths. The overlay/objects[].listViews/views[].list refusal + clean tests from [finding] Both field-resolution gates for a list view (searchableFields and sort) are CLI-only — the reference-integrity suite dispatches at the runtime publish gate for flow alone, so a Studio/MCP view write gets neither #9313 are byte-unchanged and pass in both runs. Plus the rung-split controls: a record carrying both a bad config.sort and a stray bad top-level sort yields exactly ONE finding, on the config path; a form record with a stray sort inside its config is not judged.
  3. Container control (green before AND after) — the [finding] Both field-resolution gates for a list view (searchableFields and sort) are CLI-only — the reference-integrity suite dispatches at the runtime publish gate for flow alone, so a Studio/MCP view write gets neither #9313 behavioural control is unchanged and passing: a container view write naming stack-level actions is not refused (validateActionNameRefs never reaches the view door), and the member-surface pin still asserts exactly ['validateSearchableFields', 'validateSortableFields'] declare view.

False-positive budget (#4716 discipline) — record-shaped corpus, population stated

Population: 39 record-shaped console round-trip bodies — one per shipped list surface in the four shipped stacks (examples/app-crm 9 = 3 container list + 6 container listViews.*; examples/app-showcase 26 = 6 + 20; examples/app-todo 3 = 1 + 2; packages/qa/downstream-contract 1 = 1 container list; the stacks ship zero object-level listViews and zero defineViewItem records, so the container surfaces are the whole shipped list population — the same 39 surfaces #9313's lane B enumerated, record-shaped this time). Each body is the shape saveMetaItem really stores after a Studio save + console pin toggle: { name, object: bound object (config data.objectahead of the owner), viewKind: 'list', label, config: the surface's stored list config verbatim withsort[].idcarryingcrypto.randomUUID() row ids (#5074), isPinned, sortOrder }, replayed through the real dispatch path (runRuntimeAuthoringRules, type: 'view', context = the stack's own objects/permissions/books/datasets).

  • 0 refusals, 0 advisories over 39 bodies (at dd90178ad). A lower bound, as every authored corpus is.
  • Anti-vacuity leg: a known-bad record body through the same harness refuses — sort-field-unknown at views[0].config.sort[0].
  • Re-derivation: enumerate objects[].listViews.* + views[].list + views[].listViews.* across the four stack configs, wrap each as above, replay. The replay script imports the lint package source in-package (the same resolution the package's own tests use — no dist in the path).

Verification (all at dd90178ad, the final commit, after merging origin/main ceb33a9f1)

  • pnpm --filter @objectstack/lint test: Tests 2194 passed (2194), TESTEXIT=0. pnpm exec tsc --noEmit (packages/lint): clean, TSCEXIT=0. All exits captured before any pipe (cmd > log 2>&1; EXIT=$?).
  • Spec moved on the incoming merge side, so: pnpm --filter @objectstack/spec build + check:generated — "All 14 generated artifacts are up to date."
  • Gate union from node scripts/pm/dispatch-gates.mjs (no path args; change set derived by the script from merge base ceb33a9f1): 9 path-matched + 5 convention-triggered (new test files) + check:nul-bytes from standing orders — all exit 0. Verdict lines, each quoted from the gate's own output:
    • check:changeset-gate-self-tests — "check-changeset-no-major --self-test: 116 assertions … + the wiring"
    • check:cross-package-test-inputs (both invocations) — "OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob."
    • check:objectui-changeset — "objectui-range --self-test: all checks passed"
    • check:slot-lookup — "slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new"
    • check-adr-0087-registration — "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)."
    • check-changeset-no-major — "This diff introduces no major bump."
    • check-empty-changeset — exit 0
    • check-affected-docs — "affected-docs self-test: 281 cases pass." (exit 0; the unreachable-rows table it prints is its standing repo report, not a finding on this diff)
    • check:query-options-erasure — "test surface: 240 site(s) in 47 file(s) — at the ceiling … baseline key set verified against ceb33a9: no files added."
    • check:type-check-coverage — "OK — 64/77 workspace packages type-checked (plus the root), 13 in the DEBT ledger (436 frozen raw errors …), 1 exempt."
    • check:type-check-debt (after turbo run build --filter=./packages/* --filter=./packages/*/*, BUILDEXIT=0) — "--re-measure: OK — 33 ledger entr(ies) re-measured in 344.3s, 1924 raw tsc error(s) total, none above its recorded number. surplus: none"
    • check:engine-double-contract — "OK — 342 pinned, 133 in the DEBT ledger, 2 exempt."
    • check:where-matcher — "269 matcher(s) discovered, 269 answer the combinator battery correctly or refuse it loudly (161 refuse). 0 silently-wrong and 0 unjudged … none new."
    • check:nul-bytes — "OK (scanned 6179 text file(s) … no raw ASCII control bytes)."
  • No baseline raised; every ratchet sits at its recorded number. check:engine-split-ratio is report-only and this diff does not move engine code.

Deliberately not done


Generated by Claude Code

…ig.sort/searchableFields must be judged (#10001)
The three record-rung refusal tests, the retarget/clean/round-trip
controls, and the rung-split positive controls. Red on origin/main's
walks by design — the walk rungs land in the following commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
…bleFields reach the runtime publish gate (#10001)
The RECORD rung in both twin walks: a views[] entry carrying
viewKind 'list' AND a record-shaped config — ViewMetadataSchema's
member 1 (ViewItemWireSchema), the Studio-saved-view shape through
PUT /api/v1/meta/view — is judged on config.sort / config.searchableFields
against listViewObject(config) ?? entry.object, path views[i].config.*.
The #9313 dispatch widening already routes view writes to exactly these
two members; this closes the recorded follow-up scope, walk-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

5 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/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))
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 68ca346446a47d94a1b60e7061a2a5dc60b29d27packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 68ca346446a47d94a1b60e7061a2a5dc60b29d27 → 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/mteststooling

Projects

None yet

2 participants

@os-elon@claude