Skip to content

Draw 「Pipeline by Stage」 as a ranked bar, not a funnel - #57

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-48-pipeline-mark
Sep 10, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-48-pipeline-mark

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes #48

Pipeline by Stage on 法务工作台 was authored as a funnel. A funnel encodes monotonic decline — each stage narrower than the last, because contracts drop out between them. This book does not decline, so the mark was asserting something the numbers do not say. It is now a ranked bar over the same dimension and the same measure.

What the data actually is (measured, not assumed)

Counted out of the running database, POST /api/v1/analytics/dataset/query on contract_metrics with dimension status and no filter — all ten statuses, 120 contracts:

stage contracts stage contracts
draft 10 active 60
submitted 6 expired 8
in_review 12 terminated 4
in_approval 8 cancelled 2
approved 4
signing 6

The card's premise holds: the late stages outnumber the thin draft/submitted head, by 60 to 10. (The card says "active / signed"; there is no signed status — signing is 6 and active is 60.) This is DESIGN.md §10's spread, pinned by assertSpread in src/data/plan-contracts.ts, and it is unchanged on origin/main @ d34b225 after #47/#55 landed — that card re-dealt which types carry in_review, not how many contracts sit in each status.

Rendered as a funnel that shape is a bowtie. Measured band widths in Chromium, before the change: 115 · 138 · 138 · 92 · 69 · 692 · 692 px — narrow, wide, narrow, then eleven times wider at the end.

The fix, and why it is correct at any distribution

  • type: 'funnel' → type: 'horizontal-bar'. A bar over the stage dimension says exactly what the data says and cannot bowtie at any distribution — which is the requirement, not a preference: the corpus is dealt by a fixture and re-dealt by other cards, so a mark that merely suited today's counts would break again quietly.
  • options.stageOrder → options: { sortBy: 'contract_count', sortOrder: 'desc' }. See below — this is the ordering half of the card, and the answer turned out to be measurable rather than assumable.
  • The widget's description changes with the mark ("intake through activation" described an order the chart no longer has), so both translation bundles change with it. No other string moves; the title is untouched in both locales.

No data was reshaped. src/data/, README.md and DESIGN.md are untouched.

The ordering question — answered, and it is not ours to fix

options.stageOrder is lowered by the dashboard renderer onto every chart type as categoryOrder, and only the funnel branch reads it. Measured twice on @objectstack/console 17.4.0:

  1. In its code: categoryOrder is consumed inside the chartType === 'funnel' branch and nowhere else; the bar branch passes the rows through untouched.
  2. In the browser: this widget as horizontal-bar with stageOrder still authored rendered Active · Approved · Draft · In Approval · In Review · Signing · Submitted — alphabetical.

So an explicit lifecycle order is not authorable for a bar on this platform version, and keeping stageOrder would have been metadata that does nothing. It is removed rather than left inert, and reported upstream rather than patched.

What is authorable is an order that cannot depend on a label, and that is the robustness the card was after. The zh-CN measurement before the change is the reason it matters:

zh-CN console, funnel, stageOrder authored: labels fell back to English and stage order was lost entirely — Active · Approved · Draft · In Approval · In Review · Signing · Submitted, i.e. alphabetical by the English label.

The mechanism: the query rows carry the API's English labels while the client's order map carries the bundle's Chinese ones, so nothing matches and every row falls to the same sort sentinel. The label bug was silently reordering the chart, exactly as the card suspected.

sortBy on the measure cannot be reordered by any label in any locale. It lowers to order: { contract_count: 'desc' } on the dataset query (verified in the request body), and the rendered order is now byte-identical in both locales:

en console zh-CN console
category order Active · In Review · Draft · In Approval · Submitted · Signing · Approved Active · In Review · Draft · In Approval · Submitted · Signing · Approved
bar widths (px) 615 · 123 · 103 · 82 · 62 · 62 · 41 615 · 123 · 103 · 82 · 62 · 62 · 41

Including the 6–6 tie between Submitted and Signing, which lands the same way both times.

The "spills outside its card" half does not reproduce

That claim came from a screenshot in the #45 dogfood pass, and it was re-checked rather than inherited. On main @ 2807b3b with console 17.4.0, the funnel's ink sits 25px inside the card on every side at 1440 / 1280 / 1024 / 820 px viewport widths, with scrollWidth === clientWidth (no horizontal scroll) at each. The mark was wrong-shaped, not overflowing. Nothing about the fix changes; the card should just not carry a claim nobody re-checked. The bar measures the same 25px inset at all four widths.

Browser verification

pnpm demo on a clean database (port 3048), signed in as admin@objectos.ai, navigated to Dashboards → Legal Workbench, in an en and then a zh-CN console, before and after the change; the widget was screenshotted in all four states.

  • Before, en: a bowtie — a narrow hourglass over two full-width triangles for Signing and Active.
  • After, en: seven ranked bars, Contracts axis 0–60, every stage label legible.
  • Before, zh-CN: the same funnel with a giant Active band at the top — English labels, alphabetical order.
  • After, zh-CN: Chinese title, Chinese description, Chinese measure axis (合同数), same order as en. The category labels are still English — that is the platform gap the card routes upstream, unchanged and untouched here.

Console during both passes: two lines, identical before and after and therefore pre-existing — a 404 on a resource fetch and [AuthProvider] Failed to load organizations: TypeError: Failed to fetch. Boot carried only the two known platform ERROR lines (objectstack#17175, #17176) and the background seed continuation (objectstack#17329).

Gates

Exit codes captured before any pipe (cmd > file 2>&1; EXIT=$?).

pnpm validate        → 0
pnpm lint            → 0     21 warning(s), 5 suggestion(s)  (unchanged from base)
pnpm typecheck       → 0
pnpm lint:i18n-gate  → 0     LOCALES "en","zh-CN" · REACH 12 surface classes (widget=38)
                             COVERAGE 0 missing keys across 2 locale(s)

Acceptance notes

  • PR title deviates from the issue title deliberately: the issue title asserts the widget "renders as a bowtie spilling outside its card", and the spilling half did not reproduce (above). The bowtie half did.
  • The widget id stays stage_funnel. It is the key both bundles address this widget by, it is not user-visible, and type one line below is what states the mark; renaming it would move three files for no user-visible gain.
  • suppressWarnings: ['chart-config-missing'] is kept and is inert on this lint version — measured in both directions: removing it produces the same 21 warning(s), 5 suggestion(s) on this branch and on base 2807b3b with the funnel. It is pre-existing, unaffected by this change, and left alone rather than tidied under this card.
  • Platform gap for the upstream tracker: dashboard.widgets[].options.stageOrder is forwarded to every chart type as categoryOrder and honoured only by the funnel branch, so an authored stage order is silently dropped on a bar/column widget. Noted, not filed here — it belongs in objectstack-ai/objectstack, and this card owns no issue-filing on that repo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR


Generated by Claude Code

A funnel encodes monotonic decline. The seeded book does not decline —
draft 10 / submitted 6 / in_review 12 / in_approval 8 / approved 4 /
signing 6 / active 60 — so the mark rendered as a bowtie and, worse,
asserted that contracts fall out at each stage. They do not: this is a
snapshot of where a live book sits, not a conversion sequence.

A bar over the stage dimension cannot bowtie at any distribution, which
matters because the corpus is dealt by a fixture and re-dealt by other
cards.

Order comes from the measure, not from a label. `options.stageOrder` is
read only by the funnel branch of the console renderer (measured in its
code and in the browser), and in a zh-CN console it was already being
ignored: the labels fall back to English while the client's order map
holds the bundle's Chinese ones, so the chart fell back to alphabetical
order. `sortBy: 'contract_count'` lowers to `order: { contract_count:
'desc' }` on the dataset query and cannot be reordered by any label in
any locale.

The widget's description changes with the mark, so both bundles change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 10, 2026 07:55
@zhuangjianguo
zhuangjianguo merged commit 2b74c4f into main Sep 10, 2026
2 checks passed
zhuangjianguo pushed a commit that referenced this pull request Sep 10, 2026
§09 still specified 各阶段合同数漏斗 — a funnel over every lifecycle stage.
Two merged cards moved the app off that in two independent ways, and the
design authority never caught up:

- #48 / PR #57 replaced the mark with `type: 'horizontal-bar'` ordered by
  the measure, because a funnel asserts monotonic decline over a book that
  does not decline (§10: draft 10 · submitted 6 · in_review 12 ·
  in_approval 8 · approved 4 · signing 6 · active 60 — a bowtie).
- #59 / PR #62 narrowed the filter to the six in-flight stages, on the
  criterion the widget's comment states: whether legal still has work to do
  on the stage. `active` and the four terminal outcomes are all out.

The bullet now names the mark and the coverage; a new paragraph carries the
reasoning and states that no authored stage order is promised —
`options.stageOrder` is read only by console 17.4.0's funnel branch and is
dropped silently outside `en` (objectstack-ai/objectstack#17344). Promising
an ordering this platform version cannot deliver is the same defect class
PR #40 just removed from this section.

Documentation only: no app file changes, and no gate reads `DESIGN.md`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
zhuangjianguo pushed a commit that referenced this pull request Sep 10, 2026
…ages (#63)

09 still specified the Legal Workbench stage widget as 各阶段合同数漏斗 — a
funnel over every lifecycle stage. The app stopped shipping a funnel in #48
(PR #57 made it a horizontal-bar ordered by the measure) and narrowed what it
plots in #59 (PR #62 dropped `active`, leaving the six in-flight stages), and
the design authority never caught up.

The bullet now names the mark and the coverage, and a new paragraph carries
the reasoning: a funnel asserts monotonic decline and this book does not
decline (10's spread — draft 10 · submitted 6 · in_review 12 · in_approval 8
· approved 4 · signing 6 · active 60), so the mark was asserting something the
numbers do not say. The admission criterion is whether legal still has work to
do on the stage, which is why `active` and the four terminal outcomes are all
out; the size of the in-force book belongs to the 管理层 board.

It also records the non-promise, which is the half a list bullet cannot
carry: 09 must NOT describe an authorable stage order. options.stageOrder was
removed deliberately in PR #57 — console 17.4.0 reads it only in the funnel
branch and drops it silently outside `en` (objectstack-ai/objectstack#17344).
Recording the negative is what stops the next reader re-adding it, the same
way PR #40 recorded rather than silently deleted the 各段时长 promises from
this section.

Sits alongside PR #40's 09 rewrite: the 各段时长 paragraph and every other row
are byte-identical, and 01-04 are unchanged. No app file is touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
zhuangjianguo pushed a commit that referenced this pull request Sep 10, 2026
The legal dashboard's header carried a copy of §09's six-brick list. That
copy drifted three times in two days — two items rewritten by PR #40
(1127e52), the third by PR #70 (30fd863) — and by the end all three
described bricks this file does not implement. Replace the copy with a
pointer to §09, which cannot drift.

The cycle-time dataset's stage-count paragraph contrasted its counts with
"the status funnel on the legal workbench", a funnel that has not existed
since PR #57 made that widget a bar chart. Keep the measurement and its
reason (signed_at 72 exceeds submitted_at 60; backfilled in-force book,
no intake) and drop the claim that a funnel exists to contrast with.

Comments only: no metadata, no behaviour, no DESIGN.md change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
zhuangjianguo pushed a commit that referenced this pull request Sep 10, 2026
Two comments quoted a 09 that has since moved — the mirror of the cards that
fixed documents describing code wrongly.

legal.dashboard.ts's header carried a copy of 09's six-item brick list, and
the copy lost the race three times in two days: two items were rewritten by
PR #40 (1127e52) and the third by PR #70 (30fd863), leaving all three
describing bricks this file does not implement — the board ships
review_ageing titled "In Review Over 30 Days", approval_throughput titled
"Approved This Month", and a horizontal-bar. Two of them named metrics 09 now
explicitly declines to deliver.

The quote is removed rather than updated: updating buys correctness until the
next 09 edit and leaves the same trap armed, while a pointer cannot drift.
The header now says the list is deliberately absent, so the next reader does
not paste it back, and cites commit shas — immutable — instead of prose.

cycle-time.dataset.ts contrasted its five coverage counts with "the status
funnel on the legal workbench", which has been a horizontal-bar since PR #57.
The contrast is inverted rather than deleted: there is no funnel there
either, and the same non-monotone book is the reason. The measurement stays —
signed_at (72) exceeding submitted_at (60), and the backfill that explains it.

Comments only: no metadata key, no widget, no dataset field, no dimension.
These files are inside the gates' surface and the gates pass — but comments
are invisible to tsc, which is why the drift went unnoticed for two days.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

「Pipeline by Stage」 uses a funnel mark on data whose late stages outnumber the early ones, so it renders as a bowtie spilling outside its card

2 participants