Uh oh!
There was an error while loading. Please reload this page.
fix(lint): resolve every field reference on a list view at validate and build - #14283
Conversation
Every field-naming position on a list view — columns, filter keys, grouping, rowColor, userFilters, filterableFields, hiddenFields, fieldOrder and every binding inside the kanban / calendar / gantt / timeline / gallery / map / tree blocks — was resolved by nothing, on both `os validate` and `os build`. Resolution goes through the shared object-graph seam, on the HEAD segment: a list view compiles no joins and all three runtime doors refuse a dotted reference, so walking relationship hops here would bless what the runtime refuses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…time-gate doors Suite membership plus the runtime publish-gate crossing (refusal + clean pair) for validateListViewFieldRefs, and the changeset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
The wider narrowing the module docblock defers — a dotted list-view reference whose head resolves, refused by all three runtime doors — is filed as #14282; name it so the next author can read the decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 f170075cc7645caf77aa29c0de92270082061b4f && git checkout f170075cc7645caf77aa29c0de92270082061b4f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8125673462b3c0a6d42ef5b002acd4f77c1a9789 910f9d26bdad38dd06395af1b23a03936dda3277 && git checkout -B drift-repro 8125673462b3c0a6d42ef5b002acd4f77c1a9789 && git merge --no-ff 910f9d26bdad38dd06395af1b23a03936dda3277
node scripts/docs-audit/affected-docs.mjs --json 8125673462b3c0a6d42ef5b002acd4f77c1a9789
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14107
A list view names fields in more than forty places and none of them was resolved against the bound object — not by
os validate, and not byos build, which is the publish gate. This adds one rule,list-view-field-unknown(validateListViewFieldRefs), over every one of those positions, resolving through the shared object-graph seam that #14105 and #14148 landed.Premise re-check on today's
origin/mainRe-measured on this branch's merge base (
0fb3044f6), not inferred from the card:indexObjectGraph,resolveFieldPath,describeFieldPathVerdict,isUnjudgeable,suggestNameandlistNamesare all public frompackages/lint/src/object-graph.ts, whose own docblock already names this card as its third consumer. Both family landings are in the base —objectstack validateandbuildaccept a dataset whose base object,includepath and dimension/measurefieldpaths all name nothing — the same walker already resolves date-macro tokens on the identical node #14105 (345fc33a) and A dashboard widget's OWNfilterkeys andoptions.sortByare not resolved at author time — validate/build exit 0, widget renders empty #14148 (fa1eca31).runAuthoringRules, bothvalidateandbuild) over one mutated list view per position, for each of the card's five measured positions. With the new rule present each mutation produced exactly one finding, from this rule; with it filtered out, the only remaining findings were fixture-inherent and unrelated (field/choice-without-optionson the fixture's select field,security-owd-unseton the fixture object) — no existing rule reports any of the five. The probe was a throwaway file, deleted before the first commit.sort[].fieldalready reportssort-field-unknown; a badsearchableFields[]entry already reportssearchable-field-unknown. Both are pinned as non-regression tests here, so this rule cannot start double-reporting them.Seam reuse
Resolution is
resolveFieldPath's and the message half isdescribeFieldPathVerdict's; the "did you mean" is the seam'ssuggestName. No second field-resolution implementation and no copied suggestion helper — the existing three-copy problem is carded separately as #14268 and gains no fourth copy here. The three standard skips arrive as the seam'sunknowableverdicts (object not in this stack, no readable field map, registry-injected system column), and this surface adds a fourth of its own: a list view whosedata.provideris notobjectbinds to no object graph at all.The dotted-path decision — recorded, not silent
The card (written before the seam existed) suggested skipping dotted paths "as the chart rule already does". The seam can now walk relationship hops, so this needed an explicit call. Decision: judge the HEAD segment; do not walk hops. The reason is what the runtime does, not effort:
ListViewSchemadeclares no ADR-0021include, so a list view compiles no joins — the declaration that makes hop-walking meaningful at a dataset position has no counterpart here.assertProjectionHasNoDottedPathsinpackages/objectql/src/engine.ts(verified present on this base) andassertProjectionFieldsExistat the REST ingress. Filter: the dotted-head door. Sort:assertSortFieldsExist's unknown / dotted / unmaterializable ladder.Walking hops would therefore bless
owner.namein a list view's columns — a reference every runtime door refuses — and teach an AI author that a traversal works on a surface where nothing implements it. Judging the head is also whatvalidate-sortable-fieldsalready does on this same surface.The result is strictly wider than the card's suggestion:
ownr.nameis reported (a skip would have passed it), and the finding says which segment was judged so the author reads back the string they typed. What stays unreported is a dotted path whose head does resolve — a larger accept-set narrowing whose failure mode is a loud 400 rather than the silent-empty class this card gates. Filed as #14282 rather than folded in, and both halves are pinned in tests so a later change that starts walking hops has to delete a test that states the reason.Positions covered
Top level:
columns[](bare-string and{ field }forms, plussummary.fieldandprefix.field),filter[]keys,tabs[].filter[]keys,grouping.fields[],rowColor.field,userFilters.fields[],userFilters.tabs[].filter[]keys,filterableFields[],hiddenFields[],fieldOrder[].Layout blocks: every field binding on
kanban,calendar,gantt(includingtooltipFields[]andquickFilters[].field),timeline,gallery,mapandtree. The rule'sPOSITIONStable was checked key-by-key againstListViewShapeSchemaand each block schema inpackages/spec/src/ui/view.zod.ts, so no walked key is a phantom and no field-naming key is missing.Filter keys go through
walkFilterFieldKeys, so all three authored filter shapes are judged rather than one being walked and the others silently skipped.Deliberately excluded, with reasons
sort[]andsearchableFields[]— owned byvalidate-sortable-fields(A view's declaredsortnaming aformulafield clears lint, then 400s on every load — the SORT axis has no authoring gate, unlike SEARCH (#6674) #9257) andvalidate-searchable-fields(A virtual formula field declared in searchableFields clears lint and the #4254 gate, then silently matches nothing — the same fail-open shape #4254 closed one axis over #6674/compiler/validate: view searchableFields not validated against searchable field types — a declaration typo 400s the whole list at runtime #4830), each adding a runtime-admissibility verdict this rule has no business restating.chart(dataset/dimensions/values) — dataset, dimension and measure names, not fields on the bound object; owned byvalidateChartBindings/validateDatasetReferences.rowActions/bulkActions/columns[].action— action names, owned byvalidateActionNameRefs.conditionalFormatting[].condition— a CEL predicate, owned by the expression rules.pageName/tabs[].view/addRecord.formView— page and view names, owned byvalidateViewPageRefsandlintViewRefs.data.objectitself — owned byvalidateObjectReferences. When the bound object does not resolve, this rule skips the whole list view, so one typo yields one finding rather than one per position (pinned).Severity — two tiers, the
validateFlowTemplatePathsprecedenterrorwhere the miss changes what data the view returns or collapses the layout it configures (every position in the card's measured table);warningwhere the renderer drops one decoration and renders the rest (optional colour/title/tooltip/cover bindings, a stalehiddenFieldsorfieldOrderentry).Acceptance —
validateANDbuild, pinned end-to-endFollowing the #14148 precedent, the acceptance block drives
runAuthoringRulesfor both commands rather than inferring the answer from the registry entry: each of the card's five measured positions failsvalidateand failsbuild, the fully-bound list view passes both, and a warning-tier position advises rather than gates. The rule also joins the reference-integrity suite'sviewruntime type, so the standalone list view a Studio tenant or an MCP author writes through the per-write publish door is judged too — that crossing is argued in the suite's own granularity-wall test.Verification
All readings below are from commit
910f9d26b, the branch head this PR opens on.pnpm --filter @objectstack/lint test— 91 files, 2660 tests passed.pnpm --filter @objectstack/lint run typecheck— clean. Note the package'stsconfig.jsonexcludes*.test.ts, so that run says nothing about the test files; a throwaway config including them was run separately and produced zero errors naming any of the three test files this PR adds or edits (the 22 errors it did report are pre-existing, in seven sibling test files).node scripts/pm/dispatch-gates.mjs, 33 families, harvested with--commands): 30 green, 3 NOT MEASURED because they refuse without inputs this worktree does not have —check-test-completeness(exit 3, needs a savedturbo run testlog; the gate documents this branch as the local NOT-MEASURED case),check:dual-build-cjs-loads(exit 3, needs a full workspace build),check:type-check-debt(exit 3, same).check-half-statestimed out at 300s making live GitHub calls and is likewise recorded as NOT MEASURED — none of the four is a red, and none is a finding about this diff.check:ratchet-remedy-authority,check:declared-population-live,check:nul-bytes.pnpm lint(repo-wide ESLint,eslint . --no-inline-config) — clean, run whole rather than narrowed.Changeset:
minorfor@objectstack/lint— an accept-set narrowing, matching the family precedent set by #14105 and #14148.This branch was recovered rather than restarted: the predecessor dev on this card died in a container restart with its work committed in the worktree but never pushed, so the remote branch read as empty. The recovered commit was re-verified here from the premise up before this PR was opened.
Generated by Claude Code