@ticket-gate/engine 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +81 -2
- package/bin/ticket-gate.mjs +261 -0
- package/checks/assertions-unguarded.mjs +31 -0
- package/checks/assertions.mjs +319 -0
- package/checks/cache-report.mjs +313 -0
- package/checks/docs-stale.mjs +69 -0
- package/checks/duplication.mjs +258 -0
- package/checks/ratchet-options.mjs +51 -0
- package/checks/surface.mjs +397 -0
- package/cli.mjs +379 -0
- package/commands-config.mjs +55 -0
- package/config.mjs +597 -0
- package/digest.mjs +195 -0
- package/env-name.mjs +24 -0
- package/failures.mjs +256 -0
- package/fu.mjs +458 -0
- package/graph/js.mjs +517 -0
- package/graph/load-time.mjs +309 -0
- package/graph/typescript.mjs +28 -0
- package/hooks/pre-tool-use.mjs +213 -0
- package/hooks/stop.mjs +495 -0
- package/is-main.mjs +15 -0
- package/kill-group.mjs +13 -0
- package/ledger.mjs +186 -0
- package/lock.d.mts +72 -0
- package/lock.mjs +329 -0
- package/move-proof.mjs +206 -0
- package/mutations/check.mjs +506 -0
- package/mutations/cli.mjs +361 -0
- package/mutations/exec.mjs +120 -0
- package/mutations/gate.mjs +148 -0
- package/mutations/harness.mjs +240 -0
- package/mutations/kinds.mjs +148 -0
- package/mutations/locate.mjs +71 -0
- package/mutations/project.mjs +23 -0
- package/mutations/registry.mjs +173 -0
- package/mutations/rotate.mjs +419 -0
- package/mutations/rotation-status.mjs +93 -0
- package/mutations/run.mjs +252 -0
- package/mutations/select.mjs +333 -0
- package/mutations/served.mjs +216 -0
- package/mutations/validate.mjs +86 -0
- package/mutations/verdict.mjs +159 -0
- package/nightly/cli.mjs +30 -0
- package/nightly/define.mjs +111 -0
- package/nightly/e2e-runs.mjs +73 -0
- package/nightly/gate.mjs +25 -0
- package/nightly/io.mjs +109 -0
- package/nightly/launchd.mjs +42 -0
- package/nightly/record.mjs +231 -0
- package/nightly/run.mjs +470 -0
- package/nightly/schedule.mjs +76 -0
- package/nightly/seed.mjs +78 -0
- package/nightly/status.mjs +189 -0
- package/nightly/suites.mjs +43 -0
- package/nightly-config.mjs +38 -0
- package/nightly-window.mjs +6 -0
- package/pack-hooks-all.mjs +15 -0
- package/pack-hooks.mjs +26 -0
- package/package.json +19 -4
- package/plan.mjs +272 -0
- package/precheck.mjs +90 -0
- package/project.mjs +21 -0
- package/ratchet.mjs +431 -0
- package/receipt.mjs +34 -0
- package/review-map.mjs +432 -0
- package/root.mjs +33 -0
- package/run.mjs +291 -0
- package/runners/playwright.mjs +132 -0
- package/score/adapters/stryker.mjs +111 -0
- package/score/base.mjs +82 -0
- package/score/cli.mjs +182 -0
- package/score/judge.mjs +107 -0
- package/score/lines.mjs +40 -0
- package/score/schema.mjs +68 -0
- package/score/survivors.mjs +103 -0
- package/score/targets.mjs +34 -0
- package/score-config.mjs +120 -0
- package/selection/check.mjs +45 -0
- package/selection/cli.mjs +267 -0
- package/selection/define.mjs +55 -0
- package/selection/for.mjs +235 -0
- package/selection/graphs.mjs +38 -0
- package/selection/project.mjs +11 -0
- package/selection/select.mjs +159 -0
- package/selection/units.mjs +646 -0
- package/selection/value-rules.mjs +142 -0
- package/selection-config.mjs +76 -0
- package/serve.mjs +142 -0
- package/summary.mjs +34 -0
- package/tree.mjs +211 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// The surface ratchet: every tracked file under the configured roots is classified in a
|
|
2
|
+
// manifest with one kind, and the kind decides what the file may import or do. Engine-shaped:
|
|
3
|
+
// no project path is named here; the manifest, baseline, roots and messages come from the caller.
|
|
4
|
+
// A manifest with `strict: true` also fails every pending entry, the code lines of `check` files over
|
|
5
|
+
// `budget.check` (which may only go down, against the baseline) and more than `budget.wiring` wiring files.
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join, posix } from 'node:path';
|
|
9
|
+
|
|
10
|
+
// Built from parts so no source file carries the package scope as a literal.
|
|
11
|
+
const SCOPE = '@ticket-' + 'gate/';
|
|
12
|
+
|
|
13
|
+
const KINDS = [
|
|
14
|
+
'config',
|
|
15
|
+
'data',
|
|
16
|
+
'check',
|
|
17
|
+
'support',
|
|
18
|
+
'shim',
|
|
19
|
+
'test',
|
|
20
|
+
'wiring',
|
|
21
|
+
'pending',
|
|
22
|
+
];
|
|
23
|
+
const CONFIG_TARGETS = new Set(['config', 'data', 'check', 'pending']);
|
|
24
|
+
const CONFIG_NODE = new Set(['node:path', 'node:url']);
|
|
25
|
+
const SHIM_MAX = 15;
|
|
26
|
+
const SHIM_IMPORT = /^(?:@ticket-gate\/|node:url$)/;
|
|
27
|
+
const ENGINE_INTERNAL = /^@ticket-gate\/[^/]+\/(?!config\.mjs$).+/;
|
|
28
|
+
const TEST_EXT = /\.(?:mjs|js|ts)$/;
|
|
29
|
+
|
|
30
|
+
class InputError extends Error {}
|
|
31
|
+
const inputError = (message) => new InputError(message);
|
|
32
|
+
|
|
33
|
+
/** '**' as a whole segment matches 0+ segments, '*' a run of non-'/' characters. */
|
|
34
|
+
export function globMatch(glob, path) {
|
|
35
|
+
const g = glob.split('/');
|
|
36
|
+
const p = path.split('/');
|
|
37
|
+
const seg = (s) =>
|
|
38
|
+
new RegExp(
|
|
39
|
+
`^${s
|
|
40
|
+
.split('*')
|
|
41
|
+
.map((x) => x.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
|
|
42
|
+
.join('[^/]*')}$`,
|
|
43
|
+
);
|
|
44
|
+
const walk = (gi, pi) => {
|
|
45
|
+
if (gi === g.length) return pi === p.length;
|
|
46
|
+
if (g[gi] === '**') {
|
|
47
|
+
for (let k = pi; k <= p.length; k++) if (walk(gi + 1, k)) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return pi < p.length && seg(g[gi]).test(p[pi]) && walk(gi + 1, pi + 1);
|
|
51
|
+
};
|
|
52
|
+
return walk(0, 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether a '/' after `out` opens a regex literal (not a division): after nothing, an operator or punctuation. */
|
|
56
|
+
function regexAllowed(out) {
|
|
57
|
+
let k = out.length - 1;
|
|
58
|
+
while (k >= 0 && (out[k] === ' ' || out[k] === '\t')) k--;
|
|
59
|
+
if (k < 0 || out[k] === '\n') return true;
|
|
60
|
+
if ('=(,:;[!&|?{}+-*%<>~^'.includes(out[k])) return true;
|
|
61
|
+
return /(?:^|[^\w$])(?:return|typeof)$/.test(out.slice(0, k + 1));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The text with comments removed (string and template contents are kept), as non-blank lines. */
|
|
65
|
+
function codeOf(text) {
|
|
66
|
+
let out = '';
|
|
67
|
+
let quote = '';
|
|
68
|
+
for (let i = 0; i < text.length; i++) {
|
|
69
|
+
const c = text[i];
|
|
70
|
+
const next = text[i + 1];
|
|
71
|
+
if (quote) {
|
|
72
|
+
out += c;
|
|
73
|
+
if (c === '\\') out += text[++i] ?? '';
|
|
74
|
+
else if (c === quote) quote = '';
|
|
75
|
+
} else if (c === '/' && next === '/') {
|
|
76
|
+
while (i < text.length && text[i] !== '\n') i++;
|
|
77
|
+
out += '\n';
|
|
78
|
+
} else if (c === '/' && next === '*') {
|
|
79
|
+
i += 2;
|
|
80
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) {
|
|
81
|
+
if (text[i] === '\n') out += '\n';
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
i++;
|
|
85
|
+
} else if (c === '/' && regexAllowed(out)) {
|
|
86
|
+
out += c;
|
|
87
|
+
let inClass = false;
|
|
88
|
+
// Quotes inside a regex body are blanked, so importsOf never reads one as a string start.
|
|
89
|
+
const body = (ch) => (ch === "'" || ch === '"' || ch === '`' ? ' ' : ch);
|
|
90
|
+
for (i++; i < text.length && text[i] !== '\n'; i++) {
|
|
91
|
+
out += body(text[i]);
|
|
92
|
+
if (text[i] === '\\') out += body(text[++i] ?? '');
|
|
93
|
+
else if (text[i] === '[') inClass = true;
|
|
94
|
+
else if (text[i] === ']') inClass = false;
|
|
95
|
+
else if (text[i] === '/' && !inClass) break;
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
if (c === "'" || c === '"' || c === '`') quote = c;
|
|
99
|
+
out += c;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out.split('\n').filter((l) => l.trim() !== '');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Non-blank lines outside '//' and block comments. */
|
|
106
|
+
export function codeLines(text) {
|
|
107
|
+
return codeOf(text).length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A string literal, consumed first so a `from` or `import` inside one is never a specifier. */
|
|
111
|
+
const STRING = '\'(?:[^\'\\\\\\n]|\\\\.)*\'|"(?:[^"\\\\\\n]|\\\\.)*"|`(?:[^`\\\\]|\\\\.)*`';
|
|
112
|
+
|
|
113
|
+
/** Specifiers of `from '...'`, `import('...')` and `import '...'` on code lines, outside strings. */
|
|
114
|
+
export function importsOf(text) {
|
|
115
|
+
const code = codeOf(text).join('\n');
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const re of [
|
|
118
|
+
/\bfrom\s*['"]([^'"]+)['"]/.source,
|
|
119
|
+
/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/.source,
|
|
120
|
+
/(?:^|[;{}])\s*import\s*['"]([^'"]+)['"]/.source,
|
|
121
|
+
])
|
|
122
|
+
for (const m of code.matchAll(new RegExp(`${STRING}|${re}`, 'gm')))
|
|
123
|
+
if (m[1] !== undefined) out.push(m[1]);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readJson(path, what) {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
throw inputError(`unreadable ${what} ${path}: ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function loadManifest(path) {
|
|
136
|
+
const m = readJson(path, 'manifest');
|
|
137
|
+
if (!Array.isArray(m?.entries)) throw inputError('entries must be an array');
|
|
138
|
+
// prettier-ignore
|
|
139
|
+
if (m.strict !== undefined && typeof m.strict !== 'boolean') throw inputError('strict must be true or false');
|
|
140
|
+
if (m.strict === true) {
|
|
141
|
+
for (const key of ['check', 'wiring']) {
|
|
142
|
+
// prettier-ignore
|
|
143
|
+
if (!Number.isInteger(m.budget?.[key]) || m.budget[key] < 0) throw inputError(`strict needs budget.${key}, a non-negative integer`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
m.entries.forEach((e, i) => {
|
|
148
|
+
const at = `entries[${i}]${Array.isArray(e?.match) ? ` (${e.match[0]})` : ''}`;
|
|
149
|
+
if (!Array.isArray(e?.match) || e.match.length === 0)
|
|
150
|
+
throw inputError(`${at}: match must be a non-empty array`);
|
|
151
|
+
for (const glob of e.match) {
|
|
152
|
+
if (typeof glob !== 'string' || glob === '')
|
|
153
|
+
throw inputError(`${at}: match holds a non-string glob`);
|
|
154
|
+
if (seen.has(glob)) throw inputError(`${at}: glob ${glob} is repeated`);
|
|
155
|
+
seen.add(glob);
|
|
156
|
+
}
|
|
157
|
+
// prettier-ignore
|
|
158
|
+
if (!KINDS.includes(e.kind)) throw inputError(`${at}: unknown kind ${e.kind}`);
|
|
159
|
+
// prettier-ignore
|
|
160
|
+
if (typeof e.why !== 'string' || e.why.trim() === '') throw inputError(`${at}: why must be a non-empty string`);
|
|
161
|
+
});
|
|
162
|
+
return m;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function loadBaseline(path) {
|
|
166
|
+
const b = readJson(path, 'baseline');
|
|
167
|
+
if (!Array.isArray(b?.pending))
|
|
168
|
+
throw inputError(`baseline ${path} has no "pending" array`);
|
|
169
|
+
// prettier-ignore
|
|
170
|
+
if (b.budget !== undefined && !(Number.isInteger(b.budget?.check) && b.budget.check >= 0)) throw inputError(`baseline ${path}: budget.check must be a non-negative integer`);
|
|
171
|
+
return { pending: new Set(b.pending), budgetCheck: b.budget?.check };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function trackedFiles(root, roots) {
|
|
175
|
+
let raw;
|
|
176
|
+
try {
|
|
177
|
+
raw = execFileSync('git', ['ls-files', '-z', '--', ...roots], {
|
|
178
|
+
cwd: root,
|
|
179
|
+
encoding: 'utf8',
|
|
180
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
throw inputError(`cannot list tracked files in ${root}: ${e.message}`);
|
|
184
|
+
}
|
|
185
|
+
const inRoots = (f) =>
|
|
186
|
+
roots.some((r) => (r.endsWith('/') ? f.startsWith(r) : f === r));
|
|
187
|
+
return raw.split('\0').filter((f) => f && inRoots(f));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A surface ratchet bound to `cfg`: { manifest, baseline, roots, comment, fix, usage } (paths relative
|
|
192
|
+
* to the repo root). Returns the ratchet entry { baseline, check({ root, args, update }) }.
|
|
193
|
+
*/
|
|
194
|
+
export function surface(cfg) {
|
|
195
|
+
const { manifest: manifestPath, baseline: baselinePath, roots } = cfg;
|
|
196
|
+
|
|
197
|
+
function run(root, args, updateFlag) {
|
|
198
|
+
let update = Boolean(updateFlag);
|
|
199
|
+
let seed = false;
|
|
200
|
+
for (const a of args ?? []) {
|
|
201
|
+
if (a === '--update') update = true;
|
|
202
|
+
else if (a === '--seed') seed = true;
|
|
203
|
+
else
|
|
204
|
+
throw Object.assign(inputError(`unknown argument ${a}`), {
|
|
205
|
+
usage: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (seed && !update)
|
|
209
|
+
throw Object.assign(inputError('--seed needs --update'), {
|
|
210
|
+
usage: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const manifest = loadManifest(join(root, manifestPath));
|
|
214
|
+
const absBaseline = join(root, baselinePath);
|
|
215
|
+
const haveBaseline = existsSync(absBaseline);
|
|
216
|
+
if (!haveBaseline && !(update && seed))
|
|
217
|
+
throw inputError(
|
|
218
|
+
`no baseline at ${baselinePath}. Fix: run the ratchet once with --update --seed`,
|
|
219
|
+
);
|
|
220
|
+
const { pending: before, budgetCheck: floor } = haveBaseline
|
|
221
|
+
? loadBaseline(absBaseline)
|
|
222
|
+
: { pending: new Set(), budgetCheck: undefined };
|
|
223
|
+
const strict = manifest.strict === true;
|
|
224
|
+
// prettier-ignore
|
|
225
|
+
if (strict && !update && floor === undefined) throw inputError(`baseline ${baselinePath} has no budget.check. Fix: run the ratchet once with --update`);
|
|
226
|
+
|
|
227
|
+
const globs = manifest.entries.flatMap((entry) =>
|
|
228
|
+
entry.match.map((glob) => ({ glob, entry })),
|
|
229
|
+
);
|
|
230
|
+
const pendingGlobs = globs
|
|
231
|
+
.filter((g) => g.entry.kind === 'pending')
|
|
232
|
+
.map((g) => g.glob);
|
|
233
|
+
|
|
234
|
+
if (update) {
|
|
235
|
+
const seeding = !haveBaseline;
|
|
236
|
+
const added = seeding ? [] : pendingGlobs.filter((g) => !before.has(g));
|
|
237
|
+
const refuse = (list) => ({
|
|
238
|
+
code: 1,
|
|
239
|
+
lines: [
|
|
240
|
+
`surface: refused: pending glob(s) not in the baseline: ${list.join(', ')}. A pending entry is never added.`,
|
|
241
|
+
cfg.fix,
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
// prettier-ignore
|
|
245
|
+
if (added.length > 0) return refuse(added);
|
|
246
|
+
const refuseBudget = () => ({
|
|
247
|
+
code: 1,
|
|
248
|
+
lines: [
|
|
249
|
+
`surface: refused: budget.check ${manifest.budget.check} is over ${floor} in ${baselinePath}. A budget only goes down.`,
|
|
250
|
+
cfg.fix,
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
// prettier-ignore
|
|
254
|
+
if (strict && manifest.budget.check > floor) return refuseBudget();
|
|
255
|
+
const next = [...new Set(pendingGlobs)].sort();
|
|
256
|
+
const removed = [...before].filter((g) => !next.includes(g)).length;
|
|
257
|
+
const kept = strict ? manifest.budget.check : floor;
|
|
258
|
+
const nextBaseline = { comment: cfg.comment, pending: next };
|
|
259
|
+
if (kept !== undefined) nextBaseline.budget = { check: kept };
|
|
260
|
+
writeFileSync(absBaseline, `${JSON.stringify(nextBaseline, null, 2)}\n`);
|
|
261
|
+
return {
|
|
262
|
+
code: 0,
|
|
263
|
+
lines: [
|
|
264
|
+
`surface: baseline ${baselinePath}: ${next.length} pending globs (removed ${removed})${strict ? `, budget.check ${kept}` : ''}`,
|
|
265
|
+
],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const files = trackedFiles(root, roots);
|
|
270
|
+
const used = new Set();
|
|
271
|
+
const hitsOf = new Map();
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
const hits = globs.filter((g) => globMatch(g.glob, file));
|
|
274
|
+
for (const h of hits) used.add(h.glob);
|
|
275
|
+
hitsOf.set(file, hits);
|
|
276
|
+
}
|
|
277
|
+
const kindOf = (file) => hitsOf.get(file)?.[0]?.entry.kind;
|
|
278
|
+
const ownerRe = new RegExp(manifest.ownerPattern ?? '.+');
|
|
279
|
+
const out = [];
|
|
280
|
+
let checkLines = 0;
|
|
281
|
+
|
|
282
|
+
function configImportOk(spec, file) {
|
|
283
|
+
if (spec.startsWith(SCOPE) || CONFIG_NODE.has(spec)) return true;
|
|
284
|
+
if (!spec.startsWith('.')) return false;
|
|
285
|
+
const target = posix.join(posix.dirname(file), spec);
|
|
286
|
+
return CONFIG_TARGETS.has(kindOf(target));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const file of files) {
|
|
290
|
+
const hits = hitsOf.get(file);
|
|
291
|
+
// prettier-ignore
|
|
292
|
+
if (hits.length === 0) out.push([1, file, 'matches no entry']);
|
|
293
|
+
// prettier-ignore
|
|
294
|
+
if (hits.length > 1) out.push([1, file, `matches ${hits.length} entries`]);
|
|
295
|
+
const kind = hits[0]?.entry.kind;
|
|
296
|
+
if (!kind || kind === 'pending') continue;
|
|
297
|
+
const abs = join(root, file);
|
|
298
|
+
if (!existsSync(abs)) continue;
|
|
299
|
+
const text = readFileSync(abs, 'utf8');
|
|
300
|
+
// prettier-ignore
|
|
301
|
+
if (kind === 'check') checkLines += codeLines(text);
|
|
302
|
+
if ((kind === 'config' || kind === 'data') && file.endsWith('.mjs')) {
|
|
303
|
+
for (const spec of importsOf(text))
|
|
304
|
+
// prettier-ignore
|
|
305
|
+
if (!configImportOk(spec, file)) out.push([3, file, `imports ${spec}`]);
|
|
306
|
+
for (const code of codeOf(text))
|
|
307
|
+
// prettier-ignore
|
|
308
|
+
if (/\bfetch\(/.test(code)) out.push([3, file, 'calls fetch(']);
|
|
309
|
+
}
|
|
310
|
+
if (kind === 'shim' && file.endsWith('.mjs')) {
|
|
311
|
+
const n = codeLines(text);
|
|
312
|
+
if (n > SHIM_MAX)
|
|
313
|
+
out.push([4, file, `has ${n} code lines, over ${SHIM_MAX}`]);
|
|
314
|
+
for (const spec of importsOf(text))
|
|
315
|
+
// prettier-ignore
|
|
316
|
+
if (!SHIM_IMPORT.test(spec)) out.push([4, file, `imports ${spec}`]);
|
|
317
|
+
}
|
|
318
|
+
if (kind === 'test' && TEST_EXT.test(file)) {
|
|
319
|
+
for (const spec of importsOf(text))
|
|
320
|
+
// prettier-ignore
|
|
321
|
+
if (ENGINE_INTERNAL.test(spec)) out.push([6, file, `imports ${spec}`]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const { glob, entry } of globs) {
|
|
326
|
+
// prettier-ignore
|
|
327
|
+
if (!used.has(glob)) out.push([1, glob, 'matches no tracked file']);
|
|
328
|
+
if (entry.kind !== 'pending') continue;
|
|
329
|
+
// prettier-ignore
|
|
330
|
+
if (!before.has(glob)) out.push([2, glob, 'pending entry is not in the baseline']);
|
|
331
|
+
// prettier-ignore
|
|
332
|
+
if (!ownerRe.test(entry.owner ?? '')) out.push([2, glob, 'pending entry names no owner ticket']);
|
|
333
|
+
// prettier-ignore
|
|
334
|
+
if (strict) out.push([2, glob, 'pending entry in strict mode']);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const wiring = files.filter((f) => kindOf(f) === 'wiring');
|
|
338
|
+
// prettier-ignore
|
|
339
|
+
if (strict && checkLines > manifest.budget.check) out.push([5, manifestPath, `check files have ${checkLines} code lines, over budget.check ${manifest.budget.check}`]);
|
|
340
|
+
// prettier-ignore
|
|
341
|
+
if (strict && manifest.budget.check > floor) out.push([5, manifestPath, `budget.check ${manifest.budget.check} is over ${floor} in ${baselinePath}; the budget only goes down`]);
|
|
342
|
+
// prettier-ignore
|
|
343
|
+
if (strict && wiring.length > manifest.budget.wiring) out.push([6, manifestPath, `${wiring.length} files of kind wiring (${wiring.join(', ')}), over budget.wiring ${manifest.budget.wiring}`]);
|
|
344
|
+
|
|
345
|
+
if (out.length > 0) {
|
|
346
|
+
out.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1]));
|
|
347
|
+
return {
|
|
348
|
+
code: 1,
|
|
349
|
+
lines: [
|
|
350
|
+
`surface: FAIL ${out.length} problem(s) in ${files.length} tracked files`,
|
|
351
|
+
...out.map(([rule, path, why]) => ` [rule ${rule}] ${path}: ${why}`),
|
|
352
|
+
cfg.fix,
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const kinds = Object.fromEntries(KINDS.map((k) => [k, 0]));
|
|
358
|
+
for (const f of files) kinds[kindOf(f)]++;
|
|
359
|
+
const owners = new Map();
|
|
360
|
+
for (const { entry } of globs) {
|
|
361
|
+
if (entry.kind !== 'pending') continue;
|
|
362
|
+
owners.set(entry.owner, (owners.get(entry.owner) ?? 0) + 1);
|
|
363
|
+
}
|
|
364
|
+
const ownerList = [...owners]
|
|
365
|
+
.sort((a, b) => a[0].localeCompare(b[0], 'en', { numeric: true }))
|
|
366
|
+
.map(([o, n]) => `${o} ${n}`)
|
|
367
|
+
.join(', ');
|
|
368
|
+
return {
|
|
369
|
+
code: 0,
|
|
370
|
+
lines: [
|
|
371
|
+
`surface: OK ${files.length} tracked files, ${manifest.entries.length} entries, ${globs.length} globs`,
|
|
372
|
+
` kinds (files): ${KINDS.map((k) => `${k} ${kinds[k]}`).join(', ')}`,
|
|
373
|
+
` pending: ${pendingGlobs.length} globs (${ownerList})`,
|
|
374
|
+
...(strict
|
|
375
|
+
? [
|
|
376
|
+
` budget: check ${checkLines}/${manifest.budget.check} code lines, wiring ${wiring.length}/${manifest.budget.wiring} files`,
|
|
377
|
+
]
|
|
378
|
+
: []),
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
baseline: baselinePath,
|
|
385
|
+
check({ root, args, update }) {
|
|
386
|
+
try {
|
|
387
|
+
return run(root, args, update);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
if (!(e instanceof InputError)) throw e;
|
|
390
|
+
return {
|
|
391
|
+
code: 2,
|
|
392
|
+
lines: [`surface: ${e.message}`, ...(e.usage ? [cfg.usage] : [])],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|