@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,159 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { buildGraph, listFiles, readText } from '../graph/js.mjs';
|
|
5
|
+
import { loadTimeChanged } from '../graph/load-time.mjs';
|
|
6
|
+
import { FullSignal } from './value-rules.mjs';
|
|
7
|
+
import { projectSelection } from './project.mjs';
|
|
8
|
+
|
|
9
|
+
export const startsAny = (path, list) => list.some((p) => path.startsWith(p));
|
|
10
|
+
export const isFullTrigger = (path, cfg = projectSelection()) =>
|
|
11
|
+
startsAny(path, cfg.triggers);
|
|
12
|
+
|
|
13
|
+
export function isOutside(path, cfg = projectSelection()) {
|
|
14
|
+
const { prefixes, rootRe, testFile } = cfg.outside;
|
|
15
|
+
const inDomain = (rootRe?.test(path) ?? false) || startsAny(path, prefixes);
|
|
16
|
+
return (
|
|
17
|
+
!inDomain ||
|
|
18
|
+
path.endsWith('.md') ||
|
|
19
|
+
startsAny(path, cfg.ignored) ||
|
|
20
|
+
(testFile?.test(path) ?? false)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function full(path, rule) {
|
|
25
|
+
return {
|
|
26
|
+
mode: 'full',
|
|
27
|
+
why: `${path}: ${rule}`,
|
|
28
|
+
rule,
|
|
29
|
+
areas: [],
|
|
30
|
+
specs: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** spec path (relative to the spec dir) -> areas declared on line 1; no header -> absent. */
|
|
35
|
+
export function specHeaders(root, cfg = projectSelection()) {
|
|
36
|
+
const out = new Map();
|
|
37
|
+
const dir = cfg.specs.dir;
|
|
38
|
+
for (const f of listFiles(root, dir, (p) => cfg.specs.match.test(p))) {
|
|
39
|
+
const first = (readText(root, f) ?? '')
|
|
40
|
+
.split('\n', 1)[0]
|
|
41
|
+
.replace(/\r$/, '');
|
|
42
|
+
const m = cfg.specs.header.exec(first);
|
|
43
|
+
if (m) out.set(f.slice(dir.length + 1), m[1].split(', '));
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------- select
|
|
49
|
+
|
|
50
|
+
/** No base reader means the load-time rule does not run (the nightly's suspect rule). */
|
|
51
|
+
const NO_BASE = () => null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One path through the pipeline is outside -> before-rules -> triggers ->
|
|
55
|
+
* after-rules -> spec files -> graph reach. A rule's `full` ends the run; its specs/areas are
|
|
56
|
+
* added and the next path is taken.
|
|
57
|
+
* @param {{ root?: string, changed: string[], readBase?: (path: string) => string|null,
|
|
58
|
+
* nightly?: { status?: string, flakySpecs?: string[] } | null, graph?: object,
|
|
59
|
+
* config?: object }} input
|
|
60
|
+
*/
|
|
61
|
+
export function select({
|
|
62
|
+
root: rootArg,
|
|
63
|
+
changed,
|
|
64
|
+
readBase = NO_BASE,
|
|
65
|
+
nightly = null,
|
|
66
|
+
graph: given = null,
|
|
67
|
+
config = null,
|
|
68
|
+
}) {
|
|
69
|
+
const cfg = config ?? projectSelection();
|
|
70
|
+
const root = rootArg ?? cfg.root;
|
|
71
|
+
const specDir = cfg.specs.dir;
|
|
72
|
+
const areas = new Set();
|
|
73
|
+
const specs = new Set();
|
|
74
|
+
let graph = given;
|
|
75
|
+
const getGraph = () => (graph ??= buildGraph(root, cfg));
|
|
76
|
+
const ctx = {
|
|
77
|
+
root,
|
|
78
|
+
cfg,
|
|
79
|
+
readBase,
|
|
80
|
+
readText: (rel) => readText(root, rel),
|
|
81
|
+
graph: getGraph,
|
|
82
|
+
};
|
|
83
|
+
const before = cfg.rules.filter((r) => r.stage !== 'after-triggers');
|
|
84
|
+
const after = cfg.rules.filter((r) => r.stage === 'after-triggers');
|
|
85
|
+
/** @returns {object|null} a full selection, or null when the path was consumed or passed */
|
|
86
|
+
const applyRules = (rules, path) => {
|
|
87
|
+
for (const r of rules) {
|
|
88
|
+
const out = r.rule(path, ctx);
|
|
89
|
+
if (!out) continue;
|
|
90
|
+
if (out.full) return { done: full(path, out.full) };
|
|
91
|
+
for (const a of out.areas ?? []) areas.add(a);
|
|
92
|
+
for (const s of out.specs ?? []) specs.add(s);
|
|
93
|
+
return { done: null };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
for (const path of changed) {
|
|
100
|
+
if (isOutside(path, cfg)) {
|
|
101
|
+
// A tool the harness imports (a spec helper, a config's stack lock) is still on an
|
|
102
|
+
// import edge (D6); the rest are not input.
|
|
103
|
+
if (!startsAny(path, cfg.outside.reach)) continue;
|
|
104
|
+
const hit = getGraph().reach(path);
|
|
105
|
+
if (hit.areas.has('*')) return full(path, 'shell');
|
|
106
|
+
for (const a of hit.areas) areas.add(a);
|
|
107
|
+
for (const s of hit.specs) specs.add(s);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const early = applyRules(before, path);
|
|
111
|
+
if (early?.done) return early.done;
|
|
112
|
+
if (early) continue;
|
|
113
|
+
if (startsAny(path, cfg.triggers)) return full(path, 'trigger');
|
|
114
|
+
const late = applyRules(after, path);
|
|
115
|
+
if (late?.done) return late.done;
|
|
116
|
+
if (late) continue;
|
|
117
|
+
if (path.startsWith(`${specDir}/`) && cfg.specs.match.test(path)) {
|
|
118
|
+
if (existsSync(join(root, path)))
|
|
119
|
+
specs.add(path.slice(specDir.length + 1));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const g = getGraph();
|
|
123
|
+
if (g.unresolved) return full(g.unresolved.importer, 'unresolved');
|
|
124
|
+
const hit = g.reach(path);
|
|
125
|
+
for (const a of hit.areas) areas.add(a);
|
|
126
|
+
if (areas.has('*')) return full(path, 'shell');
|
|
127
|
+
// Load-time code every shell page evaluates, whatever area reaches it.
|
|
128
|
+
if (readBase !== NO_BASE && g.evaluates?.(path)) {
|
|
129
|
+
if (loadTimeChanged(readBase(path), readText(root, path), path))
|
|
130
|
+
return full(path, 'load-time');
|
|
131
|
+
if (g.newlyEvaluatedLoadTime(changed, readBase).has(path))
|
|
132
|
+
return full(path, 'load-time');
|
|
133
|
+
}
|
|
134
|
+
for (const s of hit.specs) specs.add(s);
|
|
135
|
+
if (hit.areas.size > 0 || hit.specs.size > 0) continue;
|
|
136
|
+
if (startsAny(path, cfg.unreachedOk)) continue;
|
|
137
|
+
return full(path, 'unknown');
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof FullSignal) return full(err.path, err.rule);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (nightly?.status === 'amber')
|
|
145
|
+
for (const s of nightly.flakySpecs ?? []) specs.add(s);
|
|
146
|
+
for (const [spec, declared] of specHeaders(root, cfg))
|
|
147
|
+
if (declared.some((a) => areas.has(a))) specs.add(spec);
|
|
148
|
+
|
|
149
|
+
const sortedAreas = [...areas]
|
|
150
|
+
.filter((a) => cfg.areas === null || cfg.areas.includes(a))
|
|
151
|
+
.sort();
|
|
152
|
+
const sortedSpecs = [...specs].sort();
|
|
153
|
+
return {
|
|
154
|
+
mode: 'affected',
|
|
155
|
+
why: `affected: ${sortedAreas.length} areas, ${sortedSpecs.length} specs`,
|
|
156
|
+
areas: sortedAreas,
|
|
157
|
+
specs: sortedSpecs,
|
|
158
|
+
};
|
|
159
|
+
}
|