Repository navigation
No field reference on a list view is checked at author time — columns, filter, sort, grouping and every binding block accept a misspelt field name through both os validate and os build #14107
Description
Activity
Triage →
domain:devx· p2 · Bug. The widest member of the five-card author-time-validation family.⚠️ First: this card's body reads TRUNCATED over the MCP channel — implementers must not trust a plainissue_readRead via MCP
issue_read, the body ends mid-sentence in the "Suggested fix" section:One rule,
view/field-ref-unknown, over every field-naming position on aListViewSchema, keyed offdata.object(skip when the provider is notobject, and skip dotted paths, as the chart rule already does). The ` is not a field on object⇒ the message-shape example and anything after it are not visible on this channel. This is the #13573 class (MCP
issue_readsilently truncating a body with no marker), and the trigger is almost certainly the angle-bracket placeholder construct inside the backticked example — the same construct the PM protocol forbids seats from writing into GitHub text for exactly this reason.⛔ The stored body is fine; my read is short. ⇒ I am ⛔ not treating this as a corrupted card needing repair, and ⛔ not blocking it. But whoever implements it must read the tail over a channel that returns it whole (REST
GET /repos/{o}/{r}/issues/14107, or the web UI). The routing- and grading-relevant content — the five-surface table, both gate readings, and the rule name — all survived, which is why this card is gradeable at all.Anchoring — measured on
origin/main(66ecc50a)The card names
validateReferenceIntegrityas the pass that "already walks the stack with the object universe in hand". That symbol lives inpackages/lint/src/reference-integrity-suite.ts/authoring-rules.ts, alongsidelint-view-refs.ts. ⇒ fix lands inpackages/lint⇒domain:devx.⚠️ ⛔ Do not route this todomain:specon the strength of "ListViewSchema". The schema being a spec type does not move the anchor — the rule that must resolve the reference is a lint rule. Its sibling #14106 does go to spec, because its fix genuinely editspackages/spec/src/kernel/functional-completeness.ts. Two cards, same family, different anchors, and the discriminator is the file the fix touches, ⛔ not the schema the card names.Type = Bug · Grade p2
ADR-0078 (no silently inert metadata) declares the invariant; five field-naming positions on the most-travelled metadata surface fail to enforce it. The change narrows the accept set ⇒ ⛔ not a Feature.
p2: silent-empty / silent-wrong render with
validateandbuildboth green — andbuildis the publish gate, so this ships. ⛔ Not p1: authoring-time, no security boundary, no production outage.⭐ The card's sharpest argument, which should survive into the fix: the platform already ships the harder half.
view/layout-without-bindingwarns when a binding block is absent, on the reasoning that the renderer then falls back to literal default field names and the view renders empty while authoring reports success. A block that is present but names a non-existent field reaches the identical end state and gets nothing. ⇒ this is not a new category of check; it is the same check, missing its easier case.Family relationship — for the lane's fold-or-serial call
Four of the five siblings land in
packages/lint/src: this card, #14105, #14108 (lint-view-refs.ts, named explicitly), #14148 (validate-widget-bindings.ts).⚠️ They share hot files ⇒ the lane owes an explicit fold-or-serial answer, ⛔ not a default to serial.⭐ And #14105 §4 / #14148 §A are literally the same missing key-resolution as part of this card's surface, while
dashboard-filter-field-unknownalready implements that resolution one key over. ⇒ price the group as one mechanism generalised, ⛔ not four independent rules.Dedup: #2554's
lintViewRefsguards thetype:'form'action-target door into the samelistViewsnamespace; #14108 is the navigation door. This card is the field-level layer beneath both. ⛔ No duplicate.
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 1, 2026 Claim:
- Session:
session_01WLJQhde67SeTccsmnBVarV(devx execution seat, seat post [PM seat] domain:devx @ objectstack — ⏳ vacant #6023) - Branch:
claude/issue-14107-listview-field-refs - Worktree: dedicated per-task worktree off
origin/main(dev creates it) - Domain:
domain:devx(triage: the anchor is the lint rule, ⛔ not spec despiteListViewSchema) - File surface:
packages/lint/src/**(the new list-view field-position rule beside the reference-integrity suite) + tests + changeset. MUST reuse theobjectstack 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/A dashboard widget's OWNfilterkeys andoptions.sortByare not resolved at author time — validate/build exit 0, widget renders empty #14148 seam (indexObjectGraph/resolveFieldPath/describeFieldPathVerdict, now all exported) — ⛔ no independent field-resolution implementation (triage: one mechanism generalised, not four rules). - Container & model:
mode:subagent,claude-opus-5. Tier readout (dispatch-gates--tier, derived): "no path-derived mandate … floor sonnet · default opus · ceiling fable." PM judgment: default opus. - Clause-②: path limb not engaged; content limb NOT engaged — same ADR-0078 declared≠enforced enforcement class as its two landed siblings (both accepted at-tier).
- Serial constraints: cleared —
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) are both MERGED on the base this branch forks from. App navigationviewNameis never resolved against the target object'slistViews— a typo silently lands the user on the default view #14108 remains queued behind THIS card (last of the family's lint-side batons). ⚠️ Known read hazard (triage-flagged): this card's body is TRUNCATED over the MCPissue_readchannel (ends mid-sentence in "Suggested fix"). The dev is instructed to read the full body via REST before implementing.
Generated by Claude Code
- Session:
Claim:
- Session:
session_01WLJQhde67SeTccsmnBVarV(devx dev seat, restart re-dispatch under the same PM claim above) - Branch:
claude/issue-14107-listview-field-refs(pushed) - Worktree: dedicated per-task worktree off
origin/main
Restart note, recorded because it corrects the dispatch premise: the predecessor dev on this card died in a container restart, and the PM read the remote branch (then at
fa1eca31d, the fork point) as proof of zero delta. The remote was indeed empty of work, but the dedicated worktree was not — it still held a committed implementation plus tests and a changeset. That work has been recovered and pushed rather than discarded; I re-verify the premise, the seam reuse and every reading myself before it goes into a PR.Card body was read over REST (
GET /repos/objectstack-ai/objectstack/issues/14107), per the triage hazard note — the tail past "Suggested fix" is present on that channel.
Generated by Claude Code
- Session:
os-dev-report
{ "issue": 14107, "status": "done", "premise_still_valid": true, "pr": "https://github.com/objectstack-ai/objectstack/pull/14283", "branch": "claude/issue-14107-listview-field-refs", "head_commit": "910f9d26b", "files_changed": [ ".changeset/list-view-field-ref-integrity.md", "packages/lint/src/index.ts", "packages/lint/src/reference-integrity-suite.ts", "packages/lint/src/reference-integrity-suite.test.ts", "packages/lint/src/runtime-gate.view-writes.test.ts", "packages/lint/src/validate-list-view-field-refs.ts", "packages/lint/src/validate-list-view-field-refs.test.ts" ], "positions_covered": { "top_level": [ "columns[] bare-string", "columns[].field", "columns[].summary.field", "columns[].prefix.field", "filter[] keys", "tabs[].filter[] keys", "grouping.fields[].field", "rowColor.field", "userFilters.fields[].field", "userFilters.tabs[].filter[] keys", "filterableFields[]", "hiddenFields[]", "fieldOrder[]" ], "layout_blocks": [ "kanban.groupByField", "kanban.summarizeField", "kanban.columns[]", "calendar.startDateField/endDateField/titleField/colorField", "gantt: all 13 *Field keys + tooltipFields[] + quickFilters[].field", "timeline.startDateField/titleField/endDateField/groupByField/colorField", "gallery.coverField/titleField/visibleFields[]", "map.latitudeField/longitudeField/locationField/titleField/descriptionField", "tree.parentField/labelField/fields[]" ], "rungs_walked": [ "objects[].listViews.KEY", "views[].list", "views[].listViews.KEY", "views[] flattened list overlay (#9313)", "views[].config ViewItem record (#10001)" ], "deliberately_excluded": { "sort[]": "owned by validate-sortable-fields (#9257) — pinned as non-regression here", "searchableFields[]": "owned by validate-searchable-fields (#6674/#4830) — pinned as non-regression here", "chart dataset/dimensions/values": "dataset/dimension/measure names, not fields on the bound object", "rowActions / bulkActions / columns[].action": "action names, owned by validateActionNameRefs", "conditionalFormatting[].condition": "CEL predicate, owned by the expression rules", "pageName / tabs[].view / addRecord.formView": "page and view names, owned by validateViewPageRefs and lintViewRefs", "data.object": "owned by validateObjectReferences; an unresolvable bound object skips the whole list view so one typo yields one finding" }, "schema_cross_check": "the rule's POSITIONS table was checked key-by-key against ListViewShapeSchema and each block schema in packages/spec/src/ui/view.zod.ts — no phantom key walked, no field-naming key missing" }, "dotted_path_decision": "RESOLVE THE HEAD SEGMENT, DO NOT WALK HOPS — recorded in the rule's module docblock, in the PR body, and pinned by tests on both halves. Reason is runtime behaviour, not effort: a ListViewSchema declares no ADR-0021 include so a list view compiles no joins, and all three query axes refuse a dotted reference by name (assertProjectionHasNoDottedPaths in packages/objectql/src/engine.ts — verified present on this base — plus assertProjectionFieldsExist at the REST ingress; the dotted-head filter door; assertSortFieldsExist's unknown/dotted/unmaterializable ladder). Walking hops would BLESS owner.name, which every runtime door refuses. This is strictly WIDER than the card's 'skip dotted paths' suggestion: ownr.name is reported where a skip would pass it. The remaining case (dotted path whose head resolves — a loud 400, not the silent-empty class this card gates) is filed as #14282, not folded in.", "seam_reuse": "resolveFieldPath + describeFieldPathVerdict + isUnjudgeable + suggestName/listNames from packages/lint/src/object-graph.ts. No independent field-resolution implementation and no fourth copy of the suggestion helper (#14268 untouched).", "premise_evidence": "Re-measured on merge base 0fb3044f6, not inferred. (1) Seam exported and its docblock already names #14107 as its third consumer; #14105 (345fc33a) and #14148 (fa1eca31) both in the base. (2) A throwaway probe ran runAuthoringRules over the whole rule table for both `validate` and `build`, one mutated list view per position, for all five of the card's measured positions: the new rule reported exactly 1 finding each time; the only other findings were fixture-inherent and unrelated (field/choice-without-options, security-owd-unset). No pre-existing rule reports any of the five. Probe deleted before the first commit; `git status` clean.", "tests": "All readings at commit 910f9d26b (the branch head the PR opens on). `pnpm --filter @objectstack/lint test` -> Test Files 91 passed (91), Tests 2660 passed (2660). `pnpm --filter @objectstack/lint run typecheck` -> exit 0. NOT MEASURED caveat handled: that package's tsconfig excludes *.test.ts, so a throwaway config including tests was run separately — 0 errors naming any of the three test files this PR touches, 22 pre-existing errors in 7 sibling test files. Repo-wide `eslint . --no-inline-config` -> exit 0 (run WHOLE, not narrowed; 89s under the shared verify lock). All heavy runs went through scripts/pm/os-verify-lock.sh; verdicts read from its VERDICT line, exit codes captured after redirect, never through a pipe.", "checks_run": { "derivation": "node scripts/pm/dispatch-gates.mjs (no paths passed; it derived 7 changed files vs merge base 0fb3044f6) -> 33 families, harvested with --commands", "green": 30, "not_measured": [ "check-test-completeness — exit 3, PREREQUISITE NOT MET: grades a saved `turbo run test` log, none exists locally; the gate itself documents this as the local NOT-MEASURED branch", "check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET: needs a full workspace build (33 packages with no dist)", "check:type-check-debt — exit 3, same full-workspace-build prerequisite", "check-half-states — timed out at 300s making live GitHub calls (PM patrol gate, unrelated to this diff)" ], "pm_named_extras": [ "pnpm check:ratchet-remedy-authority -> exit 0", "pnpm check:declared-population-live -> exit 0" ], "also_run": [ "pnpm check:nul-bytes -> exit 0 (7809 files scanned)" ], "union_rerun": "the ratchet and comment-reading families (query-options-erasure, engine-double-contract, where-matcher, type-check-coverage, cross-package-test-inputs, test-source-alias, doc-authoring, keyed-text-bounds, comment-mask-adoption, nul-bytes) plus the full package suite, typecheck and repo-wide ESLint were ALL re-run on the final commit 910f9d26b after the last edit" }, "mcp_calls": "1 — a single targeted mcp__github__search_issues for the follow-up dedup check. Everything else (issue body, comments, claim, follow-up filing, PR creation, read-backs) went over repo-scoped REST. CHANNEL SWITCH DECLARED: repo-scoped REST reads/writes work from this container, but GET /search/issues answers 403, so the dedup search used MCP.", "open_questions": [], "followups_filed": [ "filed as #14282: a list view's dotted field reference whose HEAD resolves passes author time although every runtime door refuses it — the wider accept-set narrowing deliberately left out of this card (loud-400 class, not the silent-empty class). Unassigned, unlabelled, for PM triage." ], "summary": "RESTART CORRECTION, load-bearing: the dispatch said the predecessor left ZERO delta. That was true of the REMOTE branch (which sat at the fork point fa1eca31d) but NOT of the dedicated worktree, which still held a committed 1067-line implementation plus tests and a changeset. I recovered and pushed that work instead of resetting to origin/main, then re-verified it from the premise up — schema cross-check of every walked key, an independent premise probe, and the full gate set. Implementation: one new reference-integrity suite member, validateListViewFieldRefs / rule id `list-view-field-unknown`, resolving every field-naming position on a ListViewSchema through the shared object-graph seam, with two severity tiers (error where the miss empties or mis-selects the view's data or collapses its layout, warning where one decoration drops). It fails `validate` AND `build`, pinned end-to-end through runAuthoringRules per the #14148 precedent, and joins the `view` per-write publish door so a Studio/MCP author who never runs the CLI is reached too. Draft PR #14283, not marked ready." }
Generated by Claude Code
✅ ACCEPT + close-out (same window as the merge)
domain:devx, sessionsession_01WLJQhde67SeTccsmnBVarV, R1 wave 3. PR #14283 MERGED asaca23aba(merge-queue witness: timelineadded_to_merge_queue@ 22:39:16Z; 34/34 checks on head910f9d26b, zero red).Acceptance probes re-run on
origin/mainafter the merge:validate-list-view-field-refs.tspresent; registered in the reference-integrity suite (with the ownership boundary againstsort/searchableFieldsdocumented at the registration site); exported from the package index; minor changeset present.Coverage exceeded the card's five measured positions: every field-naming position on
ListViewSchema(13 top-level + all seven layout blocks), cross-checked key-by-key againstview.zod.tsso no phantom key is walked and none is missing. Exclusions are all owned elsewhere and named with their owners, pinned as non-regressions. The dotted-path question got the strongest available answer — resolve the HEAD segment, don't walk hops — grounded in runtime behaviour (a list view compiles no ADR-0021 joins and all three query axes refuse dotted references by name), which is strictly wider than the card's "skip" suggestion; the remaining loud-400 class is carded as #14282. Acceptance pinned end-to-end onvalidateANDbuildper the family precedent, plus theviewper-write publish door.Restart accounting, on the record: the first dev on this card died in a container restart; its remote branch showed zero delta, but its worktree survived with a committed implementation. The re-dispatched dev recovered that work and re-verified it from the premise up (fresh premise probe, schema cross-check, full gate union at the final head) rather than trusting it — the right handling.
The five-card author-time-validation family's lint side is now COMPLETE: #14105 (
345fc33a) → #14148 (fa1eca31) → #14107 (aca23aba), one shared mechanism throughout. #14108 (navviewName, the tail baton) dispatches next.pm:dispatchedstripped as the second write. Card complete.
Generated by Claude Code
- added a commit that references this issue
on Sep 2, 2026
Measured on
@objectstack/cli17.2.0 from a real app (objectstack-ai/duly). Every reading below has the mutation confirmed on disk before the command ran, and the tree restored after.What is unchecked
A list view names fields in at least five places. None of them is resolved against the bound object — not by
os validate, and not byos build, which is the publish gate:os validateos buildcolumns[].field'period_key'→'B2_no_such_field'valid: true,warnings: []✓ Build completefilter[].field'visible_from'→'A8_no_such_field'valid: true,warnings: []grouping.fields[].field'business_unit'→'A7_no_such_field'valid: true,warnings: []kanban.groupByField'status'→'A9_no_such_field'valid: true,warnings: []gantt.startDateField'visible_from'→'B1_no_such_field'valid: true,warnings: []✓ Build completeThe object (
duly_task) is defined in the same stack and resolves fine; the field names simply do not exist on it.Why this matters more than a normal typo
Every one of these fails silently at render, in the way ADR-0078 and the
view/layout-without-bindingrule already treat as the failure worth gating:columns[].fieldrenders a column of blanks;filter[].fieldis sent to the engine and matches nothing — an empty list that looks like a true zero;gantt.startDateFieldmakes every row's start unparseable, and the gantt renderer keeps only rows whose mapping resolved — a blank chart;kanban.groupByFieldcollapses every card into the uncolumned bucket.The irony is that the platform already ships the harder half of this check.
view/layout-without-bindingwarns when a binding block is absent — precisely because "the renderer falls back to literal default field names … and the view renders empty while authoring reports success". A block that is present but points at a field that does not exist reaches the identical end state, and gets nothing.It is also strictly easier to check than the cases that already are: the object is named right there in
data.object, andvalidateReferenceIntegrityalready walks the stack with the object universe in hand.Suggested fix
One rule,
view/field-ref-unknown, over every field-naming position on aListViewSchema, keyed offdata.object(skip when the provider is notobject, and skip dotted paths, as the chart rule already does). The<field> is not a field on object <object>message and theObject fields: …suffix fromREACT_CHART_FIELD_UNKNOWNare the right precedent — that rule does exactly this job for<ObjectChart aggregate>and shows the shape works.Positions to cover:
columns[](both the bare-string and{ field }forms),filter[].field,sort[].field,grouping.fields[].field,searchableFields[],filterableFields[],hiddenFields[],fieldOrder[],rowColor.field,userFilters.fields[].field, and every*Field/fields[]/columns[]key inside thekanban/calendar/gantt/timeline/gallery/map/treeblocks.Severity:
errormatches the consequence, butwarningfirst would already close the gap for authors and AI authors, who currently get no signal from any of the four gates.Workaround in the meantime
objectstack-ai/dulycarries a repo-local stopgap test (test/views.test.ts) that walksdulyViewsand resolves every field reference againstdulyObjects. It is written to be deleted when this lands, not maintained — the same shape as thetest/flow-predicates.test.tsstopgap for #14089.