@xemahq/repo-build-tooling 0.6.0 → 0.7.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.6.0",
3
+ "version": "0.7.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)",
package/src/readiness.mjs CHANGED
@@ -62,8 +62,9 @@
62
62
  * honestly.
63
63
  */
64
64
  import { execFileSync } from 'node:child_process';
65
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
65
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
66
66
  import path from 'node:path';
67
+ import { fileURLToPath } from 'node:url';
67
68
  import process from 'node:process';
68
69
 
69
70
  // THE ROOT IS THE CONSUMER'S REPOSITORY, NEVER THIS PACKAGE'S LOCATION.
@@ -163,11 +164,26 @@ const STEPS = [
163
164
  {
164
165
  id: 'ledgers',
165
166
  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.
167
+ // `--allow-dirty` is REQUIRED here: without it the step is a no-op in exactly
168
+ // the case it exists for, because the biomes it must re-hash are the ones the
169
+ // specs and clients just dirtied, so the deriver skips precisely those and
170
+ // exits 0 a green command over a skipped corpus.
171
+ //
172
+ // WHAT IT DOES NOT MEAN, corrected 2026-09-19. This said "SAFE HERE AND ONLY
173
+ // HERE, because `run()` refuses to start on a dirty tree". `run()` does no
174
+ // such thing: it REPORTS pre-existing modified paths and continues, and its
175
+ // only refusal is for an install behind its own lockfile. The claim made this
176
+ // authority internally contradictory with its own `run()` forty lines down,
177
+ // and it was the load-bearing half — it is the reason the flag reads as safe,
178
+ // so a reader who checked it would have stopped there.
179
+ //
180
+ // The honest statement is narrower. A ledger hashes the WORKING TREE, so
181
+ // anything already modified when this starts is hashed in too. For the
182
+ // workflow this serves — regenerating alongside edits you are about to commit
183
+ // — that is correct, and refusing would break the one loop the command exists
184
+ // for. In a SHARED checkout holding somebody else's modifications it IS a
185
+ // waived check, which is why `run()` prints those paths and asks the reader
186
+ // whether all of them are theirs.
171
187
  run: ['node', 'tooling/codegen/derive-biome-index.mjs', '--allow-dirty'],
172
188
  needs: 'derive:biome-index',
173
189
  gate: 'check:biome-index-ledger',
@@ -282,7 +298,7 @@ function changedPackages() {
282
298
  .map((l) => l.trim())
283
299
  .filter(Boolean);
284
300
 
285
- return { base, paths, packages: packagesOwningPaths(ROOT, paths) };
301
+ return { base, paths };
286
302
  }
287
303
 
288
304
  /**
@@ -292,13 +308,13 @@ function changedPackages() {
292
308
  *
293
309
  * Two rules, both deliberate:
294
310
  * - 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".
311
+ * its script is usually `turbo run <x>` — the whole fleet, which is the
312
+ * opposite of affected. The root is handled separately, as a FALLBACK.
313
+ * - a package not declaring THIS script owns nothing for this stage, so it is
314
+ * absent from the set rather than present-and-skipped. A skipped member
315
+ * inflates the corpus line and makes "0 ran" look like "all passed".
300
316
  */
301
- export function packagesOwningPaths(root, relPaths) {
317
+ export function packagesOwningPaths(root, relPaths, script = 'test') {
302
318
  // CONTAINMENT IS A PATH QUESTION, NEVER A STRING PREFIX. `dir.startsWith(root)`
303
319
  // admits a SIBLING whose name merely begins with the same characters — for
304
320
  // root `/x/repo`, the path `/x/repo-legacy/pkg` passes — and this fleet keeps
@@ -314,10 +330,14 @@ export function packagesOwningPaths(root, relPaths) {
314
330
  let dir = path.dirname(path.resolve(root, rel));
315
331
  while (contains(dir)) {
316
332
  const manifest = path.join(dir, 'package.json');
317
- if (dir !== root && existsSync(manifest)) {
333
+ // No `dir !== root` guard: `contains()` already excludes it, because
334
+ // `path.relative(root, root)` is the empty string. A mutation restoring
335
+ // that guard changed no behaviour, which is what identified it as inert
336
+ // rather than as an untested branch.
337
+ if (existsSync(manifest)) {
318
338
  try {
319
339
  const json = JSON.parse(readFileSync(manifest, 'utf8'));
320
- if (json.name && json.scripts?.test) owners.add(json.name);
340
+ if (json.name && json.scripts?.[script]) owners.add(json.name);
321
341
  } catch {
322
342
  /* an unreadable manifest owns nothing */
323
343
  }
@@ -337,37 +357,71 @@ export function packagesOwningPaths(root, relPaths) {
337
357
  * CI owns that question. What this owns is "the thing I just edited still
338
358
  * passes its own tests", which is the one that was missing.
339
359
  */
340
- function runAffectedTests() {
341
- console.log('\n=== tests — the affected packages\' own suites ===');
342
- const { base, paths, packages } = changedPackages();
360
+ /**
361
+ * Run ONE affected stage — the changed packages' own `<script>`.
362
+ *
363
+ * THREE STAGES, NOT ONE, because they are ORTHOGONAL PROOFS and this command is
364
+ * the only place that owns the ordering:
365
+ *
366
+ * tests green != typecheck green (a suite can compile nothing)
367
+ * typecheck green != tests green (compiling proves no behaviour)
368
+ * either green != lint green
369
+ *
370
+ * They run AFTER the generators, not before, and that order is not a preference:
371
+ * a service typechecks against its GENERATED client, so typechecking before
372
+ * `clients` grades a stale artifact and reports the CONSUMER — the same fake
373
+ * cascade the header above describes for spec extraction.
374
+ *
375
+ * Dependents are deliberately excluded: that closure reaches 144 packages in the
376
+ * largest repository here, and CI owns it. What this owns is "the thing I just
377
+ * edited still passes its own proofs".
378
+ */
379
+ function runAffectedStage({ id, script, why }) {
380
+ console.log(`\n=== ${id} — ${why} ===`);
381
+ const { base, paths } = changedPackages();
343
382
  if (!base) {
344
383
  console.log(
345
384
  ' 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.',
385
+ ' there is no branch point to diff against. Fetch, or run this by hand.',
347
386
  );
348
387
  return;
349
388
  }
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.
389
+ const packages = packagesOwningPaths(ROOT, paths, script);
390
+ // The CORPUS, printed with BOTH counts: "0 ran" and "0 failed" look identical
391
+ // otherwise, and a green command over an empty scan is not proof.
352
392
  console.log(
353
393
  ` ${paths.length} changed path(s) since ${base.slice(0, 9)} -> ` +
354
- `${packages.length} package(s) with a test script`,
394
+ `${packages.length} package(s) declaring \`${script}\``,
355
395
  );
356
- if (packages.length === 0) {
396
+ if (packages.length > 0) {
397
+ for (const name of packages) console.log(` - ${name}`);
398
+ for (const name of packages) {
399
+ // One at a time, so a failure NAMES the package; a single multi-filter
400
+ // invocation reports only the first.
401
+ sh(['pnpm', '--filter', name, script]);
402
+ }
403
+ return;
404
+ }
405
+ // FALLBACK, and it is the difference between "inapplicable" and "not checked".
406
+ // Some repositories own a stage only at the root — `lint: eslint .` covers
407
+ // every package without any of them declaring `lint`. Skipping there would
408
+ // silently drop a real proof, so the root script runs, LABELLED as repo-wide
409
+ // rather than affected, so nobody reads it as an affected result.
410
+ if (paths.length > 0 && scripts[script]) {
357
411
  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).',
412
+ ` no changed package declares \`${script}\`, but the ROOT does running it\n` +
413
+ ' REPO-WIDE. This is applicable-but-not-affected, and is reported as such.',
360
414
  );
415
+ sh(['pnpm', 'run', script]);
361
416
  return;
362
417
  }
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
- }
418
+ console.log(
419
+ ` nothing to run no changed package declares \`${script}\` and the root\n` +
420
+ ' declares none either, so this stage is genuinely inapplicable here.',
421
+ );
369
422
  }
370
423
 
424
+
371
425
  function run() {
372
426
  // ── WHAT `--allow-dirty` INCLUDES, STATED RATHER THAN ASSUMED ─────────────
373
427
  // This regenerates derived artifacts and hashes them into biome ledgers, and a
@@ -502,7 +556,13 @@ function run() {
502
556
  if (scripts[gate]) sh(['pnpm', gate]);
503
557
  }
504
558
 
505
- runAffectedTests();
559
+ for (const stage of [
560
+ { id: 'typecheck', script: 'typecheck', why: 'the compiler, which a passing suite does not imply' },
561
+ { id: 'lint', script: 'lint', why: 'the rules neither the compiler nor a suite enforces' },
562
+ { id: 'tests', script: 'test', why: "the affected packages' own suites" },
563
+ ]) {
564
+ runAffectedStage(stage);
565
+ }
506
566
 
507
567
  console.log(
508
568
  '\nreadiness: converged. Every derived artifact describes the source beside it,\n' +
@@ -511,11 +571,41 @@ function run() {
511
571
  return 0;
512
572
  }
513
573
 
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);
574
+ /**
575
+ * Was this module RUN, or merely imported (by its own test)?
576
+ *
577
+ * BOTH SIDES ARE REALPATH'D, and that is the whole correctness of it. pnpm's
578
+ * isolated linker puts the package at `node_modules/.pnpm/<pkg>@<ver>/...` and
579
+ * symlinks `node_modules/@scope/<pkg>` to it, so `process.argv[1]` and
580
+ * `import.meta.url` can name the SAME FILE by two different paths. Comparing
581
+ * them unresolved gives an answer that depends on HOW THE PACKAGE WAS LINKED —
582
+ * true through pnpm's bin shim, which invokes the realpath, and false when the
583
+ * symlinked path is invoked. A predicate whose answer depends on the linker is
584
+ * not a predicate.
585
+ *
586
+ * The failure mode is the one this whole tool exists to prevent: a false answer
587
+ * takes the import branch, `run()` is never called, and the process exits 0 —
588
+ * a chain reporting success without running. Caught by a peer, not by me, and
589
+ * not by any test, which is why `invokedDirectly` is exported and asserted
590
+ * below against a REAL symlink.
591
+ *
592
+ * `fileURLToPath` rather than `new URL(...).pathname`: the latter is not the
593
+ * documented conversion and is wrong independently of the symlink — it breaks on
594
+ * a Windows drive letter and on any percent-encoded character, and a single
595
+ * SPACE in a checkout path is enough to produce one.
596
+ */
597
+ export function invokedDirectly(argv1, metaUrl) {
598
+ if (argv1 === undefined) return false;
599
+ try {
600
+ return realpathSync(argv1) === realpathSync(fileURLToPath(metaUrl));
601
+ } catch {
602
+ // An unresolvable path cannot be this module; never guess YES, because a
603
+ // wrong YES runs the chain from a context that did not ask for it.
604
+ return false;
605
+ }
606
+ }
607
+
608
+ const INVOKED_DIRECTLY = invokedDirectly(process.argv[1], import.meta.url);
519
609
 
520
610
  try {
521
611
  if (!INVOKED_DIRECTLY) {
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import test from 'node:test';
6
6
 
7
- import { packagesOwningPaths } from './readiness.mjs';
7
+ import { invokedDirectly, packagesOwningPaths } from './readiness.mjs';
8
8
 
9
9
  /** A throwaway workspace: root + four members with different shapes. */
10
10
  async function fixture() {
@@ -20,6 +20,8 @@ async function fixture() {
20
20
  await write('packages/outer/package.json', JSON.stringify({ name: '@t/outer', scripts: { test: 'jest' } }));
21
21
  await write('packages/outer/nested/package.json', JSON.stringify({ name: '@t/nested', scripts: { test: 'jest' } }));
22
22
  await write('packages/broken/package.json', '{ this is not json');
23
+ // Declares typecheck+lint but NOT test — the orthogonal-proof case.
24
+ await write('packages/compiled/package.json', JSON.stringify({ name: '@t/compiled', scripts: { typecheck: 'tsc --noEmit', lint: 'eslint .' } }));
23
25
  return root;
24
26
  }
25
27
 
@@ -127,3 +129,74 @@ test('a path escaping the root owns nothing, even reaching a REAL sibling manife
127
129
  await fs.rm(sibling, { recursive: true, force: true });
128
130
  }
129
131
  });
132
+
133
+ test('the owning scan is keyed on the SCRIPT, so the three stages see different sets', async () => {
134
+ const root = await fixture();
135
+ try {
136
+ const changed = ['packages/compiled/src/a.ts', 'packages/tested/src/a.ts'];
137
+ // `tests green != typecheck green` is not a slogan here — the two stages
138
+ // genuinely grade different packages, and a single set would silently run
139
+ // one package's suite and call the other proven.
140
+ assert.deepEqual(packagesOwningPaths(root, changed, 'test'), ['@t/tested']);
141
+ assert.deepEqual(packagesOwningPaths(root, changed, 'typecheck'), ['@t/compiled']);
142
+ assert.deepEqual(packagesOwningPaths(root, changed, 'lint'), ['@t/compiled']);
143
+ // Default stays `test`, so the original call shape is unchanged.
144
+ assert.deepEqual(packagesOwningPaths(root, changed), ['@t/tested']);
145
+ } finally {
146
+ await fs.rm(root, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ test('a script no package declares yields an EMPTY set, never the root', async () => {
151
+ const root = await fixture();
152
+ try {
153
+ // The root fixture declares `test`; asking for a script nothing declares must
154
+ // not fall back to it inside this function. The root FALLBACK is a decision
155
+ // made by the caller, which labels it repo-wide — folding it in here would
156
+ // report a repo-wide run as an affected one.
157
+ assert.deepEqual(packagesOwningPaths(root, ['packages/tested/src/a.ts'], 'nonexistent'), []);
158
+ } finally {
159
+ await fs.rm(root, { recursive: true, force: true });
160
+ }
161
+ });
162
+
163
+ /**
164
+ * The guard that decides whether this module RUNS. It had no test, and it was
165
+ * wrong: it compared `process.argv[1]` against `new URL(import.meta.url).pathname`
166
+ * UNRESOLVED, so under pnpm's isolated linker — which symlinks
167
+ * `node_modules/@scope/<pkg>` at `node_modules/.pnpm/<pkg>@<ver>/...` — the same
168
+ * file compared unequal whenever the symlinked path was the one invoked. The
169
+ * branch taken then does nothing and the process exits 0: a chain reporting
170
+ * success without running, which is the exact defect this tool exists to prevent.
171
+ */
172
+ test('the run guard sees through a SYMLINK — pnpm links every package this way', async () => {
173
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'xema-guard-'));
174
+ try {
175
+ const real = path.join(root, 'real', 'readiness.mjs');
176
+ await fs.mkdir(path.dirname(real), { recursive: true });
177
+ await fs.writeFile(real, '// module\n', 'utf8');
178
+ const link = path.join(root, 'linked.mjs');
179
+ await fs.symlink(real, link);
180
+
181
+ const url = `file://${real}`;
182
+ // THE CASE THAT FAILED: invoked by the symlink, module loaded from the
183
+ // realpath. Unresolved string comparison says false; the answer is true.
184
+ assert.equal(invokedDirectly(link, url), true);
185
+ // And the plain case still holds.
186
+ assert.equal(invokedDirectly(real, url), true);
187
+
188
+ // A DIFFERENT file must stay false, or the guard would run the chain from
189
+ // any import — the opposite failure, and the louder one.
190
+ const other = path.join(root, 'other.mjs');
191
+ await fs.writeFile(other, '// not it\n', 'utf8');
192
+ assert.equal(invokedDirectly(other, url), false);
193
+
194
+ // No argv[1] at all (`node --eval`) is an import, never a run.
195
+ assert.equal(invokedDirectly(undefined, url), false);
196
+ // An unresolvable path must answer NO rather than throw: a wrong YES runs
197
+ // the whole chain from a context that never asked for it.
198
+ assert.equal(invokedDirectly(path.join(root, 'gone.mjs'), url), false);
199
+ } finally {
200
+ await fs.rm(root, { recursive: true, force: true });
201
+ }
202
+ });