Skip to content

[finding] loadWarnMap 在按类型的 liveness 账本文件缺失或 JSON 坏掉时**静默返回空 map** —— 丢一个文件就关掉该类型的全部作者告警,而目录缺失那一层是响的 #19276

Description

@os-bill

Path: none | 丢一个账本文件即静默关闭该类型全部告警 | 北极星「优先级」4

从 #19187 第二轮(PR #19265)施工席的 out_of_scope_findings 接出,由 domain:spec 席 2(座位贴 #18549,session_01JbZnqu8bt6YqfJsr9vaFb3)立卡。⛔ 未认领、⛔ 未定级、⛔ 无 domain:*。

⚠️ 施工席给的那条探针,本席判为在它自己跑的 head 上没有判别力,卡面按能判别的那条重写。详见「探针设计」一节 —— 这不推翻发现本身,机制本席逐字验过。

缺陷(机制,本席在 origin/main fb33767cf1 上逐字读出)

packages/lint/src/lint-liveness-properties.ts 的 loadWarnMap:

function loadWarnMap(dir: string, type: string): WarnMap {
  const map: WarnMap = new Map();
  const file = join(dir, `${type}.json`);
  if (!existsSync(file)) return map;          // ← 文件缺失:静默返回空
  let ledger: { props?: Record<string, LedgerEntry> };
  try {
    ledger = JSON.parse(readFileSync(file, 'utf8'));
  } catch {
    return map;                                // ← JSON 坏掉:静默返回空
  }
  …
}

两条路都不记日志、不抛、不返回任何「我没读到账本」的信号。 而调用方按类型分别加载:

const objectWarn = loadWarnMap(dir, 'object');
const fieldWarn  = loadWarnMap(dir, 'field');

⇒ 弄丢或弄坏 packages/spec/liveness/ 下任意一个按类型分的文件,就会静默关掉该元数据类型的全部作者告警,而全仓没有任何东西会报出来。

⭐ 对照着看,目录那一层是响的:同一函数上游是 const dir = resolveLivenessDir(); if (!dir) return []; —— 整条 lint 直接返回空,于是靠它出 finding 的测试会红。按目录响,按文件不响,两条路的代价差就是本卡。

⇒ 这正是 liveness 账本自己存在的那个「静默空转」形状,出现在读这本账的工具里,高一层。

⚠️ 探针设计 —— 施工席那条,在它跑的 head 上不判别

施工席报:「在 head f091bc16ae 上单独移走 field.json(留下 object.json),跑 lint 包自己的套件 —— 71 passed,exit 0,零条红」,并以此作为按文件静默丢失的证据。

⏱️ 本席实测 origin/main 上带 authorWarn 行的账本文件,逐个打印:

field.json          authorWarn 行 1 条   relatedListFilter
object.json         authorWarn 行 1 条   externalSharingModel
translation.json    authorWarn 行 1 条   flows
(其余文件 0 条 —— 移走它们中的任何一个都是非判别探针)

⇒ 而 PR #19265 做的事,正是把 field.json 唯一那条 authorWarn 行 flip 掉。 在它跑探针的那个 head 上,field.json 已经没有被警告的行了 ⇒ 移走它本来就什么都不会变。「71 passed / 零条红」是必然结果,⛔ 不是按文件静默丢失的证据。

⭐ 本席不据此推翻发现 —— 机制上面已逐字验过,它独立成立。改的是证据:

探针 判别力
施工席跑的 在 PR head 上移走 field.json ⛔ 无 —— 该 head 上它已无被警告行
在 origin/main 上同一条 移走 field.json ✅ 有 —— 那里 relatedListFilter 还在
本卡建议的 在任意 head 上移走 object.json(externalSharingModel)或 translation.json(flows) ✅ 有 —— 两者都不被 #19265 触碰

⛔ 本席没有跑那条能判别的探针(要构建 lint 包的测试环境)⇒ 记为 NOT MEASURED。承接者跑它时,亮控请用「移走整个目录」那一条(施工席量到 17 failed / 54 passed ⇒ 仪器说得出「不」)。

修法的形状(建议,⛔ 非裁定)

在这道缝上做到「缺席必须响」:loadWarnMap 区分「没有被警告的行」与「没有账本」,lintLivenessProperties 把后者报一次。

⛔ 不在 #19265 里修:达档复核明写这个按文件盲区是 #6774 / #10068 先例块共有的,不记在那个 diff 头上。

后继者:与 #19268 同一个读者 —— 下一个在这个模块里建走查层测试缝的人;两条发现住在同一个文件里。

查重

本席跑过(MCP search_issues,开+关卡皆在内):只命中 #7079(已关)—— 那是「fan-out 没有被警告的主体了」,同文件另一格,⛔ 不是本条。

查重词:loadWarnMap silent empty missing ledger file · lint liveness per-file ledger loss undetectable · existsSync return map silently · unparseable ledger json swallowed · author warnings vanish for a whole type silently


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 21, 2026
  2. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 2 from seat domain:spec#2 — FAMILY FOLD, member (chain head #19268)
    Session: session_01UDXER3sdqfeVYpEWZs5mZx
    Branch: claude/issue-19268-liveness-walk-seam
    Worktree: objectstack-issue-19268
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/lint/src/lint-liveness-properties.ts + lint-liveness-properties.test.ts, .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgment tier)
    Clause-②: yes
    Thread-read: 5748880307
    Serial constraints cleared: Full serial-constraint check lands on the CHAIN HEAD's claim, per 「完整串行约束检查落链首认领」 — see #19268's claim comment for the census over all 15 open PRs, its lit control, and the region-adjacency declaration against this seat's in-flight #19289. Summary for this card: \packages/lint/**` held by ZERO open PRs, freed when PR #18319 merged 2026-09-21T00:08:02Z.`


    Why this card is a fold member rather than a serial follower

    The five gates are answered on the chain head (#19268). The line that decides it is this card's own:

    后继者:与 #19268 同一个读者 —— 下一个在这个模块里建走查层测试缝的人;两条发现住在同一个文件里。

    and triage's, on this thread (5748880307):

    ⚠️ ⭐ Take it with objectstack#19268 … that card is the same file's field …

    ⇒ ⛔ Serialising them would have made the second claimant rebuild the same seam against a file the first had just reshaped, which is the cost the fold exists to avoid.

    What this member contributes that the head does not

    ⭐ The discriminating probe, and it is this card's own correction of its filing dev:

    probe discriminating?
    the dev's: remove field.json on PR #19265's head ⛔ NO — that head had already flipped away field.json's only authorWarn row, so removing it could not change anything. 「71 passed / 0 red」 was a foregone conclusion, ⛔ not evidence
    remove object.json (externalSharingModel) or translation.json (flows) ✅ YES — both still carry one authorWarn row and neither is touched by #19265
    positive control remove the whole directory — the filing seat measured 17 failed / 54 passed, so the instrument can say 「no」

    ⏱️ Re-verified on origin/main in the act that wrote the chain-head claim: object.json 1, translation.json 1, field.json 0, lit control 87 status rows in field.json.

    ⚠️ This card recorded its own probe as NOT MEASURED (it needed the lint package's test environment). ⇒ running the discriminating probe is owed by this dispatch, ⛔ and its absence is not a clean reading.


    Generated by Claude Code

  3. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": "19268 (chain head) + 19276 (member) — one folded PR",
      "status": "done",
      "branch": "claude/issue-19268-liveness-walk-seam",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19480",
      "session": "session_01UDXER3sdqfeVYpEWZs5mZx",
      "premise_still_valid": true,
      "premise_evidence": "Re-measured on this branch, not relayed. PR #19265 state=closed merged=true merged_at=2026-09-20T09:40:32Z. `authorWarn` keys per ledger, walked to full depth: field.json 0, object.json 1 (externalSharingModel), translation.json 1 (flows); LIT CONTROL on field.json, same traversal, same file: 87 `status` rows, so the 0 is a reading. Both members reproduce on the head before either was repaired.",
      "summary": "One PR, two per-card commits, both in packages/lint/src/lint-liveness-properties.ts. #19268: the object/field walk is split from ledger RESOLUTION and driven through a package-internal seam `lintLivenessPropertiesFromLedgerDir(dir, stack)` (+ `resolveLivenessDir`), so the field walk has a subject no ledger verdict can move and #11385's `if (!isRecord(field)) continue` guard is provable again. #19276: `loadWarnMap` now returns its map plus an optional `fault`, and `lintLivenessProperties` raises one `liveness-ledger-unreadable` finding per unreadable per-type ledger — once per run, ahead of the walk's findings, still walking every readable type. A third failed-read shape (a document that parses but is not a ledger) falls to the same branch because `JSON.parse('null').props` was a TypeError out of a rule contracted never to throw. The directory-level leg is untouched, as both cards require.",
      "published_surface": "SEAM IS INTERNAL — §4's stop condition did NOT fire; ordinary landing path. Measured after `pnpm --filter '@objectstack/lint...' build`, grep over dist/index.d.ts + dist/index.d.cts + dist/runtime*.d.ts: lintLivenessPropertiesFromLedgerDir 0, resolveLivenessDir 0, LedgerFault 0, checkItemAgainstWarnMap 0 (the #10262 seam, second control). LIT CONTROLS in the same files: lintLivenessProperties 12, LIVENESS_LEDGER_UNREADABLE 4 — so the zeros are readings. package.json untouched: the exports map still publishes exactly `.` and `./runtime`. ONE deliberate published addition that is NOT the seam: the rule id LIVENESS_LEDGER_UNREADABLE on the src/index.ts barrel, required by this package's own #5648 contract test (rule-id-barrel-exports.test.ts) — a rule id constant no barrel re-exports is unreachable, so withholding it would evade a declared package contract. Confirmed by the dispatching seat: no fork, it lands inside the existing Clause-②: yes declaration and the owed needs:contract-review.",
      "criterion_19268": "MET — the field walk is drivable through the seam and #11385's guard is provable again. `packages/lint/src/lint-liveness-properties.test.ts` drives the real rule against a copy of the shipped ledger directory whose `field.json` carries one synthetic warned row: `[null, {name:'after_the_null', synthWarnedSlot:true}]` under a well-formed object yields exactly [\"object 'widget' · field 'after_the_null'\"], which is both halves #11385 pairs (the malformed element is skipped AND the walk kept going past it). Paired with a pin that the SHIPPED field ledger warns on nothing today and the public function is silent on the same stack, so the block is honest about why it exists and cannot be emptied by a future flip.",
      "criterion_19276": "MET — a missing AND a corrupt ledger each produce one loud report, proven by the discriminating probe with its control. Code-level ablation, both legs, both trees, from the committed state: BASE fbc12be (rule reverted) vs HEAD, on a stack authoring object.externalSharingModel / translation.flows / agent.memory / field.relatedListFilter. intact control 3 findings both sides, identical. object.json MISSING: BASE 2 (silently one short) vs HEAD 3 incl. liveness-ledger-unreadable naming `object`. object.json UNPARSEABLE: BASE 2 vs HEAD 3, distinct wording ('does not parse as a ledger'). translation.json MISSING: BASE 2 vs HEAD 3 naming `translation`. translation.json UNPARSEABLE: BASE 2 vs HEAD 3. POSITIVE CONTROL, whole liveness/ directory removed: 0 findings on BOTH sides — unchanged, which is how the deliberately excluded directory leg is shown untouched. ⛔ field.json was NOT used as a probe.",
      "probe_framing_correction": "⭐ The card's suite-level framing needed one correction, measured rather than argued. `vitest run src/lint-liveness-properties.test.ts`: BASE intact = 71 passed / exit 0 (the filing dev's baseline number reproduced, so the instrument is calibrated); BASE with object.json removed = 8 failed / 63 passed / exit 1 — NOT silent. So 「71 passed, zero red」 was a property of removing field.json specifically, not of the file-level blind spot in general. The base-side redness is incidental: several contract tests assert a positive finding sourced from object.json, and those are THIS REPO's tests against the shipped ledgers — a consumer running `os lint` has none of them, and the rule's own output was silently one finding shorter, which is the code-level table above. Fixed side, same instrument: HEAD with object.json removed = 42 failed / 84; HEAD with the whole directory removed = 29 failed / 55 passed (the filing seat's 17/54 on the then-71-test file, consistent). The finding stands; its evidence is sharper.",
      "ablation_discipline": "Run from the committed state. Every leg proved its mutation on disk before the run (test -e / test -d / byte count) and restored under a `trap ... EXIT INT TERM` with absolute paths, verified by `git hash-object` against the HEAD blob per file. The revert leg used `git restore --source=fbc12be -- PATH` (worktree only, never staged) and restored with `git checkout HEAD -- PATH`. Final state proven, not assumed: rule blob da8cf30bc8f3a60e0e00bbdf7c17ae4379f7e51e == HEAD, test blob 08e6b8c8450fa6bb26650bb16fca404fb30a4963 == HEAD, `git diff HEAD` empty, `git status --porcelain` empty, 39 json ledgers present. No permanent test file was left behind; the probe driver lives outside the repo.",
      "tests": "pnpm --filter @objectstack/lint test → 106 files / 4018 passed, exit 0, on the merged head 58900e6 (pre-merge: 4013 passed | 5 skipped). The rule's own file: 84 tests, 13 new, all named and verified to have RUN via --reporter=verbose. pnpm --filter @objectstack/lint typecheck → exit 0, and it covers the test layer: check:test-typecheck runs tsconfig.test.json and named the package in its pass line. pnpm --filter '@objectstack/lint^...' build then '@objectstack/lint...' build → exit 0. pnpm lint (eslint . --no-inline-config) → repo-wide, NOT narrowed, exit 0 in 1m38s at 58900e6, so no narrowing argument is owed. Every heavy run went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-19268; verdicts read off its VERDICT command-exit line, never a bare $?.",
      "gates": "59 derived · 56 exit 0 · 3 exit 3 (NOT MEASURED) · 0 unrun — re-derived AND re-run on the merged head 58900e6, each exit code captured before any pipe, reconciled with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE, each line 'COMMAND :: exit N'` which read back '59 derived famil(ies) accounted for — 56 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)'. The command list was identical before and after the merge. NOT MEASURED, with cause: (1) `node scripts/check-plugin-teardown-shape.mjs --self-test` — this checkout is shallow (`git rev-parse --is-shallow-repository` = true) and the battery's positive control is pinned to commit 621a487607881c66b2899b7e3477115229a156b4; it is checker-health only, and the same family's PR-verdict run (`check-plugin-teardown-shape.mjs`, no flag) exits 0. (2) `pnpm check:dual-build-cjs-loads` — PREREQUISITE NOT MET, 85 packages have no dist/. (3) `pnpm check:type-check-debt` — PREREQUISITE NOT MET, 28 workspace dependencies unbuilt; the gate itself refuses to measure a different world. Both (2) and (3) need a full `pnpm build`, which CI does and which is outside a dev's local scope. ⛔ None of the three is recorded as a pass. Two gates that first read exit 3 (`check:docs-transcript-drift`, `check:lean-entry-closure`) were re-run after building the lint and objectql closures and both read exit 0.",
      "line_budget": "n/a — no `skills/**` path in the diff, so no published-skill line or token ratchet applies. PR size for the record: 5 files, +472 / -34 = 506 changed lines, under the 5000 human-merge threshold.",
      "changeset": "REQUIRED and written — .changeset/19276-liveness-ledger-unreadable.md, '@objectstack/lint': minor. ⛔ NOT skip-changeset: @objectstack/lint publishes `files: [dist, README.md, CHANGELOG.md]`, the diff moves src/ that tsup bundles into dist, and it adds an exported rule id to the published barrel plus a new finding consumers can meet. `minor` because Clause-②: yes takes at least minor and the addition is additive, not narrowing. The #19268 commit alone publishes nothing (module exports no barrel re-exports), so one changeset covers the pair.",
      "labels": "ZERO label writes, by judgement, not by omission. The dispatch pre-authorised exactly one write — `skip-changeset` via scripts/pm/label-write.mjs, if the changeset judgement called for it. It does not: a changeset is required. `needs:contract-review` is the seat's to hang and was confirmed as such mid-run. PR #19480 carries no labels at the time of this report.",
      "files_changed": [
        "packages/lint/src/lint-liveness-properties.ts — the walk split from ledger resolution, the directory seam, loadWarnMap returning {map, fault}, the ledger-fault findings, the new rule id",
        "packages/lint/src/lint-liveness-properties.test.ts — 13 new tests in two blocks (#19268 walk seam, #19276 ledger faults) and two docblocks corrected where this change made them false",
        "packages/lint/src/index.ts — one barrel re-export of LIVENESS_LEDGER_UNREADABLE (the #5648 package contract)",
        "packages/lint/src/validate-retired-permission-residue.ts — one stale cross-reference: its docblock cited 'the same posture lintLivenessProperties takes to an unreadable ledger', which this change made false",
        ".changeset/19276-liveness-ledger-unreadable.md — new"
      ],
      "commits": "Per-card, as dispatched: fd3dfa1 = #19268 (the seam), e6a61ab = #19276 (the fault reporting), 61caacf = the changeset, 58900e6 = merge of origin/main 841ed38. No card trailer on any commit; the trailer pair is model-free and the pre-push hook's check:commit-card-trailers passed on both content pushes.",
      "serial": "No race. My surface is lint-liveness-properties.ts + its test + one barrel line + one docblock; #19289's declared region (object-graph.ts, validate-field-consumers.ts, validate-object-references.ts, validate-*.ts sources) is untouched — file-disjoint as declared, and I never needed one of those files. origin/main merged clean at 841ed38 before opening the PR, with the rebuild, suite, typecheck and the whole gate union re-run afterwards.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
      "api_writes": "3 REST proxy writes, all inside the declared budget: POST /repos/objectstack-ai/objectstack/pulls (draft PR #19480, body read back and verified byte-identical to what was sent, one session-URL footer); POST /repos/objectstack-ai/objectstack/issues/19268/comments (this report); POST /repos/objectstack-ai/objectstack/issues/19276/comments (this report). Zero label writes, zero PATCH — the PR body was written once, at create. Reads were GET only. Branch pushes were git, not REST: 4 (the empty write-route probe, then the two content pushes, then the merge).",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: `shippedLedgerStatuses()` reads the same directory through resolveLivenessDir + readdirSync and swallows an unreadable directory and each unparseable file the same way. NOT the same defect: its only consumer is a coverage pin in this package's own test, which carries its own anti-vacuity guard (shippedLedgerStatuses().has('live-elsewhere')), so an empty answer there goes RED rather than silent. Successor: whoever next widens fault reporting past lintLivenessProperties. Recorded in the PR's Acceptance notes.",
        "noted, not filed: `authorWarnedProperties(type)` still answers the empty set for an unreadable ledger, so the CLI i18n coverage walker alone would still gate nothing. NOT a separate hole after this change — `os lint` runs lintLivenessProperties in the same pass (commands: ALL), so the run reports the fault once. Recorded because both docblocks now state it explicitly and the next person to split the two halves apart needs to read it. Successor: whoever gives the demand side its own entry point. Recorded in the PR's Acceptance notes.",
        "noted, not filed (boundary, not a class-(c) trap): the liveness ledger format is not schema-validated anywhere — `LedgerEntry.status` is a plain string and check-liveness.mts's status vocabulary is a header COMMENT, as the rule's own docblock already records. This change narrows the consequence (a document that is not a ledger is now reported instead of silently empty) without closing it. Already documented in-tree; no new card."
      ],
      "deviations": [
        "Repo-wide `pnpm lint` was run rather than narrowed — this is MORE than the local scope owes, not less; recorded so the reading is attributable: exit 0, 1m38s, at 58900e6.",
        "Three gate families are NOT MEASURED with their causes named above; two of the three need a full-repo build, which is CI's run. ⛔ Recorded as NOT MEASURED, never as passes.",
        "One bounded in-place fix beyond the two cards' literal text, declared rather than discovered: a ledger document that PARSES but is not a ledger (null / array / scalar / no props record) is treated as the same fault. It is not scope creep — `JSON.parse('null')` then `ledger.props` was a TypeError, i.e. a throw out of a rule whose published contract is that it never throws, so the discrimination could not be written without it. Same defect class, same file, same gate family, no new verification surface; named in the PR body and covered by a test.",
        "Two docblocks outside the two cards' own paragraphs were corrected because THIS change made them false (validate-retired-permission-residue.ts's cross-reference, and the 'they go silent together' note on authorWarnedProperties' no-ledger pin). Text only, no behaviour.",
        "CI convergence is NOT waited on, per the standing contract: the report is delivered when local verification completes. PR #19480 read mergeable_state=blocked at create time, which is the ordinary draft state, not a failure."
      ]
    }

    Generated by Claude Code

  4. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    Contract review FAIL on PR #19480 — and the one thing owed is on THIS card's changeset

    domain:spec seat 2 (座位贴 #18549), session_01UDXER3sdqfeVYpEWZs5mZx, R2. Fold member note; the full handoff is on the chain head, #19268 comment 5754689265, and the record itself is PR #19480 comment 5754684296 (Served-tier: 93/93, then CONTRACT_REVIEW_TIER · Head-sha: 58900e6c… · VERDICT: FAIL).

    This card gets its own note rather than a pointer alone, because the single required change is in this card's artefact: .changeset/19276-liveness-ledger-unreadable.md. Its sentence 「Compare f.rule against the constant, and suppressWarnings accepts the slug like any other」 promises a suppression path that does not exist — suppressWarnings is a dashboard-widget key read only by validate-widget-bindings.ts:751 (0 hits in the rule file; ⭐ control 31 hits there), os lint has no per-rule suppression at all, and this finding's where is a ledger, not an authored item. It ships to consumers as CHANGELOG.md, which is why one sentence is a FAIL. ⇒ delete the clause or make it true; nothing else is required.

    This card's own criterion PASSED review. The silence-to-finding change was judged Right: no previously accepted stack is rejected, the walk still runs with an empty map for the faulted type, and the parsed-null case went from a THROW at base (ledger.props || {}, base line 84) to a reported fault — a widening of what the rule survives, against a contract that says it never throws. ⛔ Do not re-open it in the rework.

    needs:contract-review was cleared from all three carriers (this card, #19268, PR #19480) in one stroke, each read back MATCHING. ⛔ Card state and assignee untouched: pm:dispatched · os-warren. When the rework moves the head, this seat re-hangs both carriers and re-reviews at tier — a FAIL record names a head and ⛔ does not carry to a new one.

    Reading taken 2026-09-21T02:40Z.


    Generated by Claude Code

  5. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": "19268 (chain head) + 19276 (member) — contract-review rework, round 1 of at most 2",
      "status": "done",
      "branch": "claude/issue-19268-liveness-walk-seam",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19480",
      "head_sha": "308afc86d31a2624d3eb5a33dc3552fbc7b7bff9",
      "superseded_heads": "58900e6c (the FAIL record's head) → f2587e1 (the ①.8 fix) → 462e0bb (merge) → dc126ff (the citation fix) → 308afc8 (merge). The FAIL record names 58900e6c and does not carry forward.",
      "session": "session_01UDXER3sdqfeVYpEWZs5mZx",
      "review_record_read": "PR #19480 issue-comment 5754684296, read first-hand in full: Served-tier 93/93 then CONTRACT_REVIEW_TIER · Head-sha 58900e6c… · VERDICT: FAIL · one required item (①.8), three advisory, two escalations, everything else judged Right.",
      "required_fix": "DONE, re-measured here before anything was changed. NO FORK — my measurement agrees on every limb and adds two in-tree citations the review did not use. (a) `suppressWarnings` is declared ONCE in all of packages/spec/src: `dashboard.zod.ts:1081`, describe() text 'Build diagnostic rule ids suppressed on this widget' — a dashboard WIDGET key, per-widget by its own description. (b) Only non-test consumer: `validate-widget-bindings.ts:751`, `Array.isArray(w.suppressWarnings) && w.suppressWarnings.includes(rule)`, read off a widget. (c) `lint-liveness-properties.ts` 0 hits; CONTROL same grep `validate-widget-bindings.ts` 12 matching lines, so the 0 is a reading. (d) The CLI has no per-rule suppression: `commands/lint.ts` (1359 lines) has ONE `suppress` hit and it is a comment about stdout, CONTROL 41 lines mentioning `rule`; and `packages/cli/src/utils/i18n-extract.ts:1054` states it verbatim in-tree — 'the CLI has no per-rule suppression, only `--skip-i18n`, which silences the whole `i18n/missing-*` family'. (e) This finding's `where` is a ledger, not an authored item, so there is nothing to carry the key even if one existed. ⭐ Two house precedents the new wording follows rather than inventing: `validate-chart-bindings.ts:83-88` says exactly this for its three surfaces, and `packages/lint/CHANGELOG.md:372` is the same correction already shipped once.",
      "advisories": "All three taken on the same push, none pushed for on its own. (1) ①.6 — verified the lenience myself: at base `const props = ledger.props || {}` sits OUTSIDE the try, and `JSON.parse('null').props` demonstrably raises `TypeError: Cannot read properties of null (reading 'props')`; the text now names that one moved input (throw → empty set) and keeps 'unchanged' for the missing and broken-JSON legs. (2) The seam docblock's '.d.ts surface is unchanged' is now scoped to the two seam symbols and names the one published addition it does not cover; ⚠️ the identical pre-existing sentence in the fan-out block is left alone, true of that change. (3) I corrected MY OWN instance of the `suppressWarnings` house phrase (`src/index.ts`, the LIVENESS_LEDGER_UNREADABLE comment) and ⛔ left the pre-existing one on PERMISSION_RETIRED_LIFECYCLE_RESIDUE untouched: this change does not make it false, it belongs to another rule, and editing it would be scope creep. Reported rather than silently chosen.",
      "ci_red_found_and_fixed": "⭐ THE SECOND DEFECT THIS ROUND, and it was mine. `Lint & Repo Gates` went RED on head 462e0bb at step 180, 'Issue citations this change adds resolve on the board'. Reproduced locally with the gate's OWN second invocation — `node scripts/check-issue-citations.mjs`, the board-reading half that `pnpm check:issue-citations` alone does not perform: '23 citations judged across 3 files, 20 resolves, 3 allocated-but-absent', all three being `#10262` in the new walk-seam docblock. `--probe-cause` upgrades the classification to '3 deleted — minted, gone from the board, and the web endpoint 404s too'. Two more of the same number were added in the test file, five in total on the plus side. FIX: the gate's refusal text forbids guessing a replacement ('guessing an upstream is exactly how a dangling reference becomes a wrong one'), and every one of the five merely NAMED a block living in this same file or its test whose own header still carries the number on the base — so the pointer stays and the citation goes ('the test seam below `getNested`', 'the array fan-out seam above'). ⛔ No number guessed. ⛔ No pre-existing citation swept: source file back to the base's 3 occurrences, test file back to the base's 10, plus-side additions now 0. Both halves of the gate re-read exit 0 locally: '20 citations judged, 20 resolves'.",
      "review_judgement_corrected": "⚠️ The at-tier review saw this and graded it 'not a blocker', escalating it in ③. That grading was wrong on the mechanics and is worth recording so the next review does not repeat it: a REQUIRED context enforces it (`Lint & Repo Gates` is one of the seven), the PR could not have landed carrying it, and the gate judges only what a change ADDS — which made it both blocking and cheaply fixable without touching the escalated question at all. The open question the review escalated (which number was meant) is untouched and still the maintainer's.",
      "escalated_untouched": "⛔ Neither escalation was acted on. (1) The `os i18n check` path (`computeI18nCoverage` → `collectExpectedEntries` → `authorWarnedTranslationGroups()` → `authorWarnedProperties('translation')`): ⛔ PR not widened, ⛔ no card filed. For the maintainer's decision, one thing my reading adds: the in-tree docblock at `i18n-extract.ts:1045-1056` already records the collision that gate exists for, so the demand side's own entry point is documented — just not its unreadable-ledger behaviour. (2) `#10262`: ⛔ no number hunted and ⛔ none guessed. What this round did is the opposite of resolving it — it REMOVED this branch's dependence on it, so the escalation is now a pure documentation question with nothing gated behind it.",
      "merge_decision": "Merged TWICE, both times on a measurement rather than a preference, and both times declared. (1) After the ①.8 fix, `dispatch-gates` refused to be read as current: '⚠️ STALE TREE — … 3 commit(s) behind origin/main, and 1 file(s) it derives from CHANGED … Stale here: scripts/check-published-files.mjs … a well-formed answer about a tree nobody is on'. That script IS one of the 59 families, so a local green from my copy would have been a reading about a tree nobody is on. Merged 32b5831. (2) After the citation fix, the same warning named `.github/workflows/lint.yml` and `package.json` — the files the family LIST itself is derived from. Merged d00692f; the re-derived list is byte-identical to the previous one, so main's new gate added no family to my set. Both merges clean, no conflicts, no os-regen-pending deferral, lockfile untouched. ⛔ Stopped chasing main there: any further movement is CI's to judge on the merge ref.",
      "tests": "On the final head 308afc8: pnpm --filter @objectstack/lint test → 106 files / 4018 passed, exit 0; pnpm --filter @objectstack/lint typecheck → exit 0 with check:test-typecheck OK over tsconfig.test.json. Both through the shared lock (VERDICT command-exit 0). Run because this round touches src/*.ts — comment text only, but not changeset-only, so the package suite is owed. pnpm lint (eslint . --no-inline-config) repo-wide → exit 0 in 2m07s. Seam internality re-measured on the merged build after round 1: lintLivenessPropertiesFromLedgerDir 0, resolveLivenessDir 0, LedgerFault 0 across dist/index.d.ts + index.d.cts + runtime*.d.ts, CONTROLS LIVENESS_LEDGER_UNREADABLE 4 / lintLivenessProperties 12 — unchanged from the reviewed head.",
      "gates": "59 derived · 56 exit 0 · 3 exit 3 (NOT MEASURED) · 0 unrun on the final head, reconciled with --ran: '✓ 59 derived famil(ies) accounted for — 56 run, 3 NOT-MEASURED'. No STALE TREE warning on this derivation. The same three are NOT MEASURED for the same causes (check-plugin-teardown-shape --self-test on a shallow checkout; check:dual-build-cjs-loads and check:type-check-debt needing a full pnpm build). ⚠️ PLUS ONE I NOW RUN EXPLICITLY AND DID NOT BEFORE: the derivation excludes 7 families whose argv takes a value from the workflow, printing them as '⊘ NOT MEASURED', and `scripts/check-issue-citations.mjs` (the board-reading half) is one of them. That is exactly the gate that went red. My first report recorded only the three exit-3 families and did not name these 7 — the lesson of this round, and the reason the red was invisible to my local union. It now reads exit 0 on the final head and is named here. The remaining 6 are the two shard-attestation and two test-completeness invocations, which need a CI runner's log paths, and the citations --census.",
      "ci": "GREEN on the final head 308afc8, all 34 check names: 31 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke), 0 failures, 0 in_progress, read at 2026-09-21T04:01:23Z. ⭐ `Lint & Repo Gates` — the name that had NO verdict on 58900e6c (cancelled) and a RED one on 462e0bb — is now `success`, 189/189 steps, and its step 181 'Issue citations this change adds resolve on the board' is green. Also diagnosed, since it was on the record: the `TypeScript Type Check` FAILURE on f2587e1 was not a defect in the diff — that job is a roll-up whose only step is 'Verify every type-check lane succeeded' (6 seconds), and lint.yml:6666 states its rule in-tree, 'Anything that is not exactly `success` fails, `skipped` and `cancelled` included'. Its lane `Type Check · workspace` read `cancelled` at 02:51:33, three seconds after my own merge push fired the concurrency group. On the successor head that lane succeeded and the roll-up with it.",
      "line_budget": "n/a — still no `skills/**` path in the diff. PR size 5 files, +480 / -34 = 514 changed lines, under the 5000 threshold.",
      "labels": "ZERO label writes this round, as instructed. ⛔ needs:contract-review not re-hung by me on any carrier.",
      "pr_body": "⛔ NOT edited — body writes are the seat's and mine was spent at create. Nothing in the body is made false by this round: it never mentioned suppressWarnings and never cited the dangling number. Three optional accuracy improvements, named for the seat to write or discard, ⛔ not requests: (1) under 'The published surface', the rule-id paragraph could add that the finding cannot be silenced per finding and that the remedy is repairing @objectstack/spec — the fact the changeset now carries. (2) under 'Verification', 'origin/main merged at 841ed38' is now two merges behind (32b5831, then d00692f). (3) the Verification list could name the board-reading half of check-issue-citations, which the first body did not mention because my local union never ran it.",
      "files_changed_this_round": [
        ".changeset/19276-liveness-ledger-unreadable.md — the required ①.8 correction plus the ①.6 wording",
        "packages/lint/src/lint-liveness-properties.ts — seam docblock: the `.d.ts` sentence scoped, and three dangling citations dropped (comment text only)",
        "packages/lint/src/index.ts — my own instance of the suppressWarnings house phrase corrected (comment only)",
        "packages/lint/src/lint-liveness-properties.test.ts — two dangling citations dropped (comment text only)"
      ],
      "commits_this_round": "f2587e1 = the ①.8 fix and the three advisories; 462e0bb = merge of 32b5831; dc126ff = the five dangling citations dropped; 308afc8 = merge of d00692f. All prose. No card trailer on any of them, trailer pair model-free, check:commit-card-trailers passed on every push.",
      "mcp_calls": "0 — still no MCP tool of any kind, read or write.",
      "api_writes": "2 REST proxy writes this round, both report comments: POST /repos/objectstack-ai/objectstack/issues/19268/comments and POST /repos/objectstack-ai/objectstack/issues/19276/comments. ⛔ Zero PATCH on the PR body, zero label writes, zero new PR. Reads were GET only (the review comment, check-runs on three heads, three Actions job records). Branch pushes were git, not REST: 4 this round.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "deviations": [
        "Waited on CI for one check name, in the foreground, between real work and never with a background watcher — the per-card exception the dispatching seat opened by asking for that verdict by name. It paid for itself: the red it surfaced was real and is fixed.",
        "Merged origin/main twice although the round could have been changeset-only, both times on a STALE TREE reading from dispatch-gates rather than on preference; declared above with the files each warning named.",
        "The pre-existing suppressWarnings phrase on PERMISSION_RETIRED_LIFECYCLE_RESIDUE (src/index.ts) is knowingly LEFT FALSE for its own rule — not made false by this change, and fixing it would widen the diff past both cards.",
        "⚠️ A gap in my FIRST report, named rather than quietly closed: it recorded the three exit-3 families as the NOT MEASURED set and did not name the 7 workflow-valued families the derivation also excludes. One of those seven is the gate that later went red. Both reports' gate counts were correct; the first one's account of what was NOT covered was incomplete."
      ]
    }

    Generated by Claude Code

  6. os-warren commented on Sep 21, 2026

    @os-warren
    Collaborator

    LANDED — PR #19480 is in main as c9b23cd

    Closing record from seat domain:spec#2, session_01UDXER3sdqfeVYpEWZs5mZx. Both required readings taken; ⛔ neither the merged boolean alone nor head ancestry was accepted as proof. Same record on #19276.

    Reading 1 — queue membership (timeline). ready_for_review 2026-09-21T04:17:27Z → added_to_merge_queue 2026-09-21T04:18:32Z → removed_from_merge_queue 2026-09-21T04:48:14Z → merged 2026-09-21T04:48:15Z at c9b23cd06628 → closed. Through the queue; ⛔ not direct-merged, ⛔ not bypassed.

    Reading 2 — content on origin/main, after git fetch origin main. origin/main = c9b23cd, subject fix(lint): give the liveness walk a seam of its own, and report a ledger that could not be read (#19480). By content: LIVENESS_LEDGER_UNREADABLE reads 1 in packages/lint/src/index.ts (the barrel export #5648 forces) and 3 in packages/lint/src/lint-liveness-properties.ts; the corrected changeset sentence 「cannot be silenced per finding」 reads 1. ⭐ LIT CONTROL, same instrument, same file: the pre-existing sibling id PERMISSION_RETIRED_LIFECYCLE_RESIDUE also reads 1 in index.ts — so the instrument reads that barrel and discriminates between two rule ids; the counts above are readings, not a grep that matches anything.

    What it took: two rounds, two defects, each caught by a different instrument

    Round 1 delivered 58900e6c. The at-tier review FAILED it (5754684296) on one required ground: the changeset promised that suppressWarnings accepts this rule's slug 「like any other」 — a per-finding suppression path that does not exist. It ships to consumers as CHANGELOG.md, which is why one sentence is a FAIL. Everything else in ① came back Right on first-hand re-measurement.

    ⭐ The second defect the review missed, and the gate caught. The PR added 5 citations of #10262, a number that 404s on the board (⭐ controls: neighbours #10261 and #10263 both 200). The review saw it and graded it "Not a blocker", escalating it. That grading was wrong on the mechanics: Lint & Repo Gates is a required context, it enforces exactly this, and it went red. The dev and this seat reached that conclusion independently. The remedy stayed bounded — the gate judges only what a change ADDS, so the 5 added citations were rewritten into in-file position descriptions; ⛔ no number was guessed and ⛔ none of the base's 13 pre-existing citations was swept (re-verified at the passing head: test 10→10, rule 3→3).

    ⇒ Recorded because it generalises: the at-tier review is not a substitute for the gates, and the gates are not a substitute for it. This round each caught what the other missed.

    Round 2 delivered 308afc86 and passed at tier (5755305207, Served-tier: 62/62). Lint & Repo Gates — the check name that had no verdict at all on the first head (cancelled) and a red one in between — read success.

    Left open on purpose, ⛔ neither filed nor swept

    Both escalations were re-verified as still standing and were not acted on:

    1. os i18n check reaches authorWarnedProperties('translation') through computeI18nCoverage → collectExpectedEntries → authorWarnedTranslationGroups(), and that command imports no authoring rule (⭐ control: lint.ts:11 imports runAuthoringRules) ⇒ under --strict an unreadable translation.json still gates nothing, silently. Pre-existing, outside both cards.
    2. #10262 still 404s and the base's 13 citations remain. ⛔ No longer a gate matter (this PR adds none) — purely the question of which number was meant.

    Both are the maintainer's: whether either is owed a card is ⛔ not this seat's call, and ⛔ nothing was filed.

    Also left as-is by design: the pre-existing suppressWarnings house phrase on PERMISSION_RETIRED_LIFECYCLE_RESIDUE — this change does not make it false and fixing it would widen the diff past both cards; successor is that rule's owner.

    Label state

    pm:dispatched removed from both cards in the same act as this record (read-backs MATCH: bug, priority:p2, domain:spec). domain:spec, the grading and the type label stay — ownership is not state. GitHub closed both cards on the closing keywords, state_reason=completed.

    Reading taken 2026-09-21T04:50Z.


    Generated by Claude Code

  7. added 2 commits that reference this issue on Sep 28, 2026
    c9b23cd
    1f69917
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions