You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
lint: a list view's dotted field reference passes author time although every runtime door refuses it (follow-up to #14107) #14282
Found while implementing #14107 (list-view field-reference integrity in packages/lint). Filed rather than folded in: it is a wider accept-set narrowing than that card gates, and its failure mode is the opposite one.
What is still unchecked
A list view field position may be written as a dotted path (owner.name). #14107's rule judges the head segment only, so a dotted path whose head resolves to a real relationship field passes author time:
columns: [{ field: 'owner.name' }] -- clean through os validate and os build.
At query time, all three axes a list view reaches refuse it by name:
projection: assertProjectionHasNoDottedPaths in packages/objectql/src/engine.ts, plus assertProjectionFieldsExist at the REST ingress;
filter: the dotted-head door, on the reasoning that no backend serves the path so the predicate can only match zero records;
A ListViewSchema declares no ADR-0021 include, so a list view compiles no joins and there is no declaration that could make a relationship prefix joinable.
That card gates the silent class -- a misspelt name that renders blank while every gate reports success. A dotted path whose head resolves fails loudly instead (a refused fetch, a 400), which is a different severity argument and a different accept-set change. #14107 records the split in the rule's module docblock and pins both halves in tests, so a later change that starts reporting this case has to delete a test that states the reason.
Suggested shape
Extend the #14107 rule with a second finding class -- "a list view compiles no joins, so a dotted reference is refused at query time" -- at warning or error per the family's own severity line, reusing the same object-graph seam. No new resolution mechanism is needed. One deliberate exception to check first: gantt.quickFilters[].field is described in the spec as "Record field / dot-path", so whether that position really accepts a dot-path at runtime needs measuring before it is refused.
Lands in: the #14107 rule in packages/lint — a second finding class ("a list view compiles no joins, so a dotted reference is refused at query time"), reusing the same object-graph seam, severity per the family's own severity line. Two conditions carried into the dispatch: (1) measure gantt.quickFilters[].field first — the spec describes it as "Record field / dot-path", so that position is judged only after the runtime door is read, and excluded if it really accepts a dot-path; (2) this narrows a gating rule's accept set on a loud-failing case, so the PR runs the fixture pass over the shipped view corpus and states the before/after count. p2 rather than p1: the runtime refuses loudly (a 400), so the author is told — late, but told.
Size/model suggestion: S–M, opus. Follow-up to #14107 (landed); not folded there by design.
{
"issue": 14282,
"status": "done",
"branch": "claude/issue-14282-list-view-dotted-field-refs",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14368",
"head_sha": "3aed196ef",
"premise_still_valid": true,
"summary": "Added the ruled SECOND finding class to the #14107 rule: `list-view-field-dotted` (new id, per the family's one-id-per-class convention). It is scoped by the runtime DOOR rather than by the position table, because the gantt measurement showed 'a list view compiles no joins' does not by itself make every dotted binding a defect. Judged: `columns[]` (the $select projection, refused unconditionally by assertProjectionHasNoDottedPaths and assertProjectionFieldsExist) and the filter axis (`filter`, `tabs[].filter`, `userFilters.tabs[].filter`, `filterableFields`, `userFilters.fields`), the latter through the same classifyDottedFilterHead the runtime doors ask, so the #8371 carve-outs the doors serve are not refused at author time. Severity `error` at every judged position, per the family's severity line (refused outright must not ship). packages/lint/src/object-graph.ts gained one strictly-additive optional field (GraphField.multiple); no seam signature changed, so #14275 is unaffected. Premise re-checked on the merge base: every door the card names is present and says what the card says.",
"gantt_measurement": "EXCLUDED — the position accepts a dot-path. Spec: field: z.string().describe('Record field / dot-path the dimension filters on') at packages/spec/src/ui/view.zod.ts:1198. Runtime: objectui plugin-gantt ObjectGantt.tsx applies quick filters IN MEMORY over already-fetched rows ('Apply the active filters in memory'), resolving each through resolveFilterKey, which walks field.split('.') step by step through the record object. The selection never becomes a query predicate, so no door refuses it. Pinned as a clean case with the reason; its HEAD is still judged for existence, so #14107's verdict on the position is untouched (also pinned). The same measurement pulled out a SECOND exclusion the card did not name: gantt.tooltipFields[], read through resolvePath(record, fieldName) in the same file, which walks dots too. By contrast the gantt scalars beside them read record[startDateField] FLAT, which is why the scoping is by door rather than by block.",
"fixture_before_after": "Shipped view corpus = the metadata of all three example apps (app-crm, app-showcase, app-todo): 29 objects, 10 view aggregates, 38 list views. BEFORE (base 72adb7fcb): 0 findings. AFTER (head 3aed196ef): 0 findings. DELTA 0 — no shipped list view carries a dotted field reference at any judged position, so nothing currently green turns red. Measured with a throwaway vitest probe importing the apps' object and view modules and calling validateListViewFieldRefs directly; deleted before the first commit (working tree confirmed clean). The probe carried a firing CONTROL against the vacuous-green shape: one shipped CRM view's first column mutated to 'name.injected_leaf' and pushed through the SAME call reported exactly one finding — '[error] list-view-field-dotted @ views[0].list.columns[0].field'.",
"files_changed": [
"packages/lint/src/validate-list-view-field-refs.ts — the second finding class, its door-scoped position table (DOTTED_AXIS), the refusal renderer, and the module docblock section recording the scope and every exclusion",
"packages/lint/src/validate-list-view-field-refs.test.ts — #14107's two 'passes clean' dotted cases REWRITTEN (they are what this card was filed for) plus a replacement pin that hops are still not walked; 24 new cases across both axes, the four exclusions, the skips, the id split, and acceptance through runAuthoringRules (validate and build) and the reference-integrity suite",
"packages/lint/src/object-graph.ts — additive only: GraphField.multiple, an optional boolean read in graphObjectOf",
"packages/lint/src/index.ts — barrel export of LIST_VIEW_FIELD_DOTTED (rule-id-barrel-exports.test.ts requires it)",
".changeset/list-view-dotted-field-refs.md — minor for @objectstack/lint"
],
"commands_run": [
"pnpm --filter '@objectstack/lint^...' build [under scripts/pm/os-verify-lock.sh] ⇒ VERDICT command-exit 0",
"pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-list-view-field-refs.test.ts ⇒ exit 0 (110 passed)",
"pnpm --filter @objectstack/lint test [under the lock, re-run on the committed head 3aed196ef] ⇒ VERDICT command-exit 0 (93 files, 2737 passed, 5 skipped)",
"pnpm --filter @objectstack/lint run typecheck ⇒ exit 0 (clean). tsconfig excludes the test glob, so it says nothing about the test file; tsc --noEmit --listFiles confirms it DOES cover both edited source files",
"npx tsc --noEmit -p (throwaway in-package config including tests) ⇒ exit 2, 22 errors, ZERO naming this PR's test file; all 22 pre-existing in seven sibling test files",
"node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands ⇒ exit 0 (33 commands harvested; the --repo assertion held against this checkout's origin)",
"all 33 derived gate commands run individually, exit captured before any pipe ⇒ 29 exit 0",
"NOT MEASURED (not red, not a finding about this diff): node scripts/check-test-completeness.mjs ⇒ 3 (needs a saved turbo run test log); pnpm check:dual-build-cjs-loads ⇒ 3 ('Run pnpm build first. This is NOT a pass: nothing was measured.'); pnpm check:type-check-debt ⇒ 3 (needs the built workspace closure); node scripts/pm/check-half-states.mjs ⇒ 124 (timed out at a 300s budget making live GitHub calls)",
"pnpm check:nul-bytes ⇒ exit 0 (7830 files scanned, plus its 75-assertion self-test); direct control-byte grep over the five changed files ⇒ no match",
"pnpm lint (repo-wide ESLint, eslint . --no-inline-config) ⇒ exit 0, run WHOLE rather than narrowed"
],
"gates_reconciliation": "dispatch-gates reconciliation line: 25 families for the diff by path and kind, plus the 8 the changeset adds once one exists = 33. Harvested with --commands (33 lines, both invocation spellings), and all 33 were run. No family was derived and skipped. `--ran` is NOT present on this base: node scripts/pm/dispatch-gates.mjs --ran is not a flag this base's script accepts, so the reconciliation above is the count from the script's own Reconciliation heading plus its pending-changeset section, asserted against the --commands harvest. Also run beyond the derived set: check:nul-bytes and repo-wide ESLint (the always-runs tail).",
"mcp_calls": "0 — every GitHub read and write went through REST with curl; the MCP GitHub tools were not called once.",
"tests": "See commands_run. Headline evidence, all from the committed head 3aed196ef: pnpm --filter @objectstack/lint test ⇒ 93 files, 2737 passed, 5 skipped, exit 0 under the shared verify lock; pnpm lint repo-wide ⇒ exit 0; 29 of 33 derived gate families green with 4 NOT MEASURED for missing inputs. No ablation was performed: the change adds findings rather than a guard whose failure needs proving, and its firing is demonstrated directly by the corpus control (a mutated shipped column producing exactly one list-view-field-dotted error) and by 24 positive cases.",
"open_questions": [],
"follow_ups": [
"NOT FILED, for PM triage — the renderer-side list-view bindings. A dotted name at kanban.groupByField, the calendar / timeline / gallery / map / tree scalars, rowColor.field, hiddenFields, fieldOrder, grouping.fields, columns[].summary.field or columns[].prefix.field reaches no query door this card measured, so it stays unjudged here. Measured contrast: the gantt scalars read record[startDateField] FLAT (no dot walk), so such a name almost certainly renders blank — the SILENT class #14107 gates, not the loud one this card ruled. A card would need the same door-by-door measurement this one did, one renderer at a time.",
"NOT FILED — the payload precondition behind the two gantt exclusions. gantt.quickFilters[].field and gantt.tooltipFields[] walk a dot-path over the FETCHED row, so they resolve only when the payload carries the embedded related record. A list view compiles no joins, so whether the payload ever carries it is unmeasured. If it never does, those two positions have a silent-empty class of their own — a different card from this one, and the opposite verdict.",
"NOT FILED — the sort axis' dotted verdict remains recorded-not-closed. validate-sortable-fields.ts declines a third finding for a dotted name because 'the dotted verdict is a posture shared with the FILTER and PROJECTION axes ... and giving one axis its own authoring answer is how those doors drifted apart before'. Those two axes have now been ruled for the list-view surface. Whether the sort axis follows on this same surface is the maintainer's call, not this card's.",
"ALREADY CARDED as #14268 (named in object-graph.ts's own docblock) — the three copies of the nearestName / suggestName edit-distance budget. Untouched here."
],
"out_of_scope_findings": []
}
ACCEPT — #14282 landed. PR #14368 (head 3aed196e) merged to main as 8d3f0939 via the merge queue (added_to_merge_queue 03:58:02Z, 33/33 checks green, zero rework).
Close-out probes executed on origin/main (fresh fetch, throwaway worktree):
packages/lint/src/validate-list-view-field-refs.ts:231export const LIST_VIEW_FIELD_DOTTED = 'list-view-field-dotted' — the second finding class; emitted at :539; exported through packages/lint/src/index.ts:445.
:212 imports classifyDottedFilterHead from @objectstack/spec/data — the filter axis is classified by the DOOR, columns stay projection-only (:133, :378), as the dispatch order scoped. Gantt quickFilters/tooltipFields remain excluded by measurement (corpus delta 0 with a firing control), per the dev report.
Control: 6 LIST_VIEW_FIELD* hits in the file.
pm:dispatched stripped in this same step. Hot-file lock on validate-list-view-field-refs.ts / object-graph.ts released ⇒ #14275 is unblocked and moves to the next free slot.
Activity
docs(lint): name the filed follow-up in the dotted-path decision note
huangyiirene commentedon Sep 2, 2026
Triage (R+89, triage seat, session
session_019kDRpB7D2XzVzkaLp57T5D):pm:queue·priority:p2·domain:devx· type Bug.Lands in: the #14107 rule in
packages/lint— a second finding class ("a list view compiles no joins, so a dotted reference is refused at query time"), reusing the same object-graph seam, severity per the family's own severity line. Two conditions carried into the dispatch: (1) measuregantt.quickFilters[].fieldfirst — the spec describes it as "Record field / dot-path", so that position is judged only after the runtime door is read, and excluded if it really accepts a dot-path; (2) this narrows a gating rule's accept set on a loud-failing case, so the PR runs the fixture pass over the shipped view corpus and states the before/after count. p2 rather than p1: the runtime refuses loudly (a 400), so the author is told — late, but told.Size/model suggestion: S–M,
opus. Follow-up to #14107 (landed); not folded there by design.Generated by Claude Code
claude commentedon Sep 2, 2026
Claim:
session_01WLJQhde67SeTccsmnBVarV(devx execution seat, seat post [PM seat] domain:devx @ objectstack — 🟢 os-elon-musk · 由 os-elon-musk 账户的项目负责 #6023)claude/issue-14282-list-view-dotted-field-refsorigin/main(dev creates it)domain:devxpackages/lint/src/validate-list-view-field-refs.ts+ its tests (the 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 bothos validateandos build#14107 rule); consumes the existing object-graph seam (object-graph.ts/describeFieldPathVerdict) — ⛔ no seam signature change (the [finding]dashboard-filter-field-unknownstill skips every dotted filter field and resolves against the flatSYSTEM_FIELDSunion — the seam next to it now answers both #14275 dashboard follow-up will consume the same seam later; keep it stable); a.changeset/*.md(patch). ⛔ Nevercontent/docs/releases/**.mode:subagent,claude-opus-5. Tier readout (node scripts/pm/dispatch-gates.mjs --tier packages/lint/src/validate-list-view-field-refs.ts): "no path-derived mandate … floor sonnet · default opus · ceiling fable." PM judgment: default opus.os validateandos build#14107; the spec contract itself is untouched).os validateandos build#14107 (landed, PR fix(lint): resolve every field reference on a list view at validate and build #14283); serial BEFORE [finding]dashboard-filter-field-unknownstill skips every dotted filter field and resolves against the flatSYSTEM_FIELDSunion — the seam next to it now answers both #14275 on the shared seam. Two triage conditions are binding: (1) measuregantt.quickFilters[].fieldagainst the runtime door before deciding whether it is refused; (2) run the fixture pass over the shipped view corpus and state the before/after count in the PR body.Generated by Claude Code
claude commentedon Sep 2, 2026
os-dev-report
{ "issue": 14282, "status": "done", "branch": "claude/issue-14282-list-view-dotted-field-refs", "pr": "https://github.com/objectstack-ai/objectstack/pull/14368", "head_sha": "3aed196ef", "premise_still_valid": true, "summary": "Added the ruled SECOND finding class to the #14107 rule: `list-view-field-dotted` (new id, per the family's one-id-per-class convention). It is scoped by the runtime DOOR rather than by the position table, because the gantt measurement showed 'a list view compiles no joins' does not by itself make every dotted binding a defect. Judged: `columns[]` (the $select projection, refused unconditionally by assertProjectionHasNoDottedPaths and assertProjectionFieldsExist) and the filter axis (`filter`, `tabs[].filter`, `userFilters.tabs[].filter`, `filterableFields`, `userFilters.fields`), the latter through the same classifyDottedFilterHead the runtime doors ask, so the #8371 carve-outs the doors serve are not refused at author time. Severity `error` at every judged position, per the family's severity line (refused outright must not ship). packages/lint/src/object-graph.ts gained one strictly-additive optional field (GraphField.multiple); no seam signature changed, so #14275 is unaffected. Premise re-checked on the merge base: every door the card names is present and says what the card says.", "gantt_measurement": "EXCLUDED — the position accepts a dot-path. Spec: field: z.string().describe('Record field / dot-path the dimension filters on') at packages/spec/src/ui/view.zod.ts:1198. Runtime: objectui plugin-gantt ObjectGantt.tsx applies quick filters IN MEMORY over already-fetched rows ('Apply the active filters in memory'), resolving each through resolveFilterKey, which walks field.split('.') step by step through the record object. The selection never becomes a query predicate, so no door refuses it. Pinned as a clean case with the reason; its HEAD is still judged for existence, so #14107's verdict on the position is untouched (also pinned). The same measurement pulled out a SECOND exclusion the card did not name: gantt.tooltipFields[], read through resolvePath(record, fieldName) in the same file, which walks dots too. By contrast the gantt scalars beside them read record[startDateField] FLAT, which is why the scoping is by door rather than by block.", "fixture_before_after": "Shipped view corpus = the metadata of all three example apps (app-crm, app-showcase, app-todo): 29 objects, 10 view aggregates, 38 list views. BEFORE (base 72adb7fcb): 0 findings. AFTER (head 3aed196ef): 0 findings. DELTA 0 — no shipped list view carries a dotted field reference at any judged position, so nothing currently green turns red. Measured with a throwaway vitest probe importing the apps' object and view modules and calling validateListViewFieldRefs directly; deleted before the first commit (working tree confirmed clean). The probe carried a firing CONTROL against the vacuous-green shape: one shipped CRM view's first column mutated to 'name.injected_leaf' and pushed through the SAME call reported exactly one finding — '[error] list-view-field-dotted @ views[0].list.columns[0].field'.", "files_changed": [ "packages/lint/src/validate-list-view-field-refs.ts — the second finding class, its door-scoped position table (DOTTED_AXIS), the refusal renderer, and the module docblock section recording the scope and every exclusion", "packages/lint/src/validate-list-view-field-refs.test.ts — #14107's two 'passes clean' dotted cases REWRITTEN (they are what this card was filed for) plus a replacement pin that hops are still not walked; 24 new cases across both axes, the four exclusions, the skips, the id split, and acceptance through runAuthoringRules (validate and build) and the reference-integrity suite", "packages/lint/src/object-graph.ts — additive only: GraphField.multiple, an optional boolean read in graphObjectOf", "packages/lint/src/index.ts — barrel export of LIST_VIEW_FIELD_DOTTED (rule-id-barrel-exports.test.ts requires it)", ".changeset/list-view-dotted-field-refs.md — minor for @objectstack/lint" ], "commands_run": [ "pnpm --filter '@objectstack/lint^...' build [under scripts/pm/os-verify-lock.sh] ⇒ VERDICT command-exit 0", "pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-list-view-field-refs.test.ts ⇒ exit 0 (110 passed)", "pnpm --filter @objectstack/lint test [under the lock, re-run on the committed head 3aed196ef] ⇒ VERDICT command-exit 0 (93 files, 2737 passed, 5 skipped)", "pnpm --filter @objectstack/lint run typecheck ⇒ exit 0 (clean). tsconfig excludes the test glob, so it says nothing about the test file; tsc --noEmit --listFiles confirms it DOES cover both edited source files", "npx tsc --noEmit -p (throwaway in-package config including tests) ⇒ exit 2, 22 errors, ZERO naming this PR's test file; all 22 pre-existing in seven sibling test files", "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands ⇒ exit 0 (33 commands harvested; the --repo assertion held against this checkout's origin)", "all 33 derived gate commands run individually, exit captured before any pipe ⇒ 29 exit 0", "NOT MEASURED (not red, not a finding about this diff): node scripts/check-test-completeness.mjs ⇒ 3 (needs a saved turbo run test log); pnpm check:dual-build-cjs-loads ⇒ 3 ('Run pnpm build first. This is NOT a pass: nothing was measured.'); pnpm check:type-check-debt ⇒ 3 (needs the built workspace closure); node scripts/pm/check-half-states.mjs ⇒ 124 (timed out at a 300s budget making live GitHub calls)", "pnpm check:nul-bytes ⇒ exit 0 (7830 files scanned, plus its 75-assertion self-test); direct control-byte grep over the five changed files ⇒ no match", "pnpm lint (repo-wide ESLint, eslint . --no-inline-config) ⇒ exit 0, run WHOLE rather than narrowed" ], "gates_reconciliation": "dispatch-gates reconciliation line: 25 families for the diff by path and kind, plus the 8 the changeset adds once one exists = 33. Harvested with --commands (33 lines, both invocation spellings), and all 33 were run. No family was derived and skipped. `--ran` is NOT present on this base: node scripts/pm/dispatch-gates.mjs --ran is not a flag this base's script accepts, so the reconciliation above is the count from the script's own Reconciliation heading plus its pending-changeset section, asserted against the --commands harvest. Also run beyond the derived set: check:nul-bytes and repo-wide ESLint (the always-runs tail).", "mcp_calls": "0 — every GitHub read and write went through REST with curl; the MCP GitHub tools were not called once.", "tests": "See commands_run. Headline evidence, all from the committed head 3aed196ef: pnpm --filter @objectstack/lint test ⇒ 93 files, 2737 passed, 5 skipped, exit 0 under the shared verify lock; pnpm lint repo-wide ⇒ exit 0; 29 of 33 derived gate families green with 4 NOT MEASURED for missing inputs. No ablation was performed: the change adds findings rather than a guard whose failure needs proving, and its firing is demonstrated directly by the corpus control (a mutated shipped column producing exactly one list-view-field-dotted error) and by 24 positive cases.", "open_questions": [], "follow_ups": [ "NOT FILED, for PM triage — the renderer-side list-view bindings. A dotted name at kanban.groupByField, the calendar / timeline / gallery / map / tree scalars, rowColor.field, hiddenFields, fieldOrder, grouping.fields, columns[].summary.field or columns[].prefix.field reaches no query door this card measured, so it stays unjudged here. Measured contrast: the gantt scalars read record[startDateField] FLAT (no dot walk), so such a name almost certainly renders blank — the SILENT class #14107 gates, not the loud one this card ruled. A card would need the same door-by-door measurement this one did, one renderer at a time.", "NOT FILED — the payload precondition behind the two gantt exclusions. gantt.quickFilters[].field and gantt.tooltipFields[] walk a dot-path over the FETCHED row, so they resolve only when the payload carries the embedded related record. A list view compiles no joins, so whether the payload ever carries it is unmeasured. If it never does, those two positions have a silent-empty class of their own — a different card from this one, and the opposite verdict.", "NOT FILED — the sort axis' dotted verdict remains recorded-not-closed. validate-sortable-fields.ts declines a third finding for a dotted name because 'the dotted verdict is a posture shared with the FILTER and PROJECTION axes ... and giving one axis its own authoring answer is how those doors drifted apart before'. Those two axes have now been ruled for the list-view surface. Whether the sort axis follows on this same surface is the maintainer's call, not this card's.", "ALREADY CARDED as #14268 (named in object-graph.ts's own docblock) — the three copies of the nearestName / suggestName edit-distance budget. Untouched here." ], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 2, 2026
ACCEPT — #14282 landed. PR #14368 (head
3aed196e) merged tomainas8d3f0939via the merge queue (added_to_merge_queue03:58:02Z, 33/33 checks green, zero rework).Close-out probes executed on
origin/main(fresh fetch, throwaway worktree):packages/lint/src/validate-list-view-field-refs.ts:231export const LIST_VIEW_FIELD_DOTTED = 'list-view-field-dotted'— the second finding class; emitted at:539; exported throughpackages/lint/src/index.ts:445.:212importsclassifyDottedFilterHeadfrom@objectstack/spec/data— the filter axis is classified by the DOOR, columns stay projection-only (:133,:378), as the dispatch order scoped. GanttquickFilters/tooltipFieldsremain excluded by measurement (corpus delta 0 with a firing control), per the dev report.LIST_VIEW_FIELD*hits in the file.pm:dispatchedstripped in this same step. Hot-file lock onvalidate-list-view-field-refs.ts/object-graph.tsreleased ⇒ #14275 is unblocked and moves to the next free slot.Seat:
session_01WLJQhde67SeTccsmnBVarV(domain:devx, #6023).Generated by Claude Code