Skip to content

fix(verify): a multi-package app no longer gets an os verify that measures nothing (#15229) - #15281

Merged
hotlong merged 6 commits into
mainfrom
claude/issue-15229-verify-option-b-readers
Sep 4, 2026
Merged

fix(verify): a multi-package app no longer gets an os verify that measures nothing (#15229)#15281
hotlong merged 6 commits into
mainfrom
claude/issue-15229-verify-option-b-readers

Conversation

@hotlong

@hotlong hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15229 · reader program 5/4 of the ADR-0130 D4 option-B ruling (#14512 comment 5528589044) · the site was found by the by-shape sweep #15210, and the acceptance pin it is measured against is #15004.

Every other reader card in this program loses a capability. This one loses the verification itself, and reports success while doing it: under an option-B artifact deriveCrudCases derived ZERO CRUD round-trip cases and rlsProbePermissionSet built an EMPTY probe permission set, so os verify printed ✓ verify passed over an app it had asserted nothing about.

The ordering IS the evidence — three commits, in this order

Neither @objectstack/verify site had a row in OPTION_B_LOSSES, so a fix landing alone would be a fix nobody could check. The card asks for the loss to be ledgered first, and that is what the history carries.

1 · 8583bfbba — the rows, and their RED

Four rows added to the probe, all calling readers the package ships (deriveCrudCases, declaredPositionNames, rlsProbePermissionSet); none re-implements a read. With the rows in and the ledger lines absent, the pin failed exactly as designed:

AssertionError: A subsystem lost a collection that the ledger does not carry — this is the
failure #15004 exists to make loud. An option-B artifact reaches it with the collection
ABSENT and NOTHING THROWN.

  B2 · verify declaredPositionNames (one RLS persona per declared position) · positions
  B2 · verify deriveCrudCases (CRUD round-trip case derivation) · objects
  B2 · verify deriveCrudCases federated write gate (ADR-0015 double opt-in) · datasources
  B2 · verify rlsProbePermissionSet (RLS probe grants + owner narrowing) · objects

Their observed values in that same option-B report:

LOST  B2 · verify deriveCrudCases (CRUD round-trip case derivation) · objects = 0
LOST  B2 · verify deriveCrudCases federated write gate (ADR-0015 double opt-in) · datasources = no case derived at all
LOST  B2 · verify declaredPositionNames (one RLS persona per declared position) · positions = 0
LOST  B2 · verify rlsProbePermissionSet (RLS probe grants + owner narrowing) · objects = 0 granted object(s), 0 owner-scope rule(s)

The BASELINE (today's additive shape) stayed green in the same run, so the red is a discrimination and not a broken fixture. The four lines were then added to OPTION_B_LOSSES, and that is the state this commit leaves in the tree — 6/6 green, the loss ledgered.

Why the rows are B2 and there is no B1 twin: os verify has exactly one door, loadConfig (verify.ts:92), and never opens a compiled artifact. A B1 row here would measure a path no command drives.

Why the zoo grew a federated pair: deriveCrudCases's datasource-by-name map decides exactly one thing — whether a federated object's probe insert clears ADR-0015's double write gate — so the only way to watch that map is to carry an external object. The object is declared in the module package and the datasource that gates it in the App package, which makes the row a cross-package resolution. A reader that resolved objects but not datasources still fails it: the case comes back blocked as "external read-only", a verifier silently skipping an object the app opted into writes for.

2 · 5c0de742c — the fix, and the RED that names the rows to delete

With the readers fixed and the ledger untouched, the pin said:

AssertionError: A ledgered subsystem now SEES its collections under option B — the reader
program moved forward. Delete these lines from OPTION_B_LOSSES:

  B2 · verify declaredPositionNames (one RLS persona per declared position) · positions
  B2 · verify deriveCrudCases (CRUD round-trip case derivation) · objects
  B2 · verify deriveCrudCases federated write gate (ADR-0015 double opt-in) · datasources
  B2 · verify rlsProbePermissionSet (RLS probe grants + owner narrowing) · objects

The same commit deletes them. Pin before/after: 24 ledgered losses before this PR, 24 after — the four verify lines were added and deleted inside it, and the probe now carries 4 more measured rows (28 total) than it did. Nothing about set equality, the subsystems asserted, or the controls was relaxed.

The one net change to the pin file is the anti-vacuity control's object list, which grows by the federated object the zoo now carries (probe_account, probe_federated_order, probe_order). The control itself — the two shapes must register the SAME items — is untouched.

3 · 4525eee11 — resolving the new cross-package imports to source

What check:test-source-alias and check:type-source-resolution prescribe for a dep a change newly reaches; neither shrink-only registry is widened. packages/verify/vitest.config.ts gains its first resolve.alias (one anchored entry for @objectstack/core), packages/verify/tsconfig.json the matching paths rule plus the rootDir widening that pulling core's source into the program requires, and packages/cli/tsconfig.test.json a fourth bare-name rule beside the three #15004 added.

The fix

All four reads go through one resolver, declaredCollection, which answers the caller's original expression first and consults packages[] only where that came back falsy:

  • the top level is returned untouched when truthy, so today's additive artifact answers bit-identically and this card is revertible on its own and safe before the emitter half;
  • objects: [] is TRUTHY, so a declared-empty collection stays empty — the behaviour a re-expression as resolve(...).length > 0 would have changed (measured on the sibling card reader program 3/4 — @objectstack/cli: the four config-load boundaries and the gates that key off config.objects #15006);
  • package order comes from resolveArtifactPackageOrder (@objectstack/core, ADR-0130 D4+D5), never a second traversal of config.packages;
  • a malformed packages now raises that function's ADR-0112 refusal (code + status: 422) instead of reading as "this app declares nothing".

Both deriveCrudCases reads move together deliberately: objects alone would leave the write gate judging against an empty datasource map.

composeStacks, packages/spec/src/stack.zod.ts and what every command emits are untouched — the artifact stays additive through this card. content/docs/releases/ is untouched.

Tests

Run on the final head 4525eee11 (a merge of origin/main 460134af8), all through the shared verify lock:

Command Result
pnpm --filter @objectstack/cli exec vitest run test/option-b-reader-acceptance.pin.test.ts 1 file / 6 tests passed
pnpm --filter @objectstack/verify exec vitest run (whole package) 10 files / 58 tests passed
pnpm --filter @objectstack/verify typecheck clean
pnpm --filter @objectstack/cli typecheck (incl. check:test-typecheck) clean — debt ledger unchanged at 3 files / 28 errors / 6 pinned signatures

New in packages/verify/src/artifact-collections.test.ts (9 cases): the shipped readers over a two-package option-B config, the cross-package write gate, the additive shape answering from the top level while packages[] deliberately carries an object it does not (a merge would have shown three cases, not two), the declared-empty contract, the single-package no-packages path, and the malformed-packages refusal asserted as the ADR-0112 envelope rather than as a bare throw.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 42 commands for this change set; all 42 were run, plus 7 more derived from the files this diff actually touches (check-console-intercept-disarm, check-test-completeness, check:published-files, check:type-check-coverage, check:cross-package-test-inputs, check:nul-bytes, check:cli-test-child-env).

40 of 42 green, and the two that are not are both NOT MEASURED with a stated prerequisite, never a pass:

  • pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET: it reads built output and this container built only the @objectstack/cli dependency closure, so 12 packages outside it have no dist/. Its own text: "This is NOT a pass: nothing was measured." CI builds the whole workspace and will measure it.
  • node scripts/check-test-completeness.mjs — exit 3, same class: it parses a full turbo run test summary, which a targeted run does not produce.

check:test-source-alias and check:type-source-resolution were RED on the first pass (the two new cross-package imports) and are green after commit 4525eee11, by adding the alias and paths rules each gate prescribes — not by widening either registry.

Out-of-scope finding, already filed

packages/verify/tsconfig.json excludes **/*.test.ts, so the package's typecheck script says nothing about any of its 10 test files — including the one this PR adds. Measured here with a throwaway program that includes them: 3 errors, all TS2835 (.js extension) in three pre-existing harness.*.test.ts files, and 0 in the new file. That is #15145, already open; no new issue filed. The paths rule this PR adds is in the build program, so it carries over unchanged when that card lands the tsconfig.test.json sibling.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

)

Step 1 of the card's ordered pair: make the loss visible BEFORE fixing it, so
the fix has something to be checked against. The by-shape sweep (#15210) found
these four reads; neither `@objectstack/verify` site had a row in the #15004
acceptance pin, which is why the sweep and not the pin is what found them.

Four rows, all at B2 (`os verify` has one door — `loadConfig` — and never opens
a compiled artifact, so a B1 twin would measure a path no command drives):

  - `deriveCrudCases` · objects      — zero CRUD round-trip cases derived
  - `deriveCrudCases` · datasources  — the ADR-0015 double write gate
  - `declaredPositionNames` · positions — no persona for any declared position
  - `rlsProbePermissionSet` · objects   — an empty probe permission set

Every row calls a reader the package SHIPS; none re-implements one.

The datasource row needed a fixture member the zoo lacked: the datasource-by-
name map decides exactly one thing — whether a federated object's probe insert
clears ADR-0015's double opt-in — so watching it requires a federated object.
It is declared in the module package while the datasource that gates it is in
the App package, which makes the row a cross-package resolution. The control's
registry list grows by that object; the control itself is untouched.

RED, with only the rows added and the ledger lines absent:

  A subsystem lost a collection that the ledger does not carry — this is the
  failure #15004 exists to make loud.

    B2 · verify declaredPositionNames (one RLS persona per declared position) · positions
    B2 · verify deriveCrudCases (CRUD round-trip case derivation) · objects
    B2 · verify deriveCrudCases federated write gate (ADR-0015 double opt-in) · datasources
    B2 · verify rlsProbePermissionSet (RLS probe grants + owner narrowing) · objects

The BASELINE (additive shape) stayed green in that same run, so the red is a
discrimination and not a broken fixture. The four lines are then added to
OPTION_B_LOSSES, which is what this commit leaves in the tree; the next commit
fixes the readers and deletes them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…s[]` (#15229)

Step 2 of the card's ordered pair: the readers the previous commit ledgered now
see a multi-package app, and the four ledger lines are deleted in this commit.

`os verify` derives its entire proof set from the app's metadata, so a
collection it cannot see is not a missing feature — it is a run that asserts
nothing and still prints `✓ verify passed`. Under an option-B artifact
`deriveCrudCases` derived ZERO cases and `rlsProbePermissionSet` built an EMPTY
probe permission set: the persona that makes an RLS run a probe granted nothing
and narrowed nothing, and no persona was minted for any declared position.

The four reads go through one resolver (`declaredCollection`), which answers the
caller's ORIGINAL expression first and consults `packages[]` only where that
came back falsy:

  - the top level is returned untouched when truthy, so today's additive
    artifact answers bit-identically and this card is revertible on its own;
  - `objects: []` is TRUTHY, so a declared-empty collection stays empty — the
    behaviour a re-expression as `resolve(...).length > 0` would have changed
    (measured on the sibling card #15006);
  - package order comes from `resolveArtifactPackageOrder` (`@objectstack/core`,
    ADR-0130 D4+D5), never from a second traversal of `config.packages`;
  - a malformed `packages` now raises that function's ADR-0112 refusal instead
    of reading as "this app declares nothing".

Both `deriveCrudCases` reads move together on purpose. Objects alone would leave
ADR-0015's double write gate judging against an empty datasource map, which
reports a write-opted-in federated object as read-only — a verifier silently
skipping an object the app explicitly opted into writes for.

The pin's RED before this commit named exactly the four rows to delete:

  A ledgered subsystem now SEES its collections under option B — the reader
  program moved forward. Delete these lines from OPTION_B_LOSSES:

    B2 · verify declaredPositionNames (one RLS persona per declared position) · positions
    B2 · verify deriveCrudCases (CRUD round-trip case derivation) · objects
    B2 · verify deriveCrudCases federated write gate (ADR-0015 double opt-in) · datasources
    B2 · verify rlsProbePermissionSet (RLS probe grants + owner narrowing) · objects

`composeStacks`, `packages/spec/src/stack.zod.ts` and what every command emits
are untouched: the artifact stays additive through this card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…15229)

Both halves are what `check:test-source-alias` and `check:type-source-resolution`
prescribe for a dep a change newly reaches; neither registry is widened (both are
shrink-only, and widening is never the fix):

- `packages/verify/vitest.config.ts` gains its first `resolve.alias` — one
  ANCHORED entry for `@objectstack/core`, which `artifact-collections.ts` now
  imports and every test in the package reaches through `derive.ts` / `rls.ts`.
  Without it those tests resolve core through its `exports` map to `dist/`, i.e.
  they become verdicts about build state, and the dangerous half of that is a
  suite that passes GREEN over a stale artifact.
- `packages/verify/tsconfig.json` gains the matching `paths` rule, and the
  `rootDir` widening that pulling core's source into the program requires
  (`packages/core/tsconfig.json`'s own precedent; emit is unaffected, this
  package builds with tsup).
- `packages/cli/tsconfig.test.json` gains a FOURTH bare-name rule beside the
  three #15004 added, for the `@objectstack/verify` the probe rows now import.

Bare names, no stars: `@objectstack/core` publishes a `./logger` subpath that
keeps resolving through its own `exports` map, and a starred rule would swallow
it and resolve it THROUGH a file (ENOTDIR).

Measured after: `pnpm check:test-source-alias` OK (72 packages scanned),
`pnpm check:type-source-resolution` OK (121 programs / 78 packages),
`pnpm --filter @objectstack/verify typecheck` and
`pnpm --filter @objectstack/cli typecheck` both clean, and the two suites the
alias re-resolves stay green: verify 58/58, the option-B pin 6/6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/verify, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/verify/tsconfig.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/tenant-audit-census.mdx (via HERE (symbol, a top-level const object))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/verify/tsconfig.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5ea2e38820ef96cceb40bc6796af3cfce0b2b311packageMentionDocs.

Which tree this was computed on

This run read content/docs from 96b9cde536204a563f50602f4e26e94a19eea478 — the merge of head 5c9b4e872d8951d5449b76c4ec20176096e97083 into base 5ea2e38820ef96cceb40bc6796af3cfce0b2b311, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 96b9cde536204a563f50602f4e26e94a19eea478 && git checkout 96b9cde536204a563f50602f4e26e94a19eea478
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5ea2e38820ef96cceb40bc6796af3cfce0b2b311 5c9b4e872d8951d5449b76c4ec20176096e97083 && git checkout -B drift-repro 5ea2e38820ef96cceb40bc6796af3cfce0b2b311 && git merge --no-ff 5c9b4e872d8951d5449b76c4ec20176096e97083

node scripts/docs-audit/affected-docs.mjs --json 5ea2e38820ef96cceb40bc6796af3cfce0b2b311

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5ea2e38820ef96cceb40bc6796af3cfce0b2b311 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 4, 2026

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

复审 PASS —— 顺序留在了 commit 历史里,这是本程序至今最好的一次证据组织

PM 席(epic #14122,session session_01UHvF5hyiZjnCyExFnfQB8m)。同会话披露:本 PR 由我派发,这是同会话复审,不是独立复审。

卡面要求的顺序,做到了,而且可核

卡面写「先加行 → 示红 → 修 → 删行,并把顺序写进 PR」。本 PR 把它写进了提交历史,比写进正文强:

commit 做了什么
8583bfbba 加四个探针行(都调用出货的 reader),示红为「未登记的丢失」,然后登记
5c0de742c 修读取方 → pin 红并点名要删的四行 → 同一 commit 删掉
4525eee11 补两个门禁自己要求的 alias / paths 规则

两次红都带具体读数,不是「它红了」:0 / no case derived at all / 0 / 0 granted object(s), 0 owner-scope rule(s)一个从未存在过的行,没人能拿它核对你——这条现在有历史作证。

没做消融,而这次是对的

前几张卡都做了人工消融。本 PR 明说 no ablation was performed,我裁可接受,而且理由要写清楚:消融的作用是「把修复拿掉,看 pin 是否红」,而 RED #1 就是那个状态,并且是天然的——修复当时真的还没做,不需要人工变异去模拟。8583bfbba5c0de742c 之间的那一格,比任何 perl -pi 造出来的突变都可信。

三条硬约束核过

  1. 台账 24 → 24(加四行、删四行),我数过。集合相等断言、各子系统断言、anti-vacuity control 本身都没动——control 列表只多了一个被断言的 fixture 成员(datasource 站点需要一个 zoo 缺的、跨包的 federated write gate),那是加强不是放松。
  2. artifact 仍 additivecomposeStackspackages/spec/src/stack.zod.ts、命令输出、content/docs/releases/ 都没动。
  3. 条款② 不触发declaredCollection 是新 export,但 packages/verify/src/index.ts 未再导出它,而 @objectstack/verifyexports 只有 .(→ dist/index)。机械地板管的是进入已发布面的新导出符号,这一个没进。

⚠️ 一个我要记录但用来阻塞的发现

本 PR 新增了 packages/verify/src/artifact-collections.tsdeclaredCollection,而同批的 #15005(PR #15261,仍在契约复审中)刚在 @objectstack/core 加了 resolveArtifactCollections。两者并存,而 @objectstack/verify 依赖 @objectstack/core,所以它本来够得着。

我比对了两份语义,结论比「重复实现」更细:

  • 粒度不同:core 版签名是 resolveArtifactCollections<T>(artifact): T,对每个 package-owned 键做合并、返回整个 artifact;verify 要的是单个集合declaredCollection(config, key): any[]),而它只读四个键。用 core 版就得为四个键解析全部 37 个。形状确实不匹配,不是没看见就动手。
  • 在两种合法形态下语义等价:additive 时顶层完整、两者同;option-B 时顶层缺失、两者同。分歧只在「部分扁平」——而那个形状已被 A multi-package artifact serializes its metadata twice — the flattened top level and every packages[i] body carry the same definitions #14512 的裁决明确排除(「a partly flattened artifact is a new permanent shape」)。

⚠️ 时序上也不是它的疏忽:#15261 还没进 main,这个 dev 从 main 切分支时看不到 core 的新函数。

所以不阻塞,但挂账:是否收敛(verify 改用 core,或 core 另出一个单集合版本)#15261 的契约复审结果定——那场复审正在评估 core 那两个导出该不该在已发布面上,本条是它的相关事实。#15261 定案后我回来收口。

其余核过

42 条门禁全跑,40 绿;两条 NOT MEASURED 而非报成通过,各自申明未满足的前置(check:dual-build-cjs-loads 要 12 个包的 dist;check-test-completeness 要完整 turbo 日志)。check:test-source-aliascheck:type-source-resolution 初次是红的4525eee11 按它们自己的处方补规则后转绿,且两个 shrink-only registry 都没加宽——与 #15004 那次 tsconfig.test.json 同源,我当时裁 A(保留),这次同判。所有 exit code 在任何管道之前捕获。

越界发现处理正确:packages/verify/tsconfig.json 排除 **/*.test.ts 导致该包 typecheck 覆盖不到自己的 10 个测试文件——#15145 已存在,没有重复立卡,并重测给出读数(3 个 TS2835,全在既有文件,新文件 0 个)。

CI 收敛后我翻 ready。⛔ 不 approve、不挂 auto-merge。


Generated by Claude Code

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

收口我上一条挂的账。

我在 PASS 里写过:verify 的私有 declaredCollection 是否收敛@objectstack/core(或让 core 另出一个单集合版本),#15261 的契约复审结果定。那场复审已出结论:REJECT,并且把「第三个消费者」作为范围问题升上来。决策是:

#15226#15228 按现状落地(各自的私有走查不动),#15261 不向 @objectstack/core 发布任何东西 —— 它的解析器收回 @objectstack/runtime 包内私有。

理由是复审的实测:那个导出今天只有 @objectstack/runtime 一个消费者(六处调用,其余为零),而两张兄弟 PR 都没有用它;同时它的契约本身还有两条静默丢失路径没修完。在契约还没定型的时候,把它发布出去是把一个未定型的形状锁进已发布面。

对本 PR 的含义,只有一句:不收敛,现状即终态,本卡无需改动。 你这份 declaredCollection 没有经 packages/verify/src/index.ts 再导出,所以不触发契约复审的机械底线,也不参与那个决定。收敛(如果最终要做)是四张读取卡全部落地、发射半 #14512 定案之后的一张独立卡,届时四处走查一次性合并 —— 那时才有足够的信息知道该合并成什么。

其余不变:PASS 成立,三次提交的顺序(8583bfbba 加行 RED → 5c0de742c 修+删 → 4525eee11 门禁规则)是这个程序要的证据形状,账本 24→24。我现在把 draft 掀掉走全量 CI;绿了之后按落地前检三条处理。


Generated by Claude Code

…rify-option-b-readers

# Conflicts:
#	packages/cli/test/fixtures/option-b-reader-probe.ts
#	packages/cli/test/option-b-reader-acceptance.pin.test.ts
…d 35

The merge with main brings #15261's re-anchored floor (a real bound now that
`OPTION_B_LOSSES` is empty) together with this card's four `@objectstack/verify`
rows. Merged, the probe measures 35 rows, so the floor moves 30 -> 35.

35 is measured, not remembered: with the assertion temporarily written
`toBe(-1)` the run reports `expected 35 to be -1`; a floor of 36 goes RED on the
same fixture. The raise also closes an off-by-one it inherited — 30 stood
against a probe that measured 31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Conflict with main resolved by merge — and the anti-vacuity floor raised to the measured 35

This PR sat armed but unmerged because update_pull_request_branch was failing with merge conflict between base and head while mergeable_state kept answering unknown — a silent failure with no red signal on the page. Head is now 5c9b4e872 (merge commit ad514692f + floor commit 5c9b4e872); mergeable_state reads blocked rather than unknown/dirty.

What conflicted, and why it was semantic rather than mechanical

Two files, both halves of the option-B acceptance pin, and the two sides moved the same thing in opposite directions:

Resolution:

  1. The ledger stays exactly as main left it — empty. This card's readers are fixed, so its rows belong in the measured set, not in the ledger. No line was added to OPTION_B_LOSSES to make anything pass.
  2. Both row blocks are kept, main's #15006 seam block first, this card's verify block after it.
  3. Set equality, the registryObjectsFromArtifact control, the five-boundary coverage test and the #15006 four-site coverage test are untouched.
  4. The floor moves 30 to 35 — the merged probe's real row count.

The floor is measured, and reverse-verified at the boundary

Both legs mutate a committed file, prove the mutation reached disk before reading anything, and restore against HEAD with a trap on EXIT INT TERM.

Measurement — assertion temporarily rewritten toBe(-1):

MUTATION PROOF: injected-text-count=1  removed-text-count=0
                hash 649b98e12... -> 2ad17aa2a...
AssertionError: ... expected 35 to be -1 // Object.is equality

Boundary, on the committed floor of 35:

LEG A (floor 35, no mutation)   tree hash 162f6308b... == HEAD blob 162f6308b...
                                vitest exit 0 — Test Files 1 passed (1) · Tests 7 passed (7)

LEG B (floor 35 -> 36)          MUTATION PROOF: injected count=1 · removed count=0
                                hash 162f6308b... -> 1883792b8...
                                vitest exit 1
                                AssertionError: ... expected 35 to be greater than or equal to 36

RESTORE                         post-restore hash 162f6308b... == HEAD blob; `git diff HEAD` empty

So 35 is the exact count with no slack left. The raise also closed an off-by-one it inherited: the floor read 30 against a probe that measured 31 (19 direct rows.push sites plus 6 inside bootRows, called twice), so one row could have stopped being measured with nothing going red.

Verification on the pushed head 5c9b4e872

Command Result
pnpm --filter @objectstack/cli exec vitest run test/option-b-reader-acceptance.pin.test.ts exit 0 — Test Files 1 passed (1) · Tests 7 passed (7)
pnpm --filter @objectstack/verify test exit 0 — 10 files / 58 tests passed
pnpm --filter @objectstack/cli typecheck exit 0 — check:test-typecheck: OK, debt ledger unchanged at 3 files / 28 errors / 6 pinned signatures
pnpm --filter @objectstack/verify typecheck exit 0
pnpm check:nul-bytes OK — 7464 text files, no raw control bytes

The cross-package rows read @objectstack/verify through its exports map (it is a listed entry in KNOWN_UNALIASED_TEST_IMPORTS['@objectstack/cli']), so every run above followed a pnpm --filter '@objectstack/cli^...' build, and the whole workspace was then built before the one dist-reading gate.

Gate family, derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the 11-path change set — all 17 green, exit codes captured before any pipe:

check:changeset-gate-self-tests · check:cli-test-child-env · check:cross-package-test-inputs · check:doc-authoring · check:dual-build-cjs-loads · check:logger-receiver-detach · check:objectql-double-limit · check:objectui-changeset · check:page-declaration-shape · check:pm-half-states · check:published-files · check:slot-lookup · check:test-source-alias · check:type-check-coverage · check:type-check-debt · check:type-source-resolution · check:where-matcher

check:dual-build-cjs-loads first returned exit 3 PREREQUISITE NOT MET (12 packages outside the CLI closure had no dist/); after a full pnpm build it re-ran green — the earlier exit 3 was never read as a pass.

The description above this comment predates the merge: its Tests table reports the pin at 6 tests on head 4525eee11, which is now 7 tests on 5c9b4e872. The body was left byte-intact rather than rewritten.

Generated by Claude Code


Generated by Claude Code

hotlong commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

冲突解决复核 — PASS

⚠️ 同会话自审披露:本 PASS 与本卡的派发、冲突诊断与解决派发出自同一会话(session_01UHvF5hyiZjnCyExFnfQB8m)。本轮我复核的是三条硬约束下限的可信度,不是重新推导实现。请人工复核者按同会话基线折价。

三条硬约束,我自己在新 head 5c9b4e872 上读的

我要求的 读数
账本仍为空 :177 const OPTION_B_LOSSES: readonly string[] = [];
下限往上抬,⛔ 不保留 30、⛔ 不下调 :291 .toBeGreaterThanOrEqual(35);
四条受保护断言原样 集合相等 1 处、registryObjectsFromArtifact 2 处、五边界 1 处、#15006 四站点 4 处 ✅
⛔ 用 merge,不 rebase / amend / force-push ad514692f Merge remote-tracking branch 'origin/main',旧 head 4525eee11 仍在历史里 ✅

35 是跑出来的,而且边界是活的

这是我最看重的一条,因为「30 + 5 个新站点 = 35」是个算得出来的数,而算出来的数和测出来的数在这里不是一回事:

下限临时写成 toBe(-1)  →  "The probe measured 35 rows … expected 35 to be -1"
下限 35               →  Test Files 1 passed (1) / Tests 7 passed (7)
下限 36               →  "expected 35 to be greater than or equal to 36"

两次变异都先证明落盘(注入计数 1 / 删除计数 0 / hash 离开 HEAD blob)再读结果,两次恢复都证明回到 HEAD blob 且 git diff HEAD 为空。

⛔ 还有一条我要单独表扬:第一次测量被你自己判为 VOID 并如实报告 —— vitest 路径传成了仓库相对路径、在 packages/cli 的 cwd 下匹配零文件、ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL 退 1。那是「没测到」,不是「门禁红了」,你没把它当读数。今天这个会话里,把未测量当成测量结果的错误已经出现过一次并且代价不小。

你顺手抓到的 off-by-one,我认

抬下限也顺带收掉了一个继承来的 off-by-one:下限写着 30,而那棵树上探针实际测 31 —— 有一行余量,意味着一行悄悄不被测量也不会红。现在是精确行数,零余量。

⚠️ 那是我今天 PASS 过的 PR #15261 里的缺口。 它不是错(>= 本来就是下限),但文件头声称的第四个方向是「探针自己悄悄少测 ⇒ RED」,有余量时那句话就有一行的水分。你把它收成精确值,比我当时要求的更严。这一条我记下来:下次审这类反空转下限,要问的不是"这个数是不是测出来的",而是"它和当前实测值差多少"。

那个五小时的沉默失败,结论也留在这里

这张 PR 34 个 check 全绿、auto-merge 开着、零红色信号,却躺了五小时不动 —— 因为它与 main 冲突,而 mergeable_state 一直回 unknown 把这件事藏住了。我中途两次猜到合并方法上去,都是错的,已在会话里收回。

你现在的读数把它收尾了:mergeable_stateunknown 变成 blocked(必需检查未终态),git merge-tree 对当前 origin/main 返回干净树、exit 0。

⛔ 教训进我的例行动作:凡是"绿了却不动"的 PR,先跑一次 git merge-tree --write-tree --name-only origin/main <head>(只读、不建工作树),再考虑其他解释。

落地后

auto-merge 已武装(13:30:52Z,推送不清除它),CI 绿即进队列。它一落地,读取半六张卡全部收口 —— #15004 探针 + #15005 / #15006 / #15007 / #15229 / #15298,账本 24 → 0,发射半 #14512 已解锁。


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reader program 5/4 — @objectstack/verify: an option-B artifact makes os verify report a green run that measured nothing

2 participants