@diffci.com/diffci 0.1.0-alpha.3

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * What do this repository's directories MEAN? (Phase 01 follow-up, 2026-08-26.)
3
+ *
4
+ * WHY. After the first Phase 01 pass the engine named no particular repository, but it still
5
+ * *described* one. `src/repo/impact.ts` answered "is this a script?" with
6
+ * `path.startsWith("scripts/") || path.startsWith("ops/")` and "is this documentation?" with
7
+ * `startsWith("docs/")` - DiffCI's own directory vocabulary, applied to every repository regardless
8
+ * of what that repository actually has. A project keeping its tooling in `tools/` or `bin/` had
9
+ * those files classified as ordinary source; a project with no `ops/` had a rule that could never
10
+ * fire. And the Next.js entry-point rules ran everywhere: `classifyNextEntryPoint()` was called
11
+ * without checking whether the repository was a Next.js app at all, so any file named `route.ts`,
12
+ * `error.ts` or `page.ts` acquired Next.js semantics. Measured across the Phase 01 cohort, that
13
+ * mislabelled files in five of nine repositories - `unjs/h3` alone has seven, where `route` and
14
+ * `error` are HTTP concepts and nothing to do with Next.
15
+ *
16
+ * WHAT THIS IS. One place that turns a RepositoryProfile into answers about THAT repository's
17
+ * layout. Two kinds of input, and the distinction matters:
18
+ *
19
+ * - DISCOVERED: `profile.sourceRoots` records the directories this repository actually has, with
20
+ * the kind they were discovered as. A `scripts` root exists here only if the repository has one.
21
+ * - CONVENTIONAL: a small list of names that mean the same thing across the ecosystem (`docs/`,
22
+ * `doc/`, `documentation/`). These are conventions, not one repository's invention, and they are
23
+ * applied only as names - never as an assumption that the directory exists.
24
+ *
25
+ * What is NOT here: anything derived from a specific repository's habits.
26
+ */
27
+ import { extname } from "node:path";
28
+ /**
29
+ * Directory names that mean "documentation" across the ecosystem. Deliberately short: a name earns a
30
+ * place here by being a convention many projects share, not by appearing in one repository.
31
+ */
32
+ const CONVENTIONAL_DOC_DIRECTORIES = ["docs", "doc", "documentation"];
33
+ function normalizeRoot(root) {
34
+ return root.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
35
+ }
36
+ function isUnder(path, root) {
37
+ return path === root || path.startsWith(`${root}/`);
38
+ }
39
+ export function repositoryLayout(profile) {
40
+ const scriptRoots = Array.from(new Set((profile.sourceRoots ?? [])
41
+ .filter((root) => root.kind === "scripts" || root.kind === "operations")
42
+ .map((root) => normalizeRoot(root.path))
43
+ .filter((root) => root !== ""))).sort();
44
+ // A repository is a Next.js app if it says so - a next.config file, or a declared dependency. Both
45
+ // are the repository's own statement about itself, not an inference from a filename.
46
+ const declaresNext = profile.packageJson.dependencies.includes("next") || profile.packageJson.devDependencies.includes("next");
47
+ const isNextApp = profile.nextConfig?.exists === true || declaresNext;
48
+ return {
49
+ scriptRoots,
50
+ isNextApp,
51
+ isScriptPath(path) {
52
+ const normalized = normalizeRoot(path);
53
+ return scriptRoots.some((root) => isUnder(normalized, root));
54
+ },
55
+ isDocumentationPath(path) {
56
+ const normalized = normalizeRoot(path);
57
+ const ext = extname(normalized).toLowerCase();
58
+ if (ext === ".md" || ext === ".mdx")
59
+ return true;
60
+ const first = normalized.split("/")[0];
61
+ return first !== undefined && CONVENTIONAL_DOC_DIRECTORIES.includes(first.toLowerCase());
62
+ },
63
+ };
64
+ }
65
+ /** The layout of a repository nothing is known about: no script roots, not a Next.js app. Used where
66
+ * a profile genuinely is not available, so the absence is explicit rather than a silent default. */
67
+ export const UNKNOWN_REPOSITORY_LAYOUT = {
68
+ scriptRoots: [],
69
+ isNextApp: false,
70
+ isScriptPath: () => false,
71
+ isDocumentationPath(path) {
72
+ const ext = extname(path).toLowerCase();
73
+ return ext === ".md" || ext === ".mdx";
74
+ },
75
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A repository's own DiffCI configuration (Phase 01 follow-up, 2026-08-26).
3
+ *
4
+ * WHY. `DEFAULT_ALWAYS_RUN_CHECKS` in src/repo/impact.ts forced certain test files to be selected
5
+ * regardless of what the dependency graph said, matching on patterns like `test-api-guardrails`,
6
+ * `verify-*.test.` and `scripts/*.test.mjs`. Those are DiffCI's and DentalPresence's own file names,
7
+ * compiled into the engine and applied to every repository it analysed. On an external repository
8
+ * they matched nothing, so the policy was simultaneously a repo-specific default AND dead weight.
9
+ *
10
+ * The policy itself is worth keeping - some tests really must run regardless of reachability, because
11
+ * they check global properties a dependency graph cannot see. What was wrong is WHERE the list lived.
12
+ * A repository is the only thing that can say which of its own tests those are, so it says so, here:
13
+ *
14
+ * // package.json
15
+ * { "diffci": { "alwaysRunTests": ["**\/security.test.*", "scripts/**\/*.test.mjs"] } }
16
+ *
17
+ * or in a `diffci.json` at the repository root, with the same shape. Absent configuration means no
18
+ * always-run policy - not a guessed one.
19
+ *
20
+ * Globs, not regular expressions: globs are already the vocabulary a repository uses to describe its
21
+ * tests (vitest `include`, jest `testMatch`), and they go through the same matcher as everything else
22
+ * so one definition of "does this path match" holds throughout.
23
+ */
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ function readJsonFile(path) {
27
+ if (!existsSync(path))
28
+ return undefined;
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
31
+ return parsed !== null && typeof parsed === "object" ? parsed : undefined;
32
+ }
33
+ catch {
34
+ // Unreadable or malformed configuration is treated as absent rather than as an error. Getting
35
+ // this wrong must not take down analysis of a repository that is otherwise fine; the cost of the
36
+ // mistake is that the repository's own always-run policy silently does not apply, which is the
37
+ // same position every repository was in before this existed.
38
+ return undefined;
39
+ }
40
+ }
41
+ function parseConfig(raw) {
42
+ if (raw === null || typeof raw !== "object")
43
+ return {};
44
+ const record = raw;
45
+ const alwaysRunTests = Array.isArray(record.alwaysRunTests)
46
+ ? record.alwaysRunTests.filter((entry) => typeof entry === "string" && entry.trim() !== "")
47
+ : undefined;
48
+ return alwaysRunTests && alwaysRunTests.length > 0 ? { alwaysRunTests } : {};
49
+ }
50
+ /**
51
+ * Reads the repository's DiffCI configuration. `diffci.json` at the root wins over a `diffci` key in
52
+ * `package.json`; a repository using both has stated a preference by creating the dedicated file.
53
+ */
54
+ export function readRepositoryConfig(repoPath, packageJson) {
55
+ const dedicated = readJsonFile(join(repoPath, "diffci.json"));
56
+ if (dedicated) {
57
+ const parsed = parseConfig(dedicated);
58
+ if (parsed.alwaysRunTests)
59
+ return parsed;
60
+ }
61
+ const pkg = packageJson ?? readJsonFile(join(repoPath, "package.json"));
62
+ return parseConfig(pkg?.diffci);
63
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The runner's OWN test universe — what the configured test runner can actually execute.
3
+ *
4
+ * WHY THIS EXISTS (defect 17, found 2026-08-31 closing M2 on the ts-jest mutation result).
5
+ *
6
+ * `test-discovery` answered "what looks like a test?" by scanning the tree for `.test.`/`.spec.`
7
+ * files and UNIONING that with whatever globs a config declared. On `kulshekhar/ts-jest` that
8
+ * reported a universe of 40 when jest executes 20: its `jest.config.ts` declares
9
+ * `testMatch: ['<rootDir>/src/**\/*.spec.ts']`, but
10
+ *
11
+ * 1. `<rootDir>/` was never stripped, so the declared glob matched NOTHING - a repo-relative path
12
+ * never contains that token; and
13
+ * 2. even had it matched, the conventional defaults were unioned on top, so 20 `e2e/` and
14
+ * `examples/` spec files that jest is configured never to touch stayed in the universe.
15
+ *
16
+ * On that repository the damage was confined to reporting, because none of the extra 20 was ever
17
+ * selected. That was luck. A change under `e2e/` could have led DiffCI to select files the runner
18
+ * ignores - selection that looks like coverage and detects nothing.
19
+ *
20
+ * THE SAFETY DIRECTION, which governs every decision in this file.
21
+ *
22
+ * Narrowing the modelled universe is the DANGEROUS direction. A test DiffCI cannot see is a test it
23
+ * cannot select, and an unselected test that the runner would have run is exactly the shape of a
24
+ * false green. Over-inclusion only wastes compute.
25
+ *
26
+ * So a declaration may replace the defaults ONLY when it was completely understood. The moment an
27
+ * element is a spread, an unresolved identifier, an interpolated template or a call, `complete` goes
28
+ * false and the caller keeps the old over-inclusive union. Half-understanding a config must never be
29
+ * enough to hide a test.
30
+ *
31
+ * STATIC ONLY. Config files are repository code and are never imported or executed.
32
+ */
33
+ /** Jest's repo-root token. */
34
+ const ROOT_DIR = "<rootDir>";
35
+ function stripRootDir(value) {
36
+ return value.startsWith(ROOT_DIR) ? value.slice(ROOT_DIR.length).replace(/^\//, "") : value;
37
+ }
38
+ /** Blanks comments so a commented-out glob is not lifted. */
39
+ export function stripComments(source) {
40
+ let out = "";
41
+ let i = 0;
42
+ while (i < source.length) {
43
+ const two = source.slice(i, i + 2);
44
+ if (two === "//") {
45
+ const end = source.indexOf("\n", i);
46
+ if (end === -1)
47
+ break;
48
+ out += "\n";
49
+ i = end + 1;
50
+ }
51
+ else if (two === "/*") {
52
+ const end = source.indexOf("*/", i + 2);
53
+ if (end === -1)
54
+ break;
55
+ out += " ".repeat(end + 2 - i);
56
+ i = end + 2;
57
+ }
58
+ else if (source[i] === "'" || source[i] === '"' || source[i] === "`") {
59
+ const quote = source[i];
60
+ let j = i + 1;
61
+ while (j < source.length && source[j] !== quote)
62
+ j += source[j] === "\\" ? 2 : 1;
63
+ out += source.slice(i, Math.min(j + 1, source.length));
64
+ i = j + 1;
65
+ }
66
+ else {
67
+ out += source[i];
68
+ i++;
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ /**
74
+ * Blanks `coverage: { ... }` blocks.
75
+ *
76
+ * Their `include` lists INSTRUMENTED SOURCE files, not tests. Lifting it would classify the whole
77
+ * `src/` tree as tests.
78
+ */
79
+ export function blankCoverageBlocks(source) {
80
+ let out = source;
81
+ for (;;) {
82
+ const m = /\bcoverage\s*:\s*\{/.exec(out);
83
+ if (!m)
84
+ return out;
85
+ const open = m.index + m[0].length - 1;
86
+ let depth = 0;
87
+ let close = -1;
88
+ for (let i = open; i < out.length; i++) {
89
+ if (out[i] === "{")
90
+ depth++;
91
+ else if (out[i] === "}") {
92
+ depth--;
93
+ if (depth === 0) {
94
+ close = i;
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ if (close === -1)
100
+ return out;
101
+ out = out.slice(0, m.index) + " ".repeat(close + 1 - m.index) + out.slice(close + 1);
102
+ }
103
+ }
104
+ /** A single-quoted, double-quoted or NON-interpolated template literal, and nothing else. */
105
+ const PLAIN_LITERAL = /^(?:'([^'\n]*)'|"([^"\n]*)"|`([^`\n$]*)`)$/;
106
+ /**
107
+ * Lift the runner's declared universe out of a config file.
108
+ *
109
+ * Never throws, never executes. Unreadable input yields `complete: false`, which the caller must
110
+ * treat as "keep the wide default universe".
111
+ */
112
+ export function extractTestPatterns(source) {
113
+ const stripped = blankCoverageBlocks(stripComments(source));
114
+ const arrayBodyAt = (open) => {
115
+ let depth = 0;
116
+ for (let i = open; i < stripped.length; i++) {
117
+ if (stripped[i] === "[")
118
+ depth++;
119
+ else if (stripped[i] === "]") {
120
+ depth--;
121
+ if (depth === 0)
122
+ return stripped.slice(open + 1, i);
123
+ }
124
+ }
125
+ return undefined;
126
+ };
127
+ // `const patterns = [ ... ]` so `include: patterns` can be resolved.
128
+ const arrays = new Map();
129
+ for (const m of stripped.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\[/g)) {
130
+ const body = arrayBodyAt(m.index + m[0].length - 1);
131
+ if (body !== undefined)
132
+ arrays.set(m[1], body);
133
+ }
134
+ /** Split on TOP-LEVEL commas, so a nested call or array stays one element. */
135
+ const elementsOf = (body) => {
136
+ const out = [];
137
+ let depth = 0;
138
+ let start = 0;
139
+ for (let i = 0; i < body.length; i++) {
140
+ const ch = body[i];
141
+ if (ch === "[" || ch === "(" || ch === "{")
142
+ depth++;
143
+ else if (ch === "]" || ch === ")" || ch === "}")
144
+ depth--;
145
+ else if (ch === "," && depth === 0) {
146
+ out.push(body.slice(start, i));
147
+ start = i + 1;
148
+ }
149
+ }
150
+ out.push(body.slice(start));
151
+ return out.map((e) => e.trim()).filter((e) => e.length > 0);
152
+ };
153
+ let complete = true;
154
+ const literalsOf = (body) => {
155
+ const out = [];
156
+ for (const element of elementsOf(body)) {
157
+ const m = PLAIN_LITERAL.exec(element);
158
+ if (!m) {
159
+ // A spread, an identifier, an interpolated template, a call. Not understood.
160
+ complete = false;
161
+ continue;
162
+ }
163
+ out.push(m[1] ?? m[2] ?? m[3] ?? "");
164
+ }
165
+ return out;
166
+ };
167
+ /** Returns undefined when the key is absent; [] when present but unreadable. */
168
+ const readKey = (key) => {
169
+ let found;
170
+ const re = new RegExp(`\\b${key}\\s*:\\s*(\\[|([A-Za-z_$][\\w$]*))`, "g");
171
+ for (const m of stripped.matchAll(re)) {
172
+ found ??= [];
173
+ if (m[1] === "[") {
174
+ const body = arrayBodyAt(m.index + m[0].length - 1);
175
+ if (body === undefined) {
176
+ complete = false;
177
+ continue;
178
+ }
179
+ found.push(...literalsOf(body));
180
+ }
181
+ else if (m[2] !== undefined && arrays.has(m[2])) {
182
+ found.push(...literalsOf(arrays.get(m[2])));
183
+ }
184
+ else {
185
+ complete = false;
186
+ }
187
+ }
188
+ return found;
189
+ };
190
+ const rawInclude = readKey("include");
191
+ const rawTestMatch = readKey("testMatch");
192
+ const declaresTests = rawInclude !== undefined || rawTestMatch !== undefined;
193
+ const includes = [];
194
+ for (const raw of [...(rawInclude ?? []), ...(rawTestMatch ?? [])]) {
195
+ // Negations and non-globs (env names and the like) are not part of the universe.
196
+ if (raw.startsWith("!") || !/[*/]/.test(raw))
197
+ continue;
198
+ const value = stripRootDir(raw);
199
+ if (value.includes(ROOT_DIR)) {
200
+ // `<rootDir>` somewhere other than the front - not resolvable without evaluating the config.
201
+ complete = false;
202
+ continue;
203
+ }
204
+ includes.push(value);
205
+ }
206
+ // A declaration that yielded no usable glob is a declaration we failed to read.
207
+ if (declaresTests && includes.length === 0)
208
+ complete = false;
209
+ const roots = (readKey("roots") ?? [])
210
+ .map(stripRootDir)
211
+ .map((r) => r.replace(/^\.\//, "").replace(/\/+$/, ""))
212
+ .filter((r) => r.length > 0 && !r.includes(ROOT_DIR));
213
+ return {
214
+ includes: Array.from(new Set(includes)),
215
+ excludeGlobs: Array.from(new Set((readKey("exclude") ?? []).map(stripRootDir).filter((g) => /[*/]/.test(g) && !g.includes(ROOT_DIR)))),
216
+ ignoreRegexSources: Array.from(new Set(readKey("testPathIgnorePatterns") ?? [])),
217
+ roots,
218
+ declaresTests,
219
+ complete,
220
+ };
221
+ }
222
+ /**
223
+ * Compile jest `testPathIgnorePatterns` to regexes.
224
+ *
225
+ * They are regex sources matched against the test path, and jest matches them against the ABSOLUTE
226
+ * path - which is why the default is `/node_modules/` with leading and trailing slashes. Repo-relative
227
+ * paths are therefore tested with a leading "/" prepended, so the same sources mean the same thing.
228
+ * An uncompilable source is DROPPED rather than thrown: an ignore we cannot read must not silently
229
+ * become an ignore-everything.
230
+ */
231
+ export function compileIgnoreRegexes(sources) {
232
+ const out = [];
233
+ for (const source of sources) {
234
+ try {
235
+ out.push(new RegExp(source));
236
+ }
237
+ catch {
238
+ // Not a regex we can honour. Leaving it out keeps the universe WIDER, which is the safe side.
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+ /** True when `path` is ignored by any of `regexes`, using jest's absolute-path convention. */
244
+ export function isIgnoredPath(path, regexes) {
245
+ const absoluteish = path.startsWith("/") ? path : `/${path}`;
246
+ return regexes.some((r) => r.test(absoluteish));
247
+ }
248
+ /** True when `path` lies under one of `roots`. An empty `roots` means no restriction. */
249
+ export function isUnderRoots(path, roots) {
250
+ if (roots.length === 0)
251
+ return true;
252
+ return roots.some((root) => root === "" || root === "." || path === root || path.startsWith(`${root}/`));
253
+ }