Repository navigation
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 #14106
Description
Measured on @objectstack/cli 17.2.0 / @objectstack/spec 17.2.0, from a real app (objectstack-ai/duly), while adding kanban / gantt / timeline lenses over one object.
The rule
checkViewCompleteness (spec/dist/kernel — functional-completeness.ts) drives the view/layout-without-binding warning off this table:
var VIEW_BINDING_BLOCKS = {
kanban: "kanban",
calendar: "calendar",
gantt: "gantt"
};ListViewSchema.type has nine members, and six of them carry a type-specific binding block: kanban, calendar, gantt, timeline, map, tree. The table names three.
Why the other three are the same defect, not a lesser one
The rule's own message is "the renderer falls back to literal default field names … the view renders empty while authoring reports success." That is literally what the uncovered three do. From the console's ListView adapter (plugins-views-*.js, 17.2.0), the fallbacks are hard-coded strings:
| view type | renderer fallback when the block is absent | gate |
|---|---|---|
calendar |
startDateField || 'start_date', endDateField || 'end_date' |
✅ warns |
gantt |
startDateField || 'start_date', endDateField || 'end_date', progressField || 'progress', dependenciesField || 'dependencies' |
✅ warns |
kanban |
groupBy = groupByField || groupField || <inferred> |
✅ warns |
timeline |
startDateField || 'created_at', titleField || 'name' |
❌ silent |
map |
locationField || 'location' |
❌ silent |
tree |
labelField || titleField || 'name' |
❌ silent |
timeline is the sharpest of the three: 'created_at' is a plausible-looking name that many objects do not declare, and the timeline renderer drops every row whose start date does not parse — so the view is blank, not merely mis-titled.
Reproduction (measured, both directions)
In a stack with a duly_task object that declares neither created_at nor start_date:
listViews.recent={ type: 'timeline', … }with itstimeline: { startDateField: 'last_update_at', titleField: 'subject', … }block →os validate --json→warnings: [].- Delete the
timelineblock (confirmed on disk:startDateFieldoccurrences 1 → 0, marker present) →os validate --json→warnings: []again,valid: true. No diagnostic at all. - Same ablation against the sibling
ganttview →view/layout-without-bindingwarning fires as designed.
So the gate is working; the table it reads is short.
Suggested fix
Extend VIEW_BINDING_BLOCKS to the full set and give each the same shaped fix hint the existing three carry:
var VIEW_BINDING_BLOCKS = {
kanban: "kanban", calendar: "calendar", gantt: "gantt",
timeline: "timeline", map: "map", tree: "tree",
};Hints, matching the required keys in each block's schema:
timeline: { startDateField: '<date_field>', titleField: '<text_field>' }(both required byTimelineConfigSchema)map: { latitudeField: '<number_field>', longitudeField: '<number_field>' }or{ locationField: '<field>' }tree: { labelField: '<text_field>', parentField: '<self_lookup_field>' }
tree is the one worth a second look: TreeConfigSchema's keys are all optional, so "has a tree block" is a weaker assertion there than for the others — see the sibling issue on tree having no value-grouped mode.
Related
- spec: measure whether
timeline/gantt/map/ other switcher visualizations share the #13748 shape — whitelisted inappearance.allowedVisualizationswith no binding block, renderer behavior unmeasured #14074 asks whether the switcher-whitelisted visualizations share the 视图声明 calendar 可视化但缺 calendar 配置块时,平台不报错——直接渲染成一屏错位画面(全部记录堆在今天) #13748 shape. This is the authored-typehalf of the same root cause; I have posted the renderer measurements there.
Activity
os-justin commentedon Sep 1, 2026
Triage →⚠️ Note the lane split from its four family siblings — this is the one card of the five that does not land in
domain:spec· p2 · Bug.packages/lint.Anchoring — measured on
origin/main(66ecc50a).VIEW_BINDING_BLOCKSand thelayout-without-bindingrule live inpackages/spec/src/kernel/functional-completeness.ts(confirmed: 2 files carry the symbol, both underpackages/spec/src). The card's own citation of "spec/dist/kernel" is the built artifact; the source is the above. ⇒domain:spec.⇒⚠️ Its four siblings (#14105 · #14107 · #14108 · #14148) all land in
packages/lint⇒domain:devx. Same defect class, two lanes. ⛔ They cannot be folded into one PR across that boundary.Type = Bug. ADR-0078 (no silently inert metadata) is the declared invariant; the rule exists and enforces it for 3 of the 6 view types that carry a binding block. Declared ≠ enforced on the uncovered three ⇒ Bug. The change narrows the accept set, so it is ⛔ not a Feature.
Grade p2, and flag as the family's cheapest fix. The remedy is extending a three-entry table to six plus three
fixhints; the card supplies both directions of the measurement (ablatetimeline→ silent; ablate the siblinggantt→ warns as designed), so the gate is proven working and the table proven short.timelineis the sharpest member —'created_at'is a plausible-looking default that many objects do not declare, and the renderer drops every row whose start date fails to parse ⇒ blank, not merely mis-titled.domain:specseat — Clause ② path limbpackages/spec/src/**is the Clause-② path limb. This card's diff lands there by construction ⇒ the path limb trips regardless of how narrow the change is, and the dispatch tier must be priced accordingly (tier single-source =CONTRACT_REVIEW_TIER, ⛔ not a model name written here).⛔ This is not me pre-hanging
needs:contract-review— forward pre-hanging was retired by the 2026-08-28 ruling; the label goes on with a reviewable contract increment at PR time. Recording the tier consequence only so the card is not priced as a trivial table edit.The
treecaveat is real and should survive into implementation. The card flags thatTreeConfigSchema's keys are all optional, so "has atreeblock" is a weaker assertion there than for the others. ⇒ addingtreeto the table buys less than addingtimeline/map, and the sibling card #14109 (atreecannot express value-grouping at all) is the reason why. ⛔ Do not let the weakertreecase stall the strongtimeline/maphalf.Dedup: related-but-distinct from #14074 (whether switcher-whitelisted visualizations share the #13748 shape) — that is the renderer half, this is the authored-
typehalf; the reporter has already cross-posted measurements. ⛔ No duplicate.Generated by Claude Code
os-justin commentedon Sep 1, 2026
范围补充 —— 把 #14109 的「静默扁平」半边折进本卡
分诊席补记。本卡入队时定级/路由不变(
domain:spec· p2 · Bug),这条只加一项范围。#14109 有两个半边:①
tree表达不了「按字段值分组的树」(能力扩张 ⇒ 已入决策箱);② 在没有自引用的对象上,type: 'tree'视图既不报错也不空 —— 它扁平渲染,带一个永远展不开的展开槽,四道门禁全绿。半边 ② 是 ADR-0078 的同一条不变量,与本卡同族、同文件,且不依赖那个特性裁决 ⇒ 折进本卡,⛔ 不另立卡、⛔ 不等裁。
tree加进VIEW_BINDING_BLOCKS」就能解决的 —— 本卡正文已经预告了这点本卡自己写着:
TreeConfigSchema的键全部可选,所以「有没有tree块」对tree而言是比对其它五种更弱的断言 —— 一个tree: {}就能满足绑定块检查。#14109 独立测到了同一件事的另一面:parentField缺席时渲染器会去自动探测(fields[].type === 'tree',或一个指向对象自身的 lookup),探测不到就每条记录都是深度 0 的根。⇒ ⭐ 两条检查,⛔ 不是一条:
timeline/map足够(它们的必填键是真必填)。tree的父指针解不解析得出来 ——parentField既未声明、也无法在绑定对象上自动探测 ⇒ 作者时告警。这一条是tree专属的,因为只有tree的块可以整体为空还「合法」。⛔ 只做 1 而宣称覆盖了
tree,会留下一个通过了新门禁却依旧静默扁平的视图 —— 那比不加检查更糟,因为门禁会为它背书。验收补一条:一个绑定在无自引用对象上的
type: 'tree'视图(tree块存在但为空,或整个缺席)必须在os validate上产生诊断;夹具覆盖失败形与干净形两向。为什么折而不是串行
同一文件(
packages/spec/src/kernel/functional-completeness.ts)、同一规则族、同一 ADR、同一位报告者、同一次实测会话 ⇒ 家族派发五门里前四门直接成立;排除清单只有一条:#14109 的能力半边 ⛔ 不在本卡范围内(它在决策箱等裁,⛔ 实施者不得顺手实现)。Generated by Claude Code
os-zhuang commentedon Sep 2, 2026
Claim: PM loop round 1 (batch 1)
Session:
session_01GDA48PuRFrHyRfdkBz8m21Branch:
claude/issue-14106-view-binding-blocks-completeWorktree:
objectstack-issue-14106Domain:
domain:specFile surface:
packages/spec/src/kernel/functional-completeness.ts(+ its tests) — theVIEW_BINDING_BLOCKStable, its docblock, and the tree parent-pointer check;packages/spec/src/ui/view.zod.tsdocblock cross-reference lines only if the wording must follow; cross-lane consumer wiring declared under the exception path:packages/lint/src/validate-functional-completeness.ts(hand the bound object to the view check so the tree half can resolveparentField; no rule logic in lint) (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: fable—node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/kernel/functional-completeness.ts(tree4fac7a0) answered "no path-derived mandate … Clause ② SUSPECT surface:packages/spec/src/kernel/functional-completeness.ts⇢packages/spec/src/**"; tier judged from content —os validate --stricttreats warnings as errors (validate.ts,strictflag), so a new warning changes what passes validation ⇒ CONTRACT_REVIEW_TIERClause-②: yes
Serial constraints cleared: no open PR touches
functional-completeness.tsor lint'svalidate-functional-completeness.ts(every open PR's file list read; devx sibling PR #14286 toucheslint-view-refs.ts,index.tsand tests only); noclaude/issue-14106*branch on origin; #14074 (measurement card,pm:queue) is read-coupled — its renderer measurements (comment 5489373025) are the verify-then-enforce evidence this table's docblock demands — and stays unclaimed, grouped with #14073 for a later batch; none else.Fold-or-serial (triage asked, comment 5496335306): FOLD. Five gates: ① same defect form — ADR-0078 view metadata that authors as complete and renders inert; the tree parent-pointer half is a second check on the same predicate, not a second logic family; ② same file and region — the view predicate in
functional-completeness.ts, one worktree, one changeset; ③ every member ruled or graded — this card triaged p2 Bug, the silent-flat half folded by triage; the capability half of #14109 stays in the decision inbox and is EXCLUDED; ④ each independently verifiable — one fixture pair per newly covered view type, plus the tree unresolvable-parent fixture in both directions; ⑤ exclusion list: #14109's value-grouped-tree capability half (⛔ not implemented here),page(deliberately absent per the table's docblock — apageview refuses at parse),gallery(a literal fallback per the measurements but no required binding key — measured and reported, not silently added).Generated by Claude Code
claude commentedon Sep 2, 2026
Claim (dev): implementing #14106 (with the silent-flat
treehalf of #14109 folded in per triage comment 5496335306 and the PM's FOLD answer in comment 5502705510).Session:
session_01GDA48PuRFrHyRfdkBz8m21(subagent mode — this dev seat runs inside the dispatching PM session, so the session ID equals the PM's).Branch:
claude/issue-14106-view-binding-blocks-complete(pushed empty as the write-route probe; worktreeobjectstack-issue-14106offorigin/main=a39b02a6).Scope: extend
VIEW_BINDING_BLOCKSinpackages/spec/src/kernel/functional-completeness.tsfrom 3 to 6 (timeline,map,tree), add thetreeparent-pointer resolution check (mirroring objectui'sdetectParentFieldrule), wire the bound object throughpackages/lint/src/validate-functional-completeness.ts(wiring only). Exclusions: #14109's value-grouped-tree capability half stays in the decision inbox;pagestays deliberately absent;galleryis measured and reported, not added.Generated by Claude Code
claude commentedon Sep 2, 2026
os-dev-report
{ "issue": 14106, "status": "done", "branch": "claude/issue-14106-view-binding-blocks-complete", "pr": "https://github.com/objectstack-ai/objectstack/pull/14331", "head_sha": "9332aca5", "premise_still_valid": true, "summary": "Premise verified on origin/main a39b02a6: VIEW_BINDING_BLOCKS at functional-completeness.ts:215 named exactly kanban/calendar/gantt with the deferring docblock. Extended the table to timeline/map/tree with schema-derived fix hints (TimelineConfigSchema requires startDateField+titleField; ListMapConfigSchema and TreeConfigSchema require nothing), rewrote the docblock to cite the objectui ListView.tsx fallbacks and the card's both-direction ablation (page paragraph kept verbatim), and added the tree parent-pointer rule `view/tree-without-parent-field` (warning, path tree.parentField) mirroring objectui plugin-tree ObjectTree.tsx detectParentField (:126-152 at 67dadd6): fires only when parentField is undeclared AND the bound object has neither a type:'tree' field nor a lookup/master_detail whose reference is the object's own name. checkViewCompleteness gained an optional second parameter (bound object); the lint walk resolves it from stack.objects (list view data.object first, then container objectName/object) — wiring only, all 92 lint suites green. One triage premise falsified and handled under the bounded in-place exemption, declared in the PR: 'map's required keys are truly required' is false at this tree (every ListMapConfigSchema key optional, no refinement), so a present map block declaring neither locationField nor the latitudeField+longitudeField pair is warned under the same rule id at path map.locationField. gallery measured (titleField || 'name') and NOT added; page stays absent; #14109's capability half untouched. Corpus effect: 0 new warnings across app-crm (3 containers), app-todo (1), app-showcase (26 list views over all 9 types) and the blank template (no list views). Deviation from the PM claim's file surface: scripts/adr-anchors/packages__spec__src__kernel__functional-completeness.ts.json invariant prose updated (it named timeline/tree as exempt; non-governed presence-check anchor) and the two generated spec artifacts regenerated (api-surface/kernel.json, export-origins/kernel.json — one new const each).", "tests": "At 9332aca5, exits captured before any pipe, verdict lines quoted. spec: `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/kernel/functional-completeness.test.ts` → `Test Files 1 passed (1)`, `Tests 40 passed (40)`; targeted `src/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)` (#13817 scope pin untouched, green); `pnpm --filter @objectstack/spec typecheck` exit 0 → `check:test-typecheck: OK … 54 file(s) / 262 error(s) … held in test-typecheck-debt.json` (count unchanged), `tsc --listFiles -p tsconfig.test.json` lists both edited kernel files, 0 errors in them; `check:generated` → `All 15 generated artifacts are up to date` after `--fix` regenerated the 2 proved stale. lint: `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2` (full) → `Test Files 92 passed (92)`, `Tests 2685 passed | 5 skipped (2690)`; `pnpm --filter @objectstack/lint typecheck` exit 0. Ablation (source-level; spec tests resolve ./functional-completeness directly, no dist in the path; trap EXIT/INT/TERM, absolute paths): leg A removed the three new table entries — on-disk anchored grep `timeline: 'timeline',` count 0 vs 1 in HEAD — → `Tests 5 failed | 35 passed (40)` (turned red); leg B forced hasDetectableParentField to bless every object — on-disk marker count 1 — → `Tests 8 failed | 32 passed (40)`; both legs restored via `git checkout HEAD -- ABS_PATH`, proven by empty `git diff HEAD` and blob hash 386f2bbc == HEAD blob. The lint acceptance block resolves spec through dist and was NOT ablated (declared). Corpus: runAuthoringRules('validate') over normalizeStackInput of app-crm / app-todo configs and the showcase views+objects barrels → binding-family hits 0 / 0 / 0. Object-nested coverage measurement on a39b02a6: container `views[]` gantt-without-block → 1 finding at views[0].listViews.schedule.gantt; the same view under objects[].listViews → 0 findings (filed #14320).", "gates": "60 families derived by `node scripts/pm/dispatch-gates.mjs --commands` with no paths at 9332aca5 (stderr: 'gate list derived from the tree of objectstack-ai/objectstack at commit 9332aca5'; 44 pnpm / 16 node — the two-file path reading gave 40 (28/12, count asserted); the extra 20 come from the changeset, ADR anchor, tests and regenerated artifacts). 56 green incl. check:api-surface ('public API surface + factory signatures unchanged ✓'), check:export-origins ('export-origins/ is current: 5227 exports'), check:docs ('229 generated files in sync'), check:authorable-surface, check:llms-txt, check:yaml-examples, check:liveness ('state-counts.md is current'), check:strictness-ledger ('437 site(s) measured'), check:adr-anchors, check:nul-bytes ('scanned 7813 text file(s) … no raw ASCII control bytes'), check:changeset-gate-self-tests, check:adr-0087-registration ('adds no declared-breaking changeset'). 4 NOT MEASURED (prerequisite class, exit 1/3, not red): node scripts/check-dev-prereqs.mjs and pnpm check:dual-build-cjs-loads (need the whole workspace dist; only the lint closure, spec and lint are built here), node scripts/check-test-completeness.mjs (grades a saved turbo test log), pnpm check:type-check-debt (exit 3 by design without every closure built). Six dist-reading gates (api-surface, browser-reachable-entries, dual-source-exports, entry-nameability, exported-any, lint doc-formula-expressions) first refused on an mtime-stale dist — the ablation restore touched src after the build — and are green after `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/lint build`. Declared narrowing: spec's full 450-file suite not run locally (targeted set above); CI runs the farm. CI status at report time: in_progress (not waited on).", "deviations": [ "Bounded in-place addition (declared in PR body): `map` block read for its coordinate binding (path map.locationField) because ListMapConfigSchema requires no key — the triage's 'map required keys are truly required' premise is false at this tree.", "File surface beyond the claim: scripts/adr-anchors/packages__spec__src__kernel__functional-completeness.ts.json (invariant prose), packages/spec/api-surface/kernel.json and packages/spec/export-origins/kernel.json (regenerated; one new const each), .changeset/view-binding-blocks-complete.md.", "Spec full vitest suite not run locally — targeted set declared above.", "PR body carries a platform-appended second session footer after a '---' (create-time behaviour, left as is)." ], "files_changed": [ "packages/spec/src/kernel/functional-completeness.ts", "packages/spec/src/kernel/functional-completeness.test.ts", "packages/lint/src/validate-functional-completeness.ts", "packages/lint/src/validate-functional-completeness.test.ts", "packages/spec/api-surface/kernel.json", "packages/spec/export-origins/kernel.json", "scripts/adr-anchors/packages__spec__src__kernel__functional-completeness.ts.json", ".changeset/view-binding-blocks-complete.md" ], "labels": "needs:contract-review added by additive REST POST on PR #14331 (read-back: ['needs:contract-review']) and on #14106 (read-back: bug, domain:spec, needs:contract-review, pm:dispatched, priority:p2). skip-changeset not applicable (changeset present).", "mcp_calls": "0 — every GitHub read/write went through the zero-quota page payload, REST curl (repo-scoped probe answered 200) and git; no mcp__github__ call was made", "open_questions": [ { "question": "gallery measurement verdict: objectui falls back to `titleField || 'name'` for gallery cards (#14074 comment 5489373025), but GalleryConfigSchema requires no key, so adding it to VIEW_BINDING_BLOCKS would let `gallery: {}` pass while asserting nothing, and the degradation is mis-titled cards rather than an empty surface. Should gallery get a binding rule at all?", "options": [ "A keep gallery out of the table; record the measurement in the docblock (done)", "B add gallery to the table AND read the block for titleField (same shape as the map coordinate read) — warns on gallery views whose object lacks a `name` field only coincidentally", "C make GalleryConfigSchema require titleField at parse (contract change)" ], "recommendation": "A, because the long-term-sound stance of this family is 'warn where the renderer empties the surface'; gallery still renders every card, and B/C would widen the accept-narrowing beyond the measured inert shape with zero pull (the showcase declares titleField already). Reported, not added — as the dispatch exclusion list asked." }, { "question": "Object-nested coverage: the completeness walk visits only top-level views[] containers; object-nested objects[].list / listViews (ADR-0017, ADR-0047) never reach checkViewCompleteness, so the new rules are inert on that shape (measured on a39b02a6: same gantt-without-block view, 1 finding in the container form, 0 object-nested). Filed as #14320 in the lint lane. Should #14320 be pulled into the same batch so the tree rule reaches object-nested views before the card closes?", "options": [ "A land this PR as is; queue #14320 in the devx lane (walk extension mirroring lint-view-refs' containerFromObject, with the object as bound object)", "B fold #14320 into this PR (cross-lane walk logic in lint — beyond the declared wiring-only edit)" ], "recommendation": "A, because the walk defect predates this card and belongs to lint's own family (the sibling reference-integrity rules already walk both doors); folding it would move rule-shaped logic across the lane boundary the dispatch drew. Note the card's own repro was necessarily the container form (its gantt sibling fired), so the card's acceptance holds on the shape it measured." }, { "question": "map coordinate binding — schema or completeness rule? This PR warns (completeness, same rule id) when a present map block declares neither coordinate form. The parse-time alternative is a ListMapConfigSchema refinement requiring locationField or the latitudeField+longitudeField pair (the #13817 shape for calendar).", "options": [ "A leave it as the completeness warning (done); no contract change", "B add a schema refinement so an unbound map block is refused at parse (Clause-② contract change, breaking for stacks that relied on the 'location' fallback)" ], "recommendation": "A for now, because the family's ruling keeps this door at WARNING under verify-then-enforce and B narrows the parse accept set on a fallback that objectui still honours; revisit B if #14074's outcome moves the switcher door to parse-time refusals for the same types." } ], "out_of_scope_findings": [ "filed as #14320: validate-functional-completeness walks only top-level views[] containers — object-nested objects[].list / listViews (ADR-0017) never reach the view completeness rules (measured 0 findings for a gantt view without its block in that form; dedup: all 433 open issues listed via REST and grepped locally, no prior card)" ] }Generated by Claude Code
claude commentedon Sep 2, 2026
ACCEPT — PR #14331 (head
9332aca5), reviewed against GitHub, not the report.Verdict on the open questions in the report
map.locationFieldunder the existing rule id), no schema refinement. Accepted as a bounded file-surface increment declared in the PR body.Contract review (Clause ②) — PASS, in-seat
packages/spec/src/kernel/functional-completeness.ts⇒ Clause ② applies.view/tree-without-parent-fieldand the map coordinate check change--strictoutcomes for existing stacks (accept-set change); one new exported constVIEW_TREE_WITHOUT_PARENT_FIELD;checkViewCompletenessgains an additive optional second parameter (public surface widening, backwards compatible).packages/lint/src/validate-functional-completeness.ts(view.data.object→ containerobjectName→ containerobject) is read-only over the stack and cannot widen what the schema accepts.parentFieldis undeclared and the bound object has no detectable parent field (treetype, or self-referentiallookup/master_detail); when no bound object resolves it stays a warning, never an error. Existing conformant stacks are unaffected.claude-fable-5-1. The fuse literal inscripts/pm/dispatch-gates.mjsreadsclaude-fable-5; the mismatch and this seat's floor reading are recorded on finding(pm-dispatch):CONTRACT_REVIEW_TIER = 'claude-fable-5'no longer matches the served Fable tierclaude-fable-5-1, so the in-seat review fuse reads a literal mismatch on every current Fable seat #14303 and remain open for maintainer veto.Pre-checks (all on head
9332aca5)check-clause2-carriers.mjs --pair).origin/main: CLEAN.@objectstack/specminor,@objectstack/lintpatch — both published packages covered.api-surfaceandexport-originskernel products regenerated for the one new const.Landing
needs:contract-reviewis cleared on this card and on PR fix(spec): view/layout-without-binding covers timeline, map and tree; new view/tree-without-parent-field #14331 in the same stroke as this verdict.buttonrow to the spelling its key reaches #14321, fix(spec): withdraw the offered-but-refusediconinput from the object.form options repeater #14326 and feat(spec): refuse an auto-launched flow whose stack omits requires: ['triggers'] #14330; iforigin/mainmoves on shared generated files before its turn, the branch is resynced withscripts/pm/os-regen-merge.shfirst.Generated by Claude Code