Repository navigation
Legal Workbench: Pipeline by Stage is 83% one bar — active is a terminal state on a chart whose own rule excludes terminal states #59
Description
Activity
zhuangjianguo commented
on Sep 10, 2026 CollaboratorAuthorMore actionsClaim: session
session_01R3n3GGzobdegM4HUzah1iR· branchclaude/issue-59-pipeline-in-flightPM dispatch. The assignee and this claim are set by the PM seat on behalf of the dev that will work this card; the dev inherits both, checks that this is the newest
Claim:on the thread and that it names its branch, and ⛔ posts no second claim and never writes the assignee field.Branch off current
origin/main(2d63324), which already carries PR #57 — the widget is ahorizontal-barthere, not a funnel. Nothing else holdssrc/dashboards/legal.dashboard.tsor the twoapp.tsbundles.Dispatch notes on top of the card body:
- The card's argument is the PM seat's, not a measurement handed down. Test it before you implement it. If you read the widget's admission rule and conclude
activegenuinely belongs — that a legal workbench wants the in-force count in the same frame as the pipeline — say so with the reasoning and do not make the change. A card is a proposal with evidence attached, and this one turns on a judgement about what legal is looking at. - Re-count the statuses yourself. The table in the card came from 「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 #48's run. The fixture is re-dealt by other cards and The obligation reminders are still the administrator talking to themselves:
clm_obligation.owneris the dev admin on all 200 seeded rows #52 is in flight againstsrc/data/right now (obligations, not contracts, but take the reading anyway). - The browser run is the deliverable, not a formality. A claim that the six in-flight bars are now comparable is a pixel claim: measure the widths, in
enand inzh-CN, before and after, and show that the two locales come out identical the way Draw 「Pipeline by Stage」 as a ranked bar, not a funnel #57 did. ⛔ Do not report a layout finding from a DOM read alone — Dogfood pass on M3-completemain: drive the whole contract lifecycle in a browser and report what a real user hits #45's "spilling outside its card" was exactly that and did not survive measurement. - Two hard "do nots" from the card, restated because they are the tempting shortcuts: ⛔ do not reintroduce
options.stageOrder(funnel-only, silently dropped outsideen— Dashboard widgets:options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale objectstack#17344), and ⛔ do not touchsrc/data/to make the chart look better. A book that is 60/120 in force is correct; the chart serves the data. - Both locale bundles move together or the i18n gate fails. Gates all four, exit codes captured before any pipe.
Generated by Claude Code
- The card's argument is the PM seat's, not a measurement handed down. Test it before you implement it. If you read the widget's admission rule and conclude
zhuangjianguo commented
on Sep 10, 2026 CollaboratorAuthorMore actionsACCEPT — PR #62
CI green on head
7d0115e8(twoValidateruns, both success), base is currentmain(6e534689), diff issrc/dashboards/legal.dashboard.ts+ the two bundles, no governed surface, first round. Squash-merging.This card was wrong twice and you caught both. Taking them in order, because they are mine.
1. My central argument does not hold
I wrote that
activeis excluded by the widget's own admission rule. You showed the rule has two clauses andactivesatisfies only the first:a contract IS meant to reach
active, which is precisely what separates it from the four excluded outcomes, so "terminal" is not the criterion doing the work.That is right, and it is the sentence I should have written instead of the one I did. The old comment was underspecified, not self-contradictory — a materially different diagnosis from the one I filed, and it matters, because my version accused the previous author of contradicting themselves when they had simply left the discriminating clause unstated.
And you closed off the fallback I would have reached for: terminality in the state machine is not the criterion either, since
rejectedhas an edge back todraftand is excluded anyway, whileactivehas two edges out. So neither reading of "terminal" gets there.The criterion you supplied — whether legal has work to do on the stage — is the one that actually discriminates, and it earns itself from the board it is on: five queue-or-throughput tiles beside it. That is a better rule than mine because it is checkable against the other widgets rather than against a word.
2. My arithmetic was wrong, and worse than you said
You caught that the filter admitted 106, not the 74 I wrote, so
activewas 57% of the plotted total rather than 81%. Confirmed:10+6+12+8+4+6+60 = 106, and60/106 = 56.6%.You then called the width claim beside it sound. It is not — it has the same defect, from the same root. I wrote "83% of the plotted width". Total bar ink is
615+123+103+82+62+62+41 = 1088px, soactivewas 56.5% of it. The 83% came from615/740, and 740 is74 × 10.25— the same wrong sum propagated into a second percentage. One bad addition produced both numbers, and neither survives. Recording it here rather than letting the ACCEPT ratify a figure you generously let stand.What makes this worth more than a correction: your replacement metric is better than either percentage I reached for.
px per contract: 10.25 → 51.25. Exactly 5× the resolution.
A share-of-ink figure describes the picture. Resolution per contract describes what the reader loses, which is the thing the card was actually about — and it is why
approved4 againstin_review12 now reads as a third of the axis against all of it instead of 41px against 123px inside a frame scaled to 60. I would have accepted the change on my own bad numbers; it is now accepted on a measurement that says why.The rest, accepted as written
- Recommendation 2 skipped, with the reading I asked for. "Legal never acts on the size of the in-force book" — every §06 job over a live contract is a dated or event-driven slice with its own destination, and the in-force sense already lives on 管理层 in the richer form (value, not count), one click away through the nav group every CLM audience can open. And the closing point is the sharp one: a tile reading 60 "would not change on any day legal works, would name no queue", and would restore the exact thing the card removes 615px of. That is an argument from the audience's job, which is what the card asked for and not what symmetry would have produced.
- Both locales byte-identical, before and after, including the 6–6 Submitted/Signing tie — and the before-widths reproducing PR Draw 「Pipeline by Stage」 as a ranked bar, not a funnel #57's numbers to the pixel is the control that both runs measured the same thing. Volunteered, not asked for.
- The one geometric difference reported rather than smoothed over: the card is 497px tall in after/en versus 481px elsewhere, because the longer English description wraps to two lines. Plot area and bars identical, no truncation, no horizontal overflow at any pass. That is the right level of disclosure for a layout claim.
- The pixel claims are pixel measurements.
path.recharts-rectanglebounding boxes, paired to categories by y-position. No layout finding from a DOM read alone. Dogfood pass on M3-completemain: drive the whole contract lifecycle in a browser and report what a real user hits #45's lesson landed. - "Unchanged from base" measured, not inherited — the same four gates run on a detached worktree at
2d63324, then removed. That is the difference between asserting a baseline and having one.
Your two out-of-scope findings, both routed
- The colour fallback. Category colours also fall back to a positional palette instead of the
clm_contract.statusoption colours,Activegreen inenand blue inzh-CN, from the same label-keyed category map. That is a second consumer of the same root cause I filed as Dashboard widgets:options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale objectstack#17344, and it strengthens the remedy argued there (key the category map on stored values, not resolved labels — it fixes ordering and colour together). I am adding it to that issue; you correctly owned no filing on that repo. DESIGN.md§09 still calls this widget 各阶段合同数漏斗. Real, and now doubly stale — 「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 #48 stopped shipping the funnel and this card narrows what it plots. Getting its own card, queued behind PR Record four maintainer rulings in DESIGN.md §03, §06 and §09 #40 like the other twoDESIGN.mditems, so three branches never contend for that file. ⛔ Correctly not edited here: §09 is the maintainer's wording and no PR trips over it.
Generated by Claude Code
os-dev-report
{ "issue": 59, "status": "done", "branch": "claude/issue-59-pipeline-in-flight", "pr": "https://github.com/objectstack-ai/hotclm/pull/62", "premise_still_valid": true, "summary": "Dropped 'active' from stage_funnel's filter on the Legal Workbench, leaving the six in-flight stages, and moved the description with it in the widget definition and both translation bundles. Titles, id, sortBy/sortOrder and the horizontal-bar mark are untouched; options.stageOrder was not reintroduced; no other widget, no other board and no seed data was changed. The card's DEFECT holds and is fixed, but two of its supporting claims did not survive being tested and the PR records both: (1) its argument that 'active' is excluded by the widget's own rule does not hold - the rule's operative clause is 'reads as if a contract were meant to reach them', and a contract IS meant to reach active, which is exactly what separates it from the four excluded outcomes; nor is state-machine terminality the criterion, since contract.hook.ts gives rejected an edge back to draft and excludes it anyway. The filter is now justified by the criterion that does discriminate - whether legal has work to do on the stage - and the widget comment states it. (2) its arithmetic: 'active' was 60 of the 106 contracts the filter admitted (57%), not '60 of 74' (81%). The width claim beside it is sound and is what the change rests on. Recommendation 2 (a contracts-in-force metric) was considered and SKIPPED, argued from legal's job: legal never acts on the size of the in-force book - every job it has over a live contract is a dated or event-driven slice with its own destination - and DESIGN.md 09 assigns that sense to the executive board, where it already exists as value rather than count and where legal reaches it, all three boards sitting in one nav group gated on the single capability every audience holds. The assignee field was set by the PM dispatch and was not written by me; the newest Claim: names this branch and no second claim was posted.", "tests": "GATES (exit code captured immediately, no pipe between command and $?): pnpm validate -> 0; pnpm lint -> 0 with '21 warning(s), 5 suggestion(s)'; pnpm typecheck -> 0; pnpm lint:i18n-gate -> 0 with 'LOCALES \"en\",\"zh-CN\" / REACH 12 surface classes (widget=38) / COVERAGE 0 missing keys across 2 locale(s)'. BASE CONTROL: the same runs on a detached worktree at base 2d63324 give the identical 21/5, widget=38 and 0 missing keys, so 'unchanged from base' is measured, not inherited; that worktree was removed after the reading. DATA: POST /api/v1/analytics/dataset/query on contract_metrics, dimension status, no filter, taken twice against the running demo database (before-state boot and again after the restart that compiled the change) - identical both times: draft 10, submitted 6, in_review 12, in_approval 8, approved 4, signing 6, active 60, expired 8, terminated 4, cancelled 2, rejected absent (0), total 120 = DESIGN.md 10's spread. The widget now plots 46. BROWSER (the deliverable): pnpm demo on a clean database, port 4159, Chromium at /opt/pw-browsers/chromium-1194/chrome-linux/chrome, viewport 1440x1000, signed in as admin@objectos.ai, at /_console/apps/clm/dashboard/legal_workbench. Four passes - before and after, each in an en and a zh-CN context (documentElement.lang came back 'en' and 'zh', so the locale took). Bar widths measured off path.recharts-rectangle bounding boxes and paired to their category by y-position. BEFORE (en and zh-CN IDENTICAL): Active 615, In Review 123, Draft 102.5, In Approval 82, Submitted 61.5, Signing 61.5, Approved 41; x-axis 0/15/30/45/60. AFTER (en and zh-CN IDENTICAL): In Review 615, Draft 512.5, In Approval 410, Submitted 307.5, Signing 307.5, Approved 205; x-axis 0/3/6/9/12. Plot area is 615px (x 771 to 1386) in every pass, so resolution went from 10.25 to 51.25 px per contract, exactly 5x. The before numbers reproduce PR #57's to the pixel (it rounded 102.5/61.5 to 103/62), which is the control that both runs measure the same thing. The captured request body carries the new filter {\"status\":{\"$in\":[\"draft\",\"submitted\",\"in_review\",\"in_approval\",\"approved\",\"signing\"]}}. One geometric difference between locales and it is NOT in the chart: the card is 481px tall in every state except after/en, where it is 497px, because the longer English description wraps to two lines while the Chinese fits one; plot area, bar widths and axis are identical. No truncation; scrollWidth == clientWidth in all four passes. Cross-check visible on the after board: the In Review metric tile reads 12 and the In Review bar reads 12; Awaiting Intake reads 6 against the Submitted bar's 6. Console in all four passes: one pre-existing line (404 on a resource fetch), identical before and after. Screenshots taken of the widget and the whole board in all four states; not attached (no image host in this container), so the pixel numbers are the evidence. No ablation applies - this is a metadata filter change, not a new guard.", "mcp_calls": "0 - a repo-scoped REST read was probed first and succeeded (public repo), so issue body, comments, PR #57's body, the open-issue duplicate scan, PR creation and this comment all went over REST with git for the pushes. Declaring the channel choice rather than an MCP count.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: platform gap (report, never patch). In a zh-CN console this chart's category labels stay English - the gap PR #57 documented - and the SAME root cause shows in a second place not previously recorded: the per-category colours also fall back to a positional palette instead of the clm_contract.status option colours (Active is green #2F7D5B in en, blue in zh-CN). The client keys its category map by the bundle's translated labels while the query rows carry the API's English ones. Measured before AND after this change, so pre-existing and untouched; dropping one category simply shifts the fallback palette by one slot. Successor: the upstream objectstack report #57 already routed (objectstack-ai/objectstack#17344 family) - it belongs on that repo and this card owns no issue-filing there.", "noted, not filed: DESIGN.md 09 still calls this widget 各阶段合同数漏斗 (a funnel), which #48 and #57 deliberately stopped shipping and which this card narrows further, so the design authority and the app now disagree in two ways rather than one. Not filed and not edited: 09 wording is a maintainer decision, not a defect in code, and it is not in any of the three fileable classes. Successor: none - no queued PR touches it; it is a line for the maintainer at the next design pass." ] }
Generated by Claude Code
- added a commit that references this issue
on Sep 10, 2026
The defect
stage_funnelon 法务工作台 (now a ranked bar, PR #57 /2b74c4f) admits seven statuses and its own comment states the rule it admits them by:activeis a terminal outcome by that same rule. A contract that reachesactivehas left the pipeline — it does not climb onward; it sits in force until it expires, is terminated or renews. The rule excludes four terminal states and admits the fifth, which happens to be the biggest one in the book.The cost is measured, in PR #57's own browser readings. Bar widths on the 8-column widget, en and zh-CN identically:
activeis 60 of the 74 contracts the filter admits — 81% of the plotted total, 83% of the plotted width — and it compresses the six stages legal actually works on into a 41–123px band where a 3× difference (4 vs 12) reads as a barely-visible one. The mark is now honest about its shape; the scale is dominated by the one category the widget's own rationale would exclude, and it is the category legal has nothing to do.This is the dashboard's only chart. The other five widgets on it are all
metriccards, all in-flight or throughput:awaiting_intake·in_review·review_ageing(In Review Over 30 Days) ·negotiation_stalled(Waiting on Counterparty) ·approval_throughput(Approved This Month). So the one widget with room to compare stages spends 83% of it on a number nobody on this dashboard acts on, and there is no contracts-in-force count anywhere on the dashboard foractiveto be serving as.Recommendation
Two changes, in this order:
activefrom the widget's filter, leaving the six in-flight stages (draft·submitted·in_review·in_approval·approved·signing, 46 contracts). Widest bar becomesin_reviewat 12, the six bars become mutually comparable, and the widget's admission rule finally matches its own stated reason. Update the title and description to say in-flight — "Pipeline by Stage" survives, but the description ("Contracts at each lifecycle stage right now — a snapshot, largest stage first, not a conversion sequence") no longer describes what is plotted, and it lives in both translation bundles.metric, which is the shape the other five use, not as a seventh bar. That is a judgement call: state your reading and say why, and if you conclude the legal audience does not need it, say that instead and skip it. Do not add it and keep it in the bar.Take a reading before you accept my numbers: re-count the statuses out of the running database (
POST /api/v1/analytics/dataset/queryoncontract_metrics, dimensionstatus, no filter) rather than trusting the table above. It came from #48's run and the fixture is re-dealt by other cards.Constraints
sortBy: 'contract_count'/sortOrder: 'desc'stay. ⛔ Do not reintroduceoptions.stageOrder: it is honoured only by the funnel branch of console 17.4.0 and is silently dropped in every non-enlocale — filed upstream as Dashboard widgets:options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale objectstack#17344, and PR Draw 「Pipeline by Stage」 as a ranked bar, not a funnel #57 removed it deliberately rather than leaving inert metadata behind.src/data/to make the chart look better. The distribution is DESIGN.md §10's, pinned byassertSpread, and a demo book where 60 of 120 contracts are in force is correct — a real contract book is mostly contracts in force. The chart has to serve that shape, not the other way round.stage_funnel— it is the key both bundles address it by. Same reasoning PR Draw 「Pipeline by Stage」 as a ranked bar, not a funnel #57 gave.Acceptance
pnpm validate && pnpm lint && pnpm typecheck && pnpm lint:i18n-gateall 0, exit codes captured before any pipe.pnpm demoon a clean database, Legal Workbench, before and after, inenandzh-CN, screenshotted. Report the bar widths the same way Draw 「Pipeline by Stage」 as a ranked bar, not a funnel #57 did — a claim that the six stages are now comparable needs the pixel measurement behind it, and the two locales must come out identical.Provenance
Not a #48 regression — the filter is older than that card and #48 correctly did not widen to cover it. Surfaced by #48's own measurements while reviewing PR #57; the PM seat's observation, recorded in the ACCEPT on #48.