@antelopejs/tooling-configs 0.2.2 → 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/README.md CHANGED
@@ -173,7 +173,7 @@ pnpm install
173
173
  ```
174
174
 
175
175
  - A rule listed under `rules` is off for `oxlint`, so editors, hooks and `lint` stay green. `antelopejs-ratchet` counts it with every rule on, and fails when a count rises. It prints the findings in the files the change touched.
176
- - A file listed under `oversizeFiles` may exceed 20,000 bytes and `max-lines`, up to its recorded size. Any other file over either limit fails.
176
+ - A file listed under `oversizeFiles` may exceed 20,000 bytes and `max-lines`, up to its recorded size. Any other file over either limit fails. When git sees a listed file renamed (`git mv`, or a staged delete and add), the allowance moves to the new path and never exceeds the old one; a copy or a new file inherits nothing. `antelopejs-ratchet --base <ref>` detects renames against that revision instead of `HEAD`, for a rename that was committed before the file was updated.
177
177
  - `antelopejs-ratchet --update` lowers each number to the current count and removes what reached zero. It never raises a number or adds an entry. Commit the lowered file.
178
178
 
179
179
  When every rule count is zero and `oversizeFiles` is empty, the ratchet is retired. `antelopejs-ratchet --update` deletes the file and asks you to run `antelopejs-sync`. Sync then removes `ratchet`, `ratchet:update`, the ratchet hook and the Debt instructions. Commit those changes together. An empty baseline on first adoption creates no file. Later syncs never recreate it because the `sync:check` script marks adoption.
@@ -182,7 +182,7 @@ When every rule count is zero and `oversizeFiles` is empty, the ratchet is retir
182
182
 
183
183
  ### Size
184
184
 
185
- `antelopejs-check-size` fails on an authored file over 20,000 bytes, tests included. Generated code, fixtures, lockfiles, `CHANGELOG.md`, Markdown and binary assets are excluded. Data JSON under `locales/` or `i18n/`, `blocks.json`, `schema.json`, and `evals/**/cases.json` are excluded too. Files over the limit with a NUL byte in their first 8,000 bytes are binary even without a known extension. Staged checks inspect the index blob. Excluded files are also removed from recorded size allowances on ratchet update.
185
+ `antelopejs-check-size` fails on an authored file over 20,000 bytes, tests included. Generated code (`dist`, `coverage`, `.nuxt`, `.output`, and `build` or `output` at the repository root or a package root, never under `src/` or `test/`), fixtures, lockfiles, `CHANGELOG.md`, Markdown and binary assets are excluded. Data JSON under `locales/` or `i18n/`, `blocks.json`, `schema.json`, and `evals/**/cases.json` are excluded too. Files over the limit with a NUL byte in their first 8,000 bytes are binary even without a known extension. Staged checks inspect the index blob. Excluded files are also removed from recorded size allowances on ratchet update.
186
186
 
187
187
  Pass paths to check only those files, as the pre-commit hook does. The command also fails on an `oxlint-disable` or `eslint-disable` directive (line, next-line or file form) that names no rule: oxlint applies such a directive before any rule runs, so `no-comments` cannot report it.
188
188
 
@@ -1,9 +1,190 @@
1
1
  #!/usr/bin/env node
2
- import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
3
- import { i as compareRevisions, n as isTestSource, r as packagesAt, t as emitPackage } from "../typescript-CHOlkIXa.mjs";
4
- import { copyFileSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { c as revisionCounts, i as repositoryRoot, o as DIRECTORY_LINK, s as compareRevisions, u as runCli } from "../files-jKlueTMd.mjs";
3
+ import { i as parseManifest, r as packageManager } from "../package-Ckd7e4tA.mjs";
4
+ import { i as packagesAt, n as isTestSource, r as outputRoot, t as emitPackage } from "../typescript-aLCrMi2y.mjs";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
6
  import { dirname, join, relative, resolve } from "node:path";
7
+ import { execFileSync, spawnSync } from "node:child_process";
6
8
  import { generateDtsBundle } from "dts-bundle-generator";
9
+ //#region src/guardrails/runtime-diff.ts
10
+ const DESCRIBE = `
11
+ const { readFileSync, existsSync } = require("node:fs");
12
+ const { dirname, join, extname } = require("node:path");
13
+ const { pathToFileURL } = require("node:url");
14
+ const file = process.argv[1];
15
+ let directory = dirname(file);
16
+ while (!existsSync(join(directory, "package.json")) && dirname(directory) !== directory) directory = dirname(directory);
17
+ const manifest = existsSync(join(directory, "package.json")) ? JSON.parse(readFileSync(join(directory, "package.json"), "utf8")) : {};
18
+ const esm = extname(file) === ".mjs" || (extname(file) === ".js" && manifest.type === "module");
19
+ const seen = new Map();
20
+ function functionInfo(fn) {
21
+ let constructible = true;
22
+ try { Reflect.construct(Object, [], fn); } catch { constructible = false; }
23
+ return { name: fn.name, length: fn.length, async: Object.getPrototypeOf(fn) === Object.getPrototypeOf(async function() {}), constructible };
24
+ }
25
+ function valueInfo(value) {
26
+ if (typeof value !== "function") return { kind: value === null ? "null" : typeof value };
27
+ if (seen.has(value)) return { kind: "reference", id: seen.get(value) };
28
+ const id = seen.size;
29
+ seen.set(value, id);
30
+ return { kind: "function", id, function: functionInfo(value), static: properties(value, false), prototype: value.prototype ? properties(value.prototype, false) : [] };
31
+ }
32
+ function properties(object, observe) {
33
+ if (object === null || !["object", "function"].includes(typeof object)) return [];
34
+ return Reflect.ownKeys(object).map(key => {
35
+ const d = Object.getOwnPropertyDescriptor(object, key);
36
+ const common = { key: typeof key === "symbol" ? { symbol: String(key) } : key, enumerable: d.enumerable, configurable: d.configurable };
37
+ return "value" in d
38
+ ? { ...common, kind: "data", writable: d.writable, value: valueInfo(d.value) }
39
+ : { ...common, kind: "accessor", get: d.get ? functionInfo(d.get) : null, set: d.set ? functionInfo(d.set) : null, observed: observe ? valueInfo(Reflect.get(object, key)) : null };
40
+ });
41
+ }
42
+ (async () => {
43
+ const value = esm ? await import(pathToFileURL(file).href) : require(file);
44
+ process.stdout.write(JSON.stringify({ kind: esm ? "esm" : "cjs", value: valueInfo(value), properties: typeof value === "function" ? [] : properties(value, true) }));
45
+ })().catch(error => { process.stderr.write(String(error.stack)); process.exitCode = 1; });
46
+ `;
47
+ function runtimeTargets(target, entry = "exports") {
48
+ if (target == null) return [];
49
+ if (Array.isArray(target)) return target.flatMap((value, index) => runtimeTargets(value, `${entry}/${index}`));
50
+ if (target.constructor === String) return [{
51
+ entry,
52
+ target: String(target)
53
+ }];
54
+ return Object.entries(target).flatMap(([key, value]) => {
55
+ if (key === "types") return [];
56
+ if ([
57
+ "browser",
58
+ "deno",
59
+ "react-native"
60
+ ].includes(key)) throw new Error(`${entry}/${key}: unsupported runtime environment.`);
61
+ return runtimeTargets(value, `${entry}/${key}`);
62
+ });
63
+ }
64
+ function builtEntries(directory, manifest) {
65
+ const targets = runtimeTargets(manifest.exports);
66
+ if (manifest.main) targets.push({
67
+ entry: "main",
68
+ target: manifest.main
69
+ });
70
+ if (!targets.length) throw new Error(`${directory}: no published runtime entry.`);
71
+ return targets.filter(({ target }) => !target.endsWith(".json")).flatMap(({ entry, target }) => {
72
+ if (!target.includes("*")) {
73
+ const file = resolve(directory, target);
74
+ if (!existsSync(file)) throw new Error(`${directory}: missing built runtime entry ${target}.`);
75
+ return [{
76
+ entry,
77
+ file
78
+ }];
79
+ }
80
+ const prefix = resolve(directory, target.slice(0, target.indexOf("*")));
81
+ const parent = dirname(prefix);
82
+ const pattern = new RegExp(`^${resolve(directory, target).split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("(.*)")}$`, "u");
83
+ if (!existsSync(parent)) throw new Error(`${directory}: missing build output for ${target}.`);
84
+ const output = outputRoot(directory);
85
+ const matches = readdirSync(parent, {
86
+ recursive: true,
87
+ withFileTypes: true
88
+ }).filter((file) => file.isFile()).map((file) => join(file.parentPath, file.name)).filter((file) => pattern.test(file) && !isTestSource(output, file));
89
+ if (!matches.length) throw new Error(`${directory}: missing built runtime entry ${target}.`);
90
+ return matches.sort().map((file) => {
91
+ const capture = pattern.exec(file)?.[1] ?? relative(directory, file);
92
+ return {
93
+ entry: entry.replaceAll("*", capture),
94
+ file
95
+ };
96
+ });
97
+ });
98
+ }
99
+ function childJson(args, cwd) {
100
+ const result = spawnSync(process.execPath, args, {
101
+ cwd,
102
+ encoding: "utf8",
103
+ timeout: 1e4,
104
+ maxBuffer: 16777216,
105
+ stdio: [
106
+ "ignore",
107
+ "pipe",
108
+ "pipe"
109
+ ]
110
+ });
111
+ if (result.error) throw result.error;
112
+ if (result.status !== 0) throw new Error(result.stderr || `Runtime child exited with ${result.status ?? result.signal}.`);
113
+ return `${JSON.stringify(JSON.parse(result.stdout), null, 2)}\n`;
114
+ }
115
+ function buildPackage(directory, output, manifest) {
116
+ const buildManifest = parseManifest(readFileSync(join(directory, "package.json"), "utf8"));
117
+ if (buildManifest.scripts?.build) execFileSync(packageManager(buildManifest), ["run", "build"], {
118
+ cwd: directory,
119
+ encoding: "utf8",
120
+ shell: process.platform === "win32",
121
+ env: {
122
+ ...process.env,
123
+ pnpm_config_verify_deps_before_run: "false"
124
+ },
125
+ timeout: 12e4,
126
+ stdio: [
127
+ "ignore",
128
+ "pipe",
129
+ "pipe"
130
+ ]
131
+ });
132
+ let entries;
133
+ try {
134
+ entries = builtEntries(directory, manifest);
135
+ } catch (error) {
136
+ if (buildManifest.scripts?.build) throw error;
137
+ return;
138
+ }
139
+ for (const { entry, file } of entries) {
140
+ const destination = join(output, `${encodeURIComponent(entry)}.json`);
141
+ const metadata = childJson([
142
+ "-e",
143
+ DESCRIBE,
144
+ file
145
+ ], directory);
146
+ mkdirSync(dirname(destination), { recursive: true });
147
+ writeFileSync(destination, metadata);
148
+ }
149
+ return entries.length;
150
+ }
151
+ /** Builds both committed revisions and compares published runtime metadata and one captured optional probe. */
152
+ function runtimeDiff(root, baseRef, probe) {
153
+ const errors = [];
154
+ const failedPackages = /* @__PURE__ */ new Set();
155
+ const skippedPackages = /* @__PURE__ */ new Set();
156
+ const result = compareRevisions(root, baseRef, "Runtime API", (worktree, output, scratch) => {
157
+ const packages = packagesAt(worktree, true);
158
+ let entries = 0;
159
+ for (const { directory, manifest } of packages) try {
160
+ const built = buildPackage(join(worktree, directory), join(output, encodeURIComponent(directory).replaceAll(".", "%2E")), manifest);
161
+ if (built === void 0) skippedPackages.add(directory);
162
+ else entries += built;
163
+ } catch (error) {
164
+ failedPackages.add(directory);
165
+ errors.push(`Execution error: ${directory}: ${error instanceof Error ? error.message : String(error)}\n`);
166
+ }
167
+ for (const directory of failedPackages) for (const side of ["base", "head"]) rmSync(join(dirname(output), side, encodeURIComponent(directory).replaceAll(".", "%2E")), {
168
+ recursive: true,
169
+ force: true
170
+ });
171
+ if (probe !== void 0) {
172
+ mkdirSync(scratch, { recursive: true });
173
+ const script = join(scratch, "probe.cjs");
174
+ writeFileSync(script, probe);
175
+ writeFileSync(join(output, "probe.json"), childJson([script, worktree], worktree));
176
+ }
177
+ return {
178
+ packages: packages.map(({ directory }) => directory),
179
+ entries
180
+ };
181
+ });
182
+ return {
183
+ different: result.different || errors.length > 0,
184
+ output: (errors.length && !result.different ? result.output.replace(/^Runtime API:[^\n]*\n/u, "Runtime API: comparison incomplete due to execution errors.\n") : result.output) + [...skippedPackages].map((directory) => `Runtime API: skipped ${directory}, which has no build script and no committed runtime entry.\n`).join("") + errors.join("")
185
+ };
186
+ }
187
+ //#endregion
7
188
  //#region src/guardrails/api-diff.ts
8
189
  function entrySegment(value) {
9
190
  return encodeURIComponent(value).replaceAll(".", "%2E").replaceAll("*", "%2A");
@@ -99,7 +280,7 @@ function bundleDependencies(directory, scratch) {
99
280
  if (parent === current) return;
100
281
  current = parent;
101
282
  }
102
- symlinkSync(join(current, "node_modules"), join(scratch, "node_modules"), "dir");
283
+ symlinkSync(join(current, "node_modules"), join(scratch, "node_modules"), DIRECTORY_LINK);
103
284
  }
104
285
  /** dts-bundle-generator 9 remaps a .d.mts/.d.cts root twice; a source-named copy preserves its declaration text. */
105
286
  function generatorEntry(file) {
@@ -140,8 +321,7 @@ function bundlePackage(directory, output, scratch, manifest) {
140
321
  }
141
322
  return entries.length;
142
323
  }
143
- /** Proves published declarations match between a base commit and committed HEAD. */
144
- function apiDiff(root, baseRef) {
324
+ function declarationDiff(root, baseRef) {
145
325
  return compareRevisions(root, baseRef, "API", (worktree, output, scratch) => {
146
326
  const packages = packagesAt(worktree, true);
147
327
  let entries = 0;
@@ -152,12 +332,27 @@ function apiDiff(root, baseRef) {
152
332
  };
153
333
  });
154
334
  }
335
+ /** Compares declarations and built runtime exports, with an optional behavioral probe captured before either build. */
336
+ function apiDiff(root, baseRef, probeFile) {
337
+ const probe = probeFile === void 0 ? void 0 : readFileSync(resolve(root, probeFile));
338
+ const results = [];
339
+ const errors = [];
340
+ for (const compare of [() => declarationDiff(root, baseRef), () => runtimeDiff(root, baseRef, probe)]) try {
341
+ results.push(compare());
342
+ } catch (error) {
343
+ errors.push(`Execution error: ${error instanceof Error ? error.message : String(error)}\n`);
344
+ }
345
+ return {
346
+ different: errors.length > 0 || results.some((result) => result.different),
347
+ output: results.map((result) => result.output).join("") + errors.join("") + (probe === void 0 ? "No behavioural probe ran.\n" : "Behavioural probe supplied.\n") + revisionCounts(root, baseRef)
348
+ };
349
+ }
155
350
  //#endregion
156
351
  //#region src/bin/api-diff.ts
157
352
  runCli(() => {
158
353
  const args = process.argv.slice(2);
159
- if (args.length !== 1) throw new Error("Usage: antelopejs-api-diff <base-ref>");
160
- const result = apiDiff(repositoryRoot(), args[0]);
354
+ if (args.length !== 1 && !(args.length === 3 && args[1] === "--probe")) throw new Error("Usage: antelopejs-api-diff <base-ref> [--probe <file.cjs>]");
355
+ const result = apiDiff(repositoryRoot(), args[0], args[2]);
161
356
  process.stdout.write(result.output);
162
357
  return result.different ? 1 : 0;
163
358
  });
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as runCli, i as repositoryRoot, r as repositoryPath, t as authoredSizes } from "../files-DAD5OtfV.mjs";
3
- import { t as checkSize } from "../size-D7JJp_W4.mjs";
2
+ import { i as repositoryRoot, r as repositoryPath, t as authoredSizes, u as runCli } from "../files-jKlueTMd.mjs";
3
+ import { t as checkSize } from "../size-BdYmv57u.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  import { spawnSync } from "node:child_process";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "../ceilings-BSw9p4YB.mjs";
3
- import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
3
+ import { i as repositoryRoot, u as runCli } from "../files-jKlueTMd.mjs";
4
4
  import { i as parseManifest, r as packageManager } from "../package-Ckd7e4tA.mjs";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
@@ -29,6 +29,7 @@ function runChecks(root) {
29
29
  const result = spawnSync(manager, ["run", step], {
30
30
  cwd: root,
31
31
  encoding: "utf8",
32
+ shell: process.platform === "win32",
32
33
  env: {
33
34
  ...process.env,
34
35
  CI: "1"
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
3
- import { i as compareRevisions, r as packagesAt, t as emitPackage } from "../typescript-CHOlkIXa.mjs";
2
+ import { c as revisionCounts, i as repositoryRoot, s as compareRevisions, u as runCli } from "../files-jKlueTMd.mjs";
3
+ import { i as packagesAt, t as emitPackage } from "../typescript-aLCrMi2y.mjs";
4
4
  import { copyFileSync, mkdirSync } from "node:fs";
5
5
  import { dirname, join, relative } from "node:path";
6
6
  //#region src/guardrails/comment-proof.ts
7
7
  /** Proves comment-free JavaScript emit matches, including private packages and excluding tests. */
8
8
  function commentProof(root, baseRef) {
9
- return compareRevisions(root, baseRef, "Comment proof", (worktree, output, scratch) => {
9
+ const result = compareRevisions(root, baseRef, "Comment proof", (worktree, output, scratch) => {
10
10
  const packages = packagesAt(worktree, false);
11
11
  let entries = 0;
12
12
  for (const { directory } of packages) {
@@ -25,6 +25,10 @@ function commentProof(root, baseRef) {
25
25
  entries
26
26
  };
27
27
  });
28
+ return {
29
+ ...result,
30
+ output: result.output + revisionCounts(root, baseRef)
31
+ };
28
32
  }
29
33
  //#endregion
30
34
  //#region src/bin/comment-proof.ts
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { s as readCeilings, t as CEILINGS_FILE } from "../ceilings-BSw9p4YB.mjs";
3
- import { a as runCli, i as repositoryRoot, r as repositoryPath } from "../files-DAD5OtfV.mjs";
4
- import { n as ratchet, r as lintDiagnostics } from "../ratchet-B2_QUucT.mjs";
3
+ import { n as renamedPaths } from "../renames-BGO4dWJ8.mjs";
4
+ import { a as resolveRevision, i as repositoryRoot, r as repositoryPath, u as runCli } from "../files-jKlueTMd.mjs";
5
+ import { n as ratchet, r as lintDiagnostics } from "../ratchet-CNbpU42F.mjs";
5
6
  import { resolve } from "node:path";
6
7
  import { execFileSync } from "node:child_process";
7
8
  import { parseArgs } from "node:util";
@@ -18,6 +19,7 @@ function stagedRatchet(root, files) {
18
19
  encoding: "utf8"
19
20
  }).split("\0").filter(Boolean);
20
21
  if (!targets.length) return [];
22
+ const renames = renamedPaths(root, true);
21
23
  const additions = /* @__PURE__ */ new Map();
22
24
  for (const file of targets) {
23
25
  const diff = execFileSync("git", [
@@ -26,8 +28,9 @@ function stagedRatchet(root, files) {
26
28
  "--no-ext-diff",
27
29
  "--no-color",
28
30
  "--unified=0",
31
+ "--find-renames",
29
32
  "--",
30
- file
33
+ ...[renames.get(file), file].filter((path) => path !== void 0)
31
34
  ], {
32
35
  cwd: root,
33
36
  encoding: "utf8"
@@ -46,18 +49,20 @@ runCli(() => {
46
49
  const { values, positionals } = parseArgs({
47
50
  options: {
48
51
  update: { type: "boolean" },
49
- staged: { type: "boolean" }
52
+ staged: { type: "boolean" },
53
+ base: { type: "string" }
50
54
  },
51
55
  allowPositionals: true
52
56
  });
53
57
  const root = repositoryRoot();
58
+ if (values.base !== void 0 && values.staged) throw new Error("--base cannot be combined with --staged.");
54
59
  if (values.update && values.staged) throw new Error("--staged cannot be combined with --update.");
55
60
  if (positionals.length && !values.staged) throw new Error("File arguments require --staged.");
56
61
  const result = values.staged ? {
57
62
  failures: stagedRatchet(root, positionals.length ? positionals.map((file) => repositoryPath(root, resolve(file))) : void 0),
58
63
  lowered: [],
59
64
  retired: false
60
- } : ratchet(root, values.update);
65
+ } : ratchet(root, values.update, values.base === void 0 ? void 0 : resolveRevision(root, values.base));
61
66
  if (result.retired) process.stdout.write(`Deleted ${CEILINGS_FILE}: no debt left. Run antelopejs-sync.\n`);
62
67
  for (const message of result.lowered) process.stdout.write(`Lowered ${message}\n`);
63
68
  for (const message of result.failures) process.stderr.write(`${message}\n`);
package/dist/bin/sync.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as formatCeilings, o as isRetired, s as readCeilings, t as CEILINGS_FILE } from "../ceilings-BSw9p4YB.mjs";
3
- import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
3
+ import { i as repositoryRoot, u as runCli } from "../files-jKlueTMd.mjs";
4
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-B2_QUucT.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:
@@ -22,7 +27,13 @@ Hold every test you write or keep to this checklist:
22
27
  - An invariant over many files is a lint rule, not a test.`;
