Skip to content

Two binding guards now overlap: collapse test/views.test.ts's stopgap half into test/metadata-bindings.test.ts #58

Description

@os-warren

Observation from #51 (PR #57). Not urgent, and deliberately not done there — deleting another card's guard is not a rider on the card that supersedes it.

What happened

#51's dispatch said "nothing has ever checked the five view files or the app nav". That was not accurate as of main — test/views.test.ts (landed in #49) already carries a stopgap half that resolves the simple view surface and the nav pair, and its docblock names the same three upstream issues (#14106 / #14107 / #14108).

#51 shipped test/metadata-bindings.test.ts anyway, because the value was in the superset — datasets, dotted paths, platform objects, and a self-test, none of which existed. But that leaves two guards covering overlapping ground, which is the "keep two guards in step" cost both files' headers warn against.

The overlap, precisely

test/views.test.ts › describe('every non-grid lens is bound to real fields') — the two assertions:

  • names only fields that exist on the object it is bound to
  • every nav entry resolves to a view that exists on the object it names (in describe('navigation'))

Both are strict subsets of metadata-bindings.test.ts. The rest of views.test.ts is product pins (the gantt bar spanning visible_from → due_date, the timeline reading last_update_at, one colour source, nothing ranked by a count, the deliberate absence of gantt.colorField) — those outlive the platform gaps and must stay wherever they end up.

