Skip to content

feat(scripts): census — do package-root Markdown TS blocks compile? - #18751

Merged
huangyiirene merged 3 commits into
mainfrom
claude/issue-18715-markdown-codeblock-typecheck-census
Sep 17, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
claude/issue-18715-markdown-codeblock-typecheck-census

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #18715 — this PR answers the card's own "⛔ Not measured" question with a number. It deliberately does not close the blind spot: nothing here gates anything, so the half left open is the wiring decision, which is a new required gate and therefore the maintainer's floor (live precedent: the PR parked in the decision box for that reason, referenced in the dispatch). #18715 stays open on merge.

Clause-②: no

What landed

One file: scripts/measure-markdown-ts-blocks.mjs, 1086 lines, a member of the existing scripts/measure-*.mjs family (8 prior members). It is a measurement instrument, not a gate:

  • it exits 0 on every census outcome;
  • it is wired into no CI job, no package.json script, no package test/lint chain;
  • it exits non-zero for exactly one reason — the instrument itself could not run. "Could not run" is a failure, not a skip.

Usage: node scripts/measure-markdown-ts-blocks.mjs (census, 12s on a warm build) · --json · --only PATH · --controls-only · --self-test (30 batteries, no build needed).

The census

Population: *.md at the root of every directory under packages/ that carries a package.json — 76 package roots, 141 Markdown files, 1002 fenced ts/typescript/tsx blocks. content/docs/** is a different population with its own gates and is out of scope.

Three numbers, because two would hide the answer inside the convention (see below). RAW counts a block failing on any diagnostic; TOLERANT counts it failing on any diagnostic outside the forgiven set; WELL-FORMED AND WRONG counts blocks that parse as a TypeScript module and still fail — the class the repaired kernel.logger block belonged to.

stratum files blocks RAW TOLERANT WELL-FORMED AND WRONG
HAND-WRITTEN (repairable by an author) 54 282 195 (69.1%) 76 (27.0%) 58 (20.6%)
CHANGELOG.md (release-owned history) 46 720 646 (89.7%) 356 (49.4%) 109 (15.1%)
sub-stratum: the literal packages/*/*.md glob the card names 42 654 574 (87.8%) 314 (48.0%) 131 (20.0%)

RAW is an upper bound — it counts fragments that were never meant to stand alone. TOLERANT is a lower bound, and that is the direction that matters: a forgiven "cannot find name" leaves that name typed any, so every downstream use of it goes unchecked. Real defects hide behind an elision; they never appear because of one. The honest sentence is "between TOLERANT and RAW".

⚠️ The two strata are reported separately and never summed. CHANGELOG.md is compiled by changeset version from .changeset/*.md and is release-owned: its blocks are before/after migration snippets that deliberately document removed APIs (57 of its counted diagnostics are TS2305 "has no exported member"). A failing block there is frequently correct documentation, and it cannot be repaired in a code PR at all.

Diagnostic composition over the whole corpus:

  forgiven  elided-name                 1297 diagnostics in  527 blocks   TS2304x1275 TS2503x20 TS2552x2
  COUNTED   syntax                       772 diagnostics in  265 blocks   TS1005x370 TS1128x170 TS1109x115 TS1127x73
  COUNTED   semantic                     332 diagnostics in  141 blocks   TS2305x57 TS2300x56 TS2451x49 TS18004x47
  COUNTED   implicit-any                  65 diagnostics in   42 blocks   TS7006x54 TS7031x8 TS7008x2 TS7023x1
  forgiven  doc-local-path                12 diagnostics in    9 blocks   TS2307x12
  COUNTED   unpublished-subpath            8 diagnostics in    7 blocks   TS2307x8
  forgiven  missing-ambient-environment     1 diagnostic  in    1 block    TS2593x1

Per-file breakdown (blocks / RAW fail / TOLERANT fail), every file carrying at least one TOLERANT failure:

   259  229  141  packages/spec/CHANGELOG.md                        4    4    4  packages/plugins/plugin-auth/ARCHITECTURE.md
    33   29   23  packages/services/service-automation/CHANGELOG.md 7    7    4  packages/plugins/plugin-auth/CHANGELOG.md
    37   35   22  packages/runtime/CHANGELOG.md                     9    9    4  packages/plugins/plugin-sharing/CHANGELOG.md
    50   46   19  packages/objectql/CHANGELOG.md                    6    4    4  packages/services/service-analytics/CHANGELOG.md
    25   23   15  packages/metadata-protocol/CHANGELOG.md           7    6    4  packages/services/service-datasource/CHANGELOG.md
    18   16   12  packages/lint/CHANGELOG.md                        4    4    4  packages/triggers/trigger-record-change/CHANGELOG.md
    20   18   11  packages/cli/CHANGELOG.md                         4    4    3  packages/client/README.md
    27   25   11  packages/client/CHANGELOG.md                      7    7    3  packages/core/CHANGELOG.md
    23   23   10  packages/rest/CHANGELOG.md                        4    3    3  packages/drivers/driver-memory/README.md
    14   14    9  packages/client-react/README.md                   8    8    3  packages/plugins/plugin-hono-server/CHANGELOG.md
    17   15    9  packages/drivers/driver-mongodb/CHANGELOG.md      3    3    3  packages/rest/README.md
    10   10    9  packages/metadata/CHANGELOG.md                   15   13    3  packages/spec/DEVELOPMENT_PLAN.md
    10   10    8  packages/platform-objects/CHANGELOG.md
    20   20    8  packages/runtime/README.md                       ... 2 each: client-react/CHANGELOG, metadata-core/CHANGELOG,
    33   27    7  packages/drivers/driver-sql/CHANGELOG.md             plugin-audit/CHANGELOG, plugin-auth/README,
    10    9    6  packages/core/ADVANCED_FEATURES.md                   plugin-reports/CHANGELOG, service-i18n/CHANGELOG,
    12   12    6  packages/plugins/plugin-security/CHANGELOG.md        service-job/README, service-queue/CHANGELOG,
    11    7    5  packages/services/service-automation/README.md       service-realtime/README, service-sms/CHANGELOG,
     7    5    5  packages/spec/V3_MIGRATION_GUIDE.md                  service-storage/CHANGELOG, spec/PLUGIN_STANDARDS,
    14   12    4  packages/drivers/driver-memory/CHANGELOG.md          trigger-record-change/README, trigger-schedule/CHANGELOG,
                                                                      trigger-schedule/README
 ... 1 each: cli/README, driver-mongodb/README, driver-turso/README, mcp/CHANGELOG, mcp/README, observability/README,
             knowledge-ragflow/README, service-cache/README, service-cluster/CHANGELOG, service-i18n/README,
             service-package/README, service-queue/README, service-settings/CHANGELOG, service-storage/README,
             spec/README, spec/REST_API_PLUGIN, spec/ZOD_SCHEMA_AUDIT_REPORT, types/CHANGELOG, types/README,
             verify/CHANGELOG

  33 file(s) with TS blocks and zero TOLERANT failures.

The full per-block record, with every diagnostic, is node scripts/measure-markdown-ts-blocks.mjs --json.

The elision convention — the design question, and why the obvious answer is disqualified

The card says the convention is the real design question, not the extraction. It is, and the obvious answer fails.

⛔ Exclusion is disqualified, and this PR proves it rather than asserting it. The obvious rule is: a block carrying an elision marker (// ...) is skipped. Run that rule against the one failure this repository has already measured — the pre-#18712 kernel.logger block in packages/core/PHASE2_IMPLEMENTATION.md, tsc --noEmit --strict exit 2 with TS2341 — and it is excluded, because that block ends with the line // ... plugin registration code .... An exclusion rule would have hidden the only defect in this family anyone has ever measured. The instrument reports the exclusion reading on every run, labelled disqualified, so the number cannot hide inside the convention:

reading hand-written CHANGELOG
[disqualified] EXCLUSION 188 fail of 272 considered, 10 blocks skipped 642 fail of 716 considered, 4 blocks skipped

✅ The rule used is a TOLERANCE rule. Every block is compiled; what a legitimately partial block can produce is forgiven and nothing else is:

  • forgiven — TS2304 / TS2552 / TS2503: a name or namespace the block elided.
  • forgiven — TS2307 on a relative specifier (./my-kernel, ./objectstack.config.js): the reader's own file.
  • forgiven — "cannot find name it / describe / process", and a third-party module that ships no declarations: the reader's ambient environment.
  • counted — everything else, including TS2341 (private member), TS2339, TS2345, TS2305 / TS2724, and TS2307 on an @objectstack/* specifier. The paths map is generated from each package's own exports map, so an unresolved one means the document tells a reader to import a subpath the package does not publish.
  • counted — syntax. A tolerance rule that forgave TS1xxx would forgive a typo.

The forward convention, for what tolerance cannot absolve. A block that is a bare fragment — a method-signature listing, half an object literal — produces syntax errors, and those need an explicit declaration. Proposed spelling: an info-string tag, ```ts partial. Zero blocks carry it today, so it contributes nothing to this census; the instrument reads it, and counts how many blocks would need one: 18 hand-written, 239 in CHANGELOGs.

The firing control

An instrument that finds zero failures and was never shown capable of finding one has measured nothing. Two controls run on every census, through the identical extractor, normalisation and compiler path as corpus blocks, and the run refuses to print a census if either misbehaves:

  • GREEN_CONTROL must compile. Its failure means the workspace is not built — the state in which every other block would report TS2307 and the census would read as a catastrophe made of nothing.
  • FIRING_CONTROL is the pre-docs(core): repair four copy-fails in PHASE2_IMPLEMENTATION.md #18712 kernel.logger block, reconstructed line-for-line from that PR's diff. It must be reported failing, must carry TS2341, must survive the tolerance rule, and must still carry its elision marker.

Observed, every run:

  GREEN_CONTROL  compiles clean — `@objectstack/*` resolves through the built exports maps.
  FIRING_CONTROL reports 5 diagnostic(s) including TS2341 at block line 13 — the pre-#18712
                 `kernel.logger` block, reconstructed from that PR's diff, is caught.
  FIRING_CONTROL also carries an elision marker, so the EXCLUSION reading SKIPS it.

Five TS2341, at block lines 13–17: one per constructor the block passed kernel.logger to.

⭐ The firing control earned its keep during this task. The instrument was first written against ts.createProgram + getPreEmitDiagnostics, then rewritten onto the tsc binary because check:parse-guard correctly forbids the parser API outside scripts/ts-parse.mjs (and createProgramChecked is the wrong tool here — it exits 3 on the first syntactic diagnostic, and blocks that do not parse are this census's subject). The first binary version reported zero diagnostics for the firing control, and the control refused the run. Cause, a measured property of the CLI: when any file in a program has a syntax error, tsc reports the syntax errors and never type-checks ANY file. One pass over this corpus returned 772 TS1xxx and not one semantic diagnostic. Hence two passes: pass 1 finds the blocks that do not parse, pass 2 re-runs over the ones that do. Without the firing control this would have shipped as "the corpus has no type errors".

Cross-check: over all 1002 corpus blocks, the binary implementation and the compiler-API one it replaced agree on every block's verdict — raw, tolerant and well-formed-and-wrong — with zero disagreements.

Wiring options, with costs, for the maintainer's letter

Nothing below is done in this PR. What it would take for the number to reach zero is stated first, because it decides everything else.

Reaching zero, hand-written stratum: 76 blocks across 31 files — 58 repairs (a block that parses and is wrong) plus 18 explicit partial tags (a block that is a signature listing or a fragment). Reaching zero, CHANGELOG stratum: impossible in principle. Those files are release-owned, compiled from changesets, and deliberately show removed APIs. Any gate whose population includes CHANGELOG.md is a gate that can never be green without rewriting published release history. ⇒ The only gate-able population is hand-written package-root Markdown.

  1. Leave it unwired (what this PR does). Cost: zero CI time. The number is known only when someone runs it — the declared-not-enforced shape this family of cards is about, mitigated only by the instrument being cheap (12s) and carrying its own controls.
  2. Advisory job, printing the census on every PR. Cost: it resolves through built .d.ts, so it must piggyback on a job that has already built (or pay a full build). Nothing blocks. Risk: an advisory red rides main's merge ref into every later PR until someone stanches it.
  3. Shrink-only ratchet with a checked-in per-file baseline. Cost: a required job (maintainer's floor) plus a baseline artefact and its merge-driver discipline. Opening number to hold: 76 hand-written TOLERANT failures. Buys: the number can only go down, and grandfathered debt is visible rather than forgotten.
  4. Gate at zero over hand-written package-root Markdown. Cost: the 76-block entry price above, paid before the gate can be armed, plus the partial tag convention landing first. Buys: the strongest guarantee, and the only one that makes the card's class genuinely closed.
  5. Diff-scoped gate — only blocks a PR adds or edits. Cost: block identity across edits; today's debt is grandfathered on day one. Buys: catches the PR-docs(core): repair four copy-fails in PHASE2_IMPLEMENTATION.md #18712 class at the moment it is written, at near-zero entry price. ⚠️ Grandfathering is exactly the failure mode core: PHASE2_IMPLEMENTATION.md teaches @objectstack/core/security, a subpath the package exports in no entry #15931 demonstrated, so this option is only honest if the standing debt number is recorded somewhere that gets re-read — which is what option 3 is for. 3 + 5 together is the combination with no known hole.

Verification

  • node scripts/measure-markdown-ts-blocks.mjs --self-test — 30 cases pass, 30 batteries at or above the pinned floor of 30. Roster-and-handshake shape copied from scripts/check-agent-model-declared.mjs; needs no build.
  • node scripts/measure-markdown-ts-blocks.mjs — exit 0, both controls fire, 12s under the shared verify lock.
  • All 26 gates scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives for this diff: 26/26 exit 0. Two were red on the first draft and drove real corrections — check:parse-guard (the parser-API rule) and check:entry-guard (the import guard).
  • eslint . --no-inline-config --format json — the union, not a narrowing: 6830 files governed by eslint's own config, 0 errors, 0 warnings, at 3c64a03c4e.
  • Control-character self-scan over the new file: no match.
  • skip-changeset, measured with a positive control rather than assumed: the new file's basename has 0 hits anywhere under any package's built dist/; the positive control ObjectKernel has 4 hits in packages/core/dist; no package's files[] names a path outside its own directory, so a repo-root scripts/ file cannot enter any tarball.

Acceptance notes

  • Not repaired, by instruction. 58 hand-written blocks parse and are wrong. A sample, all real: packages/runtime/README.md — TS2339, Property 'use' does not exist on the promise the kernel builder returns, because the front-page example chains .use(...) without awaiting it (the type in the diagnostic is a Promise parameterised by ObjectKernel; spelled out here because a raw angle-bracket fragment does not survive this surface); packages/plugins/plugin-auth/README.md — 'plugins' does not exist in type 'ObjectKernelConfig'; packages/services/service-job/README.md — 'timeout' does not exist in type 'JobScheduleOptions'. Did you mean 'timeoutMs'?; packages/drivers/driver-mongodb/README.md — 'driver' does not exist in type 'ObjectStackDefinitionInput'; packages/services/service-cache/README.md — the documented MyCache class does not implement ICacheService.
  • The sharpest instance found, not filed by me: packages/spec/V3_MIGRATION_GUIDE.md tells a reader to import @objectstack/spec/hub, @objectstack/core/errors and @objectstack/core/plugin. None of the three is in its package's exports map, so the import fails for anyone who copies it. This is the check-published-readme-exports family arriving through a document that gate's population does not reach — V3_MIGRATION_GUIDE.md is not in @objectstack/spec's files[]. Reported to the dispatching seat with dedupe words rather than filed here, to keep this PR a census.
  • Noted, not filed: the census counts packages/*/CHANGELOG.md because they are package-root Markdown, and they dominate the raw number (720 of 1002 blocks). They are structurally unrepairable in a code PR. Successor: whoever writes the wiring letter — the stratum split exists precisely so that reader does not have to re-derive it.
  • No census snapshot is committed. A number checked into a file is a number that rots; the instrument regenerates it in 12 seconds. The census lives in this PR body and in the report comment on [finding] packages/core/PHASE2_IMPLEMENTATION.md's fenced TypeScript blocks are in no tsc program — a block that has NEVER compiled survived two sweeps, and the gate #15931 proposed would not have caught it #18715.
  • Population note: the card's example glob is packages/*/*.md, which misses nested package roots such as packages/plugins/plugin-auth/. The instrument measures every package root and reports the card's literal glob as a named sub-stratum, so both readings are available and neither is assumed.

⚠️ Line count corrected by the dispatching seat, not by the author. This body first read 「918 lines」, which was the count at the first commit, before the two-pass tsc rewrite; the landed file at 3c64a03c4e is 1086 lines (git show … | wc -l, taken by the seat). The author found the slip after the single permitted body write and reported it rather than patching, because the role file allows a dev exactly one body write in the POST /pulls stroke and routes later changes through its report to the seat. ⛔ Nothing else in this body was changed, and everything else in it was re-verified by the author at the landed head.


Generated by Claude Code

A census instrument, not a gate: it exits 0 on every census outcome, is
wired into no CI job and no package test/lint chain.

It extracts fenced ts/typescript/tsx blocks from Markdown at the root of
every package under packages/, compiles them in one tsc program against a
`paths` map generated from each package's own exports map, and reports the
number two ways — raw (upper bound) and under a tolerance rule that forgives
only what a legitimately partial block can produce (lower bound).

Two controls run on every census. GREEN_CONTROL must compile, so an unbuilt
workspace refuses instead of reporting a catastrophe made of nothing.
FIRING_CONTROL reconstructs the pre-#18712 `kernel.logger` block and must be
caught with TS2341 — and, because that block ends in an elision marker, it is
also the standing proof that an exclusion-style elision rule would have hidden
the one defect in this family anyone has measured.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>
…k census

Three corrections the controls and the repo's own gates forced:

- `check:parse-guard` is right that nothing outside `scripts/ts-parse.mjs` may
  reach the TypeScript parser API, and `createProgramChecked` is the wrong tool
  here: it exits 3 on the first syntactic diagnostic, and blocks that do not
  parse are this census's subject. Driving `tsc --noEmit --strict` asks #18715's
  own question instead.
- `check:entry-guard`: the census dispatch now sits behind `isEntrypoint`.
- TWO passes, because of a measured property of the CLI: when any file in a
  program has a syntax error, tsc reports the syntax errors and never
  type-checks ANY file. One pass returned 772 TS1xxx and zero semantic
  diagnostics — FIRING_CONTROL among them, which is what caught it.

Over all 1002 corpus blocks the binary implementation and the compiler-API one
it replaces agree on every block's verdict, with zero disagreements.

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

Copy link
Copy Markdown
Collaborator Author

Pre-landing readings for PR #18751, written 2026-09-17T19:03Z — ⭐ including two REQUIRED contexts that are SKIPPED, and why that is not a gap.

domain:engine execution seat, session_01CqmCgU5RGDoJYhHUMVp2af. ACCEPT and the full review are on card #18715.

check reading
① in-seat clause-② review ⛔ none owed — Clause-②: no, and --pair confirms both carriers agree with no widening tell
② check-clause2-carriers.mjs --pair 18751 exit 0, re-taken at this stroke
③ CI on 3c64a03c4e 31 names · 0 pending · 0 non-green
governed-surface predicate (--pr 18751) 0 of 1 path ⇒ ordinary queue landing

⚠️⚠️ Build Core and Temporal Conformance (live PG + MySQL) read skipped, and both are REQUIRED contexts

A landing decision taken on 「0 non-green」 alone would have passed two required contexts unmeasured. It is not a gap, and the roster says why in its own words — check-expected-skips' CORE_REASON:

gated on ci.yml's filter job core output; a diff outside the core path set skips it on the PR head, and the merge-queue build widens every output to true and runs it on the merged tree

