Skip to content

spec(ui): declare titleField on KanbanConfigSchema — the optional key its five item-titled siblings already carry - #18561

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-16894-kanban-titlefield
Sep 17, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-16894-kanban-titlefield

Conversation

@os-bill

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

Copy link
Copy Markdown
Collaborator

Fixes #16894

Clause-②: yes (widening)

KanbanConfigSchema now declares titleField as optional z.string(), executing the director seat's decision batch #87 (recorded at objectstack-ai/objectui#8367 comment 5582071618, confirmed by the maintainer verbatim 「批 #87 同意」). The direction was settled there; this PR is the implementation only.

What moved

file change
packages/spec/src/ui/view.zod.ts titleField: z.string().optional() on KanbanConfigSchema, plus the docblock that records why it is optional
packages/spec/src/ui/view.test.ts the card's four-leg probe (both controls firing on the same call shape) and the optionality leg
packages/spec/authorable-surface/ui.json regenerated — one added row, ui/KanbanConfig:titleField
content/docs/references/ui/view.mdx, content/docs/references/api/protocol.mdx, content/docs/references/data/object.mdx regenerated by gen:docs
.changeset/16894-kanban-config-titlefield.md @objectstack/spec: minor

Regenerated, never hand-edited: check:authorable-surface wrote the first and pnpm --filter @objectstack/spec gen:docs the rest. check:generated reports all 15 artifacts up to date.

Premise re-derivation — the card's sibling table does NOT match the tree

Re-derived on this worktree at origin/main 79a046f8c (the dispatch's own derivation ran one commit behind, at 582d3e5). git grep -n 'titleField' -- packages/spec/src/ui/view.zod.ts returns six carriers; mapping each to the schema whose member list encloses it:

line (pre-change) owning schema arity
:1057 GalleryConfigSchema (declared :1050) optional
:1071 TimelineConfigSchema (declared :1065) required
:1407 CalendarConfigSchema (declared :1401) optional — the ADR-0079 fallback docblock the ruling names
:1490 GanttConfigSchema (declared :1484) required
:1638 ListMapConfigSchema (declared :1631) optional
— KanbanConfigSchema (declared :1346) absent — the defect

Two corrections to the card body, neither of which disturbs the ruling:

  • The card's table calls Timeline optional. It is required on the tree.
  • The card's table lists five schemas. There are six carriers: ListMapConfigSchema also declares titleField, optional, and the card does not mention it.

The ruling is unaffected: it prescribes the arity directly (optional z.string()) and names CalendarConfigSchema's docblock as the reference, which is optional at :1407. So the shape landed here is the ruled one, and the card's optional/required split was simply not re-measured when it was written. The defect itself re-derives exactly as filed: KanbanConfigSchema is a strictObject and refused titleField by name.

Evidence

leg command verdict
build pnpm --filter @objectstack/spec build VERDICT command-exit 0
② typecheck pnpm --filter @objectstack/spec typecheck TYPECHECK_EXIT=0 — check:test-typecheck: OK
② tests pnpm --filter @objectstack/spec test TEST_EXIT=0 — Test Files 483 passed (483) / Tests 13775 passed (13775)
targeted pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/view.test.ts Test Files 1 passed (1) / Tests 376 passed (376)
artifacts pnpm --filter @objectstack/spec check:generated 15 of 15 up to date

① is empty by construction: packages/spec has no workspace dependencies, so --filter '@objectstack/spec^...' build has an empty closure. The package itself was built before any gate that reads dist/.

③ Gate families were derived on this worktree, not inherited: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands over the real 7-path change set yields 108 families. All 108 were run with each exit code landed to disk before being read, and reconciled with --ran:

dispatch-gates --ran: 108 derived family(ies) accounted for — 105 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)

