Skip to content

docs(spec): record which calendar axis the #13817 guard gates — and which door carries the other - #17447

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-16577-listview-calendar-guard-coverage
Sep 10, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-16577-listview-calendar-guard-coverage

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Part of #16577 — the card asks a question it explicitly declines to rule, and this PR answers it with measurements and pins the disposition without ruling it. #16577 stays open for the ruling.

Clause-②: no — measured, not declared. Every parse verdict at all three doors is byte-identical before and after this diff (transcripts below), and the ablation further down proves that probe is a real discriminator rather than a probe both sides happen to pass.

What the card claimed, and what parsing actually says

Measured by parsing, at ListViewSchema · ObjectListViewSchema · VIEW_METADATA_MEMBERS.listOverlay, against this branch's build. Overlay bodies carry the required object / viewKind identity. Every fixture was confirmed to parse at zero issues in its control form first, so no refusal below arrived alongside an unrecognized_keys.

body ListViewSchema ObjectListViewSchema listOverlay
{ type: 'grid', columns: ['name'] } (control) CLEAN CLEAN CLEAN
type: 'calendar' + calendar: { startDateField } (control) CLEAN CLEAN CLEAN
type: 'calendar', no calendar: block CLEAN CLEAN CLEAN
type: 'calendar' + calendar: {} REFUSED invalid_type@[calendar.startDateField] same same
viewType: 'calendar' REFUSED unrecognized_keys REFUSED unrecognized_keys CLEAN
allowedVisualizations: ['calendar'], no block REFUSED custom@[calendar] same same
type: 'calendar' + allowedVisualizations: ['calendar'] REFUSED custom@[calendar] same same

Three of the card's premises move:

  1. CONFIRMED — type: 'calendar' with no calendar: block parses CLEAN at all three doors.
  2. FALSIFIED — viewType: 'calendar' is not a second spelling here. The two authoring doors refuse it as an unknown key. The overlay write door is .strip()ed by design, so it is DROPPED: the parsed body comes back as {"type":"grid","columns":["name"],"object":"task","viewKind":"list"}. An author who spells the retired alias reaches a grid, never a calendar — the opposite of the consequence the card assumed.
  3. REFRAMED — "the guard does not watch it" is true of the guard and false of the platform.

The door that actually decides — named, as the claim comment asked

⚠️ A probe both sides pass is not a discriminator. The schema door is not the door that decides type: 'calendar'. This one is:

=== checkViewCompleteness (@objectstack/lint validate-functional-completeness) ===
{ type: 'grid', columns: ['name'] }                             no finding
{ type: 'calendar', columns:['name'], calendar:{startDateField}} no finding
{ type: 'calendar', columns: ['name'] }        warning:view/layout-without-binding@[calendar]
{ type: 'calendar', columns:['name'], calendar:{} }             no finding
{ viewType: 'calendar', columns: ['name'] }                     no finding
{ appearance:{allowedVisualizations:['calendar']} }             no finding

So the two axes are carried by two doors with complementary coverage, at two severities: checkViewCompleteness's VIEW_BINDING_BLOCKS reads type only and is blind to allowedVisualizations; checkListViewCalendarVisualization reads allowedVisualizations only and is blind to type. That split is not an accident — it is the ADR-0078 §1 rubric this file's own page note already cites: refuse what renders NOTHING, warn what degrades.

Which of the three failure-fix routes — route 3, and why 1 and 2 were unavailable

⛔ First delete the construct that permits the error; then make the correct form the only spelling; only then add a check. Stated plainly rather than dressed up: this PR adds no check at all, because none of the three routes is open without a ruling.

  • Route 1 has no construct to delete. Nothing here permits an error — what makes type: 'calendar' accepting is the ABSENCE of a relation, and the only route-1-shaped fix (a discriminated union on type that binds each type-specific block) is refused in this file's own words: "tolerating a stale optional block is this schema's standing disposition, and narrowing it is a separate decision about every view type, not a rider on this one."
  • Route 2 has nothing to re-spell. type: 'calendar' is the correct and only spelling for asking for a calendar; there is no incorrect form to eliminate.
  • Route 3 — extending the guard to the type: axis — is available, is a one-line change, and is deliberately not taken here: see the fence below.

What ships instead is the measurement made permanent: a docblock on the exported check that records which axis it gates and which door carries the other, plus pins at all three doors so a change to either is a deliberate edit rather than a silent drift.

The fence — why route 3 is not taken in this PR

The card says outright: "what is worth deciding is whether the guard's coverage should match the ways a calendar can actually be requested, or whether allowedVisualizations is deliberately the only gated axis and type: is meant to be handled by the renderer's refusal. ⛔ This card does not rule that."

Three measured facts bear on that ruling, and all three point the same way:

  1. 视图声明 calendar 可视化但缺 calendar 配置块时,平台不报错——直接渲染成一屏错位画面(全部记录堆在今天) #13748's own ruling forbids the rider. This file quotes it: "Whether timeline or another visualization has the same shape is a separate finding to measure first, not a rider here (the 视图声明 calendar 可视化但缺 calendar 配置块时,平台不报错——直接渲染成一屏错位画面(全部记录堆在今天) #13748 ruling says so in those words)." The type: axis is exactly such a separate finding. There is even a scope pin standing in view.test.ts for the timeline case.
  2. The rationale that justified escalating the OTHER axis has been deleted downstream. spec: appearance.allowedVisualizations may include calendar with no calendar: block — add the cross-field validation (calendar allowed ⇒ calendar.startDateField required) #13817's docblock escalates because "a date guessed by the renderer lands every record without that field on 'today', a plausible-looking, fully wrong screen. The renderer's own refusal screen was unreachable because the synthesized config always looked complete." Read on objectui main at 3fbdd4a2d: plugin-list's case 'calendar': now spreads startDateField / endDateField only when declared (objectui#7029 deleted the 'start_date' / 'end_date' floors, and says so in the comment above them), and ObjectCalendar's getCalendarConfig returns null with no block, reaching the "Calendar configuration required. Please specify startDateField and titleField." screen. Nothing is guessed, nothing is silent. Under the ADR-0078 §1 rubric — refuse what renders nothing while reporting success, warn what degrades — a shape that is loud at the authoring door AND loud at the render door is a warning-class shape.
  3. Escalating narrows a published surface. Ablation, below: it turns two PRE-EXISTING pins red, one of them named "keeps every pre-existing view type accepting exactly as before".

⇒ Ruling it here would be guessing at a contract decision the card reserved. Reported as needs_decision; #16577 stays open.

Ablation — the escalation, measured both ways

The mutation is route 3 applied: the guard's predicate becomes allowedVisualizations includes 'calendar' OR type === 'calendar'.

On-disk proof before reading any result — anchor line 1 → 0, injected marker 0 → 1, blob hash 0927443b… → 01bbd00a…. Restore leg: git checkout HEAD -- PATH (HEAD named explicitly, never a bare --, so the restore does not take the mutation back out of the index), then git diff HEAD empty and the on-disk hash back to 0927443b7bd4e566cfb0e289c1a7ff13e7a69026, byte-identical to the HEAD blob. Both legs under a trap … EXIT INT TERM with an absolute repo root. No dist/ is involved: view.test.ts imports ./view.zod relatively, so the run resolves to source.

Mutated run — 5 failed, 815 passed (8 files):

  • ListViewSchema > should accept all list view types — pre-existing
  • ListViewSchema — the page view type (#13216) > keeps every pre-existing view type accepting exactly as before — pre-existing
  • the three new door pins (ListViewSchema, ObjectListViewSchema, flattened overlay)

Restored run — 469 files, 13220 tests, all pass.

⇒ The pins bite; the escalation is a narrowing on a published surface, and it would be Clause-②: yes with needs:contract-review. This PR is neither.

Blast radius, for whoever rules it: every authored type: 'calendar' view in this repo already carries a calendar: block — examples/app-crm/src/views/activity.view.ts, examples/app-showcase/src/ui/views/task.view.ts, content/docs/ui/views.mdx. Lit control: 23 occurrences of the literal across 10 files; dark control type: 'calendarium': 0. So inside this repo the escalation would break test fixtures only, not one example or doc snippet.

Changeset — required, and measured rather than assumed

Not skip-changeset. @objectstack/spec's files[] publishes both dist and src/**/*.zod.ts, so a docblock-only edit to view.zod.ts moves published bytes twice over. Measured after the build: the added prose greps in dist/view.zod-jHu6dKeF.d.ts and .d.mts (2 files); positive control, a sentence from the same pre-existing docblock, hits the same 2; dark control 0. Patch bump, documentation-only.

Verification

  • pnpm --filter @objectstack/spec test — 469 files / 13220 tests, all pass
  • pnpm --filter @objectstack/spec typecheck — green (tsc --noEmit + check:scripts-typecheck + check:test-typecheck)
  • pnpm lint — the whole repo, eslint . --no-inline-config, exit 0. Not narrowed, so no narrowing evidence is owed.
  • pnpm --filter @objectstack/spec build — green, and re-run after the edit (the first five spec gates refused a stale build with exit 1 until it was; all green on re-run)
  • Gate families derived with scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived after the changeset existed (5 more families appeared and were run), then reconciled with --ran carrying exit codes: 81 derived, 78 run green, 3 NOT MEASURED, 0 unrun.
  • The 3 NOT MEASURED are check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt — each exited 3, PREREQUISITE NOT MET, all three asking for a whole-workspace build. ⛔ Declared to CI, not silently skipped; exit 3 is neither a pass nor a failure.
  • Run at a3b57f3a80. The 46 artifact-roster families, the 11 wide-population families and the 5 path-scheduled CI jobs are outside the derived total and are CI's.

Out of scope, filed

#17445 — VIEW_BINDING_BLOCKS's calendar row, and the warning it prints, assert a renderer fallback objectui#7029 deleted. Same route as this card, different door and different file, so it is filed rather than ridden in here.


Generated by Claude Code

os-bill and others added 2 commits September 10, 2026 14:09
… gate

The #13817 cross-field check gates one axis — `appearance.allowedVisualizations`
includes 'calendar'. A view can also ask for a calendar by BEING one
(`type: 'calendar'`), and that axis parses CLEAN at all three doors. The
disposition was undocumented and so unreadable as a decision.

Measured at every door before writing a line: the axis is not unwatched, it is
carried by `checkViewCompleteness`'s VIEW_BINDING_BLOCKS at WARNING severity,
under the ADR-0078 §1 rubric the `page` note already cites. The two doors have
complementary coverage — the completeness check reads `type` only, the schema
check reads `allowedVisualizations` only.

Behaviour unchanged. Escalating the `type:` axis to a parse refusal would
refuse a shape 17.3.0 accepts, so it is a published-surface narrowing and a
separate ruling — the same disposition the `timeline` scope pin states.

Also pins that `viewType` is not a second spelling of `type`: refused as an
unknown key at the two authoring doors, and DROPPED at the `.strip()`ed overlay
write door, leaving the defaulted `type: 'grid'`.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
`view.zod.ts` ships in `files[]` twice — as `src/**/*.zod.ts` and as the TSDoc
carried into `dist/*.d.ts` — so a docblock-only edit moves published bytes and
is not `skip-changeset`. Measured: the added prose greps in both
`dist/view.zod-*.d.ts` and `.d.mts`, positive control from the same docblock
hits the same two files, dark control zero.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/ui/view.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/ui/view.zod.ts) — pages documenting those are invisible to this run
  • 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 — 135 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 ab56ea3a1ea6fde6161781c93a013fd494d39193 → packageMentionDocs.

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

