fix(spec): view/layout-without-binding covers timeline, map and tree; new view/tree-without-parent-field - #14331
Conversation
…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
📓 Docs Drift CheckThis PR changes 2 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Fixes #14106
What
checkViewCompleteness'sVIEW_BINDING_BLOCKStable (packages/spec/src/kernel/functional-completeness.ts) namedkanban/calendar/ganttonly, while six members ofListViewSchema.typecarry a type-specific binding block. The card measured the other three falling through the same trapdoor the rule exists to close (delete atimelineblock:os validate --jsonstillwarnings: [],valid: true; delete the siblingganttblock: warns as designed). This PR:timeline,map,tree— each with afixhint naming the keys that make the block a binding, read from the schemas at this tree:TimelineConfigSchemarequiresstartDateField+titleField(both named);ListMapConfigSchemarequires nothing and documents two coordinate forms (both named:locationField, or thelatitudeField/longitudeFieldpair);TreeConfigSchemarequires nothing (parentField+labelFieldnamed). Severity stayswarning, rule id staysview/layout-without-binding(ADR-0078 §1 — the view degrades, it does not die).view/tree-without-parent-field(warning, pathtree.parentField) — the silent-flat half oftype: '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). Atype: 'tree'view whoseparentFieldis undeclared, bound to an object that carries neither atreefield nor alookup/master_detailback to itself, renders every record at depth 0. EveryTreeConfigSchemakey is optional, sotree: {}satisfies the table and is still flat; a gate that passed it would be vouching for it.checkViewCompleteness(view, boundObject?)— an additive optional second parameter (the bound object definition,name+fieldsin either authorable spelling). One-argument callers are unchanged; without an object the tree rule stays silent rather than guess.@objectstack/lint'svalidate-functional-completenessresolves the object by name fromstack.objects(the list view's owndata.objectretarget first, then the container'sobjectName/object, mirroring the sibling reference-integrity rules) and hands it over. Wiring only — no rule logic in lint; every existing lint test stays green.pageparagraph ([feature] Give runtime-published custom pages an end-user entry point: apageview type on objects, or nav registration at publish time #13216) is kept verbatim andpagestays deliberately absent. Updates the ADR-0078 anchor's invariant prose (scripts/adr-anchors/), which still namedtimeline/treeas 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)timelinepackages/plugin-list/src/ListView.tsx:2440-2441 — `startDateFieldmapListView.tsx:2509 — `locationFieldtreeListView.tsx:2521-2522 —parentField: treeCfg.parentField, `labelFieldtreeparent auto-detectionpackages/plugin-tree/src/ObjectTree.tsx:126-152detectParentField— firsttype === 'tree'field, else firstlookup/master_detailwhosereference(orreference_to) equals the object's own name;buildForest:176 makes every record with no resolvable parent a roothasDetectableParentFieldmirrors exactly: same two arms, same object-name requirement.reference_toneeds no arm here — it is the retired spelling the ADR-0087 conversion layer folds toreferenceinsidenormalizeStackInput, one layer before this predicate runsgalleryBounded in-place addition, declared:
mapis read for its coordinate bindingThe triage premise "timeline / map: their required keys are truly required" holds for
timelineand is false formapat this tree —ListMapConfigSchema(view.zod.ts:1323) has every key optional and no refinement demanding a form. So block presence alone would blessmap: {}andmap: { titleField: 'title' }on their way tolocationField || 'location'— the exact "passed the new gate, still silently inert" shape the triage warned about fortree. 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 presentmapblock declaring neitherlocationFieldnor thelatitudeField+longitudeFieldpair is warned about under the same rule id at pathmap.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 havestatusis 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 exactos validatepath (normalizeStackInputthenrunAuthoringRules('validate')): aganttview with no block in a top-levelviews[]container warns as designed; the same view object-nested underobjects[].listViewsproduces 0 findings — the completeness walk only visitsstack.views, and normalization does not hoist object-nested list views. The card's own repro must therefore be the container form (itsganttsibling 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-objectstackblank 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), themapcoordinate-binding cases, the tree parent-pointer pairs (block empty / absent / label-only on a non-self-referencing object → finding; declaredparentField/treefield / selflookup/ selfmaster_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, containerlistslot,data.objectretarget, undeclared object → silent), plus a#14106 acceptanceblock throughrunAuthoringRulesfor bothvalidateandbuild: repro (timeline / map / tree-empty / tree-absent) yields exactly the five expected findings; the bound twin yields none../functional-completenessdirectly — 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 madehasDetectableParentFieldreturn true for every object (on-disk marker count 1) → 8 red / 32 green. Both legs restored withgit checkout HEAD -- ABS_PATH, proven by emptygit diff HEADand blob hash386f2bbc==HEAD:blob; trap on EXIT/INT/TERM. The lint acceptance block resolves spec throughdist/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)vitest run src/kernel/functional-completeness.test.ts→Tests 40 passed (40); targetedsrc/kernel src/ui/view.test.ts scripts/export-origins.test.ts scripts/sharded-artifacts.test.ts→Test Files 50 passed (50),Tests 1281 passed (1281)(the spec:appearance.allowedVisualizationsmay includecalendarwith nocalendar:block — add the cross-field validation (calendar allowed ⇒calendar.startDateFieldrequired) #13817 scope pin inview.test.tsuntouched and green);pnpm --filter @objectstack/spec typecheckexit 0 —check:test-typecheck: OK … 54 file(s) / 262 error(s) … held in test-typecheck-debt.json(unchanged);--listFilesshows both edited kernel files in the test program;check:generated→All 15 generated artifacts are up to dateafter--fixregenerated the two proved stale (api-surface/kernel.json,export-origins/kernel.json: one new const each).vitest run→Test Files 92 passed (92),Tests 2685 passed | 5 skipped (2690);pnpm --filter @objectstack/lint typecheckexit 0.node scripts/pm/dispatch-gates.mjs --commandswith no paths at9332aca5(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, includingcheck: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-prereqsandcheck:dual-build-cjs-loads(whole-workspacedist/required — only the lint closure, spec and lint are built here),check-test-completeness(grades a savedturbo run testlog),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 touchedsrcafter the build) and are green after a rebuild.Notes for review
packages/spec/src/**diff;os validate --strictturns the new warnings into failures.needs:contract-reviewhung on the PR and the card.@objectstack/specminor (new rule id + table growth + additive parameter),@objectstack/lintpatch (wiring).Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
Generated by Claude Code