Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(metadata-protocol): add the missing `typecheck` script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger by zhuangjianguo · Pull Request #14018 · objectstack-ai/objectstack · GitHub
Skip to content

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger - #14018

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck
Sep 1, 2026
Merged

fix(metadata-protocol): add the missing typecheck script and repair all 63 test-layer type errors, graduating the package out of the DEBT ledger#14018
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-13978-metadata-protocol-typecheck

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes#13978

Route (a) FIX — chosen on the taxonomy, not on effort appetite. All of this
package's tsc --noEmit errors are repaired, the typecheck script is added,
and the package graduates out of the DEBT ledger in this same PR.

Notation: TypeScript generics are written with square brackets throughout
this body (Omit[Partial[Row], 'metadata']). The GitHub body sanitizer eats
angle-bracket fragments, including inside backticks.

The count is 63, not 159 — and it is already in a ledger

Two corrections to the card, both measured on current main:

1. The fresh count is 63. Re-measured with the dependency closure built
first: tsc --noEmit -p tsconfig.json reports 63 error lines, all 63 in
*.test.ts, zero in non-test source
(the card's 159 was measured on
aee1fd9ec2). The card's three named offenders reproduce to the unit —
protocol.stored-migration.test.ts 27, seed-loader-multi-value-reference.test.ts
10, protocol.stored-conversions.test.ts 7 — so the difference is not those
files. 63 is independently corroborated below.

2. "Nothing runs tsc over this package" is no longer true. The package
carries a DEBT entry in scripts/check-type-check-coverage.mjs, and its
recorded number is 63 with a per-code composition that matches this
measurement exactly, code for code:

codeledger notemeasured here
TS2322x3434
TS700613 (noise)13
TS2835x99
TS2532 / TS2493x2 each2 / 2
TS2550x11
TS2353 / TS2339x1 each1 / 1
total6363

CI's typecheck-debt lane runs pnpm check:type-check-debt, whose MEASURED
invariant re-runs the package's own tsc --noEmit and reds if the count
exceeds the recorded one. So the test surface was already type-checked in CI —
as a shrink-only ratchet tolerating 63, not as a gate tolerating zero. That
is a materially smaller and declared gap than the card describes, and it is
the gap this PR closes.

Taxonomy: 63 errors, 6 root causes, 2 of them dominant

Grouped by root cause rather than by code:

#root causeerrorsrepair
1metadata collapsing to string in the seed-row parameter type342 lines
2extensionless ./seed-loader import under NodeNext229 lines
32-arity insert mock read at call[2]41 line
4evt helper narrower than the projector it feeds11 line
5insertMany not on the declared IDataEngine return type11 line
6.at(-1) needs lib ES202211 line

They collapse. Two causes carry 56 of the 63, and the ledger's own note
already said so: "read it as two repairs, not as forty." That is the criterion
— route (a) was chosen because the population is concentrated and mechanical,
which is exactly the condition under which (a) is cheap and strictly better than
declaring the surface unchecked.

Root cause 1 (34 errors) — the repair was already written down in this package

makeStubEngine in the two affected files declared its parameter as
Partial[Row] & { type, name, metadata: unknown }. On the row metadata is the
stored string, and string & unknown is string — so the plain intersection
refuses every body written as an object literal, which is the entire seeding
convenience the harness exists for. Both harnesses already did
typeof r.metadata === 'string' ? r.metadata : JSON.stringify(r.metadata) at
runtime: the declared type contradicted the implementation beside it.

Three sibling test files in this same package already use the correct form, and
one of them states the reason verbatim:

metadata is Omit-ed out of the Partial[Row] half rather than merely
intersected over it: on the row metadata is the STORED string, and
string & unknown is string, so a plain intersection refuses every body
written as an object literal — the seeding convenience this harness exists
for. Stated once, here, so the fixtures below need no casts.

So this is drift back to an idiom the package had already adjudicated, not a new
shape. Two of the 34 wore a different face (Type '{ name, label }' is not assignable to type 'string') — the same cause showing metadata already
collapsed to string.

Root cause 2 (22 errors) — one broken import, and the cascade it caused

Nine test files imported './seed-loader' without the extension. Under
NodeNext that does not resolve, so every symbol it names becomes any — which
is where the 13 TS7006 came from. AGENTS.md predicts this exact shape ("a pile
of TS7006 implicitly-any is usually one broken import upstream ... fix the
extension first and re-measure"), and the re-measure confirms it: 63 to 41,
with the 9 TS2835 and all 13 TS7006 clearing together from one 9-line codemod.

'./seed-loader.js' is not a preference here: 8 other files in this package
already spell it that way, index.ts among them, and these 9 were the only
extensionless relative imports in the entire package.

Evidence

Measured at 381db66f4e, the final commit of this branch, dependency closure
built first.

  • pnpm --filter @objectstack/metadata-protocol typecheckexit 0
    (63 to 41 after cause 2, to 0 after all six).
  • pnpm --filter @objectstack/metadata-protocol testexit 0,
    Test Files 148 passed | 2 skipped (150), Tests 2060 passed | 10 skipped (2070).
    The 2/10 skips are pre-existing; this PR adds no .skip, .only, @ts-ignore
    or eslint-disable (verified over the diff).
  • The zero is not vacuous:tsc --listFiles puts 150 of 150*.test.ts
    files in the program, including all four of the most-repaired files.
  • pnpm check:type-check-coverageexit 0, and its own verdict line moves
    from 12 ledger entries to 11: 67/78 workspace packages type-checked (plus the root), 11 in the DEBT ledger (309 frozen raw errors). 372 minus 309 = 63,
    the entry this PR retires.
  • 39 further gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — all exit 0. Two returned exit 3
    (PREREQUISITE NOT MET, nothing measured, not a pass)
    :
    check:dual-build-cjs-loads and check-test-completeness.mjs, both needing a
    full workspace build/test summary this seat did not run. CI runs both with the
    prerequisite met.

turbo behaviour, verified rather than inherited (with a firing control)

The card's claim that turbo run typecheck runs nothing for a script-less
package is correct, and the dry run alone would not have shown it:

  • Dry-run graph lists @objectstack/core#typecheck — with
    command: 'NONEXISTENT'.
  • Real run, turbo run typecheck --filter=@objectstack/core: exit 0,
    Tasks: 3 successful — and those three are spec:build, types:build,
    metadata-core:build, the ^build dependencies. core:typecheck appears
    nowhere in the run output.
  • Firing positive control, same command shape against this package now that
    it declares the script: @objectstack/metadata-protocol:typecheck: cache miss, executing then tsc --noEmit, 14 tasks. The task really does run once the
    script exists, which is also the proof that this PR puts the package into the
    typecheck-workspace lane.

The shrink-only ratchet does not count up

check:type-check-debt returns exit 3 locally (PREREQUISITE NOT MET — it
refuses to measure without the full built closure, deliberately, since an
unbuilt closure measures a different world). Declared narrowing: rather than
run that repo-level sweep, this seat measured the only coupling that could move
another entry — packages/rest is the one ledgered package whose tsconfig
paths redirects @objectstack/metadata-protocol into the producer's
source. Its test program contains 23 metadata-protocol source files and 0
*.test.ts
; none of the 15 files this PR edits is among them. So no other
ledger entry can move, and this package's own entry is deleted, never raised.

Scope

Not expanded to the sibling packages. Triage measured 12 of 73 packages
with no typecheck script, packages/core among them. Those 159 — now 63 —
errors are this package's; every other package has its own population needing
its own triage. No script was added anywhere else. packages/core is untouched
and stays in the ledger.

One bounded in-place edit is declared here rather than left implicit: two
prose comments in scripts/check-type-check-coverage.mjs made present-tense
claims about the entry this PR deletes ("metadata-protocol quotes the misleading
note ... and is skipped for exactly that reason — correct entry"). Left as-is
they would describe a ledger row that no longer exists, which is the note-rot
failure that file legislates against. Both are now past tense and point at the
synthetic self-test case that carries the shape. Comment-only; the rule and its
self-test fixtures are unchanged.

Non-test source: untouched. The STOP condition for route (a) was that the
compiler would demand a behaviour-adjacent change; it did not. Every repair is a
test file, and the two that looked producer-shaped were not: body is already
declared on the projector's own parameter
(MetadataMutationEvent & { body?: unknown }) and only the test helper was
narrower; insertMany is already on the fixture and only the helper's declared
return type hid it.

Changeset

None — skip-changeset. This PR releases nothing: 13 test files, a
CI-internal gate script, and one dev-only scripts entry in a manifest. No
src/ non-test source, no published behaviour, no consumer-observable change.
That is the gate's own prescription for the tests-only / CI-internal-script case.
The label is applied on the PR.

Clause-②: no. Path limb — nothing under packages/spec/src/**. Content
limb — no accept/reject behaviour moves and no published surface changes; a
typecheck script is not shipped to consumers, and the diff reached no non-test
source, so the condition the PM flagged as the one thing that would flip this
did not occur.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actionsgithub-actionsBot added size/s dependencies Pull requests that update a dependency file tests labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 62a137baecb75d03c028779ef5c2ed0eacad8399packageMentionDocs.

@zhuangjianguoClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, and ⭐ the card's headline was falsified in the seat's own PR body

domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L. ⛔ Not an approving review — agent seats do not submit those.


1. ⭐⭐ Two of the card's factual claims are false, and the work is still right

(a) The count is 63, not 159. Re-measured on current mainwith the dependency closure built first: 63 error lines, all in *.test.ts, zero in non-test source. The card's own three named offenders reproduce to the unit (27/10/7) — so the delta is not those files, and the 159 was not a different code population.

⭐ The diagnosis is the valuable part: the 159 was most plausibly measured without the dependency closure built, which inflates counts through unresolved-import cascades. ⚠️ That is the same failure family I routed to #13513 forty minutes ago — an unbuilt or half-built closure producing TS#### noise that reads like real errors in the author's own code. Two seats, two cards, one underlying trap: a type error count taken against an unbuilt closure is not a reading.

(b) "Nothing runs tsc over this package" is NOT TRUE on current main. The package carries a DEBT ledger entry recording exactly 63, with a per-code composition matching the fresh measurement code-for-code (TS2322×34, TS7006×13, TS2835×9, TS2532×2, TS2493×2, TS2550×1, TS2353×1, TS2339×1 = 63), and CI's typecheck-debt lane already re-ran the package's own tsc every run and reds if the count exceeds it.

The real gap was narrower than the card claimed, and already declared: the surface was ratcheted at 63-tolerated rather than gated at zero. That is the gap this PR closes, and it is a real one — but it is not "type-checked by nothing".

⚠️ Recording against myself: my dispatch order carried the card's framing forward ("⭐ the one-line manifest edit is not the work — the 159 errors are"). The number was wrong and the premise was overstated, and the seat corrected both in the PR body rather than quietly shipping against a false description.

2. Route (a) taken, on the taxonomy rather than on effort

63 errors collapse to 6 root causes, two of which carry 56 of the 63 — so (a) is cheap and strictly better, which is the criterion I asked for.

⭐ The largest (34× TS2322) is worth naming because it is the same shape this lane keeps meeting: makeStubEngine typed its seed rows as Partial<Row> & { metadata: unknown }, but on the row metadata is the stored string, and string & unknown is string — so the declared type refuses every object-literal body while the harness beside it already JSON.stringifys an object at runtime. A declared type contradicting the implementation next to it. Repair is 2 lines using Omit<Partial<Row>, 'metadata'>the idiom three sibling test files in this package already use, one of them documenting it verbatim.

3. ⭐⭐ A2.2 was confirmed by RUNNING it, and the distinction was load-bearing

I asked for a firing positive control rather than an inherited claim. The seat found that the naive check would have lied:

  • Negative: turbo run typecheck --filter=@objectstack/core (still no script) exits 0 with "Tasks: 3 successful" — and those three are spec:build, types:build, metadata-core:build, i.e. the ^build dependencies. core:typecheck appears nowhere in the run output (grep count 0).
  • ⚠️The dry-run graph DOES list @objectstack/core#typecheck, with command NONEXISTENT. ⇒ A dry-run graph alone would have "confirmed" the task exists. Reading the graph instead of the run is exactly the secondary-artifact substitution that has produced most of this lane's misreads today.
  • Positive control fires: the same command against metadata-protocol now prints "cache miss, executing" then tsc --noEmit, 14 tasks — which doubles as proof the PR actually puts the package into the typecheck-workspace lane.

4. My hard constraint held: the ratchet counted DOWN, never up

A2.4 came back partly falsified — "and this is the finding": adding the script does not leave CI unchanged, because the package was never absent from the type-check gates. RECONCILED forbids a script and a DEBT entry coexisting, so the entry must be deleted in this same PR.

check:type-check-coverage: 12 entries / 372 frozen errors → 11 entries / 309
delta = 63 — exactly the retired entry

⭐ The entry is DELETED, not raised. ⇒ The MAINTAINER-ONLY ratchet-raising path was never touched and the STOP did not fire. That was the constraint I was least willing to delegate, and it is satisfied by measurement rather than by assurance.

⚠️ Declared narrowing, and a good one: check:type-check-debt refuses locally (exit 3, prerequisite not met), so instead of claiming a repo-level sweep the seat measured the only coupling that could move another entrypackages/rest is the single ledgered package whose tsconfig paths redirects @objectstack/metadata-protocol into the producer's source; its test program holds 23 metadata-protocol source files and 0*.test.ts, and none of the 15 edited files is among them. ⇒ No other ledger entry can move. That is a bounded argument, not a hope.

5. The green is not vacuous

typecheck EXIT=0, and the control: tsc --listFiles puts 150 of 150*.test.ts in the program, including all four most-repaired files. Suite: 148 passed / 2 skipped (pre-existing), 2060 tests passed / 10 skipped, and the diff adds no.skip / .only / @ts-ignore / eslint-disable — grepped over the diff rather than asserted.

⭐ No ablation, and the report says why instead of fabricating one: the single causal claim (the TS7006 pile being downstream of the broken import) was proven by the staged re-measure 63 → 41 → 0, in which the 9 TS2835 and all 13 TS7006 cleared together from a 9-line codemod whose landing was proven on disk by before/after greps in both directions.

6. Scope held

⛔ No sibling package was given a script — the other 11 script-less packages (including packages/core) are untouched, as ruled. Clause-②: no: manifest, tsconfig-adjacent and test files only; nothing under packages/spec/src/**, no published surface, no accept/reject change.


Status

⛔ Not released: CI is still in flight at the time of writing. The bar is every check completed and green — not the required subset — because enqueue is irreversible here. On green: flip ready, arm auto-merge, verify enqueue, then verify the landing by content on origin/main (⛔ never the merged boolean) and strip pm:dispatched from #13978.

⚠️ One thing this PR makes worth deciding separately, ⛔ not here: triage measured that 12 of 73 packages have no typecheck script, and this PR proves the pattern for retiring one — fix, add script, delete the DEBT entry in the same stroke. packages/core is on that list and is the kernel. That is a class card, and its population is now known.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 1, 2026 00:17
@zhuangjianguo
zhuangjianguo added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 030dc17Sep 1, 2026
38 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-13978-metadata-protocol-typecheck branch September 1, 2026 00:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@zhuangjianguo@claude