23
28
  const CLEANUP_PROOF = `### Cleanup proof
24
29
 
25
- A behaviour-preserving cleanup PR shows \`antelopejs-api-diff <base-ref>\` passing. Published declarations stay unchanged. A comment-only PR shows \`antelopejs-comment-proof <base-ref>\` passing instead of a verifier. Both compare committed HEAD.`;
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.`;
26
37
  const SHARED_RULES = [
27
38
  ["Formatting", "oxfmt"],
28
39
  ["Lint findings are errors; no rule warns", "oxlint preset"],
@@ -32,14 +43,19 @@ const SHARED_RULES = [
32
43
  ["No assertion that restates a constant", "`antelopejs/no-constant-value-assertion`"],
33
44
  ["No test whose only check is \"is a function\"", "`antelopejs/no-typeof-function-only-test`"],
34
45
  ["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"],
35
- ["Cleanup preserves published declarations", "`antelopejs-api-diff <base-ref>`"],
36
- ["Comment-only changes preserve JavaScript emit", "`antelopejs-comment-proof <base-ref>`"]
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"]
37
52
  ];
38
53
  const FULL_RULES = [
39
54
  ["Files ≤ 20,000 bytes, tests included", "`antelopejs-check-size` (pre-commit)"],
40
55
  ["Types check", "`tsc` (pre-push)"],
41
56
  ["No dead files, exports, or dependencies", "knip"],
42
57
  ["Managed files match the pack", "`antelopejs-sync --check`"],
58
+ ["Entry-only rules exempt no tracked outsider", "`antelopejs-sync --check`"],
43
59
  ["A test names the wrong implementation it catches", "review; no tool"]
44
60
  ];
45
61
  /** Pads each column the way oxfmt does, so formatting AGENTS.md never reports sync drift. */
@@ -258,6 +274,76 @@ function hasChecksWorkflow(root) {
258
274
  return false;
259
275
  }
260
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
261
347
  //#region src/guardrails/sync.ts
262
348
  const LEGACY_COMMENT_INSTRUCTION = "- Explain non-obvious constraints and decisions in comments; avoid restating the code. Document public contracts with TSDoc.";
263
349
  const RETIRED_RATCHET = `${CEILINGS_FILE} has no debt left. Run ratchet:update to delete it, then run antelopejs-sync.`;
@@ -463,6 +549,8 @@ function syncRepository(root, options = {}) {
463
549
  const context = repositoryContext(root, current, profile);
464
550
  let plan = planSync(current, modes, context);
465
551
  const errors = plan.filter((artifact) => artifact.kind === "error").map(({ message }) => message);
552
+ const entryOnly = checkEntryOnlyConfiguration(root);
553
+ errors.push(...entryOnly.errors);
466
554
  const notes = plan.flatMap((artifact) => artifact.kind === "migrated" ? artifact.notes : []);
467
555
  (options.check ? errors : notes).push(...repositoryNotices(root, context));
468
556
  if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json") && manifest.scripts?.["sync:check"] === void 0) {
@@ -483,6 +571,7 @@ function syncRepository(root, options = {}) {
483
571
  paths: errors.length ? [] : writes.map(({ path }) => path),
484
572
  errors,
485
573
  notes,
574
+ advisories: entryOnly.advisories,
486
575
  dependenciesAdded
487
576
  };
488
577
  }
@@ -502,6 +591,7 @@ runCli(() => {
502
591
  profile: values.profile
503
592
  });
504
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`);
505
595
  for (const error of result.errors) process.stderr.write(`${error}\n`);
506
596
  if (result.errors.length) return 1;
507
597
  for (const path of result.paths) process.stdout.write(`${values.check ? "Drift" : "Updated"}: ${path}${values.check ? "; run antelopejs-sync" : ""}\n`);
@@ -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, s as readCeilings } from "../ceilings-BSw9p4YB.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")],
@@ -1,6 +1,7 @@
1
- import { a as formatCeilings, n as MAX_FILE_BYTES, o as isRetired, r as MEASURE_ENV, s as readCeilings, t as CEILINGS_FILE } from "./ceilings-BSw9p4YB.mjs";
2
- import { n as gitFiles, r as repositoryPath, t as authoredSizes } from "./files-DAD5OtfV.mjs";
3
- import { n as sizeFailure } from "./size-D7JJp_W4.mjs";
1
+ import { a as formatCeilings, n as MAX_FILE_BYTES, o as isRetired, r as MEASURE_ENV, t as CEILINGS_FILE } from "./ceilings-BSw9p4YB.mjs";
2
+ import { t as readCarriedCeilings } from "./renames-BGO4dWJ8.mjs";
3
+ import { n as gitFiles, r as repositoryPath, t as authoredSizes } from "./files-jKlueTMd.mjs";
4
+ import { n as sizeFailure } from "./size-BdYmv57u.mjs";
4
5
  import { createRequire } from "node:module";
5
6
  import { existsSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
7
  import { basename, dirname, join } from "node:path";
@@ -50,7 +51,7 @@ function lintDiagnostics(root, config, files) {
50
51
  writeFileSync(path, config ?? "");
51
52
  args.push("--config", path, "--ignore-pattern", `${basename(temporary)}/**`);
52
53
  }
53
- if (files) args.push("--", ...files);
54
+ if (files) args.push("--no-error-on-unmatched-pattern", "--", ...files);
54
55
  const result = spawnSync(process.execPath, args, {
55
56
  cwd: root,
56
57
  encoding: "utf8",
@@ -137,9 +138,9 @@ function lowerCeilings(ceilings, current) {
137
138
  }
138
139
  return lowered;
139
140
  }
140
- function ratchet(root, update = false) {
141
+ function ratchet(root, update = false, base = "HEAD") {
141
142
  if (!existsSync(join(root, "guardrails.ratchet.json"))) throw new Error(`${CEILINGS_FILE} is missing. Run antelopejs-sync first.`);
142
- const ceilings = readCeilings(root);
143
+ const ceilings = readCarriedCeilings(root, false, [base]);
143
144
  const diagnostics = lintDiagnostics(root);
144
145
  const current = baseline(root, diagnostics);
145
146
  const changed = changedFiles(root);
@@ -0,0 +1,59 @@
1
+ import { s as readCeilings } from "./ceilings-BSw9p4YB.mjs";
2
+ import { spawnSync } from "node:child_process";
3
+ //#region src/guardrails/renames.ts
4
+ /**
5
+ * Renames git detects, as new path to old path. Without `revisions` it
6
+ * compares the index with HEAD when `staged`, otherwise the working tree with
7
+ * HEAD; with revisions it compares those. Copies are never reported, and a
8
+ * repository without HEAD has no renames.
9
+ */
10
+ function renamedPaths(root, staged = false, revisions = staged ? [] : ["HEAD"]) {
11
+ const result = spawnSync("git", [
12
+ "diff",
13
+ "--find-renames",
14
+ "--diff-filter=R",
15
+ "--name-status",
16
+ "-z",
17
+ "--no-ext-diff",
18
+ ...staged ? ["--cached"] : [],
19
+ ...revisions,
20
+ "--"
21
+ ], {
22
+ cwd: root,
23
+ encoding: "utf8",
24
+ maxBuffer: 67108864
25
+ });
26
+ const renames = /* @__PURE__ */ new Map();
27
+ if (result.error || result.status !== 0) return renames;
28
+ const fields = result.stdout.split("\0");
29
+ for (let index = 0; index + 2 < fields.length; index += 3) renames.set(fields[index + 2], fields[index + 1]);
30
+ return renames;
31
+ }
32
+ /**
33
+ * Moves the oversize allowance of each renamed file to its new path. The new
34
+ * path never gets more than the old allowance; a copy or a new file has no
35
+ * rename entry and so inherits nothing.
36
+ */
37
+ function carryAllowances(ceilings, renames) {
38
+ const oversizeFiles = { ...ceilings.oversizeFiles };
39
+ const moved = [];
40
+ for (const [file, previous] of renames) {
41
+ const allowance = ceilings.oversizeFiles[previous];
42
+ if (allowance === void 0) continue;
43
+ delete oversizeFiles[previous];
44
+ moved.push([file, Math.min(allowance, ceilings.oversizeFiles[file] ?? allowance)]);
45
+ }
46
+ for (const [file, allowance] of moved) oversizeFiles[file] = allowance;
47
+ return {
48
+ rules: ceilings.rules,
49
+ oversizeFiles
50
+ };
51
+ }
52
+ /** The ceilings at `root` with allowances carried across the renames in `staged` or working-tree scope. */
53
+ function readCarriedCeilings(root, staged = false, revisions) {
54
+ const ceilings = readCeilings(root);
55
+ if (Object.keys(ceilings.oversizeFiles).length === 0) return ceilings;
56
+ return carryAllowances(ceilings, renamedPaths(root, staged, revisions));
57
+ }
58
+ //#endregion
59
+ export { renamedPaths as n, readCarriedCeilings as t };
@@ -3,16 +3,18 @@
3
3
  * Paths every AntelopeJS project generates or vendors, and that no linter should read.
4
4
  * `.github/ISSUE_TEMPLATE/**` holds GitHub's legacy issue templates: Markdown with
5
5
  * YAML frontmatter saved under a .yml extension, which a YAML formatter cannot parse.
6
+ * `build/**` and `output/**` match only at the repository root: a directory with
7
+ * either name under `src/` or another authored root is source, not build output.
6
8
  */
7
9
  const IGNORE_PATTERNS = [
8
10
  "**/node_modules/**",
9
11
  "**/dist/**",
10
- "**/build/**",
11
12
  "**/coverage/**",
12
13
  "**/.antelope/**",
13
14
  "**/.nuxt/**",
14
15
  "**/.output/**",
15
- "**/output/**",
16
+ "build/**",
17
+ "output/**",
16
18
  "**/i18n-registry.generated.ts",
17
19
  ".github/ISSUE_TEMPLATE/**"
18
20
  ];
@@ -1,11 +1,12 @@
1
- import { s as readCeilings } from "./ceilings-BSw9p4YB.mjs";
2
- import { t as authoredSizes } from "./files-DAD5OtfV.mjs";
1
+ import "./ceilings-BSw9p4YB.mjs";
2
+ import { t as readCarriedCeilings } from "./renames-BGO4dWJ8.mjs";
3
+ import { t as authoredSizes } from "./files-jKlueTMd.mjs";
3
4
  //#region src/guardrails/size.ts
4
5
  function sizeFailure(file, size, limit) {
5
6
  return `${file}: ${size} bytes, ${size - limit} bytes over the ${limit}-byte ceiling. Split by responsibility into sibling modules, one cohesive group of exports each; keep the original path re-exporting only if it is a public entry point. Split a test file by behaviour under test, one file per describe. Do not minify, strip whitespace, or move code under an excluded path to pass.`;
6
7
  }
7
8
  function checkSize(root, files, staged = false) {
8
- const ceilings = readCeilings(root);
9
+ const ceilings = readCarriedCeilings(root, staged);
9
10
  const failures = [];
10
11
  for (const [file, size] of authoredSizes(root, files, staged)) {
11
12
  const limit = ceilings.oversizeFiles[file] ?? 2e4;
@@ -1,106 +1,7 @@
1
- import { n as TEST_FILE_GLOBS } from "./shared-WC0z3y9F.mjs";
1
+ import { l as trackedManifests } from "./files-jKlueTMd.mjs";
2
2
  import { createRequire } from "node:module";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
- import { dirname, join, matchesGlob, relative, sep } from "node:path";
5
- import { execFileSync, spawnSync } from "node:child_process";
6
- import { tmpdir } from "node:os";
7
- //#region src/guardrails/revisions.ts
8
- function trackedManifests(root) {
9
- return execFileSync("git", ["ls-files", "-z"], {
10
- cwd: root,
11
- encoding: "utf8"
12
- }).split("\0").filter((file) => file === "package.json" || file.endsWith("/package.json"));
13
- }
14
- function linkDependencies(root, worktree) {
15
- const directories = /* @__PURE__ */ new Set([".", ...trackedManifests(root).map(dirname)]);
16
- for (const directory of directories) {
17
- const source = join(root, directory, "node_modules");
18
- const target = join(worktree, directory, "node_modules");
19
- if (existsSync(source) && existsSync(dirname(target)) && !existsSync(target)) symlinkSync(source, target, "dir");
20
- }
21
- }
22
- /** Compares isolated committed revisions and removes both worktrees even when compilation fails. */
23
- function compareRevisions(root, baseRef, label, emit) {
24
- const temporary = mkdtempSync(join(tmpdir(), "antelope-proof-"));
25
- const worktrees = [];
26
- const results = [];
27
- try {
28
- for (const [side, ref] of [["base", baseRef], ["head", "HEAD"]]) {
29
- const revision = execFileSync("git", [
30
- "rev-parse",
31
- "--verify",
32
- "--end-of-options",
33
- `${ref}^{commit}`
34
- ], {
35
- cwd: root,
36
- encoding: "utf8",
37
- stdio: [
38
- "ignore",
39
- "pipe",
40
- "pipe"
41
- ]
42
- }).trim();
43
- const worktree = join(temporary, "worktrees", side);
44
- execFileSync("git", [
45
- "worktree",
46
- "add",
47
- "--quiet",
48
- "--detach",
49
- worktree,
50
- revision
51
- ], {
52
- cwd: root,
53
- stdio: [
54
- "ignore",
55
- "pipe",
56
- "pipe"
57
- ]
58
- });
59
- worktrees.push(worktree);
60
- linkDependencies(root, worktree);
61
- const output = join(temporary, side);
62
- mkdirSync(output, { recursive: true });
63
- results.push(emit(worktree, output, join(temporary, "emit", side)));
64
- }
65
- const diff = spawnSync("git", [
66
- "diff",
67
- "--no-index",
68
- "--no-color",
69
- "--no-renames",
70
- "--no-ext-diff",
71
- "--no-textconv",
72
- "--",
73
- "base",
74
- "head"
75
- ], {
76
- cwd: temporary,
77
- encoding: "utf8",
78
- maxBuffer: 67108864
79
- });
80
- if (diff.error) throw diff.error;
81
- if (diff.status !== 0 && diff.status !== 1) throw new Error(diff.stderr || "Could not compare revision outputs.");
82
- const packages = new Set(results.flatMap((result) => result.packages)).size;
83
- return {
84
- different: diff.status === 1,
85
- 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`
86
- };
87
- } finally {
88
- for (const worktree of worktrees.reverse()) execFileSync("git", [
89
- "worktree",
90
- "remove",
91
- "--force",
92
- worktree
93
- ], {
94
- cwd: root,
95
- stdio: "pipe"
96
- });
97
- rmSync(temporary, {
98
- recursive: true,
99
- force: true
100
- });
101
- }
102
- }
103
- //#endregion
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
104
5
  //#region src/guardrails/typescript.ts
105
6
  function packagesAt(root, publishedOnly) {
106
7
  return trackedManifests(root).flatMap((file) => {
@@ -118,9 +19,16 @@ function packagesAt(root, publishedOnly) {
118
19
  function loadTypeScript(directory) {
119
20
  return createRequire(join(directory, "package.json"))("typescript");
120
21
  }
22
+ const TEST_SOURCE_GLOBS = [
23
+ "**/*.test.*",
24
+ "**/*.spec.*",
25
+ "**/__tests__/**",
26
+ "test/**",
27
+ "tests/**"
28
+ ];
121
29
  function isTestSource(root, file) {
122
30
  const path = relative(root, file).split("\\").join("/");
123
- return TEST_FILE_GLOBS.some((pattern) => matchesGlob(path, pattern));
31
+ return TEST_SOURCE_GLOBS.some((pattern) => matchesGlob(path, pattern));
124
32
  }
125
33
  function failDiagnostics(compiler, diagnostics) {
126
34
  const errors = diagnostics.filter((item) => item.category === compiler.DiagnosticCategory.Error);
@@ -149,14 +57,24 @@ function copyDeclarations(program, directory, output) {
149
57
  }
150
58
  return files;
151
59
  }
152
- /** Emits with the package's compiler and config; test roots and outputs never enter either proof. */
153
- function emitPackage(directory, output, declarations) {
60
+ function parseConfig(directory) {
154
61
  const compiler = loadTypeScript(directory);
155
- const configPath = join(directory, "tsconfig.json");
156
- const config = compiler.readConfigFile(configPath, compiler.sys.readFile);
62
+ const config = compiler.readConfigFile(join(directory, "tsconfig.json"), compiler.sys.readFile);
157
63
  if (config.error) failDiagnostics(compiler, [config.error]);
158
64
  const parsed = compiler.parseJsonConfigFileContent(config.config, compiler.sys, directory);
159
65
  failDiagnostics(compiler, parsed.errors);
66
+ return {
67
+ compiler,
68
+ parsed
69
+ };
70
+ }
71
+ /** The configured `outDir`, the root under which the declaration half already judges a built path as test output. */
72
+ function outputRoot(directory) {
73
+ return resolve(directory, parseConfig(directory).parsed.options.outDir ?? ".");
74
+ }
75
+ /** Emits with the package's compiler and config; test roots and outputs never enter either proof. */
76
+ function emitPackage(directory, output, declarations) {
77
+ const { compiler, parsed } = parseConfig(directory);
160
78
  const options = {
161
79
  ...parsed.options,
162
80
  rootDir: parsed.options.rootDir ?? (parsed.options.outDir ? void 0 : directory),
@@ -199,4 +117,4 @@ function emitPackage(directory, output, declarations) {
199
117
  };
200
118
  }
201
119
  //#endregion
202
- export { compareRevisions as i, isTestSource as n, packagesAt as r, emitPackage as t };
120
+ export { packagesAt as i, isTestSource as n, outputRoot as r, emitPackage as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/tooling-configs",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Shared tooling presets for AntelopeJS repositories: oxlint (with the vendored anti-slop rules), oxfmt, ESLint for Nuxt layers, Knip, and an interface range check",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -1,129 +0,0 @@
1
- import "./ceilings-BSw9p4YB.mjs";
2
- import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "./shared-NC0T8ykm.mjs";
3
- import { closeSync, existsSync, lstatSync, openSync, readSync } from "node:fs";
4
- import { join, matchesGlob, relative, resolve } from "node:path";
5
- import { execFileSync, spawnSync } from "node:child_process";
6
- //#region src/guardrails/cli.ts
7
- function runCli(main) {
8
- try {
9
- process.exitCode = main();
10
- } catch (error) {
11
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
12
- process.exitCode = 1;
13
- }
14
- }
15
- //#endregion
16
- //#region src/guardrails/files.ts
17
- const SIZE_EXCLUSIONS = [
18
- {
19
- kind: "generated",
20
- patterns: [
21
- ...IGNORE_PATTERNS,
22
- ...AGENT_IGNORE_PATTERNS,
23
- "**/*.generated.*",
24
- "**/generated/**",
25
- "**/__snapshots__/**"
26
- ]
27
- },
28
- {
29
- kind: "fixtures",
30
- patterns: ["**/fixtures/**", "**/__fixtures__/**"]
31
- },
32
- {
33
- kind: "data",
34
- patterns: [
35
- "**/locales/**/*.json",
36
- "**/i18n/**/*.json",
37
- "**/blocks.json",
38
- "**/schema.json",
39
- "**/evals/**/cases.json"
40
- ]
41
- },
42
- {
43
- kind: "lockfiles",
44
- patterns: [
45
- "**/{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}",
46
- "**/*.lock",
47
- "**/*.lockb"
48
- ]
49
- },
50
- {
51
- kind: "documentation",
52
- patterns: ["**/CHANGELOG.md", "**/*.{md,mdx,markdown,MD}"]
53
- },
54
- {
55
- kind: "assets",
56
- 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}"]
57
- }
58
- ];
59
- function gitFiles(root) {
60
- return [...new Set(execFileSync("git", [
61
- "ls-files",
62
- "--cached",
63
- "--others",
64
- "--exclude-standard",
65
- "-z"
66
- ], {
67
- cwd: root,
68
- encoding: "utf8"
69
- }).split("\0").filter(Boolean))];
70
- }
71
- function repositoryRoot(cwd = process.cwd()) {
72
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
73
- cwd,
74
- encoding: "utf8"
75
- }).trim();
76
- }
77
- function repositoryPath(root, file) {
78
- return relative(root, resolve(root, file)).split("\\").join("/");
79
- }
80
- function fileBytes(root, file, staged = false) {
81
- if (staged) {
82
- const result = spawnSync("git", [
83
- "cat-file",
84
- "-s",
85
- `:${file}`
86
- ], {
87
- cwd: root,
88
- encoding: "utf8"
89
- });
90
- if (result.error) throw result.error;
91
- return result.status === 0 ? Number(result.stdout.trim()) : void 0;
92
- }
93
- const path = join(root, file);
94
- if (!existsSync(path)) return void 0;
95
- const stat = lstatSync(path);
96
- return stat.isFile() ? stat.size : void 0;
97
- }
98
- /** `path.matchesGlob` never lets `*` or `**` cross a segment starting with a dot, so each path is also tried with those dots removed. */
99
- function isExcluded(file) {
100
- const undotted = file.replace(/(^|\/)\./gu, "$1");
101
- return SIZE_EXCLUSIONS.some(({ patterns }) => patterns.some((pattern) => matchesGlob(file, pattern) || matchesGlob(undotted, pattern)));
102
- }
103
- function isBinary(root, file, staged) {
104
- if (staged) return execFileSync("git", ["show", `:${file}`], {
105
- cwd: root,
106
- maxBuffer: fileBytes(root, file, true)
107
- }).subarray(0, 8e3).includes(0);
108
- const descriptor = openSync(join(root, file), "r");
109
- try {
110
- const prefix = Buffer.alloc(8e3);
111
- const length = readSync(descriptor, prefix, 0, prefix.length, 0);
112
- return prefix.subarray(0, length).includes(0);
113
- } finally {
114
- closeSync(descriptor);
115
- }
116
- }
117
- function authoredSizes(root, files = gitFiles(root), staged = false) {
118
- const sizes = /* @__PURE__ */ new Map();
119
- for (const input of files) {
120
- const file = repositoryPath(root, input);
121
- if (isExcluded(file)) continue;
122
- const size = fileBytes(root, file, staged);
123
- if (size !== void 0 && size > 2e4 && isBinary(root, file, staged)) continue;
124
- if (size !== void 0) sizes.set(file, size);
125
- }
126
- return sizes;
127
- }
128
- //#endregion
129
- export { runCli as a, repositoryRoot as i, gitFiles as n, repositoryPath as r, authoredSizes as t };