declares the binding block its type needs is also NOT covered by the new file: that is binding-block presence (#14106), a different property from reference resolution (#14107). Keep it.

Two concrete defects in the older half, which is the argument for collapsing rather than leaving both

  1. Its system-column list is hand-copied and has already drifted. SYSTEM_FIELDS there lists business_unit_id, which is not a platform column, and omits owning_business_unit_id, tenant_id, user_id and deleted_at, which are. The platform ships the registry — SystemFieldName from @objectstack/spec/system — and metadata-bindings.test.ts imports it. Net effect today: a view naming business_unit_id on duly_task (which declares business_unit) passes there as a "system field".

  2. It skips every dotted path by construction. Its checker opens with if (!name || name.includes('.')) return, so a joined path on a view is unchecked. Harmless today only because no view authors one.

Suggested shape

Delete the two subsumed assertions from test/views.test.ts, leave a one-line pointer to test/metadata-bindings.test.ts, and keep the product pins and the binding-block-presence check where they are. Roughly a 40-line deletion; the 402-test suite should stay green minus those two.

Worth folding into whoever removes the stopgaps when objectstack#14105 / #14107 / #14108 land — at that point metadata-bindings.test.ts goes away entirely and only the product pins should remain.

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claiming this.

    • Session: session_01SqkTcrxUFci7nqXdbBSe2p
    • Branch: claude/issue-58-collapse-binding-guards
    • Worktree: ../duly-issue-58

    This is a deletion card, so the plan is mutation-first: for every assertion I propose to remove from test/views.test.ts, I will mutate a real fixture and show test/metadata-bindings.test.ts fails on the same mutation (method per PR #77 / issue #37). Anything the surviving guard does not catch stays where it is.


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor
    {
      "issue": 58,
      "status": "done",
      "branch": "claude/issue-58-collapse-binding-guards",
      "pr": "https://github.com/objectstack-ai/duly/pull/97",
      "premise_still_valid": true,
      "summary": "Deleted the two subsumed assertions from test/views.test.ts ('names only fields that exist on the object it is bound to' and 'every nav entry resolves to a view that exists on the object it names') and handed reference resolution to test/metadata-bindings.test.ts. The card's premise held, but one part of it did not: the two guards did NOT fully overlap. metadata-bindings' walkNav recursed into children on `group` items only, while the spec ties the recursive `children` knot on the object branch too (NavigationItem = (ObjectNavItem & { children?: NavigationItem[] }) | ... | GroupNavItem), so a nav entry nested under an `object` entry was resolved by the deleted assertion and by nothing else. Closed that in walkNav and pinned it with a new self-test BEFORE deleting, then re-measured. Kept binding-block presence (#14106), the product pins, and the reachability/placement nav tests, per the card. views.test.ts 324 -> 263 lines; suite 662 -> 661 tests (two assertions deleted, one self-test added).",
      "tests": "Gates at d2876c5 (the commit the PR points at, clean tree), each verdict read from the tool's own line, not a piped $?: `pnpm validate` -> '✓ Validation passed (376ms)' VALIDATE_EXIT=0; `pnpm typecheck` -> no output, TYPECHECK_EXIT=0; `pnpm test` -> 'Test Files 26 passed (26) · Tests 661 passed (661)' TEST_EXIT=0; `pnpm build` -> '✓ Build complete (581ms)' BUILD_EXIT=0. The one security-enterprise warning is the documented expected state (AGENTS.md rule 7). All heavy runs went through scripts/pm/os-verify-lock.sh (VERDICT lines: command-exit 0, held 25-29s, waited 0s).\n\nABLATION (18 cases + 2 follow-ups, pre- and post-change). Each case: cp byte-backup (never an index restore); mutation applied with a hit count asserted == 1; arrival proved on disk with `grep -F -c` in BOTH directions (-F so a [...] anchor is a literal, not a character class); vitest run; restore from the byte copy verified by sha256sum; mutate+measure inside ONE invocation with a per-case EXIT/INT/TERM restore trap. No rebuild leg applies — this repo runs tests from source, no dist.\n\nRESULT: 15/15 real defects still red after the collapse, through metadata-bindings. A1-A8/A10/A13 (columns, filter, sort, grouping, rowColor, kanban.columns, gantt.tooltipFields, calendar/gantt block keys, data.object) and B1/B2 (nav viewName, nav objectName): red on both guards before, red on the survivor after. B3 (ghost viewName nested under an `object` nav entry): views-only red BEFORE (the hole), survivor red AFTER. A9b (gantt.typeField): red on the survivor only — it reads three reachable slots the deleted assertion never did.\n\nTWO reds deliberately gone, both false positives, each with a named registry as the authority: A12 (a column of `deleted_at`) — `deleted_at` IS a platform column; Object.values(SystemFieldName) = [created_at, created_by, deleted_at, id, organization_id, owner_id, owning_business_unit_id, tenant_id, updated_at, updated_by, user_id], and the deleted hand-copied list carried `business_unit_id` (not in it) while omitting four that are. The other direction is measured too: A11 puts `business_unit_id` on a view and it PASSED the deleted assertion while failing the survivor. B4 (a nav entry to `sys_user`) — the deleted assertion required nav objects to be in dulyObjects, so it rejected real platform objects. Typo net proved intact: C1 (`sys_bogus`) is still a finding, C2 (a viewName reaching into sys_user) still fails via the boundary assertion.\n\nINSTRUMENT FAILURE, DECLARED: the first pass reported case A9 as '(none — GREEN)'. It was VOID, not green — numTotalTests 0, numFailedTests 0, success false. `defineView` parses with .strict(), so the mutation threw at import and the suite never ran; a failures-only summary reads an import-time throw as an absence of failures. Driver fixed to print the collected-test count and fail loudly on zero; every other case in both passes was then audited to have collected the full suite (63 pre, 62 post). That refusal also replaced A9's intended claim with a stronger one: the 19-keys-x-6-blocks breadth of the deleted assertion is mostly UNREACHABLE, since an unknown block key cannot be authored. Reading the Zod shapes off @objectstack/spec/ui rather than the error behaviour, the reachable slots are kanban 3/3, calendar 4/4, gantt 12 of 15 (survivor reads all 15), timeline 5/5, tree 3/3, map 4 of 5 (survivor 5), gallery 0 of 3 (survivor 3) — no reachable slot was read by the deleted assertion and not by the survivor.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    (Supersedes the previous comment: its leading <!-- os-dev-report --> marker did not survive GitHub's body sanitizer, so that copy is invisible to a marker scan. Same JSON, marker as literal text.)

    {
      "issue": 58,
      "status": "done",
      "branch": "claude/issue-58-collapse-binding-guards",
      "pr": "https://github.com/objectstack-ai/duly/pull/97",
      "premise_still_valid": true,
      "summary": "Deleted the two subsumed assertions from test/views.test.ts ('names only fields that exist on the object it is bound to' and 'every nav entry resolves to a view that exists on the object it names') and handed reference resolution to test/metadata-bindings.test.ts. The card's premise held, but one part of it did not: the two guards did NOT fully overlap. metadata-bindings' walkNav recursed into children on `group` items only, while the spec ties the recursive `children` knot on the object branch too (NavigationItem = (ObjectNavItem & { children?: NavigationItem[] }) | ... | GroupNavItem), so a nav entry nested under an `object` entry was resolved by the deleted assertion and by nothing else. Closed that in walkNav and pinned it with a new self-test BEFORE deleting, then re-measured. Kept binding-block presence (#14106), the product pins, and the reachability/placement nav tests, per the card. views.test.ts 324 -> 263 lines; suite 662 -> 661 tests (two assertions deleted, one self-test added).",
      "tests": "Gates at d2876c5 (the commit the PR points at, clean tree), each verdict read from the tool's own line, not a piped $?: `pnpm validate` -> 'Validation passed (376ms)' VALIDATE_EXIT=0; `pnpm typecheck` -> no output, TYPECHECK_EXIT=0; `pnpm test` -> 'Test Files 26 passed (26) · Tests 661 passed (661)' TEST_EXIT=0; `pnpm build` -> 'Build complete (581ms)' BUILD_EXIT=0. The one security-enterprise warning is the documented expected state (AGENTS.md rule 7). All heavy runs went through scripts/pm/os-verify-lock.sh (VERDICT lines: command-exit 0, held 25-29s, waited 0s).\n\nABLATION (18 cases + 2 follow-ups, pre- and post-change). Each case: cp byte-backup (never an index restore); mutation applied with a hit count asserted == 1; arrival proved on disk with `grep -F -c` in BOTH directions (-F so a [...] anchor is a literal, not a character class); vitest run; restore from the byte copy verified by sha256sum; mutate+measure inside ONE invocation with a per-case EXIT/INT/TERM restore trap. No rebuild leg applies - this repo runs tests from source, no dist.\n\nRESULT: 15/15 real defects still red after the collapse, through metadata-bindings. A1-A8/A10/A13 (columns, filter, sort, grouping, rowColor, kanban.columns, gantt.tooltipFields, calendar/gantt block keys, data.object) and B1/B2 (nav viewName, nav objectName): red on both guards before, red on the survivor after. B3 (ghost viewName nested under an `object` nav entry): views-only red BEFORE (the hole), survivor red AFTER. A9b (gantt.typeField): red on the survivor only - it reads three reachable slots the deleted assertion never did.\n\nTWO reds deliberately gone, both false positives, each with a named registry as the authority: A12 (a column of `deleted_at`) - `deleted_at` IS a platform column; Object.values(SystemFieldName) = [created_at, created_by, deleted_at, id, organization_id, owner_id, owning_business_unit_id, tenant_id, updated_at, updated_by, user_id], and the deleted hand-copied list carried `business_unit_id` (not in it) while omitting four that are. The other direction is measured too: A11 puts `business_unit_id` on a view and it PASSED the deleted assertion while failing the survivor. B4 (a nav entry to `sys_user`) - the deleted assertion required nav objects to be in dulyObjects, so it rejected real platform objects. Typo net proved intact: C1 (`sys_bogus`) is still a finding, C2 (a viewName reaching into sys_user) still fails via the boundary assertion.\n\nINSTRUMENT FAILURE, DECLARED: the first pass reported case A9 as '(none - GREEN)'. It was VOID, not green - numTotalTests 0, numFailedTests 0, success false. `defineView` parses with .strict(), so the mutation threw at import and the suite never ran; a failures-only summary reads an import-time throw as an absence of failures. Driver fixed to print the collected-test count and fail loudly on zero; every other case in both passes was then audited to have collected the full suite (63 pre, 62 post). That refusal also replaced A9's intended claim with a stronger one: the 19-keys-x-6-blocks breadth of the deleted assertion is mostly UNREACHABLE, since an unknown block key cannot be authored. Reading the Zod shapes off @objectstack/spec/ui rather than the error behaviour, the reachable slots are kanban 3/3, calendar 4/4, gantt 12 of 15 (survivor reads all 15), timeline 5/5, tree 3/3, map 4 of 5 (survivor 5), gallery 0 of 3 (survivor 3) - no reachable slot was read by the deleted assertion and not by the survivor.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    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

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions