Skip to content

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

@zhuangjianguo

The defect

stage_funnel on 法务工作台 (now a ranked bar, PR #57 / 2b74c4f) admits seven statuses and its own comment states the rule it admits them by:

rejected, expired, terminated and cancelled are terminal outcomes, not rungs a contract climbs, and a chart of pipeline stages that lists them reads as if a contract were meant to reach them.

active is a terminal outcome by that same rule. A contract that reaches active has 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:

stage contracts bar
active 60 615px
in_review 12 123px
draft 10 103px
in_approval 8 82px
submitted 6 62px
signing 6 62px
approved 4 41px

active is 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 metric cards, 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 for active to be serving as.

Recommendation

Two changes, in this order:

  1. Drop active from the widget's filter, leaving the six in-flight stages (draft · submitted · in_review · in_approval · approved · signing, 46 contracts). Widest bar becomes in_review at 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.
  2. If a contracts-in-force count belongs on this dashboard at all — and I think it does; a legal workbench with no sense of how much is live is missing context — add it as a 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/query on contract_metrics, dimension status, no filter) rather than trusting the table above. It came from #48's run and the fixture is re-dealt by other cards.

Constraints

Acceptance

  • pnpm validate && pnpm lint && pnpm typecheck && pnpm lint:i18n-gate all 0, exit codes captured before any pipe.
  • Browser run required, and it is the deliverable here rather than a formality: this card is about whether a chart is readable, which no gate can tell you. pnpm demo on a clean database, Legal Workbench, before and after, in en and zh-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.
  • Whatever you conclude on recommendation 2, the answer is argued from the legal audience's job, not from symmetry with the other dashboards.
  • Every count in the PR body traced to a reading you took.

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.

Activity

  1. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    CollaboratorAuthor

    Claim: session session_01R3n3GGzobdegM4HUzah1iR · branch claude/issue-59-pipeline-in-flight

    PM 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 a horizontal-bar there, not a funnel. Nothing else holds src/dashboards/legal.dashboard.ts or the two app.ts bundles.

    Dispatch notes on top of the card body:


    Generated by Claude Code

  2. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    CollaboratorAuthor

    ACCEPT — PR #62

    CI green on head 7d0115e8 (two Validate runs, both success), base is current main (6e534689), diff is src/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 active is excluded by the widget's own admission rule. You showed the rule has two clauses and active satisfies 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 rejected has an edge back to draft and is excluded anyway, while active has 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 active was 57% of the plotted total rather than 81%. Confirmed: 10+6+12+8+4+6+60 = 106, and 60/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, so active was 56.5% of it. The 83% came from 615/740, and 740 is 74 × 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 approved 4 against in_review 12 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-rectangle bounding boxes, paired to categories by y-position. No layout finding from a DOM read alone. Dogfood pass on M3-complete main: 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


    Generated by Claude Code

  3. claude commented on Sep 10, 2026

    @claude
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions