@venizia/dev-configs 0.4.4 → 0.4.5

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.
@@ -8,4 +8,8 @@ export * from './purity/manifest';
8
8
  export * from './purity/probe';
9
9
  export * from './purity/run';
10
10
  export * from './split-report';
11
+ export * from './test-controls';
12
+ export * from './test-dead-code';
13
+ export * from './test-mutate';
14
+ export * from './test-weight';
11
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC"}
@@ -24,4 +24,8 @@ __exportStar(require("./purity/manifest"), exports);
24
24
  __exportStar(require("./purity/probe"), exports);
25
25
  __exportStar(require("./purity/run"), exports);
26
26
  __exportStar(require("./split-report"), exports);
27
+ __exportStar(require("./test-controls"), exports);
28
+ __exportStar(require("./test-dead-code"), exports);
29
+ __exportStar(require("./test-mutate"), exports);
30
+ __exportStar(require("./test-weight"), exports);
27
31
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;GAGG;AACH,4CAA0B;AAC1B,kDAAgC;AAChC,oDAAkC;AAClC,iDAA+B;AAC/B,+CAA6B;AAC7B,iDAA+B","sourcesContent":["/**\n * `@venizia/dev-configs/checks`: the engines behind `devz check <name>`. Bun-only tooling - never\n * import it from code a browser runs; the purity gate asserts that no browser-claimed entry reaches it.\n */\nexport * from './catalog';\nexport * from './module-cycles';\nexport * from './purity/manifest';\nexport * from './purity/probe';\nexport * from './purity/run';\nexport * from './split-report';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;GAGG;AACH,4CAA0B;AAC1B,kDAAgC;AAChC,oDAAkC;AAClC,iDAA+B;AAC/B,+CAA6B;AAC7B,iDAA+B;AAC/B,kDAAgC;AAChC,mDAAiC;AACjC,gDAA8B;AAC9B,gDAA8B","sourcesContent":["/**\n * `@venizia/dev-configs/checks`: the engines behind `devz check <name>`. Bun-only tooling - never\n * import it from code a browser runs; the purity gate asserts that no browser-claimed entry reaches it.\n */\nexport * from './catalog';\nexport * from './module-cycles';\nexport * from './purity/manifest';\nexport * from './purity/probe';\nexport * from './purity/run';\nexport * from './split-report';\nexport * from './test-controls';\nexport * from './test-dead-code';\nexport * from './test-mutate';\nexport * from './test-weight';\n"]}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A positive control is a test that proves a gate can fail: a fixture that must be refused, a broken
3
+ * subject the check must catch. Deleting one makes the gate pass for the wrong reason, so the trim-tests
4
+ * method keeps a committed registry of them and fails when one disappears.
5
+ */
6
+ export interface IControlEntry {
7
+ /** The gate the control belongs to; reported with a failure. */
8
+ gate: string;
9
+ /** Repository-relative path of the test file. */
10
+ file: string;
11
+ /**
12
+ * `test` (default): `file` is a test file and `titlePattern` is matched against its titles.
13
+ * `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the
14
+ * scanner must ignore). It only has to exist: without it the test's "not found" assertion passes for the
15
+ * wrong reason.
16
+ */
17
+ kind?: 'test' | 'file';
18
+ /** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */
19
+ titlePattern?: string;
20
+ /** At least this many titles must match. Default 1. */
21
+ min?: number;
22
+ }
23
+ export interface IControlProblem {
24
+ entry: IControlEntry;
25
+ message: string;
26
+ }
27
+ export interface IControlsReport {
28
+ checked: number;
29
+ problems: IControlProblem[];
30
+ }
31
+ /**
32
+ * The titles in a test file: the first string argument of `test`, `it`, `describe` and of the
33
+ * second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.
34
+ * A `test.each` title is returned as written (`%s` and all): the registry patterns are regular
35
+ * expressions, so the expansion does not matter.
36
+ */
37
+ export declare const listTestTitles: (opts: {
38
+ source: string;
39
+ fileName?: string;
40
+ }) => Promise<string[]>;
41
+ /** Checks every entry of the registry against the test files it names. */
42
+ export declare const checkControls: (opts: {
43
+ root: string;
44
+ controls: IControlEntry[];
45
+ }) => Promise<IControlsReport>;
46
+ //# sourceMappingURL=test-controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-controls.d.ts","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,KAAG,OAAO,CAAC,MAAM,EAAE,CAgDnB,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,aAAa,GAAU,MAAM;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,KAAG,OAAO,CAAC,eAAe,CAkC1B,CAAC"}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkControls = exports.listTestTitles = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ /**
7
+ * The titles in a test file: the first string argument of `test`, `it`, `describe` and of the
8
+ * second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.
9
+ * A `test.each` title is returned as written (`%s` and all): the registry patterns are regular
10
+ * expressions, so the expansion does not matter.
11
+ */
12
+ const listTestTitles = async (opts) => {
13
+ const typescript = (await import('typescript')).default;
14
+ const file = typescript.createSourceFile(opts.fileName ?? 'file.test.ts', opts.source, typescript.ScriptTarget.Latest, true);
15
+ const titles = [];
16
+ const RUNNERS = new Set(['test', 'it', 'describe']);
17
+ const rootName = (expression) => {
18
+ if (typescript.isIdentifier(expression)) {
19
+ return expression.text;
20
+ }
21
+ if (typescript.isPropertyAccessExpression(expression)) {
22
+ return rootName(expression.expression);
23
+ }
24
+ if (typescript.isCallExpression(expression)) {
25
+ return rootName(expression.expression);
26
+ }
27
+ return undefined;
28
+ };
29
+ const visit = (node) => {
30
+ if (typescript.isCallExpression(node)) {
31
+ const first = node.arguments[0];
32
+ const root = rootName(node.expression);
33
+ if (root !== undefined &&
34
+ RUNNERS.has(root) &&
35
+ first &&
36
+ (typescript.isStringLiteral(first) || typescript.isNoSubstitutionTemplateLiteral(first))) {
37
+ titles.push(first.text);
38
+ }
39
+ }
40
+ typescript.forEachChild(node, visit);
41
+ };
42
+ visit(file);
43
+ return titles;
44
+ };
45
+ exports.listTestTitles = listTestTitles;
46
+ /** Checks every entry of the registry against the test files it names. */
47
+ const checkControls = async (opts) => {
48
+ const problems = [];
49
+ for (const entry of opts.controls) {
50
+ const path = (0, node_path_1.join)(opts.root, entry.file);
51
+ if (!(0, node_fs_1.existsSync)(path)) {
52
+ problems.push({
53
+ entry,
54
+ message: `the ${entry.kind === 'file' ? 'negative fixture' : 'test file'} ${entry.file} does not exist`,
55
+ });
56
+ continue;
57
+ }
58
+ if (entry.kind === 'file') {
59
+ continue;
60
+ }
61
+ const titles = await (0, exports.listTestTitles)({
62
+ source: (0, node_fs_1.readFileSync)(path, 'utf8'),
63
+ fileName: entry.file,
64
+ });
65
+ const pattern = new RegExp(entry.titlePattern ?? '');
66
+ const matched = titles.filter((title) => pattern.test(title)).length;
67
+ const min = entry.min ?? 1;
68
+ if (matched < min) {
69
+ problems.push({
70
+ entry,
71
+ message: `${matched} title(s) match /${entry.titlePattern ?? ''}/ in ${entry.file}, ${min} required | the control of gate '${entry.gate}' is gone or renamed`,
72
+ });
73
+ }
74
+ }
75
+ return { checked: opts.controls.length, problems };
76
+ };
77
+ exports.checkControls = checkControls;
78
+ //# sourceMappingURL=test-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-controls.js","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type ts from 'typescript';\n\n/**\n * A positive control is a test that proves a gate can fail: a fixture that must be refused, a broken\n * subject the check must catch. Deleting one makes the gate pass for the wrong reason, so the trim-tests\n * method keeps a committed registry of them and fails when one disappears.\n */\nexport interface IControlEntry {\n /** The gate the control belongs to; reported with a failure. */\n gate: string;\n /** Repository-relative path of the test file. */\n file: string;\n /**\n * `test` (default): `file` is a test file and `titlePattern` is matched against its titles.\n * `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the\n * scanner must ignore). It only has to exist: without it the test's \"not found\" assertion passes for the\n * wrong reason.\n */\n kind?: 'test' | 'file';\n /** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */\n titlePattern?: string;\n /** At least this many titles must match. Default 1. */\n min?: number;\n}\n\nexport interface IControlProblem {\n entry: IControlEntry;\n message: string;\n}\n\nexport interface IControlsReport {\n checked: number;\n problems: IControlProblem[];\n}\n\n/**\n * The titles in a test file: the first string argument of `test`, `it`, `describe` and of the\n * second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.\n * A `test.each` title is returned as written (`%s` and all): the registry patterns are regular\n * expressions, so the expansion does not matter.\n */\nexport const listTestTitles = async (opts: {\n source: string;\n fileName?: string;\n}): Promise<string[]> => {\n const typescript: typeof ts = (await import('typescript')).default;\n const file = typescript.createSourceFile(\n opts.fileName ?? 'file.test.ts',\n opts.source,\n typescript.ScriptTarget.Latest,\n true,\n );\n const titles: string[] = [];\n const RUNNERS = new Set(['test', 'it', 'describe']);\n\n const rootName = (expression: ts.Expression): string | undefined => {\n if (typescript.isIdentifier(expression)) {\n return expression.text;\n }\n\n if (typescript.isPropertyAccessExpression(expression)) {\n return rootName(expression.expression);\n }\n\n if (typescript.isCallExpression(expression)) {\n return rootName(expression.expression);\n }\n\n return undefined;\n };\n\n const visit = (node: ts.Node): void => {\n if (typescript.isCallExpression(node)) {\n const first = node.arguments[0];\n const root = rootName(node.expression);\n\n if (\n root !== undefined &&\n RUNNERS.has(root) &&\n first &&\n (typescript.isStringLiteral(first) || typescript.isNoSubstitutionTemplateLiteral(first))\n ) {\n titles.push(first.text);\n }\n }\n\n typescript.forEachChild(node, visit);\n };\n\n visit(file);\n\n return titles;\n};\n\n/** Checks every entry of the registry against the test files it names. */\nexport const checkControls = async (opts: {\n root: string;\n controls: IControlEntry[];\n}): Promise<IControlsReport> => {\n const problems: IControlProblem[] = [];\n\n for (const entry of opts.controls) {\n const path = join(opts.root, entry.file);\n if (!existsSync(path)) {\n problems.push({\n entry,\n message: `the ${entry.kind === 'file' ? 'negative fixture' : 'test file'} ${entry.file} does not exist`,\n });\n continue;\n }\n\n if (entry.kind === 'file') {\n continue;\n }\n\n const titles = await listTestTitles({\n source: readFileSync(path, 'utf8'),\n fileName: entry.file,\n });\n const pattern = new RegExp(entry.titlePattern ?? '');\n const matched = titles.filter((title) => pattern.test(title)).length;\n const min = entry.min ?? 1;\n\n if (matched < min) {\n problems.push({\n entry,\n message: `${matched} title(s) match /${entry.titlePattern ?? ''}/ in ${entry.file}, ${min} required | the control of gate '${entry.gate}' is gone or renamed`,\n });\n }\n }\n\n return { checked: opts.controls.length, problems };\n};\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Support files of the tests (fixtures, helpers, probes) that no other file refers to. The second
3
+ * step of the trim-tests method. The output is CANDIDATES: a fixture can be loaded by a path built at
4
+ * run time or by scanning a folder, so a candidate is deleted only after the package suite is green
5
+ * without it.
6
+ */
7
+ export interface IDeadTestCodeCandidate {
8
+ file: string;
9
+ /**
10
+ * `unreferenced`: neither its name nor its path under `__tests__` appears in another file, nor does
11
+ * its folder. `folder-referenced`: only its folder path does, so a scan of that folder may load it.
12
+ */
13
+ kind: 'unreferenced' | 'folder-referenced';
14
+ }
15
+ export declare const findDeadTestCode: (opts: {
16
+ root: string;
17
+ }) => IDeadTestCodeCandidate[];
18
+ //# sourceMappingURL=test-dead-code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-dead-code.d.ts","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,EAAE,cAAc,GAAG,mBAAmB,CAAC;CAC5C;AA6BD,eAAO,MAAM,gBAAgB,GAAI,MAAM;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAG,sBAAsB,EA+B/E,CAAC"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findDeadTestCode = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_path_1 = require("node:path");
6
+ const SUPPORT_EXTENSION = /\.(?:[cm]?[jt]sx?|json|sql|txt)$/;
7
+ const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
8
+ const git = (opts) => {
9
+ const result = (0, node_child_process_1.spawnSync)('git', opts.args, {
10
+ cwd: opts.root,
11
+ encoding: 'utf8',
12
+ maxBuffer: 256 * 1024 * 1024,
13
+ });
14
+ return { status: result.status ?? 1, stdout: result.stdout ?? '' };
15
+ };
16
+ /** Files other than `self` whose text contains `needle` (a fixed string). */
17
+ const referencedElsewhere = (opts) => {
18
+ const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-F', '-e', opts.needle] });
19
+ return result.stdout.split('\n').some((line) => line !== '' && line !== opts.self);
20
+ };
21
+ const escapeForRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
+ /** Files other than `self` that mention the name of the file as a path segment or an import. */
23
+ const nameReferencedElsewhere = (opts) => {
24
+ const pattern = `[/'"\`]${escapeForRegex(opts.stem)}(\\.|'|"|\`|$)`;
25
+ const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });
26
+ return result.stdout.split('\n').some((line) => line !== '' && line !== opts.self);
27
+ };
28
+ const findDeadTestCode = (opts) => {
29
+ const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\n');
30
+ const support = tracked.filter((file) => file.includes('/__tests__/') &&
31
+ SUPPORT_EXTENSION.test(file) &&
32
+ !TEST_FILE.test(file) &&
33
+ !file.endsWith('.d.ts'));
34
+ const candidates = [];
35
+ for (const file of support) {
36
+ const underTests = file.slice(file.indexOf('/__tests__/') + '/__tests__/'.length);
37
+ const stem = underTests.split('/').pop().replace(SUPPORT_EXTENSION, '');
38
+ const withoutExtension = underTests.replace(SUPPORT_EXTENSION, '');
39
+ if (nameReferencedElsewhere({ root: opts.root, stem, self: file }) ||
40
+ referencedElsewhere({ root: opts.root, needle: withoutExtension, self: file })) {
41
+ continue;
42
+ }
43
+ const folder = (0, node_path_1.dirname)(underTests);
44
+ const folderReferenced = folder !== '.' && referencedElsewhere({ root: opts.root, needle: folder, self: file });
45
+ candidates.push({ file, kind: folderReferenced ? 'folder-referenced' : 'unreferenced' });
46
+ }
47
+ return candidates;
48
+ };
49
+ exports.findDeadTestCode = findDeadTestCode;
50
+ //# sourceMappingURL=test-dead-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-dead-code.js","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\n/**\n * Support files of the tests (fixtures, helpers, probes) that no other file refers to. The second\n * step of the trim-tests method. The output is CANDIDATES: a fixture can be loaded by a path built at\n * run time or by scanning a folder, so a candidate is deleted only after the package suite is green\n * without it.\n */\nexport interface IDeadTestCodeCandidate {\n file: string;\n /**\n * `unreferenced`: neither its name nor its path under `__tests__` appears in another file, nor does\n * its folder. `folder-referenced`: only its folder path does, so a scan of that folder may load it.\n */\n kind: 'unreferenced' | 'folder-referenced';\n}\n\nconst SUPPORT_EXTENSION = /\\.(?:[cm]?[jt]sx?|json|sql|txt)$/;\nconst TEST_FILE = /\\.(?:test|spec)\\.[cm]?[jt]sx?$/;\n\nconst git = (opts: { root: string; args: string[] }): { status: number; stdout: string } => {\n const result = spawnSync('git', opts.args, {\n cwd: opts.root,\n encoding: 'utf8',\n maxBuffer: 256 * 1024 * 1024,\n });\n return { status: result.status ?? 1, stdout: result.stdout ?? '' };\n};\n\n/** Files other than `self` whose text contains `needle` (a fixed string). */\nconst referencedElsewhere = (opts: { root: string; needle: string; self: string }): boolean => {\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-F', '-e', opts.needle] });\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nconst escapeForRegex = (text: string): string => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/** Files other than `self` that mention the name of the file as a path segment or an import. */\nconst nameReferencedElsewhere = (opts: { root: string; stem: string; self: string }): boolean => {\n const pattern = `[/'\"\\`]${escapeForRegex(opts.stem)}(\\\\.|'|\"|\\`|$)`;\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nexport const findDeadTestCode = (opts: { root: string }): IDeadTestCodeCandidate[] => {\n const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\\n');\n const support = tracked.filter(\n (file) =>\n file.includes('/__tests__/') &&\n SUPPORT_EXTENSION.test(file) &&\n !TEST_FILE.test(file) &&\n !file.endsWith('.d.ts'),\n );\n const candidates: IDeadTestCodeCandidate[] = [];\n\n for (const file of support) {\n const underTests = file.slice(file.indexOf('/__tests__/') + '/__tests__/'.length);\n const stem = underTests.split('/').pop()!.replace(SUPPORT_EXTENSION, '');\n const withoutExtension = underTests.replace(SUPPORT_EXTENSION, '');\n\n if (\n nameReferencedElsewhere({ root: opts.root, stem, self: file }) ||\n referencedElsewhere({ root: opts.root, needle: withoutExtension, self: file })\n ) {\n continue;\n }\n\n const folder = dirname(underTests);\n const folderReferenced =\n folder !== '.' && referencedElsewhere({ root: opts.root, needle: folder, self: file });\n\n candidates.push({ file, kind: folderReferenced ? 'folder-referenced' : 'unreferenced' });\n }\n\n return candidates;\n};\n"]}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Replays the mutation proof of the trim-tests method. A ledger line says: "this test (the
3
+ * candidate) is redundant because that test (the survivor) already catches the same break". The
4
+ * replay applies the break (the mutant) to a file, runs the survivor and the candidate, and puts the
5
+ * file back. The survivor must go red. The candidate must go red too, unless the line says it is
6
+ * vacuous and a second mutant leaves it green as well.
7
+ *
8
+ * The mutant edits the file the test really loads: the package's own `src` for a test that imports
9
+ * through the `@/` alias, or the `dist` of the dependency for a test that reaches another package
10
+ * through its `exports`. A rebuild is never needed.
11
+ */
12
+ export interface IMutant {
13
+ /** Repository-relative file to edit (a `src` file, or a `dist` file of a dependency). */
14
+ file: string;
15
+ /** Text that must occur exactly once in the file. */
16
+ find: string;
17
+ replace: string;
18
+ }
19
+ export interface ILedgerLine {
20
+ id: string;
21
+ /** Repository-relative test file that is deleted. */
22
+ candidate: string;
23
+ /** Run only the cases whose name matches (`bun test -t`). Default: the whole file. */
24
+ candidateName?: string;
25
+ /** Repository-relative test file that stays. */
26
+ survivor: string;
27
+ survivorName?: string;
28
+ /**
29
+ * The break. An array applies every edit together: a survivor in the package that owns the code
30
+ * loads its `src`, a candidate in another package loads that package's `dist`, so one break is two edits.
31
+ */
32
+ mutant: IMutant | IMutant[];
33
+ /** The source line the mutant stands for, `path:line`; recorded, not used by the replay. */
34
+ srcLine?: string;
35
+ /** `red` (default): the candidate must fail. `vacuous`: it must stay green under `mutant` AND `secondMutant`. */
36
+ expectCandidate?: 'red' | 'vacuous';
37
+ secondMutant?: IMutant | IMutant[];
38
+ }
39
+ export interface ILedger {
40
+ mutants: ILedgerLine[];
41
+ }
42
+ export interface ILedgerResult {
43
+ id: string;
44
+ survivorRed: boolean;
45
+ candidateRed: boolean;
46
+ /** Only for a `vacuous` line: whether the candidate also stayed green under the second mutant. */
47
+ candidateGreenUnderSecond?: boolean;
48
+ ok: boolean;
49
+ problem?: string;
50
+ }
51
+ /** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */
52
+ export declare const replayLedger: (opts: {
53
+ root: string;
54
+ ledger: ILedger;
55
+ scratch?: string;
56
+ }) => ILedgerResult[];
57
+ export declare const formatLedgerResults: (results: ILedgerResult[]) => string[];
58
+ //# sourceMappingURL=test-mutate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-mutate.d.ts","sourceRoot":"","sources":["../../src/checks/test-mutate.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,OAAO;IACtB,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,MAAM,EAAE,OAAO,GAAG,OAAO,EAAE,CAAC;IAC5B,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iHAAiH;IACjH,eAAe,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,kGAAkG;IAClG,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAgJD,6GAA6G;AAC7G,eAAO,MAAM,YAAY,GAAI,MAAM;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,KAAG,aAAa,EAuDhB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,SAAS,aAAa,EAAE,KAAG,MAAM,EAWpE,CAAC"}
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatLedgerResults = exports.replayLedger = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const TEST_TIMEOUT_MILLISECONDS = 300_000;
9
+ /** The one place this tool builds an `Error`: `getError` would make dev-configs depend on inversion, which depends on dev-configs. */
10
+ // eslint-disable-next-line venizia/no-new-error -- getError would be a dev-configs -> inversion cycle
11
+ const replayError = (message) => new Error(message);
12
+ const checksum = (bytes) => (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
13
+ /** The nearest folder at or above the test file that holds a `package.json`: where `bun test` runs. */
14
+ const packageDirectoryOf = (opts) => {
15
+ let current = (0, node_path_1.dirname)((0, node_path_1.join)(opts.root, opts.file));
16
+ for (;;) {
17
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(current, 'package.json'))) {
18
+ return current;
19
+ }
20
+ const parent = (0, node_path_1.dirname)(current);
21
+ if (parent === current || parent.length < opts.root.length) {
22
+ return opts.root;
23
+ }
24
+ current = parent;
25
+ }
26
+ };
27
+ /** Runs one test file (or the cases matching `name`) and reports whether it failed. */
28
+ const runIsRed = (opts) => {
29
+ const directory = packageDirectoryOf({ root: opts.root, file: opts.file });
30
+ const target = (0, node_path_1.join)(opts.root, opts.file).slice(directory.length + 1);
31
+ const args = ['test', target, ...(opts.name ? ['-t', opts.name] : [])];
32
+ const result = (0, node_child_process_1.spawnSync)('bun', args, {
33
+ cwd: directory,
34
+ encoding: 'utf8',
35
+ timeout: TEST_TIMEOUT_MILLISECONDS,
36
+ killSignal: 'SIGKILL',
37
+ env: { ...process.env, NODE_ENV: 'test' },
38
+ });
39
+ if (result.error) {
40
+ throw replayError(`[test-mutate] could not run bun test ${target}: ${result.error.message}`);
41
+ }
42
+ // A name filter that matches nothing runs no case and exits 0: that would read as "green" and could
43
+ // hide a typo as a vacuous test, so a run that executed nothing is an unusable ledger line.
44
+ const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
45
+ const passed = Number(/^\s*(\d+) pass/m.exec(output)?.[1] ?? 0);
46
+ const failed = Number(/^\s*(\d+) fail/m.exec(output)?.[1] ?? 0);
47
+ if (passed + failed === 0) {
48
+ throw replayError(`[test-mutate] bun test ${target}${opts.name ? ` -t ${opts.name}` : ''} ran no case | check the file and the name`);
49
+ }
50
+ return result.status !== 0 || failed > 0;
51
+ };
52
+ /** Applies `mutant`, runs `action`, and puts the file back byte for byte, verified by checksum. */
53
+ const withMutant = (opts) => {
54
+ const path = (0, node_path_1.join)(opts.root, opts.mutant.file);
55
+ const original = (0, node_fs_1.readFileSync)(path);
56
+ const text = original.toString('utf8');
57
+ const occurrences = text.split(opts.mutant.find).length - 1;
58
+ if (occurrences !== 1) {
59
+ throw replayError(`[test-mutate][${opts.id}] \`find\` occurs ${occurrences} times in ${opts.mutant.file}, it must occur exactly once`);
60
+ }
61
+ (0, node_fs_1.mkdirSync)(opts.scratch, { recursive: true });
62
+ const backup = (0, node_path_1.join)(opts.scratch, `test-mutate-${opts.id.replace(/[^\w.-]/g, '_')}.orig`);
63
+ if ((0, node_fs_1.existsSync)(backup)) {
64
+ throw replayError(`[test-mutate][${opts.id}] a backup from an interrupted run exists: ${backup} | restore ${opts.mutant.file} from it, then delete it`);
65
+ }
66
+ (0, node_fs_1.writeFileSync)(backup, original);
67
+ const expected = checksum(original);
68
+ let outcome;
69
+ try {
70
+ (0, node_fs_1.writeFileSync)(path, text.replace(opts.mutant.find, () => opts.mutant.replace));
71
+ outcome = { value: opts.action() };
72
+ }
73
+ catch (error) {
74
+ outcome = { error };
75
+ }
76
+ // Restored whatever the action did, and checked, before its result or error is passed on.
77
+ (0, node_fs_1.writeFileSync)(path, original);
78
+ if (checksum((0, node_fs_1.readFileSync)(path)) !== expected) {
79
+ throw replayError(`[test-mutate][${opts.id}] ${opts.mutant.file} was not restored | the backup is ${backup}`);
80
+ }
81
+ (0, node_fs_1.rmSync)(backup, { force: true });
82
+ if ('error' in outcome) {
83
+ throw outcome.error;
84
+ }
85
+ return outcome.value;
86
+ };
87
+ /** Applies every edit of `mutant`, runs `action`, and puts each file back byte for byte, verified by checksum. */
88
+ const withMutants = (opts) => {
89
+ const edits = Array.isArray(opts.mutant) ? opts.mutant : [opts.mutant];
90
+ const apply = (index) => index === edits.length
91
+ ? opts.action()
92
+ : withMutant({
93
+ root: opts.root,
94
+ mutant: edits[index],
95
+ scratch: opts.scratch,
96
+ id: `${opts.id}-${index}`,
97
+ action: () => apply(index + 1),
98
+ });
99
+ return apply(0);
100
+ };
101
+ /** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */
102
+ const replayLedger = (opts) => {
103
+ const root = (0, node_path_1.resolve)(opts.root);
104
+ const scratch = opts.scratch ?? (0, node_path_1.join)(root, '.agents/local/tmp');
105
+ const results = [];
106
+ for (const line of opts.ledger.mutants) {
107
+ const { survivorRed, candidateRed } = withMutants({
108
+ root,
109
+ mutant: line.mutant,
110
+ scratch,
111
+ id: line.id,
112
+ action: () => ({
113
+ survivorRed: runIsRed({ root, file: line.survivor, name: line.survivorName }),
114
+ candidateRed: runIsRed({ root, file: line.candidate, name: line.candidateName }),
115
+ }),
116
+ });
117
+ const result = { id: line.id, survivorRed, candidateRed, ok: true };
118
+ const expectation = line.expectCandidate ?? 'red';
119
+ if (!survivorRed) {
120
+ result.ok = false;
121
+ result.problem =
122
+ 'the survivor stayed green under the mutant: it does not cover the behaviour';
123
+ }
124
+ else if (expectation === 'red' && !candidateRed) {
125
+ result.ok = false;
126
+ result.problem =
127
+ 'the candidate stayed green under the mutant: record it as vacuous with a second mutant, or pick a mutant it checks';
128
+ }
129
+ else if (expectation === 'vacuous') {
130
+ if (!line.secondMutant) {
131
+ result.ok = false;
132
+ result.problem = 'a vacuous line needs a secondMutant';
133
+ }
134
+ else if (candidateRed) {
135
+ result.ok = false;
136
+ result.problem = 'the candidate went red under the mutant: it is not vacuous';
137
+ }
138
+ else {
139
+ const greenUnderSecond = !withMutants({
140
+ root,
141
+ mutant: line.secondMutant,
142
+ scratch,
143
+ id: `${line.id}-second`,
144
+ action: () => runIsRed({ root, file: line.candidate, name: line.candidateName }),
145
+ });
146
+ result.candidateGreenUnderSecond = greenUnderSecond;
147
+ if (!greenUnderSecond) {
148
+ result.ok = false;
149
+ result.problem = 'the candidate went red under the second mutant: it is not vacuous';
150
+ }
151
+ }
152
+ }
153
+ results.push(result);
154
+ }
155
+ return results;
156
+ };
157
+ exports.replayLedger = replayLedger;
158
+ const formatLedgerResults = (results) => {
159
+ const lines = results.map((result) => `${result.ok ? 'ok ' : 'FAIL'} ${result.id}: survivor ${result.survivorRed ? 'red' : 'GREEN'}, candidate ${result.candidateRed ? 'red' : 'green'}${result.problem ? ` | ${result.problem}` : ''}`);
160
+ const failed = results.filter((result) => !result.ok).length;
161
+ lines.push(`\n[test-mutate] ${results.length - failed}/${results.length} ledger lines hold${failed ? `, ${failed} do not` : ''}.`);
162
+ return lines;
163
+ };
164
+ exports.formatLedgerResults = formatLedgerResults;
165
+ //# sourceMappingURL=test-mutate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-mutate.js","sourceRoot":"","sources":["../../src/checks/test-mutate.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\n/**\n * Replays the mutation proof of the trim-tests method. A ledger line says: \"this test (the\n * candidate) is redundant because that test (the survivor) already catches the same break\". The\n * replay applies the break (the mutant) to a file, runs the survivor and the candidate, and puts the\n * file back. The survivor must go red. The candidate must go red too, unless the line says it is\n * vacuous and a second mutant leaves it green as well.\n *\n * The mutant edits the file the test really loads: the package's own `src` for a test that imports\n * through the `@/` alias, or the `dist` of the dependency for a test that reaches another package\n * through its `exports`. A rebuild is never needed.\n */\nexport interface IMutant {\n /** Repository-relative file to edit (a `src` file, or a `dist` file of a dependency). */\n file: string;\n /** Text that must occur exactly once in the file. */\n find: string;\n replace: string;\n}\n\nexport interface ILedgerLine {\n id: string;\n /** Repository-relative test file that is deleted. */\n candidate: string;\n /** Run only the cases whose name matches (`bun test -t`). Default: the whole file. */\n candidateName?: string;\n /** Repository-relative test file that stays. */\n survivor: string;\n survivorName?: string;\n /**\n * The break. An array applies every edit together: a survivor in the package that owns the code\n * loads its `src`, a candidate in another package loads that package's `dist`, so one break is two edits.\n */\n mutant: IMutant | IMutant[];\n /** The source line the mutant stands for, `path:line`; recorded, not used by the replay. */\n srcLine?: string;\n /** `red` (default): the candidate must fail. `vacuous`: it must stay green under `mutant` AND `secondMutant`. */\n expectCandidate?: 'red' | 'vacuous';\n secondMutant?: IMutant | IMutant[];\n}\n\nexport interface ILedger {\n mutants: ILedgerLine[];\n}\n\nexport interface ILedgerResult {\n id: string;\n survivorRed: boolean;\n candidateRed: boolean;\n /** Only for a `vacuous` line: whether the candidate also stayed green under the second mutant. */\n candidateGreenUnderSecond?: boolean;\n ok: boolean;\n problem?: string;\n}\n\nconst TEST_TIMEOUT_MILLISECONDS = 300_000;\n\n/** The one place this tool builds an `Error`: `getError` would make dev-configs depend on inversion, which depends on dev-configs. */\n// eslint-disable-next-line venizia/no-new-error -- getError would be a dev-configs -> inversion cycle\nconst replayError = (message: string): Error => new Error(message);\n\nconst checksum = (bytes: Buffer): string => createHash('sha256').update(bytes).digest('hex');\n\n/** The nearest folder at or above the test file that holds a `package.json`: where `bun test` runs. */\nconst packageDirectoryOf = (opts: { root: string; file: string }): string => {\n let current = dirname(join(opts.root, opts.file));\n\n for (;;) {\n if (existsSync(join(current, 'package.json'))) {\n return current;\n }\n\n const parent = dirname(current);\n if (parent === current || parent.length < opts.root.length) {\n return opts.root;\n }\n\n current = parent;\n }\n};\n\n/** Runs one test file (or the cases matching `name`) and reports whether it failed. */\nconst runIsRed = (opts: { root: string; file: string; name?: string }): boolean => {\n const directory = packageDirectoryOf({ root: opts.root, file: opts.file });\n const target = join(opts.root, opts.file).slice(directory.length + 1);\n const args = ['test', target, ...(opts.name ? ['-t', opts.name] : [])];\n const result = spawnSync('bun', args, {\n cwd: directory,\n encoding: 'utf8',\n timeout: TEST_TIMEOUT_MILLISECONDS,\n killSignal: 'SIGKILL',\n env: { ...process.env, NODE_ENV: 'test' },\n });\n\n if (result.error) {\n throw replayError(`[test-mutate] could not run bun test ${target}: ${result.error.message}`);\n }\n\n // A name filter that matches nothing runs no case and exits 0: that would read as \"green\" and could\n // hide a typo as a vacuous test, so a run that executed nothing is an unusable ledger line.\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;\n const passed = Number(/^\\s*(\\d+) pass/m.exec(output)?.[1] ?? 0);\n const failed = Number(/^\\s*(\\d+) fail/m.exec(output)?.[1] ?? 0);\n\n if (passed + failed === 0) {\n throw replayError(\n `[test-mutate] bun test ${target}${opts.name ? ` -t ${opts.name}` : ''} ran no case | check the file and the name`,\n );\n }\n\n return result.status !== 0 || failed > 0;\n};\n\n/** Applies `mutant`, runs `action`, and puts the file back byte for byte, verified by checksum. */\nconst withMutant = <T>(opts: {\n root: string;\n mutant: IMutant;\n scratch: string;\n id: string;\n action: () => T;\n}): T => {\n const path = join(opts.root, opts.mutant.file);\n const original = readFileSync(path);\n const text = original.toString('utf8');\n const occurrences = text.split(opts.mutant.find).length - 1;\n\n if (occurrences !== 1) {\n throw replayError(\n `[test-mutate][${opts.id}] \\`find\\` occurs ${occurrences} times in ${opts.mutant.file}, it must occur exactly once`,\n );\n }\n\n mkdirSync(opts.scratch, { recursive: true });\n const backup = join(opts.scratch, `test-mutate-${opts.id.replace(/[^\\w.-]/g, '_')}.orig`);\n if (existsSync(backup)) {\n throw replayError(\n `[test-mutate][${opts.id}] a backup from an interrupted run exists: ${backup} | restore ${opts.mutant.file} from it, then delete it`,\n );\n }\n\n writeFileSync(backup, original);\n const expected = checksum(original);\n\n let outcome: { value: T } | { error: unknown };\n\n try {\n writeFileSync(\n path,\n text.replace(opts.mutant.find, () => opts.mutant.replace),\n );\n outcome = { value: opts.action() };\n } catch (error) {\n outcome = { error };\n }\n\n // Restored whatever the action did, and checked, before its result or error is passed on.\n writeFileSync(path, original);\n\n if (checksum(readFileSync(path)) !== expected) {\n throw replayError(\n `[test-mutate][${opts.id}] ${opts.mutant.file} was not restored | the backup is ${backup}`,\n );\n }\n\n rmSync(backup, { force: true });\n\n if ('error' in outcome) {\n throw outcome.error;\n }\n\n return outcome.value;\n};\n\n/** Applies every edit of `mutant`, runs `action`, and puts each file back byte for byte, verified by checksum. */\nconst withMutants = <T>(opts: {\n root: string;\n mutant: IMutant | IMutant[];\n scratch: string;\n id: string;\n action: () => T;\n}): T => {\n const edits = Array.isArray(opts.mutant) ? opts.mutant : [opts.mutant];\n\n const apply = (index: number): T =>\n index === edits.length\n ? opts.action()\n : withMutant({\n root: opts.root,\n mutant: edits[index],\n scratch: opts.scratch,\n id: `${opts.id}-${index}`,\n action: () => apply(index + 1),\n });\n\n return apply(0);\n};\n\n/** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */\nexport const replayLedger = (opts: {\n root: string;\n ledger: ILedger;\n scratch?: string;\n}): ILedgerResult[] => {\n const root = resolve(opts.root);\n const scratch = opts.scratch ?? join(root, '.agents/local/tmp');\n const results: ILedgerResult[] = [];\n\n for (const line of opts.ledger.mutants) {\n const { survivorRed, candidateRed } = withMutants({\n root,\n mutant: line.mutant,\n scratch,\n id: line.id,\n action: () => ({\n survivorRed: runIsRed({ root, file: line.survivor, name: line.survivorName }),\n candidateRed: runIsRed({ root, file: line.candidate, name: line.candidateName }),\n }),\n });\n const result: ILedgerResult = { id: line.id, survivorRed, candidateRed, ok: true };\n const expectation = line.expectCandidate ?? 'red';\n\n if (!survivorRed) {\n result.ok = false;\n result.problem =\n 'the survivor stayed green under the mutant: it does not cover the behaviour';\n } else if (expectation === 'red' && !candidateRed) {\n result.ok = false;\n result.problem =\n 'the candidate stayed green under the mutant: record it as vacuous with a second mutant, or pick a mutant it checks';\n } else if (expectation === 'vacuous') {\n if (!line.secondMutant) {\n result.ok = false;\n result.problem = 'a vacuous line needs a secondMutant';\n } else if (candidateRed) {\n result.ok = false;\n result.problem = 'the candidate went red under the mutant: it is not vacuous';\n } else {\n const greenUnderSecond = !withMutants({\n root,\n mutant: line.secondMutant,\n scratch,\n id: `${line.id}-second`,\n action: () => runIsRed({ root, file: line.candidate, name: line.candidateName }),\n });\n result.candidateGreenUnderSecond = greenUnderSecond;\n\n if (!greenUnderSecond) {\n result.ok = false;\n result.problem = 'the candidate went red under the second mutant: it is not vacuous';\n }\n }\n }\n\n results.push(result);\n }\n\n return results;\n};\n\nexport const formatLedgerResults = (results: ILedgerResult[]): string[] => {\n const lines = results.map(\n (result) =>\n `${result.ok ? 'ok ' : 'FAIL'} ${result.id}: survivor ${result.survivorRed ? 'red' : 'GREEN'}, candidate ${result.candidateRed ? 'red' : 'green'}${result.problem ? ` | ${result.problem}` : ''}`,\n );\n const failed = results.filter((result) => !result.ok).length;\n lines.push(\n `\\n[test-mutate] ${results.length - failed}/${results.length} ledger lines hold${failed ? `, ${failed} do not` : ''}.`,\n );\n\n return lines;\n};\n"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Reads the JUnit report of `bun test --reporter=junit` and says where the test time and the test
3
+ * count are: per file, per package, the heaviest files, duplicates by name, files that assert nothing,
4
+ * and skips. The first step of the trim-tests method (`.agents/knowledge/process/trimming-tests.md`).
5
+ *
6
+ * The numbers are only as good as the run that made them: a `--parallel` run times each file under
7
+ * contention, so a file that takes 7 s there can take 1 s alone. Re-time a suspect file alone.
8
+ */
9
+ export interface ITestWeightInput {
10
+ /** Names the report in the output: the package. */
11
+ label: string;
12
+ xml: string;
13
+ }
14
+ export interface ITestFileWeight {
15
+ label: string;
16
+ file: string;
17
+ milliseconds: number;
18
+ cases: number;
19
+ assertions: number;
20
+ skipped: number;
21
+ slowestCase: string;
22
+ slowestMilliseconds: number;
23
+ }
24
+ export interface ITestWeightReport {
25
+ files: ITestFileWeight[];
26
+ totalMilliseconds: number;
27
+ totalCases: number;
28
+ totalAssertions: number;
29
+ totalSkipped: number;
30
+ perLabel: Array<{
31
+ label: string;
32
+ files: number;
33
+ cases: number;
34
+ milliseconds: number;
35
+ }>;
36
+ /** Heaviest first. */
37
+ heaviest: ITestFileWeight[];
38
+ /** How many of the heaviest files hold 80% of the time. */
39
+ filesFor80Percent: number;
40
+ /** Files that take under 100 ms, and what they cost together. */
41
+ lightFiles: {
42
+ count: number;
43
+ milliseconds: number;
44
+ };
45
+ /** The same suite and case name in more than one file. */
46
+ sameNameAcrossFiles: Array<{
47
+ name: string;
48
+ files: string[];
49
+ }>;
50
+ zeroAssertionFiles: string[];
51
+ skippedFiles: Array<{
52
+ file: string;
53
+ skipped: number;
54
+ }>;
55
+ }
56
+ export declare const analyseJunit: (opts: {
57
+ reports: ITestWeightInput[];
58
+ }) => ITestWeightReport;
59
+ /** The report as the lines `devz check test-weight` prints. */
60
+ export declare const formatTestWeight: (opts: {
61
+ report: ITestWeightReport;
62
+ top?: number;
63
+ }) => string[];
64
+ //# sourceMappingURL=test-weight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-weight.d.ts","sourceRoot":"","sources":["../../src/checks/test-weight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvF,sBAAsB;IACtB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,2DAA2D;IAC3D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,0DAA0D;IAC1D,mBAAmB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC9D,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AA+BD,eAAO,MAAM,YAAY,GAAI,MAAM;IAAE,OAAO,EAAE,gBAAgB,EAAE,CAAA;CAAE,KAAG,iBAqFpE,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,GAAI,MAAM;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,MAAM,EAoC1F,CAAC"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatTestWeight = exports.analyseJunit = void 0;
4
+ const ENTITIES = {
5
+ amp: '&',
6
+ lt: '<',
7
+ gt: '>',
8
+ quot: '"',
9
+ apos: "'",
10
+ };
11
+ const decodeEntities = (text) => text.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-z]+);/g, (whole, body) => {
12
+ if (body.startsWith('#x')) {
13
+ return String.fromCodePoint(Number.parseInt(body.slice(2), 16));
14
+ }
15
+ if (body.startsWith('#')) {
16
+ return String.fromCodePoint(Number.parseInt(body.slice(1), 10));
17
+ }
18
+ return ENTITIES[body] ?? whole;
19
+ });
20
+ /** Bun writes a file as a top-level suite whose `name` equals its `file`. */
21
+ const FILE_SUITE = /<testsuite name="([^"]+\.(?:test|spec)\.[cm]?[jt]sx?)" file="[^"]+" tests="(\d+)" assertions="(\d+)" failures="\d+" skipped="(\d+)" time="([\d.eE+-]+)"/g;
22
+ const CASE = /<testcase name="([^"]*)" classname="([^"]*)" time="([\d.eE+-]+)" file="([^"]+)"/g;
23
+ const LIGHT_FILE_MILLISECONDS = 100;
24
+ const HEAVY_SHARE = 0.8;
25
+ const analyseJunit = (opts) => {
26
+ const files = [];
27
+ const names = new Map();
28
+ for (const { label, xml } of opts.reports) {
29
+ const start = files.length;
30
+ for (const match of xml.matchAll(FILE_SUITE)) {
31
+ files.push({
32
+ label,
33
+ file: match[1],
34
+ milliseconds: Number(match[5]) * 1000,
35
+ cases: Number(match[2]),
36
+ assertions: Number(match[3]),
37
+ skipped: Number(match[4]),
38
+ slowestCase: '',
39
+ slowestMilliseconds: 0,
40
+ });
41
+ }
42
+ const byFile = new Map(files.slice(start).map((entry) => [entry.file, entry]));
43
+ for (const match of xml.matchAll(CASE)) {
44
+ const caseName = decodeEntities(match[1]);
45
+ const suiteName = decodeEntities(match[2]);
46
+ const entry = byFile.get(match[4]);
47
+ const milliseconds = Number(match[3]) * 1000;
48
+ if (entry && milliseconds > entry.slowestMilliseconds) {
49
+ entry.slowestMilliseconds = milliseconds;
50
+ entry.slowestCase = caseName;
51
+ }
52
+ const key = `${suiteName} > ${caseName}`;
53
+ const where = names.get(key) ?? new Set();
54
+ where.add(`${label}/${match[4]}`);
55
+ names.set(key, where);
56
+ }
57
+ }
58
+ const heaviest = [...files].sort((left, right) => right.milliseconds - left.milliseconds);
59
+ const totalMilliseconds = files.reduce((sum, entry) => sum + entry.milliseconds, 0);
60
+ let accumulated = 0;
61
+ let filesFor80Percent = 0;
62
+ for (const entry of heaviest) {
63
+ accumulated += entry.milliseconds;
64
+ filesFor80Percent += 1;
65
+ if (accumulated >= totalMilliseconds * HEAVY_SHARE) {
66
+ break;
67
+ }
68
+ }
69
+ const light = files.filter((entry) => entry.milliseconds < LIGHT_FILE_MILLISECONDS);
70
+ const labels = [...new Set(files.map((entry) => entry.label))];
71
+ return {
72
+ files,
73
+ totalMilliseconds,
74
+ totalCases: files.reduce((sum, entry) => sum + entry.cases, 0),
75
+ totalAssertions: files.reduce((sum, entry) => sum + entry.assertions, 0),
76
+ totalSkipped: files.reduce((sum, entry) => sum + entry.skipped, 0),
77
+ perLabel: labels.map((label) => {
78
+ const own = files.filter((entry) => entry.label === label);
79
+ return {
80
+ label,
81
+ files: own.length,
82
+ cases: own.reduce((sum, entry) => sum + entry.cases, 0),
83
+ milliseconds: own.reduce((sum, entry) => sum + entry.milliseconds, 0),
84
+ };
85
+ }),
86
+ heaviest,
87
+ filesFor80Percent,
88
+ lightFiles: {
89
+ count: light.length,
90
+ milliseconds: light.reduce((sum, entry) => sum + entry.milliseconds, 0),
91
+ },
92
+ sameNameAcrossFiles: [...names.entries()]
93
+ .filter(([, where]) => where.size > 1)
94
+ .map(([name, where]) => ({ name, files: [...where].sort() })),
95
+ zeroAssertionFiles: files.filter((entry) => entry.assertions === 0).map((entry) => entry.file),
96
+ skippedFiles: files
97
+ .filter((entry) => entry.skipped > 0)
98
+ .map((entry) => ({ file: `${entry.label}/${entry.file}`, skipped: entry.skipped })),
99
+ };
100
+ };
101
+ exports.analyseJunit = analyseJunit;
102
+ /** The report as the lines `devz check test-weight` prints. */
103
+ const formatTestWeight = (opts) => {
104
+ const { report, top = 25 } = opts;
105
+ const seconds = (milliseconds) => (milliseconds / 1000).toFixed(1);
106
+ const lines = [
107
+ `files ${report.files.length} | cases ${report.totalCases} | assertions ${report.totalAssertions} | skipped ${report.totalSkipped} | sum of file times ${seconds(report.totalMilliseconds)} s`,
108
+ ];
109
+ for (const row of report.perLabel) {
110
+ lines.push(` ${row.label.padEnd(14)} files ${String(row.files).padStart(4)} cases ${String(row.cases).padStart(5)} time ${seconds(row.milliseconds).padStart(7)} s`);
111
+ }
112
+ lines.push('', `heaviest ${top} files:`);
113
+ for (const entry of report.heaviest.slice(0, top)) {
114
+ lines.push(`${entry.milliseconds.toFixed(0).padStart(8)} ms ${String(entry.cases).padStart(5)} cases slowest ${entry.slowestMilliseconds.toFixed(0).padStart(7)} ms ${entry.label}/${entry.file}`);
115
+ }
116
+ lines.push('', `80% of the time is in the ${report.filesFor80Percent} heaviest files of ${report.files.length}`, `files under ${LIGHT_FILE_MILLISECONDS} ms: ${report.lightFiles.count}, together ${seconds(report.lightFiles.milliseconds)} s`, `same suite and case name in more than one file: ${report.sameNameAcrossFiles.length}`);
117
+ for (const duplicate of report.sameNameAcrossFiles.slice(0, 10)) {
118
+ lines.push(` ${duplicate.name.slice(0, 90)} -> ${duplicate.files.join(', ')}`);
119
+ }
120
+ lines.push(`files with 0 assertions: ${report.zeroAssertionFiles.length}${report.zeroAssertionFiles.length ? ` (${report.zeroAssertionFiles.slice(0, 3).join(', ')})` : ''}`, `files with skipped cases: ${report.skippedFiles.length} (${report.skippedFiles.reduce((sum, entry) => sum + entry.skipped, 0)} cases)`);
121
+ return lines;
122
+ };
123
+ exports.formatTestWeight = formatTestWeight;
124
+ //# sourceMappingURL=test-weight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-weight.js","sourceRoot":"","sources":["../../src/checks/test-weight.ts"],"names":[],"mappings":";;;AA4CA,MAAM,QAAQ,GAA2B;IACvC,GAAG,EAAE,GAAG;IACR,EAAE,EAAE,GAAG;IACP,EAAE,EAAE,GAAG;IACP,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;CACV,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,IAAY,EAAU,EAAE,CAC9C,IAAI,CAAC,OAAO,CAAC,iCAAiC,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;IACtE,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;AACjC,CAAC,CAAC,CAAC;AAEL,6EAA6E;AAC7E,MAAM,UAAU,GACd,0JAA0J,CAAC;AAC7J,MAAM,IAAI,GAAG,kFAAkF,CAAC;AAEhG,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,WAAW,GAAG,GAAG,CAAC;AAEjB,MAAM,YAAY,GAAG,CAAC,IAAqC,EAAqB,EAAE;IACvF,MAAM,KAAK,GAAsB,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE7C,KAAK,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;QAE3B,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,IAAI,CAAC;gBACT,KAAK;gBACL,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;gBACd,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;gBACrC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBACvB,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAC5B,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBACzB,WAAW,EAAE,EAAE;gBACf,mBAAmB,EAAE,CAAC;aACvB,CAAC,CAAC;QACL,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/E,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACnC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;YAE7C,IAAI,KAAK,IAAI,YAAY,GAAG,KAAK,CAAC,mBAAmB,EAAE,CAAC;gBACtD,KAAK,CAAC,mBAAmB,GAAG,YAAY,CAAC;gBACzC,KAAK,CAAC,WAAW,GAAG,QAAQ,CAAC;YAC/B,CAAC;YAED,MAAM,GAAG,GAAG,GAAG,SAAS,MAAM,QAAQ,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;YAClD,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAClC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IAC1F,MAAM,iBAAiB,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IAEpF,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAC1B,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,WAAW,IAAI,KAAK,CAAC,YAAY,CAAC;QAClC,iBAAiB,IAAI,CAAC,CAAC;QAEvB,IAAI,WAAW,IAAI,iBAAiB,GAAG,WAAW,EAAE,CAAC;YACnD,MAAM;QACR,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,GAAG,uBAAuB,CAAC,CAAC;IACpF,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE/D,OAAO;QACL,KAAK;QACL,iBAAiB;QACjB,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QAC9D,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;QACxE,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAClE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;YAC3D,OAAO;gBACL,KAAK;gBACL,KAAK,EAAE,GAAG,CAAC,MAAM;gBACjB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;gBACvD,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC;aACtE,CAAC;QACJ,CAAC,CAAC;QACF,QAAQ;QACR,iBAAiB;QACjB,UAAU,EAAE;YACV,KAAK,EAAE,KAAK,CAAC,MAAM;YACnB,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC;SACxE;QACD,mBAAmB,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;aACtC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;aACrC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/D,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;QAC9F,YAAY,EAAE,KAAK;aAChB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;aACpC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;KACtF,CAAC;AACJ,CAAC,CAAC;AArFW,QAAA,YAAY,gBAqFvB;AAEF,+DAA+D;AACxD,MAAM,gBAAgB,GAAG,CAAC,IAAiD,EAAY,EAAE;IAC9F,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC;IAClC,MAAM,OAAO,GAAG,CAAC,YAAoB,EAAU,EAAE,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACnF,MAAM,KAAK,GAAa;QACtB,SAAS,MAAM,CAAC,KAAK,CAAC,MAAM,YAAY,MAAM,CAAC,UAAU,iBAAiB,MAAM,CAAC,eAAe,cAAc,MAAM,CAAC,YAAY,wBAAwB,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI;KAC/L,CAAC;IAEF,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CACR,KAAK,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAC1J,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,GAAG,SAAS,CAAC,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QAClD,KAAK,CAAC,IAAI,CACR,GAAG,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,mBAAmB,KAAK,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CACzL,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CACR,EAAE,EACF,6BAA6B,MAAM,CAAC,iBAAiB,sBAAsB,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,EAChG,eAAe,uBAAuB,QAAQ,MAAM,CAAC,UAAU,CAAC,KAAK,cAAc,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,EAC9H,mDAAmD,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,CACvF,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAChE,KAAK,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,KAAK,CAAC,IAAI,CACR,4BAA4B,MAAM,CAAC,kBAAkB,CAAC,MAAM,GAAG,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EACjK,6BAA6B,MAAM,CAAC,YAAY,CAAC,MAAM,KAAK,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CACxI,CAAC;IAEF,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AApCW,QAAA,gBAAgB,oBAoC3B","sourcesContent":["/**\n * Reads the JUnit report of `bun test --reporter=junit` and says where the test time and the test\n * count are: per file, per package, the heaviest files, duplicates by name, files that assert nothing,\n * and skips. The first step of the trim-tests method (`.agents/knowledge/process/trimming-tests.md`).\n *\n * The numbers are only as good as the run that made them: a `--parallel` run times each file under\n * contention, so a file that takes 7 s there can take 1 s alone. Re-time a suspect file alone.\n */\nexport interface ITestWeightInput {\n /** Names the report in the output: the package. */\n label: string;\n xml: string;\n}\n\nexport interface ITestFileWeight {\n label: string;\n file: string;\n milliseconds: number;\n cases: number;\n assertions: number;\n skipped: number;\n slowestCase: string;\n slowestMilliseconds: number;\n}\n\nexport interface ITestWeightReport {\n files: ITestFileWeight[];\n totalMilliseconds: number;\n totalCases: number;\n totalAssertions: number;\n totalSkipped: number;\n perLabel: Array<{ label: string; files: number; cases: number; milliseconds: number }>;\n /** Heaviest first. */\n heaviest: ITestFileWeight[];\n /** How many of the heaviest files hold 80% of the time. */\n filesFor80Percent: number;\n /** Files that take under 100 ms, and what they cost together. */\n lightFiles: { count: number; milliseconds: number };\n /** The same suite and case name in more than one file. */\n sameNameAcrossFiles: Array<{ name: string; files: string[] }>;\n zeroAssertionFiles: string[];\n skippedFiles: Array<{ file: string; skipped: number }>;\n}\n\nconst ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n};\n\nconst decodeEntities = (text: string): string =>\n text.replace(/&(#x[0-9a-fA-F]+|#\\d+|[a-z]+);/g, (whole, body: string) => {\n if (body.startsWith('#x')) {\n return String.fromCodePoint(Number.parseInt(body.slice(2), 16));\n }\n\n if (body.startsWith('#')) {\n return String.fromCodePoint(Number.parseInt(body.slice(1), 10));\n }\n\n return ENTITIES[body] ?? whole;\n });\n\n/** Bun writes a file as a top-level suite whose `name` equals its `file`. */\nconst FILE_SUITE =\n /<testsuite name=\"([^\"]+\\.(?:test|spec)\\.[cm]?[jt]sx?)\" file=\"[^\"]+\" tests=\"(\\d+)\" assertions=\"(\\d+)\" failures=\"\\d+\" skipped=\"(\\d+)\" time=\"([\\d.eE+-]+)\"/g;\nconst CASE = /<testcase name=\"([^\"]*)\" classname=\"([^\"]*)\" time=\"([\\d.eE+-]+)\" file=\"([^\"]+)\"/g;\n\nconst LIGHT_FILE_MILLISECONDS = 100;\nconst HEAVY_SHARE = 0.8;\n\nexport const analyseJunit = (opts: { reports: ITestWeightInput[] }): ITestWeightReport => {\n const files: ITestFileWeight[] = [];\n const names = new Map<string, Set<string>>();\n\n for (const { label, xml } of opts.reports) {\n const start = files.length;\n\n for (const match of xml.matchAll(FILE_SUITE)) {\n files.push({\n label,\n file: match[1],\n milliseconds: Number(match[5]) * 1000,\n cases: Number(match[2]),\n assertions: Number(match[3]),\n skipped: Number(match[4]),\n slowestCase: '',\n slowestMilliseconds: 0,\n });\n }\n\n const byFile = new Map(files.slice(start).map((entry) => [entry.file, entry]));\n for (const match of xml.matchAll(CASE)) {\n const caseName = decodeEntities(match[1]);\n const suiteName = decodeEntities(match[2]);\n const entry = byFile.get(match[4]);\n const milliseconds = Number(match[3]) * 1000;\n\n if (entry && milliseconds > entry.slowestMilliseconds) {\n entry.slowestMilliseconds = milliseconds;\n entry.slowestCase = caseName;\n }\n\n const key = `${suiteName} > ${caseName}`;\n const where = names.get(key) ?? new Set<string>();\n where.add(`${label}/${match[4]}`);\n names.set(key, where);\n }\n }\n\n const heaviest = [...files].sort((left, right) => right.milliseconds - left.milliseconds);\n const totalMilliseconds = files.reduce((sum, entry) => sum + entry.milliseconds, 0);\n\n let accumulated = 0;\n let filesFor80Percent = 0;\n for (const entry of heaviest) {\n accumulated += entry.milliseconds;\n filesFor80Percent += 1;\n\n if (accumulated >= totalMilliseconds * HEAVY_SHARE) {\n break;\n }\n }\n\n const light = files.filter((entry) => entry.milliseconds < LIGHT_FILE_MILLISECONDS);\n const labels = [...new Set(files.map((entry) => entry.label))];\n\n return {\n files,\n totalMilliseconds,\n totalCases: files.reduce((sum, entry) => sum + entry.cases, 0),\n totalAssertions: files.reduce((sum, entry) => sum + entry.assertions, 0),\n totalSkipped: files.reduce((sum, entry) => sum + entry.skipped, 0),\n perLabel: labels.map((label) => {\n const own = files.filter((entry) => entry.label === label);\n return {\n label,\n files: own.length,\n cases: own.reduce((sum, entry) => sum + entry.cases, 0),\n milliseconds: own.reduce((sum, entry) => sum + entry.milliseconds, 0),\n };\n }),\n heaviest,\n filesFor80Percent,\n lightFiles: {\n count: light.length,\n milliseconds: light.reduce((sum, entry) => sum + entry.milliseconds, 0),\n },\n sameNameAcrossFiles: [...names.entries()]\n .filter(([, where]) => where.size > 1)\n .map(([name, where]) => ({ name, files: [...where].sort() })),\n zeroAssertionFiles: files.filter((entry) => entry.assertions === 0).map((entry) => entry.file),\n skippedFiles: files\n .filter((entry) => entry.skipped > 0)\n .map((entry) => ({ file: `${entry.label}/${entry.file}`, skipped: entry.skipped })),\n };\n};\n\n/** The report as the lines `devz check test-weight` prints. */\nexport const formatTestWeight = (opts: { report: ITestWeightReport; top?: number }): string[] => {\n const { report, top = 25 } = opts;\n const seconds = (milliseconds: number): string => (milliseconds / 1000).toFixed(1);\n const lines: string[] = [\n `files ${report.files.length} | cases ${report.totalCases} | assertions ${report.totalAssertions} | skipped ${report.totalSkipped} | sum of file times ${seconds(report.totalMilliseconds)} s`,\n ];\n\n for (const row of report.perLabel) {\n lines.push(\n ` ${row.label.padEnd(14)} files ${String(row.files).padStart(4)} cases ${String(row.cases).padStart(5)} time ${seconds(row.milliseconds).padStart(7)} s`,\n );\n }\n\n lines.push('', `heaviest ${top} files:`);\n for (const entry of report.heaviest.slice(0, top)) {\n lines.push(\n `${entry.milliseconds.toFixed(0).padStart(8)} ms ${String(entry.cases).padStart(5)} cases slowest ${entry.slowestMilliseconds.toFixed(0).padStart(7)} ms ${entry.label}/${entry.file}`,\n );\n }\n\n lines.push(\n '',\n `80% of the time is in the ${report.filesFor80Percent} heaviest files of ${report.files.length}`,\n `files under ${LIGHT_FILE_MILLISECONDS} ms: ${report.lightFiles.count}, together ${seconds(report.lightFiles.milliseconds)} s`,\n `same suite and case name in more than one file: ${report.sameNameAcrossFiles.length}`,\n );\n for (const duplicate of report.sameNameAcrossFiles.slice(0, 10)) {\n lines.push(` ${duplicate.name.slice(0, 90)} -> ${duplicate.files.join(', ')}`);\n }\n\n lines.push(\n `files with 0 assertions: ${report.zeroAssertionFiles.length}${report.zeroAssertionFiles.length ? ` (${report.zeroAssertionFiles.slice(0, 3).join(', ')})` : ''}`,\n `files with skipped cases: ${report.skippedFiles.length} (${report.skippedFiles.reduce((sum, entry) => sum + entry.skipped, 0)} cases)`,\n );\n\n return lines;\n};\n"]}
@@ -1,5 +1,9 @@
1
1
  /** `devz check <name>`: runs one of the shared repository gates in the current folder. */
2
2
  export declare class CheckCommand {
3
+ private static runTestWeight;
4
+ private static runTestDeadCode;
5
+ private static runTestMutate;
6
+ private static runTestControls;
3
7
  private static runSplitReport;
4
8
  private static runCatalog;
5
9
  static run(opts: {
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"AA0HA,0FAA0F;AAC1F,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,cAAc;mBAkBR,UAAU;WAsClB,GAAG,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAiF3E"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"AAwIA,0FAA0F;AAC1F,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,aAAa;IA8B5B,OAAO,CAAC,MAAM,CAAC,eAAe;IAqB9B,OAAO,CAAC,MAAM,CAAC,aAAa;mBA2BP,eAAe;IAmCpC,OAAO,CAAC,MAAM,CAAC,cAAc;mBAkBR,UAAU;WAsClB,GAAG,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAiG3E"}
package/dist/cli/check.js CHANGED
@@ -7,6 +7,10 @@ const catalog_1 = require("../checks/catalog");
7
7
  const module_cycles_1 = require("../checks/module-cycles");
8
8
  const run_1 = require("../checks/purity/run");
9
9
  const split_report_1 = require("../checks/split-report");
10
+ const test_controls_1 = require("../checks/test-controls");
11
+ const test_dead_code_1 = require("../checks/test-dead-code");
12
+ const test_mutate_1 = require("../checks/test-mutate");
13
+ const test_weight_1 = require("../checks/test-weight");
10
14
  const USAGE = `Usage: devz check <name> [options]
11
15
 
12
16
  Checks:
@@ -15,6 +19,10 @@ Checks:
15
19
  split-report [--dist <dir>] [folder...] report hub files, stray types, missing barrels, long files, cycles (never a gate)
16
20
  catalog [--glob <pattern>]... [--pinned <file>]... [--preset-prefix <prefix>]
17
21
  guard dependency ranges against the root workspaces.catalog
22
+ test-weight [--top <n>] <junit.xml>... per-file time, cases, assertions, duplicates and skips from bun's JUnit report
23
+ test-dead-code list support files under __tests__ that no other file refers to (candidates)
24
+ test-mutate <ledger.json> replay a mutation ledger: the survivor must go red under each mutant
25
+ test-controls <registry.json> fail when a registered positive-control test is gone
18
26
 
19
27
  purity
20
28
  --claims <file> a module that exports PURITY_CLAIMS (the claims) and, optionally, PURITY_OPTIONS
@@ -22,6 +30,10 @@ purity
22
30
  --json print the verdict as JSON (no paths of this machine, no timing) instead of the report
23
31
 
24
32
 
33
+ test-weight, test-dead-code, test-mutate, test-controls: the tools of the trim-tests method, see
34
+ .agents/knowledge/process/trimming-tests.md in the IGNIS repository. Produce the JUnit files with
35
+ bun test ./src --parallel --reporter=junit --reporter-outfile=<file>.
36
+
25
37
  module-cycles
26
38
  <dir>, or --dist <dir> the built folder (dist/esm); exits 1 when more cycles than --max, 2 on a usage error
27
39
 
@@ -90,6 +102,95 @@ const parseRepeatable = (opts) => {
90
102
  };
91
103
  /** `devz check <name>`: runs one of the shared repository gates in the current folder. */
92
104
  class CheckCommand {
105
+ static runTestWeight(opts) {
106
+ const parsed = parseRepeatable({ args: opts.args, repeatable: [], single: ['top'] });
107
+ if (typeof parsed === 'string' || parsed.positionals.length === 0) {
108
+ console.error(`devz check test-weight: ${typeof parsed === 'string' ? parsed : 'give at least one JUnit file'}\n${USAGE}`);
109
+ return USAGE_ERROR_CODE;
110
+ }
111
+ const reports = parsed.positionals.map((file) => {
112
+ const path = (0, node_path_1.resolve)(opts.root, file);
113
+ return {
114
+ label: (0, node_path_1.basename)(file).replace(/\.xml$/, ''),
115
+ xml: (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf8') : '',
116
+ };
117
+ });
118
+ const missing = parsed.positionals.filter((file) => !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(opts.root, file)));
119
+ if (missing.length > 0) {
120
+ console.error(`devz check test-weight: not found: ${missing.join(', ')}`);
121
+ return USAGE_ERROR_CODE;
122
+ }
123
+ const top = parsed.values.top?.[0] === undefined ? undefined : Number(parsed.values.top[0]);
124
+ for (const line of (0, test_weight_1.formatTestWeight)({ report: (0, test_weight_1.analyseJunit)({ reports }), top })) {
125
+ console.log(line);
126
+ }
127
+ return 0;
128
+ }
129
+ static runTestDeadCode(opts) {
130
+ if (opts.args.length > 0) {
131
+ console.error(`devz check test-dead-code: unexpected argument "${opts.args[0]}"\n${USAGE}`);
132
+ return USAGE_ERROR_CODE;
133
+ }
134
+ const candidates = (0, test_dead_code_1.findDeadTestCode)({ root: opts.root });
135
+ for (const kind of ['unreferenced', 'folder-referenced']) {
136
+ const own = candidates.filter((candidate) => candidate.kind === kind);
137
+ console.log(`${kind}: ${own.length}`);
138
+ for (const candidate of own) {
139
+ console.log(` ${candidate.file}`);
140
+ }
141
+ }
142
+ console.log('\nThese are candidates: delete one, run the package suite, and keep it if anything fails or loads it by a built path.');
143
+ return 0;
144
+ }
145
+ static runTestMutate(opts) {
146
+ const [file, ...extra] = opts.args;
147
+ if (file === undefined || extra.length > 0) {
148
+ console.error(`devz check test-mutate: give one ledger file\n${USAGE}`);
149
+ return USAGE_ERROR_CODE;
150
+ }
151
+ const path = (0, node_path_1.resolve)(opts.root, file);
152
+ if (!(0, node_fs_1.existsSync)(path)) {
153
+ console.error(`devz check test-mutate: ledger not found: ${file}`);
154
+ return USAGE_ERROR_CODE;
155
+ }
156
+ const ledger = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
157
+ if (!Array.isArray(ledger.mutants)) {
158
+ console.error(`devz check test-mutate: ${file} has no "mutants" array`);
159
+ return USAGE_ERROR_CODE;
160
+ }
161
+ const results = (0, test_mutate_1.replayLedger)({ root: opts.root, ledger });
162
+ for (const line of (0, test_mutate_1.formatLedgerResults)(results)) {
163
+ console.log(line);
164
+ }
165
+ return results.every((result) => result.ok) ? 0 : 1;
166
+ }
167
+ static async runTestControls(opts) {
168
+ const [file, ...extra] = opts.args;
169
+ if (file === undefined || extra.length > 0) {
170
+ console.error(`devz check test-controls: give one registry file\n${USAGE}`);
171
+ return USAGE_ERROR_CODE;
172
+ }
173
+ const path = (0, node_path_1.resolve)(opts.root, file);
174
+ if (!(0, node_fs_1.existsSync)(path)) {
175
+ console.error(`devz check test-controls: registry not found: ${file}`);
176
+ return USAGE_ERROR_CODE;
177
+ }
178
+ const registry = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
179
+ if (!Array.isArray(registry.controls)) {
180
+ console.error(`devz check test-controls: ${file} has no "controls" array`);
181
+ return USAGE_ERROR_CODE;
182
+ }
183
+ const report = await (0, test_controls_1.checkControls)({ root: opts.root, controls: registry.controls });
184
+ if (report.problems.length === 0) {
185
+ console.log(`test-controls: OK - ${report.checked} registered controls found`);
186
+ return 0;
187
+ }
188
+ console.error(`test-controls: ${report.problems.length} of ${report.checked} registered controls are gone:`);
189
+ for (const problem of report.problems) {
190
+ console.error(` [${problem.entry.gate}] ${problem.message}`);
191
+ }
192
+ return 1;
193
+ }
93
194
  static runSplitReport(opts) {
94
195
  const parsed = parseRepeatable({ args: opts.args, repeatable: [], single: ['dist'] });
95
196
  if (typeof parsed === 'string') {
@@ -154,6 +255,18 @@ class CheckCommand {
154
255
  if (name === 'catalog') {
155
256
  return CheckCommand.runCatalog({ root, args: rest });
156
257
  }
258
+ if (name === 'test-weight') {
259
+ return CheckCommand.runTestWeight({ root, args: rest });
260
+ }
261
+ if (name === 'test-dead-code') {
262
+ return CheckCommand.runTestDeadCode({ root, args: rest });
263
+ }
264
+ if (name === 'test-mutate') {
265
+ return CheckCommand.runTestMutate({ root, args: rest });
266
+ }
267
+ if (name === 'test-controls') {
268
+ return CheckCommand.runTestControls({ root, args: rest });
269
+ }
157
270
  if (name !== 'purity') {
158
271
  console.error(`devz check: unknown check "${name}"\n${USAGE}`);
159
272
  return USAGE_ERROR_CODE;
@@ -1 +1 @@
1
- {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { findCatalogProblems } from '../checks/catalog';\nimport { runModuleCycles } from '../checks/module-cycles';\nimport { checkPurity } from '../checks/purity/run';\nimport { SplitReport } from '../checks/split-report';\nimport type { IPurityClaim, IPurityOptions } from '../checks/purity/manifest';\n\nconst USAGE = `Usage: devz check <name> [options]\n\nChecks:\n purity [package] --claims <file> [--json] probe every entry claimed browser-pure (needs a build)\n module-cycles <dir> [--max <n>] list the import cycles of a built ESM folder\n split-report [--dist <dir>] [folder...] report hub files, stray types, missing barrels, long files, cycles (never a gate)\n catalog [--glob <pattern>]... [--pinned <file>]... [--preset-prefix <prefix>]\n guard dependency ranges against the root workspaces.catalog\n\npurity\n --claims <file> a module that exports PURITY_CLAIMS (the claims) and, optionally, PURITY_OPTIONS\n [package] probe only the rows of this package; a token that matches nothing fails\n --json print the verdict as JSON (no paths of this machine, no timing) instead of the report\n\n\nmodule-cycles\n <dir>, or --dist <dir> the built folder (dist/esm); exits 1 when more cycles than --max, 2 on a usage error\n\nsplit-report\n --dist <dir> the built ESM folder inside a package: dist/esm, or dist where one build is published (default dist)\n [folder...] package folders to report; none scans every packages/* and examples/* that has a src/\n\ncatalog\n --glob <pattern> a manifest glob beside packages/*/package.json (repeatable)\n --pinned <file> a manifest outside the workspace: its catalogued ranges must equal the catalog's (repeatable)\n --preset-prefix <p> files under this prefix are presets: catalog: is allowed in their peerDependencies\n\n --help print this text and exit`;\n\n/** A usage error: nothing was probed. */\nconst USAGE_ERROR_CODE = 2;\n\ninterface IClaimsModule {\n PURITY_CLAIMS?: IPurityClaim[];\n PURITY_OPTIONS?: IPurityOptions;\n}\n\ninterface IPurityArguments {\n claimsFile?: string;\n filter?: string;\n json: boolean;\n}\n\nconst parsePurityArguments = (args: string[]): IPurityArguments | string => {\n const parsed: IPurityArguments = { json: false };\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n\n if (arg === '--json') {\n parsed.json = true;\n continue;\n }\n\n if (arg === '--claims') {\n const value = args[index + 1];\n if (value === undefined || value.startsWith('--')) {\n return '--claims needs a file';\n }\n\n parsed.claimsFile = value;\n index += 1;\n continue;\n }\n\n if (arg.startsWith('--')) {\n return `unknown option \"${arg}\"`;\n }\n\n if (parsed.filter !== undefined) {\n return `unexpected argument \"${arg}\"`;\n }\n\n parsed.filter = arg;\n }\n\n return parsed;\n};\n\n/** Splits repeatable `--name value` options and positionals; `undefined` for an option that lacks its value. */\nconst parseRepeatable = (opts: {\n args: string[];\n repeatable: string[];\n single: string[];\n}): { positionals: string[]; values: Record<string, string[]> } | string => {\n const { args, repeatable, single } = opts;\n const values: Record<string, string[]> = {};\n const positionals: string[] = [];\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n\n if (arg.startsWith('--')) {\n const name = arg.slice(2);\n if (!repeatable.includes(name) && !single.includes(name)) {\n return `unknown option \"${arg}\"`;\n }\n\n const value = args[index + 1];\n if (value === undefined || value.startsWith('--')) {\n return `${arg} needs a value`;\n }\n\n values[name] = [...(values[name] ?? []), value];\n index += 1;\n continue;\n }\n\n positionals.push(arg);\n }\n\n return { positionals, values };\n};\n\n/** `devz check <name>`: runs one of the shared repository gates in the current folder. */\nexport class CheckCommand {\n private static runSplitReport(opts: { root: string; args: string[] }): number {\n const parsed = parseRepeatable({ args: opts.args, repeatable: [], single: ['dist'] });\n if (typeof parsed === 'string') {\n console.error(`devz check split-report: ${parsed}\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const targets = parsed.positionals.length\n ? parsed.positionals.map((target) => resolve(opts.root, target))\n : SplitReport.defaultTargets({ root: opts.root });\n\n for (const packageDir of targets) {\n SplitReport.run({ packageDir, distDir: parsed.values.dist?.[0] });\n }\n\n return 0;\n }\n\n private static async runCatalog(opts: { root: string; args: string[] }): Promise<number> {\n const parsed = parseRepeatable({\n args: opts.args,\n repeatable: ['glob', 'pinned'],\n single: ['preset-prefix'],\n });\n if (typeof parsed === 'string' || parsed.positionals.length > 0) {\n console.error(\n `devz check catalog: ${typeof parsed === 'string' ? parsed : `unexpected argument \"${parsed.positionals[0]}\"`}\\n${USAGE}`,\n );\n return USAGE_ERROR_CODE;\n }\n\n const report = await findCatalogProblems({\n root: opts.root,\n globs: parsed.values.glob,\n pinned: parsed.values.pinned,\n presetPrefix: parsed.values['preset-prefix']?.[0],\n });\n\n if (report.noCatalog) {\n console.error('check-catalog: no `workspaces.catalog` in the root package.json');\n return 1;\n }\n\n if (report.problems.length === 0) {\n console.log(`check-catalog: OK - ${report.entries} entries, ${report.referenced} referenced`);\n return 0;\n }\n\n console.error(`check-catalog: ${report.problems.length} problem(s):`);\n for (const problem of report.problems) {\n console.error(` ${problem.file}: ${problem.message}`);\n }\n\n return 1;\n }\n\n static async run(opts: { root: string; args?: string[] }): Promise<number> {\n const { root, args = [] } = opts;\n const [name, ...rest] = args;\n\n if (name === undefined || name === '--help' || name === '-h') {\n console.log(USAGE);\n return name === undefined ? USAGE_ERROR_CODE : 0;\n }\n\n if (name === 'module-cycles') {\n return runModuleCycles({\n args: rest,\n print: (line) => console.log(line),\n printError: (line) => console.error(line),\n });\n }\n\n if (name === 'split-report') {\n return CheckCommand.runSplitReport({ root, args: rest });\n }\n\n if (name === 'catalog') {\n return CheckCommand.runCatalog({ root, args: rest });\n }\n\n if (name !== 'purity') {\n console.error(`devz check: unknown check \"${name}\"\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n if (rest.includes('--help') || rest.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n\n const parsed = parsePurityArguments(rest);\n if (typeof parsed === 'string') {\n console.error(`devz check purity: ${parsed}\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n if (parsed.claimsFile === undefined) {\n console.error(`devz check purity: --claims <file> is required\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const claimsPath = isAbsolute(parsed.claimsFile)\n ? parsed.claimsFile\n : resolve(root, parsed.claimsFile);\n if (!existsSync(claimsPath)) {\n console.error(`devz check purity: claims file not found: ${parsed.claimsFile}`);\n return USAGE_ERROR_CODE;\n }\n\n const claimsModule: IClaimsModule = await import(claimsPath);\n if (!Array.isArray(claimsModule.PURITY_CLAIMS)) {\n console.error(`devz check purity: ${parsed.claimsFile} does not export PURITY_CLAIMS`);\n return USAGE_ERROR_CODE;\n }\n\n const lines: string[] = [];\n const report = await checkPurity({\n root,\n claims: claimsModule.PURITY_CLAIMS,\n options: claimsModule.PURITY_OPTIONS,\n filter: parsed.filter,\n claimsFile: relative(root, claimsPath),\n print: (line) => (parsed.json ? lines.push(line) : console.log(line)),\n printError: (line) => console.error(line),\n });\n\n if (report === undefined) {\n return 1;\n }\n\n if (parsed.json) {\n console.log(JSON.stringify(report, null, 2));\n }\n\n return report.failed > 0 ? 1 : 0;\n }\n}\n"]}
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/cli/check.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { basename, isAbsolute, relative, resolve } from 'node:path';\nimport { findCatalogProblems } from '../checks/catalog';\nimport { runModuleCycles } from '../checks/module-cycles';\nimport { checkPurity } from '../checks/purity/run';\nimport { SplitReport } from '../checks/split-report';\nimport { checkControls } from '../checks/test-controls';\nimport type { IControlEntry } from '../checks/test-controls';\nimport { findDeadTestCode } from '../checks/test-dead-code';\nimport { formatLedgerResults, replayLedger } from '../checks/test-mutate';\nimport type { ILedger } from '../checks/test-mutate';\nimport { analyseJunit, formatTestWeight } from '../checks/test-weight';\nimport type { IPurityClaim, IPurityOptions } from '../checks/purity/manifest';\n\nconst USAGE = `Usage: devz check <name> [options]\n\nChecks:\n purity [package] --claims <file> [--json] probe every entry claimed browser-pure (needs a build)\n module-cycles <dir> [--max <n>] list the import cycles of a built ESM folder\n split-report [--dist <dir>] [folder...] report hub files, stray types, missing barrels, long files, cycles (never a gate)\n catalog [--glob <pattern>]... [--pinned <file>]... [--preset-prefix <prefix>]\n guard dependency ranges against the root workspaces.catalog\n test-weight [--top <n>] <junit.xml>... per-file time, cases, assertions, duplicates and skips from bun's JUnit report\n test-dead-code list support files under __tests__ that no other file refers to (candidates)\n test-mutate <ledger.json> replay a mutation ledger: the survivor must go red under each mutant\n test-controls <registry.json> fail when a registered positive-control test is gone\n\npurity\n --claims <file> a module that exports PURITY_CLAIMS (the claims) and, optionally, PURITY_OPTIONS\n [package] probe only the rows of this package; a token that matches nothing fails\n --json print the verdict as JSON (no paths of this machine, no timing) instead of the report\n\n\ntest-weight, test-dead-code, test-mutate, test-controls: the tools of the trim-tests method, see\n .agents/knowledge/process/trimming-tests.md in the IGNIS repository. Produce the JUnit files with\n bun test ./src --parallel --reporter=junit --reporter-outfile=<file>.\n\nmodule-cycles\n <dir>, or --dist <dir> the built folder (dist/esm); exits 1 when more cycles than --max, 2 on a usage error\n\nsplit-report\n --dist <dir> the built ESM folder inside a package: dist/esm, or dist where one build is published (default dist)\n [folder...] package folders to report; none scans every packages/* and examples/* that has a src/\n\ncatalog\n --glob <pattern> a manifest glob beside packages/*/package.json (repeatable)\n --pinned <file> a manifest outside the workspace: its catalogued ranges must equal the catalog's (repeatable)\n --preset-prefix <p> files under this prefix are presets: catalog: is allowed in their peerDependencies\n\n --help print this text and exit`;\n\n/** A usage error: nothing was probed. */\nconst USAGE_ERROR_CODE = 2;\n\ninterface IClaimsModule {\n PURITY_CLAIMS?: IPurityClaim[];\n PURITY_OPTIONS?: IPurityOptions;\n}\n\ninterface IPurityArguments {\n claimsFile?: string;\n filter?: string;\n json: boolean;\n}\n\nconst parsePurityArguments = (args: string[]): IPurityArguments | string => {\n const parsed: IPurityArguments = { json: false };\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n\n if (arg === '--json') {\n parsed.json = true;\n continue;\n }\n\n if (arg === '--claims') {\n const value = args[index + 1];\n if (value === undefined || value.startsWith('--')) {\n return '--claims needs a file';\n }\n\n parsed.claimsFile = value;\n index += 1;\n continue;\n }\n\n if (arg.startsWith('--')) {\n return `unknown option \"${arg}\"`;\n }\n\n if (parsed.filter !== undefined) {\n return `unexpected argument \"${arg}\"`;\n }\n\n parsed.filter = arg;\n }\n\n return parsed;\n};\n\n/** Splits repeatable `--name value` options and positionals; `undefined` for an option that lacks its value. */\nconst parseRepeatable = (opts: {\n args: string[];\n repeatable: string[];\n single: string[];\n}): { positionals: string[]; values: Record<string, string[]> } | string => {\n const { args, repeatable, single } = opts;\n const values: Record<string, string[]> = {};\n const positionals: string[] = [];\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index];\n\n if (arg.startsWith('--')) {\n const name = arg.slice(2);\n if (!repeatable.includes(name) && !single.includes(name)) {\n return `unknown option \"${arg}\"`;\n }\n\n const value = args[index + 1];\n if (value === undefined || value.startsWith('--')) {\n return `${arg} needs a value`;\n }\n\n values[name] = [...(values[name] ?? []), value];\n index += 1;\n continue;\n }\n\n positionals.push(arg);\n }\n\n return { positionals, values };\n};\n\n/** `devz check <name>`: runs one of the shared repository gates in the current folder. */\nexport class CheckCommand {\n private static runTestWeight(opts: { root: string; args: string[] }): number {\n const parsed = parseRepeatable({ args: opts.args, repeatable: [], single: ['top'] });\n if (typeof parsed === 'string' || parsed.positionals.length === 0) {\n console.error(\n `devz check test-weight: ${typeof parsed === 'string' ? parsed : 'give at least one JUnit file'}\\n${USAGE}`,\n );\n return USAGE_ERROR_CODE;\n }\n\n const reports = parsed.positionals.map((file) => {\n const path = resolve(opts.root, file);\n return {\n label: basename(file).replace(/\\.xml$/, ''),\n xml: existsSync(path) ? readFileSync(path, 'utf8') : '',\n };\n });\n const missing = parsed.positionals.filter((file) => !existsSync(resolve(opts.root, file)));\n if (missing.length > 0) {\n console.error(`devz check test-weight: not found: ${missing.join(', ')}`);\n return USAGE_ERROR_CODE;\n }\n\n const top = parsed.values.top?.[0] === undefined ? undefined : Number(parsed.values.top[0]);\n for (const line of formatTestWeight({ report: analyseJunit({ reports }), top })) {\n console.log(line);\n }\n\n return 0;\n }\n\n private static runTestDeadCode(opts: { root: string; args: string[] }): number {\n if (opts.args.length > 0) {\n console.error(`devz check test-dead-code: unexpected argument \"${opts.args[0]}\"\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const candidates = findDeadTestCode({ root: opts.root });\n for (const kind of ['unreferenced', 'folder-referenced'] as const) {\n const own = candidates.filter((candidate) => candidate.kind === kind);\n console.log(`${kind}: ${own.length}`);\n for (const candidate of own) {\n console.log(` ${candidate.file}`);\n }\n }\n console.log(\n '\\nThese are candidates: delete one, run the package suite, and keep it if anything fails or loads it by a built path.',\n );\n\n return 0;\n }\n\n private static runTestMutate(opts: { root: string; args: string[] }): number {\n const [file, ...extra] = opts.args;\n if (file === undefined || extra.length > 0) {\n console.error(`devz check test-mutate: give one ledger file\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const path = resolve(opts.root, file);\n if (!existsSync(path)) {\n console.error(`devz check test-mutate: ledger not found: ${file}`);\n return USAGE_ERROR_CODE;\n }\n\n const ledger: ILedger = JSON.parse(readFileSync(path, 'utf8'));\n if (!Array.isArray(ledger.mutants)) {\n console.error(`devz check test-mutate: ${file} has no \"mutants\" array`);\n return USAGE_ERROR_CODE;\n }\n\n const results = replayLedger({ root: opts.root, ledger });\n for (const line of formatLedgerResults(results)) {\n console.log(line);\n }\n\n return results.every((result) => result.ok) ? 0 : 1;\n }\n\n private static async runTestControls(opts: { root: string; args: string[] }): Promise<number> {\n const [file, ...extra] = opts.args;\n if (file === undefined || extra.length > 0) {\n console.error(`devz check test-controls: give one registry file\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const path = resolve(opts.root, file);\n if (!existsSync(path)) {\n console.error(`devz check test-controls: registry not found: ${file}`);\n return USAGE_ERROR_CODE;\n }\n\n const registry: { controls?: IControlEntry[] } = JSON.parse(readFileSync(path, 'utf8'));\n if (!Array.isArray(registry.controls)) {\n console.error(`devz check test-controls: ${file} has no \"controls\" array`);\n return USAGE_ERROR_CODE;\n }\n\n const report = await checkControls({ root: opts.root, controls: registry.controls });\n if (report.problems.length === 0) {\n console.log(`test-controls: OK - ${report.checked} registered controls found`);\n return 0;\n }\n\n console.error(\n `test-controls: ${report.problems.length} of ${report.checked} registered controls are gone:`,\n );\n for (const problem of report.problems) {\n console.error(` [${problem.entry.gate}] ${problem.message}`);\n }\n\n return 1;\n }\n\n private static runSplitReport(opts: { root: string; args: string[] }): number {\n const parsed = parseRepeatable({ args: opts.args, repeatable: [], single: ['dist'] });\n if (typeof parsed === 'string') {\n console.error(`devz check split-report: ${parsed}\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const targets = parsed.positionals.length\n ? parsed.positionals.map((target) => resolve(opts.root, target))\n : SplitReport.defaultTargets({ root: opts.root });\n\n for (const packageDir of targets) {\n SplitReport.run({ packageDir, distDir: parsed.values.dist?.[0] });\n }\n\n return 0;\n }\n\n private static async runCatalog(opts: { root: string; args: string[] }): Promise<number> {\n const parsed = parseRepeatable({\n args: opts.args,\n repeatable: ['glob', 'pinned'],\n single: ['preset-prefix'],\n });\n if (typeof parsed === 'string' || parsed.positionals.length > 0) {\n console.error(\n `devz check catalog: ${typeof parsed === 'string' ? parsed : `unexpected argument \"${parsed.positionals[0]}\"`}\\n${USAGE}`,\n );\n return USAGE_ERROR_CODE;\n }\n\n const report = await findCatalogProblems({\n root: opts.root,\n globs: parsed.values.glob,\n pinned: parsed.values.pinned,\n presetPrefix: parsed.values['preset-prefix']?.[0],\n });\n\n if (report.noCatalog) {\n console.error('check-catalog: no `workspaces.catalog` in the root package.json');\n return 1;\n }\n\n if (report.problems.length === 0) {\n console.log(`check-catalog: OK - ${report.entries} entries, ${report.referenced} referenced`);\n return 0;\n }\n\n console.error(`check-catalog: ${report.problems.length} problem(s):`);\n for (const problem of report.problems) {\n console.error(` ${problem.file}: ${problem.message}`);\n }\n\n return 1;\n }\n\n static async run(opts: { root: string; args?: string[] }): Promise<number> {\n const { root, args = [] } = opts;\n const [name, ...rest] = args;\n\n if (name === undefined || name === '--help' || name === '-h') {\n console.log(USAGE);\n return name === undefined ? USAGE_ERROR_CODE : 0;\n }\n\n if (name === 'module-cycles') {\n return runModuleCycles({\n args: rest,\n print: (line) => console.log(line),\n printError: (line) => console.error(line),\n });\n }\n\n if (name === 'split-report') {\n return CheckCommand.runSplitReport({ root, args: rest });\n }\n\n if (name === 'catalog') {\n return CheckCommand.runCatalog({ root, args: rest });\n }\n\n if (name === 'test-weight') {\n return CheckCommand.runTestWeight({ root, args: rest });\n }\n\n if (name === 'test-dead-code') {\n return CheckCommand.runTestDeadCode({ root, args: rest });\n }\n\n if (name === 'test-mutate') {\n return CheckCommand.runTestMutate({ root, args: rest });\n }\n\n if (name === 'test-controls') {\n return CheckCommand.runTestControls({ root, args: rest });\n }\n\n if (name !== 'purity') {\n console.error(`devz check: unknown check \"${name}\"\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n if (rest.includes('--help') || rest.includes('-h')) {\n console.log(USAGE);\n return 0;\n }\n\n const parsed = parsePurityArguments(rest);\n if (typeof parsed === 'string') {\n console.error(`devz check purity: ${parsed}\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n if (parsed.claimsFile === undefined) {\n console.error(`devz check purity: --claims <file> is required\\n${USAGE}`);\n return USAGE_ERROR_CODE;\n }\n\n const claimsPath = isAbsolute(parsed.claimsFile)\n ? parsed.claimsFile\n : resolve(root, parsed.claimsFile);\n if (!existsSync(claimsPath)) {\n console.error(`devz check purity: claims file not found: ${parsed.claimsFile}`);\n return USAGE_ERROR_CODE;\n }\n\n const claimsModule: IClaimsModule = await import(claimsPath);\n if (!Array.isArray(claimsModule.PURITY_CLAIMS)) {\n console.error(`devz check purity: ${parsed.claimsFile} does not export PURITY_CLAIMS`);\n return USAGE_ERROR_CODE;\n }\n\n const lines: string[] = [];\n const report = await checkPurity({\n root,\n claims: claimsModule.PURITY_CLAIMS,\n options: claimsModule.PURITY_OPTIONS,\n filter: parsed.filter,\n claimsFile: relative(root, claimsPath),\n print: (line) => (parsed.json ? lines.push(line) : console.log(line)),\n printError: (line) => console.error(line),\n });\n\n if (report === undefined) {\n return 1;\n }\n\n if (parsed.json) {\n console.log(JSON.stringify(report, null, 2));\n }\n\n return report.failed > 0 ? 1 : 0;\n }\n}\n"]}
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
4
  * Test preload: `[test] preload = ["@venizia/dev-configs/testing/tmpdir-preload"]` in a bunfig makes
5
- * a bare `bun test` keep its temp folders in the repository, like `make test` does.
5
+ * a bare `bun test` keep its temp folders in the repository, like `make test` does. Bun reads only the
6
+ * `bunfig.toml` of the folder it runs in, so a root file applies to a run from the root, not to `bun test`
7
+ * run inside a package: there, pass `--preload @venizia/dev-configs/testing/tmpdir-preload` in the `test` script.
6
8
  */
7
9
  const repo_tmp_1 = require("./repo-tmp");
8
10
  (0, repo_tmp_1.useRepoTmp)();
@@ -1 +1 @@
1
- {"version":3,"file":"tmpdir-preload.js","sourceRoot":"","sources":["../../src/testing/tmpdir-preload.ts"],"names":[],"mappings":";;AAAA;;;GAGG;AACH,yCAAwC;AAExC,IAAA,qBAAU,GAAE,CAAC","sourcesContent":["/**\n * Test preload: `[test] preload = [\"@venizia/dev-configs/testing/tmpdir-preload\"]` in a bunfig makes\n * a bare `bun test` keep its temp folders in the repository, like `make test` does.\n */\nimport { useRepoTmp } from './repo-tmp';\n\nuseRepoTmp();\n"]}
1
+ {"version":3,"file":"tmpdir-preload.js","sourceRoot":"","sources":["../../src/testing/tmpdir-preload.ts"],"names":[],"mappings":";;AAAA;;;;;GAKG;AACH,yCAAwC;AAExC,IAAA,qBAAU,GAAE,CAAC","sourcesContent":["/**\n * Test preload: `[test] preload = [\"@venizia/dev-configs/testing/tmpdir-preload\"]` in a bunfig makes\n * a bare `bun test` keep its temp folders in the repository, like `make test` does. Bun reads only the\n * `bunfig.toml` of the folder it runs in, so a root file applies to a run from the root, not to `bun test`\n * run inside a package: there, pass `--preload @venizia/dev-configs/testing/tmpdir-preload` in the `test` script.\n */\nimport { useRepoTmp } from './repo-tmp';\n\nuseRepoTmp();\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/dev-configs",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Shareable development configurations for TypeScript projects: oxlint presets with per-rule builders, the ESLint naming-convention rule, the legacy ESLint flat config, and TypeScript compiler options with critical decorator support (experimentalDecorators, emitDecoratorMetadata, useDefineForClassFields). Single source of truth for consistent code style, type checking, and build settings across monorepo packages.",
5
5
  "keywords": [
6
6
  "build-tools",