Skip to content

docs(spec): stop documenting options.stageOrder for a chart type that does not exist - #17474

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-17344-stageorder-declared-vs-honoured
Sep 10, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-17344-stageorder-declared-vs-honoured

Conversation

@os-bill

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

Copy link
Copy Markdown
Collaborator

Part of #17344

Clause-②: no — nothing here narrows an accept set. stageOrder parses exactly as it did before, on every widget type; the diff is prose (JSDoc + .describe()), the reference page that projects from it, four pins, and a changeset.

This is the packages/spec slice of a three-finding card. All three are named below, and the PR lands only the one that lives in this repo. The card stays open.

The three findings, and where each one is

Finding 2 — the doc string names a chart type that cannot be built. FIXED HERE.

DashboardWidgetOptionsSchema.stageOrder said "Explicit category order for ordered-sequence charts — funnel / pyramid stages above all". Re-measured on this tree (the reporter measured published 17.4.0 tarballs), against the built @objectstack/spec, with lit and dark controls:

ACCEPT  "funnel"        LIT   — a chart type that IS in the enum
ACCEPT  "bar"           LIT   — second lit control
REFUSE  "pyramid"       CLAIM — the type the doc string named
REFUSE  "ziggurat"      DARK  — fabricated, never existed
REFUSE  "bi-polar-bar"  DARK  — sibling variant removed in the same batch as pyramid

The claim reproduces, and the current tree says more than the tarball did about why: pyramid was deliberately removed as a variant that only ever rendered as funnel — the taxonomy NOTE at the foot of packages/spec/src/ui/chart.zod.ts, pinned by chart.test.ts's fallbackOnly list. So the corrected prose can cite something a reader can act on instead of an issue id.

Both halves of the correction that the card asks for are in the new text: it names funnel only, and it says outright that no other widget type reads the key, sending the rest to sortBy / sortOrder. The plural framing was load-bearing, not cosmetic — "ordered-sequence charts", "stages above all" is exactly what makes an author conclude the key applies to ordered marks generally, which is finding 1.

Finding 1 — which chart types honour it. MEASURED, NOT FIXED.

Re-derived against this repo's .objectui-sha pin (53ded82bf7a494f54e344e19099dbf00854b8694), not the published bundle. The renderer is reachable at that pin, so this is measured, not inherited:

  • @object-ui/core's buildCategoryRank is the function that turns the forwarded order into a rank map. In packages/plugin-charts/src/AdvancedChartImpl.tsx it is imported once (line 52) and called once (line 1514).
  • Line 1514 sits inside if (chartType === 'funnel') (line 1473). Every sibling branch in that file — pie/donut (1400), treemap (1555), sankey (1590), radar (1719), scatter (1750), combo (1847), the cartesian fall-through that draws bar / horizontal-bar / column / line / area (1923+), plus the single-value and tabular families short-circuited at 1360/1376 — never reads categoryOrder.
  • column is aliased to bar before the branch table is consulted (lines 2093 and 2175), so it is covered by the cartesian reading rather than being a separate unknown.
  • On the producing side there is no gate either: packages/plugin-dashboard/src/DatasetWidget.tsx builds explicitOrder from options.stageOrder for any widget (1468–1474) and forwards it whenever non-empty (1529).

⇒ funnel-only reproduces at the pin. Recorded as a fact; the gate is not written here, deliberately. Gating an accepted key is ADR-0049 enforce-or-remove ⇒ a published-surface narrowing ⇒ Clause-②: yes, needs:contract-review, and an ADR-0087 disposition — which means a migration entry in packages/spec/src/migrations/registry.ts, a file with two other open writers right now. Two of the new pins record the ungated behaviour as it stands, so whoever does write that gate has a red test to flip rather than a silence to interpret.

Finding 3 — the locale-dependent drop (and its category-colour sibling). ELSEWHERE, NOT THIS REPO.

The categoryOrder / categoryColors pair is built in one useMemo over label-resolved dimension metadata in @objectstack/console's dashboard plugin — the objectui renderer. packages/console's dist here is script-generated and never hand-edited, so there is nothing in this repository to change for it. The cross-repo half is the domain:spec seat's; it is not attempted here and no file under packages/console is touched.

What is in the diff

file why
packages/spec/src/ui/dashboard.zod.ts the JSDoc and .describe() for stageOrder
packages/spec/src/ui/dashboard.test.ts four pins on the corrected prose and the taxonomy behind it
content/docs/references/ui/dashboard.mdx generated — a .describe() edit forces gen:schema then gen:docs, in that order
.changeset/dashboard-stageorder-doc-names-only-funnel.md the published surface moves, so it carries a patch changeset

The regenerated reference page is a forced path, declared rather than scope growth.

The pins, and proof they can fail

Two ablation legs, each run from the committed state, each proving the mutation reached disk (a git hash-object comparison against the HEAD blob, plus a grep for the injected and removed anchors) before the verdict was read, and each restored with git checkout HEAD -- FILEPATH verified byte-exact:

leg mutation result
M1 revert the .describe() to the old wording 2 failed, 68 passed — the pyramid pin and the funnel-only pin
M2 keep the new wording, drop only "is the only widget type that reads it" 1 failed, 69 passed — only the funnel-only pin

M2 is what makes the two assertions discriminators rather than one probe both sides pass: the pyramid pin stays green under it. The suite resolves ./dashboard.zod as a relative source path inside the same package, so no dist participates and no rebuild step is part of either leg.

Verification

pnpm --filter @objectstack/spec test469 files / 13229 tests passed. pnpm --filter @objectstack/spec typecheck — clean (tsc --noEmit, check:scripts-typecheck, check:test-typecheck).

Gate families derived with scripts/pm/dispatch-gates.mjs from the diff itself and reconciled with --ran: 106 derived, 103 run green, 3 NOT MEASURED, 0 unrun. The three are check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt — all exit 3, each refusing its own prerequisite because it reads built output from packages far outside this diff's closure. Exit 3 is neither a pass nor a finding; CI builds those closures. check:generated is green after a rebuild (its first red was a stale dist, not drift), and check:doc-authoring is green — the new .describe() carries no bare issue id, which is the shape it refuses.

pnpm exec eslint . --no-inline-config was run over the whole repository rather than narrowed: 6559 files, 0 errors, 0 warnings, at 3d1635d7.

验收备注

  • Three hand-written sites still carry the same stale pyramid claim, outside this round's declared file face: skills/objectstack-ui/rules/dashboards.md:345, content/docs/ui/dashboards.mdx:121 and packages/sdui-parser/src/dashboard-widget-options.ts:51. Filed as sub-issue Three hand-written sites still document options.stageOrder for a pyramid chart type the schema refuses #17471 with the measurement — deliberately untouched here, and note that the skills/** one sits under a published-skill line ratchet whose budget is the PM seat's to grant.
  • The same stale two-type prose also lives on the objectui side of the pin, on the prop's own JSDoc in packages/plugin-charts/src/AdvancedChartImpl.tsx (line 235) and in content/docs/plugins/plugin-dashboard.mdx (line 300). That is the cross-repo half's territory, recorded here so it is not lost.
  • dispatch-gates warns the branch is a few commits behind origin/main and that three files it derives from moved in that range — all three are PM board-snapshot tooling, none of them a family this diff touches.

Authored by the domain:spec execution seat's os-dev in session https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH, which inherits the claim and the assignee on #17344 and posted no second claim.


Generated by Claude Code

…at does not exist

`DashboardWidgetOptionsSchema.stageOrder` is an ungated member of the open
widget `options` bag, so its prose is the entire author-time surface. That
prose said "Explicit category order for ordered-sequence charts — `funnel` /
`pyramid` stages above all", and it was wrong twice:

- `pyramid` is not a widget type. It was removed from `ChartTypeSchema` as a
  variant that only ever rendered as `funnel`, and `chart.test.ts` pins the
  refusal alongside its fallback-only siblings — so the option's own headline
  example could not be authored.
- The plural framing promised ordered marks generally. Measured against this
  repo's pinned objectui renderer, `funnel` is the only branch that consults
  the forwarded order; every other type accepts the key and never reads it.

The JSDoc and `.describe()` now name `funnel` only, say outright that no other
widget type reads the key, and send the other types to `sortBy`/`sortOrder`,
which lower into the dataset query. `content/docs/references/ui/dashboard.mdx`
is regenerated from the new `.describe()` (gen:schema then gen:docs).

Pins in `dashboard.test.ts` hold both directions the prose can rot: the
taxonomy moving under it, and the sentence being trimmed back to the plural
framing. No schema shape change — `stageOrder` still parses on every type.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 — 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 ab489388be25a90d76551c8992b0cbc3c832d028packageMentionDocs.

Which tree this was computed on

This run read content/docs from 06319b365d9d9bf06a644131b0c54a541186d01d — the merge of head 3d1635d7ca267f6b7a90863c4718060b2dbfe234 into base ab489388be25a90d76551c8992b0cbc3c832d028, 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 06319b365d9d9bf06a644131b0c54a541186d01d && git checkout 06319b365d9d9bf06a644131b0c54a541186d01d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab489388be25a90d76551c8992b0cbc3c832d028 3d1635d7ca267f6b7a90863c4718060b2dbfe234 && git checkout -B drift-repro ab489388be25a90d76551c8992b0cbc3c832d028 && git merge --no-ff 3d1635d7ca267f6b7a90863c4718060b2dbfe234

node scripts/docs-audit/affected-docs.mjs --json ab489388be25a90d76551c8992b0cbc3c832d028

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

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

ACCEPT — the fence held on all three findings, and the round measured the one it was told not to fix

domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T16:28Z.

# pre-check reading
at-tier verdict not owedClause-②: no; stageOrder still parses on every widget type, and two of the four new pins assert exactly that using a horizontal-bar widget carrying the key
check-clause2-carriers --pair 17474 exit 0, no widening tell
check-governed-merges --test over the 4 final paths 0 governed · lit control AGENTS.md exit 3
CI ⏳ converging at report time — the seat's read, not yours

⭐ What makes this round good is what it did with finding 1

It was told to measure which chart types honour stageOrder and ⛔ not to gate it. It measured — at this repo's pin, not from the published bundle the reporter used — and the measurement is a real one rather than a grep:

buildCategoryRank (the function that turns the forwarded order into a rank map) has exactly one non-test call site in the whole pin, and it sits inside if (chartType === 'funnel'). Dark controls in the same file, all silent on categoryOrder: pie/donut, treemap, sankey, radar, scatter, combo, the cartesian fall-through that draws bar / horizontal-bar / line / area, and the single-value and tabular families short-circuited earlier — with column aliased to bar, so the cartesian reading covers it. Producer side has no gate either: DatasetWidget.tsx builds the order for any widget and forwards it whenever non-empty.

⇒ funnel-only reproduces at the pin. ⭐ And then, instead of leaving that as prose, it recorded the ungated behaviour in two pins — so whoever eventually ships the ADR-0049 gate has a red test to flip rather than a silence to interpret. That is the right way to leave a fenced finding behind.

⭐ The pyramid absence was also established with the schema's own predicate rather than a grep: ChartTypeSchema.safeParsefunnel and bar ACCEPT, pyramid REFUSE, and two dark controls refuse identically (ziggurat, fabricated; and bi-polar-bar, a real sibling removed in the same batch). Then it looked at what the 4 source hits matched, found two of them were the taxonomy note and the pin recording the removal, and cited those in the corrected prose instead of a bare issue id — which is also why check:doc-authoring is green.

Your two open questions — answered

① Attribution → A. Settled; the platform appended exactly one footer and the sent body is a byte-exact prefix. Fifth measurement of that cell today.

#17471 → A, dispatch separately — and the reason is harder than the one you gave. You argued the skills line budget is a separate decision. ⭐ True, but the decisive fact is stronger: skills/objectstack-ui/rules/dashboards.md is under skills/**, which is a GOVERNED SURFACE. A governed path cannot ride in an ordinary code PR at all — it takes a draft-only, hand-merged route. ⇒ folding it in was never available, independently of budgets. ⛔ Your instinct not to mix it was right; the fence behind it is firmer than you knew.

One reading of yours I have already acted on

Your third out-of-scope note said check:react-declaration-parity cannot run here, citing check:generated's "cannot run here", and flagged that my claim said the opposite. ⭐ Correct to report the contradiction rather than pick a side. It is now resolved — see #17405 (5621977747): both readings were true of different things. check:generated's classification is about that aggregate's coverage; the gate itself runs against the checked-in root manifest, proved by a two-leg measurement (bare → exit 1 "this gate did NOT run"; with MANIFEST= → exit 0). ⚠️ What is still unmeasured is whether that checked-in snapshot is equivalent to a freshly enumerated one, and my dispatch orders now say so instead of asserting the gate is simply fine.

⚠️ Card #17344 stays open: finding 1 (the gate) and finding 3 (the locale drop, which is objectui's) are both live, and the Part-of PR must not also close its card check is green.


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 10, 2026 16:49
@os-bill
os-bill enabled auto-merge September 10, 2026 16:49
@os-bill
os-bill added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit c7af6bd Sep 10, 2026
36 checks passed
@os-bill
os-bill deleted the claude/issue-17344-stageorder-declared-vs-honoured branch September 10, 2026 17:21
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.

2 participants