103 green. The five non-zero results, none of them a finding against this diff:

  • pnpm check:dual-build-cjs-loads, pnpm check:lean-entry-closure, pnpm check:type-check-debt — exit 3, PREREQUISITE NOT MET, all three refusing because the whole monorepo is not built on this worktree. NOT MEASURED, declared to CI, which checks out and builds fresh.
  • node scripts/check-plugin-teardown-shape.mjs --self-test — exit 1, but a prerequisite refusal in its own words: "cannot read the positive control at 621a487607881c66b2899b7e3477115229a156b4 … Deepen the clone". This container's checkout is shallow. NOT MEASURED. The gate itself (without --self-test) ran green.
  • pnpm check:cross-package-test-inputs — exit 1, pre-existing on the base tree, proven by control rather than asserted. With all seven changed paths restored to 79a046f8c (and the changeset removed), the gate fails identically; the finding it prints names packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/, and this diff touches neither that test, nor turbo.json, nor any declaration table. Restore was proven by git diff HEAD empty plus a git hash-object match against every HEAD blob.

Lint, as a proven narrowing rather than a repo-wide sweep. pnpm exec eslint --no-inline-config --format json over the two changed TypeScript files: exit 0, 2 files, 0 errors, 0 warnings, the file count read from the JSON output's own length. The population it narrows from is 6798 files — computed from ESLint's own resolved config via ESLint#isPathIgnored over git ls-files, not estimated. The narrowing excludes nothing, and that is a property of this repository's config rather than a hope: eslint.config.mjs states it in its own comment — this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or not." With no rule reading types across a file boundary, a diff confined to these two files cannot move the verdict on any of the other 6796. The repo-wide pnpm lint run remains CI's.

Every reading above was taken at 6d01b4b, this branch's final commit, on a tree whose git status --porcelain is empty. origin/main had not moved from the branch point (79a046f8c) when the PR was opened, so no merge was owed.

Acceptance notes

File surface

Two files beyond the dispatch's declared surface, both generated by the mandated gen:docs run and neither hand-edited: content/docs/references/api/protocol.mdx and content/docs/references/data/object.mdx. The kanban config shape is inlined on those two reference pages as well as on ui/view.mdx, so each picks up titleField?: string in its kanban row. Omitting them would leave check:docs red. Every hunk in all three files is this one key and nothing else.


Generated by Claude Code

`KanbanConfigSchema` was the one item-titled view config of its family that
omitted `titleField`, while Gallery, Timeline, Calendar, Gantt and ListMap all
declare the key under the same name and the same `z.string()`. The schema is a
`strictObject`, so an author writing the key the board actually reads was
refused by name.

Declared optional, matching the shape `CalendarConfigSchema` already writes
down for this exact key: absence resolves through the ADR-0079 record
display-name chain, so requiring it would demand more than the renderer reads.
Timeline and Gantt spell it required and are the two siblings this declaration
deliberately does not copy.

Tests carry the four-leg probe with both controls firing on the same call
shape, plus the optionality leg.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
…tion

`authorable-surface/ui.json` gains `ui/KanbanConfig:titleField`; the three
reference pages that inline the kanban config shape pick up `titleField?:
string`. Regenerated, not hand-edited: `check:authorable-surface` wrote the
first and `gen:docs` the rest.

Adds the minor changeset for the widening.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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 — 136 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 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 67d8299911aca37ad3db044fbe890779d35791c7 — the merge of head 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff into base 79a046f8cdf085d95200826ee9bb2fa6584bc3d5, 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 67d8299911aca37ad3db044fbe890779d35791c7 && git checkout 67d8299911aca37ad3db044fbe890779d35791c7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff && git checkout -B drift-repro 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 && git merge --no-ff 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff

node scripts/docs-audit/affected-docs.mjs --json 79a046f8cdf085d95200826ee9bb2fa6584bc3d5

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

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 30/30 CONTRACT_REVIEW_TIER
Head-sha: 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff

Isolated, at-tier clause-② review of PR #18561 for card #16894, executing the director ruling (decision batch #87, objectstack-ai/objectui#8367 comment 5582071618, maintainer 「批 #87 同意」). The dispatch order and the dispatching seat's acceptance were withheld from this seat; every reading below was taken from the tree and the diff, on a detached worktree of the head, with the objectui pin 53ded82b fetched for the renderer readings. Live re-read: the PR head is still 6d01b4b6, two commits on base 79a046f8; origin/main has moved 16 commits since that base and none of them touches any of the 7 changed paths (git diff --stat 79a046f8..origin/main over the seven paths is empty), so the head reviewed is the head that would merge.

① Derived judgments

Each accept-set or public-surface change the diff produces, named and judged:

  1. KanbanConfigSchema gains titleField: z.string().optional() — packages/spec/src/ui/view.zod.ts:1370 at the head. Arity matches the ruled shape (optional, string, absence falls through). Judged RIGHT. Sibling table re-derived on the head, not quoted: Gallery :1057 optional, Timeline :1071 REQUIRED, Calendar :1426 optional, Gantt :1509 required, ListMap :1657 optional. The card's table (Timeline optional, five carriers) is wrong on both counts and the PR's correction is accurate; the ruling names the arity directly, so nothing in it is disturbed.
  2. Transitive accept sets: ListView.kanban, ObjectListView.kanban and the published metadata item body (api/protocol.mdx) widen by the same one key through schema reuse — one declaration, no second one anywhere. Judged RIGHT.
  3. packages/spec/authorable-surface/ui.json: exactly one added row, ui/KanbanConfig:titleField (file 1218 → 1219 lines; ui/KanbanConfig:* rows 3 → 4). No other shard, no authorable-surface.base.json re-anchor, no api-surface/ or api-surface-signatures.json movement (the latter hashes define* helpers only). Re-derived: pnpm --filter @objectstack/spec build then check:generated on the head worktree → 15 of 15 artifacts up to date, check:authorable-surface, check:api-surface, check:docs, check:liveness all ✓. Judged RIGHT, and regenerated rather than hand-edited.
  4. Generated reference pages: ui/view.mdx (+3 table rows in the KanbanConfig, ListView.kanban, ObjectListView.kanban nested-shape tables; 6 inline-shape lines rewritten), api/protocol.mdx (2 inline lines), data/object.mdx (1 inline line). Every hunk is this one key and nothing else — no smuggled second change. Judged RIGHT.
  5. Mechanical widening control: node scripts/pm/check-widening-tells.mjs --declaration yes --diff pr.diff → exit 0 (a yes is never blocked). The control run with --declaration no → exit 4 with exactly ONE tell, T1 at packages/spec/src/ui/view.zod.ts:1370, and no T2/T3/T4 — the diff carries one widening shape, and it is the declared one.
  6. The .describe() — "Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)" — measured against the pinned renderer rather than assumed. The key IS read: plugin-list/src/ListView.tsx:1464 projects kanban.titleField into the fetch, and plugin-kanban/src/ObjectKanban.tsx:302-341 tries the explicit field first and, when it yields nothing, calls getRecordDisplayName — the ADR-0079 resolver (core/src/utils/record-title.ts:28-40: nameField → displayNameField → titleFormat → type-aware derivation → Record #id). So a layer delivers the promised fallback. ⚠️ One rung sits ahead of it at the pin: app-shell/src/views/ObjectView.tsx:2388 floors an omitted viewDef.kanban?.titleField at the literal 'name' before the node sees it, pinned by ObjectView.kanbanLane-8193.test.tsx:129 ("floors titleField at name"). objectui#7029 deleted exactly that rung for calendar, which is why the identical Calendar describe is exact and this one is exact only for records with no non-empty name value. The wording is the ruled wording (「exactly as the CalendarConfigSchema docblock already states」) and the residual rung is renderer-side in the sibling repo, so this does not refuse the spec change; it is escalated in ③ (d). Judged RIGHT as ruled, with the rung named.
  7. Tests (view.test.ts:190-223): the four-leg probe — CONTROL-1 bogus key refused on the named surface, CONTROL-2 canonical accepted, PROBE titleField accepted AND survives the parse as a member — and the optionality leg (key absent on output when omitted). Mirrors the card's executable acceptance criterion. Re-run here: vitest run src/ui/view.test.ts src/ui/view-metadata-schema.test.ts → 426 passed. eslint on the two changed TS files → exit 0. Judged RIGHT.
  8. Nothing else widens: no enum/union member, no export, no registry row, no ADR text (the ruling forbids one), no governed surface in the file list.

② Semver level

.changeset/16894-kanban-config-titlefield.md → "@objectstack/spec": minor, carrying Clause-②: yes (widening) — the same declaration as the PR body line 3 and the card. Right level: a new optional key on a published strict accept set is a widening, yes takes at least minor, nothing admitted before is refused, nothing is renamed or retired, so no migration text, no tombstone and no ADR-0087 marker is owed. Gates run on the head tree: check-empty-changeset ✓ (1 declaring changeset added, none from the base modified), check-adr-0087-registration ✓ (non-breaking), check-changeset-no-major ✓. The changeset text matches the diff: the six-carrier sentence and the "Timeline and Gantt spell it required" sentence check against the head lines in ① item 1; the "generated projections" sentence names exactly the files that moved; "the key the board already reads" checks against ListView.tsx:1464 / ObjectKanban.tsx:303 at the pin. Judged RIGHT.

③ Boundary flags

  • (a) packages/lint gap — CONFIRMED on the head, and it is a metadata-authoring trap rather than a cosmetic note. validate-list-view-field-refs.ts:317-322 POSITIONS.kanban lists groupByField (error), summarizeField (warning), columns (warning) and no titleField, while calendar/gallery/map warn and gantt/timeline error on the same key. No test pins POSITIONS against the schema (the test table at validate-list-view-field-refs.test.ts:199-280 is hand-maintained), so CI stays green — and from this release a misspelled kanban.titleField is accepted by the spec, reported by nothing, and at the pin silently degrades to the resolver title (ObjectKanban.tsx:334-341). Demonstrated on the head lint sources with sibling controls: kanban: { titleField: "nope_field" } → no finding; the identical typo on kanban.summarizeField → warning, calendar.titleField → warning, gallery.titleField → warning, timeline.titleField → error (the lint suite itself: 112 passed). That is the ADR-0078 silently-inert shape; Prime Directive chore: version packages #10 says file, and the PR only noted it. Needs its own card (one-line fix: titleField: 'warning' in POSITIONS.kanban plus a test row). This seat's proxy refuses the search endpoint, so whether a card already exists is unverified — the adopting seat files or links one. Out of this card's declared scope; not a reason to refuse the diff.
  • (b) objectui#7742 conditional resolves to "no" — verified: component.zod.ts:2846 ObjectKanbanPropsSchema.titleField already exists as an optional legacy alias of cardTitle on a different schema with a different semantic, so one declaration cannot serve both faces and the card is correctly not widened. Agreed.
  • (c) KanbanConfigSchema carries no object-level .describe() where its siblings do — cosmetic, generated-docs only, out of scope. Agreed.
  • (d) NEW — renderer rung ahead of the ruled fallback (① item 6): objectui pin ObjectView.tsx:2388 || 'name'. When the objectui leg of objectui#8367 declares the member and narrows the refused residual (KNOWN_REFUSED_RESIDUAL in ObjectView.kanbanGroupByRetired-8213.test.tsx:102 → ['cardFields']), it should also drop that floor as objectui#7029 did for calendar, so that "omit" reaches the ADR-0079 chain directly and the describe is exact. Sibling-repo work; escalated, not blocking here.
  • (e) NEW, cosmetic — the new TSDoc docblock's chain (titleFormat → displayNameField → derivation → 'Untitled') is copied verbatim from Calendar's [spec: appearance.allowedVisualizations may include calendar with no calendar: block — add the cross-field validation (calendar allowed ⇒ calendar.startDateField required) #13817] docblock via the ruling's own quotation; ADR-0079 D2/D3/D6 in this repo and objectui record-title.ts:28-40 order it nameField first and floor at Record #id, with 'Untitled' only for an id-less record. Not in the .describe() that ships to the reference page; leave it with the sibling docblock it copies.
  • (f) Downstream unblock is installability, not merge: objectui#8367 stays blocked until a released @objectstack/spec carrying this is pinned in objectui. Nothing for this PR to do.
  • Gate families this seat could not measure here: check:react-declaration-parity (needs the objectui manifest — declared by check:generated as external input), the lint package's own vitest run (measured separately, see (a)). CI on the head: 34 success / 5 skipped / 0 failure, including Spec property liveness, Lint & Repo Gates, Test Core (6/6), TypeScript Type Check, Governed Surface Queue Guard.

Implemented-by: claude/issue-16894-kanban-titlefield
Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3

VERDICT: PASS


Generated by Claude Code

os-bill commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Clause-② adopted — PASS; both carriers stripped in this act

Review of record: PR-thread comment 5710778226.
Head judged: 6d01b4b63e07b5fbdc0069ce8f5143167b9813ff — re-read live at adoption and unmoved (draft: true, mergeable_state: clean), so the head judged is the head that would merge.

Adopted verbatim. A dispatching seat may adopt an isolated reviewer's record whole or void it whole; it may ⛔ never rewrite or polish one, and this seat did neither. Everything below is disposition of the flags the record raises — ⛔ not a supplement to its judgments.

needs:contract-review is stripped from both carriers in this same act: PR #18561 and card #16894.

Independence pair

  • Implemented-by: claude/issue-16894-kanban-titlefield — the branch that produced the diff (mode:subagent).
  • Reviewed-by: session_01JbZnqu8bt6YqfJsr9vaFb3 — the seat that renders/adopts. An isolated review subagent holds no session of its own, so contract-review.md records the adopting seat.
  • Different identities ⇒ no SELF-REVIEW.

Why an at-tier review exists here at all

This seat measures below CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs), so it is ⛔ barred from self-reviewing a clause-② card. It ran an isolated at-tier reviewer under the rule in #18511 — 「未达档席 ⛔ 不自审,起隔离达档子代理转录核档采信」 — on the maintainer's explicit instruction for this PR. That rule is not yet merged; this is stated plainly rather than implied.

Tier evidence, in the shape that rule demands: the reviewer parsed every type:"assistant" line of its own transcript for the harness-stamped per-request message.model and compared each against CONTRACT_REVIEW_TIER imported live from dispatch-gates.mjs — 30/30 at tier at posting, 32/32 on the final line, exactly one distinct model value, no fallback line anywhere in the run. ⛔ get_session was not used: inside mode:subagent it measures the dispatching session and is not mutual proof. The Served-tier: line of the record carries that 30/30 stamp control, and check-clause2-carriers --pair 18561 re-reads it at C7.

The reviewer was fed the card, the ruling it executes, and the PR body/diff — ⛔ not the dispatch order and ⛔ not this seat's own acceptance conclusions.

Disposition of the reviewer's boundary flags

Landing pre-checks, read on this head

# check reading
① in-seat clause-② PASS on record comment 5710778226, at-tier, Head-sha matches
② double carrier check-clause2-carriers.mjs --pair 18561 → exit 0 (both limbs agree, no widening tell, record on head) — re-run after the strip
③ PR checks 39 check-runs: 34 success · 5 skipped · 0 other; check-expected-skips.mjs --pr 18561 → exit 0, all 5 skips in the roster (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in) ×2)
— governed surface check-governed-merges.mjs --pr 18561 → 0 of 7 paths hit the register ⇒ NOT governed; ordinary queue landing applies, ⛔ not the governed four-piece

Next: ready → auto-merge, then followed to MERGED.

[EDIT — added after posting, ⛔ not a re-measurement dressed as the original] The sentence above says the #18511 rule is "not yet merged". That was true when this comment was posted and is now stale: origin/main was re-fetched for unrelated work minutes later and its tip reads ab1d359259 skills(pm-dispatch): off-tier seat discharges clause-② review by spawning an at-tier subagent (ruling C) (#18511) — the rule is MERGED. Nothing about the review or this adoption changes; the rule this seat invoked simply stopped being provisional. Recorded rather than quietly edited away.


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 17, 2026 07:45
@os-bill
os-bill enabled auto-merge September 17, 2026 07:45
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 0bd7dae Sep 17, 2026
44 checks passed
@os-bill
os-bill deleted the claude/issue-16894-kanban-titlefield branch September 17, 2026 08:16
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…— PENDING_GOVERNANCE reaches empty (objectstack-ai#18609)

Fixes objectstack-ai#18582

Clause-②: no

`PENDING_GOVERNANCE` reaches **empty**. `connector` and `analytics_cube`
— the two debts left on this card after `sharing_rule` was paid by PR
objectstack-ai#18587 — move into `GOVERNED` with a ledger each, so every authorable
metadata type in the denominator objectstack-ai#18133 widened now has one.

## What was measured

**`connector` — 74 properties: 20 `live`, 1 `planned`, 53 `dead`.**

- **Which schema the walker really resolves** (the seat's open question
1): `getMetadataTypeSchema('connector')` returns
`DeclarativeConnectorEntrySchema`, and that schema is
`ConnectorSchema.superRefine(...)`. In Zod 4 a `superRefine` attaches a
check to the **same object def** rather than wrapping it, so `shapeOf()`
returns `ConnectorSchema`'s shape unchanged: the walked key set is
byte-identical to the base's, tombstones included. **The gate cannot
tell the two schemas apart.** What the entry schema buys is refusals,
which are invisible to the walk and show up only on the three rows where
they are the whole verdict. That difference is recorded in the ledger's
`_note` and in the README row.
- **One schema, two doors** is the shape fact behind the split. The
ledger's denominator entry exists for the AUTHORING doors
(`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the
same `ConnectorSchema` is what `AutomationEngine.registerConnector`
parses for a def a plugin or an ADR-0097 provider factory builds in
code. So a key can have a real consumer and still do nothing when a
metadata author writes it — every row says which door its consumer is
fed from, and every `live` row carries a `producer` (objectstack-ai#4837).
- The keys an authored entry can reach are exactly the
`ConnectorProviderContext` fields plus `name` and `enabled`. `type` and
`icon` reach that context and are dropped by **all three** shipped
provider factories (`ctx.icon` census: zero reads across
`packages/connectors`, with `ctx.label` — four hits — as the lit
control). `authentication` is the ledger's one `planned`: refused
outright by ADR-0097 §3 (objectstack-ai#7990), never ignored.
- The 53 `dead` are four declared subsystems with no engine
(`syncConfig` 7, `fieldMappings` 7, `retryConfig` 8, `health` 14),
`triggers` (6 — the schema's own docblock already said so, objectstack-ai#3197), the
connector's nested `webhooks`, `status`, `metadata`, both timeouts,
`actions.description` / `.outputSchema`, and four `retiredKey`
tombstones whose rows stay because the key stays in the walked shape
(the `rls.priority` precedent).

**`analytics_cube` — 29 properties: 17 `live`, 12 `dead`.**

- **An honest `dead` was the outcome on 12 rows** (the seat's open
question 2), and none was inflated to green the gate. `cube-registry.ts`
names three producers into one registry — authored cubes, compiled
datasets (ADR-0021) and ad-hoc query inference — and only the first is
the door this ledger governs, so a key whose only reader sits on the
compiled-dataset path is not live for an authored cube. That is
`dimensions.granularities` (read by `dataset-executor#granularityOf`,
whose argument is a `CompiledDataset` an authored cube never becomes)
and `measures.format`.
- **Whether ADR-0049 wants a retirement is answered per row, and mostly
the answer is no** — the ledger says so explicitly so the
enforce-or-remove channel does not act on the word `dead`.
`granularities` and `measures.format` are the dataset compiler's own
output channel on a shared shape: deleting them breaks a live internal
write. `joins[].sql` is REQUIRED and documented as the ON clause while
the strategies synthesise an FK equality and never consult it — a
decision, not a sweep. `public` is an access-control flag that gates
nothing (three sites write `false`, nothing reads it): a knob that was
never wired, not a hole that was opened. `refreshKey.every` / `.sql` are
the only rows where retirement is the obvious shape, and even there the
showcase example authors them.
- **objectstack-ai#10238 is not prejudged.** Whether cube authoring is live end to end
remains its own measurement; this ledger answers the per-key question
only, and says so in the `_note`.

**Two prior in-repo claims were falsified by this measurement and are
corrected in the ledgers** (not in their source files — that is out of
scope here, and both are filed below):

1. `packages/spec/src/conversions/registry.ts` states `retryConfig` "and
the timeouts beside it are untouched — they are live". The word
`retryConfig` does not occur anywhere in `packages/` or `examples/`
outside `packages/spec`, and every `connectionTimeoutMs` /
`requestTimeoutMs` occurrence is a WRITE of the literal 30000 so a def
satisfies the post-parse type.
2. `bootstrapDeclaredWebhooks` documents itself as materializing each
"stack/connector-authored webhook", while its source is `readDeclared(…,
'webhook')` — metadata items the decomposition registers from the
top-level `webhooks:` collection, which a connector's nested array never
becomes.

## The gate, red before and green after

Both ledgers in place and both types in `GOVERNED`, before the README /
counts caught up — `pnpm --filter @objectstack/spec check:liveness`,
**exit 1**:

```
✗ 2 governed type(s) with NO row in the README state table:
    connector
    analytics_cube
✗ 1 README state-table heading error(s):
    heading says 37 governed types, GOVERNED has 39
✗ the generated count artifact is not current:
    packages/spec/liveness/state-counts.md is STALE — it does not match what the gate measures right now.
    first difference at line 67:
      - | **total** | **878** | **5** | **1** | **96** | **11** | **991** |
      + | `connector` | 20 | 0 | 0 | 53 | 1 | 74 |
✗ 2 row(s) where README.md and state-counts.md disagree:
    connector — counted in state-counts.md, no row in the README table
    analytics_cube — counted in state-counts.md, no row in the README table
✗ 1 UNDECLARED container inheritance — a blanket verdict covers keys nothing classified:
    connector/webhooks — one verdict covers 21 unclassified child key(s): …
```

That run is also the answer to the seat's warning about
`liveness/README.md`: `check-liveness.mts` declares `readmeMissingRows`
for exactly this, so the README rows, the heading count and
`state-counts.md` are not optional extras — the gate reverse-requires
them. After the README rows + heading (37 → 39), `gen:liveness-counts`,
and the `connector/webhooks` row in `undrilled-containers.baseline.json`
— **exit 0**:

```
governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack
collection(s) (analytics_cube, connector, sharing_rule, webhook); 30 governed, 0 awaiting a ledger.
  (+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS — 39 governed in total.)

✓ every governed-type property, at every depth the ledger drills, is classified, every authorable type
  — registered kind or unregistered-kind stack collection — is governed or explicitly pending, …
  and the README state table carries a row for each of the 39 governed type(s) it claims to index.
✓ packages/spec/liveness/state-counts.md is current — the same 39 row(s).
```

`connector/webhooks` is RECORDED in the undrilled baseline rather than
deferred or drilled, and the ledger row says why: `WebhookConfigSchema`
is `WebhookSchema.extend({ events, signatureAlgorithm })`, so a
`deferred` row to the governed `webhook` type would be refused by the
gate's key-set EQUALITY check — correctly — and drilling would mean
writing 21 child rows of which 8 are the ADR-0010 protection envelope
this gate auto-classifies `live` everywhere else.

## Verification

| Command | Result |
|---|---|
| `pnpm --filter @objectstack/spec check:liveness` | exit 0 —
`PENDING_GOVERNANCE` empty, 39 governed |
| `pnpm --filter @objectstack/spec check:generated` | exit 0 — all 15
generated artifacts up to date |
| `pnpm --filter @objectstack/spec check:authorable-surface` | exit 0 —
1536 schemas generated |
| `pnpm --filter @objectstack/spec check:api-surface` | exit 0 — public
API surface unchanged |
| `pnpm --filter @objectstack/spec check:docs` | exit 0 — 223 generated
files in sync |
| `pnpm --filter @objectstack/spec typecheck` +
`check:scripts-typecheck` | exit 0 |
| `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/` |
11 files, 315 tests passed |
| `pnpm check:platform-checklist` | exit 0 — 38 kinds mapped, 1 waived |
| `pnpm check:nul-bytes`, `check:published-files`, `check:merge-driver`,
`check:doc-authoring`, the three changeset gates + their self-tests,
`check:keyed-text-bounds`, `check:comment-mask-*`,
`check:closing-keyword-parity`, `check:pm-*` | exit 0 (22 families) |

`packages/spec` has no `lint` script; the repo runs one root `eslint .
--no-inline-config`, so the ESLint reading here is a **declared
narrowing** with its three pieces of evidence: (1) the universe comes
from the config itself — the only config object with a `files` glob for
source is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, and the seven
non-`.mts` paths in this diff are `.json` / `.md`, confirmed by running
ESLint on `liveness/connector.json` and getting `File ignored because no
matching configuration was supplied`; (2) `--format json` on the one
file this diff adds to that universe reports **1 file, 0 errors, 0
warnings**; (3) `eslint.config.mjs`'s own header states this repo "never
enables type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file", so nothing in this diff can
move the verdict on a file it does not touch. The whole-farm run is
CI's. Both figures are read at `a442b583fb`.

**Widening tells.** `node scripts/pm/check-widening-tells.mjs
--declaration no --diff DIFFPATH (this PR's diff)` exits **0 with no
tell**, matching PR objectstack-ai#18587 — but its own output is the honest reading
and it is not "clean": `8 changed file(s) — 0 judged against a declared
surface (no widening tell), 8 NOT MEASURED`, because no declared surface
covers ledger JSON, a changeset, a checklist map or a gate script.
Reported as NOT MEASURED rather than as a pass.

## File surface

| Path | Why |
|---|---|
| `packages/spec/liveness/connector.json` | new ledger (23 top-level
rows, 7 drilled containers) |
| `packages/spec/liveness/analytics_cube.json` | new ledger (9 top-level
rows, 4 drilled containers) |
| `packages/spec/scripts/liveness/check-liveness.mts` | both types into
`GOVERNED`; `PENDING_GOVERNANCE` emptied; its `[objectstack-ai#18582]` note rewritten
(it said "two left") |
| `packages/spec/liveness/README.md` | from the pre-declared OPEN set —
two state-table rows and the heading count 37 → 39, both
reverse-required by `readmeMissingRows` / `readmeHeadingErrors`; the
closing `PENDING_GOVERNANCE` paragraph rewritten |
| `packages/spec/liveness/state-counts.md` | OPEN set — regenerated with
`gen:liveness-counts`, never hand-edited |
| `packages/spec/scripts/liveness/undrilled-containers.baseline.json` |
one recorded row, `connector/webhooks`, reverse-required by the
container-coverage leg (see above) |
| `docs/qa/platform-checklist/coverage.json` | OPEN set — two entries;
the map is keyed by ledger name and `check:platform-checklist` reds on
an unmapped kind. Existing key order left as it was |
| `.changeset/18582-connector-analytics-cube-liveness-ledgers.md` | OPEN
set — `patch`, because `liveness` is in this package's published
`files[]`, so both ledgers ship in the tarball |

Neither `packages/spec/src/ui/view.zod.ts` (PR objectstack-ai#18561) nor
`packages/spec/scripts/check-generated.ts` (objectstack-ai#17735) is touched.

## Acceptance notes

Seen and deliberately not fixed here — three are findings this seat asks
the dispatching seat to file, the rest are noted only:

- **to file (contract violation; dedupe words: `retryConfig` live claim,
connector timeouts, conversions registry comment):**
`packages/spec/src/conversions/registry.ts`'s
`connector-rate-limit-config-removed` entry asserts `retryConfig` "and
the timeouts beside it are untouched — they are live". Measured false;
the comment is what a later reader will trust.
- **to file (contract violation; dedupe words:
`bootstrapDeclaredWebhooks` docblock, connector-authored webhook,
sys_webhook source):** the materializer's docblock claims it
materializes each "stack/connector-authored webhook"; its source is
`readDeclared(…, 'webhook')`, which a connector's nested array never
reaches.
- **to file (metadata-authoring trap; dedupe words: `analytics_cube`
joins sql ON clause, synthesised FK equality, cube join relationship):**
`Cube.joins[].sql` is REQUIRED and documented as the join's ON clause,
and both strategies synthesise `ON "parent"."seg" = "alias"."id"`
without reading it, so a non-FK join condition returns a 200 carrying
different arithmetic than the author declared. `joins[].relationship` is
the same shape one key over.
- **noted, not filed:** `packages/spec/docs/SYNC_ARCHITECTURE.md` still
ticks "✅ Monitoring: Health checks, metrics, logging" and "✅ Conflict
Resolution: Multiple strategies" at L3, both unbacked on this surface —
the `health` and `syncConfig` subtrees are dead. Carrier: the next PR
that acts on the `syncConfig` / `health` ADR-0049 decision; that file is
the one an author reads before writing either block.
- **noted, not filed:** `analytics_cube.public` is an access-control key
that gates nothing. Not filed separately because the ledger row IS the
record and the remedy is the ADR-0049 decision the `dead` verdict opens.
Carrier: the enforce-or-remove sweep that reads this ledger.
- **noted, not filed:** `Metric.name` / `Dimension.name` are required
inner fields shadowed by their record key, so a disagreement is silently
resolved in the key's favour. Carrier: none — no PR and no person is
near these files today; recorded here so a later sweep does not have to
re-derive it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/s tests tooling

Projects

None yet

2 participants