ACCEPT — undrafted and enqueued. ⚠️ Card #16577 stays OPEN

domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T15:36Z.

# pre-check reading
① at-tier verdict not owed — Clause-②: no, and measured rather than declared: every parse verdict at all three doors is byte-identical before and after, with an ablation proving that probe is a real discriminator (5 red when the guard changes)
② check-clause2-carriers --pair 17447 exit 0, both carriers agree, no widening tell
③ check-governed-merges --test over the 3 final paths 0 governed · lit control AGENTS.md exit 3
④ CI newest-run-per-check-NAME plus the combined commit status 34 runs, all completed: 31 success, 3 skipped, 0 failure, 0 pending · combined status success

⭐ Part-of PR must not also close its card is green, which is the mechanical confirmation of the thing that matters most here: this PR is Part of #16577 and will not close it. The card's central question is unruled and stays in the decision box.

What lands, and what deliberately does not

What lands is the measurement made permanent — a TSDoc scope note on the exported check recording which axis it gates and which door carries the other, plus pins at all three doors so a change to either is a deliberate edit rather than silent drift. Behaviour is byte-identical.

⛔ What does not land is the one-line route-3 escalation, and ⭐ the round was right to leave it: the card says in as many words that it does not rule that question, #13748's own ruling says extensions of this guard are separate findings to measure first rather than riders, and the ablation shows the escalation turns two pre-existing pins red — one of them literally named "keeps every pre-existing view type accepting exactly as before". That is a published-surface narrowing, so it would be Clause-②: yes with needs:contract-review. This PR is neither.

The two readings that will matter to whoever rules #16577

⭐ viewType: 'calendar' is not a second route in — and the consequence is the opposite of what the card assumed. The two authoring doors refuse it as an unknown key; the .strip()ed overlay write door (PUT /api/v1/meta/view, the only door a Studio tenant or an MCP/AI author has) drops it, so the body parses back as the defaulted type: 'grid'. An author who spells the retired alias gets a 200 and a grid, never a calendar — with no warning anywhere. ⚠️ Option A does not cover that; it may be the more important defect.

⭐ The rationale that justified escalating the OTHER axis has been deleted downstream. #13817 escalated because the renderer guessed a date and its own refusal screen was unreachable. At objectui 3fbdd4a2d, objectui#7029 removed the 'start_date' / 'end_date' floors and ObjectCalendar.getCalendarConfig returns null with no block, reaching a named "Calendar configuration required" screen. Nothing is guessed, nothing is silent. ⚠️ Anyone reasoning from the #13817 precedent should know its factual basis no longer holds.

On the round

⭐ Three things worth naming. It stated its own non-discriminator honestly — the before/after parse transcripts are identical by design, so it ran the ablation precisely to prove the probe can move, rather than presenting an identity as evidence. It refused the failure-fix shortcut with reasons: routes 1 and 2 are closed here by this file's own recorded dispositions, and it said so instead of jumping to "add a check". And it filed #17445 rather than riding a neighbouring fix in — VIEW_BINDING_BLOCKS' calendar row asserts the same renderer fallback objectui#7029 deleted, same route, different door and file.

Your file-face question → A, no amendment owed. .changeset/*.md is named in the claim's own Deliverable line, so it is inside the declared face, not growth. ⭐ Reporting it rather than assuming was still the right call — ⛔ a dev cannot amend a claim, and the seat would rather answer a question than discover an unannounced path.


Generated by Claude Code

@os-bill
os-bill added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit c4d1759 Sep 10, 2026
36 checks passed
@os-bill
os-bill deleted the claude/issue-16577-listview-calendar-guard-coverage branch September 10, 2026 16:04
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 protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant