docs(skills): stageOrder is funnel-only at the three hand-written sites - #17538
Conversation
The dashboards skill rule table, the dashboards docs page and the sdui-parser header comment still documented `options.stageOrder` for a `funnel` / `pyramid` pair. `pyramid` is not a chart type: it was removed from `ChartTypeSchema` as a variant that only ever rendered as `funnel`, and `chart.test.ts` pins the refusal. Each site now carries the two statements the schema prose already makes: explicit stage order for a funnel widget, as the dimension's stored values, and `funnel` is the only widget type that reads the key. Net 0 lines; the ratcheted skill row grows by 2 bytes, paid inside the row (`category` -> `stage`, `omit it` -> `omit`, `field's own` -> `field's`). Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
…nt fix The edited header comment is carried into the published `index.d.ts` (measured: the new sentence appears in `dist/index.d.ts` and `dist/index.d.mts`, and `files[]` ships `dist`), so the tarball moves and a patch changeset is owed; `skip-changeset` does not apply. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
维护者速读改了什么 —— 三处手写文档里 为什么改 —— 风险与代价(含回滚) —— 纯文案,不动接受集、不动运行时; 席位意见 —— 通过。技能表那行的压缩措辞(删掉 你要做的 —— 一个动作:人工合并本 draft PR。 skills 席,session Generated by Claude Code |
Fixes #17471
What
Three hand-written sites documented
options.stageOrderfor afunnel/pyramidpair. There is nopyramidwidget type: it was removed fromChartTypeSchemaas a variant that only ever rendered asfunnel(taxonomy NOTE at the foot ofpackages/spec/src/ui/chart.zod.ts;packages/spec/src/ui/chart.test.tspins the refusal alongside its fallback-only siblings). An author copyingtype: 'pyramid'out of the skill table got a parse refusal.Each site now carries the two statements parent #17344's schema PR #17474 put into
dashboard.zod.ts(carried, not re-derived): (1) explicit stage order for a funnel widget, as the dimension's stored values; (2)funnelis the only widget type that reads the key — on any other type it parses and is never consulted (those order withsortBy/sortOrder). Same facts, each in its site's own register. Net 0 lines at all three sites; the schema and the generated reference page are untouched (the parent's).skills/objectstack-ui/rules/dashboards.md:345(ratcheted, row 183 → 185 bytes, +2)Explicit category order forfunnel/pyramid. Stored values, not display labels; omit it to inherit the field's own picklist order.Explicit stage order forfunnel; no other type reads it. Stored values, not display labels; omit to inherit the field's picklist order.content/docs/ui/dashboards.mdx:121(row 196 → 342 bytes; no ratchet)Explicit stage order forfunnel/pyramid, as the dimension's **stored values**. Omit to use the dimension field's picklist option order.Explicit stage order for afunnelwidget, as the dimension's **stored values**.funnelis the only widget type that reads it — on any other type the key parses and is never consulted; order those withsortBy/sortOrder. Omit to use the dimension field's picklist option order.packages/sdui-parser/src/dashboard-widget-options.ts:51(row 77 → 101 bytes)(funnel/pyramid stage order)(funnel stage order — the only type that reads it)Budget on the ratcheted file: file 24356 → 24358 bytes,
ceil(bytes / 4)6089 → 6090 against ceiling 6090 (headroom 1 → 0). The +2 bytes are paid inside the same row:category→stage(−3, and the parent's own word),omit it→omit(−3),field's own→field's(−4); no ceiling raise, no payment from another row.Changeset:
.changeset/sdui-parser-stageorder-funnel-only.md,@objectstack/sdui-parserpatch. Measured rather than assumed:files[]shipsdist, and afterpnpm --filter @objectstack/sdui-parser run buildthe new sentence appears indist/index.d.tsanddist/index.d.mts(positive controlstageOrderhitsindex.js/index.mjs/ both d.ts; the oldfunnel/pyramidtext: 0 hits), so the tarball moves andskip-changesetdoes not apply. Comment-only ⇒ patch.Premise check (measured on this branch, spec built from
efa2533dsources)(a)
ChartTypeSchema.safeParsethrough the built package (import.meta.resolve('@objectstack/spec/ui')→packages/spec/dist/ui/index.mjs, notsrc/):Premise holds (
premise_still_valid: true).(b)
grep -n pyramidon the three files atefa2533d: exactly one hit each (dashboards.md:345,dashboards.mdx:121,dashboard-widget-options.ts:51), none elsewhere in those files. After the editgrep -c pyramid= 0 on all three; line counts unchanged (462 / 553 / 193).(c) Lockstep:
scripts/check-sdui-lockstep.mjsfingerprints three things — the.objectui-shapin againstrecordedAgainstPin, the grammar region ofpackages/sdui-parser/src/parse.ts(delimiter to EOF, git blob id) and the diagnostic-code set extracted from the AST (code:properties anderror(...)first arguments). A comment line indashboard-widget-options.ts's header reaches none of them, and the file's own header names the header as "the one deliberate divergence" from objectui's copy (the byte-equal region starts at theimportline). So the line was edited here. Gate before and after the edit, identical verdict:check:sdui-lockstep: OK — this copy is byte-identical to objectui@53ded82bf7a4 (2026-09-05T15:42:42+00:00) over 214 grammar line(s) [blob 0131f27cf86d] and agrees on all 24 diagnostic code(s), across 7 non-test source(s).(exit 0 both times). objectui's twin at the pin (53ded82b, read through the REST contents endpoint) carries the same(funnel/pyramid stage order)line at its :25 — objectui's, see Acceptance notes.(d)
check-skills-token-ratchetbefore:skills/objectstack-ui/rules/dashboards.md is 6089 tokens (ceiling 6090; headroom 1), bundle total 139496. After (head6a854fe9):skills/objectstack-ui/rules/dashboards.md is 6090 tokens (ceiling 6090; headroom 0), bundle total 139497;34 authored bundle file(s) within their ceilings— exit 0 both times.The one judgement — how much of the parent's second statement each site carries, on the four axes
The skill row keeps the fact (
no other type reads it) and drops the routing hint (sortBy/sortOrder), while the docs row and the code comment carry the statement in full. 实际业务需求: the fact is what stops an author puttingstageOrderon abarwidget and reading the unchanged order as a bug; the routing hint is already thesortByrow three lines up in the same table, so in the skill it buys nothing an author does not have in view, and the measured budget on that file is 4 bytes. 项目长远合理性: the corpus and the schema say the same two things; the skill row is the compressed form, the docs row the expository form, the schema prose the authority — one contract, three registers, no fork. 防 AI 写元数据犯错: the refusal side is already structural (ChartTypeSchemarefusespyramidat parse, pinned); the silent side — the key parsing on a type that never reads it — is exactly what the sentenceno other type reads itguards in the corpus AI authors from, so it is the sentence that survives the budget. 创业阶段不扩散需求: no new row, no new example, no ceiling raise; the ratchet's+2 bytesis paid by deleting words in the same row, which is the only currency theskills/**rule accepts.Verification
Local = targeted gates; the farm is CI. Exit codes captured before any pipe (
cmd > log 2>&1; EXIT=$?); verdict lines quoted from the gates' own output.dispatch-gates.mjs --commandsderived ona9a2f0ba(83 commands) and again on the final head6a854fe9(90; the 7 added are the changeset families). Every command was run and recorded ascommand :: exit N;dispatch-gates.mjs --ranon6a854fe9:✓ dispatch-gates --ran: 90 derived famil(ies) accounted for — 89 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).(exit 0).pnpm check:dual-build-cjs-loads— exit 3PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/(a repo-wide build exceeds the foreground cap on the shared box). Declared narrowing: the diff's only source change is a header comment whose emitted JS is byte-identical (funnel/pyramid0 hits and the new sentence 0 hits indist/index.js/index.mjsafter rebuild — it reaches only the d.ts), so this gate's inputs did not move; CI owns the measurement.@objectstack/formula,@objectstack/lint,@objectstack/client-reactclosures under the verify lock, 318 s):check:doc-formula-expressionsexit 0 (✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 438 files / 1375 TS blocks judged clean),check:doc-security-postureexit 0 (✅ 27 ObjectSchema.create example(s) in 227 marked block(s) across 239 prose file(s) in 2 root(s) carry an os validate-clean security posture),check:skill-examplesexit 0 (✅ 258 prose examples type-check across 3 surface(s));check-plugin-teardown-shape.mjs --self-testexit 0 aftergit fetch --depth=1of its pinned positive-control commit621a4876(first run refused on the shallow clone — a prerequisite, not a red).check-skills-token-ratchet(quoted above);check:sdui-lockstep(quoted above);check:doc-authoring(✓ doc authoring guard: 401 files clean — no bare metadata literals./44 published skill files clean);check:nul-bytes(check-nul-bytes: OK (scanned 8319 text file(s) … no raw ASCII control bytes)) plus agrep -naPcontrol-character self-scan on the three files, 0 hits;check-changeset-no-major.mjs --base origin/main,check-adr-0087-registration.mjs --base origin/main,check-empty-changeset.mjs --base origin/main,check:changeset-gate-self-tests,check:objectui-changeset,check:pm-changeset-deadline-census,check:published-files,check:widget-option-census,check:pm-governed-merges.pnpm --filter @objectstack/sdui-parser run typecheckexit 0 (tsc --noEmit, script name echoed) andpnpm --filter @objectstack/sdui-parser run testexit 0 —Test Files 7 passed (7),Tests 138 passed (138)— underos-verify-lock.sh(VERDICT lines read, not bare$?). No public surface moves, so no consumer sweep is owed.node scripts/pm/check-governed-merges.mjs --branch claude/issue-17471-stageorder-funnel-onlyon6a854fe9: exit 3,⛔ GOVERNED — a human merge is the review record for this PR,skills/** ×1 — skills/objectstack-ui/rules/dashboards.md; paths not on the register: the changeset,dashboards.mdx,dashboard-widget-options.ts.dispatch-gates.mjs --tieron the three paths printsMANDATORY(publishedskills/**, clause ①).Clause-②: no — prose at three sites plus a changeset; no accept set or public surface moves (
stageOrderparses exactly as before, shown by the probe's last line).Acceptance notes
packages/sdui-parser/src/dashboard-widget-options.tsat the pin53ded82bcarries the same(funnel/pyramid stage order)comment at :25. It is in objectui's header, which the lockstep gate deliberately does not compare, so nothing here goes red; it is a code comment, not an authoring example, so it is outside the three filed classes. 承接者: the next objectui-side port of this header (whoever runspnpm gen:sdui-lockstepafter a pin bump reads both headers side by side).check:dual-build-cjs-loadscannot be measured on a partially built worktree (exit 3 prerequisite); nothing about the gate is wrong — its refusal is loud and names the remedy. 承接者: none.skip-changesetrule.维护者速读(草稿)
改了什么:三处手写文档里
options.stageOrder的说明从「funnel/pyramid」改为只说funnel,并加一句「只有funnel读这个键」——技能规则表一行、文档页表格一行、sdui-parser头注释一行,行数净零;sdui-parser附一条 patch changeset(注释会进发布的 d.ts)。为什么改:
pyramid不是图表类型,schema 直接拒收;作者(人或 AI)照抄技能表里的pyramid会得到一次解析失败。父卡 #17344 已把 schema 的说明改对,这三处是 schema 之外的残留,改成与 schema 同一口径。风险与代价(含回滚):纯文案,不动接受集、不动运行时;
dashboards.md的 token 预算刚好贴顶(6090/6090),多出的 2 字节在同一行内删词支付。回滚即 revert 两个 commit,无数据或迁移影响。席位意见:(留空,席位定稿)
你要做的:确认技能表那一行的压缩措辞可接受(删掉了
sortBy的指引,因为同表上方三行就是sortBy行),然后人工合并;本 PR 保持 draft,不进队列。Generated by Claude Code