@fantastic.dev/repo-gates 0.2.1-bootstrap.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,796 @@
1
+ import { Ctx, GateSpec, RepoGatesConfig } from './config.js';
2
+ export { CONFIG_FILENAMES, DEFAULT_CONFIG, findConfigFile, loadConfig, loadContext, mergeConfig } from './config.js';
3
+ export { Boundary, BoundaryEslintConfig, BoundaryPattern, boundariesToEslintConfigs } from './eslint-boundaries.js';
4
+
5
+ /**
6
+ * Quiet, manifest-driven gate runner (the `check:all` engine).
7
+ *
8
+ * Resolves its gate manifest from the consumer's {@link RepoGatesConfig}
9
+ * filtered against the repo's package.json: required gates in the selected
10
+ * manifest are mandatory (a missing one fails the run); conditional gates run
11
+ * only when defined. A consumer may deliberately replace the default manifest
12
+ * — for example, with a coverage-backed test gate instead of a plain test run.
13
+ *
14
+ * Output contract ("quiet"):
15
+ * - one aligned `<status> <gate> (N.Ns)` line per gate
16
+ * - a one-line tally on success
17
+ * - on failure: the failing gate names plus parsed failure signatures
18
+ * from the captured output — never the full log
19
+ * - opts.verbose streams every gate's full output instead
20
+ * - opts.bail stops at the first failing gate
21
+ */
22
+
23
+ /**
24
+ * Resolve the manifest: every required gate (whether or not the repo defines
25
+ * it — a missing required gate must fail loudly, not silently narrow the
26
+ * manifest) plus each conditional gate the repo's package.json defines.
27
+ */
28
+ declare function resolveGates(scripts: Record<string, string>, gates: readonly GateSpec[]): string[];
29
+ /** The resolved manifest for a repo — the single source of truth that the
30
+ * CI-parity gate consumes. */
31
+ declare function gatesForRepo(ctx: Ctx): string[];
32
+ declare function formatGateLine(status: "ok" | "fail", gate: string, seconds: number, width: number): string;
33
+ /**
34
+ * Pull the failure-relevant lines out of a gate's captured output: known
35
+ * failure signatures (tsc/eslint/vitest errors, prettier style
36
+ * complaints, budget-ratchet violations) when present, otherwise the tail
37
+ * of the output.
38
+ */
39
+ declare function extractFailureSignatures(output: string): string[];
40
+ /**
41
+ * Ratchet gates announce a one-line headline metric by printing a
42
+ * `SCORE: <text>` line; this pulls those out of a gate's captured output so
43
+ * the success summary can show them (binary gates emit nothing). Quiet mode
44
+ * only — verbose streams the gate output directly.
45
+ */
46
+ declare function extractScores(output: string): string[];
47
+ /** Render the success `Scores:` block from collected score strings of the
48
+ * form `<label> — <value>`, aligned by label. Empty input → no block. */
49
+ declare function formatScoresBlock(scores: string[]): string[];
50
+ type GateResult = {
51
+ gate: string;
52
+ ok: boolean;
53
+ seconds: number;
54
+ output: string;
55
+ };
56
+ type CheckAllOptions = {
57
+ verbose?: boolean;
58
+ bail?: boolean;
59
+ };
60
+ /** Run every gate in the manifest. Returns the process exit code. */
61
+ declare function runCheckAll(ctx: Ctx, opts?: CheckAllOptions): number;
62
+
63
+ /**
64
+ * CI <-> `check:all` parity drift detector.
65
+ *
66
+ * Parses every gate workflow under `.github/workflows` (basename starting
67
+ * with `ciParity.workflowPrefix`) and fails when a gate-script invocation
68
+ * in a CI `run:` step is not transitively reachable from the gate
69
+ * manifest — i.e. when CI enforces something `check:all` does not exercise
70
+ * locally.
71
+ *
72
+ * Per-repo escape hatches live in `ciParity.configPath`:
73
+ *
74
+ * { "aliases": { "check:coverage:ci": "check:coverage" },
75
+ * "ciOnly": ["test:e2e"] }
76
+ *
77
+ * - `aliases` maps a CI-side script name onto a gate-reachable
78
+ * equivalent (same gate, different reporter/preamble).
79
+ * - `ciOnly` allowlists scripts that are intentionally CI-only (heavy
80
+ * e2e / setup / summaries with no local equivalent).
81
+ */
82
+
83
+ type ParityConfig = {
84
+ aliases: Record<string, string>;
85
+ ciOnly: ReadonlySet<string>;
86
+ };
87
+ declare function loadParityConfig(configPath: string): ParityConfig;
88
+ /** Build the matcher for the configured runner, e.g. runner "pnpm run"
89
+ * matches `pnpm run <name>` and captures `<name>`. A bare invocation
90
+ * without the runner keyword (e.g. `pnpm install`) is intentionally not
91
+ * matched. */
92
+ declare function makeRunTargetRe(runner: string): RegExp;
93
+ declare function extractRunTargets(command: string, runner: string): string[];
94
+ /**
95
+ * Everything reachable from the gate manifest: the manifest itself, the
96
+ * configured entry points (check:all / verify), and the transitive
97
+ * closure of runner references in script bodies.
98
+ */
99
+ declare function computeReachable(scripts: Record<string, string>, gates: readonly string[], entryGates: readonly string[], runner: string): Set<string>;
100
+ type CiInvocation = {
101
+ workflow: string;
102
+ job: string;
103
+ step: number;
104
+ script: string;
105
+ };
106
+ declare function extractCiInvocations(filePath: string, repoRoot: string, runner: string): CiInvocation[];
107
+ /** Gate workflows only — release/deploy orchestration is intentionally
108
+ * out-of-band from the per-PR gate. */
109
+ declare function listCiWorkflows(dir: string, prefix: string): string[];
110
+ type ParityFailure = CiInvocation & {
111
+ canonical: string;
112
+ reason: string;
113
+ };
114
+ declare function evaluateParity(invocations: CiInvocation[], reachable: ReadonlySet<string>, config: ParityConfig, rootGate: string): ParityFailure[];
115
+ declare function checkParity(ctx: Ctx): {
116
+ invocations: CiInvocation[];
117
+ reachable: Set<string>;
118
+ failures: ParityFailure[];
119
+ };
120
+ /** Run the parity check. Returns the process exit code. */
121
+ declare function runCiParity(ctx: Ctx): number;
122
+
123
+ /**
124
+ * Per-file line-count guard (ratchet).
125
+ *
126
+ * - Files NOT listed in the budgets file must be ≤ config.fileSize.threshold.
127
+ * - Files listed in the budgets file must be ≤ their frozen budget (the
128
+ * file's line count when it was grandfathered in). The ratchet only
129
+ * goes DOWN: refactor and lower a budget, never raise it.
130
+ *
131
+ * `--init` writes the current over-threshold files as the grandfather
132
+ * baseline so a repo adopting the gate is green on day one.
133
+ */
134
+
135
+ type BudgetsFile = {
136
+ _comment?: string;
137
+ budgets: Record<string, number>;
138
+ };
139
+ type SizeFailure = {
140
+ path: string;
141
+ lines: number;
142
+ budget: number;
143
+ reason: string;
144
+ };
145
+ declare function loadBudgets$2(budgetsPath: string): Record<string, number>;
146
+ /** Every scanned source file with its repo-relative posix path + line count. */
147
+ declare function collectFiles(ctx: Ctx): {
148
+ rel: string;
149
+ lines: number;
150
+ }[];
151
+ declare function scan$3(ctx: Ctx): {
152
+ failures: SizeFailure[];
153
+ staleBudgetEntries: string[];
154
+ };
155
+ /** Compute the grandfather baseline: every file currently over threshold,
156
+ * frozen at its present line count. */
157
+ declare function seedBudgets$1(ctx: Ctx): Record<string, number>;
158
+ declare function writeSeed$3(ctx: Ctx): {
159
+ path: string;
160
+ count: number;
161
+ };
162
+ /** The scanned file with the least headroom to its budget/threshold — the
163
+ * one closest to failing the ratchet. */
164
+ declare function tightest(ctx: Ctx): {
165
+ path: string;
166
+ lines: number;
167
+ budget: number;
168
+ headroom: number;
169
+ } | undefined;
170
+ /** Run the guard. Returns the process exit code. */
171
+ declare function runFileSizes(ctx: Ctx, init?: boolean): number;
172
+
173
+ type fileSizes_BudgetsFile = BudgetsFile;
174
+ type fileSizes_SizeFailure = SizeFailure;
175
+ declare const fileSizes_collectFiles: typeof collectFiles;
176
+ declare const fileSizes_runFileSizes: typeof runFileSizes;
177
+ declare const fileSizes_tightest: typeof tightest;
178
+ declare namespace fileSizes {
179
+ export { type fileSizes_BudgetsFile as BudgetsFile, type fileSizes_SizeFailure as SizeFailure, fileSizes_collectFiles as collectFiles, loadBudgets$2 as loadBudgets, fileSizes_runFileSizes as runFileSizes, scan$3 as scan, seedBudgets$1 as seedBudgets, fileSizes_tightest as tightest, writeSeed$3 as writeSeed };
180
+ }
181
+
182
+ /**
183
+ * Debt-marker scanner (ratchet).
184
+ *
185
+ * Flags any TODO / FIXME / HACK / XXX (config.debt.markerTokens) that is
186
+ * not paired with a tracker reference on the same line (a Linear-style
187
+ * `ABC-123`, a `#123` issue ref, or a URL — config.debt.trackerPatterns).
188
+ *
189
+ * Untracked markers must be removed, paired with a tracker id, or — as an
190
+ * escape hatch — grandfathered in the allowlist (`path:line` entries).
191
+ * The ratchet only goes down. `--init` seeds the allowlist with every
192
+ * current untracked marker so adoption is green on day one.
193
+ */
194
+
195
+ type AllowlistFile$2 = {
196
+ _comment?: string;
197
+ allowlist: string[];
198
+ };
199
+ type Marker = {
200
+ path: string;
201
+ line: number;
202
+ marker: string;
203
+ text: string;
204
+ };
205
+ type AllowlistEntry$1 = {
206
+ path: string;
207
+ line: number;
208
+ };
209
+ declare function parseAllowlist$2(allowlistPath: string): {
210
+ entries: AllowlistEntry$1[];
211
+ raw: string[];
212
+ };
213
+ declare function scan$2(ctx: Ctx): {
214
+ untracked: Marker[];
215
+ staleAllowlistEntries: string[];
216
+ };
217
+ declare function seedAllowlist$2(ctx: Ctx): string[];
218
+ declare function writeSeed$2(ctx: Ctx): {
219
+ path: string;
220
+ count: number;
221
+ };
222
+ /** Run the guard. Returns the process exit code. */
223
+ declare function runDebtMarkers(ctx: Ctx, init?: boolean): number;
224
+
225
+ type debtMarkers_Marker = Marker;
226
+ declare const debtMarkers_runDebtMarkers: typeof runDebtMarkers;
227
+ declare namespace debtMarkers {
228
+ export { type AllowlistFile$2 as AllowlistFile, type debtMarkers_Marker as Marker, parseAllowlist$2 as parseAllowlist, debtMarkers_runDebtMarkers as runDebtMarkers, scan$2 as scan, seedAllowlist$2 as seedAllowlist, writeSeed$2 as writeSeed };
229
+ }
230
+
231
+ /**
232
+ * Circular-import detector (ratchet).
233
+ *
234
+ * Walks `scanRoots` (same universe as the debt-marker/file-size guards),
235
+ * extracts each file's *relative* import/require specifiers, and resolves
236
+ * them to files on disk. A directed graph of that resolved edge set is
237
+ * reduced to its non-trivial strongly-connected components (Tarjan) — each
238
+ * one is a group of files that import each other in a cycle.
239
+ *
240
+ * Scope: only relative specifiers (`./x`, `../x`) are resolved, so this
241
+ * finds cycles *within* a scanned tree, not cross-package cycles reached
242
+ * through a bare workspace-package specifier (e.g. `@acme/a` importing back
243
+ * into `@acme/b` which imports `@acme/a`) — that needs package-graph
244
+ * resolution this tool doesn't have.
245
+ *
246
+ * Extraction is a regex over raw source text, not a syntax-aware parse — the
247
+ * same tradeoff `check-debt` makes for marker tokens. A specifier-shaped
248
+ * string inside a comment or a string literal (`const hint = 'from "./a"'`)
249
+ * can produce a false edge. A real parser would close this, at the cost of a
250
+ * TS/JS parser dependency this package otherwise has none of; not worth it
251
+ * for the rare false positive, which — like any regex-based gate — is fixed
252
+ * by allowlisting the resulting cycle.
253
+ *
254
+ * Like `check-debt`, new cycles fail the gate; existing ones are
255
+ * grandfathered into an allowlist (`path, path, …` signatures) that can
256
+ * only shrink. `--init` seeds it from the current tree.
257
+ */
258
+
259
+ type AllowlistFile$1 = {
260
+ _comment?: string;
261
+ allowlist: string[];
262
+ };
263
+ type Cycle = {
264
+ files: string[];
265
+ };
266
+ /** Extract every relative import/require specifier from a source file:
267
+ * `import … from "./x"`, `require("./x")`, `import("./x")`, and the
268
+ * bare side-effect form `import "./x"` (no `from`). */
269
+ declare function extractRelativeSpecifiers(source: string): string[];
270
+ /** Resolve a relative specifier from `fromFile` to a real file on disk,
271
+ * trying each source extension and an `/index.<ext>` fallback — mirrors
272
+ * Node/TS module resolution closely enough for local relative imports. */
273
+ declare function resolveSpecifier(fromFile: string, spec: string, sourceExtensions: readonly string[]): string | undefined;
274
+ /** Tarjan's SCC algorithm, iterative to avoid recursion-depth limits on
275
+ * large graphs. Returns every strongly-connected component of size > 1
276
+ * (a genuine cycle — a lone node is never its own SCC here since the
277
+ * graph has no self-loops from this extractor). */
278
+ declare function findCycles(graph: ReadonlyMap<string, ReadonlySet<string>>): string[][];
279
+ declare function buildGraph(ctx: Ctx): Map<string, Set<string>>;
280
+ declare function parseAllowlist$1(allowlistPath: string): string[];
281
+ declare function scan$1(ctx: Ctx): {
282
+ untracked: Cycle[];
283
+ staleAllowlistEntries: string[];
284
+ };
285
+ declare function seedAllowlist$1(ctx: Ctx): string[];
286
+ declare function writeSeed$1(ctx: Ctx): {
287
+ path: string;
288
+ count: number;
289
+ };
290
+ /** Run the guard. Returns the process exit code. */
291
+ declare function runCircularImports(ctx: Ctx, init?: boolean): number;
292
+
293
+ type circularImports_Cycle = Cycle;
294
+ declare const circularImports_buildGraph: typeof buildGraph;
295
+ declare const circularImports_extractRelativeSpecifiers: typeof extractRelativeSpecifiers;
296
+ declare const circularImports_findCycles: typeof findCycles;
297
+ declare const circularImports_resolveSpecifier: typeof resolveSpecifier;
298
+ declare const circularImports_runCircularImports: typeof runCircularImports;
299
+ declare namespace circularImports {
300
+ export { type AllowlistFile$1 as AllowlistFile, type circularImports_Cycle as Cycle, circularImports_buildGraph as buildGraph, circularImports_extractRelativeSpecifiers as extractRelativeSpecifiers, circularImports_findCycles as findCycles, parseAllowlist$1 as parseAllowlist, circularImports_resolveSpecifier as resolveSpecifier, circularImports_runCircularImports as runCircularImports, scan$1 as scan, seedAllowlist$1 as seedAllowlist, writeSeed$1 as writeSeed };
301
+ }
302
+
303
+ /**
304
+ * Secret-shaped-string scanner (ratchet).
305
+ *
306
+ * A lightweight, dependency-free static scan for well-known credential
307
+ * shapes (AWS/GitHub/Slack/Stripe/npm/Google API keys, PEM private-key
308
+ * headers, userinfo-in-URL credentials — `config.secrets.patterns`) across
309
+ * every **git-tracked** file in the repo, not just `scanRoots` — a leaked
310
+ * secret is exactly as bad in a root `.env.example` or a YAML config as it
311
+ * is in `src/`.
312
+ *
313
+ * This is NOT a substitute for a dedicated secret-scanning tool (gitleaks,
314
+ * trufflehog): no entropy analysis, no git-history scan (only the current
315
+ * tracked tree), and only the shapes above. It exists to catch an
316
+ * accidental commit of a live token during normal review, not to be a
317
+ * complete secrets program.
318
+ *
319
+ * A finding is reported as a path:line plus a redacted fingerprint
320
+ * (`sha256` prefix) — the matched text itself is never printed. Like
321
+ * `check-debt`, untracked findings fail the gate; existing ones are
322
+ * grandfathered into an allowlist (`path:line` entries) that can only
323
+ * shrink. `--init` seeds it from the current tree — review that seed
324
+ * carefully, since it silences whatever it captures.
325
+ */
326
+
327
+ type AllowlistFile = {
328
+ _comment?: string;
329
+ allowlist: string[];
330
+ };
331
+ type Finding = {
332
+ path: string;
333
+ line: number;
334
+ pattern: string;
335
+ fingerprint: string;
336
+ };
337
+ type AllowlistEntry = {
338
+ path: string;
339
+ line: number;
340
+ };
341
+ declare function parseAllowlist(allowlistPath: string): {
342
+ entries: AllowlistEntry[];
343
+ raw: string[];
344
+ };
345
+ /** Every git-tracked file, repo-relative, forward-slashed. Empty on any
346
+ * git failure (e.g. not a git repo) rather than throwing — callers decide
347
+ * whether that's fatal. */
348
+ declare function listTrackedFiles(repoRoot: string): string[];
349
+ declare function scan(ctx: Ctx): {
350
+ untracked: Finding[];
351
+ staleAllowlistEntries: string[];
352
+ };
353
+ declare function seedAllowlist(ctx: Ctx): string[];
354
+ declare function writeSeed(ctx: Ctx): {
355
+ path: string;
356
+ count: number;
357
+ };
358
+ /** Run the guard. Returns the process exit code. */
359
+ declare function runSecrets(ctx: Ctx, init?: boolean): number;
360
+
361
+ type secrets_AllowlistFile = AllowlistFile;
362
+ type secrets_Finding = Finding;
363
+ declare const secrets_listTrackedFiles: typeof listTrackedFiles;
364
+ declare const secrets_parseAllowlist: typeof parseAllowlist;
365
+ declare const secrets_runSecrets: typeof runSecrets;
366
+ declare const secrets_scan: typeof scan;
367
+ declare const secrets_seedAllowlist: typeof seedAllowlist;
368
+ declare const secrets_writeSeed: typeof writeSeed;
369
+ declare namespace secrets {
370
+ export { type secrets_AllowlistFile as AllowlistFile, type secrets_Finding as Finding, secrets_listTrackedFiles as listTrackedFiles, secrets_parseAllowlist as parseAllowlist, secrets_runSecrets as runSecrets, secrets_scan as scan, secrets_seedAllowlist as seedAllowlist, secrets_writeSeed as writeSeed };
371
+ }
372
+
373
+ /**
374
+ * Coverage guard (ratchet) for a vitest monorepo — PER-PACKAGE floors.
375
+ *
376
+ * Runs the repo's `test:coverage` script (each package emits its own
377
+ * `coverage/coverage-summary.json` via vitest's json-summary reporter),
378
+ * then holds EACH package to its own floor. A single repo-wide aggregate
379
+ * is deliberately avoided: line-weighting lets big well-covered packages
380
+ * mask regressions in smaller ones, and one floor forces a false uniform
381
+ * standard on packages with very different achievable coverage.
382
+ *
383
+ * Floors live in config.coverage.budgetsPath as
384
+ * { "default": {functions,lines}, "packages": { "<dir>": {functions,lines} } }
385
+ * where `<dir>` is the repo-relative package directory (e.g. "apps/web").
386
+ * A package with no explicit entry must meet `default`. Floors only
387
+ * ratchet UP. `--init` seeds every package's floor just below its current
388
+ * number (keeping the existing `default`, or 80/80 on first seed).
389
+ */
390
+
391
+ type Floor = {
392
+ functions: number;
393
+ lines: number;
394
+ };
395
+ type Budgets$1 = {
396
+ default: Floor;
397
+ packages: Record<string, Floor>;
398
+ };
399
+ type Counts = {
400
+ covered: number;
401
+ total: number;
402
+ };
403
+ type PkgCoverage = {
404
+ pkg: string;
405
+ totals: Floor;
406
+ };
407
+ type PkgFailure = {
408
+ pkg: string;
409
+ metric: "functions" | "lines";
410
+ actual: number;
411
+ floor: number;
412
+ isNew: boolean;
413
+ };
414
+ declare const DEFAULT_MIN: Floor;
415
+ declare function loadBudgets$1(raw: string, source?: string): Budgets$1;
416
+ /** Expand a repo-relative glob whose only wildcard is `*` matching a
417
+ * single path segment (e.g. `packages/*​/coverage/coverage-summary.json`). */
418
+ declare function expandGlob(repoRoot: string, glob: string): string[];
419
+ declare function readSummaryCounts(path: string): {
420
+ functions: Counts;
421
+ lines: Counts;
422
+ } | undefined;
423
+ declare function pct(counts: Counts): number;
424
+ /** The package directory a coverage summary belongs to, e.g.
425
+ * `apps/web/coverage/coverage-summary.json` → `apps/web`. */
426
+ declare function pkgKey(repoRoot: string, summaryPath: string): string;
427
+ /** Per-package coverage percentages, one row per matched summary. */
428
+ declare function collectPerPackage(ctx: Ctx): PkgCoverage[];
429
+ declare function checkPerPackage(perPkg: PkgCoverage[], budgets: Budgets$1): {
430
+ failures: PkgFailure[];
431
+ newPkgs: string[];
432
+ stale: string[];
433
+ };
434
+ /** A floor 0.5pt below the measured value, to absorb run-to-run noise. */
435
+ declare function seedFloor(pct_: number): number;
436
+ declare function seedBudgets(perPkg: PkgCoverage[], keepDefault: Floor): Budgets$1;
437
+ declare function lowest(perPkg: PkgCoverage[]): {
438
+ pkg: string;
439
+ lines: number;
440
+ } | undefined;
441
+ /** The `check:all` headline for coverage: the lowest package + how many meet
442
+ * their floor. `undefined` when there are no packages. */
443
+ declare function coverageScore(perPkg: PkgCoverage[]): string | undefined;
444
+ /** Run the coverage guard. Returns the process exit code. `init` seeds the
445
+ * floors; `skipRun` reads already-produced summaries without re-running. */
446
+ declare function runCoverage(ctx: Ctx, opts?: {
447
+ init?: boolean;
448
+ skipRun?: boolean;
449
+ }): number;
450
+
451
+ type coverage_Counts = Counts;
452
+ declare const coverage_DEFAULT_MIN: typeof DEFAULT_MIN;
453
+ type coverage_Floor = Floor;
454
+ type coverage_PkgCoverage = PkgCoverage;
455
+ type coverage_PkgFailure = PkgFailure;
456
+ declare const coverage_checkPerPackage: typeof checkPerPackage;
457
+ declare const coverage_collectPerPackage: typeof collectPerPackage;
458
+ declare const coverage_coverageScore: typeof coverageScore;
459
+ declare const coverage_expandGlob: typeof expandGlob;
460
+ declare const coverage_lowest: typeof lowest;
461
+ declare const coverage_pct: typeof pct;
462
+ declare const coverage_pkgKey: typeof pkgKey;
463
+ declare const coverage_readSummaryCounts: typeof readSummaryCounts;
464
+ declare const coverage_runCoverage: typeof runCoverage;
465
+ declare const coverage_seedBudgets: typeof seedBudgets;
466
+ declare const coverage_seedFloor: typeof seedFloor;
467
+ declare namespace coverage {
468
+ export { type Budgets$1 as Budgets, type coverage_Counts as Counts, coverage_DEFAULT_MIN as DEFAULT_MIN, type coverage_Floor as Floor, type coverage_PkgCoverage as PkgCoverage, type coverage_PkgFailure as PkgFailure, coverage_checkPerPackage as checkPerPackage, coverage_collectPerPackage as collectPerPackage, coverage_coverageScore as coverageScore, coverage_expandGlob as expandGlob, loadBudgets$1 as loadBudgets, coverage_lowest as lowest, coverage_pct as pct, coverage_pkgKey as pkgKey, coverage_readSummaryCounts as readSummaryCounts, coverage_runCoverage as runCoverage, coverage_seedBudgets as seedBudgets, coverage_seedFloor as seedFloor };
469
+ }
470
+
471
+ /**
472
+ * Bundle-size guard (ratchet).
473
+ *
474
+ * For each configured target it builds the target (turbo, cached), scans
475
+ * its dist dir, and enforces a ratchet per bucket (js/css/…):
476
+ * - totals.raw / totals.gzip — total bytes per bucket
477
+ * - largest.gzip — the single largest chunk per bucket
478
+ * (catches one chunk ballooning even when the total stays flat).
479
+ *
480
+ * The ratchet only goes DOWN: code-split / trim deps, then lower the
481
+ * budget. `--init` re-baselines from a fresh build (measured + headroom).
482
+ *
483
+ * Each target's dist dir is REMOVED before the build. A build tool empties
484
+ * its own output dir, but a cache hit skips the tool entirely and restores
485
+ * cached outputs *into* whatever is already there — restore is additive,
486
+ * because the cache cannot know which foreign files are safe to delete. Two
487
+ * builds' hash-suffixed chunks then coexist and every one of them is counted.
488
+ * Cleaning first makes restore exact and keeps the cache: the measurement
489
+ * becomes a function of the build rather than of the directory's history.
490
+ * Especially valuable for the Cloudflare worker target, where the bundle
491
+ * has a HARD size limit — this fails the PR instead of the prod deploy.
492
+ */
493
+
494
+ type BucketDef = Record<string, string[]>;
495
+ type BucketSize = {
496
+ raw: number;
497
+ gzip: number;
498
+ largest: number;
499
+ };
500
+ type Measurement = {
501
+ buckets: Record<string, BucketSize>;
502
+ /** `dir` is the slash-separated path from distDir to the file's directory
503
+ * (`""` at the top level). `name` stays the bare basename so existing
504
+ * consumers are unaffected. */
505
+ files: {
506
+ name: string;
507
+ dir: string;
508
+ bucket: string;
509
+ raw: number;
510
+ gzip: number;
511
+ }[];
512
+ };
513
+ type TargetBudget = {
514
+ totals: {
515
+ raw: Record<string, number>;
516
+ gzip: Record<string, number>;
517
+ };
518
+ largest: {
519
+ gzip: Record<string, number>;
520
+ };
521
+ };
522
+ type Budgets = Record<string, TargetBudget>;
523
+ type Failure$1 = {
524
+ target: string;
525
+ metric: string;
526
+ bucket: string;
527
+ actual: number;
528
+ budget: number;
529
+ };
530
+ declare function measure(distDir: string, buckets: BucketDef): Measurement;
531
+ /** A dist dir must sit strictly inside the repo. `cleanDist` deletes
532
+ * recursively, so a config typo — or a symlink on the path — that resolved to
533
+ * the repo root or above would take the working tree with it.
534
+ *
535
+ * Returns the symlink-resolved path, which is the one that was actually
536
+ * checked; deleting anything else would defeat the point of checking. */
537
+ declare function assertSafeDistDir(distDir: string, repoRoot: string): string;
538
+ /** Remove a target's dist dir so the build writes into an empty directory.
539
+ * Missing is fine — that is the state we are trying to reach. */
540
+ declare function cleanDist(distDir: string, repoRoot: string): void;
541
+ /** Strip a content hash so two builds of the same chunk share a name:
542
+ * `ProjectArea-DWbILnNi.js` and `ProjectArea-LeWXd_sH.js` both become
543
+ * `ProjectArea.js`, while `react-markdown.js` and `use-debounce.js` are
544
+ * left exactly as they are. */
545
+ declare function logicalChunkName(name: string): string;
546
+ type DuplicateChunk = {
547
+ bucket: string;
548
+ /** Directory-qualified, so `dist/esm/index.js` and `dist/cjs/index.js` — the
549
+ * normal shape of a dual-format library build — are not mistaken for one
550
+ * chunk emitted twice.
551
+ *
552
+ * This is a deliberate trade, and it costs real detection. Pollution whose
553
+ * two copies land in DIFFERENT directories now escapes: a build id in the
554
+ * output path (`.next/static/<buildId>/`), a renamed assets dir, a hashed
555
+ * directory name, or one copy at the top level and one under `assets/`.
556
+ * Basename grouping caught those and this does not.
557
+ *
558
+ * There is no local signal separating the two cases — `esm/x-AAAA.js` +
559
+ * `cjs/x-BBBB.js` and `build1/x-AAAA.js` + `build2/x-BBBB.js` are the same
560
+ * shape. The trade goes this way because the failure modes are not
561
+ * symmetric: a false positive REFUSES a clean dist and tells the operator to
562
+ * remove it and re-run, which reproduces the refusal forever, while a false
563
+ * negative loses one backstop. `cleanDist` is the actual defence against
564
+ * layered output; this only catches a dist someone else laid out. */
565
+ logical: string;
566
+ files: {
567
+ name: string;
568
+ raw: number;
569
+ }[];
570
+ };
571
+ declare function findDuplicateChunks(m: Measurement): DuplicateChunk[];
572
+ declare function loadBudgets(raw: string, source?: string): Budgets;
573
+ declare function diff(target: string, m: Measurement, budget: TargetBudget): Failure$1[];
574
+ declare function seedBudget(m: Measurement): TargetBudget;
575
+ declare function fmtBytes(n: number): string;
576
+ /** Seams for the two effects the guard performs before it can measure.
577
+ * Exposed so tests can assert the ORDER: cleaning after the build would
578
+ * delete the outputs, and cleaning is only useful before one. */
579
+ type BundleSizeDeps = {
580
+ clean: (distDir: string, repoRoot: string) => void;
581
+ build: (ctx: Ctx) => number;
582
+ };
583
+ /** Run the bundle-size guard. Returns the process exit code. */
584
+ declare function runBundleSize(ctx: Ctx, init?: boolean, deps?: BundleSizeDeps): number;
585
+
586
+ type bundleSize_BucketSize = BucketSize;
587
+ type bundleSize_Budgets = Budgets;
588
+ type bundleSize_BundleSizeDeps = BundleSizeDeps;
589
+ type bundleSize_DuplicateChunk = DuplicateChunk;
590
+ type bundleSize_Measurement = Measurement;
591
+ type bundleSize_TargetBudget = TargetBudget;
592
+ declare const bundleSize_assertSafeDistDir: typeof assertSafeDistDir;
593
+ declare const bundleSize_cleanDist: typeof cleanDist;
594
+ declare const bundleSize_diff: typeof diff;
595
+ declare const bundleSize_findDuplicateChunks: typeof findDuplicateChunks;
596
+ declare const bundleSize_fmtBytes: typeof fmtBytes;
597
+ declare const bundleSize_loadBudgets: typeof loadBudgets;
598
+ declare const bundleSize_logicalChunkName: typeof logicalChunkName;
599
+ declare const bundleSize_measure: typeof measure;
600
+ declare const bundleSize_runBundleSize: typeof runBundleSize;
601
+ declare const bundleSize_seedBudget: typeof seedBudget;
602
+ declare namespace bundleSize {
603
+ export { type bundleSize_BucketSize as BucketSize, type bundleSize_Budgets as Budgets, type bundleSize_BundleSizeDeps as BundleSizeDeps, type bundleSize_DuplicateChunk as DuplicateChunk, type Failure$1 as Failure, type bundleSize_Measurement as Measurement, type bundleSize_TargetBudget as TargetBudget, bundleSize_assertSafeDistDir as assertSafeDistDir, bundleSize_cleanDist as cleanDist, bundleSize_diff as diff, bundleSize_findDuplicateChunks as findDuplicateChunks, bundleSize_fmtBytes as fmtBytes, bundleSize_loadBudgets as loadBudgets, bundleSize_logicalChunkName as logicalChunkName, bundleSize_measure as measure, bundleSize_runBundleSize as runBundleSize, bundleSize_seedBudget as seedBudget };
604
+ }
605
+
606
+ /**
607
+ * Agent-doc validator (check:agents).
608
+ *
609
+ * Fails when an agent doc (AGENTS.md / CLAUDE.md) drifts out of sync with the
610
+ * repo: any package-manager script it references in a fenced bash block must
611
+ * exist in package.json's scripts, and any backticked path-shaped token must
612
+ * resolve on disk (or be listed in `agents.knownMissingPaths`).
613
+ */
614
+
615
+ type Failure = {
616
+ file: string;
617
+ kind: string;
618
+ detail: string;
619
+ };
620
+ declare function extractFencedBashBlocks(markdown: string): string[];
621
+ /** Script names referenced via the package manager in bash blocks. Handles
622
+ * both `<pm> run <name>` (explicit) and bare `<pm> <name>` (skipping the
623
+ * configured builtin subcommands like install/exec). */
624
+ declare function extractScriptRefs(blocks: string[], runnerCommand: string, ignoredSubcommands: readonly string[]): Set<string>;
625
+ /** Backtick-quoted tokens that look like a repo path (contain `/` or a known
626
+ * doc/config extension). URLs, scoped packages, globs, placeholders skipped. */
627
+ declare function extractBacktickedPaths(markdown: string): string[];
628
+ declare function validateAgents(ctx: Ctx): Failure[];
629
+ /** Run the agent-doc validator. Returns the process exit code. */
630
+ declare function runValidateAgents(ctx: Ctx): number;
631
+
632
+ type validateAgents$1_Failure = Failure;
633
+ declare const validateAgents$1_extractBacktickedPaths: typeof extractBacktickedPaths;
634
+ declare const validateAgents$1_extractFencedBashBlocks: typeof extractFencedBashBlocks;
635
+ declare const validateAgents$1_extractScriptRefs: typeof extractScriptRefs;
636
+ declare const validateAgents$1_runValidateAgents: typeof runValidateAgents;
637
+ declare const validateAgents$1_validateAgents: typeof validateAgents;
638
+ declare namespace validateAgents$1 {
639
+ export { type validateAgents$1_Failure as Failure, validateAgents$1_extractBacktickedPaths as extractBacktickedPaths, validateAgents$1_extractFencedBashBlocks as extractFencedBashBlocks, validateAgents$1_extractScriptRefs as extractScriptRefs, validateAgents$1_runValidateAgents as runValidateAgents, validateAgents$1_validateAgents as validateAgents };
640
+ }
641
+
642
+ /**
643
+ * Docs-coverage gate: block a PR that changes a user-facing surface but
644
+ * adds no docs for it.
645
+ *
646
+ * WHAT COUNTS AS A SURFACE is `config.docsCoverage` (empty `surfaces` ⇒
647
+ * no-op). A PR passes when either: no surface changed (nothing to gate); a
648
+ * surface changed AND docs changed too; or the author opted out with an
649
+ * escape hatch line `docs: n/a - <reason>` in the PR body. Otherwise it
650
+ * fails closed.
651
+ *
652
+ * This is inherently a *pull-request* gate (a PR body, a base/head diff) —
653
+ * it reads the changed-file list from the GitHub REST API using
654
+ * `GITHUB_REPOSITORY` / `PR_NUMBER` / `GITHUB_TOKEN` / `PR_BODY`. Unlike
655
+ * every other gate here, it deliberately does NOT hard-fail when those are
656
+ * absent: a repo may wire `check:docs-coverage` into the local `check:all`
657
+ * manifest (so it's part of the same battery as everything else), and a
658
+ * local run or a plain `push` CI job has no PR to evaluate — that's a soft
659
+ * no-op, not a failure. It only enforces when actually invoked with PR
660
+ * context, e.g. a `pull_request`-triggered CI job (see the
661
+ * `monorepo-turborepo.yml` example's comment on wiring this up).
662
+ *
663
+ * Zero npm dependencies: Node built-ins + global fetch.
664
+ */
665
+
666
+ type DocsCoverageConfig = RepoGatesConfig["docsCoverage"];
667
+ /** Convert a glob to an anchored RegExp. `*` matches within a path segment;
668
+ * `**` matches across segments (and `**​/` also matches zero directories). */
669
+ declare function globToRegExp(glob: string): RegExp;
670
+ declare function matchesAny(path: string, globs: readonly string[] | undefined): boolean;
671
+ type ChangedFile = {
672
+ filename: string;
673
+ status: string;
674
+ };
675
+ /** Does a file (with its GitHub status) trigger the given surface rule? */
676
+ declare function fileTriggersSurface(file: ChangedFile, surface: DocsCoverageConfig["surfaces"][number]): boolean;
677
+ type TriggeredSurface = {
678
+ file: string;
679
+ label: string;
680
+ };
681
+ /** Surfaces a PR touched that need docs. */
682
+ declare function triggeredSurfaces(files: readonly ChangedFile[], config: DocsCoverageConfig): TriggeredSurface[];
683
+ /** Did the PR add or update any docs (present, not excluded, under docsGlobs)? */
684
+ declare function hasDocsChange(files: readonly ChangedFile[], config: DocsCoverageConfig): boolean;
685
+ /** Remove fenced code so an escape-hatch example in a code block is not
686
+ * read as a real opt-out. */
687
+ declare function stripFencedCode(markdown: string | undefined): string;
688
+ type EscapeHatch = {
689
+ present: false;
690
+ } | {
691
+ present: true;
692
+ reason: string;
693
+ };
694
+ /** The `docs: n/a - <reason>` opt-out, if present. Tolerates `n/a` or `na`,
695
+ * an optional leading list/quote marker, and any dash/colon before the
696
+ * reason. */
697
+ declare function escapeHatch(body: string | undefined): EscapeHatch;
698
+ type Verdict = {
699
+ status: "skip";
700
+ } | {
701
+ status: "pass";
702
+ triggered: TriggeredSurface[];
703
+ } | {
704
+ status: "waived";
705
+ triggered: TriggeredSurface[];
706
+ reason: string;
707
+ } | {
708
+ status: "fail";
709
+ triggered: TriggeredSurface[];
710
+ };
711
+ /** The gate verdict.
712
+ * skip — no surface changed; nothing to gate.
713
+ * pass — a surface changed and docs changed too.
714
+ * waived — a surface changed, no docs, but an escape hatch opted out.
715
+ * fail — a surface changed, no docs, no escape hatch. */
716
+ declare function evaluate(files: readonly ChangedFile[], body: string | undefined, config: DocsCoverageConfig): Verdict;
717
+ /** Percent-encode the characters a GitHub Actions workflow command treats
718
+ * specially (`%`, `\r`, `\n`) so a multi-line message isn't mangled or
719
+ * truncated. See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands */
720
+ declare function escapeWorkflowCommand(message: string): string;
721
+ declare function fetchChangedFiles(opts: {
722
+ repo: string;
723
+ number: string;
724
+ token: string;
725
+ }): Promise<ChangedFile[]>;
726
+ /** Run the gate. Returns the process exit code. */
727
+ declare function runDocsCoverage(ctx: Ctx): Promise<number>;
728
+ declare function readDocsCoverageConfig(path: string): DocsCoverageConfig;
729
+
730
+ type docsCoverage_ChangedFile = ChangedFile;
731
+ type docsCoverage_DocsCoverageConfig = DocsCoverageConfig;
732
+ type docsCoverage_EscapeHatch = EscapeHatch;
733
+ type docsCoverage_TriggeredSurface = TriggeredSurface;
734
+ type docsCoverage_Verdict = Verdict;
735
+ declare const docsCoverage_escapeHatch: typeof escapeHatch;
736
+ declare const docsCoverage_escapeWorkflowCommand: typeof escapeWorkflowCommand;
737
+ declare const docsCoverage_evaluate: typeof evaluate;
738
+ declare const docsCoverage_fetchChangedFiles: typeof fetchChangedFiles;
739
+ declare const docsCoverage_fileTriggersSurface: typeof fileTriggersSurface;
740
+ declare const docsCoverage_globToRegExp: typeof globToRegExp;
741
+ declare const docsCoverage_hasDocsChange: typeof hasDocsChange;
742
+ declare const docsCoverage_matchesAny: typeof matchesAny;
743
+ declare const docsCoverage_readDocsCoverageConfig: typeof readDocsCoverageConfig;
744
+ declare const docsCoverage_runDocsCoverage: typeof runDocsCoverage;
745
+ declare const docsCoverage_stripFencedCode: typeof stripFencedCode;
746
+ declare const docsCoverage_triggeredSurfaces: typeof triggeredSurfaces;
747
+ declare namespace docsCoverage {
748
+ export { type docsCoverage_ChangedFile as ChangedFile, type docsCoverage_DocsCoverageConfig as DocsCoverageConfig, type docsCoverage_EscapeHatch as EscapeHatch, type docsCoverage_TriggeredSurface as TriggeredSurface, type docsCoverage_Verdict as Verdict, docsCoverage_escapeHatch as escapeHatch, docsCoverage_escapeWorkflowCommand as escapeWorkflowCommand, docsCoverage_evaluate as evaluate, docsCoverage_fetchChangedFiles as fetchChangedFiles, docsCoverage_fileTriggersSurface as fileTriggersSurface, docsCoverage_globToRegExp as globToRegExp, docsCoverage_hasDocsChange as hasDocsChange, docsCoverage_matchesAny as matchesAny, docsCoverage_readDocsCoverageConfig as readDocsCoverageConfig, docsCoverage_runDocsCoverage as runDocsCoverage, docsCoverage_stripFencedCode as stripFencedCode, docsCoverage_triggeredSurfaces as triggeredSurfaces };
749
+ }
750
+
751
+ /**
752
+ * Non-gating CI dashboards (report:test-timing, report:quality-metrics).
753
+ *
754
+ * These enforce nothing — each ratchet gate already fails the build on its
755
+ * own. They render the current state into `$GITHUB_STEP_SUMMARY` (and stdout)
756
+ * so reviewers see it at a glance. Missing inputs degrade to a "—" row rather
757
+ * than failing.
758
+ */
759
+
760
+ type TestCase = {
761
+ name: string;
762
+ classname: string;
763
+ file: string;
764
+ timeSeconds: number;
765
+ };
766
+ type TimingReport = {
767
+ totalSeconds: number;
768
+ totalTests: number;
769
+ cases: TestCase[];
770
+ };
771
+ declare function parseJUnit(xml: string): TestCase[];
772
+ declare function collectTiming(ctx: Ctx): {
773
+ report: TimingReport;
774
+ files: number;
775
+ };
776
+ declare function formatTiming(report: TimingReport, topN: number): string;
777
+ /** A consolidated table of the ratchet gates' current headline metrics. */
778
+ declare function formatQualityMetrics(ctx: Ctx): string;
779
+ declare function emitSummary(markdown: string): void;
780
+ declare function runTestTiming(ctx: Ctx): number;
781
+ declare function runQualityMetrics(ctx: Ctx): number;
782
+
783
+ type report_TestCase = TestCase;
784
+ type report_TimingReport = TimingReport;
785
+ declare const report_collectTiming: typeof collectTiming;
786
+ declare const report_emitSummary: typeof emitSummary;
787
+ declare const report_formatQualityMetrics: typeof formatQualityMetrics;
788
+ declare const report_formatTiming: typeof formatTiming;
789
+ declare const report_parseJUnit: typeof parseJUnit;
790
+ declare const report_runQualityMetrics: typeof runQualityMetrics;
791
+ declare const report_runTestTiming: typeof runTestTiming;
792
+ declare namespace report {
793
+ export { type report_TestCase as TestCase, type report_TimingReport as TimingReport, report_collectTiming as collectTiming, report_emitSummary as emitSummary, report_formatQualityMetrics as formatQualityMetrics, report_formatTiming as formatTiming, report_parseJUnit as parseJUnit, report_runQualityMetrics as runQualityMetrics, report_runTestTiming as runTestTiming };
794
+ }
795
+
796
+ export { type CheckAllOptions, type CiInvocation, Ctx, type GateResult, GateSpec, type ParityConfig, type ParityFailure, RepoGatesConfig, bundleSize, checkParity, circularImports, computeReachable, coverage, debtMarkers, docsCoverage, evaluateParity, extractCiInvocations, extractFailureSignatures, extractRunTargets, extractScores, fileSizes, formatGateLine, formatScoresBlock, gatesForRepo, listCiWorkflows, loadParityConfig, makeRunTargetRe, report, resolveGates, runCheckAll, runCiParity, secrets, validateAgents$1 as validateAgents };