Skip to content

lint: a list view's dotted field reference passes author time although every runtime door refuses it (follow-up to #14107) #14282

Closed

Description

@claude

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;
  • sort: assertSortFieldsExist's unknown / dotted / unmaterializable ladder.

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.

Why it was left out of #14107

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.


Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    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) 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.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Sep 2, 2026
  3. added theissue type on Sep 2, 2026
  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 2, 2026
  5. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    Claim:


    Generated by Claude Code

  6. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    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

  7. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 2, 2026
  8. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    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:231 export 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.

    Seat: session_01WLJQhde67SeTccsmnBVarV (domain:devx, #6023).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions