Skip to content

feat(spec): pin every export by its .d.ts declaration text, and retire the 27 signature hashes - #18971

Merged
os-elon-musk merged 4 commits into
mainfrom
claude/issue-16045-declaration-text-snapshots
Sep 18, 2026
Merged

os-elon-musk merged 4 commits into
mainfrom
claude/issue-16045-declaration-text-snapshots

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #16045

Clause-②: yes (widening)

Ruled at 5560224701 (director batch #60, 2026-09-06, maintainer verbatim 「同意」), re-affirmed by triage at 5724532096: option A, a readable declaration-text snapshot, ⛔ not a hash. The card body's three mutually exclusive routes predate that ruling and were not re-litigated here.

@objectstack/spec pinned its public surface on one axis. api-surface/ records each export as name (kind), and a signature change, a renamed interface field and a dropped union member move none of those rows. The only shape pin was api-surface-signatures.json: 27 rows, and reference-level even there. This adds api-surface-declarations/, the declaration text the packed build actually emits for every export of every published entry point, and retires the 27 hashes it subsumes.

The counts, re-derived on this head before the first generation

The ruling asks for this by name; the card's own numbers were self-declared unverified and 12 days old.

Number Card This head (b33898f5d) Unit, and what would make it something else
entry points 17 17 type entry points in the exports map — those whose require.types ends in .d.ts. Adding or removing one such subpath.
exports map entries (not stated) 19 every key in the map. The extra two are ./openapi.json and ./package.json — asset subpaths with no declaration at all, filtered out by the same .d.ts test build-api-surface.ts has always applied. ⇒ premise 1 resolved: 17 is right and the map did not grow; 19 counts two things that were never entry points.
pinned rows 5309 5336 name (kind) rows summed over the 17 api-surface/ shards. +27 since the card. Ratio unmoved: 27/5336 = 0.51%, so the headline 99.5% stands.
distinct exported names (not stated) 5200 (entry, name) pairs. The gap to 5336 is dual-declared names, which are two rows by design.
signature hashes 27 27 top-level keys of api-surface-signatures.json. Bright control: the first value really is a sha256: string, so this counts signature entries and not empty objects.

Premise 3 also holds: all 17 packed .d.ts files exist and resolve through the map (3,215,437 bytes for the root entry down to 13,081 for ./integration). No entry point lacks a packed declaration, so the gap the dispatch reserved for itself did not open.

What the artefact costs — premise 4, which nobody had costed

shards 17, one per entry point
declaration blocks 5336
bytes 12,661,943 (12.08 MiB)
lines 237,706
gzipped 1,071,825 (1.02 MiB) — against this package's ~17.57 MiB compressed dist, so about +5.8% of tarball
largest shard system.txt, 3,592,701 bytes / 73,283 lines
median declaration 81 bytes
skew the 20 largest declarations hold ~65% of all bytes; four exceed 20,000 lines each (EnvironmentArtifactSchema 21,868, ObjectStackDefinitionSchema and ObjectStackSchema 21,851, ChangeSetSchema 20,395)

Stated plainly, as the dispatch asks, and ⛔ not as a veto: the packed .d.ts is a tsup dts rollup, so a Zod schema's declaration is its fully expanded structural type. That expansion is exactly what makes an inner field rename visible — and it is also why a single schema can produce a 21,000-line diff. The ruling's stated reason for choosing text over a hash is that the contract-review seat reads the diff; that reasoning holds per declaration and is worth a second look at the top twenty. One reading, for whoever wants it: 31% of declarations hold 97.7% of the bytes, so nothing cheap is available by trimming the tail.

Both instruments, measured on one tree at one commit

The card's thesis is that the old pin cannot fail on a shape change. Not argued — ablated, with the mutation proven on disk by blob hash and the mutation proven to have reached dist/ before any verdict was read.

A. the source-level control — a renamed interface field, the card's own class. JobRunOutcome.reason? renamed to degradationReason? in packages/spec/src/contracts/job-service.ts (blob 363443e2 to d4b1520c), spec rebuilt, ablation-dist-preflight exit 0 confirming the marker reached the built artefact:

check:api-surface              exit=0    "public API surface unchanged"      [BLIND]
check:api-surface-declarations exit=1    "~ JobRunOutcome (interface)"       [SEES IT]

Restore leg: blob back to 363443e2, rebuilt, ablation-dist-preflight --absent exit 0 (marker gone from all 214 built files), git diff HEAD clean, gate back to exit 0.

B. the gate can fail on its own artefact. One field renamed inside qa.txt by hand (blob 3f5efb04 to 5b86fec2, injected occurrences 1, deleted text 0): exit 1, attributed to TestSuiteSchema (const), failure text naming the regenerate command. Restored to the HEAD blob, git diff HEAD empty: exit 0.

The retirement, and the coverage proof the ruling demands

All 27 signature names resolve to a declaration block in api-surface-declarations/root.txt, 0 missing — enumerated from defineAction through defineWebhook, each as (function).

One honest qualification, because the subsumption is not uniform. For those 27 factory declarations the text is declare function defineAction(config: z.input of ActionSchema): ActionParsed; — a type reference, exactly as blind to an inner-key narrowing as typeToString was. What is gained is not sharper text on the 27; it is the 5309 other declarations, including ActionSchema itself, whose own expanded block is where such a narrowing shows up. So the retirement is a strict superset of pinned declarations, not an equal trade. Nothing published read the retired file — it was never in this package's files[].

Where it lands, and why there

  • Generator: packages/spec/scripts/build-api-surface-declarations.ts, beside the eight sibling artefact generators, reading the same input through the same collectEntries logic. The ruling says "one generator script under scripts/"; this reads that as the directory the whole family lives in, because the artefact reads the built dist and only the lane that builds spec can run its gate.
  • Artefact: packages/spec/api-surface-declarations/ENTRY.txt, a sibling directory of api-surface/. Not inside it: listShardNames throws on any file in that directory that is not a NAME.json shard, so api-surface/ is closed by construction. No existing api-surface/*.json is regenerated by this PR (check:api-surface green throughout), which keeps it clear of PR feat(spec)!: a structured region body refuses a pause-capable node and an 'end' node #18688 and PR feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319.
  • Gate: check:api-surface-declarations, a step in lint.yml's Type Check · consumer gates lane after the two build steps, with check:api-surface and the other dist-reading gates. No new required context — a step in an existing lane. Registered in the check:generated ledger, in REGEN_ARTIFACTS, and in .gitattributes as merge=os-regen.
  • Sharded per entry point from day one, for the reason its neighbour is: the merge queue rebuilds server-side where no custom driver runs, so two PRs sharing one generated file evict the second. Pit 1 from 5715457322 is answered by the layout rather than by an assumption — and check:merge-driver, which reconciles .gitattributes against REGEN_ARTIFACTS in both directions, is green over the swap.
  • Published, with the reason the gate demands. check:published-files refuses a files[] entry that carries none; the registered line says what a consumer does with it — read two published tarballs and see which declared shape moved between releases, the question api-surface cannot answer. If 1.02 MiB of tarball is judged too much, one line of files[] removes it without touching anything else.

Three registries had to learn about the new gate, each because it discovered the gate on its own rather than because a list named it:

  • check:published-files — demanded the reason above.
  • scripts/pm/dispatch-gates.mjs — its live manifest edge gave the new gate a population before anything listed it, which is the eighth member of a class whose seventh was recorded the same way. Declared as CLASS_EIGHTH, with a case asserting the edge really reaches it.
  • scripts/pm/check-widening-tells.mjs — PUBLISHED_SURFACES is derived from REGEN_ARTIFACTS, so retiring the signatures row dropped it off that surface and reddened two self-test cases. Both are retargeted to state the retirement as a counterfactual (the surface follows the table, not a literal); ⛔ the new artefact is not added to that surface, because the ruling assigns "is a snapshot diff a Clause-② signal" to the skills seat by name and out of this card's scope. Both directions are now pinned, so the boundary is declared rather than forgotten. 483 cases pass, up from 481.

Verification

  • Gate families: derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the merge base, 120 commands, every exit code redirected to a file and read back. All 120 green. Four returned exit 3 PREREQUISITE NOT MET on first pass (check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt); each names a build, each was built and re-run green, and none is recorded as a finding. Reconciled with --ran.
  • Tests: @objectstack/spec local project 488 files / 14,182 tests passed; the tooling suites that name the edited scripts, both projects, 10 files / 220 tests passed (sharded-artifacts, check-generated-ledger, dist-freshness, dist-freshness-adoption, api-surface-dual-kind-rows.pin, build-schemas-check-mode, def-key-collisions, root-index, export-list, docs-import-surface). pnpm --filter @objectstack/spec typecheck green.
  • eslint, the union rather than a narrowing: eslint . --no-inline-config --format json at b33898f5d examined 6856 files, 0 errors, 0 warnings, exit 0. The population is eslint's own config resolution and the count is read from its JSON output; type-aware linting is not enabled in eslint.config.mjs (no parserOptions.project, no typed rules), so this diff cannot move an untouched file's verdict either way.
  • Control bytes: check:nul-bytes green over 8906 files, plus a direct scan of all 31 changed paths for the wider control-byte class — no matches.
  • scripts/check-single-claim-paths.mjs in the diffstat is not mine: it arrived with the one-commit origin/main merge (16cb493d5) this PR carries.

Acceptance notes

维护者速读(草稿)

改了什么。 @objectstack/spec 从今天起为它的每一个公开导出留一份"形状快照" —— 不是哈希,而是打包后 .d.ts 里那段声明原文,按入口点分成 17 个文件签入仓库,并配一道 CI 闸门:重新生成后对不上就红,失败信息里直接给出重新生成的命令。同时退休了旧的 27 条签名哈希文件。

为什么改。 原来的 pin 只记"某个名字还在不在",5336 行里只有 27 行能看出"形状变没变"。也就是说:把一个接口字段改名、砍掉一个联合成员、改一个函数签名 —— 这些都是会让客户升级后编译失败的破坏性改动 —— 全部一路绿灯。本次 PR 里有实测:改了 JobRunOutcome 的一个字段名之后,旧闸门 check:api-surface 退出码 0(看不见),新闸门退出码 1(点名了那个 interface)。路线是 2026-09-06 决策批次 #60 里您逐字「同意」的那一条。

风险与代价(含回滚)。 代价是体积:12.08 MiB 文本、23.7 万行,压缩后 1.02 MiB,相当于 npm 包增长约 5.8%。更值得注意的是分布极不均匀 —— 最大的 4 个 schema 各自超过 2 万行声明文本,一旦它们变动,复核席位面对的是一份 2 万行的 diff;而裁决选"文本不选哈希"的理由恰恰是"diff 可读"。这一点我按实测如实报告,未自行改动路线。回滚成本很低:从 files[] 去掉一行即可停止随包发布;整道闸门回滚就是撤销本 PR,不留任何数据迁移。

席位意见。

你要做的。 只有一件事需要您判断:12 MiB / 23.7 万行这个量级,以及最大 4 个 schema 的 diff 可读性,是否仍符合当初选 A 方案时的预期。若认为需要收窄,那是裁决层面的一次增补,不是本 PR 的返工。其余部分已按裁决落地并自证。


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 19 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/ai.txt, packages/spec/api-surface-declarations/api.txt, packages/spec/api-surface-declarations/automation.txt, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 19 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/ai.txt, packages/spec/api-surface-declarations/api.txt, packages/spec/api-surface-declarations/automation.txt, …) — pages documenting those are invisible to this run
  • 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 — 136 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 89c6ec52b56a24d94c5bdea57fb8a7a8db4273da → packageMentionDocs.

@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Test Core (1/6) failed on this head — measured, and the single re-run is being held deliberately

Failing check: Test Core (1/6), head b33898f5d8d0dc8eff732eb8ac26452506b8622e, job 105540309254, read 2026-09-18T0854Z.

The failure itself, from the job log:

FAIL packages/qa/vitest-filter-preflight/test/config-wiring-sweep.test.ts
     > packages/cli/vitest.config.ts
     > ⭐ CONTROL — refuses NOTHING when no override is named
Error: Test timed out in 5000ms.   (test/config-wiring-sweep.test.ts:306:3)
Test Files  1 failed | 2 passed (3)
     Tests  1 failed | 110 passed (111)

Why this is very likely not this pull request's

Measured on the diff rather than assumed:

  • The 31 changed files contain 0 files under packages/qa/**, and 0 changed paths matching vitest. Lit control on the same instrument: 23 changed files under packages/spec/, so the zero is a real absence and not a dead query.
  • The failing subject is packages/cli/vitest.config.ts, which this diff does not touch either.
  • The failure is a 5000ms timeout on a test that spawns a real vitest child process. In the SAME run, the same-named ⭐ CONTROL leg passed for other configs at 2098ms and 1442ms — so this family habitually runs at roughly 30–40% of its own limit, and a loaded runner crossing 5s is the timing shape rather than a behavioural change.
  • 110 of 111 tests in that package passed.

Two grounds I checked and did NOT get to rely on, stated so this is not read as hand-waving

  • Not red on the base branch. On main tip 89c6ec52b5, Test Core (1/6) is success, and main has 0 failing checks. The "red on base too" limb does not hold here.
  • It did not pass earlier on this exact commit. There is exactly one run of that check name on this head. The names with repeated runs on this head are Auto Label, Check PR Size, Packed-tarball smoke (opt-in) and Check Changeset — not Test Core.

So the only remaining ground for "not this PR's" is that it reproduces identically on one re-run, and that re-run has not been spent.

Why the re-run is being held rather than fired now

At the time of reading, 16 checks were still in progress on this head. The standing rule allows at most one re-run in total, so spending it on a single job while other jobs could still fail would waste it. It is held until the run converges, then spent once across the whole failure set.

⛔ Not done and not on the table: skipping, disabling or quarantining the test; an empty commit or a close/reopen to kick CI; editing the check.

One more reading worth recording

The same job log shows @objectstack/spec:test was scheduled but never reached — "the run stopped before it" — along with 10 other packages (check-test-completeness: OK (1 of 12 scheduled package(s) reported … 11 never reached)). This shard therefore has not yet exercised this pull request's own package at all, so it is no evidence either way about the change itself.

What happens next

  • Re-run reproduces identically ⇒ the failure is not this PR's by the standing definition, and the timeout gets its own card rather than being ignored: whoever finds a flake fixes it or files it, never routes around it.
  • Re-run passes ⇒ flake confirmed on a timing-sensitive subprocess test, and it still gets that card, because a green re-run is not a root cause.

Either way this PR is not landed on a red head, and the contract review for it is proceeding separately.


Generated by Claude Code

os-elon-musk commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 88/88 CONTRACT_REVIEW_TIER
Head-sha: b33898f5d8d0dc8eff732eb8ac26452506b8622e

Tier was verified from the reviewing subagent's OWN transcript, not from its self-report: 88 per-turn model stamps read, all 88 equal to the constant above, zero stamps of any other value. A subagent cannot self-attest, because its get_session reads the parent session rather than itself. This seat serves below that constant, which is why the review was routed to an isolated subagent instead of being done in seat.

① Derived judgments

The ruling (5560224701, director batch #60, maintainer verbatim 「同意」) required a readable declaration-text snapshot of every export of every published entry point, ⛔ not a hash. Independently re-measured by the reviewer against the branch blobs:

  • Text, not a hash. First-token histogram over all 5,336 blocks: type 2,376 · declare const 2,076 · declare function 447 · interface 430 · declare class 7; 0 blocks fail to start with a declaration keyword, 0 (other)/(namespace) kinds, 0 inline import(...) types. No digest anywhere in the artefact or the check path — the verdict is a byte compare of regenerated against on-disk shard text.
  • Coverage is total and symmetric. Per-shard set comparison of the api-surface JSON rows against the api-surface-declarations text markers, shard by shard: all 17/17 shards show only_json = 0 and only_txt = 0; totals 5,336 rows = 5,336 blocks. collectEntries() in the new generator is byte-identical to build-api-surface.ts's, so both artefacts agree on the 17 entry points and exclude the two asset subpaths identically.
  • The retirement drops no signal — the load-bearing claim, re-verified rather than accepted. All 27 signature-hash entries of the deleted api-surface-signatures.json (read from origin/main) were checked against the branch's root.txt: 27 hit, 0 miss, each block's first line carrying the full declare function signature. Lit controls: the fixture's fake name defineWorkflow → 0; defineAction sought in the wrong shard → 0; total markers in root.txt → 213 (must-hit). The new instrument additionally covers 8 define* functions outside root that the old root-only, functions-only hash never saw.
  • Both directions of the new gate. The retired instrument hashed checker.typeToString(...) — reference-level, root only, functions only. The new one records the same 27 nodes' declaration text, which strictly contains that signature information, so anything that moved the old hash moves the new block. Ablation A (source rename JobRunOutcome.reason? → degradationReason?, rebuilt, both gates on the same dist) tests exactly the card's thesis class through the real pipeline and shows the OLD gate blind (exit 0) while the new one reds naming ~ JobRunOutcome (interface); ablation B (hand-edited shard) tests the byte compare and the per-block attribution.
  • Classes missed by BOTH the old hashes and the new snapshot, named rather than glossed: runtime-only zod narrowing (.min(), .regex(), .refine()) leaves the printed TS type unchanged and belongs to the authorable-surface/dropped-refinements axis; values behind a widened declared type; function-body behaviour; and deliberately, leading TSDoc wording. None is a regression introduced by this retirement.
  • Reproducibility across machines. CI's own Type Check · consumer gates step "Check @objectstack/spec declaration text (the shape half)" passed on this head after CI's build steps, i.e. the checked-in shards byte-matched a dist built on a GitHub runner.

② Semver level

@objectstack/spec minor. Under the launch-window rule a purely additive widening of a published package's public surface takes at least minor, and major is refused outside pre-mode (scripts/check-changeset-no-major.mjs), so minor is both floor and ceiling here. The PR body's line-initial Clause-②: yes (widening) and the changeset's backticked form were traced through the one shared parser (readClause2Line, imported by both check-changeset-no-major.mjs and check-adr-0087-registration.mjs): both read declared / yes / widening. Check Changeset is success on this head.

③ Boundary flags

  • Clause-② arm: widening, declared line-initial on both carriers, which agree.
  • Governed surface: none — check-governed-merges.mjs --pr 18971 derived 31 paths three-dot and hit 0 of 5 surfaces, exit 0. Ordinary queue landing applies; no human approval is owed for this PR.
  • Release-owned surface: untouched — 0 files under content/docs/releases/, control 23 under packages/spec/.
  • Generated-artefact registration: both the .gitattributes merge=os-regen row and regen-artifacts.mjs's REGEN_ARTIFACTS row were swapped from the retired file to the new directory, with a check-generated.ts GATED ledger row; 30 residual mentions of the deleted file are all prose, CHANGELOG history, negative test cases or two acknowledged stale comments.
  • ⚠️ Publication exceeds the ruling's letter. The ruling says "a checked-in snapshot beside api-surface/*.json" and says nothing about files[] or the npm tarball; the one files[] line is what makes this a Clause-② event at all. Reviewer measurement added on top: nothing in-repo reads a published api-surface-declarations/ today (release-spec-changes.sh reads only package/api-surface), so the +1.02 MiB gzipped (+5.8% of tarball) ships for a consumer that does not yet exist. This is escalated to the maintainer as an open option (drop the one files[] line) and is not resolved by this PASS.
  • ⚠️ The ruling's own follow-up is not in place. PUBLISHED_SURFACES is REGEN_ARTIFACTS.filter(row => row.check === 'check:api-surface'), so the new artefact is off that surface by construction and nothing yet turns a snapshot diff into a Clause-② carrier automatically. The ruling assigned that input row to the skills seat by name and out of this card's scope; filed as ruling 5560224701 assigned a contract-review input row naming the snapshot diff as a Clause-② signal, and nothing carries it #18976 so the assignment has a carrier.
  • ⚠️ One claim in this PR over-reaches its mechanism. Header, changeset and docblock all say a declaration's leading TSDoc is excluded. True of the LEADING comment only: member-level JSDoc inside interface/object bodies IS recorded — 403 blocks contain /**, 8,853 comment lines, 3.7% of the artefact. Consequence once the snapshot becomes a Clause-② signal: doc-only edits will red the gate and summon contract review. Non-blocking, but the claim should be amended or member comments stripped.
  • Artefact-stability facts for the record: 237,723 lines but only 40,806 unique (5.83× expansion); top 20 blocks hold 64.5% of bytes; four blocks exceed 20,000 lines; 24% of blocks print zod-internal names, so a zod or TypeScript emitter upgrade can rewrite a quarter of the artefact with zero source change.

Carrier clear

Both carriers are cleared on the strength of this record, in the protocol's order — record first, then the strip, so the gate is never open-with-nothing-behind-it: PR #18971 and card #16045. ⚠️ Correction to this record, made by its author. As first posted, this paragraph cited a comment id for "the full reviewer report". That id does not exist on this pull request: the isolated reviewer returned its report to the dispatching seat as an in-session hand-back, which is not a GitHub artefact, and the id was mistakenly written as though it were one. No such comment was ever posted here. The three comments on this pull request are the docs-drift check, the Test Core (1/6) note, and this record. This record is therefore the durable carrier of the review, and every reading quoted above is reproduced here rather than pointed at.

The reviewer's per-item verdicts, for audit:

item verdict
1 Faithfulness to the ruling PASS
2 Retirement of the 27 hashes PASS (27 hit / 0 miss, 3 lit controls)
3 New gate catches both directions PASS
4 Widening arm, semver level, carrier consistency PASS WITH FINDINGS
5a Generator location PASS, declared-and-sound
5b Published in files[] PASS with the finding above
5c Not on PUBLISHED_SURFACES PASS — the ruling says so verbatim
5d Three contended files PASS — holder #18948 already merged, merge-tree 0 conflicts
6 Artefact size reading, no verdict — maintainer's disposition
7 Generated-artefact registration PASS
8 Release-owned surface PASS
9 Adversarial read of the generator PASS with findings

Reviewer-declared instrument failures, carried rather than hidden: the Actions job-logs endpoint returned 403 through the proxy, so the new gate's CI result was bound through the job's steps array instead of log text; a first statistics script crashed after printing its top-20 table, so the redundancy and overload figures come from a corrected second run over the same extracted blobs; a noisy "identifiers referenced without a block" heuristic was discarded and used for no verdict; the gzip figure differs from the author's by under 4% by method; and with no dist/ in the shared checkout, the packed .d.ts could not be inspected for non-exported helper types, which is stated as CANNOT-DETERMINE and is load-bearing for nothing above.

CI on this head at 2026-09-18T0924Z: 31 success, 5 skipped, 0 failures, all converged, every run bound to this head. The earlier Test Core (1/6) failure was a timing flake in packages/qa/vitest-filter-preflight — a package this diff does not touch (0 of 31 files; lit control 23 under packages/spec/) — and its re-run passed at 09:22:36Z. It is filed separately rather than waved through.

Implemented-by: claude/issue-16045-declaration-text-snapshots
Reviewed-by: session_019srGWGCBBCBHqcDoRZpQRh

VERDICT: PASS


Generated by Claude Code

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 09:27
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit d8b12fc Sep 18, 2026
64 of 66 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-16045-declaration-text-snapshots branch September 18, 2026 09:53
os-elon-musk pushed a commit that referenced this pull request Sep 18, 2026
…/main

The declaration-text pins main brought in #18971 move with this branch's two
facts: `source` becomes required wherever the predicate contract composes, and
`cel` / `expression` now declare the `EvaluatedExpression` they always emitted.
Exactly 8 distinct changed lines across all six files; the line count is those
two facts repeated at every composing site.

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

Copy link
Copy Markdown
Collaborator Author

Maintainer ruling C — this PR is being reverted (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T11:39Z

Recorded verbatim on #16045 (the card this PR closed): the maintainer read this PR's size and review trail with the skills seat and chose, of three options, 「C」 — revert, and make consumer compilation against spec@main the shape gate. Carriers: the revert is objectstack #19011 (domain:spec, p1; git revert d8b12fca9 applies cleanly on main, 31 files, 119 / 238,310; it lands by the maintainer's hand under the new 5,000-line human-merge rule, #19012); objectui's compile gate is objectui #9860. ⛔ Nothing here is a finding against the dev or the spec seat: the ruling this PR executed (5560224701) was the maintainer's, and the reversal is too.


Generated by Claude Code

@os-elon-musk
os-elon-musk restored the claude/issue-16045-declaration-text-snapshots branch September 18, 2026 12:01
os-steve pushed a commit that referenced this pull request Sep 20, 2026
…napshot

Resolves 13 modify/delete conflicts under packages/spec/api-surface-declarations/.

Every conflict has the same shape: this branch deletes the file (no stage 2),
main regenerated it (stage 3). Retiring that directory is the revert's whole
purpose, so each conflict resolves to the delete. All 17 shards are gone from
the merged tree -- the 4 main did not touch auto-resolved to delete already.
The one other overlapping path, scripts/pm/dispatch-gates.mjs, auto-merged:
main's hunk sits about 1600 lines from the reverted one.

Verified on the merged tree rather than assumed:

- no code, script, workflow, gitattributes or package.json entry references
  api-surface-declarations in any spelling; the only three mentions left are
  historical prose in .changeset release notes (17108, 18991, 19085), reported
  separately and deliberately not edited here.
- of the 31 paths the reverted commit touched, none still carries a line that
  commit added; the four that differ from its parent are later, unrelated work
  main landed (lint.yml keeps #18889's step; check-published-files,
  dispatch-gates and regen-artifacts carry post-revert commits).
- api-surface-signatures.json is back with its 27 hashes and, built from these
  merged sources, check:api-surface reports the public API surface and factory
  signatures unchanged -- so the restored pin is correct, not merely present.
- check:generated reports all 15 artifacts up to date; main's count is 16, and
  16 is what #18971 made it when it registered check:api-surface-declarations.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
github-actions Bot deleted the claude/issue-16045-declaration-text-snapshots branch September 28, 2026 04:53
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… against both tarballs (objectstack-ai#18889)

Fixes objectstack-ai#17080

Clause-②: yes (widening)

Ruled **A** (issue comment 5643392133, decision batch objectstack-ai#119 item 2,
maintainer 「同意」 2026-09-12):
minors ship real, machine-readable change data, with a correctness gate.
All four execution
clauses land here.

## The defect, re-measured from the published tarballs

`npm pack @objectstack/spec@17.3.0 @objectstack/spec@17.4.0`, then the
repo's own export-surface
row convention (`ENTRY: NAME (KIND)` — entry point, export name, export
kind — the spelling
`build-spec-changes.ts` flattens to):

| | 17.3.0 | 17.4.0 |
|---|---|---|
| export-surface rows | 5259 | 5433 |
| **true delta** | — | **225 added, 51 removed** |
| `spec-changes.json` → `aggregate.added` / `.removed` | 0 / 0 | **0 /
0** |
| `protocolVersion` | 17.0.0 | 17.0.0 |
| `perMajor[16→17]` | converted 58, migrated 77 | converted 57, migrated
77 |
| `[REMOVED]` prescriptions in `json-schema/**` | 156 | 221 |

Every figure the card reports reproduced. The shipped manifest answered
`0 / 0` over a release
that moved 276 exports, and a consumer reading semver sees a minor.

## What lands

**1 · A per-release section, shipped in the tarball.**
`spec-changes.json` gains `release` —
`fromVersion` → `toVersion` at package-version resolution, with `added`
/ `removed` (the exports
that arrived and left, each named) and `converted` / `migrated` (the
ADR-0087 D2/D3 entries first
registered in that release). Generated at **publish time only**, from
the previously published
tarball: the committed copy stays the deterministic registry projection
and `check:spec-changes`
is green against it, which is how the determinism concern is answered
rather than traded away.

**2 · The correctness gate, as acceptance.** Before anything reaches
npm, the release lane packs
the artifact it is about to publish and recomputes the delta from **the
two tarballs**, with its
own reader — `scripts/check-release-spec-changes.mjs` deliberately does
not import the generator's
flattening, because an instrument that shares the code it audits reports
agreement with itself.
A mismatch fails the release, naming the disagreeing exports and the
**direction** of each
disagreement (claimed-but-not-real, real-but-unclaimed, per array) plus
the command that
regenerates the section. A held release arrives with its own diagnosis.

**3 · `os validate --json` reads the same data.** New key
`specReleaseChanges` — `fromVersion`,
`toVersion`, the four counts and the file it read. It is a **sibling**
of `protocolVersionGap`,
not a widening of it: that key means "the platform on disk is outside
the range you declared",
and a consumer gating CI on it must not start failing because an
ordinary minor shipped exports.
One key, one question. (Note `specVersionGap` is spelled
`protocolVersionGap` in this tree — it
was renamed by objectstack-ai#14261, which is still an unreleased changeset; the
ruling's clause 3 names the
old spelling.)

**4 · Published payload change.** `Clause-②: yes (widening)`,
contract-review carrier, changeset
carrying the migration-free declaration, and
`content/docs/upgrading.mdx` gains *What the
installed artifact tells you, without a second worktree*.

## Absence is not zero

The section is **omitted, loudly**, when the previous tarball ships no
export snapshot or no
manifest — an empty `release` is indistinguishable from "this release
changed nothing", which is
the misreading the whole section exists to end. The gate derives the
same condition from the same
artifacts, accepts that absence, and **refuses** a section that is
present when it could not have
been computed. `specReleaseChanges` is `null` in exactly those cases.

## Verification

Real artifacts, not fixtures — `npm pack` of published 17.3.0, `pnpm
pack` of this tree:

```
generate --previous-package (the unpacked published 17.3.0)  ->  pack  ->  gate
✓ release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated.
```

⚠️ **Corrected by the `domain:spec` seat** — this line read 「374 added」
when the PR opened. `origin/main` has since moved the export surface,
and **386** is the number measured at the current head `1737a24b510`
from a real `npm pack` of the published 17.3.0, independently confirmed
by the at-tier review at the previous head. `os-dev.md:56` reserves this
body to the PR-open write, so the round named the number and the seat
writes it.

**Ablation, on the real artifact.** One export dropped from
`release.added` in the packed
manifest (mutation proven on disk: sha256 `bc21927e…` → `3bbff365…`),
restored under
`trap … EXIT INT TERM`, hash verified equal after:

```
exit 1
✗ the per-release section of spec-changes.json disagrees with the two tarballs (ADR-0087 D4).
  A wrong change file is worse than none — a consumer gates its upgrade on this data.

  release.added: 1 export(s) the two tarballs show as added and the section OMITS:
        ./ai: BUILD_PROGRESS_FRAME_TYPE (const)
  Regenerate with: pnpm --filter @objectstack/spec exec tsx scripts/build-spec-changes.ts
  --previous-package PREV_PACKAGE_DIR      [the real line spells the placeholder in angle brackets]
```

Gates and tests, exit codes captured by redirect:

- `pnpm check:release-spec-changes` — 15 batteries (roster + floor +
verdict handshake), exit 0.
The real run needs two published tarballs, so the self-test is what a PR
can run; it drives the
  same `verifyRelease()` the release lane calls.
- `pnpm --filter @objectstack/spec check:generated` — **all 16 artifacts
up to date** after
`gen:api-surface` + `gen:export-origins` +
`gen:api-surface-declarations` (7 new declarations on
  the root entry, 0 removed).

⚠️ **Corrected by the `domain:spec` seat: this read 「all 15」 when the PR
opened.** The count moved
because objectstack-ai#18971 (`d8b12fca97c`) registered
`check:api-surface-declarations` as a new generated
artifact family. The gate now prints 「Checking 16 generated artifacts」.
⛔ The 15 was not wrong
when written — the base moved under it. `os-dev.md:56` reserves this
body to the PR-open write, so
  the round named the number and the seat writes it.
- `pnpm --filter @objectstack/spec exec vitest run --project local
src/migrations/migrations.test.ts`
  — 137 passed.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
src/utils/spec-release-changes.test.ts`
  — 6 passed.
- `pnpm --filter @objectstack/spec typecheck` — exit 0. `pnpm --filter
@objectstack/cli typecheck`
— exit 0 (after building the runtime closure the CLI's test project
reads; its first run reported
only `TS2307 Cannot find module` for packages this worktree had not
built).
- `pnpm check:entry-guard` — exit 0 (265 files, 205 exporters inert on
import). This one caught a
real defect in the new gate: it exports `verifyRelease` and dispatched
at the top level, so
  importing it would have run it. Fixed with `isEntrypoint`.
- **Gate families**, derived by `scripts/pm/dispatch-gates.mjs` from the
diff and reconciled with
`--ran`: **153 derived, 147 run** (146 exit 0), **4 NOT MEASURED**
(`check:i18n`,
`check:i18n-coverage`, `check:i18n-walk-parity`,
`check:dual-build-cjs-loads` — each exits 3,
PREREQUISITE NOT MET, wanting a full repo build), **2 unrun**
(`check:pm-dispatch-gates`, whose
own header forbids running it in an agent container's foreground;
`check:type-check-debt`,
repo-wide `tsc`, killed by this runner's timeout). `check:spec-changes`
and
`check:skill-examples` deserve a word each: the first is **green**,
which is the determinism
half of clause 1; the second exits 1 saying *"packages/client-react/dist
holds no .d.ts — the
package is not built"*, so it is NOT MEASURED, not a finding. None of
the six touches this
  diff's subject, and CI builds fresh.

## Acceptance notes

- **A withdrawn conversion is not reported by `release.converted`.**
Between 17.3.0 and 17.4.0
`perMajor[16→17].converted` went 58 → 57:
`field-required-notnull-explicit` left the published
chain. The registry records that as a deliberate withdrawal (`⛔
WITHDRAWN — there is
deliberately NO field-required-notnull-explicit`), so this is documented
behaviour, not drift.
The section reports entries a release **added**, which is the four
arrays ADR-0087 D4 names;
an id that disappeared is visible only by comparing two published
manifests, and the code says
  so rather than implying otherwise.
- **The tombstone trap the card names is not closed by this change, and
this change does not
claim to close it.** Of the 65 new `[REMOVED]` prescriptions between
17.3.0 and 17.4.0, exactly
one names 17.4.0 as the retiring release. `release.removed` answers
"which exports left in this
release" exactly; it says nothing about which *prescriptions* were
written in it, because the
prescription text carries no retirement version in data. A consumer
still cannot tell
「retired in X」 from 「prescription written in Y」 from the tombstones
alone.
- The card's *"every conversion entry carries `toMajor: 17`"* holds
inside `perMajor[16→17]`; the
aggregate carries `toMajor` values 11, 13, 14, 15 and 17. The substance
— no resolution finer
  than a major — reproduced.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ON clause is derived (objectstack-ai#18938)

Fixes objectstack-ai#18612

Clause-②: yes (narrowing)

Retires `sql` and `relationship` from `CubeJoin`. A cube join declares
WHICH object it
reaches; the ON clause is **derived** from the declared relationship
between the two cubes'
objects and is never authored. Per maintainer ruling `5725370783`
(director batch objectstack-ai#154 item 4,
letter 2), ADR-0049 enforce-or-remove. The other remedy — executing the
author's SQL — was
declined by that ruling and is ⛔ not reopened here.

## What this head carries — round 3 closed the one gap

The gap the earlier body described (`check:adr-0087-registration` RED on
purpose, and a fence on
`packages/spec/src/migrations/registry.ts`) is **gone**. The maintainer
answered that fork with
**A — lift the fence**, and the registration is now in-diff:

| what | where |
|:---|:---|
| D3 semantic entry `cube-join-sql-and-relationship-retired` |
`packages/spec/src/migrations/entries/semantic/18.*` |
| D2 conversion `cube-join-sql-and-relationship-removed` |
`packages/spec/src/conversions/registry.ts`, chained into
`step18.conversionIds` |
| `RETIRED_KEYS_BY_MAJOR[18]` gains `data/CubeJoin:sql` and
`data/CubeJoin:relationship` | the two per-file retired-key entries |
| the changeset's disposition marker | `<!-- adr-0087: registered
cube-join-sql-and-relationship-retired -->` |

`check:adr-0087-registration` and `check:migration-registry` are both
**exit 0** on this head.

Round 3 added three things beyond the registration:

- **The artifact at rest heals at the boot door.** All three doors in
`packages/metadata/src/plugin.ts` run `_convertArtifactForward` before
the strict parse, so a
cube persisted with the old `{ name, relationship, sql }` shape is
converted rather than
refused. Pinned by `analytics.test.ts` — *"a persisted cube heals at the
door"* — with its own
  lit control and the per-cube notice paths.
- **`name`'s describe now states the convention it always had**: the
join KEY is the foreign-key
  field on the cube's own object, and the emission is
  `LEFT JOIN <name> <key> ON <base>.<key> = <key>.id`.
- **The showcase join is re-keyed** `showcase_project` → `project`,
matching `task.object.ts`'s
`Field.masterDetail('showcase_project')`; `gap-fill.test.ts` pins every
join key against the
  base object's real field map rather than against a literal.

## The same measurement also chose the retirement ROUTE

The ruling says 「`retiredKey()` tombstones per the standing shape」.
`CubeJoinSchema` is a
`strictObject`, and for a strict shape AGENTS.md's standing shape is
**strict deletion plus a
`guidance` prescription**, not a `retiredKey()` tombstone — the route
`MetricSchema.filters`
took one shape over in this same file
(`packages/spec/src/migrations/entries/retired-keys/18.data__Metric__filters.ts`
states it in as many words). Measured both ways on this tree:

- `retiredKey()` tombstones: `check:authorable-surface` **exit 1** — *"2
key(s) were tombstoned
with no registered retirement"*, naming `data/CubeJoin:relationship` and
`data/CubeJoin:sql`
and demanding those exact lines in `RETIRED_KEYS_BY_MAJOR` (the fenced
file). Probe reverted;
  tree hash restored byte-identical to HEAD.
- guidance route: `check:authorable-surface` **exit 0**, adjudicating
the two baseline deletions
  under the objectstack-ai#4650 proof 4 it prints itself —
*"2 baseline deletion(s) since 84ba4a8 carry their own proof:
data/CubeJoin:relationship —
def reachable from the metadata-type roots; writing 'relationship' on it
is REFUSED as an
  unrecognized key"*, and the same for `sql`.
- ⏱️ Both readings above were taken by the dev in round 1 and re-taken
by the at-tier contract
review at head `e177aa2686`; this seat adopted that record at
2026-09-18T14:39Z
  (comment `5731599385`). ⛔ They are not this seat's own runs.

Either route needs the registration; it is now in-diff, in the table
above. The route choice is
independent of that registration, and is called out here so an at-tier
reviewer can overrule it cheaply.

## Acceptance legs, both readings

**LIT — an authored ON clause must be refused, in words a JSON author
reads**

| | `CubeJoinSchema.safeParse({ name: 'other', sql: 'a.id = b.a_id' })`
|
| --- | --- |
| before | **ACCEPTED** — parsed to
`{"name":"other","relationship":"many_to_one","sql":"a.id = b.a_id"}` |
| after | **REFUSED**, `unrecognized_keys`, message: *"…was removed in
@objectstack/spec 17 (ADR-0049 enforce-or-remove) — it never had an
effect… Delete the key. A cube join has no authorable ON clause: it is
DERIVED from the declared relationship between the two cubes' objects,
as a foreign-key equality."* |

**DARK — a join that declares only its object must still parse**

| | `CubeJoinSchema.safeParse({ name: 'other' })` |
| --- | --- |
| before | **REFUSED** — `sql` was required (`invalid_type` at path
`sql`) |
| after | **ACCEPTED** — `{"name":"other"}` |

**Alias leg — `{ on: 'x' }`, read once before and once after**

| | reading |
| --- | --- |
| before | ``Unrecognized key(s) on this cube join: `on`. Did you mean
`on` → `sql`?`` |
| after | ``Unrecognized key(s) on this cube join: `on`.`` followed by
the derivation prescription, and **no rename suggestion** |

`aliases: { on: 'sql' }` is deleted rather than left pointing at a
retired key: an alias naming a
key the shape cannot accept answers the author with a second rejection —
the `triggerPhrase`
failure `packages/spec/src/shared/strict-object.ts` records. `on` now
carries its own `guidance`
entry, and both directions are pinned.

## The census the ruling took, re-taken — and one correction

The ruling recorded 「authored cube `joins` in hotcrm, objectstack
examples and cloud — **0 files**」.
Re-measured first-hand on this tree, **objectstack is not 0**:

- `examples/app-showcase/src/data/analytics/showcase.cube.ts` authors
**both** keys, including
`sql: '${showcase_delivery}.project = ${showcase_project}.id'` — a live
instance of the defect,
  an ON clause the runtime was silently replacing. Fixed here.
- Seven more authoring sites in `packages/services/service-analytics`'s
own test fixtures, found
by `tsc` after the keys left `z.input`, not by grep. Two of them
authored
`relationship: 'belongsTo'` — a value the enum never declared, which is
its own evidence that
  nothing validated or read the key. All fixed here.

This does not move the ruling: those are in-repo producers, fixed in
this same diff, and they
are what the retirement checklist calls for. It does mean 「zero
producers ⇒ no conversion is
owed」 rests on the external census only, and that half was **not**
re-measurable from here
(hotcrm and cloud are other repositories).

Consumer census, with a lit control, on this tree:

- reads of a join's `sql` anywhere in source: **0**
- reads of a join's `relationship` anywhere in source: **0**
(`native-sql-strategy.ts` was checked
  by name: it does not read either)
- lit control, reads of a join's `name`: **8** across
`native-sql-strategy.ts`,
  `objectql-strategy.ts` and `analytics-service.ts`

## What else moved, and why

- `packages/services/service-analytics/src/dataset-compiler.ts`
**constructed** both keys per
join (a constant `'many_to_one'` and a synthesised ON string). The
literal now carries `name`
alone; `parentAlias`, which existed only to build that string, is gone.
No read site changes —
`analytics-service.ts:1178` still reads `name` only, exactly as the
ruling said.
- The liveness ledger rows went **with** the keys
(`packages/spec/liveness/analytics_cube.json`),
which is the strict-deletion route's disposition and the opposite of the
tombstone route's.
`analytics_cube` drops 12 `dead` to 10; `state-counts.md` regenerated,
README notes cell
  rewritten to describe the set it now has.
- `content/docs/references/data/analytics.mdx` is regenerated, not
hand-edited. The `CubeJoin`
table is now one row and its description states the derivation — which
is the docs half the
  ruling asked for.
- `packages/spec/src/data/analytics-strictness-batchd.test.ts` keeps its
batch-D pin that an
**undeclared** join key is refused by name; the fixture drops the two
now-retired spellings so
the pin isolates what it always pinned. Three new pins beside it cover
`sql`, `relationship`
  and `on`.

## Verification

Two readings, kept apart on purpose — one is the reviewer's, one is this
seat's.

**① At-tier contract review, taken at head `e177aa2686`**, adopted by
this seat at
2026-09-18T14:39Z (comment `5731599385`), run in its own detached
worktree (fresh
`pnpm install --frozen-lockfile`, heavy steps under
`scripts/pm/os-verify-lock.sh`, exit codes
captured before any pipe). All exit 0: spec `build` · `check:generated`
(*"All 15 generated
artifacts are up to date"*) · `check:authorable-surface` ·
`check:liveness` ·
`check:migration-registry` (*"225 semantic, 195 retired-key, 181
retired-def"*) ·
`check-adr-0087-registration` and `--self-test` ·
`check-changeset-no-major` ·
`check:spec-docblock-symbol-anchors` (*"3130 anchors across 1462 spec
sources resolve"*) · eslint
over the 11 changed source/test files · `@objectstack/spec test` **488
files / 14190 tests** ·
`@objectstack/service-analytics test` **112 files / 2403 tests** ·
showcase `gap-fill.test.ts`
**13 tests** · typecheck for spec, service-analytics and the showcase ·
`check:exported-any`,
`check:yaml-examples`, `check:dual-source-exports`,
`check:entry-nameability`,
`check:browser-reachable-entries`, `check:skill-examples`, `check:i18n`,
`check:i18n-coverage`,
`check:i18n-walk-parity`.

That review — same adoption, ⏱️ 2026-09-18T14:39Z — returned **FAIL on
one mechanical
blocker and nothing else**, not a judgment defect.
The REQUIRED context `TypeScript Type Check` was red at `e177aa2686`
because
`check:api-surface-declarations` landed on main at `d8b12fca97`,
**after** this branch's
merge-base, so the branch carried neither the gate nor
`packages/spec/api-surface-declarations/`.

**② This seat's own reading of the fix, taken from the GitHub API at
head `7caf92189a`**
(⏱️ 2026-09-18T14:59Z 取): commit `59bd587aea` merges `origin/main`, and
`7caf92189a`
regenerates the shards. The API reports that commit as
`{"total":30,"additions":0,"deletions":30}`
over exactly three files — `api-surface-declarations/data.txt` −12,
`root.txt` −12,
`system.txt` −6. A **pure deletion**: ⛔ not one line was added, so
nothing was hand-written into
a generated artefact. That is byte-for-byte the shape the review
predicted (each reshaped
declaration loses `sql: z.ZodString;` and the `relationship` enum block,
propagated by type
inlining).

CI at this head, ⏱️ 2026-09-18T14:59Z 取: **0 failing check runs out of
33**.
`Build Core`, `Dogfood Regression Gate`, `Temporal Conformance (live PG
+ MySQL)` and
`Governed Surface Queue Guard` are success; `Lint & Repo Gates` is in
progress;
`TypeScript Type Check` and `Test Core` have not reported yet. ⛔
Not-yet-reported is **not**
passing, and this PR is not landed on that basis.

⛔ Not a complete account of what CI runs here: the 50 artifact-roster
families, the 11 declared
wide-population families, the 6 path-scheduled CI jobs and the
always-runs tail each sit outside
any derived total above. Not measured anywhere: repo-wide `pnpm test` /
`pnpm typecheck`,
`check:dual-build-cjs-loads`, and the external hotcrm / cloud census
(other repositories).

**⚠️ Landing-order note, so nobody is surprised.** PR objectstack-ai#19024 (the
maintainer's, `priority:p1`)
reverts objectstack-ai#18971 and **deletes all 17 declaration shards**. Whichever of
the two lands second must
merge the other first; if objectstack-ai#19024 goes in ahead of this PR, the
regeneration commit above becomes
moot and its three files disappear with the rest of the snapshot. ⛔ That
is a mechanical merge,
not a defect in either diff.

## Acceptance notes

Noted, not filed — observations, no card:

- `packages/spec/liveness/analytics_cube.json` still records `public` as
an access-control flag
that gates nothing and `refreshKey.every` / `refreshKey.sql` as a
caching block with no
scheduler. Both are already recorded there with their measurements;
ADR-0049 wants a decision
on each, and neither is this card. Successor: whoever picks up the
`analytics_cube` ledger's
  remaining `dead` rows.
- `AnalyticsQueryRequestSchema` reaches `CubeJoinSchema` only through
`CubeSchema`, so no REST
  request surface changes. Successor: none.


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ank `source` (objectstack-ai#18638)

Fixes objectstack-ai#15811

Clause-②: yes

## Rework round — the seat's three items, measured on the rework head
(⚠️ NOT the current head — see 〈Base catch-up〉 below)

Seat verdict
objectstack-ai#18638 (comment)
(REWORK on a PASSed contract review). Three items, nothing else
re-opened.

### 1. The changeset and the ADR-0087 entry said something this diff
makes false

Both claimed the three union-member positions leave their sibling arm
untouched. **Re-measured here**, base `00115a8442` vs head, parsing each
value AS MOUNTED through `TraceSamplingConfigSchema`:

| position | sibling arm | base | head |
|---|---|---|---|
| `RecordAlertProps.visible` | `z.boolean()` | `true` / `false` accepted
| identical |
| `ServiceLevelIndicator.successCriteria` | structured `{ threshold,
operator, percentile? }` | accepted, **including an object carrying a
`dialect` key** | identical |
| `TraceSamplingConfig.composite[].condition` | `z.record(z.string(),
z.unknown())` | see below | **narrowed** |

At the tracing slot, six shapes the base accepted **through that arm
alone** — measured: the base's `ExpressionInputSchema` refuses all six,
so the record arm was the only thing admitting them — are refused at
head:

| authored `condition` | base | head |
|---|---|---|
| `{ dialect: 'cel' }` | accepted | refused |
| `{ dialect: 'js', source: 'x' }` | accepted | refused |
| `{ dialect: 'nope', source: 'x' }` | accepted | refused |
| `{ dialect: 'cel', source: 5 }` | accepted | refused |
| `{ dialect: 'cel', source: 'x', meta: { rationale: 5 } }` | accepted |
refused |
| `{ dialect: 'zzz', foo: 1 }` | accepted | refused |

Control that HITS: a structured filter carrying no `dialect` key — `{}`,
`{ service: 'api' }`, `{ attributes: { 'http.route': '/v1/orders' } }` —
is accepted at base and at head alike. Without it the six `refused`s
would be a schema that refuses everything.

⭐ The narrowing is correct and load-bearing (it is what makes the ruled
change non-inert at that slot) and is **not** removed. What changed is
the **description**: the changeset now carries the table and its FROM →
TO, and the migration entry's `surface` and `acceptanceCriteria` both
name the wider sweep that slot needs — flag every `condition` object
carrying a `dialect` key, not only the two spellings. A changeset
becomes the CHANGELOG and an ADR-0087 entry becomes the migration
ledger; neither may ship a false sentence.

### 2. The published reference page

`.refine()` has **no JSON Schema projection** — measured against zod
4.4.3: `z.toJSONSchema` returns byte-identical output for the plain
record, the refined record and the aborting refined record
(`{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}`).
So regenerating alone could never move that TYPE cell, and hand-editing
the page is forbidden and would be reverted. The fix is at source: the
slot's `.describe()` now states the rule, and `gen:docs` republishes it.
`content/docs/references/system/tracing.mdx` now reads:

> Condition for this strategy — a structured filter object, or a CEL
predicate an engine evaluates. ⚠️ The two are told apart by the
`dialect` key: a structured filter must NOT carry one, and an object
that does is judged as an expression — so it needs a dialect this
platform evaluates and a non-blank `source` … `{ dialect: 'cel', ast: …
}` with no `source` is refused here.

⚠️ The type cell still renders that arm as a plain record of string to
any, and that is **faithful to the JSON Schema this repo publishes** —
which is itself wider than the zod schema, for every `.refine()` in the
spec, not only this one. Making the page contradict the artifact beside
it would be worse. Reported as an out-of-scope finding rather than
repaired here.

### 3. The two unpinned message cells — the fix was in the schema, not
only in a test

Measured at the slot, on head as it arrived:

- `{ dialect: 'cel', source: '' }` → one top-level `invalid_union` with
the bare **`Invalid input`**; the published sentence appeared only
inside nested arm issues;
- `{ dialect: 'js', source: 'x' }` → refused with the **「needs a
non-blank `source`」** sentence, which misnames the fault: that value's
`source` is fine, its dialect is not.

Root cause, measured: zod 4.4 reports the ONE arm that did not abort,
else `invalid_union`. The record arm's `.refine()` was **non-aborting**,
so it was the surviving arm for every expression-shaped refusal here and
answered for all of them — and it answered with the other arm's
sentence.

⇒ The repair is in the schema, not only in a test: the refine becomes
**aborting**, and its message becomes the arm's own rule (module-local,
⛔ not a new published export). Ablation of the accept set: the refused
set is **identical** with and without `abort` — both measured over the
ten-value corpus above, so this is a message change and not a second
narrowing. After it, the slot answers exactly what the other 35 answer,
and exactly what the migration entry's own acceptance criteria promise:

| authored `condition` | before | after |
|---|---|---|
| `{ dialect: 'cel', source: '' }` / `' '` | `invalid_union` @ slot,
`Invalid input` | **one `custom` issue @ `…condition.source`, the
published sentence** |
| `{ dialect: 'cel', ast: … }` | `custom` @ slot, published sentence |
`invalid_union` @ slot, published sentence |
| `''` / `' '` (bare) | `invalid_union` @ slot, published sentence |
unchanged |
| `{ dialect: 'js', source: 'x' }` | `custom` @ slot, **published
`source` sentence** | `invalid_union` @ slot, `Invalid input` — no
longer blames `source` |

Pins, in `packages/spec/src/system/tracing.test.ts`: the accept set (six
refusals + the accepting control), both blank spellings' published
sentence and its exact `code`/`path`, the `ast`-only and bare-string
cells, the **negative** (a non-`source` fault is not answered with the
`source` sentence), and the `.describe()` the reference page renders.
And `evaluated-slot-population.test.ts`'s published-sentence pin now
runs all **three** refused spellings at all 36 positions instead of only
the `ast`-only one — 108 cases, all green. That is what would have
caught this slot in the first place.

### Not re-opened

⚠️ **`Clause-②` is now `yes`, re-declared by the seat under ruling A
(batch objectstack-ai#155 item 3, `5725503887`, maintainer 「同意」 2026-09-18T05:13Z).**
The 28-input strict-subtype measurement is NOT overturned — the
conclusion drawn from it is: 「a member replaced on a key line is a
change to a published contract … the same review a narrowing owes
**regardless of the tell**」. ⇒ a false tell was never the question; a
narrowing owes the at-tier review on its own. `minor` + BREAKING banner
+ ADR-0087 disposition stay. `printCelAst`, the package-internal helper
and the 36-position census stay. `packages/spec/api-surface/shared.json`
and `export-origins/shared.json` are still **hash-identical to base**
(`git hash-object`: `cf260910f1…` / `0429ff67a6…`), and `git diff --stat
00115a8..HEAD -- packages/spec/api-surface
packages/spec/export-origins` is empty.

### Gates, re-derived on the rework head (⚠️ two figures superseded —
see the note under it)

`node scripts/pm/dispatch-gates.mjs --commands` on the merged head,
every exit code recorded as it ran, reconciled with `--ran`: **110
derived, 104 run, 6 NOT MEASURED, 0 unrun** (`--ran` exit 0). `pnpm
--filter @objectstack/spec build && test && typecheck` green — 486 files
/ 14016 tests; `@objectstack/formula` 30 files / 871 tests, typecheck
green. `check:generated`: all 15 artifacts up to date after the
`origin/main` merge and the final rebuild.

⚠️ **Two figures in the paragraph above are readings on an EARLIER
head.** They are pinned here, ⛔ not restated as current and ⛔ not
retyped.

- The `110 derived, 104 run, 6 NOT MEASURED, 0 unrun` reconciliation was
taken on `e892c86e271`. The head is now `34a63b9d583`. ⛔ It is not
re-derived here — read it as the reading it was.
- **`all 15 artifacts` is superseded.** objectstack-ai#18971 registered a new
generated-artifact family and the registered count is now **16**.
Measured: the `GATED` array in
`packages/spec/scripts/check-generated.ts` carries **16** entries at
`70407326463d`, at `e892c86e271` and at `34a63b9d583` alike — with a
dark control on a non-existent array name extracting 0 lines — so the
`15` predates all three heads rather than describing any of them. The
`15` is left above verbatim as the historical reading.
- Current reading, on `34a63b9d583`: `check:generated` **exit 0 — all 16
generated artifacts up to date**, working tree clean.

### Base catch-up — `origin/main` merged, four deferred artifacts
regenerated

Merged `origin/main` `b14610255101` at `ab00016c221`, regenerated in
`34a63b9d583`. The `os-regen` driver **deferred** on four routed
both-sides paths — `api-surface-declarations/automation.txt`,
`data.txt`, `ui.txt` and `content/docs/references/ui/component.mdx` —
and ⭐ **a deferral silently keeps ONE side**: in the merge commit,
main's token reads **0** on all four while the branch's side is intact.
Main's side was restored and all four re-derived from the merged tree;
on the head each path carries **both** counts, against a positive
control that hits on every blob of every path (⛔ a control that fires on
one shard certifies one shard).

The two ⛔ MIXED paths are deliberately not routed to the driver and were
hand-resolved. `packages/spec/src/migrations/registry.ts`: generated
regions stripped, the hand-written remainder byte-identical across base,
both sides and the merge, line counts exactly additive (17142 + 121 + 74
= 17337), and **both** sides' migration entries present by id.
`packages/spec/src/ui/component.zod.ts`: additive text merge (3750 + 4 +
45 = 3799), its `.superRefine()` untouched.

Refinement census over non-test `packages/spec/src`, counted by
occurrence rather than by matching line: `.refine(` 59 base / 60 branch
/ 59 main / **60 head**; `.superRefine(` 80 at all four; `.check(` 4 at
all four. ⛔ Nothing deleted, nothing weakened.


Non-zero exits, all declared:

- six **exit 3 · PREREQUISITE NOT MET**
(`check:doc-formula-expressions`, `check:doc-security-posture`,
`check:docs-transcript-drift`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:type-check-debt`) — each refuses an
unbuilt workspace closure and says so. NOT MEASURED, not findings;
- `check:skill-examples` **exit 1** — the same class in exit-1 clothing
(`packages/client-react/dist` holds no `.d.ts`);
- `check:react-declaration-parity` **exit 1** — run as CI runs it,
`MANIFEST="$PWD/sdui.manifest.json" … --strict`: `111 spec-only
divergences, 1 blocks missing from the registry`. Control: the identical
command at base `00115a8442` prints **the same two numbers**, so it is
pre-existing and this PR moves neither.

One gate went red on this round's own work and is fixed:
`check:doc-authoring` refused an internal issue id in customer-facing
spec text — the `(objectstack-ai#15811)` this round put in the tracing `.describe()`.
Removed, page regenerated, gate green; the same gate at base is green,
so the id was the only offender.

`node scripts/pm/check-clause2-carriers.mjs --pair 18638` — **exit 4**,
and the dual-carrier row C1 is gone: only **C5** remains, with the same
two false tells (`ui/action.zod.ts:833` T2, `ui/component.zod.ts:1595`
T1). ⛔ Reported, not acted on; the matcher repair is objectstack-ai#18640's.

Rework round authored by the `domain:spec` execution seat, session
`session_01LvwGppdonww4zGLWZo5rho`.

Decision batch objectstack-ai#122 item 2 generalised the evaluated-slot rule:
`EvaluatedExpressionInputSchema` now composes into **every** slot an
engine evaluates, while `ExpressionSchema` / `ExpressionInputSchema`
stay the persistence contract (`source` OR `ast`) by item 2 of the same
ruling. An `ast`-only envelope and a `source` that is blank after
trimming — through the envelope key or the bare-string shorthand — are
refused at the door instead of parsing, registering, and faulting at run
time.

## The population was re-derived, not inherited

The census in the card is six days old and `shared/expression.zod.ts`
moved after the ruling, so the 36 figure was treated as a premise.
Re-derived by **identity** on this branch's base `00115a8442` — a
negative lookaround on identifier characters, because the bare substring
also fires inside `CronExpressionInputSchema`,
`TemplateExpressionInputSchema` and `EvaluatedExpressionInputSchema`,
which is the trap that inflated triage's own reading on this card (32
files, five of them Cron-only):

| reading | count |
|---|---|
| declaring source lines mounting the schema (non-test, non-comment) |
34 |
| of those, file-local alias consts mounting 2 slots each | 2 |
| **declaring positions** | **36** |
| lit control — identity hits in the definition file | 7 |
| the same file counted by bare SUBSTRING | 17 |
| dark control — `ZzzNoSuchSchema` | 0 |

That 7-versus-17 gap in one file is the trap itself, in miniature.
Identical to the measured census
(objectstack-ai#15811 (comment)),
position for position. Two aliases: `ui/action.zod.ts`
`ActionConditionInputSchema` (mounts `visible` + `disabled`) and
`system/settings-manifest.zod.ts` `SettingsVisibilityInputSchema`
(mounts the specifier and manifest `visible`). Three positions reach the
schema as a union member rather than head-of-declaration.

`PredicateInputSchema` is a plain alias of `ExpressionInputSchema` with
zero slot users; it stays wide with the schema it aliases.

## Two defects found while measuring, both fixed here

**1. The narrowing was INERT at
`TraceSamplingConfig.composite[].condition`.** That slot is
`z.union([z.record(z.string(), z.unknown()), …])`, and a bare record arm
accepts `{ dialect: 'cel', ast }` as an ordinary record — so swapping
the other arm changed nothing. Measured: after the swap and before this
fix the slot still answered `success: true` on the `ast`-only envelope,
while its 35 siblings answered `false`. The structured-filter arm now
declines an object carrying a `dialect` key, which is an expression
attempt whatever it got wrong. Shipping the swap alone would have been a
declared-but-unenforced narrowing.

**2. Four positions refused with zod's bare `Invalid input`.** Where the
declaration wraps the evaluated schema in a WIDER union — a boolean
beside it on `action.visible` / `action.disabled` /
`RecordAlertProps.visible`, a structured object beside it on
`ServiceLevelIndicator.successCriteria` — the outer union reports
`invalid_union` at the slot and the inner union's sentence never
surfaces. `evaluatedExpressionUnionRefusal` gives those unions the
published sentence. It is deliberately stricter than the inner map it
complements: it answers only for a blank string or an object carrying
`dialect`, so a malformed threshold object is not blamed on `source`. It
lives in `shared/evaluated-slot-union.ts`, **package-internal** and
absent from both barrels, on the `union-branch-policy` convention: a
narrowing PR that grows the published export surface widens on a second
axis, so `api-surface/` and `export-origins/` do not move for it.

## Item 3 — the printer path is real, and measured

The ruling asked for the lossless direction 「where the dialect has a
printer」 before falling back to a structured TODO. Measured rather than
assumed: `@marcbachmann/cel-js` ships `serialize`, and `cel-engine.ts`
already uses it for its own scope rewrites. So `@objectstack/formula`
gains **`printCelAst(ast)`**, the inverse of the existing
`parseCelToAst`, and the migration entry prescribes it by name instead
of describing a capability nobody can call.

Measured round-trip, six sources, each re-evaluated on the same scope:

```
record.amount > 10                              -> identical bytes
record.priority == 'urgent'                     -> record.priority == "urgent"
'org_admin' in current_user.positions           -> "org_admin" in current_user.positions
record.a == 1 && (record.b != 2 || record.c > 3)-> identical bytes
size(record.tags) > 0                           -> identical bytes
```

Lossless about MEANING, not bytes — the printer re-renders from the
parse tree, so quote style normalises. Dark controls, all four throwing
rather than inventing a source: `{}`, `null`, `{ type: 'nope' }` and a
plain string each raise `Unknown AST operation`. `printCelAst` converts
that into `null` and additionally requires the printed text to parse
back through the platform's own bounded `parseCelToAst`, so it can never
widen what this platform evaluates.

Where the printer answers `null`, and for every blank `source`, the
ADR-0087 D3 entry `evaluated-expression-slots-source-required` is the
structured TODO — naming the object, the field and the slot, and
splitting the judgment by fail policy, because removing a key is safe on
the fail-soft half of the population and a silent disclosure on the
fail-closed half.

**Why this is a D3 entry and not a D2 conversion, now that a printer
exists.** The conversion layer lives in `packages/spec`, which is
dependency-free by Prime Directive objectstack-ai#2 and carries no engine —
`packages/formula/src/normalize.ts` states the same boundary from the
other side. A conversion that had to call the CEL printer could not live
where conversions live, and one that guessed without a printer would be
the platform inventing a predicate.

## ⚠️ Deviation: graded `minor`, and the ruling said `major`

Item 3 ordered a 「`major` changeset」.
`scripts/check-changeset-no-major.mjs` forbids a `major` marker during
the launch window, because the fixed group versions in lockstep and one
`major` promotes all ~70 packages to a whole-stack major — which is a
release act reserved to the maintainer. The guard's own header names the
two carriers the convention uses instead, and both are present: the
**BREAKING** banner in the changeset body and the ADR-0087 disposition
line. The ruling's substance ships; only the marker differs, and it
differs because a repo gate forbids the marker. Flagged rather than
chosen silently.

## Item 4 — the mechanical acceptance surface

objectstack-ai#17630 is closed and its widening is live on this base: discovery in
`packages/qa/dogfood/test/expression-conformance.test.ts` matches a
roster name by identity anywhere on a line, attributes it to the
`field:` it mounts, and resolves file-local aliases. Both
`ExpressionInputSchema` and `EvaluatedExpressionInputSchema` are on that
roster, so every one of the 36 positions stays discovered across the
swap, the ledger's `file:Schema.field` cover keys are unchanged, and the
`SCAN_CONTROLS` floors (head 37 / inline 3 / alias 2) are unaffected —
the swap changes the identifier, never the syntactic shape. No ledger
row's `failPolicy` moves: the column records what the EVALUATOR does
with a bad expression, and no evaluator changed.

## Clause-② carrier readings, reported rather than acted on

⚠️ **Superseded in part by ruling A (`5725503887`), and the seat has
since acted.** C5 below reads three widening tells against a `Clause-②:
no` that no longer stands: the declaration is now `yes`, so the C5 tell
no longer gates this PR and the at-tier review does. C1 (the split dual
carrier) is also closed — the seat hung `needs:contract-review` on BOTH
card objectstack-ai#15811 and this PR at 2026-09-18T09:52:56Z / 09:52:58Z. ⛔ The
matcher was NOT touched and no C-class licence card was opened; ruling A
refuses both by name. The readings below are kept unedited as the record
of what was measured at the time.

`node scripts/pm/check-clause2-carriers.mjs --pair 18638` — **exit 4**,
two rows at the time of writing (re-read on the rework head: C1 has
cleared, C5 stands — see the rework section above). ⛔ Neither carrier is
touched from here; this is the reading, not a verdict.

- **C1 — the dual carrier is split.** `needs:contract-review` is on card
objectstack-ai#15811 and NOT on this PR. That is the state as found; the seat that
owns the gate hangs or clears both sides in one stroke.
- **C5 — three widening tells against `Clause-②: no`.** One was real and
is gone: the new published export `evaluatedExpressionUnionRefusal` in
`api-surface/shared.json`, removed by moving the helper package-internal
(above), so the published surface is byte-unchanged by this PR. The
remaining two are **false**, and both for the same reason — the matcher
fires on an ADDED LINE that has the shape of a widening, and these two
lines were added because an options object was appended to a union that
gained no member:
- `ui/action.zod.ts` `ActionConditionInputSchema` — read as T2 「a new
member of a closed set」. The union has the same two members before and
after; what is new on the line is `, { error: … }`.
- `ui/component.zod.ts` `RecordAlertProps.visible` — read as T1 「a new
key on a Zod object schema」. `visible` existed before this PR; the line
moved for the same options object.

Per the gate's own instruction a false tell is repaired in the matcher
(`scripts/pm/check-widening-tells.mjs`, with a `--self-test` case
pinning the shape) or filed as its own card. Repairing a `scripts/pm/**`
matcher is outside this card's surface, so it is filed rather than done
here — see the report's `out_of_scope_findings`.

## Tests

`packages/spec/src/shared/evaluated-slot-population.test.ts` is the new
pin, in two halves because either alone is a green that proves nothing:

- **structural** — no declaring position in `packages/spec/src` still
mounts the persistence schema on a code line, with a lit control (the
scan does find the name in the definition file and the barrel), a dark
control, and an explicit assertion that the Cron / Template / Evaluated
siblings do not leak in as substrings;
- **behavioural** — all 36 positions parsed AS MOUNTED, refusing all
three refused spellings and carrying the one published sentence, plus an
assertion that the table reached exactly 36 positions so a position that
stops being reachable reds instead of silently leaving;
- **controls** — `ExpressionSchema` / `ExpressionInputSchema` /
`PredicateInputSchema` still ACCEPT both shapes, and a healthy predicate
still parses at all 36 (the settings pair gets the predicate its own
closed grammar accepts).

`packages/formula/src/print-cel-ast.test.ts` pins the printer's two
claims, including seven dark-control inputs.

Three existing pins were rewritten rather than relaxed — each pinned
exactly the arm this PR deletes:

- `system/settings-manifest.test.ts` 「an `ast`-only envelope is opaque
at this layer」 now pins the refusal, and asserts the settings GRAMMAR
message is *not* the one raised, so the two refusals stay independent;
- `ui/action.test.ts` 「rejects composition with an AST-only visible
loudly (ADR-0078)」 — ADR-0078's promise is unchanged, but the refusal
moved from the `requiresFeature` lowering to the slot, so it now holds
with **and without** the flag. A second case pins that objectstack-ai#17631's shape (a
blank `source` composed into `( ) && features.admin == true`) can no
longer reach the lowering at all;
- `ui/view-form-features-root.test.ts` 「documented boundary」 now asserts
the refusal comes from the evaluated-slot rule and not from the
features-root scanner this file is about.

**Repo census for the migration:** zero authored occurrences of either
refused spelling outside `packages/spec`'s own refusal fixtures, across
`packages/`, `examples/`, `content/` and `skills/`, against a lit
control that hits. Nothing in this repository needs rewriting.

## Acceptance notes

- `PredicateInputSchema` (`shared/expression.zod.ts`) remains a plain
value alias of `ExpressionInputSchema` with zero slot users. Left wide
deliberately — it aliases the persistence contract. Noted, not filed;
carrier is the ledger's own limit 2, already written up there.
- `celEngine.evaluate` on `{ dialect: 'cel', source: '' }` answers with
the AST-only message rather than an empty-source one. Message accuracy
only; the verdict is correct. Unchanged here, still uncarried.

Authored by the `domain:spec` execution seat, session
`session_01LvwGppdonww4zGLWZo5rho`, under the dispatch claim
objectstack-ai#15811 (comment).

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… size predicate in check-governed-merges --test, the same reading in dispatch-gates, one rule line in SKILL.md and landing-operations (objectstack-ai#19033)

Fixes objectstack-ai#19012
Clause-②: no

## Maintainer ruling (verbatim, 2026-09-18)

> 「还有应该完善skills,修改代码量超过某个行数(比如5000)就应该人工审核。」

Read as: a pull request whose changed line count — GitHub's `additions +
deletions` on the PR, generated files INCLUDED — exceeds 5,000 lands
only by a human merge, at the same terminal as governed text (ACCEPT on
the card, `needs-user-decision` on the PR, a final 维护者速读, review
requested from `GOVERNED_APPROVERS`); no seat flips it ready or arms
auto-merge. 5,000 is the ruled default (「比如」), declared once as
`HUMAN_MERGE_LINE_THRESHOLD` in `scripts/pm/check-governed-merges.mjs`,
so it moves by one word from the maintainer and one edit. The case that
prompted it, PR objectstack-ai#18971 (+238,310 / −119, of which 237,706 lines were
regenerated artefacts), is the first PR the rule governs — an exemption
for generated files would exempt exactly it, so there is none.

## What changed

1. **`scripts/pm/check-governed-merges.mjs` — the SIZE predicate.**
`--pr N` reads `additions` / `deletions` off the same `GET
/repos/OWNER/REPO/pulls/N` that gives `changed_files` (a PR object
missing the pair is a refusal on exit 1 — never a size of zero, never a
"not governed" answer); `--branch REF` counts the same merge-base range
with `git diff --numstat --no-renames` (a binary file is 0 lines, as
GitHub counts it); `--test PATHS` takes `--additions N --deletions N` as
a pair, or prints `size: NOT MEASURED` on stdout naming the modes that
read it. Either limb exits on the GOVERNED code 3, so every caller that
already routes 3 to the human terminal routes an oversized PR there
without a new code; `--json` carries `size` and `humanMerge` (`governed`
stays the path limb). A certified generated-artifact regeneration lifts
the PATH off the register and lifts nothing from the size. The queue
guard's `testVerdict(paths)` reading is unchanged (no size handed in ⇒
the path answer as before).
2. **`scripts/pm/dispatch-gates.mjs` — the same reading at dispatch
time.** With no paths (the derived run) it prints `Changed lines — N (+a
/ -d; generated files INCLUDED) vs the human-merge threshold 5000:
under` or `⛔ OVER — this PR lands only by a HUMAN MERGE …` beside the
tier verdict (human and `--tier` modes), the count on stderr with the
rest of the provenance, and `changedLines` in `--json`. The count is
`--numstat` off the merge base against the working tree plus untracked
files counted from disk (under-derivation refused, like the path list).
An explicit path list carries no diff and prints `NOT MEASURED`, never a
silent under. The threshold is imported from the gate — one declaration,
no second copy.
3. **Rule text.** `.claude/skills/pm-dispatch/SKILL.md` gains one line
beside the four-piece-terminal trigger (line 608, 111 B): 「改动 >5000
行(含生成物)同换终局四件套,⛔ 无事实层例外;读数 = PR additions+deletions。」
`references/landing-operations.md` line 26 folds the size limb into the
pre-check row, now spelled `--pr N` (which reads paths and size in one
call), 117 B, ceiling unchanged at 69. The SKILL.md ceiling rises 812 →
813 in `scripts/pm/check-skill-line-ratchet.mjs` under the ratchet's own
maintainer exit, the ruling quoted in the entry (the 811 → 812
precedent's form).

## Readings — before / after, measured

| reading | before (`43f476688`) | after (this head) |
|---|---|---|
| `check-governed-merges.mjs --pr 18971` (live API through the proxy) |
exit 0 — `✅ NOT governed — ordinary queue landing applies` | exit 3 — `⛔
HUMAN MERGE — 238429 changed line(s) (+238310 / -119) > 5000` |
| `--pr 18994` (2 files, +15 / −1) | exit 0 | exit 0 — `size: 16 changed
line(s) (+15 / -1) ≤ 5000 — under the human-merge threshold` |
| `--pr 18921` (SKILL.md, +6 / −6) | exit 3 GOVERNED | exit 3 GOVERNED,
plus `size: 12 changed line(s) … under` |
| `check-governed-merges.mjs --self-test` | 328 assertions, 26 batteries
| 369 assertions, 27 batteries (new battery: the SIZE predicate, floor
30) |
| `dispatch-gates.mjs --tier` (no paths, this worktree) | no size line |
`Changed lines — 722 (+691 / -31; generated files INCLUDED) vs the
human-merge threshold 5000: under.` |
| `dispatch-gates.mjs --tier packages/spec/src/index.ts` | no size line
| `Changed lines — NOT MEASURED: a path list carries no diff to count …`
|
| `check:pm-dispatch-gates` (detached, `tail --pid`) | 1849 cases (the
dispatch's reading at `43f476688`) | 1862 cases pass (795.6 s, detached;
+13 cases) |
| `check:pm-skill-ratchet` | SKILL.md 812 / 812 · landing-operations.md
69 / 69 | SKILL.md 813 / 813 · landing-operations.md 69 / 69 |

Self-test pins on the threshold: exactly 5,000 changed lines is under;
5,001 is over; the +238,310 / −119 pair reads 238,429 and is over; a
certified pure regeneration over the threshold still lands by a human
merge; the verdict is byte-identical through `--branch` and through
`--test` once the same list and numbers are handed in.

## Line budget (measured)

- `SKILL.md`: 812 → 813 lines; ceiling 812 → 813 (maintainer exit). A
fold was not available: 0 of 598 adjacent bullet pairs merge under the
120-byte cap (smallest 123 B); the trigger line (607) stands at 118 B;
the rule's shortest self-contained form is 111 B; deleting a ruled
clause is refused on the state-machine precedent.
- `references/landing-operations.md`: 69 → 69 lines (line 26: 118 B →
117 B).
- `check:pm-skill-id-lint`: 27 files clean (no issue-ID citation in
either line).

## Gates (this head; exit codes captured before any pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree at `7fdd61ca0` (42
commands; change set 5 paths, 724 changed lines by its own reading),
every one run with `cmd > log 2>&1; status=$?` and reconciled with
`--ran`:

```text
node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 0
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/check-harness-current.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:declared-population-live :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-expected-skips :: exit 0
pnpm check:pm-governed-prose :: exit 0
pnpm check:pm-half-states :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:pm-skill-ratchet :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
```

`dispatch-gates --ran`: **42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN**
(verdict line: `✓ dispatch-gates --ran: 42 derived famil(ies) accounted
for — 42 run, 0 NOT-MEASURED`). `check:pm-dispatch-gates` ran detached
(`nohup` + `tail --pid`, 795.6 s on this box): `✓ dispatch-gates
self-test: 1862 cases pass.` `check:pm-governed-merges`: `✓
check-governed-merges --self-test: 369 assertions`.
`check:doc-formula-expressions` exited 3 (PREREQUISITE NOT MET:
`@objectstack/formula` / `@objectstack/lint` not built) on the first
pass; both were built under `scripts/pm/os-verify-lock.sh` (VERDICT
command-exit 0, 152 s held) and the gate reran green — the exit 3 was
never a measurement.

NOT MEASURED locally, by the derivation itself (CI-only, value-bearing
argv): `scripts/check-shard-attestation.mjs --emit …`,
`scripts/check-test-completeness.mjs …`,
`scripts/pm/check-half-states.mjs --format=markdown --provenance=…`;
plus the 11 wide-population families and the 50 artifact-roster families
CI runs on every PR, outside the derived total by design. `pnpm lint`
(repo-wide eslint) is CI-owned and was not run here. No package
build/test is owed: the diff touches no `packages/**` file (no ①/② in
the local verification scope), so the only lock-wrapped run was the
formula/lint build above.

Line-budget after the final commit (`7fdd61ca0`):
`check:pm-skill-ratchet` — `.claude/skills/pm-dispatch/SKILL.md is 813
lines (ceiling 813; headroom 0)`, `references/landing-operations.md is
69 lines (ceiling 69; headroom 0)`; `check:pm-skill-id-lint` — 27
file(s) clean.

## Deviations from the dispatch brief

1. Mechanism assumption 1 said a failing size read exits PREREQUISITE
NOT MET (3). Under `--pr`, 3 already means GOVERNED — the file's own
rule is that no invocation carries both meanings — so a PR object
without the pair is a REFUSAL on the derivation code 1 (a stated
refusal, never 0, never a size of zero). The ruling's intent (never read
as "not governed") is kept.
2. "812 / 812 — fold or pay": measured, neither was available (above),
so the SKILL.md line lands under the ratchet's own maintainer exit (812
→ 813), the form the 811 → 812 entry took. The hunk sits at :608,
disjoint from PR objectstack-ai#18903's bands (:509–:525, :633–:675) and from the two
PRs that landed on SKILL.md meanwhile (merged into this branch; the line
is still there once). If the seat prefers the follow-up route, drop
commit 3's SKILL.md hunk and the ratchet entry together.
3. `--branch` derives the size itself (`--numstat` on the range it
lists) rather than taking passed-in numbers; the flags beside a deriving
mode (`--pr`, `--branch`) are refused as two readings of one number, the
way two mode flags are.
4. `dispatch-gates.mjs`'s self-test pins a NAMED census of live
population markers by file and line; the import block moved this file's
own `inherited-population` marker from :702 to :705, so that one row is
updated — the census exists to be updated exactly this way.

## Acceptance notes

- to file (class b — a declared contract the queue cannot yet hold): the
queue guard's `merge_group` leg reads the PATH register only; a seat
that skips the landing pre-check can still enqueue an oversized PR.
Dedupe words: `queue guard size threshold`, `merge_group additions
deletions`, `check-governed-queue-guard 5000`, `human merge line count`.
- to file (class b): AGENTS.md §7 lists "two classes of PR never enter
this path on green alone" (governed surface; Version Packages) — the
ruled third class is missing from the rules layer. Dedupe words:
`AGENTS.md green alone third class`, `5000 lines human merge AGENTS`.
- noted, not filed: the post-merge sweep (default mode of
`check-governed-merges.mjs`) lists governed-surface merges only; an
oversized PR that landed through the queue is not listed. 承接者: the
skills seat, together with the queue-guard follow-up above.
- noted, not filed: `check:doc-formula-expressions` exits 3
(PREREQUISITE NOT MET) on a fresh worktree until `@objectstack/formula`
and `@objectstack/lint` are built — by design of that gate; built under
the verify lock here and rerun. 承接者: none.

## 维护者速读(草稿)

**改了什么**:落地前检 `check-governed-merges.mjs` 新增「体量」判据:PR 的 additions +
deletions 超过 5000 行(含生成物)⇒ 只能人合,与受管面走同一终点;`dispatch-gates`
在派发/认领时就把同一读数印在 tier 行旁;SKILL.md 与 landing-operations.md
各落一行规则。阈值只声明一次(`HUMAN_MERGE_LINE_THRESHOLD = 5000`),改它是一个词。

**为什么改**:您 2026-09-18 的裁决。触发案例是 PR objectstack-ai#18971(+238,310 / −119,其中 237,706
行是生成物)只凭 AI 审查就经队列合入;生成物不豁免,否则恰好豁免它。

**风险与代价(含回滚)**:大 PR 的落地从「席位挂 auto-merge」变成「等您点一下」,每张超 5000 行的 PR
多一次人工动作;回滚 = revert 本 PR(纯脚本 + 两行规则文本,无发布物)。已知缺口:队列守卫的 merge_group
腿尚未读体量,眼下靠席位跑落地前检;已列为后续单。

**席位意见**:(留空)

**你要做的**:确认 5000 这个默认值(「比如」)是否就是您要的;是 ⇒ 人合本 PR;要改数字 ⇒ 说一个数即可。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…erge sweep lists one — the SIZE limb reaches the queue (objectstack-ai#19175)

Fixes objectstack-ai#19036
Clause-②: no

## What this PR does

The maintainer's 2026-09-18 ruling — 「修改代码量超过某个行数(比如5000)就应该人工审核」 —
landed seat-side in PR objectstack-ai#19033 (`HUMAN_MERGE_LINE_THRESHOLD = 5000`,
`testVerdict(paths, { size })`, `sizeVerdict`, `landsByHumanMerge` in
`scripts/pm/check-governed-merges.mjs`). The merge-queue guard's
`merge_group` leg kept handing the predicate no size, so a PR over 5,000
changed lines that a seat enqueued still merged — exactly what PR objectstack-ai#18971
did (+238,310 / −119, nothing governed). This PR carries the limb to the
two places the card names.

**Queue guard (`scripts/pm/check-governed-queue-guard.mjs`)** — a fourth
leg, `runSizeGuard` / `sizeGuardVerdict` / `renderSizeVerdict`:

- reads every queued pull request's `additions` / `deletions` off the
same pull object the head read uses — `makePullReader` grew a `size`
field (through the sibling's own `pullSizeFrom`), no second endpoint, no
new workflow scope (`pull-requests: read` already covers the pull
object; the workflow file is untouched and the self-test pins that no
`issues:` scope appeared);
- judges it through the sibling's IMPORTED predicate: `testVerdict([], {
size })` is the size limb alone (the path limb is the governed leg's
question, already answered on the lifted rows), and `landsByHumanMerge`
reads it — this file declares no threshold and spells no comparison,
pinned against its own source;
- REFUSES on a new exit code `EXIT_REFUSED_OVERSIZED = 8`, printing the
two numbers, their sum, the threshold, the limb (SIZE), the source (`GET
/repos/{o}/{r}/pulls/{n}`) and the one remedy — a human merge (the
ruling quoted untranslated; it never advises making the diff smaller);
- an unreadable size (the pull read throws, or the object carries no
pair) is `EXIT_REFUSED_SIZE_UNREADABLE = 9`, fail-CLOSED — this read
DECIDES, unlike the PR head, which has decided nothing since 2026-09-04;
- the `pull_request` leg is silent and read-free (renders `''`), so that
leg's output stays byte-identical;
- the three-leg exit precedence is a pure function, `groupExitCode`:
governed, then size, then carrier — every block is always printed, one
code exits.

**Post-merge sweep (`check-governed-merges.mjs` default mode)**:

- `classifyCommit` is now `landsByHumanMerge(testVerdict(paths, { size
}))`, so a landing is an entry when EITHER limb fired — a governed merge
as before, and an oversized landing with no governed path at all;
- the size is read LOCALLY off the landed diff: `commitChanges` runs one
`git diff-tree --numstat --no-renames -m --first-parent` per mainline
commit (replacing `commitPaths`' `--name-only`; the self-test pins the
path list byte-identical against `--name-only` on a real fixture, a
merge commit read against its first parent, a binary row at zero);
- `renderReport` counts such rows apart (`N governed merge(s) and M
oversized landing(s) with no governed path`), prints a `⛔ SIZE:` row
with the numbers and the threshold, keeps the same attribution column,
and prints GitHub's own pair beside the landed number only when the two
differ (it rides the attribution GET the row already pays for; it never
decides the listing);
- `--json` entries carry `size` and `humanMerge`.

## The PM's mechanism assumptions, measured

- 「the sweep already reads each merged PR (it prints `merged_by`)」 —
**falsified.** The attribution loop reads `GET /pulls/{n}` only for rows
`classifyCommit` already produced (governed merges); an ungoverned
landing is never read at all, so no API read could have made it an
entry. Route taken instead: the local `--numstat` reading above, the
same source `--branch` already uses in this file, zero API, and it also
lets the sweep classify before any attribution is spent.
- 「the size reading cannot ride the existing head read for an oversized
PR with no governed path; one `GET /pulls/N` per PR in the group」 —
**held.** The size leg reads every queued PR through the same reader;
the call count is pinned (`apiCalls ===
carrierPullsInGroup(rows).length`, deduplicated, group order).
- 「fail-closed on an unreadable size」 — **taken**, on a code of its own
(9) rather than the governed leg's 4, for the same reason 6/7 are split
from 3/4: the legs' refusals must stay separable in a log.
- 「pick the exit code already highest in the table」 — **taken**: a
governed-unsatisfied AND oversized group prints both limbs and exits 3.
Consequence worth stating: a merge group naming no pull request now
exits on the size leg's 9 rather than the carrier's 7 (both blocks still
print their own refusal); pinned.
- **Boundary not decided here:** no authorized APPROVED review and no
review of record lifts the size limb — the landed predicate says a human
MERGE, and nothing has ruled the number the way 2026-08-27 ruled paths.
The rendering says so; widening it is a one-line maintainer decision in
the sibling. Listed under open questions in the report, not implemented.

## Acceptance (the seat's checklist)

- oversized PR, NO governed path, in a merge group → refused, size limb
named: the objectstack-ai#18971 replay pin (`⭐
objectstack-ai#18971-replay-an-OVERSIZED-PR-with-NO-governed-path-is-REFUSED-at-the-queue-on-the-size-code`)
— governed leg clear at zero reads, size leg exit 8, `groupExitCode` 8;
the rendered text names `objectstack-ai#18971`, `238429 changed line(s) (+238310 /
-119)`, `EXCEEDS the human-merge line 5000`, `HUMAN MERGE`;
- governed path AND oversized → both limbs rendered, one exit (3),
pinned;
- exactly at the threshold → clear (guard: `⭐
exactly-the-threshold-is-WITHIN-the-comparison-is-strictly-greater`;
sweep: `⭐
exactly-the-threshold-is-NOT-listed-the-comparison-is-strictly-greater`,
and a real fixture commit of exactly 5,000 lines is not listed);
- `pull_request` leg byte-identical: `renderSizeVerdict` returns `''`
there and a throwing spy proves zero reads;
- unreadable size → exit 9 (throw, missing pair, no recorded reading,
group naming no PR), never a pass;
- sweep lists an oversized merged PR, not an at-threshold one: unit pins
plus a REAL CLI sweep over a fixture repo (`PR objectstack-ai#5001` listed with `⛔
SIZE`, `PR objectstack-ai#5000` not), on stdout and in `--json`;
- self-tests green with counts up: guard 261 → 296, governed-merges 410
→ 435; battery rosters +1 each, floors 21 → 22 and 29 → 30;
- workflow file untouched.

## Verification

Self-tests (worktree at `5c7caff` — the branch merged with `origin/main`
`c229223`, which touched neither file; after `pnpm install`):

- `node scripts/pm/check-governed-queue-guard.mjs --self-test` → exit 0,
`296 cases pass` (was `261 cases pass` at `e8667ee`).
- `node scripts/pm/check-governed-merges.mjs --self-test` → exit 0, `435
assertions` (was `410 assertions`).

Ablation — the comparison inverted in the SIBLING, watched from both
files (proves the guard imports the predicate rather than restating it),
through `scripts/ablation-replace.mjs` in WRAP mode against the
committed head `5c7caff` (the final of three runs; the first two are
recorded below because each taught something):

- mutation: anchor `exceeds: changedLines > HUMAN_MERGE_LINE_THRESHOLD,`
× 1 → the same line with the greater-than sign replaced by a less-than
sign, × 1; blob `6e1112bcd55e` → `c7a42160aabd`; on-disk counts read
back inside the mutated window: replacement 1, original 0.
- guard self-test under mutation: exit 1, `14 of 296 case(s) failed`,
all in the objectstack-ai#19036 battery
(`threshold-plus-one-changed-line-is-OVERSIZED`, `one-under-is-WITHIN`,
`an-OVERSIZED-queued-PR-REFUSES-with-code-8`,
`a-within-sibling-does-NOT-carry-an-oversized-PR-through-the-group`,
`objectstack-ai#18971-replay-an-OVERSIZED-PR-with-NO-governed-path-is-REFUSED-…`,
`governed-AND-oversized-prints-BOTH-limbs-…`,
`an-authorized-APPROVAL-…-lifts-NOTHING-from-the-size-…`,
`a-certified-pure-regeneration-lifts-…-NOTHING-from-the-size-at-the-queue-either`,
…).
- governed-merges self-test under mutation: exit 1, `30 failure(s)`, 0
TypeErrors — the pre-existing SIZE battery
(`5001-changed-lines-is-OVER-it`,
`the-PR-that-prompted-the-ruling-reads-238429-…`,
`--test-with-5001-changed-lines-…-exits-3`, …), the new objectstack-ai#19036 battery
(`an-oversized-landing-with-NO-governed-path-is-a-sweep-ENTRY-…`,
`the-head-counts-the-oversized-landing-APART-…`,
`a-REAL-sweep-LISTS-the-over-by-one-landing-…`, …) and two objectstack-ai#13307
live-mirror sweep pins that now see a phantom oversized row (every small
landing reads over an inverted line — the expected direction).
- restore: blob after restore `6e1112bcd55e…` == `HEAD` blob, `git diff
HEAD` empty, `git status --porcelain` empty, anchor count back to 1.
- Run 1 (on `3c7f5ec`): guard 14/296 red as above; the merges leg exited
1 by a **TypeError** in my new battery (a pin dereferenced the fixture
that classifies to null under the mutation), so its failures were not
named — fixed in `28faa51` (null-guarded pins; a red case must be a
named one). Run 2: run against that fix while it was still UNCOMMITTED;
the tool restores to `HEAD` by design, so the fix was discarded by the
restore — caught by the pre/post blob compare (`6e1112…` before,
`859965…` after), re-applied, committed, and run 3 is the record above.
The "commit the fix first" rule, measured on the fix to the pins.

Gates (derived in the worktree with `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack --commands` — 33 commands, identical
to the dispatch's list; reconciled with `--ran`; exit codes captured
after redirection, never through a pipe; run at `5c7caff`, the final
head, after the ablation's restore was proven):

| # | command | exit | verdict line (from the gate's own output) | wall
|
|---|---|---|---|---|
| 1 | `node scripts/check-ci-filter-parity.mjs` | 0 | OK: all 184
declared cross-package glob(s) (131 unique) are covered by `core` or
`crosspkg`, every `crosspkg` entry still covers one, and the `test`
job's `if:` | 0s |
| 2 | `node scripts/check-closing-keyword-parity.mjs` | 0 |
check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and
both measured separators; sweep found 5 file(s) carrying the grammar
across 8993 tracked | 1s |
| 3 | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 |
✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations
of the shipped parsers each driven to red. | 3s |
| 4 | `node scripts/check-comment-mask-corpus.mjs` | 0 | ✓ comment-mask
corpus sweep [scripts/js-comment-mask.mjs]: 6896 files, 0 disagree, 0
unparseable, 81.9s (comparator self-test: 26 cases pass). | 83s |
| 5 | `node scripts/check-declaration-mirrors.mjs` | 0 | OK: 10
hand-written declaration(s) agree with their modules on name, kind and
required arity. | 0s |
| 6 | `node scripts/check-declaration-mirrors.mjs --self-test` | 0 | All
29 self-test cases passed. | 0s |
| 7 | `node scripts/check-scripts-symbol-anchors.mjs` | 0 | ✅
check-scripts-symbol-anchors: 3524 anchors across 265 scripts resolve —
52 symbol (52 declaration, 0 literal), 3472 file-level, 0 cross-repo, 1
exempt, 2 cont | 4s |
| 8 | `node scripts/check-scripts-symbol-anchors.mjs --self-test` | 0 |
✅ check-scripts-symbol-anchors --self-test: every finding class
provoked, comment-prose projection wired, declined shapes counted not
missed, allowance rows exa | 4s |
| 9 | `node scripts/check-self-test-wired.mjs` | 0 | ✓
check-self-test-wired: every one of the 214 script(s) CI runs that ship
a `--self-test` has that self-test run by CI. | 2s |
| 10 | `node scripts/check-self-test-wired.mjs --self-test` | 0 |
check-self-test-wired --self-test: 3 live ledger row(s) verified, plus
the comment mask, the right boundary, alias resolution and both audit
directions — 10 dec | 2s |
| 11 | `node scripts/check-self-test-workflow-commands.mjs` | 0 | ✓
check-self-test-workflow-commands: no self-test CI runs prints a line
the Actions runner would parse as a workflow command. | 51s |
| 12 | `node scripts/check-self-test-workflow-commands.mjs --self-test`
| 0 | check-self-test-workflow-commands --self-test: both measured parse
rules pinned (legacy form anywhere in a line, current form only at line
start), the innocent- | 3s |
| 13 | `node scripts/check-skills-token-ratchet.mjs` | 0 | ✓
check-skills-token-ratchet: 34 authored bundle file(s) within their
ceilings; 10 generator-owned file(s) measured, not ratcheted. | 0s |
| 14 | `node scripts/check-skills-token-ratchet.mjs --self-test` | 0 | ✓
check-skills-token-ratchet self-test: 65 cases pass. | 1s |
| 15 | `node scripts/check-whole-set-label-write.mjs` | 0 | ✓
check-whole-set-label-write: 0 violations — 328 file(s) over 3 root(s) ·
12 raw mention(s) · 12 in comments/prose (cleared) · 0 in EXECUTABLE
content (judged) · 191 `uses:` pin(s) over 18 di | 2s |
| 16 | `node scripts/check-whole-set-label-write.mjs --self-test` | 0 |
✓ check-whole-set-label-write --self-test: all cases pass (24 fixture
trees + 5 refusals + 1 allowlist hatch) | 0s |
| 17 | `node scripts/pm/bare-root-worklist.mjs --self-test` | 0 | OK
self-test: 81 live row(s), 59 unreachable as spelled, 46 recorded
verdict(s) — none stale, none missing, none contradicted (12 row(s)
whose gate carries the | 29s |
| 18 | `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 0
| ✓ check-governed-queue-guard self-test: 296 cases pass
(register-driven verdicts, the queue/PR event split, latest-decisive
approval reduction, the 2026-09-04 a | 0s |
| 19 | `pnpm check:agent-test-spelling` | 0 | ✓
check-agent-test-spelling: 0 violations — 518 file(s) · 9204 bare `--`
token(s) · 1749 launcher-rooted run(s) · 13 separator(s) JUDGED · 6
vitest-backed scrip | 4s |
| 20 | `pnpm check:bash32-floor` | 0 | ✓ check-bash32-floor: 31 tracked
shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no
bash 4+ construct outside a comment, a guarded ${VAR:-} | 2s |
| 21 | `pnpm check:cli-command-ids` | 0 | ✓ check-cli-command-ids: 63
module(s) under packages/cli/src/commands examined, all of them
default-export a class whose inheritance chain reaches oclif's `Comm |
11s |
| 22 | `pnpm check:closing-target-claim` | 0 | ✓
check-closing-target-claim self-test: 105 cases pass. | 1s |
| 23 | `pnpm check:cross-package-test-inputs` | 0 | OK: 29 package(s)
read outside themselves, all declared, and turbo.json hashes every
declared glob (6 of them on a split "test:repo" task); 13 walked root(s)
ju | 20s |
| 24 | `pnpm check:driver-memory-census` | 0 |
check-driver-memory-census: OK — every declaration is ledgered, every
ledger entry is live, and every ruled file states "objectstack-ai#6664 census: 2 ruled
consumers". This | 4s |
| 25 | `pnpm check:entry-guard` | 0 | ✓ check:entry-guard: 265 scripts/
file(s) — every entry guard goes through invoked-as.mjs; 206 export
bindings, 206 of them inert on import (0 known-unsafe, ⛔ S | 47s |
| 26 | `pnpm check:nul-bytes` | 0 | check-nul-bytes: OK (scanned 8986
text file(s) -- 8986 tracked, 0 untracked-not-ignored; skipped 7 binary;
no raw ASCII control bytes). | 3s |
| 27 | `pnpm check:parse-guard` | 0 | ✓ check:parse-guard: 264 scripts/
file(s) — every TypeScript parse goes through ts-parse.mjs. | 2s |
| 28 | `pnpm check:pm-governed-merges` | 0 | ✓ check-governed-merges
--self-test: 435 assertions (the unified governed predicate + near
misses, subject→PR spellings, window parsing, the objectstack-ai#12633 landing wind |
6s |
| 29 | `pnpm check:pnpm-filter-targets` | 0 | ✓
check:pnpm-filter-targets: 151/199 `--filter` occurrence(s) across 40
file(s) resolve against 81 workspace package(s); 48 not judged (2
foreign, 26 interpolat | 3s |
| 30 | `pnpm check:ratchet-remedy-authority` | 0 | OK
check-ratchet-remedy-authority: 259 scripts swept (scripts/*.{mjs,mts} +
scripts/pm/*.{mjs,mts}); 15 mark the expanding remedy ⛔ MAINTAINER-ONLY,
5 turn it | 4s |
| 31 | `pnpm check:refd-timer-probe` | 0 | OK check-refd-timer-probe:
6891 source file(s) swept; the process-global timer probe is read in
packages/qa/refd-timer-testkit/src/index.ts and nowhere else. | 12s |
| 32 | `pnpm check:watch-hint-literal` | 0 | ✓ check-watch-hint-literal:
71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47,
ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH | 5s |

32 command(s); 0 non-zero exit(s).

Reconciliation (`node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran` over a record whose every line carries
`:: exit N`): `33 derived, 32 run, 0 NOT-MEASURED, 1 UNRUN` — the one
unrun family is `pnpm check:pm-dispatch-gates`, which is **NOT MEASURED
at PR-open time**: it runs only under the shared verify lock (≈1,000 s
under contention); a first run started on `3c7f5ec` overlapped the
ablation window (`dispatch-gates.mjs` imports `sizeVerdict` from the
sibling, so a child it spawned in that window could have read the
inverted comparison), and a second run on `5c7caff` is queued behind it.
Its verdict lands in the `os-dev-report` comment on objectstack-ai#19036, not here —
this body is written once.

Lint, narrowed and measured: `npx eslint --no-inline-config --format
json` on the two changed files → exit 0, 2 files, 0 errors, 0 warnings.
Narrowing evidence: ① eslint's configured population
(`eslint.config.mjs`, the `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block)
includes `scripts/pm/*.mjs`, and `--print-config` resolves a config for
the file (parser `typescript-eslint/parser`); ② the JSON output counts 2
files; ③ type-aware linting is not enabled (`parserOptions.project` and
`projectService` both absent in the resolved config), so this diff
cannot move any untouched file's verdict. The repo-level `pnpm lint`
sweep is CI's run.

## Acceptance notes (noted, not filed)

- The `pull_request` event payload carries `additions` / `deletions`, so
a size early warning on the PR leg would cost zero reads. Not taken: the
ruling is about the landing, the seat-side pre-check already refuses
before arming, and the PR leg's byte-identity is a standing constraint.
承接者: the skills seat, if the maintainer wants the forecast.
- The carrier leg and the size leg each read `GET /pulls/{n}` once per
queued PR — the same endpoint twice. A shared per-run pull read would
halve it; kept separate so each leg's refusal and count stay separable
in a log. Groups are small. 承接者: none.
- No governed reference text enumerates the guard's exit table, so codes
8/9 leave no Tier S doc stale (`grep` over
`.claude/skills/pm-dispatch/references/**` and `SKILL.md` for `exit
6`/`exit 7`/`EXIT_REFUSED_CARRIER`: 0 hits).

`skip-changeset`: `scripts/pm/**` publishes nothing from any released
package (no `files[]` of any package ships it).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation domain:spec size/xl tooling

Projects

None yet

2 participants