test(spec): pin the untitled-tombstone rule on the dashboard widget row, not one carrier - #19107
Merged
os-bill merged 1 commit intoSep 18, 2026
Merged
Conversation
…ow, not one carrier `dashboard.test.ts`'s #16458 control asserted that `widgets[].actionUrl` — one `retiredKey()` tombstone out of five on that row — carries no `title`. Titling any of the other four (`actionType`, `actionIcon`, `responsive`, `aria`) left the suite green, so the control pinned one instance of the rule rather than the rule. Derive the tombstone set from the served schema instead: filter the widget row properties whose `description` starts with the `[REMOVED] ` marker both producers write (`retiredKey()` in `shared/retired-key.ts`, `retiredComponentProps` in `ui/component.zod.ts`) and assert `title` is undefined for every member, in both io modes. Head-anchored and not `includes`, for the reason stated at the constant. A by-name floor keeps the loop from passing vacuously over an empty set — the `'Widget ID'` lit leg only proves the node resolved — while leaving a sixth tombstone covered by the loop on the day it lands. Two pre-existing wording defects swept in the same pass: the control comment called the tombstone "the one widget row property that must stay untitled" (there are five), and the ledger pin's header in `kernel/repeater-item-titles.test.ts` said "for the four below" against five LEDGER entries. Tests and comments only: no schema, no accept set, no public surface. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 18, 2026
spec: the dashboard tombstone control pins 1 of 5 tombstones, not the untitled-tombstone rule
#18517
Closed
os-bill
marked this pull request as ready for review
September 18, 2026 20:16
os-bill
deleted the
claude/issue-18517-dashboard-tombstone-class-control
branch
September 18, 2026 20:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18517
Clause-②: no
Tests and comments only. No schema, no accept set, no public surface, nothing that ships:
npm pack --dry-runonpackages/speclists 292 paths,src/ui/dashboard.zod.tsamong them(lit control) and zero
*.test.ts— soskip-changeset.What was pinned, and what is pinned now
The
#16458control inpackages/spec/src/ui/dashboard.test.tsasserted thatwidgets[].actionUrlcarries notitle. That is ONEretiredKey()tombstone out of five onthe
dashboard.widgets[]row, so the suite pinned one instance of "a tombstone staysuntitled", not the rule — the other four could each be titled with the file green.
The control now DERIVES the set from the served schema — the widget row properties whose
descriptionstarts with the[REMOVED]marker — and assertstitleis undefined for everymember, in both io modes.
The five, re-derived on this tree (not copied from the card)
z.toJSONSchema(DashboardSchema, { io, unrepresentable: 'any' }), widget row = 22 properties,of which the tombstones are, by name and identical under both io modes:
actionIcon·actionType·actionUrl·aria·responsiveSeventeen row properties carry a title; the five tombstones carry none. The card's count of
five holds.
The matcher, and why it is stated in the code
Head-anchored
startsWith('[REMOVED] '), trailing space included — deliberately notincludes. Both spellings select the same five keys on this row today, so the choice is aboutthe failure direction rather than today's reading:
retiredKey()(
shared/retired-key.ts, "[REMOVED] " + guidance) and the element-grainretiredComponentProps(ui/component.zod.ts)..describe()may quote the markerwhile explaining a retirement. Under
includesthat key joins a set this test then reportsas "every tombstone" — and asserting an authorable key is untitled is the opposite of the
rule.
(
isRetirementTombstone,data/datasource-credential-redaction.ts), as is the ledger pin'sRETIRED_PREFIX(kernel/repeater-item-titles.test.ts). One marker, one predicate.The by-name floor beside it is there because the loop is vacuously green over an empty set and
the
'Widget ID'lit leg only proves the node resolved. It is a floor and not an equality: asixth tombstone is covered by the loop on the day it lands, which is the reason for deriving
the set instead of listing it.
Evidence
Reverse verification, per tombstone. Each leg runs through
scripts/ablation-replace.mjs(anchor must hit, on-disk count and blob hash verified, restore proved against
HEAD):retiredKey(...)gains.meta({ title: 'Ablated Title' }), suite runs, file is restored.dashboard.test.tsrepeater-item-titles.test.tsactionUrlactionTypeactionIconresponsiveariaTwo reds per leg = the
io: 'input'andio: 'output'cases, each naming its carrier:AssertionError: widgets.items.properties.aria.title: expected 'Ablated Title' to be undefined.Every leg ends
ok restored: blob == HEAD (30c6d785e797) and git diff HEAD is empty.Before-leg — the defect reproduced on this tree rather than taken from the card. Nested
ablation: the OLD one-carrier control restored into the test file,
ariatitled in the schema.Both mutations landed on disk, and the suite was green: 2 files passed, 144/144, exit 0.
That is the hole this PR closes.
Dark control 1 — prose, not title: rewording a tombstone's guidance text with the marker
intact (
reflowstore-flows, anchor 1 to 0, blob 30c6d785e797 to b4235bda3bea) leaves thesuite green, 144/144. The red comes from the
title, not from touching the declaration.Dark control 2 — the ledger pin stayed green in all five red legs.
repeater-item-titles.test.tsis blind to this by design and still is:deriveCarriers()skips a property whose description starts with
[REMOVED]before the title check ("a retiredkey is a parse-time refusal, not an authorable column"). Confirmed as designed and left alone.
Swept in the same pass (both pre-existing, both named on the card)
untitled". There are five.
kernel/repeater-item-titles.test.tssaid "for the four below"against five
LEDGERentries, re-derived by name here:field:options,object:fields.options,view:columns,view:sort,view:tabs.Verification
All at
d7f118d2.pnpm --filter @objectstack/spec test— 494 files, 14520 tests passed, exit 0.pnpm --filter @objectstack/spec typecheck— exit 0 (tsc --noEmit,check:scripts-typecheck,check:test-typecheck: 54 files / 259 errors / 144 pinned signatures held, unchanged).pnpm --filter @objectstack/spec build— exit 0, and the tree stayed clean (no generated drift).pnpm lint— the whole repo-wide population,eslint . --no-inline-config, exit 0. Not anarrowing: the full run fit in the foreground.
node scripts/pm/dispatch-gates.mjsderives 76 for this change set; 72 rangreen, 4 report
PREREQUISITE NOT MET(they read a built monorepo, not just this package) andare NOT MEASURED locally, declared to CI:
check:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt.dispatch-gates --ranreconciles 76/76 accounted.Acceptance notes
descriptionmatches the[REMOVED]marker inside a loop whose filter already guaranteesit can never fail. The old control's version of that line was a real assertion because it
named its carrier; under a derived set it is a tautology.
widgets[]row, which is the card'ssubject. A tombstone on another dashboard node (
DashboardSchema's ownaria, for one) isnot covered by it, and no ledger pin covers it either —
repeater-item-titles.test.tsisrepeater-row-scoped and drops
[REMOVED]properties by design. Not a defect, a boundary:the same widening decision belongs with whoever wants the whole-schema version, and it would
change what this file is about.
Generated by Claude Code