Verified against the filter contract itself (ci.yml:173-179): the core path set is packages/**, examples/**, apps/!(docs)/**, package.json, pnpm-lock.yaml, tsconfig.json. ⇒ scripts/** is not in it, and this PR changes exactly one file under scripts/.

⇒ ⭐ The two contexts skip by design on the PR head and their verdict is taken in the queue build, on the merged tree. ⛔ Landing does not skip them; it is where they run.

The other skips are roster members too: Check PR Size and Auto Label (the labeled-event re-fire), Check Changeset (this PR carries skip-changeset, measured with a positive control), Packed-tarball smoke (opt-in), Console Pin Gate and Build Docs (their own filter outputs), and the raw matrix-template check name (the Dogfood Regression Gate shard row, whose literal spelling carries an un-expanded Actions expression — reproduced here with the brace syntax elided, because post-stamped correctly refuses a double-brace opener it does not recognise) / Dogfood Verify CLI — the raw matrix template name a job reports before expansion, while the aggregate Dogfood Regression Gate runs if: always() and reads success.

⏹️ ⛔ This card does NOT close on merge: the PR is Part of #18715, deliberately. At landing the seat strips pm:dispatched in the same stroke — a Part of card does ⛔ not auto-close and would otherwise count as in-flight forever — and moves it to needs-user-decision with the four-axis analysis for the wiring letter.

⇒ Going ready and into the merge queue (SQUASH). This seat follows it to MERGED.


Generated by Claude Code

Merged via the queue into main with commit aa910a6 Sep 17, 2026
51 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-18715-markdown-codeblock-typecheck-census branch September 17, 2026 19:32
os-try-charles pushed a commit that referenced this pull request Sep 18, 2026
Splits PR #18751's census by publication (a block is published when its
document is inside its package's `files[]`) and corrects the published half:
43 of 44 syntactically-valid-and-wrong blocks across 20 package READMEs.

Internal documents (ADVANCED_FEATURES.md, PHASE2_IMPLEMENTATION.md,
V3_MIGRATION_GUIDE.md, ARCHITECTURE.md, …) are untouched, and no gate,
ratchet or CI wiring is added — #18715 ruling F.

Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…bjectstack-ai#18968)

Part of objectstack-ai#18915

Clause-②: no

Executes maintainer decision batch objectstack-ai#156 item 2 — ruling F on objectstack-ai#18715. No
gate, no ratchet, no CI wiring is added: this is the user-facing half of
PR objectstack-ai#18751's census, corrected.

## Act 1 — the split, re-derived

PR objectstack-ai#18751's instrument re-run on a fresh `origin/main` (`node
scripts/measure-markdown-ts-blocks.mjs --json`, workspace built first,
all three controls behaving: GREEN clean, FIRING reports TS2341,
UNPUBLISHED_SUBPATH reports a counted TS2307 on each of its three
specifiers).

The card's numbers reproduce exactly:

```
handwritten stratum   blocks 282   files 54   raw 195   tolerant 76   well-formed-and-wrong 58
changelog  stratum    blocks 720   (out of scope)
```

Split by publication — a block is *published* when its document is
inside its package's `files[]`:

| reading | total | published | internal |
|:---|---:|---:|---:|
| tolerant failures | 76 | **54** | 22 |
| syntactically valid and wrong | 58 | **44** | 14 |

The split is unambiguous in this repo: every non-private package's
`files[]` is `["dist","README.md","CHANGELOG.md"]` (only
`@objectstack/spec` lists more), so **the published package-root
Markdown is exactly `README.md`**, and every other package-root document
— `ADVANCED_FEATURES.md`, `PHASE2_IMPLEMENTATION.md`,
`V3_MIGRATION_GUIDE.md`, `ARCHITECTURE.md`, `DEVELOPMENT_PLAN.md`,
`PLUGIN_STANDARDS.md`, `REST_API_PLUGIN.md`,
`ZOD_SCHEMA_AUDIT_REPORT.md`, `STACKBLITZ.md`,
`CLIENT_SPEC_COMPLIANCE.md`, `ROADMAP.md`, plus the private
`packages/qa/*` READMEs — is internal.

Confirmed against the packer rather than asserted from `package.json`,
with a control from the same population that must read the other way:

```
npm pack --dry-run --json, packages/core
  README.md                   in tarball: True     # positive control
  ADVANCED_FEATURES.md        in tarball: False    # same population, must be absent
  PHASE2_IMPLEMENTATION.md    in tarball: False
```

No count in the split is zero, so no zero needed pairing.

**`packages/spec/liveness/README.md`** (PR objectstack-ai#18938's surface) measures as
**published** — `liveness` is a `files[]` entry, and `npm pack` puts
`liveness/README.md` and `liveness/state-counts.md` in the tarball. It
is nonetheless **not in this card's population**: the census population
is Markdown at a *package root* (a directory carrying a `package.json`),
`packages/spec/liveness` is not one, and the file therefore contributes
none of the 76/58. No overlap with objectstack-ai#18938, and nothing of theirs is
touched here.

## Act 2 — the published corrections

**43 of the 44** published syntactically-valid-and-wrong blocks now
compile; 20 READMEs changed. Re-measured on the same instrument:

| reading | before | after |
|:---|---:|---:|
| published, syntactically valid and wrong | 44 | **1** |
| published, tolerant failures | 54 | 11 |
| internal, syntactically valid and wrong | 14 | 14 (untouched) |
| internal, tolerant failures | 22 | 22 (untouched) |

The 11 remaining published tolerant failures are **10 syntax-only
blocks** — bare type-signature fragments in `service-automation`,
`trigger-record-change`, `trigger-schedule` and `service-job`, the
`needs-explicit-partial-tag` class the instrument's own forward
convention describes — plus the one block below. Syntax-only blocks are
outside act 2's mandate, which is the syntactically-valid-and-wrong set.

What was wrong, by class:

- **Legacy option vocabulary.** `@objectstack/client-react`'s hooks take
`fields` / `orderBy` / `limit` / `where`; the README still wrote
`select` / `sort` / `top` / `filters`, and read `PaginatedResult.value`
where the member is `records`. Also `timeout` to `timeoutMs`
(`service-job`), `attempts` to `maxAttempts` (`service-queue`), `filter`
to `where` (`IDataEngine.find`).
- **An async API used as a chainable one.** `ObjectKernel.use()` is
async and resolves to the kernel, so `kernel.use(a).use(b)` does not
type-check at all; and `ObjectKernelConfig` has no `plugins` member.
- **Interfaces implemented but never imported.** Four plugin examples
wrote `implements Plugin` with no import — which silently bound to the
DOM's `Plugin` — and three omitted the required `init`.
`PluginContext.getService` is declared with a type parameter that has no
default, so every example that read a service back left it `unknown`.
- **Removed or never-existing API, rewritten rather than left as a
fossil.** `@objectstack/driver-memory`'s default export is a legacy
`onEnable` object that `kernel.use()` refuses on both the type and the
boot path — the quick start now registers through `DriverPlugin`, and
the "Key Exports" row that called it a drop-in plugin is corrected with
it. Its persistence adapters take an options bag and hang under
`persistence.adapter`. `defineStack` has no `driver` key.
`@objectstack/rest`'s `RestServer` takes the host `IHttpServer` as its
first argument and `registerRoutes()` takes none; `RouteManager` is
constructed on a server. `ObjectSchema.parse()` returns the value — the
`{ success, data }` envelope belongs to `safeParse`. `useMutation` has
no `onMutate` and no mutation context, so the "Optimistic Updates"
example was rebuilt on the options it does have.
- **Untyped parameters under `--strict`** in React and handler examples,
annotated.

Two of the 44 (`packages/cli`, `packages/mcp`) were **measurement
artefacts worth stating plainly**: `objects: Object.values(objects)`
over an elided `./src/objects` barrel. The forgiven TS2307 leaves the
namespace `any`, and `Object.values` then infers its type parameter from
the union-shaped contextual type, producing a mismatch a reader's own
resolvable barrel would not produce. Both now name the objects they
import, which is typed and clearer either way.

Beyond the counted blocks, the same defect class was corrected in three
further `client-react` blocks (Master-Detail, Search with Debounce, and
the Type Safety comment) that the census does not flag only because they
import nothing and so type-check as `any`. Leaving `data.value` and
`select:` standing one section below a corrected copy of themselves was
not defensible; this is called out because it is work outside the
measured set.

## The one block deliberately left, and why

`packages/plugins/knowledge-ragflow/README.md` writes
`source.options.datasetId`. That is what the shipped adapter reads
(`extractRagflowOptions` casts the source to a shape carrying an
optional `options` record, and its error text names
`source.options.datasetId`), and it is **not** what
`KnowledgeSourceSchema` declares — the declared key is `adapterConfig`,
and the schema is a plain `z.object`, so a parse would strip `options`
outright.

Correcting the document to `adapterConfig` would make it compile and
stop working. Correcting the adapter is a runtime change, out of this
card's scope, and picks a winner between two live spellings.
Contract-first says the defect is upstream, so the block is left as it
stands and the conflict is reported for the maintainer instead of being
papered over in a docs PR.

That is why this PR says `Part of objectstack-ai#18915` and not `Fixes`.

## Changeset — measured for this diff, not inherited

The house `skip-changeset` argument for docs cards is "no package's
`files[]` reaches `content/docs/**`". **It inverts here.** `README.md`
is listed in `files[]` for every one of the 20 packages touched, so the
bytes this PR changes are inside the published tarball — measured above
with `npm pack --dry-run` and a same-population control that reads the
other way.

AGENTS.md: `skip-changeset` "is for a diff that publishes nothing from
any released package". This diff publishes changed bytes from twenty
released packages, and those bytes are what an upgrading agent reads. So
this PR carries a **`patch`** changeset naming all twenty, and ⛔ no
`skip-changeset` label.

## Scope

- Touched: `packages/*/README.md` only, plus the changeset. ⛔ No
internal document, ⛔ no `CHANGELOG.md`, ⛔ no `content/docs/**`, ⛔ no
runtime code, ⛔ no gate or CI wiring.
- The changeset file is the one path outside the claim's declared file
surface (`packages/**/README.md`); it is the companion artefact the
measurement above obliges, and it is named here rather than slipped in.

## Acceptance notes

- `packages/client/README.md` documents `data.find()`'s legacy
vocabulary (`select` / `filters` / `sort` / `top`). Unlike the
`client-react` case this **compiles** — `QueryOptions` still accepts it
— so it is out of this card's set, but `find` itself carries
`@deprecated` and `data.query()` is the canonical call. Noted, not
filed.
- The `check:undeclared-dep-imports` family is not affected: no
`package.json` moved.

## Verification

Tree: `a8b75f978` (`origin/main` merged in, workspace rebuilt, `pnpm
install --frozen-lockfile` after the lockfile moved). Every number below
is from that tree.

**The census, final run.** `node scripts/measure-markdown-ts-blocks.mjs
--json`, exit 0, all three controls behaving (`green=clean firing=fires
unpublished-subpath=fires`):

```
handwritten  blocks 284  files 54  raw 172  tolerant 33  well-formed-and-wrong 15
   published                                tolerant 11  well-formed-and-wrong  1
   internal                                 tolerant 22  well-formed-and-wrong 14
```

284 rather than 282 because the `observability` wiring block, which
redeclared `metrics` four times in one fence, is now three fences — one
per deployment, which is how a reader picks between them.

**Gate families**, derived in-worktree from this tree with `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no stale-tree warning after the merge):
**63 derived, 63 run, all exit 0**, reconciled back through `--ran` with
each command's exit code captured before any pipe — "63 derived
familt(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all
63 recorded an exit code and none of them is 3)".
`check:pm-dispatch-gates` is not among the derived families for this
change set. That reconciliation answers one link only; it is not a
complete account of CI.

**Tests.** The diff changes no TypeScript, so no package's `tsc` program
or vitest source set moves. Two suites do read a README this PR edits,
found by grepping every test file in `packages/` for `README.md` (19
hits, triaged by the path each one reads), and both were run:

```
pnpm --filter @objectstack/client exec vitest run --maxWorkers=2 src/readme-package-install-example.test.ts
  Test Files 1 passed (1) · Tests 6 passed (6) · CLIENT_README_EXIT=0
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/api/package-api.test.ts src/kernel/plugin-structure.test.ts
  Test Files 2 passed (2) · Tests 81 passed (81) · SPEC_TARGETED_EXIT=0
```

The first one parses `packages/client/README.md` with the TypeScript
parser and validates the manifest it finds against the install contract
— it is the pin that a README edit in that package could break.

**Lint.** Zero files in this diff are in eslint's population, measured
from eslint's own config rather than assumed, with a control from the
same tree that must read the other way:

```
ESLint#calculateConfigForFile
  packages/types/README.md                             rules: 0  ignored: true
  .changeset/18915-published-readme-examples-compile.md rules: 0  ignored: true
  packages/types/src/index.ts                          rules: 6  ignored: false   # control
```

`eslint.config.mjs` scopes every block to
`{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so no configuration in this diff can
move an untouched file's verdict either.

**Control bytes.** `grep -naP` for the C0 range over every changed path:
no hits; a fixture carrying one byte in that range hits, so the scan is
live. `pnpm check:nul-bytes` is among the 63 green gates.

Authored by Claude Code, session `session_017ef78bLdybu3AffehKkhfk`.


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

Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…n, with the create-side contrast taken on this body (objectstack-ai#18961)

Fixes objectstack-ai#18686

Clause-②: no

Governed fact layer only:
`.claude/skills/pm-dispatch/references/platform-readings.md` (the
PR-body footer cell, plus one rider row) and, for the ceiling alone,
`scripts/pm/check-skill-line-ratchet.mjs`. No SKILL.md edit, no other
file. `skip-changeset`: nothing published moves — `.claude/**` and
`scripts/pm/**` are both on the fast track and no package's `files[]`
ships either path.

## THIS BODY IS THE INSTRUMENT, not a description of one

The card's named unknown is the missing half of the controlled contrast
that `platform-readings.md` itself asks for, verbatim and untranslated:
「⛔ 无受控对照(同通道只差该块两送)⇒ 是拟合不是定论,⛔ 不外推到别的动作。」 The absent input is the
**create** side with a tail that is the signature line **without** the
preceding rule line — which is exactly the spelling the old prescription
asks for: 「⇒ PR 正文页脚不带前置横线,且写后回读正文 —— 那是唯一检测手段;评论两形皆可。」

So this body was composed to end in that spelling and sent once, through
the single budgeted `POST /repos/OWNER/REPO/pulls` (bare REST,
`Content-Type: application/json`, `draft: true`). The sent tail was,
byte for byte:

1. the last prose line of the Acceptance notes section;
2. **one** blank line;
3. **no** rule line — that is the whole point of the send;
4. the signature line in its session-URL spelling, 84 bytes (leading
underscore, `Generated by`, the bracketed link text, the parenthesised
`https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF` URL, trailing
underscore);
5. **no** trailing newline.

Read it back and count:

```bash
curl -sS -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/objectstack-ai/objectstack/pulls/PR-NUMBER --output /tmp/pr.json; EXIT=$?
node -e 'const b=JSON.parse(require("fs").readFileSync("/tmp/pr.json","utf8")).body;
  console.log("stored bytes", Buffer.byteLength(b,"utf8"));
  console.log("signature lines", (b.match(/^_Generated by /gm)||[]).length);
  console.log("tail", JSON.stringify(b.slice(-200)));'
```

⭐ The stored tail and the signature count are **not quoted in this
body**, and that is deliberate rather than an omission: this body **is**
the probe, so its stored form **is** the reading, and ⛔ it is never
`PATCH`ed afterwards — the patch cell appends unconditionally (table
below), so a tidy-up would destroy the very evidence it tidied. **Count
the signature lines at the bottom of what you are reading**: one means
the platform left the rule-less spelling alone; two means it appended
its own block. The exact byte readings live in the `os-dev-report`
comment on objectstack-ai#18686 and in this round's report.

## Pre-registered rows — so the landed row cannot be fitted to the
outcome

The row that states the create-side cell is written **before** the
read-back, one text per outcome, and the commit that follows this body
carries whichever the read-back supports. Both are inside the 120-byte
line cap.

**If the append fired (two signature lines):**

| where | bytes | text |
|:--|--:|:--|
| rewrite of the 拟合 caveat row | 109 | `-
受控对照已补齐:同通道同动作只差该块两送,尾部为无横线页脚时照追加成两条。` |
| new row after it | 103 | `- ⇒ 判据确认是送出体尾部;⛔ 未控:MCP 建侧该输入、评论侧与改侧其余输入。` |
| rewrite of the prescription row | 111 | `- ⇒
处方按动作分裂:建侧送全块(横线加页脚),改侧不送页脚;⛔ 无一形两动作通吃。` |

**If it did not fire (one signature line, stored byte-identical but for
the known trailing-newline strip):**

| where | bytes | text |
|:--|--:|:--|
| rewrite of the 拟合 caveat row | 106 | `-
受控对照已补齐:同通道同动作只差该块两送,尾部为无横线页脚时一字不追加。` |
| new row after it | 105 | `- ⇒ 判据不是整块而是尾部有无页脚行;第四形条件须按此收窄,⛔ 不按整块判。` |
| rewrite of the prescription row | 106 | `- ⇒
处方按动作分:建侧两拼法各存活一条,改侧不送页脚;⛔ 无一形两动作通吃。` |

Any **third** reading — for instance the tail being stripped, which is
what the older 「尾部横线与其后的署名页脚一并被吃」 cell records for a different channel —
is reported as measured, with the row written to it and the divergence
from both pre-registrations named. ⛔ No row states an outcome that was
not read back.

## The two cells this PR states per ACTION

### PATCH — one rule with two measured inputs (landed in the first
commit)

From the `domain:engine` seat's reading on this card, comment
5719534135, on PR objectstack-ai#18751:

| leg | sent tail | signature lines read back |
|:--|:--|--:|
| 1 | the rule line plus the session-URL signature — the tail **is** the
block | 2 |
| 2 | the same shape again, after stripping the platform's bare one | 2
|
| 3 | no rule line, no signature | 1 |

⇒ on that channel the append is **unconditional**: leg 1 sent exactly
the shape the fourth form says is left alone and got a second signature
anyway, and leg 2 reproduces it. The cell therefore becomes **one rule**
plus its discriminating input, instead of two rows of inputs that never
said the append is unconditional. The already-recorded attribution cost
(「⇒ 代价是归属:裸形无 session id,按此剥净的 PR 正文不载明哪个会话写的」) is **already present**
and is not restated.

### CREATE — the fourth form, and now its contrast

The fourth form 「建 PR 两通道同判 —— 送出体尾部不是 `---` 加页脚块时,追加一条同形页脚」 already had
one direction measured (tail already the block ⇒ 一字不追加, both channels).
This body supplies the other direction on the same channel and the same
action, differing **only** in that block. That is the contrast the
caveat row names, and the caveat row is replaced by what is now
controlled together with what still is not.

## Rows landed in the first commit, with bytes

Patch-cell rule, **rewritten in place**, 117 bytes:

```text
- 改侧 · 裸 REST `PATCH /pulls` 恒追加一条裸页脚并保留既有页脚,差恰 58 字节,与尾部无关。
```

Patch-cell discriminating input, **new**, 113 bytes:

```text
- 尾部已是 `---` 加页脚块也照追加,重送复现 ⇒ 建侧的不追加判据 ⛔ 不外推到改侧。
```

Rider row, **new**, 113 bytes:

```text
- `comments` 与枚举不等是瞬态旗:计数收敛后记录已失而差值归零 ⇒ ⛔ 不作唯一防线。
```

The rider row comes from the open question on PR objectstack-ai#18950 for card objectstack-ai#18052,
answered by the seat as riding this card. Its provenance is the filer's
own correction, comment 5654594070: the count read 872 at 15:0xZ and 818
at 16:37Z against an enumeration of 816 plus 2 self-added comments, i.e.
the **count** converged on the enumeration within roughly 90 minutes,
and 「收敛后记录已失」 is the filer's own consequence — a seat arriving after the
window sees no discrepancy at all because the records are gone. Note the
rider's byte count is **113**, not the 118 the dispatch estimated: the
dispatch's spelling measured **123** bytes, over the 120-byte cap, so it
was tightened rather than wrapped.

## Dedupe accounting for the seat's ACCEPT

Measured on `origin/main` at `16cb493d5` with `git grep` over
`.claude/skills/pm-dispatch`, each subject word carried with a lit
control:

| row | 候选 | 落地 | 已有 | 拒收 | absence evidence (lit control in brackets) |
|:--|--:|--:|--:|--:|:--|
| patch-cell rule (rewrite) | 1 | 0 new | 1 | 0 | rewrite of an existing
row; buys no line |
| patch-cell input | 1 | 1 | 0 | 0 | no row anywhere says the patch
append is independent of the sent tail; the two existing rows state
inputs only [control: 「同路送无页脚正文存回恰一条」 is present, so the cell itself is
found] |
| rider row | 1 | 1 | 0 | 0 | `收敛` 0 hits in `platform-readings.md` (6
hits elsewhere in the skill, all about CI convergence); `瞬态` 2 hits,
both unrelated (`unstable` transient, PR-files transient 404);
`updated_at` 0 hits in that file [control: `计数` 9 hits in the same file]
|
| create-cell conclusion | 1 | 1 | 0 | 0 | the caveat row itself
declares the contrast missing, so the conclusion cannot already be there
|
| prescription row (rewrite) | 1 | 0 new | 1 | 0 | rewrite of an
existing row; buys no line |

⛔ Nothing was paid in place by re-wrapping: re-wrap funding is refused
by the standing rule, and the two rewrites above buy no line — they
correct a row's scope, which is why they are counted as 落地 0.

Refused as rows, and named so the seat can check the judgement: the
observation that the older 「一并被吃」 pair records no channel and no action
for its own cell is a **meta-observation about the record**, not a
reading, so it goes in the Acceptance notes rather than into the map.

## The ceiling

`platform-readings.md` sits at 466 / 466 with zero measured folds, so
every added row is paid by the standing one-file exception in
pm-dispatch SKILL.md, verbatim and untranslated:
「唯一例外:`platform-readings.md` 增量抬上限到落地行数,免决策卡,记 `ruledRaises` 引常设裁决。」
「条件:席位验收评论逐条核实、去重计数(候选/落地/已有/拒收)、一事一行、不计重排。」

The ceiling moves to the **landed** line count (two rows landed in the
first commit, one more with the create-side conclusion), recorded as the
**eighteenth** `ruledRaises` record on this destination in
`scripts/pm/check-skill-line-ratchet.mjs`, quoting that ruling in the
shape the seventeen existing records use, with the line-by-line
accounting written beside the ceiling entry as the file's own convention
requires. `was` does not move. `node
scripts/pm/check-skill-line-ratchet.mjs --self-test` is run before and
after, and both verdict lines are quoted in the report.

## Reader test

A seat opening a PR today with the rule-less spelling the old
prescription prescribes reads back **N** signature lines, where N is
what the bottom of this body shows. With the corrected rows it reads
back **one**, because the prescription is split per action: the create
side sends the whole block, the patch side sends no signature at all.

## Gates

Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`, run in full, exit codes
captured by redirect-then-status rather than through a pipe, and
reconciled with `--ran`. `check:pm-dispatch-gates` exceeds the
container's foreground cap, so it is detached and waited on with `tail
--pid`, and its own verdict line is read. Results are in the report; a
family that had not converged by report time is recorded as NOT MEASURED
with its reason rather than as a pass.

## Acceptance notes

- The older PR-body pair 「尾部横线与其后的署名页脚一并被吃,而写调用照报成功」 and 「去掉横线只写页脚则原样存活」
records **no channel and no action** for its own cell, which is what let
its prescription be read as universal in the first place. Naming that is
a meta-observation about the record rather than a platform reading, so ⛔
it is not filed and ⛔ no row states it. Whoever next measures that cell
on a named channel is the natural carrier.
- The create-side MCP channel with a rule-less tail stays **unmeasured**
here, on purpose: the budget for this card is one create write, and it
was spent on the bare-REST cell the card names.

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

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

---------

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

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants