Skip to content

Commit 7cbe705

Browse files
claude[bot]claude
andauthored
fix(tooling): put three more package-root plugin manifests inside a tsc program (#14458)
check:type-check-coverage's isUncheckedSourceCandidate skipped depth === 0 (the package root) unconditionally, so a package-root .ts file was invisible to SOURCES_COVERED regardless of content. This is why #13284's driver-memory / plugin-hono-server manifests went unchecked for as long as they did. Per the triage on #14386 (comment 5504408509), this admits depth === 0 only for a declared, exact-name allowlist (ROOT_SOURCE_FILES, currently just objectstack.config.ts) rather than every package-root file -- the wider 104-file question stays explicitly unresolved. The uncheckedByDir aggregation now keys a root-level file at '.' (which posix.join collapses to the package's own directory) instead of the rel.slice(0, -1) garbage key the old indexOf('/') === -1 arithmetic produced. The three sites the widened predicate then surfaces (plugin-auth, plugin-security, service-i18n) are put into a program: widened include on the existing sibling noEmit programs for the first two, a new sibling tsconfig.typecheck.json (following the driver-memory shape from #13284) for service-i18n, which had none to widen. Putting service-i18n's manifest into a program onboards that package's first tsc program to reach the bare @objectstack/spec specifier (src/ only ever imports subpaths), which check:type-source-resolution correctly flagged. Repaired via that gate's own documented onboarding-limb registry re-baseline (the sanctioned tool for a dep reached only through a newly-onboarded program -- paths is measured wrong for this shape on PR #12570), with --list before/after numbers stated in place. Fixes #14386 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 54f2240 commit 7cbe705

7 files changed

Lines changed: 245 additions & 27 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
"@objectstack/plugin-security": patch
4+
"@objectstack/service-i18n": patch
5+
---
6+
7+
fix(tooling): put three more package-root plugin manifests inside a tsc program (#14386)
8+
9+
`check:type-check-coverage`'s `isUncheckedSourceCandidate` skipped `depth === 0`
10+
(the package root) unconditionally, so a package-root `.ts` file was invisible
11+
to SOURCES_COVERED no matter what it contained — not reported, and not
12+
tracked either. That is exactly why #13284's `driver-memory` /
13+
`plugin-hono-server` manifests went unchecked for as long as they did:
14+
`pnpm --filter <pkg> typecheck` exited 0 with a file no tsc program read, and
15+
the coverage gate called the package COVERED at the same time.
16+
17+
This finds three more package-root manifest authoring sites the same hole
18+
hid, all `objectstack.config.ts`: `plugin-auth`, `plugin-security` and
19+
`service-i18n`. The gate now admits `depth === 0` only for a declared,
20+
exact-name allowlist (`ROOT_SOURCE_FILES`, `objectstack.config.ts` its only
21+
member) — not every root-level file, which stays the unresolved "104-file"
22+
scope question this card explicitly declines to settle (comment
23+
5504408509 on #14386) — and each of the three manifests now sits inside a
24+
program its package's own `typecheck` script invokes: a widened `include` on
25+
the existing sibling `noEmit` program for `plugin-auth`
26+
(`tsconfig.examples.json`) and `plugin-security` (`tsconfig.scripts.json`),
27+
and a new sibling `tsconfig.typecheck.json` for `service-i18n` (which had no
28+
sibling to widen), following the `driver-memory` shape #13284 established.
29+
30+
All three type-check clean at zero recorded debt — no ledger entry is added.

packages/plugins/plugin-auth/tsconfig.examples.json

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// The EXAMPLES-layer type-check program for @objectstack/plugin-auth (#10869).
1+
// The EXAMPLES-layer type-check program for @objectstack/plugin-auth (#10869),
2+
// WIDENED (#14386) to also carry this package's manifest authoring site.
23
//
34
// `packages/plugins/plugin-auth/examples/` held `basic-usage.ts` -- the file
45
// `content/docs/permissions/authentication.mdx` publishes as "Basic Auth
@@ -17,6 +18,20 @@
1718
// would start shipping the example. This program emits nothing, so it can
1819
// neutralise `rootDir` without touching what ships.
1920
//
21+
// #14386 ADDED `objectstack.config.ts` (the package-ROOT manifest, `defineStack`
22+
// composing `./src/manifest`) to `include` below, joining `examples/**/*` in
23+
// this same sibling rather than opening a second one: it is a package-root file
24+
// for the identical reason `examples/` is -- `tsconfig.json`'s `include` is
25+
// `src/**/*`, which cannot match a root-level file either -- and this sibling
26+
// already neutralises `rootDir` to `.` for exactly that reason. Before this
27+
// change no tsc program read the manifest: `check:type-check-coverage`'s
28+
// `isUncheckedSourceCandidate` skips `depth === 0` by construction (the
29+
// package-root line), so `pnpm --filter @objectstack/plugin-auth typecheck`
30+
// exiting 0 carried no information about it, of the same shape #13284 found
31+
// and fixed in `packages/drivers/driver-memory` and `packages/plugins/
32+
// plugin-hono-server`. See `scripts/check-type-check-coverage.mjs`'s
33+
// `ROOT_SOURCE_FILES` for the declared allowlist this repairs against.
34+
//
2035
// STRICTNESS IS INHERITED and deliberately not relaxed: `strict`,
2136
// `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns` and the rest come
2237
// from the root config through `tsconfig.json`. The directory type-checks clean
@@ -27,10 +42,10 @@
2742
"extends": "./tsconfig.json",
2843
"compilerOptions": {
2944
"noEmit": true,
30-
// `.` rather than the inherited `src`, because the file this program checks
31-
// is the one outside `src`. Safe precisely because nothing is emitted from
32-
// here -- see the header.
45+
// `.` rather than the inherited `src`, because the files this program
46+
// checks (`examples/**/*`, `objectstack.config.ts`) sit outside `src`.
47+
// Safe precisely because nothing is emitted from here -- see the header.
3348
"rootDir": "."
3449
},
35-
"include": ["examples/**/*"]
50+
"include": ["examples/**/*", "objectstack.config.ts"]
3651
}

packages/plugins/plugin-security/tsconfig.scripts.json

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// The SCRIPTS-layer type-check program for this package (#11351).
1+
// The SCRIPTS-layer type-check program for this package (#11351), WIDENED
2+
// (#14386) to also carry this package's manifest authoring site.
23
//
34
// `scripts/i18n-extract.config.ts` is the input to this package's i18n
45
// extraction: it composes the package's own objects and translation bundles
@@ -15,6 +16,20 @@
1516
// `scripts/` would put the directory in front of the emit. This program
1617
// emits nothing.
1718
//
19+
// #14386 ADDED `objectstack.config.ts` (the package-ROOT manifest,
20+
// `defineStack` composing `./src/manifest`) to `include` below, joining
21+
// `scripts/**/*` in this same sibling rather than opening a second one: it is
22+
// a package-root file for the identical reason `scripts/` is -- `tsconfig
23+
// .json`'s `include` is `src/**/*`, which cannot match a root-level file
24+
// either. Before this change no tsc program read the manifest:
25+
// `check:type-check-coverage`'s `isUncheckedSourceCandidate` skips
26+
// `depth === 0` by construction (the package-root line), so `pnpm --filter
27+
// @objectstack/plugin-security typecheck` exiting 0 carried no information
28+
// about it, of the same shape #13284 found and fixed in `packages/drivers/
29+
// driver-memory` and `packages/plugins/plugin-hono-server`. See
30+
// `scripts/check-type-check-coverage.mjs`'s `ROOT_SOURCE_FILES` for the
31+
// declared allowlist this repairs against.
32+
//
1833
// STRICTNESS IS INHERITED and deliberately not relaxed: `strict`,
1934
// `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns` and the rest
2035
// come from the root config through `tsconfig.json`. The directory
@@ -26,16 +41,17 @@
2641
// `rootDir` IS NOT OVERRIDDEN HERE, and that is the package-specific half of
2742
// this file. `tsconfig.json` already widens it to `../..` (= `packages/`) to
2843
// carry the `paths` redirect of a sibling package to source, so the inherited
29-
// value ALREADY contains `scripts/` and this program needs no change. Setting
30-
// it to `.` also measures 0 today, and is still the wrong value: it would
31-
// re-narrow the root below the redirected source, so the first script here
32-
// that reaches that sibling would report TS6059 about the CHECK rather than
33-
// about the code.
44+
// value ALREADY contains `scripts/` -- and, for the same reason, the
45+
// package-root `objectstack.config.ts` added above -- and this program needs
46+
// no change. Setting it to `.` also measures 0 today, and is still the wrong
47+
// value: it would re-narrow the root below the redirected source, so the
48+
// first script here that reaches that sibling would report TS6059 about the
49+
// CHECK rather than about the code.
3450
//
3551
{
3652
"extends": "./tsconfig.json",
3753
"compilerOptions": {
3854
"noEmit": true
3955
},
40-
"include": ["scripts/**/*"]
56+
"include": ["scripts/**/*", "objectstack.config.ts"]
4157
}

packages/services/service-i18n/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"scripts": {
2222
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
23-
"typecheck": "tsc --noEmit",
23+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.typecheck.json",
2424
"test": "vitest run"
2525
},
2626
"dependencies": {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// The MANIFEST-layer type-check program for @objectstack/service-i18n (#14386).
2+
//
3+
// `objectstack.config.ts` sits at the PACKAGE ROOT and declares this service's
4+
// plugin manifest inline (a `defineStack({ manifest: { ... } })` call with no
5+
// import from `src/`, unlike `plugin-auth` / `plugin-security`, so the manifest
6+
// body itself is unread by any program too, not merely the composing site).
7+
// Until this file existed no tsc program read a line of it: `tsconfig.json`
8+
// selects `src`, and that entry cannot match a root-level file. The annotation
9+
// was therefore decorative -- `pnpm --filter @objectstack/service-i18n
10+
// typecheck` (a bare `tsc --noEmit`, this package's only program before this
11+
// change) still exited 0 regardless of what the manifest object contained.
12+
// `check:type-check-coverage`'s `isUncheckedSourceCandidate` skips
13+
// `depth === 0` by construction (the package-root line), so the gate reported
14+
// this package COVERED at the same time -- the same green-over-unread-source
15+
// shape #13284 found and fixed in `packages/drivers/driver-memory` and
16+
// `packages/plugins/plugin-hono-server`, and #14386 found two more instances
17+
// of (`plugin-auth`, `plugin-security`) beside this one. See
18+
// `scripts/check-type-check-coverage.mjs`'s `ROOT_SOURCE_FILES` for the
19+
// declared allowlist this repairs against.
20+
//
21+
// A SIBLING rather than a wider `include` on `tsconfig.json`, the distinction
22+
// #5475 drew for `packages/spec` and #10756 for `packages/objectql/scripts`,
23+
// and the shape `packages/drivers/driver-memory/tsconfig.typecheck.json`
24+
// (#13284) used for the identical case: that config EMITS (`rootDir: "src"`,
25+
// `outDir: "dist"`), so widening it to reach the package root would put the
26+
// manifest in front of the emit -- the measured failure mode there is TS6059
27+
// ("File ... is not under 'rootDir' ... 'rootDir' is expected to contain all
28+
// source files"), and it fires under `--noEmit` too. This program emits
29+
// nothing, so it can neutralise `rootDir` without touching what ships. `tsup`
30+
// builds `src/index.ts` (via the root `tsup.config.ts`) and `files` publishes
31+
// `dist` only, so nothing about the published package moves.
32+
//
33+
// STRICTNESS IS INHERITED and deliberately not relaxed: `strict`,
34+
// `esModuleInterop`, `forceConsistentCasingInFileNames` and the rest come from
35+
// the root config through `tsconfig.json`. The manifest type-checks clean
36+
// under them -- it enters with ZERO recorded debt, and there is no ledger here
37+
// to record any in.
38+
{
39+
"extends": "./tsconfig.json",
40+
"compilerOptions": {
41+
"noEmit": true,
42+
// `.` rather than the inherited `src`, because the file this program
43+
// checks is the one outside `src`. Safe precisely because nothing is
44+
// emitted from here -- see the header.
45+
"rootDir": "."
46+
},
47+
"include": ["objectstack.config.ts"]
48+
}

scripts/check-type-check-coverage.mjs

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,8 +1165,19 @@ const PHANTOM_PIN_DEBT = {};
11651165
// `packages/cli/test/helpers/serve-process.ts`
11661166
//
11671167
// This invariant governs the SECOND group only, which is why the observation
1168-
// half takes files at `depth > 0`. The line is drawn there on evidence rather
1169-
// than convenience, and the evidence cuts both ways, so both halves are here:
1168+
// half takes files at `depth > 0` -- EXCEPT for `ROOT_SOURCE_FILES`, the
1169+
// declared allowlist #14386 carved out of the first group for the 5
1170+
// `objectstack.config.ts` instances above: those are manifest AUTHORING sites
1171+
// (of the same class #13284 put inside a tsc program), not build/test
1172+
// tooling, and #14386 found three of the five (`plugin-auth`,
1173+
// `plugin-security`, `service-i18n`) still outside every program with no
1174+
// ledger entry -- a green gate over unread source. The FOR/AGAINST split
1175+
// below is about the other 49 (`vitest.config.ts`, `tsup.config.ts`, ...),
1176+
// which #14386 explicitly declined to re-decide (comment 5504408509: "queue
1177+
// now" vs "not this card") -- see `ROOT_SOURCE_FILES`'s own docblock for why
1178+
// a declared name, not a wider predicate, is the line drawn there. The line
1179+
// is drawn there on evidence rather than convenience, and the evidence cuts
1180+
// both ways, so both halves are here:
11701181
//
11711182
// FOR -- the 54 are one repo-wide convention, not 42 independent decisions.
11721183
// Every package's `include` is `src/**/*`; a tool config sits at the package
@@ -1564,26 +1575,58 @@ function unreadFiles(rels, programs) {
15641575
return rels.filter((rel) => !programs.some((c) => configCovers(c, rel)));
15651576
}
15661577

1578+
/**
1579+
* Root-level (`depth === 0`) filenames admitted into SOURCES_COVERED despite
1580+
* the package-root line below (#14386).
1581+
*
1582+
* The census behind that line found 54 root configs against 11 files in a
1583+
* source directory, and most of the 54 are build/test tooling
1584+
* (`vitest.config.ts`, `tsup.config.ts`) rather than authored source --
1585+
* deciding whether THAT population owes a program or a ledger entry is a
1586+
* scope decision about build tooling, not about authored source, and #14386
1587+
* (comment 5504408509) is explicit that a dev repair must not settle it: "if
1588+
* whoever implements this believes the allowlist is the wrong shape, they
1589+
* stop and file a needs-user-decision card -- they do not widen the predicate
1590+
* to all of depth 0".
1591+
*
1592+
* `objectstack.config.ts` is admitted because it is not tooling -- it is a
1593+
* plugin manifest authoring site of exactly the class #13284 put inside a tsc
1594+
* program (`packages/drivers/driver-memory`, `packages/plugins/
1595+
* plugin-hono-server`), and #14386 found three more package-root instances of
1596+
* it sitting outside every program with no ledger entry: a green gate over
1597+
* unread source. Declared as an exact filename set rather than matched by a
1598+
* pattern (e.g. any `*.config.ts`) so that growing it to cover a new filename
1599+
* stays a decision made here, on its own card, instead of happening as a side
1600+
* effect of some unrelated file landing at a package root.
1601+
*/
1602+
const ROOT_SOURCE_FILES = new Set(['objectstack.config.ts']);
1603+
15671604
/**
15681605
* Is this file one SOURCES_COVERED asks about (#10756)?
15691606
*
15701607
* Three exclusions, each load-bearing:
15711608
*
1572-
* `depth > 0` the package-root line. A `vitest.config.ts` beside the
1573-
* manifest is a tool's entry point, not a directory of source,
1574-
* and the census kept the two apart -- 54 root configs against
1575-
* 11 files in a source directory. Argued both ways on
1576-
* UNCHECKED_SOURCE_DEBT.
1609+
* `depth > 0` the package-root line, EXCEPT for `ROOT_SOURCE_FILES`
1610+
* above. A `vitest.config.ts` beside the manifest is a
1611+
* tool's entry point, not a directory of source, and the
1612+
* census kept the two apart -- 54 root configs against 11
1613+
* files in a source directory. Argued both ways on
1614+
* UNCHECKED_SOURCE_DEBT for everything else at the root; a
1615+
* declared manifest authoring site is not "everything else"
1616+
* (#14386).
15771617
* `.d.ts` a declaration file STATES types rather than being checked for
1578-
* them, so "no program reads it" is not the same finding.
1618+
* them, so "no program reads it" is not the same finding --
1619+
* still enforced even for a `ROOT_SOURCE_FILES` name, so a
1620+
* hypothetical declaration-file entry there could never
1621+
* bypass this exclusion.
15791622
* test files TESTS_COVERED's subject, decided per file over there. Counting
15801623
* them here too would bill one hidden file to two ledgers.
15811624
*
15821625
* @param {string} name basename
15831626
* @param {number} depth 0 at the package root
15841627
*/
15851628
function isUncheckedSourceCandidate(name, depth) {
1586-
if (depth === 0) return false;
1629+
if (depth === 0 && !ROOT_SOURCE_FILES.has(name)) return false;
15871630
if (!SOURCE_FILE.test(name) || TEST_FILE.test(name)) return false;
15881631
return !name.endsWith('.d.ts');
15891632
}
@@ -1663,7 +1706,15 @@ function testCoverage(dir, scripts) {
16631706
const uncheckedByDir = new Map();
16641707
if (scripts.typecheck !== undefined) {
16651708
for (const rel of unreadFiles(sourceRels, accountedPrograms(configs, invoked))) {
1666-
const top = rel.slice(0, rel.indexOf('/'));
1709+
// `rel.indexOf('/')` is -1 for a package-ROOT file (#14386's
1710+
// `ROOT_SOURCE_FILES` admits those past `isUncheckedSourceCandidate`
1711+
// now) -- `.slice(0, -1)` on that would produce a garbage key (the
1712+
// basename minus its last character), not a missing one. `'.'` is the
1713+
// sensible root key: fed through `posix.join(dir, top)` below it
1714+
// collapses to the package's own directory, which is exactly what an
1715+
// unread root-level file is "inside" -- there is no narrower directory
1716+
// to name.
1717+
const top = rel.includes('/') ? rel.slice(0, rel.indexOf('/')) : '.';
16671718
uncheckedByDir.set(top, (uncheckedByDir.get(top) ?? 0) + 1);
16681719
}
16691720
}
@@ -3733,6 +3784,28 @@ function selfTest() {
37333784
state: okState,
37343785
expect: [/packages\/a\/scripts: 2 non-test source file\(s\) here sit outside every tsc program/],
37353786
},
3787+
{
3788+
// #14386: every source-layer case above is a SUBDIRECTORY case
3789+
// (`packages/a/scripts`) -- that is exactly why the hole this card
3790+
// fixes survived. `testCoverage()`'s aggregation keys a root-level
3791+
// file (no `/` in its package-relative path) at `'.'`, which
3792+
// `posix.join(dir, '.')` collapses to the package's own directory --
3793+
// so the dir this fixture feeds in (`'packages/a'`, with no
3794+
// subdirectory suffix) is what the real walk now actually produces
3795+
// for an unread `objectstack.config.ts`, and this pins that it
3796+
// renders sensibly rather than as the `rel.slice(0, -1)` garbage key
3797+
// the old `indexOf('/') === -1` arithmetic would have produced.
3798+
label: 'an unread ROOT-level source file in a COVERED package fails SOURCES_COVERED too, keyed at the package directory itself (#14386)',
3799+
packages: [
3800+
pkg('a', {
3801+
scripts: { typecheck: 'tsc --noEmit' },
3802+
uncheckedSources: [{ dir: 'packages/a', files: 1 }],
3803+
}),
3804+
],
3805+
root: okRoot,
3806+
state: okState,
3807+
expect: [/packages\/a: 1 non-test source file\(s\) here sit outside every tsc program/],
3808+
},
37363809
{
37373810
label: 'an UNCHECKED_SOURCE_DEBT entry covers it, but only with a reason',
37383811
packages: [
@@ -4249,14 +4322,24 @@ function selfTest() {
42494322
}
42504323

42514324
// SOURCES_COVERED's file-level predicate (#10756). Each exclusion is a way
4252-
// this invariant can be silently wrong: lose `depth > 0` and 42 packages'
4253-
// tool configs flood the ledger, lose the test check and one hidden file is
4254-
// billed to two ledgers, lose `.d.ts` and the gate reports a finding about a
4255-
// file that states types rather than being checked for them.
4325+
// this invariant can be silently wrong: lose `depth > 0` (unqualified) and
4326+
// 42 packages' tool configs flood the ledger, lose the test check and one
4327+
// hidden file is billed to two ledgers, lose `.d.ts` and the gate reports a
4328+
// finding about a file that states types rather than being checked for
4329+
// them.
42564330
const sourceCandidateCases = [
42574331
{ label: 'a module in a subdirectory is the subject', name: 'dry-run-hash-compat.ts', depth: 1, expect: true },
42584332
{ label: 'the same module at the package root is not', name: 'dry-run-hash-compat.ts', depth: 0, expect: false },
42594333
{ label: 'a package-root tool config is out of scope by the depth rule', name: 'vitest.config.ts', depth: 0, expect: false },
4334+
// #14386: `ROOT_SOURCE_FILES` is the depth-0 exception, and it is an
4335+
// EXACT-name allowlist, not "anything that looks like config at the
4336+
// root" -- the next two rows pin both sides of that line.
4337+
{ label: 'a declared root source file IS in scope despite depth 0 (#14386)', name: 'objectstack.config.ts', depth: 0, expect: true },
4338+
{ label: 'a root-level BUILD tool config stays out of scope -- the 104-file question is not this predicate\'s to answer', name: 'tsup.config.ts', depth: 0, expect: false },
4339+
// Defense in depth: even a name that WERE declared in `ROOT_SOURCE_FILES`
4340+
// must still lose to the `.d.ts` exclusion below -- a states-types file
4341+
// is never "unread source" regardless of depth.
4342+
{ label: 'a declaration file at the root is still excluded, even hypothetically declared', name: 'globals.d.ts', depth: 0, expect: false },
42604343
{ label: 'the SAME tool config inside a directory IS in scope', name: 'i18n-extract.config.ts', depth: 1, expect: true },
42614344
{ label: 'a test file belongs to TESTS_COVERED, not here', name: 'engine.test.ts', depth: 1, expect: false },
42624345
{ label: 'a spec file likewise', name: 'engine.spec.tsx', depth: 2, expect: false },

0 commit comments

Comments
 (0)