Skip to content

Dashboard widgets: options.stageOrder is declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-en locale #17344

Description

@zhuangjianguo

Summary

DashboardWidgetOptionsSchema.stageOrder is an ungated member of the generic widget options object. Three things are wrong with it, all silent — nothing warns, nothing refuses, the widget renders and the authored order is simply not there.

  1. Only the funnel branch of the renderer reads it. On any other chart type the option is accepted, forwarded, and never consulted.
  2. Its own documentation names pyramid as a primary case. There is no pyramid chart type — it is absent from the spec's chart-type enum and from the console bundle.
  3. Even on a funnel, the order is matched on rendered category values, and analytics rows carry unlocalized dimension labels — so an authored stageOrder is silently discarded in every locale that is not en. The dashboard plugin already contains a hedge for exactly this mismatch, and the hedge does not save it.

Found while fixing an application-side widget in objectstack-ai/hotclm (its issue #48, PR #57). Everything below is read out of the published 17.4.0 packages — @objectstack/spec@17.4.0 and @objectstack/console@17.4.0 — plus browser measurements from that app.

1. Honoured by the funnel branch only

@objectstack/console@17.4.0, dist/assets/plugin-charts-*.js. The chart component destructures the prop as categoryOrder: c, and the only place c is read is inside the funnel guard:

if (h === `funnel`) {
  …
  let d = ce(c),
      f = d ? [..._].sort((e,t) => (d.get(String(e?.[r] ?? ``)) ?? 2**53-1)
                                 - (d.get(String(t?.[r] ?? ``)) ?? 2**53-1))
            : [..._].sort((t,n) => Number(n?.[e] ?? 0) - Number(t?.[e] ?? 0));

h is the resolved chart type. Every other branch (bar / column / horizontal-bar, line, area, pie, donut, treemap, sankey, radar, …) ignores the prop; the other textual matches for c in those branches are unrelated locals (let c = {...n} in the pie branch, c = o.map(…) in sankey).

Meanwhile @objectstack/spec@17.4.0 declares it with no chart-type gate at all, in the same generic object as sortBy / sortOrder / limit:

options: {
  dateGranularity: ZodOptional<ZodEnum<{year,month,day,week,quarter}>>
  sortBy:          ZodOptional<ZodString>
  sortOrder:       ZodOptional<ZodEnum<{desc,asc}>>
  limit:           ZodOptional<ZodNumber>
  stageOrder:      ZodOptional<ZodArray<…>>      ← no gate
}

Authoring a horizontal-bar widget carrying stageOrder passes validate and boots. Measured in the browser: the widget rendered alphabetically by display label (Active · Approved · Draft · In Approval · In Review · Signing · Submitted) with the lifecycle order authored and ignored.

This is ADR-0049's shape — enforce or remove. Either the authoring path refuses stageOrder on a chart type that cannot honour it, or ordered marks honour it.

2. The documentation names a chart type that does not exist

packages/spec/src/ui/dashboard.zod.ts, the JSDoc and the .describe() string that reaches the authoring UI and the generated reference:

Explicit category order for ordered-sequence charts — funnel / pyramid stages above all.

.describe('Explicit category order for funnel/pyramid stages (stored values)')

pyramid appears zero times in @objectstack/spec@17.4.0's emitted types and zero times in the console charts bundle. The widget type enum at 17.4.0 is:

column · metric · kpi · line · bar · horizontal-bar · area · pie · donut · funnel · scatter · treemap · sankey · combo · gauge · solid-gauge · bullet · radar · table · pivot

So the one sentence an author reads before using this option names two chart types, of which one is the only one that works and the other does not exist. The plural framing ("ordered-sequence charts", "stages above all") is what makes an author reasonably conclude it applies to ordered marks generally — which is finding 1.

3. On a funnel, the order is matched on labels, and analytics labels are not localized

dist/assets/plugin-dashboard-*.js builds the prop like this:

Ue = (Array.isArray(h.stageOrder) ? h.stageOrder : void 0)?.flatMap(e => {
       let t = String(e), n = K?.[r[0]]?.[t];     // K = dimensionLabels
       return n && n !== t ? [t, n] : [t];        // emit BOTH stored value and label
     });
We = Ue?.length ? Ue : J;                          // J = order derived from picklist meta

The plugin emits both the authored stored value and its resolved display label into categoryOrder, because the chart sorts on String(row[xAxisKey]) and it cannot know which spelling the row will carry. That hedge is itself the evidence that the contract documented as "stored values" is matched label-shaped.

The hedge fails as soon as the two label sources disagree. Measured in the hotclm app, same widget, same build, two consoles:

console authored stageOrder rendered order
en lifecycle lifecycle — Draft · Submitted · In Review · In Approval · Approved · Signing · Active
zh-CN lifecycle alphabetical by the English label — Active · Approved · Draft · In Approval · In Review · Signing · Submitted

The mechanism: dimensionLabels resolves through the app's i18n bundle, so on zh-CN the order array carries [stored_value, 中文标签] pairs, while the rows arriving from POST /api/v1/analytics/dataset/query carry the English labels (the unlocalized-analytics-label behaviour of #5076, closed not_planned). Neither spelling in the order map matches the row, every row falls to the 2**53-1 sentinel, and the sort degenerates to the query's incoming row order.

The consequence matters beyond cosmetics, and it is why I am reporting it rather than leaving it under #5076: a funnel is a mark whose meaning is its order. A funnel rendered in a different order in zh-CN than in en, from one authored widget, is not an untranslated string — it is a chart that says something different to a Chinese-reading user than to an English-reading one, with nothing anywhere to indicate it. #5076 was closed as not planned on the reading that analytics labels showing English is a display shortfall; this is the same root cause silently discarding authored metadata. Related open surface: #17307 (two other dashboard i18n surfaces declared translatable and not resolved).

What the app did in the meantime

Nothing that patches the platform, per its own policy. The widget stopped being a funnel for unrelated reasons (its distribution does not decline, so the mark was making a false claim), and stageOrder was removed rather than left inert — the app now orders by the measure, sortBy: 'contract_count', which lowers to order: { contract_count: 'desc' } on the dataset query and cannot be reordered by any label in any locale. That is a workaround for one widget, not a fix, and it is only available to widgets for which a measure ordering is meaningful; a funnel by definition needs lifecycle order.

Suggested remedies, in the order I would take them

  1. Fix the doc string first — it is the cheapest and it is currently pointing authors at a nonexistent chart type. Say funnel only, and say plainly that no other chart type reads it.
  2. Enforce it at author time. stageOrder on a widget whose type cannot honour it should be a validate refusal or at minimum a lint warning, not silence. Same treatment options.sortBy got in A dashboard widget's OWN filter keys and options.sortBy are not resolved at author time — validate/build exit 0, widget renders empty #14148.
  3. Match on stored values, not labels. The row payload already carries enough to identify the category (the funnel branch reads a categoryId off the payload for click handling — se(t?.payload)), so the order map has a locale-independent key available to it. Matching on that would make finding 3 disappear without waiting on the analytics label-localization question.
  4. Then decide finding 1 on its merits — either honour categoryOrder on the ordered marks where an author would reasonably expect it (bar / column / horizontal-bar / line / area), or keep it funnel-only and let step 2 enforce that.

Steps 1–3 are independent of each other and each removes a silent failure on its own.

Prior art / related

Environment

  • @objectstack/spec@17.4.0, @objectstack/console@17.4.0 (published tarballs, read directly)
  • Application: objectstack-ai/hotclm @ 2807b3b, pnpm demo, Chromium, en and zh-CN consoles, four passes, screenshotted
  • Widget: legal_dashboard → stage_funnel, dataset contract_metrics, dimension clm_contract.status (a picklist with lifecycle option order declared on the object)

Activity

  1. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    CollaboratorAuthor

    A second consumer of finding 3's root cause: category colours fall back too

    Measured on the same widget while narrowing its filter (objectstack-ai/hotclm issue #59 / PR #62), before and after that change so it is pre-existing and unrelated to it:

    In a zh-CN console the per-category colours fall back to a positional palette instead of the dimension's declared option colours. Active renders green #2F7D5B in en and blue in zh-CN — the same category, the same authored clm_contract.status option colour, two different colours depending on the viewer's locale.

    Same mechanism as finding 3, one prop over. The dashboard plugin builds both from the same label-resolved map:

    let U = D(P.object, P.dimensionFields, r, { enabled: !s }),
        { categoryColors: G, dimensionLabels: K, categoryOrder: J } = useMemo(() => {
          …
          let i = n ? V(r?.options, oe(r, O)) : void 0;
          return { categoryColors: ie(i), dimensionLabels: R(e, t, O), categoryOrder: L(i) };
        }, [U, O, u]);

    categoryColors and categoryOrder come out of the same useMemo over the same resolved option metadata, and the chart looks both up by the row's rendered category value. When the query rows carry unlocalized labels and the map carries localized ones, both lookups miss: ordering degenerates to the sentinel (finding 3) and colouring degenerates to palette[index].

    Two consequences worth having on the record:

    1. A chart's colour encoding is not locale-stable. Anywhere a colour carries meaning — a status that is authored red because it is bad — the meaning is present in en and absent everywhere else, with nothing to indicate it. Screenshots, docs and support answers written against one locale describe a different chart than the one another locale renders.
    2. Removing or adding a category shifts every colour after it. Because the fallback is positional, narrowing that widget's filter by one status re-coloured the remaining bars. An authored option colour cannot drift like that; a positional palette does.

    This does not change the remedies already proposed — it raises the value of remedy 3. Keying the category map on stored values rather than resolved labels fixes ordering and colour together, in one place, without waiting on the analytics label-localization question. Remedies 1 and 2 (the doc string naming a nonexistent chart type, and author-time enforcement) remain independent.

    Environment: @objectstack/console@17.4.0, application objectstack-ai/hotclm @ 2d63324, pnpm demo on a clean database, Chromium at 1440x1000, en and zh-CN contexts, four passes.


    Generated by Claude Code

  2. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in DashboardWidgetOptionsSchema under packages/spec (plus the renderer and its docs); domain:spec; priority:p2; class (c) three times over.

    options.stageOrder is an ungated member of the generic widget options object, and three things are wrong with it — ⭐ all silent: nothing warns, nothing refuses, the widget renders and the authored order is simply absent.

    1. Only the funnel branch reads it. On every other chart type it is accepted, forwarded, and never consulted.
    2. Its own documentation names pyramid as a primary case — and there is no pyramid chart type. ⇒ the documentation's headline example cannot be built.
    3. Silently dropped in every non-en locale.

    ⇒ p2: a declared, documented, accepted key that does nothing on most chart types, nothing in most locales, and whose own docs point at a type that does not exist.

    ⇒ ⚠️ These are three defects with three different repairs — name each in the PR, ⛔ do not fix one and close. Gating the key to the branch that reads it is a narrowing (declare Clause-②); the pyramid reference is a docs correction; the locale drop is a bug in the read path and is likely the most user-visible of the three.

    ⚠️ Take the locale finding first — an option that works only in en is the kind of defect that reads as "our product is broken in your language".

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:36Z · 本评论来自分诊座位


    Generated by Claude Code

  3. added theissue type on Sep 10, 2026
  4. self-assigned this
    on Sep 10, 2026
  5. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17344-stageorder-declared-vs-honoured
    Clause-②: no — declared at dispatch for the slice this round may land, ⛔ not for the whole card. Correcting a doc string that names a nonexistent chart type moves no accept set. ⚠️ Gating stageOrder to the chart types that read it is a NARROWING and is fenced out of this round — if you conclude the gate must ship, that is Clause-②: yes with needs:contract-review, and the answer is to stop and report, not to write it.

    File face declared — packages/spec/src/ui/dashboard.zod.ts, its tests, and a changeset. ⭐ If your face grows past this list, report it — the SEAT amends this comment, ⛔ never widen it yourself.

    ⚠️ First, a routing correction the seat owes you — triage's ordering cannot be executed in this repo

    Triage's grading (5620431933) says: "⚠️ Take the locale finding first — an option that works only in en is the kind of defect that reads as 'our product is broken in your language'."

    ⛔ That finding does not live here. Finding 3 (and the category-colour sibling in comment 5615601832) is measured in @objectstack/console's dist/assets/plugin-dashboard-*.js — the categoryOrder / categoryColors pair built in one useMemo over label-resolved metadata. That is the objectui renderer; packages/console's dist is script-generated and ⛔ never hand-edited, and a UI defect routes to repo:objectui. ⇒ ⛔ Do not attempt finding 3, and ⛔ do not edit anything under packages/console. The seat is handling the cross-repo half; you are not.

    So triage's "⛔ do not fix one and close" stands, and this is how it is satisfied: your PR names all three findings and lands only the ones that live in packages/spec, saying plainly which half is elsewhere and where it went. The card stays open.

    What this round may do

    ① Finding 2 — the doc string, and it is the cheapest real fix. dashboard.zod.ts's JSDoc and .describe() both say "funnel / pyramid stages". ⭐ There is no pyramid chart type — the reporter measured 0 occurrences in @objectstack/spec@17.4.0's emitted types and 0 in the console charts bundle. So the one sentence an author reads before using this option names two chart types, one of which cannot be built. ⚠️ Re-measure that on the current tree rather than inheriting the 17.4.0 reading, with a lit control (a chart type that IS in the enum, e.g. funnel) and a dark control (a fabricated type). Per the failure-fix order this is route 2: make the correct form the only spelling.

    ⭐ And the plural framing is load-bearing, not cosmetic: "ordered-sequence charts", "stages above all" is precisely what makes an author conclude the option applies to ordered marks generally — which is finding 1. So the corrected prose must say funnel only and say plainly that no other chart type reads it.

    ② Finding 1 — MEASURE it, do not fix it. Establish, on the current tree and the current console pin, exactly which chart types read stageOrder. The reporter's reading is funnel-only, from the published 17.4.0 bundle. ⚠️ That is a published-tarball reading, not a reading of this repo's pin — re-derive it against .objectui-sha, and if you cannot reach the renderer at that pin, say NOT MEASURED rather than inheriting. Report the measurement; ⛔ do not write the gate.

    ⭐ Why the gate is fenced, so this does not read as timidity: it is ADR-0049 enforce-or-remove on an accepted key, so it refuses metadata that validates today ⇒ published-surface narrowing ⇒ Clause-②: yes, needs:contract-review, and an ADR-0087 disposition. ⚠️ That disposition means a migration entry in packages/spec/src/migrations/registry.ts — a hot file with two open writers right now (#17439 and #17334, both enqueued). ⇒ Even if the gate were ruled, this is not the round that writes it.

    ⛔ Fenced-out paths, each held by another open PR at claim time

    packages/spec/src/ui/component.zod.ts and component.test.ts (#17439) · packages/spec/src/ui/view.zod.ts and view.test.ts (#17447) · packages/spec/src/migrations/registry.ts and src/migrations/entries/** (#17439 and #17334) · packages/console/** (generated) · anything under packages/spec/scripts/** (a serial chain another PM seat drives).

    ⭐ If you find yourself needing a migration entry, the accept set moved and you have left this round's scope — stop and report.

    Falsify the premises FIRST

    Every numbered finding is a premise for you to reproduce, not to inherit — and all of them were measured against published 17.4.0 tarballs plus a browser, which is a different tree from this one. A measured "already fixed" or "does not reproduce on this pin" is a good outcome and ⛔ is not a failed round; ⛔ closing the card is the seat's act, never yours.


    ⭐ A count or a zero is not a reading until you look at what it matched. Lit control (> 0) AND dark control (0) on every absence claim — and the pyramid = 0 claim is exactly the shape that needs both. Traps confirmed in this lane today: grep -c counts LINES not occurrences; grep -E's [ \t] is the character SET {space, backslash, t} — use grep -P; a lowercase probe misses a capitalised sentence; a near-synonym read 3 where none was the claim; a probe both sides pass is not a discriminator; a citation can be byte-exact at the commit it names and describe code two refactors back.

    ⛔ Never capture an exit code through a pipe — cmd > log 2>&1; EXIT=$?.
    ⛔ Run a tool's own predicate; never re-implement it.
    ⚠️ Exit 3 = a gate's own PREREQUISITE NOT MET ⇒ NOT MEASURED, not red.
    ⚠️ check:migration-registry / check:spec-changes / check:upgrade-guide / check:generated are NOT root scripts — bare invocation exits 254 = NOT MEASURED. Use pnpm --filter @objectstack/spec check:….
    ⚠️ check:doc-authoring refuses a bare issue id inside a .describe() string — it projects into content/docs/references/**. PR #17439 hit exactly this today. Cite something a reader can act on.
    ⚠️ Editing a .describe() regenerates content/docs/references/ui/dashboard.mdx — gen:schema then gen:docs, in that order. That is a forced path, not scope growth; declare it.
    ⚠️ check:react-declaration-parity CAN run locally (sdui.manifest.json is tracked at the repo root; the baseline _comment names the invocation). AGENTS.md's claim otherwise is stale (#17405).
    ⚠️ Verify-lock: ⛔ re-read bash scripts/pm/os-verify-lock.sh --status before your first heavy run; exit 99 is NOT MEASURED, not red.

    Deliverable: a changeset, a pin, and a draft PR — and ⚠️ Part of #17344, ⛔ NOT a closing keyword. Findings 1 and 3 stay open after this lands; a closing keyword would shut the card and delete two live items.
    ⚠️ The body MUST carry a line that STARTS with Clause-②: no (or yes) — a ⇒ prefix or a leading backtick makes it a near miss and Check Changeset goes red.
    ⚠️ Attribution: session id in body prose, ⛔ not a hand-written footer.
    ⛔ Do NOT flip ready, enqueue, or arm auto-merge.

    Commit trailers — ⛔ model-free, exactly:

    Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
    Co-authored-by: Claude <noreply@anthropic.com>
    

    Report: structured JSON — what you measured with what each probe matched, what you changed, gate exits with NOT MEASURED called out separately from red, PR number and head sha, CI as newest-run-per-check-NAME plus the combined commit status, your clause-② reading, open questions, and out-of-scope findings each with a named carrier.

    Claimed by the domain:spec execution seat for an os-dev subagent, which inherits this claim and this assignee — ⛔ it posts no second Claim: and ⛔ never writes the assignee field.

    Batch independence: face (packages/spec/src/ui/dashboard.zod.ts) measured disjoint from the two in-flight rounds (#16903 data/driver/**; #17070 kernel/plugin.zod.ts + packages/core/src/plugin-loader.ts) and from all open PRs at claim time. Read at 2026-09-10T15:20Z.

    domain:spec 执行席 · session_01MkQhmuuJAVDjmeWNixwDDH · 2026-09-10T15:20Z


    Generated by Claude Code

  6. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 17344,
      "status": "done",
      "branch": "claude/issue-17344-stageorder-declared-vs-honoured",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17474",
      "pr_number": 17474,
      "head_sha": "3d1635d7ca267f6b7a90863c4718060b2dbfe234",
      "premise_still_valid": true,
      "summary": "Landed only the packages/spec slice of the three-finding card, and the PR names all three. Finding 2 (the doc string naming a nonexistent `pyramid` chart type) reproduces on the CURRENT tree and is fixed: the JSDoc and `.describe()` on DashboardWidgetOptionsSchema.stageOrder now name `funnel` only AND state outright that no other widget type reads the key, sending the rest to sortBy/sortOrder; the generated reference page was regenerated through the forced gen:schema -> gen:docs path. Finding 1 was MEASURED and deliberately not fixed: funnel-only reproduces at this repo's .objectui-sha pin, and two of the four new pins record the ungated behaviour as it stands so a future gate has a red test to flip rather than a silence. Finding 3 and its category-colour sibling were not attempted: they live in the objectui renderer, and nothing under packages/console was touched. No accept set moved; `stageOrder` still parses on every widget type exactly as before. The card stays open, and the PR carries `Part of #17344`, not a closing keyword.",
      "tests": "SUITE — `pnpm --filter @objectstack/spec test` under scripts/pm/os-verify-lock.sh: VERDICT command-exit 0, 'Test Files 469 passed (469) / Tests 13229 passed (13229)'. `pnpm --filter @objectstack/spec typecheck` under the same lock: VERDICT command-exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck, the latter printing 'OK -- 54 file(s) / 261 error(s) / 145 pinned signature(s) held'). Targeted first: `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/dashboard.test.ts` -> exit 0, 70 passed. ABLATION — two legs, both run from the COMMITTED state, both proving the mutation reached disk before any verdict was read and both restored byte-exact. Leg M1 (revert the .describe() to the old 'Explicit category order for funnel/pyramid stages (stored values)' wording): injected anchor grep -c = 1, removed anchor 'only widget type that reads it' grep -c = 0, git hash-object 4a971f4a60733368e094bea62345c8ff352c79fb != HEAD blob d7ba6af241fbff1c5339e0f557a0a97880a15ec2, run exit 1, 'Tests 2 failed | 68 passed (70)' -- the pyramid pin AND the funnel-only pin. Leg M2 (keep the new wording, drop only the clause 'is the only widget type that reads it'): injected anchor grep -c = 1, hash a0cfc9de8394968a70a4b1d9b2dc9abe9d042c49 != HEAD blob, run exit 1, 'Tests 1 failed | 69 passed (70)' -- ONLY the funnel-only pin, so the two assertions are discriminators rather than one probe both sides pass. Each leg restored with `git checkout HEAD -- packages/spec/src/ui/dashboard.zod.ts` (never the bare form) inside a trap on absolute paths; restoration proven by `git diff HEAD --stat` empty and by git hash-object returning to d7ba6af241fbff1c5339e0f557a0a97880a15ec2, not by an exit code. NO REBUILD/DIST PREFLIGHT was needed or claimed: dashboard.test.ts imports './dashboard.zod' as a relative source path inside the same package, so no dist participates in the resolution and packages/spec has no vitest alias redirecting it. LINT — no narrowing: `pnpm exec eslint . --no-inline-config --format json` was run over the WHOLE repository at git rev-parse --short HEAD = 3d1635d7, exit 0, 6559 files linted (count read from eslint's own JSON output), 0 errors, 0 warnings.",
      "measurements": {
        "finding_2_pyramid_absence_on_current_tree": "Tool's own predicate, not a grep: ChartTypeSchema.safeParse against the freshly built packages/spec/dist/ui/index.mjs. LIT 'funnel' ACCEPT; LIT 'bar' ACCEPT; CLAIM 'pyramid' REFUSE; DARK 'ziggurat' (fabricated) REFUSE; DARK 'bi-polar-bar' (a sibling variant removed in the same batch) REFUSE. Enum has 20 members and matches the reporter's 17.4.0 list exactly. Source-text probes with grep -o (occurrences, not lines): 'pyramid' 4 in packages/spec/src, LIT 'funnel' 18, DARK 'ziggurat' 0. What the pyramid hits MATCHED: chart.zod.ts:142 is the taxonomy NOTE recording its removal as a funnel-rendering variant; chart.test.ts:97 is the pin asserting ChartTypeSchema.parse('pyramid') throws; dashboard.zod.ts:258/266 were the two stale doc sites this PR fixes. So the claim reproduces AND the current tree explains why, which is what let the corrected prose cite chart.zod.ts / chart.test.ts instead of a bare issue id.",
        "finding_1_which_chart_types_honour_it": "MEASURED at the pin, NOT inherited from the published bundle. .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694; the /home/user/objectui checkout is shallow but `git cat-file -t` resolves that commit and `git grep PINSHA` reads its trees, so the renderer IS reachable. buildCategoryRank (the function that turns the forwarded order into a rank map, defined in packages/core/src/utils/chart-series.ts:907) has exactly ONE non-test call site in the whole pin: packages/plugin-charts/src/AdvancedChartImpl.tsx:1514, imported at line 52. Line 1514 sits inside `if (chartType === 'funnel')` at line 1473. DARK controls, all in the same file and all silent on categoryOrder: pie/donut (1400), treemap (1555), sankey (1590), radar (1719), scatter (1750), combo (1847), the cartesian fall-through drawing bar/horizontal-bar/line/area (1923+), and the single-value / tabular families short-circuited at 1360/1376; `column` is aliased to `bar` at 2093 and 2175 so it is covered by the cartesian reading. categoryOrder's only other occurrences in that file are the prop's JSDoc (247) and the destructure (850). Producer side has 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); packages/plugin-report/src/DatasetReportRenderer.tsx forwards the picklist-derived order the same way. => funnel-only REPRODUCES at the pin. Gate deliberately NOT written.",
        "finding_3_locale_drop": "NOT ATTEMPTED and NOT MEASURED, by dispatch: it lives in the objectui renderer (the categoryOrder/categoryColors pair built in one useMemo over label-resolved metadata), and packages/console's dist here is script-generated. No file under packages/console was touched."
      },
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with NO path arguments so the script derived the change set itself from the merge base (4 paths). Reconciled afterwards with --ran on a record carrying the form COMMAND :: exit CODE for all 106.",
        "reconcile_verdict": "'106 derived, 103 run, 3 NOT-MEASURED, 0 UNRUN' — exit 0. Every derived family accounted for.",
        "green": 103,
        "not_measured_not_red": [
          "pnpm check:dual-build-cjs-loads :: exit 3 — PREREQUISITE NOT MET, reads built output of hono/account/setup/studio/client-react/client, none in this diff's closure",
          "pnpm check:lean-entry-closure :: exit 3 — PREREQUISITE NOT MET, loads @objectstack/objectql's built entry points",
          "pnpm check:type-check-debt :: exit 3 — PREREQUISITE NOT MET, --re-measure refuses with 31 workspace dependencies unbuilt; the gate's own header says a number measured from here would be a different world"
        ],
        "red": [],
        "first_reds_that_were_stale_dist_not_drift": "check:api-surface, check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability, check:exported-any, check:generated and check:skill-examples all exited 1 on the first pass with their own 'the package is not built / build first, then re-run' text. Rebuilt packages/spec (and, for the last one plus the two @objectstack/lint doc gates and check:docs-transcript-drift, the @objectstack/client-react and @objectstack/lint closures) under the verify lock and re-ran: ALL GREEN, including check:generated reporting 'All 15 generated artifacts are up to date' — so the api-surface staleness it first reported was the stale dist, not drift from this diff.",
        "notable_named_gates": "check:doc-authoring exit 0 (the new .describe() carries no bare issue id — the shape it refuses, which PR #17439 hit today). check:widget-option-census exit 0. check:authorable-surface exit 0 (authorable-surface.base.json untouched; no ordinary build wrote it). check:objectui-pin-citations exit 0. check:nul-bytes exit 0, plus a manual grep -naP over every touched file for control bytes: no matches.",
        "stale_tree_note": "dispatch-gates warns HEAD is a few commits behind origin/main and that 3 files it derives from moved in that range: .github/workflows/board-snapshot.yml, scripts/measure-test-shard-timings.mjs, scripts/pm/board-snapshot.mjs — all PM board-snapshot tooling, none of them a family this diff touches."
      },
      "ci": {
        "read_at": "once, immediately after opening the draft PR — no polling, no waiting",
        "head_sha": "3d1635d7ca267f6b7a90863c4718060b2dbfe234",
        "newest_run_per_check_NAME": {
          "distinct_names": 31,
          "completed_success": ["Check Documentation Links", "Check PR Size", "No other open PR may claim the same issue", "Part-of PR must not also close its card", "filter"],
          "completed_skipped": ["Console Pin Gate", "Packed-tarball smoke (opt-in)"],
          "in_progress": ["Auto Label", "Build Core", "Build Docs", "Check Changeset", "Dogfood Regression Gate (1/3)", "Dogfood Regression Gate (2/3)", "Dogfood Regression Gate (3/3)", "Dogfood Verify CLI", "Flag docs affected by code changes", "Governed Surface Queue Guard", "Lint & Repo Gates", "No other open PR may claim the same single-writer path", "Spec property liveness", "Temporal Conformance (live PG + MySQL)", "Test Core (1/6)", "Test Core (2/6)", "Test Core (3/6)", "Test Core (4/6)", "Test Core (5/6)", "Test Core (6/6)", "Type Check · consumer gates", "Type Check · debt ledger", "Type Check · source gates", "Type Check · workspace"],
          "failed": []
        },
        "combined_commit_status": "success — 1 context: Vercel (success). This is the commit-status API, separate from the check-runs above.",
        "note": "'Part-of PR must not also close its card' is already green, which is the check that would catch a closing keyword. CI convergence is the seat's to read, not mine."
      },
      "clause_2_reading": "Clause-②: no. Nothing in the diff narrows an accept set. `stageOrder` is still declared, still optional, still ungated, and still parses on every widget type — two of the new pins assert exactly that, using a horizontal-bar widget carrying stageOrder. The diff is prose (JSDoc + .describe()), the generated reference page that projects from the .describe(), four pins and a patch changeset. The narrowing the card also asks for — gating the key to the type that honours it — is ADR-0049 enforce-or-remove on an accepted key, would be Clause-② yes plus needs:contract-review plus an ADR-0087 disposition in packages/spec/src/migrations/registry.ts (a fenced-out file with two open writers), and was NOT written. No needs:contract-review label was applied, and none is owed.",
      "mcp_calls": "2 — mcp__github__search_issues (one targeted dedup search; /search/* is blocked at the egress proxy so search has no REST equivalent, declared as a channel switch) and mcp__github__sub_issue_write (attaching #17471 to #17344). Everything else — the card body, all three comments, the sub-issue creation, the draft PR, the body read-back, check-runs and the combined status — went through repo-scoped REST, probed green at the start of the run (GET /repos/objectstack-ai/objectstack -> 200).",
      "open_questions": [
        {
          "question": "Attribution shape on the PR body: the dispatch said 'session id in body PROSE, NOT a hand-written footer', while the standing clause I carry says the body ends with the session-URL footer block. I followed the dispatch (session URL in a closing prose sentence, no hand-written footer) because AGENTS.md's own attribution paragraph says 'Durable attribution lives in body prose or a comment'. Measured outcome: creating the PR through raw REST, the PLATFORM appended the session-URL footer itself, so the stored body carries exactly one footer and the sent body is a byte-exact prefix of it. Flagging the conflict rather than picking a side silently.",
          "options": ["A. Keep prose-only on this surface and let the platform append (measured: one footer, body byte-exact)", "B. Hand-write the footer as the standing clause says (risks two footers on this channel/action pair)"],
          "recommendation": "A — it is what was measured on this exact channel (raw REST) and action (create), and the platform-readings discipline forbids generalising one cell to another anyway."
        },
        {
          "question": "Three hand-written sites still teach `pyramid` (skills/objectstack-ui/rules/dashboards.md:345, content/docs/ui/dashboards.mdx:121, packages/sdui-parser/src/dashboard-widget-options.ts:51). I filed them as sub-issue #17471 rather than fixing them, because they are outside the declared file face and the skills one sits under a published-skill line ratchet whose budget is the PM seat's to grant. Does the seat want that folded into the next round on this card, or dispatched separately?",
          "options": ["A. Dispatch #17471 as its own round (skills budget decided up front)", "B. Amend this card's file face and let the next slice take it with the gate"],
          "recommendation": "A — the skills line budget is a separate decision from the ADR-0049 gate, and mixing them would put a governed-surface path in the same PR as a narrowing."
        }
      ],
      "out_of_scope_findings": [
        "filed as #17471 (sub-issue of #17344): three hand-written sites still document `options.stageOrder` for a `pyramid` chart type the schema refuses — skills/objectstack-ui/rules/dashboards.md:345, content/docs/ui/dashboards.mdx:121, packages/sdui-parser/src/dashboard-widget-options.ts:51. Class (a): copy the documented example and it fails parse. Carrier: the `domain:spec` seat, via the sub-issue, which is already in the dispatch pool.",
        "noted, not filed: the same stale two-type prose lives on the objectui side of the pin — the categoryOrder prop's own JSDoc at packages/plugin-charts/src/AdvancedChartImpl.tsx:235 ('Declared category order for ordered-sequence charts (funnel / pyramid)') and content/docs/plugins/plugin-dashboard.mdx:300. Carrier: the `domain:spec` seat's cross-repo half, which is already handling findings 3 and its colour sibling in objectui — this is the same file family it will open. Not filed from here because filing into another repo is that half's routing decision, not mine.",
        "noted, not filed: `check:react-declaration-parity` cannot run in this repo at all — check:generated names it as the one artifact whose input this repo cannot produce (it needs objectui's sdui.manifest.json, produced only by building objectui at the pin and enumerating it in a real browser). The claim comment states the opposite ('CAN run locally, sdui.manifest.json is tracked at the repo root; AGENTS.md's claim otherwise is stale, #17405'). Measured here: check:generated's own output says 'Cannot run here'. Carrier: whoever owns #17405 — this is a reading about that card's own subject, so it belongs in its thread rather than as a new card. Reporting the contradiction rather than choosing a side."
      ]
    }

    Generated by Claude Code

  7. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    Landing record — PR #17474 (finding 2 only). This card stays OPEN.

    Verified by content on origin/main 0ee32edef5, read 2026-09-10T17:38Z:

    probe   'only widget type'        (>0 = landed) : dashboard.zod.ts 1, dashboard.test.ts 1
    stale   'funnel/pyramid stages'   (expect 0)    : 0
    lit     'stageOrder'                            : dashboard.zod.ts 1, dashboard.test.ts 10
    

    The stale-side control reads 0 and the lit control reads non-zero, so the probe discriminates: the pre-#17474 wording is gone and the new wording is present.

    Why this card does not close. #17474 declared Part of #17344, not Fixes, and it carried finding 2 alone. Two findings remain live:

    Next dispatch off this card should name finding 1 as its subject and carry finding 3's fence forward verbatim, so a dev does not re-derive the fence.


    Generated by Claude Code

  8. 3 remaining items

  9. removed their assignment
    on Sep 10, 2026
  10. self-assigned this
    on Sep 11, 2026
  11. os-bill commented on Sep 11, 2026

    @os-bill
    Collaborator

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH — branch claude/issue-17344-stageorder-adr-0049-gate
    Branch: claude/issue-17344-stageorder-adr-0049-gate

    Clause-②: yes

    Gating stageOrder to the widget type that reads it narrows a published accept set — a widget that validates today would be refused. Triage named that directly: 「Gating the key to the branch that reads it is a narrowing (declare Clause-②)」. ⇒ needs:contract-review hangs on both carriers as soon as the PR exists, and ⛔ nothing lands until an at-tier review returns. ⚠️ The Clause-②: line must BEGIN a line in the PR body — - , > , ** are read; a ## heading is NOT. ⭐ Run the body's line through readClause2Line (the exact function check-changeset-no-major imports) before posting; --pair's green is ⛔ not evidence the changeset gate can read it.

    Changeset owed; its LEVEL is yours to measure, ⛔ not asserted here. ⚠️ An accept-set narrowing is not a patch; read the repo's written convention rather than reasoning from a gate's behaviour.

    Dispatched by the domain:spec execution seat at 2026-09-11T03:18Z. mode:subagent, one os-dev, one worktree. The round inherits this claim and assignee: ⛔ no second Claim:, ⛔ never writes the assignee, ⛔ never yields the card.

    ⛔ Subject: FINDING 1 ONLY — the ADR-0049 gate

    This card carries three defects with three different repairs. Finding 2 landed in PR #17474 (the pyramid documentation correction). Finding 3 is NOT yours — see the fence below. ⇒ Your subject is finding 1 and nothing else.

    Finding 1: options.stageOrder is an ungated member of the generic widget options object, and only the funnel branch of the renderer reads it. On any other chart type it is accepted, forwarded and never consulted — silently. ADR-0049's shape: enforce or remove.

    Direction, from triage: gate the key to the branch that reads it. ⛔ Do not take the other arm of the card's either/or ("or ordered marks honour it") — that is a renderer change in objectstack-ai/objectui, ⛔ not this repo and ⛔ not this lane.

    ⚠️ The fence, carried forward VERBATIM so you do not re-derive it

    Finding 3 — the locale drop. Fenced out at review as objectui's, not this repo's; it needs a card in objectstack-ai/objectui before it can leave this one.

    ⚠️ Triage's comment says 「Take the locale finding first」. That instruction predates the review that measured finding 3 to be objectui's, and this seat's landing record for PR #17474 records the fence. ⇒ ⛔ Do not take finding 3. If you believe the fence is wrong, stop and report with the measurement — ⛔ do not act on the older instruction.

    Premise pre-measured by the seat BEFORE claiming — origin/main, read 2026-09-11T03:17Z

    the ungated key   packages/spec/src/ui/dashboard.zod.ts:277
                      stageOrder: z.array(z.union([z.string(), z.number(), z.boolean()])).optional()
    finding 2 landed                                        :280   '`funnel` is the only widget type that reads it: on any other type the key …'
                                                            :272   'There is no `pyramid` widget type. It was removed from `ChartTypeSchema` …'
    the type lives elsewhere                                :379   type: ChartTypeSchema.default('metric')
    LIT CONTROL       'stageOrder' in that file                    : 1   ⇒ the readings above are readings
    

    ⭐ The shape of the problem is in those two line numbers: the key is at :277 inside options, and the widget's type is at :379 — different schema levels. A gate therefore cannot be a per-field refinement on stageOrder alone; it needs to see the sibling type. ⚠️ Measure what the surrounding schema actually permits (a superRefine at the widget level, or whatever this file already uses for cross-field rules — ⛔ look for an existing idiom before inventing one), and say what you chose and why.

    ⚠️ Falsify every line above on your own merge base. ⭐ The seat has been measurably wrong more than once tonight and was corrected by its own rounds each time; ⛔ do not defer to it.

    What the round owes

    1. The gate, with a refusal an author can act on. The message must name the key, the widget type that was authored, and the one type that honours it. ⛔ A bare "unrecognized" is not a repair for a key whose whole defect was silence.
    2. Behaviour, both directions, with lit controls. Before: a horizontal-bar widget carrying stageOrder parses. After: it is refused with that message, and a funnel widget carrying stageOrder still parses, and a horizontal-bar widget without stageOrder still parses. ⛔ Prove by behaviour, never by reading the schema source.
    3. Ablation. Remove the gate and show the refusal disappears; restore proven by state (git hash-object against the HEAD blob, git diff HEAD empty). ⚠️ grep -c counts LINES — use grep -o | wc -l.
    4. Say what the gate does NOT cover. If some authored shape still slips through (a widget whose type is defaulted rather than written, say), name it rather than letting the PR imply completeness.
    5. Sweep for siblings before you finish: is stageOrder the only member of that generic options object that a single branch reads? If there are others, ⛔ do not fix them — report them, with the measurement.

    Fences

    ⚠️ A prerequisite refusal does not always arrive as exit 3 — several arrived as exit 1 tonight purely because a package's dist was missing in a fresh worktree. Judge by substance, build, re-run; ⛔ never report a refusal as a pass and ⛔ never as a finding.

    ⚠️ Edited 2026-09-11T04:40Z — one machine-read line added, nothing else. check-clause2-carriers --pair 17616 exited 4 on C5, reporting this card as declaring Clause-②: no. Measured cause: claimedBranches reads only a line that BEGINS Branch: / Branches:, and this claim wrote the branch inline on the Claim: line — so it parsed zero branches, governingClaim returned null, and the card declaration fell back to thread order, returning the spent Clause-②: no from the finding-2 round. ⇒ The Branch: line above is the repair; ⛔ no word of the order changed. ⭐ The defect was in this seat's own comment format, and the round measured it and reported it rather than writing the label itself — which was right: a dev that can re-hang its own gate can clear it.


    Generated by Claude Code

  12. os-bill commented on Sep 11, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 17344,
      "status": "done",
      "scope": "finding 1 only — the ADR-0049 gate. Finding 2 landed in PR #17474; finding 3 stays fenced as objectui's.",
      "branch": "claude/issue-17344-stageorder-adr-0049-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17616",
      "pr_number": 17616,
      "head_sha": "2d171492efcfb7d33f59475fede039aa525f2c4f",
      "premise_still_valid": true,
      "summary": "Finding 1 is fixed: `options.stageOrder` is now refused at parse on every widget `type` except `funnel`. Every line of the seat's pre-measurement was re-derived on my own merge base (3ef96b471) and every one held, including the shape claim — the key is inside `DashboardWidgetOptionsSchema` and the deciding `type` is its sibling one level up on `DashboardWidgetSchema`, so a per-field refinement cannot see it. I did NOT invent an idiom: `dashboard.zod.ts` already attaches a named, exported `(value, ctx)` check to `GlobalFilterSchema` with `.superRefine(…)` by identifier, and `checkDashboardWidgetStageOrder` follows it exactly — object-level check on the widget, refusing at path `options.stageOrder`. The refusal names the key, the authored type (interpolated, pinned to prove it is not a fixed string) and the one type that honours it, plus `sortBy`/`sortOrder` as where ordering lives for the rest. Clause-② yes; `needs:contract-review` hangs on both carriers, read back on both. Changeset is `minor` with a BREAKING banner and an ADR-0087 `registered` disposition against a new protocol-18 semantic entry — level measured from the repo's written convention (the `Check Changeset` step's WHICH LEVEL prose plus `check-changeset-no-major`'s launch-window header), not reasoned from the gate's behaviour. ⛔ The card's other arm (ordered marks honour it) was NOT taken: it is an objectui renderer change. Finding 3 was NOT touched.",
      "the_gate": {
        "where": "packages/spec/src/ui/dashboard.zod.ts — `checkDashboardWidgetStageOrder` declared once, attached once as `.superRefine(checkDashboardWidgetStageOrder)` on `DashboardWidgetSchema`, exported on the `@objectstack/spec/ui` barrel.",
        "why_object_level": "MEASURED, not assumed. On my merge base the key sits at dashboard.zod.ts:277 inside `DashboardWidgetOptionsSchema` (which is `.passthrough()` — an open bag that cannot refuse anything by being strict) and `type: ChartTypeSchema.default('metric')` at :379 is a sibling of `options` one level up on the widget. A refinement attached to `stageOrder` sees the array and nothing else. The existing idiom in the same file is `.superRefine(checkGlobalFilterDateDefaultValue)` on `GlobalFilterSchema` (declared at ~758, attached at ~907) — one exported function per refinement, attached by identifier so a `.shape` mirror re-attaches the rule rather than a copy. I looked for that before writing anything and used it.",
        "message": "`options.stageOrder` is authored on a widget of `type: 'horizontal-bar'`, and `type: 'funnel'` is the only widget type that reads it — on every other type the key parses, is forwarded to the renderer, and no branch consults it, so the order you wrote is silently absent from what renders. Either write `type: 'funnel'`, or delete `stageOrder` and order this widget with `options.sortBy` + `options.sortOrder`, which lower into the dataset query itself instead of re-sorting what it returned."
      },
      "behaviour_both_directions": {
        "method": "`safeParse` on authored widgets — ⛔ never a reading of the schema source or its `.describe()` prose.",
        "before": "`pnpm --filter @objectstack/spec exec vitest run src/ui/dashboard.test.ts` on the unmodified merge base: exit 0, 70 passed — INCLUDING #17474's pin `CONTROL — the key is still UNGATED: a non-funnel widget carrying it parses too`, which parses a `horizontal-bar` widget carrying `stageOrder` and asserts the array round-trips. That pin is the before-reading and it is the pin this PR flips.",
        "after": "exit 0, 83 passed. horizontal-bar + stageOrder REFUSED with one `custom` issue at `options.stageOrder`; funnel + stageOrder PARSES with the array intact; horizontal-bar with the other four `options` members and no `stageOrder` PARSES; horizontal-bar with no `options` at all PARSES. Also pinned: `pie` produces a message naming `pie` and not `horizontal-bar` (so the assertion cannot be met by a hard-coded string), and the refusal travels through `DashboardSchema.widgets[]` at path `widgets.0.options.stageOrder`."
      },
      "gate_does_not_cover": [
        "A `type` outside `ChartTypeSchema`: MEASURED — zod treats `invalid_value` as aborting and skips object-level checks for that input, so `type: 'ziggurat'` + `stageOrder` reports the type refusal ALONE. The two refusals are never seen together; the author meets this one on the next parse. Pinned so a zod upgrade cannot change it silently.",
        "A widget that declares NO `type`: `type` carries `.default('metric')` and zod applies defaults BEFORE object-level checks, so an omitted `type` is indistinguishable from an authored `metric`. The verdict is right either way, but the message cannot claim the author wrote `metric` — that one case carries an extra sentence pointing at the missing `type`. Both spellings pinned.",
        "The array's CONTENTS: still unconstrained string, number or boolean members, unmatched against the dimension's picklist. A `funnel` with a misspelled stage parses and renders it in the sentinel position. Pinned as a fact rather than left implied."
      ],
      "sibling_sweep": "ASKED AND ANSWERED: `stageOrder` is the only member of that bag whose effect is confined to one branch, so nothing else is in this class and ⛔ nothing else was touched. Read at the pin `53ded82bf7a494f54e344e19099dbf00854b8694`: `dateGranularity` (DatasetWidget.tsx:443), `sortBy` (:444, into `order` at :450), `sortOrder` (:450) and `limit` (:452) are all read at the TOP of the component, outside every type branch — the only `widgetType ===` reads in that span are `isTable`/`isMatrix` at 427-428, which do not enclose them — and they lower into the DatasetSelection at 572-574. `stageOrder` (:1468) is forwarded as `categoryOrder` for any widget (:1529) and consumed at AdvancedChartImpl.tsx:1514, inside `if (chartType === 'funnel')` opened at 1473. Counts by `grep -o | wc -l`, not `grep -c`: `categoryOrder` in AdvancedChartImpl = 3 (declaration 247, destructure 850, the one read 1514); DARK control `stageOrder` in that same file = 1, a comment.",
      "tests": "TARGETED — `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/dashboard.test.ts` under scripts/pm/os-verify-lock.sh: VERDICT command-exit 0, 83 passed (70 before the change). SUITE — `pnpm --filter @objectstack/spec test` under the lock: VERDICT command-exit 0, 'Test Files 473 passed (473) / Tests 13439 passed (13439)'. TYPECHECK — `pnpm --filter @objectstack/spec typecheck` under the lock: VERDICT command-exit 0, check:test-typecheck printing 'OK -- 54 file(s) / 259 error(s) / 144 pinned signature(s) held'. ABLATION — run from the COMMITTED state (HEAD 2d171492ef), one leg, trap on absolute paths. Removed the `.superRefine(checkDashboardWidgetStageOrder)` attachment and proved the mutation reached DISK before reading any verdict: attachment occurrences by `grep -o | wc -l` 1 BEFORE and 0 AFTER, injected marker 1, and `git hash-object` moved 4d06e536943f68585297a07a614061a67eb0c58a (the HEAD blob) -> f20de6c957bb482f0ed5171059ae4f230d290ae6. Ablated run: exit 1, 'Tests 8 failed | 75 passed (83)' — and the 8 are exactly the gate pins (the GATED pin, the path pin, the message pin, the not-a-fixed-string pin, both defaulted-type pins, the attached-by-identifier pin, and the DashboardSchema.widgets[] pin) while every CONTROL pin stayed green, so the controls are discriminators rather than legs that pass either way. RESTORED with `git checkout HEAD -- packages/spec/src/ui/dashboard.zod.ts` (⛔ never the bare form) and proven BY STATE, not by an exit code: `git hash-object` back to 4d06e536943f68585297a07a614061a67eb0c58a, `git diff HEAD` EMPTY, marker occurrences 0, attachment occurrences back to 1. NO dist preflight was needed or claimed and here is the reading behind that: dashboard.test.ts imports './dashboard.zod' — a RELATIVE specifier inside the same package — so no `dist/` is on the resolution path and the alias machinery (which keys on bare `@objectstack/*` specifiers) never sees it. LINT — no narrowing, the full union: `pnpm exec eslint . --no-inline-config --format json` over the WHOLE repository at `git rev-parse --short HEAD` = 2d171492ef with a clean tree, exit 0, 6608 files linted (count read from eslint's own JSON output), 0 errors, 0 warnings.",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with NO path arguments so the script derived the change set itself from the merge base. Header confirmed the repo assertion holds against this checkout's origin. Derived twice — 6 paths on the first pass and 8 on the final tree — and the COMMAND SET was byte-identical both times (109).",
        "reconcile_verdict": "'109 derived, 108 run, 1 NOT-MEASURED, 0 UNRUN' — exit 0, via --ran on a record carrying 'COMMAND :: exit CODE' for all 109.",
        "green": 108,
        "not_measured_not_red": [
          "pnpm check:dual-build-cjs-loads :: exit 3 — the gate's own 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/' naming 33 unbuilt packages (account, setup, studio, cloud-connection, the four connectors and 25 more), none in this diff's closure. Its own last line: '⛔ This is NOT a pass: nothing was measured.'"
        ],
        "red": [],
        "first_pass_reds_that_were_NOT_drift": "Five gates exited 1 on the first sweep and every one was a real, actionable fact rather than a refusal misread as a pass — judged by substance, as instructed. (1) check:api-surface + check:export-origins + check:generated: my one new EXPORT, `checkDashboardWidgetStageOrder`, widened the public surface. Regenerated both snapshots; the diff is 2 added lines and '0 breaking (removed/narrowed)'. ⚠️ gen:api-surface REFUSED the first regeneration attempt, exit 1, because packages/spec/dist described a src that had moved under it ('recorded 652d65d5… · src now hashes to 3777cac0…') — that is the build-before-you-judge rule firing, so I rebuilt packages/spec and re-ran. (2) check:objectui-pin-citations: the pinned objectui sha in my migration entry was not in backticks, so it was a citation the gate could not find. Fixed at the entry and regenerated the registry. (3) check:skill-examples: a PREREQUISITE refusal arriving as exit 1, exactly the shape the dispatch warned about — its own text says 'packages/client-react/dist holds no .d.ts declarations — the package is not built'. Built @objectstack/client-react and re-ran: green. ⛔ Not reported as a red and ⛔ not reported as a finding. On the final sweep all five are green; `check:lean-entry-closure` and `check:type-check-debt` also converted from exit 3 to exit 0 once more packages were built.",
        "notable_named_gates": "check:widget-option-census exit 0 (no declared key added or removed, so the sdui-parser census is unmoved). check:authorable-surface exit 0 with authorable-surface.base.json untouched — no ordinary build wrote it. check:doc-authoring exit 0 (the refusal message carries no bare issue id, the shape it refuses). check:migration-registry exit 0. check:spec-changes and check:upgrade-guide exit 0 with BOTH artifacts untouched — see the fences note. check:nul-bytes exit 0, plus a manual `grep -naP` for control bytes over all 8 touched files: exit 123, no matches.",
        "forced_regeneration_path": "Editing the `.describe()` forced gen:schema then gen:docs, in that order; it moved exactly 2 lines of content/docs/references/ui/dashboard.mdx and nothing else. ⛔ Never run in a MERGE state: no merge was performed on this branch at all (no .git/MERGE_HEAD, and origin/main was never merged in), so the `merge=os-regen` re-derivation clause did not arise."
        },
      "fences_honoured": "git status over every fenced path is EMPTY: packages/spec/src/conversions/registry.ts, docs/protocol-upgrade-guide.md, packages/spec/spec-changes.json (#15437 / PR #17604), packages/spec/src/ui/action-params.zod.ts, examples/app-todo/** (#15117), content/docs/releases/**, and every CHANGELOG.md. ⭐ The two ADR-0087 projections are the interesting ones, and they are untouched by MEASUREMENT rather than by luck: spec-changes.json carries protocolVersion 17.0.0 and aggregates majors 10..17, so a protocol-18 semantic entry projects into neither artifact. LIT CONTROL — a protocol-17 semantic id, `aggregation-node-distinct-retired`, appears 2 times in spec-changes.json and 1 time in docs/protocol-upgrade-guide.md; DARK — my new id and a known-18 sibling id both appear 0 times in both. The precedent commit f8e5790593 (the grouping-field narrowing that landed last night) touched the same 6 file kinds and likewise neither projection.",
      "clause_2_reading": "Clause-②: yes, and it is the right reading: metadata that parses today is refused after this. `needs:contract-review` hung on BOTH carriers by additive REST POST (⛔ not a whole-group replace) and READ BACK on both — #17344 now reads 'domain:spec, needs:contract-review, pm:dispatched, priority:p2' and PR #17616 reads 'needs:contract-review, size/l'. Nothing was stripped: `size/l` was applied concurrently by the size-labeler and survived, which an integral PUT would have erased. The PR body's declaration line was run through `readClause2Line` (the exact function check-changeset-no-major imports) BEFORE posting and again after the read-back: {kind: 'declared', value: 'yes', line: 'Clause-②: yes'}. CI's `Check Changeset` is green on the head, which is the gate-side confirmation the line is legible. ⚠️ `node scripts/pm/check-clause2-carriers.mjs --pair 17616` exits 4 — see open_questions, it is a stale-declaration READ about the card, not about the PR body, and C5 is report-only (it writes no label).",
      "ci": {
        "read_at": "once, after opening the draft PR — ⛔ no polling, no sleeping, no waiting",
        "head_sha": "2d171492efcfb7d33f59475fede039aa525f2c4f",
        "newest_run_per_check_NAME": {
          "distinct_names": 34,
          "failed": [],
          "in_progress": [],
          "skipped": ["Check PR Size", "Console Pin Gate", "Packed-tarball smoke (opt-in)"],
          "success": ["Auto Label", "Build Core", "Build Docs", "Check Changeset", "Check Documentation Links", "Dogfood Regression Gate", "Dogfood Regression Gate (1/3)", "Dogfood Regression Gate (2/3)", "Dogfood Regression Gate (3/3)", "Dogfood Verify CLI", "Flag docs affected by code changes", "Governed Surface Queue Guard", "Lint & Repo Gates", "No other open PR may claim the same issue", "No other open PR may claim the same single-writer path", "Part-of PR must not also close its card", "Spec property liveness", "Temporal Conformance (live PG + MySQL)", "Test Core", "Test Core (1/6)", "Test Core (2/6)", "Test Core (3/6)", "Test Core (4/6)", "Test Core (5/6)", "Test Core (6/6)", "Type Check · consumer gates", "Type Check · debt ledger", "Type Check · source gates", "Type Check · workspace", "TypeScript Type Check", "filter"]
        },
        "combined_commit_status": "success — 1 context, Vercel=success. This is the commit-status API, separate from the check-runs above.",
        "note": "'Part-of PR must not also close its card' is green, which is the check that would catch a closing keyword. CI convergence is the seat's to read, not mine. ⛔ The PR is a draft; I did not flip it ready, enqueue it, or arm auto-merge."
      },
      "premise_falsification": "Every line of the seat's 03:17Z pre-measurement re-derived on merge base 3ef96b471 and every one HELD — so nothing here rests on deference. dashboard.zod.ts:277 is the ungated key with exactly the quoted declaration; :280 and :272 carry #17474's landed prose; :379 is `type: ChartTypeSchema.default('metric')`; LIT CONTROL 'stageOrder' in that file = 1 at the time of the reading. ⭐ One thing the pre-measurement did not say and that turned out to decide the design: `DashboardWidgetOptionsSchema` is `.passthrough()`, so strictness was never available as a route — an open bag cannot refuse a key by being closed, which is exactly why the gate has to be a cross-field check rather than a shape change. ⭐ And one correction to my own first draft, caught by behaviour rather than by reading: I had assumed a bad `type` would produce BOTH refusals; the probe showed zod aborts on it and reports only the type. The docblock and the pin now state the measured behaviour rather than the assumed one.",
      "mcp_calls": "2 — two targeted mcp__github__search_issues calls, both dedup searches before deciding whether to file. Declared channel switch: /search/* is refused by the egress proxy by design and has no REST equivalent, and a label-guessed REST listing would have been an unverified dark read. Everything else went through repo-scoped REST, probed green at the start of the run (GET /repos/objectstack-ai/objectstack -> 200): the card body and all 7 comments, the draft PR creation, the PR body read-back, the label writes and their read-backs, the new card, its read-back, check-runs and the combined status.",
      "open_questions": [
        {
          "question": "`check-clause2-carriers --pair 17616` exits 4 on C5, reporting that card #17344 'declares `Clause-②: no`'. It does not — the governing claim (comment 5628909337, 2026-09-11T03:15Z) declares `yes`. MEASURED cause: `governingClaim` selects the most recent claim comment THAT NAMES A BRANCH, and `claimedBranches` only reads a line beginning `Branch:` / `Branches:`. Both claim comments on this card write the branch INLINE on the `Claim:` line, so `claimedBranches` returns [] for both, `governingClaim` returns null, and `cardDeclaration` falls back to scanning every claim comment in THREAD ORDER — returning the FIRST declaration it finds, which is the spent 15:21Z `Clause-②: no` from the finding-2 round. That is precisely the stale-premise failure `governingClaim`'s own docblock says it exists to prevent. ⛔ I did not touch it: it is PM tooling, outside this card's face, and this is a fourth live instance of an already-open card rather than a new defect. Who amends the claim comment so the card reads `yes`? The seat owns it and I am forbidden to write it.",
          "options": [
            "A. The seat amends its claim comment to carry a `Branch: claude/issue-17344-stageorder-adr-0049-gate` line on its own line — one edit, makes `governingClaim` select the newest claim, and C5 clears on the next --pair",
            "B. Leave it and let the reviewer read the exit-4 text against this report — C5 is report-only and writes no label, and CI's `Check Changeset` (which reads the PR BODY, where the declaration is unambiguous) is already green",
            "C. Fix `claimedBranches` to also read the inline spelling — the durable repair, but it is #17149's subject and not mine"
          ],
          "recommendation": "A now, C eventually. A is one edit by the seat that owns the comment and it makes the board's own reading true; B leaves a red exit that a later reader has to re-derive. C is the real fix but it belongs to #17149, which is open and already names this class."
        },
        {
          "question": "Attribution shape on the PR body — the same conflict the previous round on this card flagged, and I resolved it the same way and re-measured rather than inheriting. I put the session URL in closing body PROSE and hand-wrote no footer. MEASURED on this exact channel (raw REST) and action (create): the PLATFORM appended `_Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_` itself, so the stored body carries EXACTLY ONE footer and my sent body is a byte-exact prefix of it (8159 sent, 8249 stored, startsWith true). Should a dev hand-write the footer on this channel, or keep relying on the platform?",
          "options": [
            "A. Keep prose-only on raw-REST create and let the platform append — measured twice now on this pair, one footer both times",
            "B. Hand-write it per the standing clause — risks two footers on this channel/action pair"
          ],
          "recommendation": "A, on the same grounds as last round: it is what was measured on this exact channel and action, and the platform-readings discipline forbids generalising one cell of that matrix to another anyway."
        }
      ],
      "out_of_scope_findings": [
        "filed as #17618: `check-clause2-carriers`' T1 widening tell reads a TypeScript function PARAMETER annotated with a zod type as 'a new key on a Zod object schema'. Measured on this very PR: it reports `packages/spec/src/ui/dashboard.zod.ts:434`, the line `ctx: z.RefinementCtx,` in the signature of the refinement function, as an authorable key the accept set GAINS — on a diff whose entire content is a narrowing. Systematic rather than incidental: `(value, ctx: z.RefinementCtx)` is this repo's own prescribed signature for an exported object-level check, so every PR that adds a cross-field REFUSAL will raise a widening tell for the parameter of the function doing the refusing. Deduped first against a targeted search: #16943 is the same class on T2 and is CLOSED; #17098 and #17346 are other reading defects in the same script and neither covers this. Carrier: the card itself, which is unassigned and in the dispatch pool.",
        "noted, not filed: the C5 stale-declaration read described in open_questions — `claimedBranches` returning [] for a claim that writes its branch inline, so `governingClaim` falls back to thread order and reads a spent declaration. Carrier: #17149, OPEN, whose title is exactly this class ('A `Claim:` comment that yields ZERO parsed branches silently stops governing instead of saying so — #16170's class recurring in a third spelling, now measured on three live cards'). This is a fourth live card for it, and reporting it into that thread is worth more than a duplicate.",
        "noted, not filed: three hand-written sites document `options.stageOrder` for a `pyramid` type and are now ALSO stale about the gate — skills/objectstack-ui/rules/dashboards.md:345, content/docs/ui/dashboards.mdx:121, packages/sdui-parser/src/dashboard-widget-options.ts:51. Carrier: #17471, the sub-issue filed off this card by the previous round, which already has draft PR #17538. ⛔ Not widened into this PR: the skills path sits under a published-skill line ratchet whose budget is the seat's to grant, and mixing a governed-surface path into a PR carrying a narrowing is the thing the previous round declined to do for the same reason.",
        "noted, not filed: the `Dogfood Regression Gate` and `Test Core` check names appear BOTH bare and sharded on this head (`Test Core` and `Test Core (1/6)`..`(6/6)`; `Dogfood Regression Gate` and `(1/3)`..`(3/3)`), 38 check_runs across 34 distinct names. All green here so nothing is hidden, but a required-context set matched by NAME cares about that duplication. Carrier: none identified — I could not name a PR or a person who will touch this, so I am recording it as an observation rather than claiming it has a home."
      ]
    }
    

    Generated by Claude Code

  13. os-bill commented on Sep 11, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 17344,
      "status": "done",
      "round": "contract-review rework — PASS WITH FINDINGS, one must-fix (prose)",
      "scope": "finding 1 only. Finding 2 landed in PR #17474; finding 3 stays fenced as objectui's.",
      "branch": "claude/issue-17344-stageorder-adr-0049-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17616",
      "pr_number": 17616,
      "head_sha": "47599ccc66d6a19a3dfe39f1d57056ac6b1093c0",
      "previous_head_sha": "2d171492efcfb7d33f59475fede039aa525f2c4f",
      "premise_still_valid": true,
      "summary": "The must-fix is addressed, and I reproduced the falsification myself rather than taking it on the review's word: `z.strictObject(DashboardWidgetSchema.shape)` ACCEPTS the horizontal-bar + stageOrder widget and holds ZERO object-level checks, while `.extend({})` keeps the refusal — with a lit control (`type: 'ziggurat'`) refused by BOTH, so the mirror carries the fields and it is precisely the check that is dropped. My claim that identifier attachment means a `.shape` mirror re-attaches the rule was false as a mechanism; it only makes re-attachment possible. The fourth non-coverage is now named at all three sites that presented the list as complete, plus a fifth I found while probing. The declared increment was TAKEN — and taking it turned the catalogue's parity leg RED, which I repaired at the producer rather than by dropping the fixture. Carrier for the objectui half named in the PR body: objectui#9111.",
      "must_fix": {
        "what": "Name objectui's `.shape` mirror as a fourth non-coverage at the three sites that presented the list as complete, and stop the migration entry's `acceptanceCriteria` overstating the door coverage.",
        "reproduced_myself": "`z.strictObject(DashboardWidgetSchema.shape)` => ACCEPTS, 0 object-level checks. `z.object(.shape)` => ACCEPTS. `.extend({})` => REFUSES. The door itself => REFUSES. LIT CONTROL `type: 'ziggurat'` => REFUSED by the door AND by the `.shape` mirror, so the mirror does carry the fields and the ACCEPTS is specifically the missing check, not a broken probe.",
        "objectui_consequence_re_measured": "At the `.objectui-sha` pin 53ded82bf7a494f54e344e19099dbf00854b8694: `packages/types/src/zod/complex.zod.ts:627` builds its `DashboardWidgetSchema` from `specFieldsExcept(SpecDashboardWidgetSchema.shape, ['id','type']).extend({…}).strict()`. Re-attachments of the five exported spec checks in `packages/types/src`: 0, 0, 0, 0, 0. LIT CONTROLS: the mirror line itself present (1), and `specFieldsExcept` used 17 times in that package — so the zeroes are readings. One thing I measured that the review did not name: that mirror DROPS `type` from the spread and redeclares it `DashboardWidgetTypeSchema.optional()` with NO default (the file uses `.default(` exactly once, and not there).",
        "sites_proven_by_state_on_the_PUSHED_ref": "Read back with `git show origin/claude/issue-17344-stageorder-adr-0049-gate:PATH` at 47599ccc66, counted with `grep -o | wc -l` (⛔ never `grep -c`, which counts LINES). SITE 1 packages/spec/src/ui/dashboard.zod.ts — PROBE \"objectui's CLIENT-SIDE authoring door\" 1, PROBE 'Five shapes, named so the gate' 1, STALE 'Three shapes, named so the gate' 0, STALE 're-attaches the rule rather than a copy' 0, LIT 'checkDashboardWidgetStageOrder' 5. SITE 2 .changeset/dashboard-stageorder-gated-to-funnel.md — PROBE 'client-side authoring door' 1, PROBE 'the **publish** door' 1, PROBE 'checkDashboardWidgetStageOrder' 1, PROBE '.omit()' 1, STALE 'Three shapes' 0, DARK 'ziggurat-door' (fabricated) 0, LIT 'BREAKING' 1. SITE 3 the migration entry — PROBE 'WHICH DOOR' 1, STALE 'refused on its next authoring-path save' 0, STALE 'Two shapes this does NOT reach' 0, PROBE 'Three more shapes this does NOT reach' 1, LIT 'acceptanceCriteria' 1. SITE 3b the GENERATED registry — PROBE 'WHICH DOOR' 1, LIT the entry id 1, so `gen:migration-registry` really carried the edit through. ⚠️ Honest note on my own method: my first pass at SITE 2 read 0 because I probed the uppercase 'PUBLISH door' while the changeset spells it '**publish**' — the lowercase-probe-misses-a-capitalised-sentence trap, in reverse. I re-probed with the spelling actually used rather than recording the 0.",
        "gen_and_check": "`pnpm --filter @objectstack/spec gen:migration-registry` -> '✓ wrote src/migrations/registry.ts (201 semantic, 165 retired-key, 178 retired-def)'; `check:migration-registry` -> exit 0, '✓ src/migrations/registry.ts is current'."
      },
      "fifth_non_coverage_found_while_probing": "zod 4 THROWS on `.omit()` / `.pick()` / `.partial()` of an object carrying a refinement — probed all three on this schema, all three throw ('.omit() cannot be used on object schemas containing refinements'). So this change converts those three from working to throwing for any consumer that derives the widget schema that way. Latent rather than live (the review measured 0 such consumers in either repo, lit control 6 `.shape` sites in objectui), and `.extend()` is unaffected. Named alongside the fourth at all three sites rather than left for the next reader, on the same principle the must-fix states.",
      "declared_increment": {
        "taken": true,
        "why_it_qualified": "The catalogue's population predicate is 'every mirrored spec object that carries an object-level check'. Before this PR `DashboardWidgetSchema` was mirrored but carried no check, so it was correctly absent; it now carries one, and objectui's mirror of it is exactly what non-coverage 4 measures. ⇒ leaving it out would have made the file's own count-and-bijection claim false over its stated population. It IS a per-export catalogue, so the escape did not apply.",
        "what_it_caught": "⭐ Adding it turned leg 1 RED — 2 failures, both on the omitted-`type` fixture: 'the direct call refuses at exactly the declared paths' and 'the schema parse and the direct call agree issue for issue'. Cause, measured: the catalogue calls the export DIRECTLY with the RAW fixture, so `widget.type` is `undefined`, while the door applies `type`'s `.default('metric')` BEFORE object-level checks. The export was refusing strictly LESS than the door it is exported from — which is the one thing an exported check may not do, and precisely the drift this catalogue exists to catch.",
        "repair": "At the producer, ⛔ not by dropping the fixture: the check now reads `widget.type ?? WIDGET_TYPE_DEFAULT`. Both suites then run 193/193 green.",
        "accept_set_unmoved_measured": "`parse({ …widget })` -> `type: 'metric'` (typeof string) and `parse({ …widget, type: undefined })` -> `type: 'metric'` (typeof string). `type` is therefore never `undefined` when object-level checks run at this door, so the coalesce is unreachable here and no verdict can change. The four behavioural rows re-measured identical at the new head: horizontal-bar+stageOrder REFUSED, funnel+stageOrder PARSES, horizontal-bar without stageOrder PARSES, horizontal-bar with no options PARSES. ⛔ The review's 768-fixture differential corpus was NOT re-run, as instructed, and needed no re-run. Where the coalesce DOES act is the objectui mirror, whose `type` has no default — so re-attaching there is now sufficient, and the docblock/changeset say so instead of the earlier 'necessary but not sufficient'.",
        "legs_3_and_4": "Left where they are: the catalogue's leg-4 `it.each` keeps its four rows and now carries a comment saying it is not an enumeration of every exported refinement, because this export's barrel-identity and attached-by-identifier legs live beside the schema they guard in dashboard.test.ts."
      },
      "tests": "SUITES — `pnpm --filter @objectstack/spec build && test && typecheck` chained under scripts/pm/os-verify-lock.sh: VERDICT command-exit 0, 'Test Files 473 passed (473) / Tests 13454 passed (13454)' (13439 before the rework), typecheck clean with check:test-typecheck 'OK -- 54 file(s) / 259 error(s) / 144 pinned signature(s) held'. TARGETED at the final head, under the lock: dashboard.test.ts + object-refinement-check-exports.test.ts -> exit 0, 193 passed (193). ABLATION re-run at the reworked head, from the COMMITTED state, trap on absolute paths: attachment occurrences by `grep -o | wc -l` 1 BEFORE / 0 AFTER, injected marker 1, `git hash-object` moved e85e7f6f83c0816506f15cefa16fc5b1156688bd (the HEAD blob) -> 8914e39ece5be084516efaaf38d16c1da5054115; ablated run exit 1, 'Tests 8 failed | 75 passed (83)', the 8 being exactly the gate pins while every CONTROL pin stayed green; RESTORED with `git checkout HEAD -- PATH` and proven BY STATE — hash back to e85e7f6f83, `git diff HEAD` EMPTY, marker 0, attachment 1. LINT — full union, no narrowing: `pnpm exec eslint . --no-inline-config --format json` over the whole repo at `git rev-parse --short HEAD` = 47599ccc66 with a clean tree, exit 0, 6608 files linted (count from eslint's own JSON), 0 errors, 0 warnings.",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no path arguments. 109 families, the same set as both earlier sweeps.",
        "reconcile_verdict": "'109 derived, 108 run, 1 NOT-MEASURED, 0 UNRUN' — exit 0, via --ran, on a full sweep of the FINAL tree.",
        "green": 108,
        "not_measured_not_red": [
          "pnpm check:dual-build-cjs-loads :: exit 3 — 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/', naming 33 unbuilt packages, none in this diff's closure. Building those is a whole-farm run CI owns. Its own last line: 'This is NOT a pass: nothing was measured.'"
        ],
        "red": [],
        "a_red_I_introduced_and_fixed": "⚠️ Reporting this against myself. My first rework push spelled the new objectui pin citations as 'the pinned `.objectui-sha` BACKTICKED-SHA', and `check:objectui-pin-citations` refused all three (exit 1, 'names a sha in neither recognised citation spelling'). The gate admits exactly two forms and they mean different things: ``.objectui-sha` = BACKTICKED-SHA` asserts THIS is the pin we build against (checked), ``.objectui-sha` pin BACKTICKED-SHA` records a measurement taken AT that pin (historical, not checked). Mine are historical, so all three moved to the `pin` form and the registry was regenerated. Gate now: exit 0, '12 asserting objectui pin citation(s) match .objectui-sha (53ded82bf), 23 historical citation(s) recorded and not checked, across 1385 spec source(s)'.",
        "prerequisite_refusals_resolved_by_building": "Four families first read exit 3 / exit 1 in a freshly re-created worktree purely because their inputs were unbuilt — check:doc-formula-expressions, check:doc-security-posture (both @objectstack/lint), check:skill-examples (@objectstack/client-react `dist` .d.ts), check:docs-transcript-drift. Built those closures under the lock and re-ran: all four exit 0. ⛔ None was reported as a red and ⛔ none as a finding. check:api-surface and check:generated likewise refused twice with their own 'the package is not built' / 'dist describes DIFFERENT sources' text after src moved under dist; rebuilt and re-ran, and the final reading is '@objectstack/spec public API surface + factory signatures unchanged ✓' and 'All 15 generated artifacts are up to date' — so the comment-only rework moved no public surface, and the one added export recorded earlier still stands alone."
      },
      "carrier_state": {
        "labels": "OFF both carriers, as the rule requires of a returned verdict — #17344 now reads 'domain:spec, pm:dispatched, priority:p2' and PR #17616 reads 'documentation, protocol:ui, size/l, tests, tooling'. ⛔ I did not re-hang anything.",
        "pair_predicate": "`node scripts/pm/check-clause2-carriers.mjs --pair 17616` exits 4 on C3, exactly as forecast: 'the gate WAS bound and cleared (last removal 2026-09-11T04:53:03Z), but the PR's head has MOVED since… The review that cleared this gate judged a different tree'. It states in its own text that the re-hang is a seat's act, not this script's, and ⛔ not mine.",
        "pr_state": "draft: true, state: open, auto_merge: null, head 47599ccc66. ⛔ Not flipped ready, ⛔ not enqueued, ⛔ no auto-merge. Body opens `Part of #17344`, no closing keyword (check-closing-keyword-parity exit 0; CI's 'Part-of PR must not also close its card' green).",
        "claim_and_assignee": "⛔ No second `Claim:`, ⛔ assignee never written."
      },
      "ci": {
        "read_at": "once, after the final push — ⛔ no polling, no waiting",
        "head_sha": "47599ccc66d6a19a3dfe39f1d57056ac6b1093c0",
        "newest_run_per_check_NAME": {
          "distinct_names": 34,
          "failed": [],
          "in_progress": [],
          "skipped": ["Console Pin Gate", "Packed-tarball smoke (opt-in)"],
          "success_count": 32
        },
        "combined_commit_status": "pending — 1 context, Vercel=pending. Recorded as it read; ⛔ not waited on. The check-runs API above is the separate and complete reading: 0 failures across 34 names.",
        "note": "'Check Changeset' green, so the `Clause-②: yes` line is still legible after the body edit."
      },
      "open_questions": [
        {
          "question": "RESOLVED from last round, recorded so the thread closes: the seat amended its claim comment, and the stale-declaration read is gone. Measured now — `claimedBranches(5628909337)` returns ['claude/issue-17344-stageorder-adr-0049-gate'] where it returned [] before, `governingClaim` resolves to that comment, and `cardDeclaration` reads {state: 'declared', value: 'yes'}. The C5 row that fired last round is gone; the exit 4 now is C3, which is the head-moved state and is the seat's to clear. ⛔ Nothing owed from me.",
          "options": [],
          "recommendation": "No action. The durable half is still #17149's (a claim whose branch is written inline parses to zero branches and silently stops governing)."
        },
        {
          "question": "Attribution footer form varies by ACTION on the same channel, which is a new cell of the matrix rather than a repeat of last round's. Measured on raw REST: CREATE appended the SESSION-URL form `_Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_`; the PATCH of the same body appended the BARE form `_Generated by [Claude Code](https://claude.ai/code)_`. Exactly one footer both times, no doubling, and I hand-wrote none. Does the seat want the durable session attribution preserved across body edits?",
          "options": [
            "A. Leave it — the session id is also in the body PROSE (a closing sentence), which is where AGENTS.md says durable attribution lives, so a PATCH that swaps the footer form loses nothing",
            "B. Hand-write the session-URL footer on PATCH — but that is untested on this cell and risks two footers"
          ],
          "recommendation": "A. The prose sentence survives every edit, and platform-readings forbids generalising one cell of the channel-by-action matrix to another anyway — which this measurement just demonstrated in the direction of caution."
        }
      ],
      "out_of_scope_findings": [
        "carried in the PR body, filed by the seat as objectui#9111: objectui's client-side `DashboardWidgetSchema` is a `.shape` mirror and will not carry this check until it imports and chains `checkDashboardWidgetStageOrder`. ⛔ Not mine to fix. One measurement to hand whoever takes it, which the card may not have: that mirror drops `type` from the spread and redeclares it `DashboardWidgetTypeSchema.optional()` with NO default, so a typeless widget arrives at a re-attached check as `undefined` rather than `metric` — this round made the exported check default `type` itself for exactly that caller, so re-attaching is now sufficient and no objectui-side default is needed.",
        "filed as #17618 last round, still open and unchanged by this rework: `check-clause2-carriers` T1 reads a function parameter annotated `ctx: z.RefinementCtx` as a new authorable key, so every PR adding a cross-field refusal raises a widening tell for the refusal itself.",
        "noted, not filed: the review's own nit at the refusal message — 'on every other type the key parses' in the present tense while the author is reading a refusal of exactly that. ⛔ Deliberately not folded in: it would move a published message string on a change the review graded prose-only, the seat scoped it as fold-in-only-if-already-editing-that-line, and I was not otherwise editing it. Carrier: the seat, which raised it.",
        "noted, not filed: the review recorded that a `sortBy` naming nothing in `dimensions`/`values` is silently dropped at `DatasetWidget.tsx:448` and is caught at author time by the lint rule `widget-sortby-unselected` rather than by parse — so the redirect this refusal issues is true but lands on a key with its own author-time caveat one layer over. Carrier: #14148, closed, which is where that rule landed; recording it here rather than re-filing.",
        "noted, not filed: the review measured my 'zero authored widgets carry the key' census figure as non-reproducible (it reads 29 at base and 91 at head over tracked files, against my 59) — probably because my `git grep` ran over a tree carrying built `dist/`. The CONCLUSION is independently a reading and the review re-derived it: 6 authored dashboard files under `examples/` (lit control), none carrying the key. The figure is corrected here rather than left standing in the changeset, which does not quote it. Carrier: none needed — the claim it supported holds."
      ]
    }
    

    Generated by Claude Code

  14. os-bill commented on Sep 11, 2026

    @os-bill
    Collaborator

    Closing — all three findings discharged, finding 1 verified by CONTENT on main

    domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T07:50Z. PR #17616 used Part of, ⛔ not Fixes, so GitHub did not close this card — the seat decides it, and here is the reading it decided on.

    finding disposition verified
    1 — stageOrder declared for every chart type PR #17616, merged 2026-09-11T07:41:09Z by content on main, below
    2 PR #17474, merged 2026-09-10T17:21:53Z merge state read this turn
    3 objectui's, fenced — carrier objectui#9111 ⛔ not this repo's to close

    Finding 1 verified by state on origin/main, not by the merge flag

    ⭐ Whitespace flattened before matching, because a phrase that wraps a line break reads 0 on a contiguous grep — a mistyped anchor, ⛔ not an absence. That exact trap bit me on a landed docs page earlier today.

    1  "objectui's CLIENT-SIDE authoring door"          ← must-fix prose, present
    1  "Five shapes"                                     ← must-fix prose, present
    2  "?? WIDGET_TYPE_DEFAULT"                          ← the reviewed coalesce, present
    5  "checkDashboardWidgetStageOrder"                  ← LIT CONTROL, certainly present
    0  "Three shapes, named so the gate"                 ← stale text, gone
    0  "re-attaches the rule rather than a copy"         ← the FALSE mechanism, gone
    

    Both zeros sit beside a control that fires at 5, so they are readings rather than a grep that failed to run.

    What the review actually bought

    This card took two reviews and two reworks, and the thing that justified the second was not prose. The declared increment was taken, and taking it turned the catalogue's parity leg red — the exported check refused strictly less than the door it was exported from, because the catalogue calls it with a raw fixture while the door applies .default('metric') first. The round repaired that at the producer with a ?? WIDGET_TYPE_DEFAULT coalesce.

    ⚠️ A repair that turns a red test green by widening what an export accepts would have been a real defect, so the seat pointed the re-review at exactly that. Measured: 176/176 corpus rows identical at the spec door old-head vs new-head — with a control showing 30 rows differing against the merge base, so the instrument could have read otherwise — and 0 widening rows / 24 narrowing rows on the raw call a re-attaching mirror would run. It only ever refuses more.

    Verdict adopted verbatim at #17616 (comment), tier-verified before adoption (85/85 claude-fable-5-1, lit control 70 assistant turns). ⛔ Self-report is not a reading.

    Cards left open by the review, ⛔ not blockers and ⛔ not closed here

    ⛔ pm:* and the assignee are stripped in the same write as the close. domain:spec and priority:p2 stay:归属 is not state.


    Generated by Claude Code

  15. removed their assignment
    on Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions