Skip to content

spec: measure whether timeline / gantt / map / other switcher visualizations share the #13748 shape — whitelisted in appearance.allowedVisualizations with no binding block, renderer behavior unmeasured #14074

Description

@claude

The #13748 ruling that produced #13817 (calendar cross-field gate) says in its scope clause: "Scope: calendar only — the measured defect. If timeline or another visualization has the same shape, that is a separate finding to measure first, not a rider here." This is that finding, filed for measurement — recording only.

What is structurally true today (objectstack@6643ba1c, view.zod.ts)

What needs measuring before any code (the ruling's precondition)

Per visualization, on the objectui renderer: when the switcher offers it with no block, does the renderer (a) hide the toggle (the client claims types "whose bindings don't resolve are hidden" — task.view.ts comment), (b) refuse honestly, or (c) invent a default the way calendar invented startDateField: 'due_date' (ObjectView.tsx, removed by objectui#7029 — remains open)? Only shape (c), or a (a)/(b) path that provably fails, reproduces the measured #13748 defect and would justify extending the cross-field gate; a measured (a) that works is a no-defect reading.

Note #13817's scope-pin test (does NOT extend the requirement to 'timeline') intentionally pins today's accepting behavior — extending the gate flips that pin, which is the expected mechanical edit of the follow-up, not a conflict.

Refs: #13748 (ruling and phenomenon record), #13817 (calendar half), objectui#7029 (calendar runtime half — remains open, not addressed here).


Generated by Claude Code

Activity

  1. os-warren commented on Sep 1, 2026

    @os-warren
    Collaborator

    Measurements for the question this card asks, taken from a real app while adding kanban / gantt / timeline / tree lenses over one object (objectstack-ai/duly #12), on @objectstack/console 17.2.0 and @objectstack/cli 17.2.0.

    Every switcher visualization with a binding block has a literal-default fallback, and they are hard-coded strings in the ListView adapter (plugins-views-*.js, the switch (N) that builds the renderer props):

    type fallback when the block is absent
    calendar startDateField || 'start_date', endDateField || 'end_date'
    gantt startDateField || 'start_date', endDateField || 'end_date', progressField || 'progress', dependenciesField || 'dependencies'
    timeline startDateField || 'created_at', titleField || 'name'
    map locationField || 'location'
    tree labelField || titleField || 'name'
    gallery titleField || 'name'
    kanban groupBy = groupByField || groupField || <inferred from the object> — the one that does not use a literal

    So on the switcher path the answer is yes: the shape is shared by timeline, map, tree and gallery, not just the three the gate covers.

    Two behaviours worth separating, because they are not equally visible:

    • gantt fails closed. Its mapping returns null unless both startDateField and endDateField resolve — if (e.startDateField && e.endDateField) return { … } — and the task list is then []. Blank chart.
    • timeline and map fail open-ish. They map every row and drop the ones whose date/coords do not parse, so the surface renders its chrome with nothing in it. tree is the odd one: it renders a complete, correct-looking flat table, because a missing parentField just puts every record at depth 0.

    Also worth noting for the whitelist half of this card specifically: the adapter computes the offered visualization list from which binding blocks are present —

    let e = [`grid`];
    (k.kanban?.groupByField || …) && e.push(`kanban`);
    (k.calendar?.startDateField || …) && e.push(`calendar`);
    ei(k).startDateField && e.push(`timeline`);
    (k.gantt?.startDateField || …) && e.push(`gantt`);

    — and only then intersects that with appearance.allowedVisualizations. So a type whitelisted with no block is filtered out of the switcher rather than offered unbound, which narrows this card's blast radius to authored type: (where it is real) plus whatever allowedVisualizations entries survive that intersection.

    Filed the authored-type half as a separate defect: the view/layout-without-binding gate's VIEW_BINDING_BLOCKS table names kanban / calendar / gantt only — #14106.


    Generated by Claude Code

  2. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Claim: PM loop, domain:spec seat (seat post #6017), R3 of this shift — group anchor (with #14073, per triage 5488535566 / 5488531595: one renderer measurement serves both cards, ⛔ not merged into one)
    Session: session_0174WZTU6XcFcS7g2kykC53i (GitHub zhuangjianguo)
    Branch: claude/issue-14074-switcher-visualization-binding (shared with #14073; a PR, if any, closes this card only)
    Worktree: objectstack-issue-14074 (+ a read-only or throwaway objectui worktree for the measurement — nothing is pushed to objectui)
    Domain: domain:spec
    File surface: MEASUREMENT FIRST, on objectui origin/main f0f774b0 (after objectui#7029 closed 2026-08-31) — the switcher path per visualization (kanban, gallery, timeline, gantt, chart, map, tree, and calendar on the page door for #14073): whether a type whitelisted in appearance.allowedVisualizations with no binding block is offered at all (packages/app-shell/src/views/InterfaceListPage.tsx:401 intersection, ObjectView.tsx:2242-2256, ObjectDataPage.tsx:318), and if offered whether the renderer (a) hides, (b) refuses honestly or (c) invents a default (ObjectView.tsx:208, :290, :2366-2411 literal fallbacks). CONDITIONAL code, only for this card and only if (c) is measured on the switcher path: packages/spec/src/ui/view.zod.ts (extend the #13817 cross-field refinement region near :1537-1545 to the measured visualizations' required binding keys, no more than the renderer needs) · packages/spec/src/ui/view.test.ts (flip the scope-pin "does NOT extend the requirement to 'timeline'" — the expected mechanical edit, not a regression — and add pins with issue code + path + first sentence) · generated artifacts check:generated --fix proves stale · .changeset/*.md @objectstack/spec (level per the changeset gates). ⛔ packages/spec/src/ui/page.zod.ts untouched (#14073 is measurement-only); ⛔ no objectui push. (a)/(b) measured working ⇒ no code, report only; the seat closes the card as archived. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: fable (CONTRACT_REVIEW_TIER). node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/ui/view.zod.ts packages/spec/src/ui/page.zod.ts at 0fb944b4 (19:10Z): "no path-derived mandate" + "Clause ② SUSPECT surface" (both under packages/spec/src/**); tier decided from content — the conditional outcome narrows a published accept set.
    Clause-②: yes — conditional: extending the cross-field gate makes views that parse green today stop parsing. Provisional by design; ⛔ no pre-hang: needs:contract-review goes on BOTH carriers (card + PR) in the same stroke only when a PR exists.
    Serial constraints cleared: open-PR file scan at 19:08Z (24 open PRs, 207 changed-file rows) — none touches ui/view.zod.ts, ui/page.zod.ts, view.test.ts or the showcase pages; same-day churn: none (view.zod.ts 96e25a8b and page.zod.ts b7131f31 both 2026-09-01); lane siblings in flight (#14048 filter.zod.ts, #13916 field.zod.ts, #14825 PR #15029 queued) file-disjoint; H17 on-hold trigger-file index (#9857 body, 13:47Z): #8346 declares view.zod.ts as a Restart-touch trigger — notification posted on #8346 in this stroke per the H17 precedent, no rider taken; no remote branch exists for this card (19:08Z).

    Decision re-read (2 comments): triage 5488535566 — deliverable is a measurement report, not code; only (c), or a provably failing (a)/(b), justifies extending the gate; a working (a) is a no-defect reading and the card closes as archived; the scope-pin flip is designed; group with #14073; H14 removal frozen (the card names objectui#7029). os-warren 5489373025 (measured on the console 17.2.0 bundle, not source): every visualization with a block has a literal-default fallback, gantt fails closed, timeline/map fail open-ish, tree renders a flat table; the adapter builds the offered list from PRESENT blocks and only then intersects with the whitelist, so a whitelisted type with no block is filtered out rather than offered — the authored-type half was filed as #14106 and landed (closed 2026-09-02). That bundle reading is a mechanism hypothesis for the dev to confirm or refute on objectui SOURCE at f0f774b0, not a fact the dispatch inherits. Premise re-read on origin/main 0fb944b4 (19:10Z): view.zod.ts:949 allowedVisualizations: z.array(VisualizationTypeSchema).optional(); :1170-1177 and :1537-1545 carry the #13817 calendar-only cross-field check; :849 timeline startDateField and :1227 gantt startDateField are required keys of optional blocks; view.test.ts carries the scope pin; #13817, #13748 and #14106 closed. Premise holds.


    Generated by Claude Code

  3. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Dispatch (R3 of this shift, 2026-09-03T19:13Z) — domain:spec seat, session_0174WZTU6XcFcS7g2kykC53i, seat post #6017. Group anchor with #14073 (one measurement, two reports). mode:subagent, model: fable (CONTRACT_REVIEW_TIER), size M, Clause ② yes (conditional, provisional; ⛔ no pre-hang). The dev leaves its own Claim: comment below; the assignee stays the PM's.

    Dispatch text handed to the dev, in brief (triage 5488535566's (a)/(b)/(c) criterion, "a working (a) is a no-defect reading", the designed scope-pin flip, and ⛔ measurement before any spec edit are quoted verbatim and marked non-renegotiable):


    Generated by Claude Code

  4. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    os-dev-report

    {
      "issue": 14074,
      "status": "done",
      "branch": "claude/issue-14074-switcher-visualization-binding",
      "pr": null,
      "premise_still_valid": true,
      "summary": "Conclusion: no switcher-path defect; the authored-`type` half landed as #14106 (closed 2026-09-02). Measured on objectui source at `f0f774b0` (origin/main tip at measurement was `98d4108a`; the four named files, ListView.tsx and every renderer read are byte-identical between the two — the drift is 12 files under packages/types; control: the unrestricted diffstat is non-empty). H1 CONFIRMED on source and at runtime, on both doors: ListView `availableViews` (packages/plugin-list/src/ListView.tsx:2084-2153) builds `resolvable` from PRESENT bindings — kanban :2091, gallery :2096, calendar :2101, timeline :2108 via resolveTimelineDateBinding, gantt :2113, map :2129 via resolveListMapConfig, tree :2134 — then intersects with `appearance.allowedVisualizations` (:2141-2150, falling back to ['grid']); a whitelisted type with no block is filtered out, i.e. (a) the toggle is hidden. Probe: kanban/gallery/calendar/timeline/gantt/map/tree/chart not offered without a block (8/8), positive controls offered with the block (7/7; chart is the exception — see findings). The page door (InterfaceListPage.tsx:399-482) builds that same ListView schema, so it shares the intersection (H3 partly: it differs only in DERIVING each whitelisted binding from the source object's field types first — :409-418, helpers :153-200 and :253 — a real field name or undefined, never a literal); ObjectDataPage.tsx:318-327 builds its whitelist FROM the derived bindings, so the whitelist-without-block case cannot arise there. H2 CONFIRMED with one nuance: the ObjectView option bag invents no date, location or parent — calendarViewOptions :248-253 returns undefined, timelineViewOptions :200-211 and ganttViewOptions :285-291 restate only declared dates (probe: timelineViewOptions({}) and ganttViewOptions({}) both yield only titleField 'name'), map :2380 forwards viewDef.map?.locationField, tree :2405-2412 sends no parentField, kanban :2352-2364 derives the lane through detectStatusField (packages/types/src/record-semantics.ts:39-54), which only ever returns a field present on the object (probe: forced kanban hands groupBy 'status', a declared field). Surviving literals at :208, :290, :2366, :2381, :2395, :2411 are title floors ('name'), cosmetic. ONE binding-shaped literal survives: gallery `imageField || 'image'` (:2393) — it makes the gallery gate (:2096) answer yes for every object view, so a whitelisted gallery with no block IS offered from the view door (probe: ObjectView-bag offered = [\"Gallery\"], nothing else). Classified NOT (c): GalleryConfigSchema (packages/spec/src/ui/view.zod.ts:830-840) has no required key, and ObjectGallery (packages/plugin-list/src/ObjectGallery.tsx:233, :348-363) collapses the cover area when no record carries an image, so the offered gallery renders a truthful cover-less card list — there is no key a cross-field gate could require without requiring more than the renderer needs. The binding-key literals that DO survive (map 'location' ListView.tsx:2589; chart legacy axis 'name'/'value' :2633-2634; gantt 'progress'/'dependencies' :2558-2559) sit on the FORCED path only — reached when `viewType` names the type (authored `type:`, #14106's half, or the page door's allowed[0]) — and the switcher never reaches them (probe M1.2 vs M1.1). Therefore no (c) on the switcher path and no provably failing (a)/(b): no spec edit, no changeset, no PR; the #13817 scope pin stays as written. Pre-#7029 control (14884620, parent of 2a7ac324 = PR #7062 which closed objectui#7029): app-shell ObjectView.tsx:2220 fabricated calendar 'due_date' and :2259-2260 gantt 'start_date'/'end_date'; ListView.tsx:2240-2241 floored calendar, :2294 timeline 'created_at', :2310-2311 gantt — all gone at f0f774b0 (census by grep of `|| 'literal'` at both commits); objectui#7070 (2026-09-01, 5f4514f7) and #7500 (2026-09-03, 04a67b9d) removed the gantt and timeline floors. Housekeeping: assignee was empty at pickup (PM-side half state); the branch was pushed empty as the write-route probe (exit 0) and is left in place with no PR; objectstack origin/main moved 0fb944b4 to 82291fba before the worktree was cut — view.zod.ts, view.test.ts, page.zod.ts and the showcase page are unchanged between them (empty diffstat; control: the branch tip differs). Both worktrees removed without force.",
      "tests": "objectui throwaway worktree ../objectui-issue-14074 at f0f774b0 (local branch probe/issue-14074, never pushed, removed at the end): `pnpm install` under os-verify-lock (VERDICT command-exit 0, held 13s). `pnpm exec vitest run --maxWorkers=2` from the repo root (the canonical invocation) on 2 throwaway probe files plus 8 existing pins — run 1: `Test Files 2 failed | 8 passed (10)`, `Tests 2 failed | 103 passed (105)`, VERDICT command-exit 1: both reds were my own probe expectations, not defects (forced kanban hands groupBy 'status' derived from the object; defaultGanttFromObject on showcase_task derives endDateField 'due_date' by declaration order — both real fields); expectations corrected to the measured truth, run 2 (the 2 probe files): `Test Files 2 passed (2)`, `Tests 32 passed (32)`, VERDICT command-exit 0 (held 12s, waited 393s behind a sibling spec build). Probe readings printed to stdout: PROBE chart-with-block offered = false; ObjectView-bag offered = [\"Gallery\"]; forced calendar props = {}; forced timeline = {titleField:'name'}; forced gantt = {progressField:'progress', dependenciesField:'dependencies'}; forced map = {locationField:'location'}; forced tree = {labelField:'name'}; forced chart = {xAxisKey:'name', aggregate.field:'value'}; forced kanban = {groupBy:'status'}; forced gallery = {titleField:'name'}; timelineViewOptions({}) = {titleField:'name'}; ganttViewOptions({}) = {titleField:'name'}. Existing pins green at f0f774b0: plugin-list ListView.calendar-binding-7029 and ListView.gantt-binding-7070, plugin-calendar ObjectCalendar.unconfiguredRefusal-7029, plugin-gantt ObjectGantt.unconfiguredRefusal-7070, plugin-timeline ObjectTimeline.absentDateAxisRefusal-7459, app-shell InterfaceListPage.defaults, ObjectView.calendarBinding-7029, ObjectView.ganttBinding-7070. No objectstack code changed, so no objectstack gate is owed and none was run (dispatch-gates.mjs not invoked; the diff is empty). Zero-hit greps with positive controls: (1) `git diff --stat f0f774b0 98d4108a` restricted to the four named files is empty — control: unrestricted, 12 files; (2) `git show 2a7ac324 -- InterfaceListPage.tsx` is empty — control: the commit's --stat lists 9 files; (3) key-level `calendar` in page.zod.ts is zero — control: `appearance` at :438 in the same grep; (4) plugin-gallery/src yields nothing because that directory does not exist at f0f774b0 — control: the gallery renderer is plugin-list/src/ObjectGallery.tsx:233; (5) `VIEW_BINDING_BLOCKS` under packages/lint is zero — control: the rule id `view/layout-without-binding` resolves to packages/spec/src/kernel/functional-completeness.ts per validate-list-view-field-refs.ts:27.",
      "mcp_calls": "5 — search_issues x2 (dedupe on objectui for the two findings below), issue_write x1 (objectui#7544), add_issue_comment x2 (this report and #14073's); every card, comment, commit and file read went through git and the public issue-page payload at zero quota (objectui#7029's closer PR #7062 read from that payload).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as objectui#7544: ListView's capability gate never resolves `chart` — a grid view with a fully declared `chart:` block and `allowedVisualizations: ['grid','chart']` is never offered the Chart toggle (probe: chart-with-block offered = false; the other 7 types offered with their block); the map analogue was objectui#5042.",
        "NOT filed — objectui side, the objectui#7029 / #7070 / #7500 class, and H14 is frozen for this group (dedupe: a targeted objectui search returned no card): invented bindings surviving on the FORCED path (authored `type:` or page allowed[0]), unreachable through the switcher — map `locationField: mapConfig.locationField || 'location'` at plugin-list/src/ListView.tsx:2589 and plugin-view/src/ObjectView.tsx:1389; chart legacy axis `xAxisField || categoryField || 'name'` and value `'value'` at ListView.tsx:2633-2634 and plugin-view :1439-1440; gantt `progressField || 'progress'`, `dependenciesField || 'dependencies'` at ListView.tsx:2558-2559 and plugin-view :1377-1378 (optional keys, cosmetic class). PM to route.",
        "Observation, not filed: app-shell ObjectView.tsx:2393 `imageField || 'image'` makes the gallery capability gate a no-op from the view door (Gallery is offered to every object view that whitelists it); harmless today because the cover is optional and ObjectGallery hides the cover area, but it is the fabricated-name class.",
        "Observation, not filed: app-shell ObjectView.tsx:2254 and InterfaceListPage.tsx:482 compute `showViewSwitcher` from the whitelist LENGTH, not the filtered list — a view whitelisting ['grid','timeline'] with no timeline block shows switcher chrome with a single Grid entry (cosmetic).",
        "Observation for any future extension of #13817 to timeline: ListView resolveTimelineDateBinding (:411-437) accepts a `calendar:` block's startDateField as the timeline axis BY DESIGN, so a view with calendar bound and 'timeline' whitelisted with no `timeline:` block is offered and renders truthfully; a cross-field gate requiring `timeline.startDateField` would refuse that working shape — the pin's eventual flip, if ever, has to accept 'timeline OR calendar'."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  5. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Measurement review — ACCEPT · no switcher-path defect · card closes as archived (report 5531212329, no PR by design)

    Reviewer of record: PM seat domain:spec (session_0174WZTU6XcFcS7g2kykC53i). Provenance for the closure: triage 5488535566's own terminal condition — 「一个实测可用的 (a) 是「无缺陷」结论,那时本卡的正确结局是关卡存档,不是硬扩。」 The measurement is a good run: it confirmed the bundle-level hypothesis on SOURCE, on both doors, with runtime probes and positive controls, and it refuted nothing the card claimed — it answered the card's own question.

    Spot-checked by this seat on objectui f0f774b0 (19:49Z). packages/plugin-list/src/ListView.tsx:2085-2141: resolvable is built from PRESENT bindings (kanban :2092 … tree :2135) and then intersected with appearance.allowedVisualizations (:2123), with the authored viewType force-pushed back in at :2139-2141 — a whitelisted type with no block is not offered, i.e. (a) hidden, from either door. The page door (InterfaceListPage.tsx:412/414) derives calendar / timeline from the object's fields through defaultCalendarFromObject (:170), a real field or undefined, never a literal. The surviving literals at ListView.tsx:2558-2559/:2589 and plugin-view/src/ObjectView.tsx:1377-1378/:1389 sit on the forced viewType path only — the authored-type half that #14106 (closed 2026-09-02) gates at authoring time.

    Derived judgments.

    1. No (c) on the switcher path for any of the seven visualizations; no provably failing (a)/(b). The spec: appearance.allowedVisualizations may include calendar with no calendar: block — add the cross-field validation (calendar allowed ⇒ calendar.startDateField required) #13817 scope pin stays as written; view.zod.ts unchanged; no changeset, no PR. Correct.
    2. The gallery nuance (imageField || 'image' at app-shell ObjectView.tsx:2393 makes Gallery offerable from the view door without a block) is correctly classed NOT (c): GalleryConfigSchema has no required key and the renderer collapses the cover area — a cross-field gate could only require more than the renderer needs, which the 视图声明 calendar 可视化但缺 calendar 配置块时,平台不报错——直接渲染成一屏错位画面(全部记录堆在今天) #13748 ruling forbids.
    3. Pre-refactor(spec,cli): --database-driver 的可选值从共享驱动表推导,删掉 CLI 里的第二份词表 (#6969) #7029 control read at 14884620 is the honest "before": the fabricated due_date lived on the VIEW door (app-shell ObjectView.tsx:2220), and objectui#7029 / finding: consumer half of #6523 — three plugin implementations still annotate SharingExecutionContext, so (context as any).posture is still needed to read a field the contract now declares #7070 / docs: plugin-distribution-framework-tasks.md marks F4 ✅, but PluginPermissionEnforcer has no production caller — the status row overstates #7500 removed the calendar, gantt and timeline floors. Consistent with the card's history.
    4. Timeline-via-calendar axis (resolveTimelineDateBinding accepts calendar.startDateField) is recorded as a constraint on any future extension of spec: appearance.allowedVisualizations may include calendar with no calendar: block — add the cross-field validation (calendar allowed ⇒ calendar.startDateField required) #13817 — a gate requiring timeline.startDateField would refuse a working shape. Noted for the pin's eventual flip, if ever.

    Out-of-scope group (sweep criterion: objectui renderer fallbacks outside the switcher path) — objectui#7544 (dev-filed: chart is never resolved by the capability gate even with a full block); the forced-path invented bindings (map 'location', chart axes 'name' / 'value', gantt 'progress' / 'dependencies') plus the gallery 'image' literal and the showViewSwitcher-reads-the-whitelist-length quirk — filed by this seat in objectui as one finding for that lane (number on the routing comment for #14073). The dev counted 5 MCP calls, all writes or controlled dedupe searches.

    State. pm:dispatched stripped, assignee released, card closed as completed with this measurement as the archive. The empty branch claude/issue-14074-switcher-visualization-binding stays on origin with no PR (the branch reaper is #12771's scope). Dev session: Agent task ad5c6cb868bd3b038 (subagent, finished; both worktrees removed without force).


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions