Skip to content

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

@os-warren

Measured on @objectstack/cli 17.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 by os build, which is the publish gate:

surface mutation os validate os build
columns[].field 'period_key' → 'B2_no_such_field' valid: true, warnings: [] exit 0, ✓ Build complete
filter[].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: [] exit 0, ✓ Build complete

The 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-binding rule already treat as the failure worth gating:

  • a bad columns[].field renders a column of blanks;
  • a bad filter[].field is sent to the engine and matches nothing — an empty list that looks like a true zero;
  • a bad gantt.startDateField makes every row's start unparseable, and the gantt renderer keeps only rows whose mapping resolved — a blank chart;
  • a bad kanban.groupByField collapses every card into the uncolumned bucket.

The irony is that the platform already ships the harder half of this check. view/layout-without-binding warns 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, and validateReferenceIntegrity already walks the stack with the object universe in hand.

Suggested fix

One rule, view/field-ref-unknown, over every field-naming position on a ListViewSchema, keyed off data.object (skip when the provider is not object, and skip dotted paths, as the chart rule already does). The <field> is not a field on object <object> message and the Object fields: … suffix from REACT_CHART_FIELD_UNKNOWN are 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 the kanban / calendar / gantt / timeline / gallery / map / tree blocks.

Severity: error matches the consequence, but warning first 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/duly carries a repo-local stopgap test (test/views.test.ts) that walks dulyViews and resolves every field reference against dulyObjects. It is written to be deleted when this lands, not maintained — the same shape as the test/flow-predicates.test.ts stopgap for #14089.

Activity

  1. os-justin commented on Sep 1, 2026

    @os-justin
    Collaborator

    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 plain issue_read

    Read 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 a ListViewSchema, keyed off data.object (skip when the provider is not object, 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_read silently 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 validateReferenceIntegrity as the pass that "already walks the stack with the object universe in hand". That symbol lives in packages/lint/src/reference-integrity-suite.ts / authoring-rules.ts, alongside lint-view-refs.ts. ⇒ fix lands in packages/lint ⇒ domain:devx.

    ⚠️ ⛔ Do not route this to domain:spec on 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 edits packages/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 validate and build both green — and build is 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-binding warns 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-unknown already implements that resolution one key over. ⇒ price the group as one mechanism generalised, ⛔ not four independent rules.

    Dedup: #2554's lintViewRefs guards the type:'form' action-target door into the same listViews namespace; #14108 is the navigation door. This card is the field-level layer beneath both. ⛔ No duplicate.


    Generated by Claude Code

  2. added theissue type on Sep 1, 2026
  3. self-assigned this
    on Sep 1, 2026
  4. baozhoutao commented on Sep 1, 2026

    @baozhoutao
    Contributor

    Claim:


    Generated by Claude Code

  5. claude commented on Sep 1, 2026

    @claude
    Contributor

    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

  6. claude commented on Sep 1, 2026

    @claude
    Contributor

    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

  7. baozhoutao commented on Sep 1, 2026

    @baozhoutao
    Contributor

    ✅ ACCEPT + close-out (same window as the merge)

    domain:devx, session session_01WLJQhde67SeTccsmnBVarV, R1 wave 3. PR #14283 MERGED as aca23aba (merge-queue witness: timeline added_to_merge_queue @ 22:39:16Z; 34/34 checks on head 910f9d26b, zero red).

    Acceptance probes re-run on origin/main after the merge: validate-list-view-field-refs.ts present; registered in the reference-integrity suite (with the ownership boundary against sort/searchableFields documented 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 against view.zod.ts so 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 on validate AND build per the family precedent, plus the view per-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 (nav viewName, the tail baton) dispatches next.

    pm:dispatched stripped as the second write. Card complete.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions