@xemahq/repo-build-tooling 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM with zero dependencies so the published artifact is the reviewed source.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -21,17 +21,19 @@
21
21
  "src"
22
22
  ],
23
23
  "bin": {
24
- "xema-scrub-swagger-paths": "src/scrub-swagger-plugin-paths.mjs",
24
+ "xema-check-no-vendored-deps": "src/check-no-vendored-deps.mjs",
25
25
  "xema-check-workspace-ranges": "src/check-workspace-range-matches-local.mjs",
26
- "xema-check-no-vendored-deps": "src/check-no-vendored-deps.mjs"
26
+ "xema-readiness": "src/readiness.mjs",
27
+ "xema-scrub-swagger-paths": "src/scrub-swagger-plugin-paths.mjs"
27
28
  },
28
29
  "exports": {
29
30
  "./check-no-vendored-deps": "./src/check-no-vendored-deps.mjs",
30
31
  "./check-workspace-range-matches-local": "./src/check-workspace-range-matches-local.mjs",
31
32
  "./package.json": "./package.json",
33
+ "./readiness": "./src/readiness.mjs",
32
34
  "./scrub-swagger-plugin-paths": "./src/scrub-swagger-plugin-paths.mjs"
33
35
  },
34
36
  "scripts": {
35
- "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs"
37
+ "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs"
36
38
  }
37
39
  }
@@ -75,19 +75,33 @@ export function satisfies(version, range) {
75
75
  return v[0] === base[0];
76
76
  }
77
77
 
78
- function listManifests() {
78
+ /**
79
+ * Every `package.json` in the repository, EXCLUDING `node_modules`, `dist` and
80
+ * `.git`.
81
+ *
82
+ * `-prune` rather than `-not -path`, and the difference is not a style
83
+ * preference. A `-not -path` exclusion on a node_modules glob FILTERS the
84
+ * results while still
85
+ * DESCENDING into every one of them, so this walked hundreds of thousands of
86
+ * installed files to discard all of them — and, because it descended, it raced
87
+ * anything writing there. Measured 2026-09-18 on xema-base: the walk entered
88
+ * `node_modules/.cache/xema-declared-kernel/.staging-<version>-XXXX/`, a
89
+ * temporary directory another step deleted mid-walk, `find` exited non-zero,
90
+ * and `execFileSync` threw. The gate did not report a violation — it CRASHED,
91
+ * which reads in CI as a failing check rather than an absent one.
92
+ *
93
+ * Pruning cannot race what it never enters.
94
+ */
95
+ export function listManifests(root = REPO_ROOT) {
79
96
  return execFileSync(
80
97
  'find',
81
98
  [
82
- REPO_ROOT,
83
- '-name',
84
- 'package.json',
85
- '-not',
86
- '-path',
87
- '*/node_modules/*',
88
- '-not',
89
- '-path',
90
- '*/dist/*',
99
+ root,
100
+ '(', '-name', 'node_modules', '-o', '-name', 'dist', '-o', '-name', '.git', ')',
101
+ '-prune',
102
+ '-o',
103
+ '-name', 'package.json',
104
+ '-print',
91
105
  ],
92
106
  { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 },
93
107
  )
@@ -131,16 +145,30 @@ export function findViolations({ rewrite = false } = {}) {
131
145
  }
132
146
  if (mutated) writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
133
147
  }
134
- return violations;
148
+ return { violations, scanned: manifests.length };
135
149
  }
136
150
 
137
151
  function main() {
138
152
  const rewrite = process.argv.includes('--fix');
139
- const violations = findViolations({ rewrite });
153
+ const { violations, scanned } = findViolations({ rewrite });
154
+
155
+ // A scan that read NOTHING and a scan that found nothing wrong print the
156
+ // same green and exit the same 0. Only the corpus separates them, so this
157
+ // refuses rather than reporting a pass it cannot justify. REPO_ROOT's own
158
+ // manifest is asserted to exist above, so zero here means the walk failed.
159
+ if (scanned === 0) {
160
+ console.error(
161
+ `::error::scanned 0 manifests under ${REPO_ROOT} — the walk found nothing, ` +
162
+ 'not even this repository\'s own package.json. Refusing to report a pass ' +
163
+ 'over an empty corpus.',
164
+ );
165
+ return 1;
166
+ }
140
167
 
141
168
  if (violations.length === 0) {
142
169
  console.log(
143
- 'workspace ranges match their local packages every build edge is visible to pnpm.',
170
+ `workspace ranges match their local packages in all ${scanned} manifest(s) ` +
171
+ 'every build edge is visible to pnpm.',
144
172
  );
145
173
  return 0;
146
174
  }
@@ -10,9 +10,12 @@
10
10
  * that specific wrong implementation.
11
11
  */
12
12
  import assert from 'node:assert/strict';
13
+ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { dirname, join, relative } from 'node:path';
13
16
  import test from 'node:test';
14
17
 
15
- import { satisfies } from './check-workspace-range-matches-local.mjs';
18
+ import { listManifests, satisfies } from './check-workspace-range-matches-local.mjs';
16
19
 
17
20
  test('^0.y.z pins the MINOR — the rule the whole check turns on', () => {
18
21
  // The real defect: a client moved 0.2.0 -> 0.3.0 and fell out of every
@@ -52,3 +55,51 @@ test('a prerelease is compared on its release part, never string-wise', () => {
52
55
  assert.equal(satisfies('0.3.0-rc.1', '^0.2.0'), false);
53
56
  assert.equal(satisfies('0.2.5-rc.1', '^0.2.0'), true);
54
57
  });
58
+
59
+ // ── The WALK, not the predicate ───────────────────────────────────────────
60
+ // A `-not -path` exclusion on a node_modules glob filters results while still
61
+ // DESCENDING into
62
+ // every one of them. That is not merely slow: it made the gate race a sibling
63
+ // step that was deleting a temp directory under `node_modules`, so `find`
64
+ // exited non-zero and the check CRASHED rather than reporting a violation.
65
+ // A crash reads in CI as a failing check rather than an absent one.
66
+ //
67
+ // Two-sided on purpose. Asserting only that the pruned paths are absent would
68
+ // also pass if the walk returned NOTHING — the empty-corpus failure this file
69
+ // already warns about, one function over.
70
+ test('listManifests does not DESCEND into node_modules — an untraversable dir there must not break the walk', () => {
71
+ const root = mkdtempSync(join(tmpdir(), 'xema-prune-'));
72
+ const blocked = join(root, 'node_modules', 'blocked');
73
+ try {
74
+ const write = (rel) => {
75
+ mkdirSync(join(root, dirname(rel)), { recursive: true });
76
+ writeFileSync(join(root, rel), '{"name":"x","version":"1.0.0"}\n');
77
+ };
78
+ write('package.json');
79
+ write('packages/real/package.json');
80
+ write('packages/real/dist/package.json');
81
+ write('node_modules/installed/package.json');
82
+
83
+ // Reproduces the real failure: a directory under node_modules the walk
84
+ // cannot traverse. A FILTERING walk descends, `find` exits non-zero on
85
+ // "Permission denied", and execFileSync THROWS -- the gate crashes instead
86
+ // of reporting a violation. A PRUNING walk never enters it.
87
+ //
88
+ // This is what the previous version of this test missed: `-not -path`
89
+ // filters the OUTPUT, so both walks return the identical set and an
90
+ // output assertion cannot tell them apart. Only descent is observable,
91
+ // and this is how you observe it.
92
+ mkdirSync(blocked, { recursive: true });
93
+ chmodSync(blocked, 0o000);
94
+
95
+ const found = listManifests(root).map((f) => relative(root, f)).sort();
96
+
97
+ // Control: the walk really ran and found the real manifests.
98
+ assert.deepEqual(found, ['package.json', 'packages/real/package.json']);
99
+ assert.equal(found.some((f) => f.includes('node_modules')), false);
100
+ assert.equal(found.some((f) => f.includes('dist')), false);
101
+ } finally {
102
+ try { chmodSync(blocked, 0o755); } catch { /* never created */ }
103
+ rmSync(root, { recursive: true, force: true });
104
+ }
105
+ });
@@ -0,0 +1,533 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One command between a source change and a push.
4
+ *
5
+ * -- WHY THIS EXISTS --
6
+ *
7
+ * Adopting one dependency on 2026-09-18 took SIX pushes to land, and not one of
8
+ * them found a new defect. Each found the NEXT derived artifact:
9
+ *
10
+ * push -> CI: the OpenAPI specs no longer describe the code
11
+ * push -> CI: the generated clients no longer match those specs
12
+ * push -> CI: the biome index ledgers no longer describe those clients
13
+ * push -> CI: ... and the ledgers again, for the specs regenerated since
14
+ *
15
+ * That is not four bugs. It is ONE ordered chain, discovered one failure at a
16
+ * time because nothing ran it in order:
17
+ *
18
+ * prisma -> build -> specs -> clients -> ledgers -> checks
19
+ *
20
+ * CI's job is to CONFIRM a state, not to teach the dependency graph. This runs
21
+ * the repository's OWN generators, in the order the graph actually has, so the
22
+ * chain converges before the push instead of across it.
23
+ *
24
+ * -- PRISMA COMES FIRST, AND THAT WAS LEARNED BY THIS FILE FAILING --
25
+ *
26
+ * A service typechecks against its GENERATED Prisma client, and that client is
27
+ * produced by `postinstall`. So it is current only as of the last install --
28
+ * which means a checkout that moves to a commit adding a schema column has a
29
+ * STALE client and no step regenerates it. Measured 2026-09-18: a column was
30
+ * present in `schema.prisma` and absent from the generated `index.d.ts`, and
31
+ * the build reported five errors of the form "Property 'x' does not exist",
32
+ * naming the SERVICE rather than the stale artifact behind it. That reads
33
+ * exactly like a defect in somebody's commit, and it is not.
34
+ *
35
+ * It is the same class as everything below it: a derived artifact with an
36
+ * upstream, discovered one failure at a time because nothing ran it in order.
37
+ *
38
+ * -- THE ORDER IS NOT A PREFERENCE, AND THE FIRST STEP IS THE ONE PEOPLE SKIP --
39
+ *
40
+ * `build` comes first because spec extraction BOOTS each service's Nest
41
+ * AppModule, and a module whose workspace dependencies are unbuilt fails with
42
+ * `MODULE_NOT_FOUND` on a sibling's generated client. Skipping it produces a
43
+ * FAKE cascade: regenerate the five services whose specs drifted, and the run
44
+ * fails on a sixth; regenerate that, and it fails on a seventh. Every one of
45
+ * those is the same unbuilt closure wearing a different service's name. One
46
+ * `pnpm -r build` first, and the cascade does not exist.
47
+ *
48
+ * The graph is ACYCLIC, so ONE correctly ordered pass converges. This
49
+ * deliberately does not loop. If a second pass would change anything, that is a
50
+ * violated dependency to find and fix, not a retry to institutionalise — so the
51
+ * final check step is what proves convergence, and a red one is a real finding.
52
+ *
53
+ * -- WHAT THIS IS NOT --
54
+ *
55
+ * It is not a release engine, a second source of truth, or a new authority over
56
+ * generated artifacts. Every step below shells out to a script this repository
57
+ * already owns and already runs in CI. If a step here disagrees with CI, CI is
58
+ * right and this file is wrong.
59
+ *
60
+ * It is also not a gate. It regenerates files, so it must never run inside
61
+ * `pnpm check`: a check that rewrites the tree it is grading cannot fail
62
+ * honestly.
63
+ */
64
+ import { execFileSync } from 'node:child_process';
65
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
66
+ import path from 'node:path';
67
+ import process from 'node:process';
68
+
69
+ // THE ROOT IS THE CONSUMER'S REPOSITORY, NEVER THIS PACKAGE'S LOCATION.
70
+ // This ships as a bin from `@xemahq/repo-build-tooling`, so `import.meta.url`
71
+ // resolves inside `node_modules` — deriving the root from it would grade the
72
+ // SDK's own directory and find nothing, which is the empty-scan pass this
73
+ // repository has shipped before. pnpm runs a `scripts` entry with cwd set to the
74
+ // directory of the manifest declaring it, so for a root `readiness` script that
75
+ // is exactly the repository root. It is asserted and PRINTED rather than assumed.
76
+ const ROOT = process.cwd();
77
+ if (!existsSync(path.join(ROOT, 'package.json'))) {
78
+ console.error(
79
+ `\nreadiness: REFUSING — no package.json at ${ROOT}.\n\n` +
80
+ 'This must run from a repository root (`pnpm readiness`), because every\n' +
81
+ 'step below is resolved relative to it. Grading the wrong tree silently is\n' +
82
+ 'worse than refusing.\n',
83
+ );
84
+ process.exit(1);
85
+ }
86
+ const scripts = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8')).scripts ?? {};
87
+
88
+ /**
89
+ * Is this checkout's INSTALL actually the one its lockfile describes?
90
+ *
91
+ * Everything below regenerates artifacts from whatever `node_modules` currently
92
+ * holds. If that disagrees with the lockfile, every spec, client and ledger this
93
+ * run produces is derived from the wrong bytes — and the resulting failure names
94
+ * the CONSUMER, never the stale artifact behind it.
95
+ *
96
+ * Measured FOUR times on 2026-09-18, each producing a confident wrong diagnosis
97
+ * that blamed somebody else's commit:
98
+ *
99
+ * · `@xemahq/contracts` resolving to 0.7.0 out of a foreign scratchpad while
100
+ * the fleet was on 0.13.0 — a `.strict()` schema then REFUSED a valid
101
+ * contribution, which reads as the declaration being broken;
102
+ * · a generated Prisma client missing a column its own schema declared;
103
+ * · `@xemahq/xema-decorators` at 0.17.0 (`humanBaseline` in ZERO files)
104
+ * against a lockfile saying 0.18.0, after a `--lockfile-only` relock;
105
+ * · `@xemahq/biome-supply-chain` at 0.3.0 against a lockfile saying 0.4.0,
106
+ * after a REBASE — `Cannot find module '.../verdicts'`, which reads exactly
107
+ * like somebody importing an unpublished subpath. It was published.
108
+ *
109
+ * A RELOCK IS NOT AN ADOPTION. A REBASE IS NOT AN ADOPTION. Only an install is.
110
+ *
111
+ * One-directional on purpose: extra versions PRESENT in the store are ordinary
112
+ * pnpm residue. A version the lockfile NAMES and the store LACKS is the
113
+ * direction that misleads.
114
+ */
115
+ function installBehindLockfile() {
116
+ const store = path.join(ROOT, 'node_modules', '.pnpm');
117
+ const lock = path.join(ROOT, 'pnpm-lock.yaml');
118
+ if (!existsSync(store) || !existsSync(lock)) return [];
119
+ const entries = readdirSync(store);
120
+ const missing = [];
121
+ const seen = new Set();
122
+ for (const [, name, version] of readFileSync(lock, 'utf8').matchAll(
123
+ /^\s{2}'?(@xemahq\/[a-z0-9-]+)@([0-9]+\.[0-9]+\.[0-9]+[^':(\s]*)'?:/gm,
124
+ )) {
125
+ const key = `${name}@${version}`;
126
+ if (seen.has(key)) continue;
127
+ seen.add(key);
128
+ const dir = `${name.replace('/', '+')}@${version}`;
129
+ // A peer-suffixed directory is the SAME version, so presence is a prefix
130
+ // test. Exact matching would report every peer-resolved package as missing.
131
+ if (!existsSync(path.join(store, dir)) && !entries.some((e) => e.startsWith(`${dir}_`))) {
132
+ missing.push(key);
133
+ }
134
+ }
135
+ return missing;
136
+ }
137
+
138
+ /** A step is SKIPPED when this repository does not own that generator, never faked. */
139
+ const STEPS = [
140
+ {
141
+ id: 'prisma',
142
+ why: 'the generated Prisma clients the services TYPECHECK against',
143
+ perService: 'prisma:generate',
144
+ },
145
+ {
146
+ id: 'build',
147
+ why: 'spec extraction boots each AppModule; an unbuilt closure fakes a cascade',
148
+ run: ['pnpm', '-r', 'build'],
149
+ always: true,
150
+ },
151
+ {
152
+ id: 'specs',
153
+ why: 'the extracted OpenAPI documents',
154
+ perService: 'openapi',
155
+ gate: 'check:openapi-spec-current',
156
+ },
157
+ {
158
+ id: 'clients',
159
+ why: 'the generated clients, which are derived FROM those specs',
160
+ perService: 'client:generate',
161
+ gate: 'check:client-spec-drift',
162
+ },
163
+ {
164
+ id: 'ledgers',
165
+ why: "each biome's integrity claim over its own bytes, specs and clients included",
166
+ // `--allow-dirty` is SAFE HERE AND ONLY HERE, because `run()` refuses to start
167
+ // on a dirty tree — so every modified file at this point is output THIS RUN
168
+ // produced. Without it the step is a no-op in exactly the case it exists for:
169
+ // the biomes it must re-hash are the ones the specs and clients just dirtied,
170
+ // so the deriver skips precisely those and exits 0.
171
+ run: ['node', 'tooling/codegen/derive-biome-index.mjs', '--allow-dirty'],
172
+ needs: 'derive:biome-index',
173
+ gate: 'check:biome-index-ledger',
174
+ },
175
+ ];
176
+
177
+ /**
178
+ * Surface classifications a human already decided, with the evidence.
179
+ *
180
+ * The client generator REFUSES to guess whether a retained export whose
181
+ * declaration moved is widening or narrowing — correctly, because a generated
182
+ * client does not record whether a type is a REQUEST or a RESPONSE, and those
183
+ * have opposite version consequences. That refusal stays.
184
+ *
185
+ * What this removes is the REPETITION. Before it, the same investigation —
186
+ * `npm pack` the published client, diff the declaration, decide — was owed by
187
+ * every run and every session, for a fact somebody had already established.
188
+ *
189
+ * It is deliberately NOT a global bump class. An entry names ONE export, and it
190
+ * is applied only when every export the generator names is recorded. Anything
191
+ * unrecognised still stops and asks, which is what keeps this from becoming
192
+ * `XEMA_CLIENT_BUMP_CLASS` set once and forgotten.
193
+ */
194
+ function recordedSurfaceDecisions() {
195
+ const file = path.join(ROOT, 'tooling/release/client-surface-decisions.json');
196
+ if (!existsSync(file)) return new Map();
197
+ try {
198
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
199
+ return new Map((parsed.decisions ?? []).map((d) => [d.export, d]));
200
+ } catch {
201
+ return new Map(); // an unreadable ledger must not silently grant anything
202
+ }
203
+ }
204
+
205
+ /** The exports a refusal names, e.g. `models/problemDetailsDto#ProblemDetailsDto`. */
206
+ function exportsNamedByRefusal(output) {
207
+ const named = new Set();
208
+ for (const line of output.split('\n')) {
209
+ const m = line.match(/exports whose DECLARATION changed \(\d+\): (.+)$/);
210
+ if (m) for (const e of m[1].split(/[,\s]+/).filter(Boolean)) named.add(e.trim());
211
+ }
212
+ return [...named];
213
+ }
214
+
215
+ function sh(argv, { capture = false, env } = {}) {
216
+ return execFileSync(argv[0], argv.slice(1), {
217
+ cwd: ROOT,
218
+ encoding: 'utf8',
219
+ // `pipe` for stderr even when inheriting stdout: a generator REFUSAL has to be
220
+ // readable by the caller to be matched against a recorded decision, and an
221
+ // inherited stderr reaches the terminal and nothing else.
222
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : ['inherit', 'inherit', 'pipe'],
223
+ maxBuffer: 1 << 28,
224
+ ...(env ? { env } : {}),
225
+ });
226
+ }
227
+
228
+ /** Services this repository can extract, read from the workspace rather than listed. */
229
+ function servicesWith(script) {
230
+ const out = sh(['pnpm', '-r', '--depth', '-1', 'list', '--json'], { capture: true });
231
+ let parsed;
232
+ try {
233
+ parsed = JSON.parse(out);
234
+ } catch {
235
+ return [];
236
+ }
237
+ const names = [];
238
+ for (const entry of Array.isArray(parsed) ? parsed : [parsed]) {
239
+ const manifest = path.join(entry.path ?? '', 'package.json');
240
+ if (!existsSync(manifest)) continue;
241
+ try {
242
+ const json = JSON.parse(readFileSync(manifest, 'utf8'));
243
+ if (json.scripts?.[script] && json.name) names.push(json.name);
244
+ } catch {
245
+ /* unreadable manifest is not a service */
246
+ }
247
+ }
248
+ return names;
249
+ }
250
+
251
+ /**
252
+ * The packages this working tree actually CHANGED, against the branch it will
253
+ * merge into.
254
+ *
255
+ * This exists because the chain above proves CONVERGENCE and nothing else.
256
+ * Measured 2026-09-19: `xema-base/develop` was red for four consecutive commits
257
+ * on `shell-published-capability-surface.spec.ts` — a declaration change whose
258
+ * only witness was a semantic test. `prisma`, `build`, `specs`, `clients`,
259
+ * `ledgers` and `pnpm check` are all GREEN on that tree, because none of them
260
+ * runs it. `typecheck green != tests green`, and readiness proved only the first.
261
+ *
262
+ * The set is derived from git rather than from a build-graph filter, because no
263
+ * repository in this fleet has a turbo affected-filter to copy (measured: zero
264
+ * `--filter=...[origin/...]` invocations fleet-wide) and inventing one here would
265
+ * be a second build authority. `git diff <merge-base>` needs no tooling, is the
266
+ * same question every reviewer asks, and includes UNCOMMITTED work — which is
267
+ * the whole point of a pre-push command.
268
+ */
269
+ function changedPackages() {
270
+ let base;
271
+ for (const ref of ['origin/develop', 'origin/main']) {
272
+ try {
273
+ base = sh(['git', 'merge-base', 'HEAD', ref], { capture: true }).trim();
274
+ if (base) break;
275
+ } catch {
276
+ /* the ref is not fetched here; try the next one */
277
+ }
278
+ }
279
+ if (!base) return { base: undefined, paths: [], packages: [] };
280
+ const paths = sh(['git', 'diff', '--name-only', base, '--'], { capture: true })
281
+ .split('\n')
282
+ .map((l) => l.trim())
283
+ .filter(Boolean);
284
+
285
+ return { base, paths, packages: packagesOwningPaths(ROOT, paths) };
286
+ }
287
+
288
+ /**
289
+ * The workspace packages owning these repo-relative paths, by walking UP to the
290
+ * nearest `package.json`. Pure over the filesystem, so it is the half that is
291
+ * tested directly.
292
+ *
293
+ * Two rules, both deliberate:
294
+ * - the ROOT manifest never owns anything. It is not a workspace member, and
295
+ * its `test` script is usually `turbo run test` — the whole fleet, which is
296
+ * the opposite of affected.
297
+ * - a package with no `test` script owns nothing, so it is absent from the set
298
+ * rather than present-and-skipped. A skipped member inflates the corpus line
299
+ * and makes "0 ran" look like "all passed".
300
+ */
301
+ export function packagesOwningPaths(root, relPaths) {
302
+ // CONTAINMENT IS A PATH QUESTION, NEVER A STRING PREFIX. `dir.startsWith(root)`
303
+ // admits a SIBLING whose name merely begins with the same characters — for
304
+ // root `/x/repo`, the path `/x/repo-legacy/pkg` passes — and this fleet keeps
305
+ // sibling repositories one directory apart, so a climbing path would have
306
+ // scheduled a NEIGHBOURING repository's test suite. Found by a mutation test
307
+ // whose escaping path was made to reach a real manifest.
308
+ const contains = (dir) => {
309
+ const rel = path.relative(root, dir);
310
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
311
+ };
312
+ const owners = new Set();
313
+ for (const rel of relPaths) {
314
+ let dir = path.dirname(path.resolve(root, rel));
315
+ while (contains(dir)) {
316
+ const manifest = path.join(dir, 'package.json');
317
+ if (dir !== root && existsSync(manifest)) {
318
+ try {
319
+ const json = JSON.parse(readFileSync(manifest, 'utf8'));
320
+ if (json.name && json.scripts?.test) owners.add(json.name);
321
+ } catch {
322
+ /* an unreadable manifest owns nothing */
323
+ }
324
+ break;
325
+ }
326
+ const parent = path.dirname(dir);
327
+ if (parent === dir) break;
328
+ dir = parent;
329
+ }
330
+ }
331
+ return [...owners].sort();
332
+ }
333
+
334
+ /**
335
+ * Run the changed packages' own suites. NOT their dependents': this is the
336
+ * pre-push loop, and the dependent closure in this fleet reaches 144 packages —
337
+ * CI owns that question. What this owns is "the thing I just edited still
338
+ * passes its own tests", which is the one that was missing.
339
+ */
340
+ function runAffectedTests() {
341
+ console.log('\n=== tests — the affected packages\' own suites ===');
342
+ const { base, paths, packages } = changedPackages();
343
+ if (!base) {
344
+ console.log(
345
+ ' SKIPPED — neither origin/develop nor origin/main is fetched here, so\n' +
346
+ ' there is no branch point to diff against. Fetch, or run the suites by hand.',
347
+ );
348
+ return;
349
+ }
350
+ // The CORPUS, printed: a verdict with no denominator cannot be graded in
351
+ // either direction, and "0 tests ran" and "0 tests failed" look identical.
352
+ console.log(
353
+ ` ${paths.length} changed path(s) since ${base.slice(0, 9)} -> ` +
354
+ `${packages.length} package(s) with a test script`,
355
+ );
356
+ if (packages.length === 0) {
357
+ console.log(
358
+ ' nothing to run — the changed paths belong to no workspace package with\n' +
359
+ ' a test script (a workflow, a root document, generated output).',
360
+ );
361
+ return;
362
+ }
363
+ for (const name of packages) console.log(` - ${name}`);
364
+ for (const name of packages) {
365
+ // One at a time, so a failure NAMES the package; a single multi-filter
366
+ // invocation reports only the first.
367
+ sh(['pnpm', '--filter', name, 'test']);
368
+ }
369
+ }
370
+
371
+ function run() {
372
+ // ── WHAT `--allow-dirty` INCLUDES, STATED RATHER THAN ASSUMED ─────────────
373
+ // This regenerates derived artifacts and hashes them into biome ledgers, and a
374
+ // ledger hashes the WORKING TREE. So anything already modified when this starts
375
+ // is hashed in too.
376
+ //
377
+ // That is USUALLY exactly right — a developer regenerating alongside the edits
378
+ // they are about to commit is the case `--allow-dirty` was written for, and
379
+ // refusing it would break the one workflow this command exists to serve:
380
+ // change -> readiness -> push.
381
+ //
382
+ // It is wrong in ONE situation: a SHARED checkout where the modified files are
383
+ // somebody else's. That cannot be detected from here, so it is REPORTED rather
384
+ // than guessed at. A reader who sees a path they did not touch has the one fact
385
+ // they need; a refusal would have cost everyone else the workflow.
386
+ // PROVENANCE FIRST. Unlike a dirty tree, an install behind its own lockfile is
387
+ // never the intent — there is no workflow it serves, so this REFUSES rather
388
+ // than reporting. The remedy is one command and it is printed.
389
+ const behind = installBehindLockfile();
390
+ if (behind.length > 0) {
391
+ console.error(
392
+ `\nreadiness: REFUSING — this install is BEHIND its own lockfile.\n\n` +
393
+ `${behind.length} version(s) the lockfile records are NOT in the store:\n` +
394
+ behind.slice(0, 10).map((m) => ` ${m}`).join('\n') +
395
+ (behind.length > 10 ? `\n ... and ${behind.length - 10} more` : '') +
396
+ '\n\nEverything this command regenerates would be derived from the WRONG\n' +
397
+ 'bytes, and the failure would name whichever file imports them rather\n' +
398
+ 'than the stale package behind it.\n\n' +
399
+ ' Remedy: pnpm install --frozen-lockfile\n',
400
+ );
401
+ return 1;
402
+ }
403
+
404
+ const preexisting = sh(['git', 'status', '--porcelain'], { capture: true })
405
+ .trim()
406
+ .split('\n')
407
+ .filter(Boolean);
408
+ if (preexisting.length > 0) {
409
+ console.log(
410
+ `readiness: ${preexisting.length} path(s) already modified — these are hashed into\n` +
411
+ 'any biome ledger they belong to. Every one of them should be YOURS:\n' +
412
+ preexisting.slice(0, 10).map((l) => ` ${l}`).join('\n') +
413
+ (preexisting.length > 10 ? `\n ... and ${preexisting.length - 10} more` : '') +
414
+ '\n',
415
+ );
416
+ }
417
+
418
+ const only = process.argv.find((a) => a.startsWith('--only='))?.slice('--only='.length);
419
+ console.log(`readiness: ${path.basename(ROOT)} — prisma -> build -> specs -> clients -> ledgers -> checks\n`);
420
+
421
+ for (const step of STEPS) {
422
+ if (only && only !== step.id) continue;
423
+ if (step.needs && !scripts[step.needs]) {
424
+ console.log(` - ${step.id}: SKIPPED — this repository has no \`${step.needs}\``);
425
+ continue;
426
+ }
427
+ console.log(`\n=== ${step.id} — ${step.why} ===`);
428
+ if (step.run) {
429
+ sh(step.run);
430
+ continue;
431
+ }
432
+ const services = servicesWith(step.perService);
433
+ if (services.length === 0) {
434
+ console.log(` - no package declares \`${step.perService}\`; nothing to regenerate`);
435
+ continue;
436
+ }
437
+ console.log(` ${services.length} package(s) declare \`${step.perService}\``);
438
+ const decided = step.id === 'clients' ? recordedSurfaceDecisions() : new Map();
439
+ for (const name of services) {
440
+ // One at a time on purpose: a failure must name the service, and a single
441
+ // `--filter a --filter b` invocation reports only the first.
442
+ try {
443
+ sh(['pnpm', '--filter', name, step.perService]);
444
+ } catch (error) {
445
+ const output = `${error?.stdout ?? ''}${error?.stderr ?? ''}${error?.message ?? ''}`;
446
+ const named = exportsNamedByRefusal(output);
447
+ if (named.length === 0) {
448
+ // stderr is PIPED so a refusal can be matched; an ordinary failure must
449
+ // not lose its diagnostics to that. Print what was captured before
450
+ // rethrowing, or the reader gets one line of `Command failed`.
451
+ if (output.trim()) console.error(output);
452
+ throw error;
453
+ }
454
+
455
+ const unrecorded = named.filter((e) => !decided.has(e));
456
+ if (unrecorded.length > 0) {
457
+ console.error(
458
+ `\n ${name}: the generator cannot classify ${unrecorded.length} export(s), ` +
459
+ 'and no recorded decision covers them:\n' +
460
+ unrecorded.map((e) => ` ${e}`).join('\n') +
461
+ '\n\n This needs a HUMAN, and the generator is right to ask. Decide it from the' +
462
+ '\n PUBLISHED tarball of the current version, never from this tree:' +
463
+ `\n npm pack <client>@<published> and diff the declaration.` +
464
+ '\n Then record it in tooling/release/client-surface-decisions.json with the' +
465
+ '\n evidence, so nobody has to establish it again.\n',
466
+ );
467
+ throw error;
468
+ }
469
+
470
+ const classes = [...new Set(named.map((e) => decided.get(e).class))];
471
+ if (classes.length !== 1) {
472
+ console.error(
473
+ `\n ${name}: recorded decisions disagree (${classes.join(', ')}). ` +
474
+ 'Refusing to pick one.\n',
475
+ );
476
+ throw error;
477
+ }
478
+
479
+ // Not a retry loop: ONE re-run, driven by recorded data rather than by hope,
480
+ // for a refusal whose every subject is already decided.
481
+ console.log(
482
+ ` ${name}: applying recorded classification ${classes[0]} for ` +
483
+ `${named.join(', ')}`,
484
+ );
485
+ sh(['pnpm', '--filter', name, step.perService], {
486
+ env: { ...process.env, XEMA_CLIENT_BUMP_CLASS: classes[0] },
487
+ });
488
+ }
489
+ }
490
+ }
491
+
492
+ if (only) {
493
+ console.log(`\nreadiness: ran --only=${only}. Run without it before pushing.`);
494
+ return 0;
495
+ }
496
+
497
+ console.log('\n=== checks — the proof that the chain CONVERGED ===');
498
+ // A second pass must not be needed. If a gate here is red, a dependency was
499
+ // violated; find it rather than re-running this command.
500
+ sh(['pnpm', 'check']);
501
+ for (const gate of STEPS.map((s) => s.gate).filter(Boolean)) {
502
+ if (scripts[gate]) sh(['pnpm', gate]);
503
+ }
504
+
505
+ runAffectedTests();
506
+
507
+ console.log(
508
+ '\nreadiness: converged. Every derived artifact describes the source beside it,\n' +
509
+ 'and the repository\'s own checks agree. Safe to push.',
510
+ );
511
+ return 0;
512
+ }
513
+
514
+ // Executed as a bin, imported by its test. `process.argv[1]` is this file only
515
+ // when node was asked to RUN it — an import leaves it pointing at the test.
516
+ const INVOKED_DIRECTLY =
517
+ process.argv[1] !== undefined &&
518
+ path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
519
+
520
+ try {
521
+ if (!INVOKED_DIRECTLY) {
522
+ // imported for its exports; nothing to do
523
+ } else process.exit(run());
524
+ } catch (error) {
525
+ console.error(
526
+ `\nreadiness: FAILED at the step above. That is the ROOT cause — the steps after it\n` +
527
+ 'were not reached, so do not read their absence as a second failure.\n' +
528
+ `\n ${error?.message?.split('\n')[0] ?? error}\n` +
529
+ '\nIf a CHECK failed rather than a generator, a dependency in\n' +
530
+ 'prisma -> build -> specs -> clients -> ledgers was violated. Find it; do not re-run.\n',
531
+ );
532
+ process.exit(1);
533
+ }
@@ -0,0 +1,129 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { packagesOwningPaths } from './readiness.mjs';
8
+
9
+ /** A throwaway workspace: root + four members with different shapes. */
10
+ async function fixture() {
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'xema-readiness-'));
12
+ const write = async (rel, json) => {
13
+ await fs.mkdir(path.join(root, path.dirname(rel)), { recursive: true });
14
+ await fs.writeFile(path.join(root, rel), json, 'utf8');
15
+ };
16
+ // The ROOT manifest declares a test script ON PURPOSE: it must still own nothing.
17
+ await write('package.json', JSON.stringify({ name: 'root', scripts: { test: 'turbo run test' } }));
18
+ await write('packages/tested/package.json', JSON.stringify({ name: '@t/tested', scripts: { test: 'jest' } }));
19
+ await write('packages/untested/package.json', JSON.stringify({ name: '@t/untested', scripts: { build: 'tsc' } }));
20
+ await write('packages/outer/package.json', JSON.stringify({ name: '@t/outer', scripts: { test: 'jest' } }));
21
+ await write('packages/outer/nested/package.json', JSON.stringify({ name: '@t/nested', scripts: { test: 'jest' } }));
22
+ await write('packages/broken/package.json', '{ this is not json');
23
+ return root;
24
+ }
25
+
26
+ test('a changed file maps to the package that owns it', async () => {
27
+ const root = await fixture();
28
+ try {
29
+ assert.deepEqual(
30
+ packagesOwningPaths(root, ['packages/tested/src/a.ts']),
31
+ ['@t/tested'],
32
+ );
33
+ // Two files in one package are ONE entry — the corpus line must not
34
+ // double-count, or "3 packages" would run one suite three times.
35
+ assert.deepEqual(
36
+ packagesOwningPaths(root, ['packages/tested/src/a.ts', 'packages/tested/src/b.ts']),
37
+ ['@t/tested'],
38
+ );
39
+ } finally {
40
+ await fs.rm(root, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ test('a package with no test script owns nothing — absent, never present-and-skipped', async () => {
45
+ const root = await fixture();
46
+ try {
47
+ // Present-and-skipped would inflate the corpus line, and "0 ran" would then
48
+ // look exactly like "all passed".
49
+ assert.deepEqual(packagesOwningPaths(root, ['packages/untested/src/a.ts']), []);
50
+ assert.deepEqual(
51
+ packagesOwningPaths(root, ['packages/untested/src/a.ts', 'packages/tested/src/a.ts']),
52
+ ['@t/tested'],
53
+ );
54
+ } finally {
55
+ await fs.rm(root, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ test('the ROOT manifest never owns anything, even declaring a test script', async () => {
60
+ const root = await fixture();
61
+ try {
62
+ // The fixture root declares `test: turbo run test` — the WHOLE fleet, which
63
+ // is the opposite of affected. Owning from the root would run everything on
64
+ // a README edit.
65
+ assert.deepEqual(packagesOwningPaths(root, ['README.md']), []);
66
+ assert.deepEqual(packagesOwningPaths(root, ['.github/workflows/ci.yaml']), []);
67
+ } finally {
68
+ await fs.rm(root, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ test('the NEAREST package wins, not an ancestor that also has a manifest', async () => {
73
+ const root = await fixture();
74
+ try {
75
+ assert.deepEqual(
76
+ packagesOwningPaths(root, ['packages/outer/nested/src/a.ts']),
77
+ ['@t/nested'],
78
+ );
79
+ assert.deepEqual(
80
+ packagesOwningPaths(root, ['packages/outer/src/a.ts']),
81
+ ['@t/outer'],
82
+ );
83
+ } finally {
84
+ await fs.rm(root, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ test('an unreadable manifest owns nothing and does not throw', async () => {
89
+ const root = await fixture();
90
+ try {
91
+ // A parse error here would abort the whole readiness run over one bad file.
92
+ assert.deepEqual(packagesOwningPaths(root, ['packages/broken/src/a.ts']), []);
93
+ // ...and it must not swallow its NEIGHBOURS.
94
+ assert.deepEqual(
95
+ packagesOwningPaths(root, ['packages/broken/src/a.ts', 'packages/tested/src/a.ts']),
96
+ ['@t/tested'],
97
+ );
98
+ } finally {
99
+ await fs.rm(root, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ test('a path escaping the root owns nothing, even reaching a REAL sibling manifest', async () => {
104
+ const root = await fixture();
105
+ // The hazard is concrete, not theoretical: in the aggregator,
106
+ // `repos/xema-base/../xema-community/package.json` exists. A climbing path
107
+ // must not reach a NEIGHBOURING repository's package and schedule its tests.
108
+ const sibling = path.join(path.dirname(root), `${path.basename(root)}-sibling`);
109
+ await fs.mkdir(path.join(sibling, 'packages/foreign'), { recursive: true });
110
+ await fs.writeFile(
111
+ path.join(sibling, 'packages/foreign/package.json'),
112
+ JSON.stringify({ name: '@other/foreign', scripts: { test: 'jest' } }),
113
+ 'utf8',
114
+ );
115
+ try {
116
+ const escaping = path.join('..', path.basename(sibling), 'packages/foreign/src/a.ts');
117
+ // Control: that manifest IS reachable when the sibling is the root, so the
118
+ // empty result below is the ROOT BOUND working, not a path that resolves
119
+ // to nothing.
120
+ assert.deepEqual(
121
+ packagesOwningPaths(sibling, ['packages/foreign/src/a.ts']),
122
+ ['@other/foreign'],
123
+ );
124
+ assert.deepEqual(packagesOwningPaths(root, [escaping]), []);
125
+ } finally {
126
+ await fs.rm(root, { recursive: true, force: true });
127
+ await fs.rm(sibling, { recursive: true, force: true });
128
+ }
129
+ });
@@ -438,6 +438,83 @@ async function* walk(dir) {
438
438
  }
439
439
  }
440
440
 
441
+ // ── WHERE THE PLUGIN'S REQUIRES LIVE, AND WHY THAT IS THE WHOLE SCOPE ────────
442
+ //
443
+ // This file's own header states the scope: "Post-build sanitizer for
444
+ // @nestjs/swagger plugin v11.x output", and "the swagger CLI plugin emits
445
+ // `_OPENAPI_METADATA_FACTORY()` blocks that contain `require("<path>")` calls".
446
+ // Every leak it exists to repair is therefore INSIDE such a block.
447
+ //
448
+ // Shape 2 did not honour that. It rewrote `require("@xemahq/<pkg>/dist/<rest>")`
449
+ // ANYWHERE in the emitted file, on the assumption recorded above — "every
450
+ // package they have ever matched exposes a root export". That assumption is
451
+ // measurably false today, and a HAND-WRITTEN deep import compiles to exactly
452
+ // the shape the regex matches, so it was rewritten too:
453
+ //
454
+ // src: import { RealmTokenVerifier }
455
+ // from '@xemahq/platform-common/dist/nestjs/auth/realm-token-verifier';
456
+ // dist: const realm_token_verifier_1 = require("@xemahq/platform-common");
457
+ //
458
+ // The module loads, the binding is not on the root barrel, and the service dies
459
+ // at DI time with "RealmTokenVerifier is not a constructor". That is what took
460
+ // xema-shell-api's rollout down on 2026-09-17 — and every check was green,
461
+ // because typecheck, jest and the pre-push gate all read the SOURCE, and
462
+ // nothing executes the rewritten dist before the pod does.
463
+ //
464
+ // Measured across the four packages shape 2 matches in shipped source, asking
465
+ // each published tarball whether its ROOT entry exports the symbol:
466
+ //
467
+ // @xemahq/platform-common RealmTokenVerifier NOT on root
468
+ // @xemahq/biome-supply-chain CosignErrorCode NOT on root
469
+ // @xemahq/xema-service-nest buildResourcePath NOT on root
470
+ // @xemahq/biome-database-nest assertMigrationHistoryCompatible NOT on root
471
+ //
472
+ // so the blind rewrite was wrong for all four. (Only two reach a shipped dist:
473
+ // xema-shell-api, which fired, and biome-host-api, which is armed on the
474
+ // bundle-fetch ERROR path. The other nine occurrences are in tests, which run
475
+ // from TypeScript and never see this rewriter.)
476
+ //
477
+ // The fix is a SCOPE correction, not more proof machinery: rewrite only what
478
+ // the plugin emitted, and REFUSE anything else. `reExportsReach` cannot stand
479
+ // in for this — it proves a module is REACHED, not that a BINDING is
480
+ // re-exported, and `nestjs/index.js` requires `./auth` while re-exporting only
481
+ // some of its names, so it would have called the bad rewrite provable.
482
+ //
483
+ // A refusal here is the whole point: it converts a production CrashLoopBackOff
484
+ // into a build failure in the repository that wrote the import.
485
+ function openApiFactoryRanges(src) {
486
+ const ranges = [];
487
+ const token = '_OPENAPI_METADATA_FACTORY';
488
+ let from = 0;
489
+ for (;;) {
490
+ const hit = src.indexOf(token, from);
491
+ if (hit === -1) break;
492
+ from = hit + token.length;
493
+ const open = src.indexOf('{', from);
494
+ if (open === -1) break;
495
+ // Brace-match. String and comment bodies inside a metadata factory are
496
+ // emitted by tsc and contain no unbalanced braces in practice; a miscount
497
+ // can only ever END the range early, which fails CLOSED (the require is
498
+ // then treated as hand-written and refused) rather than widening the
499
+ // rewrite to code the plugin did not emit.
500
+ let depth = 0;
501
+ let i = open;
502
+ for (; i < src.length; i += 1) {
503
+ if (src[i] === '{') depth += 1;
504
+ else if (src[i] === '}') {
505
+ depth -= 1;
506
+ if (depth === 0) break;
507
+ }
508
+ }
509
+ ranges.push([open, Math.min(i + 1, src.length)]);
510
+ from = i + 1;
511
+ }
512
+ return ranges;
513
+ }
514
+
515
+ const inAnyRange = (ranges, offset) =>
516
+ ranges.some(([start, end]) => offset >= start && offset < end);
517
+
441
518
  async function scrubFile(path) {
442
519
  const src = await readFile(path, 'utf8');
443
520
  let count = 0;
@@ -455,10 +532,35 @@ async function scrubFile(path) {
455
532
  count += 1;
456
533
  return `require("${kept}")`;
457
534
  });
458
- out = out.replace(FLAT_LEAK_REGEX, (_match, pkg) => {
535
+ // Shape 2 is scoped to the plugin's own emits. Anything else matching this
536
+ // shape is a hand-written deep import, and rewriting it silently changes
537
+ // which module the service loads — see the note above `openApiFactoryRanges`.
538
+ const factoryRanges = openApiFactoryRanges(out);
539
+ const handWritten = [];
540
+ out = out.replace(FLAT_LEAK_REGEX, (match, pkg, offset) => {
541
+ if (!inAnyRange(factoryRanges, offset)) {
542
+ handWritten.push(match.slice('require("'.length, -2));
543
+ return match;
544
+ }
459
545
  count += 1;
460
546
  return `require("${pkg}")`;
461
547
  });
548
+ if (handWritten.length > 0) {
549
+ throw new ScrubFailure(
550
+ `${path}\n` +
551
+ ` ${handWritten.length} hand-written deep import(s) into a package's dist/:\n` +
552
+ handWritten.map((spec) => ` ${spec}`).join('\n') +
553
+ `\n\n These are NOT swagger-plugin emits — they are outside every\n` +
554
+ ` _OPENAPI_METADATA_FACTORY block, so this rewriter is not entitled to\n` +
555
+ ` touch them. Rewriting one to the bare package specifier (which is what\n` +
556
+ ` this step used to do, silently) loads the package root instead, and any\n` +
557
+ ` binding the root barrel does not re-export becomes \`undefined\` at\n` +
558
+ ` runtime — a DI crash in the pod, with every build check green.\n\n` +
559
+ ` Fix the IMPORT, in source: use the package's public surface. If the\n` +
560
+ ` symbol is not on it, the package is missing an export — add it there\n` +
561
+ ` rather than reaching through dist/.`,
562
+ );
563
+ }
462
564
  out = out.replace(RELATIVE_WORKSPACE_REGEX, (_match, pkgName, subpath) => {
463
565
  count += 1;
464
566
  return `require("@xemahq/${pkgName}/${subpath}")`;
@@ -637,16 +739,57 @@ async function selfTest() {
637
739
  const green = join(root, 'green');
638
740
  await mkdir(green, { recursive: true });
639
741
  const greenFile = join(green, 'dto.js');
640
- await writeFile(greenFile, `${samples[0][1]};\n`, 'utf8');
742
+ // The plugin emits its requires INSIDE `_OPENAPI_METADATA_FACTORY`, so the
743
+ // green fixture has to as well — a bare literal at top level is a
744
+ // HAND-WRITTEN import and is now refused, which is the point of the scope.
745
+ await writeFile(
746
+ greenFile,
747
+ `class Dto {\n static _OPENAPI_METADATA_FACTORY() {\n return { x: { required: true, type: () => ${samples[0][1]} } };\n }\n}\n`,
748
+ 'utf8',
749
+ );
641
750
  const greenResult = await run([green]);
642
751
  if (greenResult.totalRewrites < 1) {
643
752
  throw new Error(`self-test: expected at least 1 rewrite, got ${greenResult.totalRewrites}`);
644
753
  }
754
+ // THE CASE THAT PINS THE SCOPE, both polarities in one fixture.
755
+ //
756
+ // Same package, same deep path, twice: once as the plugin emits it (inside
757
+ // `_OPENAPI_METADATA_FACTORY`) and once as tsc emits a hand-written
758
+ // `import ... from '@xemahq/<pkg>/dist/...'` (a top-level const). The
759
+ // rewriter must take the first and REFUSE the whole file for the second.
760
+ //
761
+ // Without this the fix is unpinned: the old blind pass rewrote both, and
762
+ // that is what put `require("@xemahq/platform-common")` into
763
+ // xema-shell-api's dist and crashlooped it in production with every build
764
+ // check green.
765
+ const mixed = join(root, 'mixed');
766
+ await mkdir(mixed, { recursive: true });
767
+ await writeFile(
768
+ join(mixed, 'dto.js'),
769
+ 'const realm_token_verifier_1 = require("@xemahq/platform-common/dist/nestjs/auth/realm-token-verifier");\n' +
770
+ 'class Dto {\n static _OPENAPI_METADATA_FACTORY() {\n' +
771
+ ' return { x: { type: () => require("@xemahq/capability-contracts/dist/lib/capability-grant") } };\n }\n}\n',
772
+ 'utf8',
773
+ );
774
+ const refusal = await expectFailure('hand-written deep dist import', () => run([mixed]));
775
+ if (!refusal.message.includes('realm-token-verifier')) {
776
+ throw new Error(
777
+ `self-test: the refusal must NAME the offending specifier; got: ${refusal.message}`,
778
+ );
779
+ }
780
+ // And it must refuse the HAND-WRITTEN one, not the plugin's — if the scope
781
+ // check were inverted this assertion is what catches it.
782
+ if (refusal.message.includes('capability-grant')) {
783
+ throw new Error(
784
+ 'self-test: the plugin-emitted require inside the factory was reported as hand-written — the scope test is inverted.',
785
+ );
786
+ }
787
+
645
788
  const greenAfter = await readFile(greenFile, 'utf8');
646
789
  // The NESTED pass unwraps to `@xemahq/subject-contracts/dist/lib/x`, which
647
790
  // the FLAT pass then reduces to the bare specifier — the two passes chain,
648
791
  // and the verification proves the chain terminated clean.
649
- if (greenAfter.trim() !== 'require("@xemahq/subject-contracts");') {
792
+ if (!greenAfter.includes('require("@xemahq/subject-contracts")')) {
650
793
  throw new Error(`self-test: unexpected rewrite result: ${greenAfter.trim()}`);
651
794
  }
652
795
  if (findLeaks(greenAfter).length !== 0) {