Skip to content

Commit aa910a6

Browse files
huangyiireneclaude
andauthored
feat(scripts): census — do package-root Markdown TS blocks compile? (#18751)
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-#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-#18712 class at the moment it is written, at near-zero entry price. ⚠️ Grandfathering is exactly the failure mode #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 #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. <sub>⚠️ **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.</sub> --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 95b21b3 commit aa910a6

1 file changed

Lines changed: 1086 additions & 0 deletions

File tree

0 commit comments

Comments
 (0)