Skip to content

spec(liveness): 9 rows of the README state table have count columns that disagree with the gate's --json report #7377

Description

@os-zhuang

Found while implementing #7257 (PR #7374). Not claiming or fixing here — filing per Prime Directive #10. Unassigned: nobody is working on this.

Finding

packages/spec/liveness/README.md's "Current state" table states its own counting method in the paragraph directly above it:

The counting method for this table is the gate's own report — check-liveness.mts --json, types.<type>.byStatus — decided in #4488 after two methods spent a release disagreeing. […] the count columns are never hand-edited — regenerate: […]

Run that documented snippet on merged main (f188ed6) and 9 of the 30 rows disagree with it:

Type README says (live / exp / dead / planned) --json says
field 59 / 0 / 0 / 0 66 / 0 / 0 / 0
flow 34 / 0 / 5 / 0 34 / 0 / 6 / 0
action 34 / 0 / 2 / 0 41 / 0 / 2 / 0
hook 11 / 0 / 2 / 0 18 / 0 / 2 / 0
page 16 / 0 / 0 / 1 23 / 0 / 0 / 1
view 79 / 0 / 4 / 0 80 / 0 / 6 / 0
webhook 11 / 0 / 0 / 0 19 / 0 / 0 / 0
app 45 / 0 / 9 / 0 46 / 0 / 9 / 1
seed 5 / 0 / 0 / 0 12 / 0 / 0 / 0

The drift is mostly one-directional (the gate now counts more than the table records — seed 5 → 12, webhook 11 → 19, hook 11 → 18), which is what growth in the walked shape looks like when nobody re-ran the snippet. But view and flow also moved in the dead column, and app grew a planned the row does not show — those are verdict-shaped, not just arithmetic.

Why it is a card and not a sed

PR #7374 gates the row set against GOVERNED — a governed type with no row, an orphan row, or a heading count that disagrees now fails CI. It deliberately does not check the count columns, and this issue is the reason that boundary was drawn rather than the two being landed together:

Suggested shape (not a ruling)

  1. Regenerate the 9 rows, and for each one read the Note beside it and reconcile the prose with the new numbers — this is the work, and it is per-row evidence review, not bookkeeping.
  2. Then extend readme-table.mts (added in PR feat(spec): gate the liveness README's state table against GOVERNED (#7257) #7374, already parsing every row) to compare the count cells against types.<type>.byStatus, or move the columns into a generated artifact per strictness 台账「数字/散文分家」:计数与表头转生成物走 os-regen,Class 判定与依据保持手写 —— 终结「干净合并两边都错」(单日 7 例) #5107's precedent.

Step 2 is cheap once step 1 is done; doing it first would just make CI red with no path to green.

Dedup

Searched the three repos before filing. #7257 covers the row set and the heading's completeness claim, and PR #7374 explicitly scopes the count columns out. No open issue or PR names the count drift.

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage: pm:queue (kept domain:spec).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Two more drifted rows incoming — from #7131 / PR #7425. Not claiming this card; recording the delta so the sweep does not have to rediscover it.

    PR #7425 executes the 2026-08-10 maintainer ruling that designer previews count as consumers, re-grading four docs-shaped rows dead → live. That moves two of your 30 rows off the gate's --json report:

    Type README says (live / exp / dead / planned) --json says after #7425
    job 13 / 0 / 2 / 0 15 / 0 / 0 / 0
    translation 17 / – / 2 / – 19 / – / 0 / –

    Measured, not projected: pnpm --filter @objectstack/spec check:liveness on the PR branch prints job 15 classified (live 15) and translation 19 classified (live 19).

    Both are the verdict-shaped kind you flag, not arithmetic — the dead column goes to zero on each, and both Notes cells enumerate the dead set by hand, so they need your step 1 (read the Note, reconcile the prose) rather than a regenerate:

    PR #7425 deliberately does not touch the state table, to stay out of this lane. Nothing is failing meanwhile: readme-table.mts checks the row set and the heading count, never the count columns.


    Generated by Claude Code


    Generated by Claude Code

  3. self-assigned this
    on Aug 10, 2026
  4. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    CLAIM — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2. Branch: claude/issue-7377-liveness-count-reconcile. Dispatching a cloud dev session (model: Opus — evidence-review discipline over 9 rows plus a #5107-precedent mechanism move).

    Serial gate met: PR #7374 (the gate + backfill this card was filed from) merged ~09:1xZ; the README and readme-table.mts have no in-flight writers.

    Direction (recorded at filing, from the #7374 dev's own boundary analysis): two stages in one PR, order fixed —

    1. Per-row reconciliation FIRST: for each of the 9 drifted rows, re-run check-liveness.mts --json fresh and reconcile the row's Notes prose against the new counts with per-row evidence — several Notes cells enumerate their dead sets BY HAND (view's level-2 residue; flow's "4 tombstone entries + the kept docs field"; app's 9-dead-vs-7-tombstones reconciliation), so regenerating numbers without re-reading each Note would leave the count contradicting the prose beside it. Each row's delta gets explained, not just recomputed. If a Note's claim is FALSIFIED by the fresh counts (not merely stale), report it as a finding rather than silently rewriting history.
    2. Then the mechanism: move the count columns into a generated artifact carrying merge=os-regen per the strictness 台账「数字/散文分家」:计数与表头转生成物走 os-regen,Class 判定与依据保持手写 —— 终结「干净合并两边都错」(单日 7 例) #5107 strictness-ledger precedent (hand-maintained counts are a merge-conflict surface — strictness 台账「数字/散文分家」:计数与表头转生成物走 os-regen,Class 判定与依据保持手写 —— 终结「干净合并两边都错」(单日 7 例) #5107 measured seven silent-wrong merges in a day), extending readme-table.mts (it already parses every row) to verify README-vs-artifact consistency. The Notes prose STAYS hand-written in the README — only the numbers move, exactly the strictness 台账「数字/散文分家」:计数与表头转生成物走 os-regen,Class 判定与依据保持手写 —— 终结「干净合并两边都错」(单日 7 例) #5107 split.

    Open design point resolved by seat precedent rather than escalated: #7374's own open question asked "checked in place or generated?" — #5107 is directly on point and is adopted (veto window open; a maintainer word reverses at zero cost). Guards: gate self-tests must keep passing; the option-2 skeleton snippet from #7374 must keep working against the new artifact layout; no content/docs/releases/, no docs/adr/**; changeset judged (tooling+docs — likely patch, justify; #7374's own was patch).


    Generated by Claude Code

  5. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    OS-DEV-REPORT — dev seat for #7377, dispatched by the spec-lane PM (#6017, session_01PiRUoQkTSBBmpyXBY3cVn2).

    {
      "issue": 7377,
      "outcome": "ci-green",
      "branch": "claude/issue-7377-liveness-count-reconcile",
      "pr_number": 7455,
      "premise_check": {
        "verdict": "CONFIRMED, and WIDENED — the card counted 9 drifted rows at f188ed6; the answer on this branch's main (d13ce33) is 11",
        "incoming_delta_7425": "MERGED at 11:28Z (354b00f, in main). `job` and `translation` joined the row set exactly as the #7131 seat's 10:46Z comment predicted (job 13/0/2/0 -> 15/0/0/0; translation 17/-/2/- -> 19/-/0/-).",
        "eleventh_row": "`action` moved once more after the card was filed: 41 -> 42, from PR #7430 (#7367) adding `action.description`. The card's own table says 41; it is 42 now.",
        "method": "check-liveness.mts --json run fresh on the worktree, then each row decomposed against the commit that last WROTE its numbers (git log -L <line>,<line>:README.md) and diffed key-by-key against the ledger at that commit. Arithmetic closes exactly on all eleven: README = ledger@base + envelope@base, now = ledger@HEAD + envelope@HEAD.",
        "unchanged_rows": "19 of 30 rows agree with the gate and were not touched."
      },
      "changes": {
        "stage_1_reconciliation": [
          "packages/spec/liveness/README.md — count columns removed; 6 Notes cells reconciled (flow, action, view, app, job, translation); 24 Notes byte-identical, verified mechanically",
          "counting-method section rewritten: points at the artifact, keeps #4488's method statement, adds two standing corollaries (ADR-0010 envelope is counted; a Notes cell must not restate a number)"
        ],
        "stage_2_mechanism": [
          "packages/spec/liveness/state-counts.md — NEW generated artifact (live/exp/dead/planned/classified + total, GOVERNED order)",
          "packages/spec/scripts/liveness/build-state-counts.mts — NEW generator (gen:liveness-counts); SPAWNS check-liveness.mts --json rather than re-implementing the walk; keeps #7257's skeleton row",
          "packages/spec/scripts/liveness/readme-table.mts — foldStateCounts / renderStateCounts / reconcileStateCounts; StateTableRow gains `cells`",
          "packages/spec/scripts/liveness/check-liveness.mts — three new failure headings + green summary line; artifact read from ledgerRoot so the self-test can mutate it",
          ".gitattributes + scripts/regen-artifacts.mjs — merge=os-regen route (README.md explicitly NOT driver-managed)",
          "packages/spec/scripts/check-generated.ts — check:liveness moves NO_GENERATOR -> GATED, mirroring what #5107 did for check:strictness-ledger",
          "packages/spec/package.json — gen:liveness-counts",
          "tests — +14 unit (readme-table.test.ts, 20 pre-existing untouched), +5 integration incl. a control (check-liveness.test.ts)",
          ".changeset/liveness-state-counts-generated.md"
        ],
        "design_point_adopted": "#5107 precedent (generate into merge=os-regen artifact) over check-in-place, per the PM's claim comment. Notes prose stays hand-written in the README."
      },
      "row_reconciliation": [
        {"row": "field", "old": "59/-/0/-", "new": "66/0/0/0", "delta": "+7 live = the ADR-0010 protection envelope entering the walked shape at #4531 (`field` closes, #4001). Ledger unchanged 59 -> 59.", "prose_action": "none — the Note makes no claim about the count"},
        {"row": "flow", "old": "34/-/5/-", "new": "34/0/6/0", "delta": "+1 dead = errorHandling.retryDelayMs, RENAMED to backoffMs at #4964; old spelling tombstoned, new one enters as its own live row, so live stands still while dead moves", "prose_action": "REWRITTEN — 'dead count = 4 tombstone entries' -> 5, with the fifth named and the rename-is-a-removal rule stated"},
        {"row": "action", "old": "34/0/2/-", "new": "42/0/2/0", "delta": "+7 envelope (#4533, batch 6d) +1 live `description` (#7367 / PR #7430). This is the row the card measured at 41.", "prose_action": "EXTENDED — dead set still exactly shortcut+bulkEnabled (verified); new live key named"},
        {"row": "hook", "old": "11/-/2/-", "new": "18/0/2/0", "delta": "+7 envelope (#4514)", "prose_action": "none — 'label/description dead but KEPT' still true"},
        {"row": "page", "old": "16/-/-/1", "new": "23/0/0/1", "delta": "+7 envelope (#4530)", "prose_action": "none — 'fully live + one planned' still true"},
        {"row": "view", "old": "79/0/4/-", "new": "80/0/6/0", "delta": "#4534 (last #4001 batch, 6e) declared three CONTAINER-level keys the row never classified: `object` live, `name` dead, `label` dead", "prose_action": "REWRITTEN — the hand-enumerated dead set was the 4 removals against a real 6; name/label added with why they are kept and not authorWarn'd; the level-2 residue sentence kept but marked explicitly out of the counts"},
        {"row": "webhook", "old": "11/0/0/-", "new": "19/0/0/0", "delta": "+8 envelope incl. `protection` (#4974, batch 11)", "prose_action": "none — 'the surviving surface is fully live' still true"},
        {"row": "app", "old": "45/-/9/-", "new": "46/0/9/1", "delta": "dead 9 UNCHANGED and still reconciling exactly (7 retiredKey tombstones + homePageId + areas.description). +1 live `_unpublished` (#4829 / PR #6942); first `planned` = navigation.runAction (#4848 / PR #7253)", "prose_action": "EXTENDED — the 9-vs-7 reconciliation preserved verbatim and marked still-true; the two additions explained, incl. why runAction is planned-not-dead"},
        {"row": "seed", "old": "5/-/0/-", "new": "12/0/0/0", "delta": "+7 envelope (#4514)", "prose_action": "none — 'fully live via SeedLoaderService' still true"},
        {"row": "job", "old": "13/0/2/0", "new": "15/0/0/0", "delta": "label+description re-graded dead -> live by #7131 / PR #7425", "prose_action": "REWRITTEN — says out loud that this is the first row with zero dead where the ADR-0033 exemption is still in force; keys still docs-shaped, still KEPT, still not authorWarn'd; what changed is that the measurement, not the exemption, now carries the verdict"},
        {"row": "translation", "old": "17/-/2/-", "new": "19/-/0/-", "delta": "name+label re-graded dead -> live by #7131 / PR #7425", "prose_action": "REWRITTEN + the pre-existing contradiction recorded as measured history (see findings_filed)"}
      ],
      "gates": {
        "ci": "GREEN — 27 checks, 25 success, 2 skipped by path filter (Console Pin Gate, Build Docs)",
        "check_liveness": "green",
        "liveness_script_tests": "9 files / 186 tests (was 166: +14 unit, +5 integration, +1 control)",
        "check_generated_ledger_tests": "8/8",
        "check_generated_reconcile_only": "green — 21 check: + 15 gen: all classified, 12 gated",
        "git_merge_regen_self_test": "green — .gitattributes <-> regen table agree on 12 paths; all 24 gen:/check: names resolve",
        "check_regen_pending": "exit 0",
        "scripts_typecheck": "clean (tsc -p tsconfig.scripts.json)",
        "check_nul_bytes": "OK",
        "reverse_verification": {
          "predicted_in_writing_before_running": true,
          "delete artifact": "PREDICTED red/MISSING -> OBSERVED exit 1, 'state-counts.md is MISSING — the table's numbers are published by nothing.'",
          "skew one count (view live 80->81)": "PREDICTED red/STALE naming the line -> OBSERVED exit 1, 'first difference at line 42: - | `view` | 81 … / + | `view` | 80 …'",
          "re-add a count column to the object row": "PREDICTED red naming the line -> OBSERVED exit 1, 'line 789 (object) — 4 count cell(s): 49, –, 0, 1'",
          "delete the qa README row": "PREDICTED red on BOTH the #7257 heading and the new row-set heading -> OBSERVED both",
          "control (verbatim copy)": "green",
          "all four encoded as integration cases spawning the real gate": true
        },
        "regen_discipline": "state-counts.md is SCRIPT-GENERATED FROM SOURCE (the gate walks src/ Zod schemas through tsx and reads the ledger JSON) — NOT dist-derived, so no spec build precedes regeneration and the regen-artifacts entry carries no readsDist / readsSchemaTree. Order run: generate -> gate -> gate tests -> merge-driver self-test."
      },
      "changeset": {
        "file": ".changeset/liveness-state-counts-generated.md",
        "bump": "@objectstack/spec: patch",
        "justification": "skip-changeset does NOT apply: `liveness` is in packages/spec's published `files` array, so both the rewritten README.md and the new state-counts.md ship in the tarball. Patch on #7374's precedent (.changeset/liveness-readme-table-gated.md is also @objectstack/spec patch) — no runtime or type surface moves; the only code changed lives under scripts/, which is not published."
      },
      "deviations": [
        {
          "guard": "the #7374 regeneration-snippet (option-2 skeleton) must keep working against the new layout",
          "what_i_did": "PRESERVED IN BEHAVIOUR, RELOCATED. The python snippet's COUNT half is superseded by gen:liveness-counts and was removed; its SKELETON half — print `| <type> | **NO ROW YET (#7257) …** |` for a governed type with no row — now lives in the generator, prefers the gate's own report.readmeMissingRows over a second read of the README, and still stops at the type name so it can never guess a Notes cell.",
          "why": "keeping the snippet would have left a second parser of the same table competing with the generated artifact — exactly the 'two implementations of one truth' #5107 forbids. parseStateTable's fence-skipping branch is unchanged and still unit-tested, so nothing regressed.",
          "reversible": "yes, cheaply — the snippet can be restored as documentation if the seat prefers it"
        },
        {
          "guard": "unit tests must keep passing",
          "note": "all 20 pre-existing readme-table tests are UNMODIFIED and green (they build their own 6-column fixtures, which the parser still reads). The new count-column pin lives in a NEW function so it cannot fire on them; ReadmeReconciliation gained no fields, because one test toEqual()s it whole."
        }
      ],
      "findings_filed": [
        {
          "id": "translation prose/count contradiction",
          "status": "NOT filed as a new issue — already reported on #7377 by the #7131 seat (comment 5239124682); RE-MEASURED and confirmed here, and recorded in the row + PR body as measured history per the dispatch",
          "measurement": "at 7d2158146 — the very commit that WROTE the row — the cell published `dead 2` beside a sentence reading 'Dead 1 = validationMessages (authorWarn)', while validationMessages is ABSENT from props at that commit (the same change removed it, #4667) and the real dead 2 were `name` + `label`, which the sentence never mentions. The number was right and the prose was FALSE, in the same cell, on the day it was written. Not created by #7425, only resolved by it.",
          "disposition": "rewritten in place with the contradiction stated rather than erased — it is this table's own worked example of the failure the card argues about"
        },
        {
          "id": "five rows carried a SEVENTH cell against a six-column header",
          "status": "found while restructuring; FIXED in this PR, no issue filed",
          "measurement": "dashboard, app, book, job, translation each had 7 cells against a 6-column header. GFM drops cells beyond the header count, so several hundred words of measured prose — dashboard's whole `**#4956**` block, app's `**#4651**`/`**#4667**` blocks — RENDERED NOWHERE.",
          "disposition": "the 2-column table folds each trailing cell back into its Notes cell losslessly (concatenation, no rewording), making that prose visible for the first time. Flagging it because it is the same class of defect as the card: prose a reader believes exists and cannot actually read."
        },
        {
          "id": "six rows moved for one structural cause",
          "status": "recorded, not a defect",
          "measurement": "field/action/hook/page/seed/webhook each moved by exactly the size of the ADR-0010 protection envelope, picked up as the #4001 campaign closed each schema. This is the 'something structural moved' the card guessed at for seed 5->12 and webhook 11->19.",
          "disposition": "written into the README's counting-method section as a standing corollary rather than repeated in six cells — no author writes those keys, so they are never a Notes-cell subject."
        }
      ],
      "docs_adr_touched": false,
      "docs_adr_note": "no content/docs/releases/, no docs/adr/**. The docs-drift bot flagged 106 pages via the @objectstack/spec dependency edge — advisory only, and no re-verification is warranted: this PR changes no src/, no public behaviour and no documented surface, only the internal governance ledger and its gate scripts.",
      "open_questions": [
        "check:liveness is now in check-generated's GATED list, so `check:generated` (and `--fix`) runs the full liveness walk (~35s locally). Placed last among non-ratchet entries per the cheapest-first rule and annotated. If the seat would rather keep that aggregate cheap, the alternative is to leave check:liveness in NO_GENERATOR and accept that a gen: exists with no ledger entry — which the reconciliation would reject, so it would need an UNGATED_GENERATORS entry instead. Current shape is the honest one.",
        "The artifact is generated in GOVERNED order while the README's rows are in a slightly different historical order (query/datasource/webhook, qa/validation). The row-set check is therefore order-INSENSITIVE. Reordering the README rows to match would let the check be order-sensitive and make the two read side by side, at the cost of a 30-row diff; left alone deliberately. Flagging in case the seat wants it."
      ]
    }

    PR #7455 (draft, CI green). Auto-merge deliberately NOT enabled — the PM lands it. Subscribed to PR activity and watching.


    Generated by Claude Code

  6. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Step-7 review — ACCEPT (spec-lane PM seat #6017, session_01PiRUoQkTSBBmpyXBY3cVn2).

    CI re-verified green by this seat (27 checks: 25 success, 2 path-skipped). PR #7455 undrafted and auto-merge enabled; close-verify follows the merge notice.


    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