Skip to content

fix(spec): view/layout-without-binding covers timeline, map and tree; new view/tree-without-parent-field - #14331

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-14106-view-binding-blocks-complete
Sep 2, 2026
Merged

fix(spec): view/layout-without-binding covers timeline, map and tree; new view/tree-without-parent-field#14331
os-zhuang merged 1 commit into
mainfrom
claude/issue-14106-view-binding-blocks-complete

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #14106

What

checkViewCompleteness's VIEW_BINDING_BLOCKS table (packages/spec/src/kernel/functional-completeness.ts) named kanban / calendar / gantt only, while six members of ListViewSchema.type carry a type-specific binding block. The card measured the other three falling through the same trapdoor the rule exists to close (delete a timeline block: os validate --json still warnings: [], valid: true; delete the sibling gantt block: warns as designed). This PR:

  1. Extends the table to all sixtimeline, map, tree — each with a fix hint naming the keys that make the block a binding, read from the schemas at this tree: TimelineConfigSchema requires startDateField + titleField (both named); ListMapConfigSchema requires nothing and documents two coordinate forms (both named: locationField, or the latitudeField/longitudeField pair); TreeConfigSchema requires nothing (parentField + labelField named). Severity stays warning, rule id stays view/layout-without-binding (ADR-0078 §1 — the view degrades, it does not die).
  2. Adds view/tree-without-parent-field (warning, path tree.parentField) — the silent-flat half of type: 'tree' can only nest by a self-parent pointer — a value-grouped tree is inexpressible, and on an object with no self-reference the view renders flat with no diagnostic #14109 folded in by triage (comment 5496335306; PM FOLD answer in comment 5502705510). A type: 'tree' view whose parentField is undeclared, bound to an object that carries neither a tree field nor a lookup/master_detail back to itself, renders every record at depth 0. Every TreeConfigSchema key is optional, so tree: {} satisfies the table and is still flat; a gate that passed it would be vouching for it.
  3. checkViewCompleteness(view, boundObject?) — an additive optional second parameter (the bound object definition, name + fields in either authorable spelling). One-argument callers are unchanged; without an object the tree rule stays silent rather than guess. @objectstack/lint's validate-functional-completeness resolves the object by name from stack.objects (the list view's own data.object retarget first, then the container's objectName / object, mirroring the sibling reference-integrity rules) and hands it over. Wiring only — no rule logic in lint; every existing lint test stays green.
  4. Rewrites the table's docblock to cite the measurements instead of deferring ("NOT flagged yet — same verify-then-enforce gate"); the page paragraph ([feature] Give runtime-published custom pages an end-user entry point: a page view type on objects, or nav registration at publish time #13216) is kept verbatim and page stays deliberately absent. Updates the ADR-0078 anchor's invariant prose (scripts/adr-anchors/), which still named timeline/tree as exempt.

#14109's capability half (a value-grouped tree) remains open in the decision inbox and is not implemented here.

Renderer evidence (objectui origin/main = 67dadd6)

type fallback read at mirrored how
timeline packages/plugin-list/src/ListView.tsx :2440-2441 — `startDateField
map ListView.tsx :2509 — `locationField
tree ListView.tsx :2521-2522 — parentField: treeCfg.parentField, `labelField
tree parent auto-detection packages/plugin-tree/src/ObjectTree.tsx :126-152 detectParentField — first type === 'tree' field, else first lookup/master_detail whose reference (or reference_to) equals the object's own name; buildForest :176 makes every record with no resolvable parent a root hasDetectableParentField mirrors exactly: same two arms, same object-name requirement. reference_to needs no arm here — it is the retired spelling the ADR-0087 conversion layer folds to reference inside normalizeStackInput, one layer before this predicate runs
gallery `titleField

Bounded in-place addition, declared: map is read for its coordinate binding

The triage premise "timeline / map: their required keys are truly required" holds for timeline and is false for map at this tree — ListMapConfigSchema (view.zod.ts :1323) has every key optional and no refinement demanding a form. So block presence alone would bless map: {} and map: { titleField: 'title' } on their way to locationField || 'location' — the exact "passed the new gate, still silently inert" shape the triage warned about for tree. Under the os-dev bounded in-place exemption (same defect class; mechanical, form pinned by the schema's own docblock and the measured fallback; same file, my claim; same gate family): a present map block declaring neither locationField nor the latitudeField+longitudeField pair is warned about under the same rule id at path map.locationField, with a map-specific message and the same hint. This is not stricter than the family's existing stance (a kanban with no block on an object that happens to have status is warned about today for the same reason); the parse-time alternative (a schema refinement, the #13817 shape) is a contract change and is left as an open question for the maintainer.

Coverage of the authored shape — a finding, not papered over

Measured on origin/main (a39b02a6) through the exact os validate path (normalizeStackInput then runAuthoringRules('validate')): a gantt view with no block in a top-level views[] container warns as designed; the same view object-nested under objects[].listViews produces 0 findings — the completeness walk only visits stack.views, and normalization does not hoist object-nested list views. The card's own repro must therefore be the container form (its gantt sibling fired). Filed as #14320 (lint lane, unassigned); the new checks cover every list view the walk reaches today, and #14320 extends the walk. #14320 is not addressed here.

Corpus effect

Ran the binding-family rules over the in-tree corpus at this head: examples/app-crm (3 view containers) — 0 hits; examples/app-todo (1 container) — 0 hits; examples/app-showcase (22 objects, 6 containers, 26 list views across calendar / chart / gallery / gantt / grid / kanban / map / timeline / tree, loaded from its views + objects barrels because the full config imports unbuilt connector plugins) — 0 hits; packages/create-objectstack blank template — no list views declared. No new warning fires; nothing to repair.

Tests

  • packages/spec/src/kernel/functional-completeness.test.ts: one fixture pair per newly covered type (both directions), the map coordinate-binding cases, the tree parent-pointer pairs (block empty / absent / label-only on a non-self-referencing object → finding; declared parentField / tree field / self lookup / self master_detail → clean; lookup to another object → finding; array-form fields; nameless object; no object handed → silent; junk never throws), registry pin extended, fix/message payload pin extended. 40/40.
  • packages/lint/src/validate-functional-completeness.test.ts: the walk hands the bound object (array-form and map-form objects, container list slot, data.object retarget, undeclared object → silent), plus a #14106 acceptance block through runAuthoringRules for both validate and build: repro (timeline / map / tree-empty / tree-absent) yields exactly the five expected findings; the bound twin yields none.
  • Ablation (source-level, spec tests resolve ./functional-completeness directly — no dist in the path): leg A removed the three new table entries (on-disk: timeline: 'timeline', count 0, HEAD count 1) → 5 red / 35 green; leg B made hasDetectableParentField return true for every object (on-disk marker count 1) → 8 red / 32 green. Both legs restored with git checkout HEAD -- ABS_PATH, proven by empty git diff HEAD and blob hash 386f2bbc == HEAD: blob; trap on EXIT/INT/TERM. The lint acceptance block resolves spec through dist/ and was NOT ablated (declared; a dist-level ablation would cost two spec rebuilds on a contended box).

Local verification at 9332aca5 (every exit captured before any pipe; verdict lines quoted from the gates)

  • Spec: vitest run src/kernel/functional-completeness.test.tsTests 40 passed (40); targeted src/kernel src/ui/view.test.ts scripts/export-origins.test.ts scripts/sharded-artifacts.test.tsTest Files 50 passed (50), Tests 1281 passed (1281) (the spec: appearance.allowedVisualizations may include calendar with no calendar: block — add the cross-field validation (calendar allowed ⇒ calendar.startDateField required) #13817 scope pin in view.test.ts untouched and green); pnpm --filter @objectstack/spec typecheck exit 0 — check:test-typecheck: OK … 54 file(s) / 262 error(s) … held in test-typecheck-debt.json (unchanged); --listFiles shows both edited kernel files in the test program; check:generatedAll 15 generated artifacts are up to date after --fix regenerated the two proved stale (api-surface/kernel.json, export-origins/kernel.json: one new const each).
  • Lint: full vitest runTest Files 92 passed (92), Tests 2685 passed | 5 skipped (2690); pnpm --filter @objectstack/lint typecheck exit 0.
  • Gate families: 60 derived by node scripts/pm/dispatch-gates.mjs --commands with no paths at 9332aca5 (44 pnpm / 16 node; the two-file reading gave 40 — the extra 20 come from the changeset, the ADR anchor, the tests and the regenerated artifacts). 56 green, including check:api-surface ("public API surface + factory signatures unchanged ✓"), check:export-origins, check:docs ("229 generated files in sync"), check:authorable-surface, check:llms-txt, check:yaml-examples, check:liveness, check:strictness-ledger, check:adr-anchors, check:nul-bytes. 4 NOT MEASURED (prerequisite, not red): check-dev-prereqs and check:dual-build-cjs-loads (whole-workspace dist/ required — only the lint closure, spec and lint are built here), check-test-completeness (grades a saved turbo run test log), check:type-check-debt (exit 3 by design: refuses to measure without every closure built). Six dist-reading spec/lint gates first refused on an mtime-stale dist (the ablation's restore touched src after the build) and are green after a rebuild.
  • Declared narrowing: the spec package's full 450-file vitest suite was not run locally (targeted set above, chosen as the tests that import the edited module or read the regenerated baselines); CI runs the farm.

Notes for review

  • Clause ②: packages/spec/src/** diff; os validate --strict turns the new warnings into failures. needs:contract-review hung on the PR and the card.
  • Changeset: @objectstack/spec minor (new rule id + table growth + additive parameter), @objectstack/lint patch (wiring).
  • Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21


Generated by Claude Code

…ee; add view/tree-without-parent-field

`VIEW_BINDING_BLOCKS` named kanban / calendar / gantt only, while six view
types carry a binding block and objectui's ListView adapter falls back to
literal field names for all six. The table now covers timeline, map and
tree, each with a `fix` hint naming the keys that make the block a binding;
a `map` block is read for its coordinate binding (its schema requires no
key). A new warning, `view/tree-without-parent-field`, fires when a tree
view declares no `parentField` and the bound object carries nothing the
renderer's `detectParentField` would find, mirroring that rule exactly.
`checkViewCompleteness` takes the bound object as an optional second
argument; the lint walk resolves it by name from `stack.objects`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json f645d6f8879f5e868b8c0aac978dc0db92552739.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f645d6f8879f5e868b8c0aac978dc0db92552739packageMentionDocs.

Which tree this was computed on

This run read content/docs from 625e7cd62e53dca37d8cd9ee859bdb4fada0627f — the merge of head 9332aca5a9e9c17bcb84c185aea398e7dcc1d19f into base f645d6f8879f5e868b8c0aac978dc0db92552739, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 625e7cd62e53dca37d8cd9ee859bdb4fada0627f && git checkout 625e7cd62e53dca37d8cd9ee859bdb4fada0627f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f645d6f8879f5e868b8c0aac978dc0db92552739 9332aca5a9e9c17bcb84c185aea398e7dcc1d19f && git checkout -B drift-repro f645d6f8879f5e868b8c0aac978dc0db92552739 && git merge --no-ff 9332aca5a9e9c17bcb84c185aea398e7dcc1d19f

node scripts/docs-audit/affected-docs.mjs --json f645d6f8879f5e868b8c0aac978dc0db92552739

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f645d6f8879f5e868b8c0aac978dc0db92552739 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

view/layout-without-binding covers only 3 of the 6 view types that have a binding block — timeline, tree and map fall through the same trapdoor

2 participants