@antelopejs/tooling-configs 0.2.1 → 0.2.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.
package/dist/bin/sync.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { a as formatCeilings, t as CEILINGS_FILE } from "../ceilings--pJmhSde.mjs";
3
- import { a as repositoryRoot, o as runCli } from "../files-Bocv-qA8.mjs";
4
- import { a as repositoryTools, i as parseManifest, n as managedPackage, o as toolingVersions, r as packageManager, t as execCommand } from "../package-B1aipTbn.mjs";
5
- import { t as measureBaseline } from "../ratchet-B2-4XxAl.mjs";
2
+ import { a as formatCeilings, o as isRetired, s as readCeilings, t as CEILINGS_FILE } from "../ceilings-BSw9p4YB.mjs";
3
+ import { i as repositoryRoot, u as runCli } from "../files-jKlueTMd.mjs";
4
+ import { a as repositoryTools, i as parseManifest, n as managedPackage, o as toolingVersions, r as packageManager, t as execCommand } from "../package-Ckd7e4tA.mjs";
5
+ import { t as measureBaseline } from "../ratchet-CNbpU42F.mjs";
6
6
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { dirname, join, resolve } from "node:path";
8
+ import { execFileSync } from "node:child_process";
8
9
  import { parseArgs } from "node:util";
9
10
  import { isMap, isScalar, isSeq, parseDocument } from "yaml";
10
11
  //#region src/guardrails/agents-block.ts
@@ -12,7 +13,11 @@ const AGENTS_BLOCK_START = "<!-- antelopejs-guardrails:start -->";
12
13
  const AGENTS_BLOCK_END = "<!-- antelopejs-guardrails:end -->";
13
14
  const COMMENTS = `### Comments
14
15
 
15
- Say it in code. A constraint belongs in a type, a lint rule, or a test. A public item's contract belongs in TSDoc (\`/** */\`) on that item. The only other comments are lint directives that name their rules, \`@ts-expect-error <reason>\`, \`SAFETY: <reason>\` above a type assertion, \`/*!\` license headers, and bundler or formatter directives.`;
16
+ Say it in code. A constraint belongs in a type, a lint rule, or a test. A public item's contract belongs in TSDoc (\`/** */\`) on that item. The only other comments are lint directives that name their rules, \`@ts-expect-error <reason>\`, \`SAFETY: <reason>\` above a type assertion, \`/*!\` license headers, and bundler or formatter directives.
17
+
18
+ Before suppressing a type rule, try inference or a type derived from its owner with \`Parameters\`, \`ReturnType\`, or indexed access. A private alias can still appear in a published declaration. Keep a suppression only after checking the published contract and the unchecked boundary it protects.
19
+
20
+ Check each directive reason against the current line and its callers. Returning a value is not the same as awaiting and discarding it. A reason must describe what the code does now.`;
16
21
  const TESTS = `### Tests
17
22
 
18
23
  Hold every test you write or keep to this checklist:
@@ -20,6 +25,15 @@ Hold every test you write or keep to this checklist:
20
25
  - Name the wrong implementation this test fails on. A test with no answer is deleted, not kept.
21
26
  - No test reads the file it tests. Drive the behaviour through the public interface.
22
27
  - An invariant over many files is a lint rule, not a test.`;
28
+ const CLEANUP_PROOF = `### Cleanup proof
29
+
30
+ A behaviour-preserving cleanup PR shows \`antelopejs-api-diff <base-ref>\` passing. Published declarations and built runtime exports stay unchanged. A comment-only PR shows \`antelopejs-comment-proof <base-ref>\` passing instead of a verifier. Both compare committed HEAD.
31
+
32
+ Copy counts from the proof's base and HEAD table. Never type counts by hand. Keep the command and the committed head beside the copied output. Directive lines are not individual suppressed rules.
33
+
34
+ Pass \`--probe <file.cjs>\` to api-diff when changing receivers, positional calls, or modules that no published entry reaches. The same captured script runs against both worktree roots, supplied as \`argv[2]\`, and prints JSON observations. Matching metadata alone does not prove those behaviours.
35
+
36
+ An entry-only rule exempts only its own module. Add a second module entry rather than exempting another importer. A no-upward rule must reject peers in the same tier as well as higher tiers. Probe the rank decision with an import, not just a reading of the configuration.`;
23
37
  const SHARED_RULES = [
24
38
  ["Formatting", "oxfmt"],
25
39
  ["Lint findings are errors; no rule warns", "oxlint preset"],
@@ -28,14 +42,20 @@ const SHARED_RULES = [
28
42
  ["Tests read no files outside fixtures, helpers, locales", "`antelopejs/no-fs-read-in-tests`"],
29
43
  ["No assertion that restates a constant", "`antelopejs/no-constant-value-assertion`"],
30
44
  ["No test whose only check is \"is a function\"", "`antelopejs/no-typeof-function-only-test`"],
31
- ["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"]
45
+ ["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"],
46
+ ["Cleanup preserves declarations and built runtime export metadata", "`antelopejs-api-diff <base-ref>`"],
47
+ ["Comment-only changes preserve JavaScript emit", "`antelopejs-comment-proof <base-ref>`"],
48
+ ["PR-body counts come from tracked code files", "api-diff and comment-proof tables"],
49
+ ["Receiver, positional and unpublished-module behaviour", "`antelopejs-api-diff <base-ref> --probe <file.cjs>`"],
50
+ ["Type suppressions and directive reasons are justified", "review; no tool"],
51
+ ["No-upward rules reject same-tier peers", "review and import probes"]
32
52
  ];
33
53
  const FULL_RULES = [
34
54
  ["Files ≤ 20,000 bytes, tests included", "`antelopejs-check-size` (pre-commit)"],
35
- ["Existing debt only shrinks", "`antelopejs-ratchet`"],
36
55
  ["Types check", "`tsc` (pre-push)"],
37
56
  ["No dead files, exports, or dependencies", "knip"],
38
57
  ["Managed files match the pack", "`antelopejs-sync --check`"],
58
+ ["Entry-only rules exempt no tracked outsider", "`antelopejs-sync --check`"],
39
59
  ["A test names the wrong implementation it catches", "review; no tool"]
40
60
  ];
41
61
  /** Pads each column the way oxfmt does, so formatting AGENTS.md never reports sync drift. */
@@ -62,7 +82,7 @@ function fullBody(context) {
62
82
  return [
63
83
  "## Guardrails",
64
84
  "",
65
- "Finish every change with `pnpm run checks`. It runs typecheck, lint, format, knip, tests, size, ratchet, and sync, and prints only what failed. The change is done when it prints no failure.",
85
+ `Finish every change with \`pnpm run checks\`. It runs typecheck, lint, format, knip, tests, size, ${context.ratchet ? "ratchet, " : ""}and sync, and prints only what failed. The change is done when it prints no failure.`,
66
86
  "",
67
87
  ...pointers(context).flatMap((line) => [line, ""]),
68
88
  "The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks.",
@@ -71,11 +91,19 @@ function fullBody(context) {
71
91
  "",
72
92
  TESTS,
73
93
  "",
74
- "### Debt",
94
+ CLEANUP_PROOF,
75
95
  "",
76
- "`guardrails.ratchet.json` records the lint findings and oversize files this repository already had. Its numbers only go down. After you remove debt, run `pnpm run ratchet:update` and commit the lowered file. A rule listed there is off for `oxlint`. The ratchet counts it instead, and the pre-commit hook rejects it on every line you add.",
77
- "",
78
- ruleTable([...SHARED_RULES, ...FULL_RULES])
96
+ ...context.ratchet ? [
97
+ "### Debt",
98
+ "",
99
+ "`guardrails.ratchet.json` records the lint findings and oversize files this repository already had. Its numbers only go down. After you remove debt, run `pnpm run ratchet:update` and commit the lowered file. A rule listed there is off for `oxlint`. The ratchet counts it instead, and the pre-commit hook rejects it on every line you add.",
100
+ ""
101
+ ] : [],
102
+ ruleTable([
103
+ ...SHARED_RULES,
104
+ ...FULL_RULES,
105
+ ...context.ratchet ? [["Existing debt only shrinks", "`antelopejs-ratchet`"]] : []
106
+ ])
79
107
  ];
80
108
  }
81
109
  function templateBody(context) {
@@ -89,6 +117,8 @@ function templateBody(context) {
89
117
  "",
90
118
  TESTS,
91
119
  "",
120
+ CLEANUP_PROOF,
121
+ "",
92
122
  ruleTable(SHARED_RULES)
93
123
  ];
94
124
  }
@@ -140,7 +170,7 @@ function presetConfig(current, preset, tools) {
140
170
  const importLine = preset.content.split("\n").filter((line) => line.includes(preset.specifier)).join("\n").replace(preset.specifier, target);
141
171
  throw new Error(`${preset.path} does not import its preset. Add \`${importLine}\` and extend that preset, then rerun antelopejs-sync. The file was not replaced.`);
142
172
  }
143
- function lefthook(current, manager, tools) {
173
+ function lefthook(current, manager, tools, ratchet = true) {
144
174
  const exec = execCommand(manager);
145
175
  const document = parseDocument(current ?? "{}\n");
146
176
  if (document.errors.length) throw new Error(`lefthook.yml: ${document.errors.map(({ message }) => message).join("; ")}`);
@@ -176,6 +206,11 @@ function lefthook(current, manager, tools) {
176
206
  key
177
207
  ], document.createNode(command));
178
208
  }
209
+ if (!ratchet) document.deleteIn([
210
+ "pre-commit",
211
+ "commands",
212
+ "antelopejs-ratchet"
213
+ ]);
179
214
  return document.toString({ lineWidth: 0 });
180
215
  }
181
216
  const SETUP_SCRIPT = `#!/usr/bin/env bash
@@ -239,8 +274,79 @@ function hasChecksWorkflow(root) {
239
274
  return false;
240
275
  }
241
276
  //#endregion
277
+ //#region src/guardrails/entry-only.ts
278
+ const LOAD_RULES = `
279
+ const advisories = [];
280
+ const rules = [];
281
+ const SUPPORTED = { from: ["pathNot"], to: ["path", "pathNot"] };
282
+ function patterns(value) {
283
+ if (value === undefined) return [];
284
+ const list = Array.isArray(value) ? value : [value];
285
+ if (list.some(pattern => typeof pattern !== "string")) return null;
286
+ try { list.forEach(pattern => new RegExp(pattern)); } catch { return null; }
287
+ return list;
288
+ }
289
+ function problem(rule) {
290
+ if (!rule.from || typeof rule.from !== "object" || !rule.to || typeof rule.to !== "object")
291
+ return "Warning: entry-only rule " + rule.name + " has no from and to selectors, so its exemptions cannot be checked.";
292
+ const unsupported = [
293
+ ...(rule.severity === "error" ? [] : ["severity " + String(rule.severity)]),
294
+ ...Object.keys(rule.from).filter(key => !SUPPORTED.from.includes(key)).map(key => "from." + key),
295
+ ...Object.keys(rule.to).filter(key => !SUPPORTED.to.includes(key)).map(key => "to." + key),
296
+ ];
297
+ if (unsupported.length)
298
+ return "Note: entry-only rule " + rule.name + " skipped, because its selectors are not supported (" + unsupported.join(", ") + "); only error rules built from from.pathNot and to.path or to.pathNot patterns are checked.";
299
+ if (rule.to.path === undefined)
300
+ return "Warning: entry-only rule " + rule.name + " has no to.path, so there is no module to exempt against.";
301
+ if (patterns(rule.from.pathNot) === null || !patterns(rule.to.path)?.length || patterns(rule.to.pathNot) === null)
302
+ return "Warning: entry-only rule " + rule.name + " has selectors that are not valid regular expression strings, so its exemptions cannot be checked.";
303
+ return undefined;
304
+ }
305
+ try {
306
+ const forbidden = require(process.cwd()+"/.dependency-cruiser.cjs").forbidden;
307
+ if (!Array.isArray(forbidden)) advisories.push("Warning: .dependency-cruiser.cjs has no forbidden rule array, so entry-only exemptions are not checked.");
308
+ else for (const rule of forbidden) {
309
+ if (!rule || typeof rule.name !== "string" || !rule.name.endsWith("-entry-only")) continue;
310
+ const advisory = problem(rule);
311
+ if (advisory === undefined) rules.push({ name: rule.name, exempt: patterns(rule.from.pathNot), domain: patterns(rule.to.path) });
312
+ else advisories.push(advisory);
313
+ }
314
+ } catch (error) {
315
+ advisories.push("Warning: .dependency-cruiser.cjs could not be loaded, so entry-only exemptions are not checked (" + String(error && error.message) + ").");
316
+ }
317
+ process.stdout.write(JSON.stringify({ rules, advisories }));
318
+ `;
319
+ function matcher(patterns) {
320
+ return patterns.map((pattern) => new RegExp(pattern));
321
+ }
322
+ /** Rejects tracked importers exempted from another module's entry-only boundary. A rule it cannot interpret yields a printed note or warning in `advisories` and never a failure. */
323
+ function checkEntryOnlyConfiguration(root) {
324
+ if (!existsSync(join(root, ".dependency-cruiser.cjs"))) return {
325
+ errors: [],
326
+ advisories: []
327
+ };
328
+ const { rules, advisories } = JSON.parse(execFileSync(process.execPath, ["-e", LOAD_RULES], {
329
+ cwd: root,
330
+ encoding: "utf8",
331
+ timeout: 1e4
332
+ }));
333
+ const paths = execFileSync("git", ["ls-files", "-z"], {
334
+ cwd: root,
335
+ encoding: "utf8"
336
+ }).split("\0").filter(Boolean);
337
+ return {
338
+ errors: rules.flatMap((rule) => {
339
+ const domain = matcher(rule.domain);
340
+ const exemption = matcher(rule.exempt);
341
+ return paths.filter((path) => exemption.some((pattern) => pattern.test(path)) && !domain.some((pattern) => pattern.test(path))).map((path) => `${rule.name} exempts ${path} outside its module; give the module a second entry instead`);
342
+ }),
343
+ advisories
344
+ };
345
+ }
346
+ //#endregion
242
347
  //#region src/guardrails/sync.ts
243
348
  const LEGACY_COMMENT_INSTRUCTION = "- Explain non-obvious constraints and decisions in comments; avoid restating the code. Document public contracts with TSDoc.";
349
+ const RETIRED_RATCHET = `${CEILINGS_FILE} has no debt left. Run ratchet:update to delete it, then run antelopejs-sync.`;
244
350
  function managedAgents(current = "", context) {
245
351
  const start = current.indexOf(AGENTS_BLOCK_START);
246
352
  const end = current.indexOf(AGENTS_BLOCK_END);
@@ -273,7 +379,7 @@ const MANAGED_ARTIFACTS = [
273
379
  path: "lefthook.yml",
274
380
  profiles: ["full"],
275
381
  desired: (current, context) => ({
276
- content: lefthook(current, context.manager, context.tools),
382
+ content: lefthook(current, context.manager, context.tools, context.ratchet),
277
383
  notes: []
278
384
  })
279
385
  },
@@ -333,6 +439,7 @@ function planSync(current, modes, context) {
333
439
  });
334
440
  for (const artifact of MANAGED_ARTIFACTS) {
335
441
  if (!artifact.profiles.includes(context.profile)) continue;
442
+ if (artifact.path === "guardrails.ratchet.json" && !context.ratchet) continue;
336
443
  try {
337
444
  const { content, notes } = artifact.desired(current.get(artifact.path), context);
338
445
  const changed = current.get(artifact.path) !== content || artifact.mode !== void 0 && (artifact.mode & 73) !== 0 && ((modes.get(artifact.path) ?? 0) & 73) === 0;
@@ -407,6 +514,7 @@ function repositoryContext(root, current, profile) {
407
514
  const desiredLint = current.get(lint.path) ?? lint.content;
408
515
  return {
409
516
  profile,
517
+ ratchet: current.has(CEILINGS_FILE),
410
518
  manager: packageManager(manifest),
411
519
  versions: toolingVersions(),
412
520
  tools: repositoryTools(manifest),
@@ -427,6 +535,13 @@ function writeArtifacts(root, artifacts) {
427
535
  if (artifact.mode !== void 0) chmodSync(path, artifact.mode);
428
536
  }
429
537
  }
538
+ function repositoryNotices(root, context) {
539
+ const notices = [];
540
+ if (context.profile === "full" && !hasChecksWorkflow(root)) notices.push(MISSING_CHECKS_WORKFLOW);
541
+ if (context.ratchet && isRetired(readCeilings(root))) notices.push(RETIRED_RATCHET);
542
+ return notices;
543
+ }
544
+ /** Measures debt only on first adoption, before sync has written the sync:check script; retired repositories stay retired. */
430
545
  function syncRepository(root, options = {}) {
431
546
  const profile = options.profile ?? "full";
432
547
  const { current, modes } = repositorySnapshot(root);
@@ -434,15 +549,16 @@ function syncRepository(root, options = {}) {
434
549
  const context = repositoryContext(root, current, profile);
435
550
  let plan = planSync(current, modes, context);
436
551
  const errors = plan.filter((artifact) => artifact.kind === "error").map(({ message }) => message);
552
+ const entryOnly = checkEntryOnlyConfiguration(root);
553
+ errors.push(...entryOnly.errors);
437
554
  const notes = plan.flatMap((artifact) => artifact.kind === "migrated" ? artifact.notes : []);
438
- if (profile === "full" && !hasChecksWorkflow(root)) {
439
- if (options.check) errors.push(MISSING_CHECKS_WORKFLOW);
440
- else notes.push(MISSING_CHECKS_WORKFLOW);
441
- }
442
- if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json")) {
555
+ (options.check ? errors : notes).push(...repositoryNotices(root, context));
556
+ if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json") && manifest.scripts?.["sync:check"] === void 0) {
443
557
  const lint = PRESET_CONFIGS[0];
444
558
  const desired = presetConfig(current.get(lint.path), lint, context.tools);
445
- context.baseline = formatCeilings(measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired));
559
+ const baseline = measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired);
560
+ context.ratchet = !isRetired(baseline);
561
+ context.baseline = formatCeilings(baseline);
446
562
  plan = planSync(current, modes, context);
447
563
  }
448
564
  const writes = plan.filter((artifact) => artifact.kind === "write" || artifact.kind === "migrated");
@@ -455,6 +571,7 @@ function syncRepository(root, options = {}) {
455
571
  paths: errors.length ? [] : writes.map(({ path }) => path),
456
572
  errors,
457
573
  notes,
574
+ advisories: entryOnly.advisories,
458
575
  dependenciesAdded
459
576
  };
460
577
  }
@@ -474,6 +591,7 @@ runCli(() => {
474
591
  profile: values.profile
475
592
  });
476
593
  for (const note of result.notes) process.stdout.write(`Note: ${values.check || result.errors.length ? "Planned migration. " : ""}${note}\n`);
594
+ for (const advisory of result.advisories) process.stdout.write(`${advisory}\n`);
477
595
  for (const error of result.errors) process.stderr.write(`${error}\n`);
478
596
  if (result.errors.length) return 1;
479
597
  for (const path of result.paths) process.stdout.write(`${values.check ? "Drift" : "Updated"}: ${path}${values.check ? "; run antelopejs-sync" : ""}\n`);
@@ -14,6 +14,10 @@ const EMPTY_CEILINGS = {
14
14
  rules: {},
15
15
  oversizeFiles: {}
16
16
  };
17
+ /** A repository retires its ratchet once no lint or file debt remains. */
18
+ function isRetired(ceilings) {
19
+ return Object.values(ceilings.rules).every((count) => count === 0) && Object.keys(ceilings.oversizeFiles).length === 0;
20
+ }
17
21
  /** Finds the nearest git directory or worktree marker, falling back to cwd outside git. */
18
22
  function findGitRoot(cwd = process.cwd()) {
19
23
  let directory = resolve(cwd);
@@ -61,4 +65,4 @@ function formatCeilings(ceilings) {
61
65
  return `${JSON.stringify(sorted, null, 2)}\n`;
62
66
  }
63
67
  //#endregion
64
- export { formatCeilings as a, findGitRoot as i, MAX_FILE_BYTES as n, readCeilings as o, MEASURE_ENV as r, CEILINGS_FILE as t };
68
+ export { formatCeilings as a, findGitRoot as i, MAX_FILE_BYTES as n, isRetired as o, MEASURE_ENV as r, readCeilings as s, CEILINGS_FILE as t };
@@ -0,0 +1,298 @@
1
+ import "./ceilings-BSw9p4YB.mjs";
2
+ import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "./shared-CsXIMK3I.mjs";
3
+ import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readSync, rmSync, symlinkSync } from "node:fs";
4
+ import { dirname, join, matchesGlob, relative, resolve } from "node:path";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { tmpdir } from "node:os";
7
+ //#region src/guardrails/cli.ts
8
+ function runCli(main) {
9
+ try {
10
+ process.exitCode = main();
11
+ } catch (error) {
12
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
13
+ process.exitCode = 1;
14
+ }
15
+ }
16
+ //#endregion
17
+ //#region src/guardrails/revisions.ts
18
+ function trackedManifests(root) {
19
+ return execFileSync("git", ["ls-files", "-z"], {
20
+ cwd: root,
21
+ encoding: "utf8"
22
+ }).split("\0").filter((file) => file === "package.json" || file.endsWith("/package.json"));
23
+ }
24
+ const DIRECTORY_LINK = process.platform === "win32" ? "junction" : "dir";
25
+ function linkDependencies(root, worktree) {
26
+ const directories = /* @__PURE__ */ new Set([".", ...trackedManifests(root).map(dirname)]);
27
+ for (const directory of directories) {
28
+ const source = join(root, directory, "node_modules");
29
+ const target = join(worktree, directory, "node_modules");
30
+ if (existsSync(source) && existsSync(dirname(target)) && !existsSync(target)) symlinkSync(source, target, DIRECTORY_LINK);
31
+ }
32
+ }
33
+ /** Compares isolated committed revisions and removes both worktrees even when compilation fails. */
34
+ function compareRevisions(root, baseRef, label, emit) {
35
+ const temporary = mkdtempSync(join(tmpdir(), "antelope-proof-"));
36
+ const worktrees = [];
37
+ const results = [];
38
+ try {
39
+ for (const [side, ref] of [["base", baseRef], ["head", "HEAD"]]) {
40
+ const revision = execFileSync("git", [
41
+ "rev-parse",
42
+ "--verify",
43
+ "--end-of-options",
44
+ `${ref}^{commit}`
45
+ ], {
46
+ cwd: root,
47
+ encoding: "utf8",
48
+ stdio: [
49
+ "ignore",
50
+ "pipe",
51
+ "pipe"
52
+ ]
53
+ }).trim();
54
+ const worktree = join(temporary, "worktrees", side);
55
+ execFileSync("git", [
56
+ "worktree",
57
+ "add",
58
+ "--quiet",
59
+ "--detach",
60
+ worktree,
61
+ revision
62
+ ], {
63
+ cwd: root,
64
+ stdio: [
65
+ "ignore",
66
+ "pipe",
67
+ "pipe"
68
+ ]
69
+ });
70
+ worktrees.push(worktree);
71
+ linkDependencies(root, worktree);
72
+ const output = join(temporary, side);
73
+ mkdirSync(output, { recursive: true });
74
+ results.push(emit(worktree, output, join(temporary, "emit", side)));
75
+ }
76
+ const diff = spawnSync("git", [
77
+ "diff",
78
+ "--no-index",
79
+ "--no-color",
80
+ "--no-renames",
81
+ "--no-ext-diff",
82
+ "--no-textconv",
83
+ "--",
84
+ "base",
85
+ "head"
86
+ ], {
87
+ cwd: temporary,
88
+ encoding: "utf8",
89
+ maxBuffer: 67108864
90
+ });
91
+ if (diff.error) throw diff.error;
92
+ if (diff.status !== 0 && diff.status !== 1) throw new Error(diff.stderr || "Could not compare revision outputs.");
93
+ const packages = new Set(results.flatMap((result) => result.packages)).size;
94
+ return {
95
+ different: diff.status === 1,
96
+ output: diff.status === 1 ? `${label}: differences against ${baseRef} (committed HEAD).\n${diff.stdout}` : `${label}: ${packages} packages, ${results[1].entries} entries unchanged against ${baseRef} (committed HEAD).\n`
97
+ };
98
+ } finally {
99
+ for (const worktree of worktrees.reverse()) execFileSync("git", [
100
+ "worktree",
101
+ "remove",
102
+ "--force",
103
+ worktree
104
+ ], {
105
+ cwd: root,
106
+ stdio: "pipe"
107
+ });
108
+ rmSync(temporary, {
109
+ recursive: true,
110
+ force: true
111
+ });
112
+ }
113
+ }
114
+ const COUNTS = [
115
+ ["oxlint-disable / eslint-disable directive lines", /^\s*(?:\/\/|\/\*|\*)\s*(?:oxlint|eslint)-disable\b/u],
116
+ ["SAFETY: lines", /^\s*(?:\/\/|\/\*|\*)\s*SAFETY:/u],
117
+ ["@ts-expect-error lines", /^\s*(?:\/\/|\/\*|\*)\s*@ts-expect-error\b/u]
118
+ ];
119
+ const CODE_PATHSPECS = [
120
+ "*.ts",
121
+ "*.tsx",
122
+ "*.mts",
123
+ "*.cts",
124
+ "*.js",
125
+ "*.jsx",
126
+ "*.mjs",
127
+ "*.cjs",
128
+ "*.vue"
129
+ ];
130
+ function countsAt(root, ref) {
131
+ const result = spawnSync("git", [
132
+ "grep",
133
+ "-h",
134
+ "-I",
135
+ "-E",
136
+ "-e",
137
+ "(oxlint|eslint)-disable|SAFETY:|@ts-expect-error",
138
+ ref,
139
+ "--",
140
+ ...CODE_PATHSPECS
141
+ ], {
142
+ cwd: root,
143
+ encoding: "utf8",
144
+ maxBuffer: 67108864
145
+ });
146
+ if (result.error) throw result.error;
147
+ if (result.status !== 0 && result.status !== 1) throw new Error(result.stderr);
148
+ const lines = result.stdout.split("\n");
149
+ return COUNTS.map(([, pattern]) => lines.filter((line) => pattern.test(line)).length);
150
+ }
151
+ /** Counts directive, `SAFETY:` and `@ts-expect-error` lines in every tracked code file at the base and at committed HEAD, so a PR body copies them instead of typing them. */
152
+ function revisionCounts(root, baseRef) {
153
+ const base = countsAt(root, baseRef);
154
+ const head = countsAt(root, "HEAD");
155
+ return `\nCounts in tracked code files (${baseRef} and committed HEAD).\n| Lines | base | HEAD |\n| --- | ---: | ---: |\n${COUNTS.map(([label], index) => `| ${label} | ${base[index]} | ${head[index]} |`).join("\n")}\n`;
156
+ }
157
+ //#endregion
158
+ //#region src/guardrails/files.ts
159
+ const SIZE_EXCLUSIONS = [
160
+ {
161
+ kind: "generated",
162
+ patterns: [
163
+ ...IGNORE_PATTERNS,
164
+ ...AGENT_IGNORE_PATTERNS,
165
+ "**/*.generated.*",
166
+ "**/generated/**",
167
+ "**/__snapshots__/**"
168
+ ]
169
+ },
170
+ {
171
+ kind: "fixtures",
172
+ patterns: ["**/fixtures/**", "**/__fixtures__/**"]
173
+ },
174
+ {
175
+ kind: "data",
176
+ patterns: [
177
+ "**/locales/**/*.json",
178
+ "**/i18n/**/*.json",
179
+ "**/blocks.json",
180
+ "**/schema.json",
181
+ "**/evals/**/cases.json"
182
+ ]
183
+ },
184
+ {
185
+ kind: "lockfiles",
186
+ patterns: [
187
+ "**/{pnpm-lock.yaml,package-lock.json,npm-shrinkwrap.json,yarn.lock,bun.lock,bun.lockb,Cargo.lock,poetry.lock,uv.lock,Gemfile.lock,composer.lock,go.sum}",
188
+ "**/*.lock",
189
+ "**/*.lockb"
190
+ ]
191
+ },
192
+ {
193
+ kind: "documentation",
194
+ patterns: ["**/CHANGELOG.md", "**/*.{md,mdx,markdown,MD}"]
195
+ },
196
+ {
197
+ kind: "assets",
198
+ patterns: ["**/*.{png,jpg,jpeg,gif,webp,avif,ico,svg,bmp,tiff,pdf,mp3,wav,ogg,flac,mp4,webm,mov,avi,woff,woff2,ttf,otf,eot,zip,gz,tgz,bz2,xz,7z,tar,rar,wasm,bin,exe,dll,so,dylib,db,sqlite,sqlite3,icns}"]
199
+ }
200
+ ];
201
+ function gitFiles(root) {
202
+ return [...new Set(execFileSync("git", [
203
+ "ls-files",
204
+ "--cached",
205
+ "--others",
206
+ "--exclude-standard",
207
+ "-z"
208
+ ], {
209
+ cwd: root,
210
+ encoding: "utf8"
211
+ }).split("\0").filter(Boolean))];
212
+ }
213
+ function repositoryRoot(cwd = process.cwd()) {
214
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
215
+ cwd,
216
+ encoding: "utf8"
217
+ }).trim();
218
+ }
219
+ /** Resolves a user-supplied revision to a commit id, so it can never read as an option. */
220
+ function resolveRevision(root, revision) {
221
+ return execFileSync("git", [
222
+ "rev-parse",
223
+ "--verify",
224
+ "--end-of-options",
225
+ `${revision}^{commit}`
226
+ ], {
227
+ cwd: root,
228
+ encoding: "utf8",
229
+ stdio: [
230
+ "ignore",
231
+ "pipe",
232
+ "pipe"
233
+ ]
234
+ }).trim();
235
+ }
236
+ function repositoryPath(root, file) {
237
+ return relative(root, resolve(root, file)).split("\\").join("/");
238
+ }
239
+ function fileBytes(root, file, staged = false) {
240
+ if (staged) {
241
+ const result = spawnSync("git", [
242
+ "cat-file",
243
+ "-s",
244
+ `:${file}`
245
+ ], {
246
+ cwd: root,
247
+ encoding: "utf8"
248
+ });
249
+ if (result.error) throw result.error;
250
+ return result.status === 0 ? Number(result.stdout.trim()) : void 0;
251
+ }
252
+ const path = join(root, file);
253
+ if (!existsSync(path)) return void 0;
254
+ const stat = lstatSync(path);
255
+ return stat.isFile() ? stat.size : void 0;
256
+ }
257
+ /** A package root is the directory of a tracked manifest outside every `src` directory; its top-level `build` and `output` are generated output. */
258
+ function packageOutputPatterns(root) {
259
+ return trackedManifests(root).flatMap((manifest) => {
260
+ const directory = dirname(manifest);
261
+ if (directory === "." || directory.split("/").includes("src")) return [];
262
+ return [`${directory}/build/**`, `${directory}/output/**`];
263
+ });
264
+ }
265
+ /** `path.matchesGlob` never lets `*` or `**` cross a segment starting with a dot, so each path is also tried with those dots removed. */
266
+ function isExcluded(file, packageOutputs) {
267
+ const undotted = file.replace(/(^|\/)\./gu, "$1");
268
+ const matches = (pattern) => matchesGlob(file, pattern) || matchesGlob(undotted, pattern);
269
+ return SIZE_EXCLUSIONS.some(({ patterns }) => patterns.some(matches)) || packageOutputs.some(matches);
270
+ }
271
+ function isBinary(root, file, staged) {
272
+ if (staged) return execFileSync("git", ["show", `:${file}`], {
273
+ cwd: root,
274
+ maxBuffer: fileBytes(root, file, true)
275
+ }).subarray(0, 8e3).includes(0);
276
+ const descriptor = openSync(join(root, file), "r");
277
+ try {
278
+ const prefix = Buffer.alloc(8e3);
279
+ const length = readSync(descriptor, prefix, 0, prefix.length, 0);
280
+ return prefix.subarray(0, length).includes(0);
281
+ } finally {
282
+ closeSync(descriptor);
283
+ }
284
+ }
285
+ function authoredSizes(root, files = gitFiles(root), staged = false) {
286
+ const sizes = /* @__PURE__ */ new Map();
287
+ const packageOutputs = packageOutputPatterns(root);
288
+ for (const input of files) {
289
+ const file = repositoryPath(root, input);
290
+ if (isExcluded(file, packageOutputs)) continue;
291
+ const size = fileBytes(root, file, staged);
292
+ if (size !== void 0 && size > 2e4 && isBinary(root, file, staged)) continue;
293
+ if (size !== void 0) sizes.set(file, size);
294
+ }
295
+ return sizes;
296
+ }
297
+ //#endregion
298
+ export { resolveRevision as a, revisionCounts as c, repositoryRoot as i, trackedManifests as l, gitFiles as n, DIRECTORY_LINK as o, repositoryPath as r, compareRevisions as s, authoredSizes as t, runCli as u };
package/dist/oxc/fmt.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-NC0T8ykm.mjs";
1
+ import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-CsXIMK3I.mjs";
2
2
  import { defineConfig } from "oxfmt";
3
3
  //#region src/oxc/fmt.ts
4
4
  /**
package/dist/oxc/lint.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { i as findGitRoot, o as readCeilings } from "../ceilings--pJmhSde.mjs";
1
+ import { i as findGitRoot } from "../ceilings-BSw9p4YB.mjs";
2
+ import { t as readCarriedCeilings } from "../renames-BGO4dWJ8.mjs";
2
3
  import { n as TEST_FILE_GLOBS, t as ASSERTION_FUNCTION_NAMES } from "../shared-WC0z3y9F.mjs";
3
- import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-NC0T8ykm.mjs";
4
+ import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-CsXIMK3I.mjs";
4
5
  import { defineConfig } from "oxlint";
5
6
  //#region src/oxc/lint.ts
6
7
  /**
@@ -179,7 +180,7 @@ function importSortingPreset() {
179
180
  /** Overrides run after rules, including test-specific rules; measurement bypasses all ceilings. */
180
181
  function ratchetOverrides() {
181
182
  if (process.env["ANTELOPEJS_RATCHET_MEASURE"] !== void 0) return [];
182
- const ceilings = readCeilings(findGitRoot());
183
+ const ceilings = readCarriedCeilings(findGitRoot());
183
184
  const rules = Object.fromEntries(Object.keys(ceilings.rules).map((rule) => [rule, "off"]));
184
185
  return [...Object.keys(ceilings.oversizeFiles).map((path) => ({
185
186
  files: [path.replaceAll(/([*?[\]{}])/gu, "\\$1")],
@@ -101,6 +101,10 @@ function managedPackage(current, versions, options = {}) {
101
101
  "ratchet:update": `${tools.commands.ratchet} --update`,
102
102
  "sync:check": `${tools.commands.sync} --check`
103
103
  });
104
+ if (options.ratchet === false) {
105
+ delete scripts.ratchet;
106
+ delete scripts["ratchet:update"];
107
+ }
104
108
  for (const [name, command] of Object.entries(DEFAULT_SCRIPTS)) scripts[name] ??= name === "typecheck" && options.nuxt ? "nuxi typecheck" : command;
105
109
  const dependencies = manifest.devDependencies ??= {};
106
110
  dependencies.lefthook ??= versions.lefthook;