@antelopejs/tooling-configs 0.2.0 → 0.2.2

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
@@ -29,6 +29,8 @@ The AntelopeJS guardrail pack: shared lint, format and dead-code presets, the co
29
29
  | bin | `antelopejs-checks` | Runs every check and prints only the failures |
30
30
  | bin | `antelopejs-ratchet` | Fails when recorded debt grows; `--update` lowers the record |
31
31
  | bin | `antelopejs-check-size` | Fails on authored files over 20,000 bytes and on blanket disable directives |
32
+ | bin | `antelopejs-api-diff <base-ref>` | Compares published declaration bundles against committed HEAD |
33
+ | bin | `antelopejs-comment-proof <base-ref>` | Compares comment-free JavaScript emit against committed HEAD |
32
34
  | bin | `antelopejs-check-interface-ranges` | Fails on interface ranges that break module startup |
33
35
 
34
36
  ## Install
@@ -153,11 +155,11 @@ pnpm install
153
155
  | `.agents/setup` | Orb setup: switches to the `.nvmrc` Node, activates `packageManager` through corepack, installs with the frozen lockfile |
154
156
  | `AGENTS.md` | Writes the managed block between `<!-- antelopejs-guardrails:start -->` and `:end -->`; text outside the markers stays |
155
157
  | `CLAUDE.md` | `@AGENTS.md` |
156
- | `guardrails.ratchet.json` | Records the current findings, once, when the file is missing |
158
+ | `guardrails.ratchet.json` | Records non-empty debt on first adoption, before `sync:check` exists |
157
159
 
158
160
  `--profile template` writes only the three preset configs and a user-facing `AGENTS.md` block, for template repositories. `antelopejs-sync --check` writes nothing and exits 1 on any drift; `pnpm run checks` runs it.
159
161
 
160
- `pnpm run checks` runs `typecheck`, `lint`, `format:check`, `knip`, `test`, `size`, `ratchet` and `sync:check`, and prints only the output of the steps that failed.
162
+ `pnpm run checks` runs `typecheck`, `lint`, `format:check`, `knip`, `test`, `size` and `sync:check`, plus `ratchet` when `guardrails.ratchet.json` exists. It prints only the output of the steps that failed. Sync adds the ratchet scripts and pre-commit hook only while that file exists.
161
163
 
162
164
  ### Ratchet
163
165
 
@@ -174,9 +176,28 @@ pnpm install
174
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.
175
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.
176
178
 
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.
180
+
181
+ `antelopejs-sync --check` rejects an existing retired file and tells you to run `ratchet:update`. Plain sync reports the same note but leaves deletion to the update command.
182
+
177
183
  ### Size
178
184
 
179
- `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. Pass paths to check only those files, as the pre-commit hook does. It 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.
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.
186
+
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
+
189
+ ### Cleanup proof
190
+
191
+ A behaviour-preserving cleanup PR shows `antelopejs-api-diff <base-ref>` passing. It checks each tracked, non-private package with a `tsconfig.json`, using that package's installed TypeScript. It emits declarations without comments, resolves published entries from `exports` (preferring `types`), `main`, `types`/`typings` and `typesVersions`, and bundles each entry's local declaration closure. External packages stay external. Stable top-level ordering ignores declaration moves between internal files while preserving overload order. Compiler errors fail the proof.
192
+
193
+ A comment-only PR shows `antelopejs-comment-proof <base-ref>` passing instead of a verifier. It includes private packages, ignores type diagnostics and compares comment-free JavaScript emit byte for byte. Both commands exclude test sources, fail on added or removed outputs, and print unified diffs for changes.
194
+
195
+ Both commands compare **committed HEAD**, not staged or working-tree edits. They check out the base and HEAD in temporary detached worktrees, reuse the current checkout's root and package `node_modules`, and remove the worktrees afterwards. Install the consumer's dependencies before running them.
196
+
197
+ ```bash
198
+ pnpm exec antelopejs-api-diff origin/main
199
+ pnpm exec antelopejs-comment-proof origin/main
200
+ ```
180
201
 
181
202
  ## oxfmt
182
203
 
@@ -279,7 +300,7 @@ checks that remain the author's responsibility.
279
300
 
280
301
  ## Contributing
281
302
 
282
- See [CONTRIBUTING.md](./CONTRIBUTING.md). `pnpm install`, then `pnpm checks` runs the typecheck, lint, format check, tests, size check and ratchet.
303
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). `pnpm install`, then `pnpm checks` runs the typecheck, lint, format check, Knip, tests, size and sync checks. Repositories with recorded debt also run the ratchet.
283
304
 
284
305
  ## License
285
306
 
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,165 @@
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";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { generateDtsBundle } from "dts-bundle-generator";
7
+ //#region src/guardrails/api-diff.ts
8
+ function entrySegment(value) {
9
+ return encodeURIComponent(value).replaceAll(".", "%2E").replaceAll("*", "%2A");
10
+ }
11
+ function entryPattern(value) {
12
+ return value.split("*").map(entrySegment).join("*");
13
+ }
14
+ function exportTargets(target, entry = "exports/root") {
15
+ if (typeof target === "string") return [{
16
+ entry,
17
+ target
18
+ }];
19
+ if (target == null) return [];
20
+ if (Array.isArray(target)) return target.flatMap((value, index) => exportTargets(value, `${entry}/fallback/${index}`));
21
+ if (target.types !== void 0) return exportTargets(target.types, entry);
22
+ return Object.entries(target).flatMap(([key, value]) => {
23
+ return exportTargets(value, key.startsWith(".") ? key === "." ? "exports/root" : `exports/subpath/${entryPattern(key.slice(2))}` : `${entry}/condition/${entrySegment(key)}`);
24
+ });
25
+ }
26
+ function declarationTarget(target) {
27
+ return target.replace(/(?<!\.d)\.(mjs|cjs|js|mts|cts|tsx?|jsx)$/u, (extension) => {
28
+ if (extension === ".mjs" || extension === ".mts") return ".d.mts";
29
+ if (extension === ".cjs" || extension === ".cts") return ".d.cts";
30
+ return ".d.ts";
31
+ });
32
+ }
33
+ function entryPatterns(manifest) {
34
+ const targets = exportTargets(manifest.exports);
35
+ for (const key of [
36
+ "main",
37
+ "types",
38
+ "typings"
39
+ ]) {
40
+ const target = manifest[key];
41
+ if (target !== void 0) targets.push({
42
+ entry: key,
43
+ target
44
+ });
45
+ }
46
+ for (const [version, mappings] of Object.entries(manifest.typesVersions ?? {})) for (const [entry, paths] of Object.entries(mappings)) for (const [index, target] of paths.entries()) targets.push({
47
+ entry: `typesVersions/${entrySegment(version)}/${entryPattern(entry)}/${index}`,
48
+ target
49
+ });
50
+ return targets.map(({ entry, target }) => ({
51
+ entry,
52
+ target: declarationTarget(target)
53
+ })).filter(({ target }) => /\.d\.[cm]?ts$/u.test(target) || target.includes("*"));
54
+ }
55
+ /** Export stars are substitutions rather than path globs and can span directory separators. */
56
+ function targetPattern(target) {
57
+ return new RegExp(`^${target.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("(.*)")}$`, "u");
58
+ }
59
+ function publishedEntries(directory, manifest, emitted, scratch) {
60
+ const outputRoot = resolve(directory, emitted.options.outDir ?? ".");
61
+ const files = emitted.files.map((file) => relative(scratch, file).split("\\").join("/"));
62
+ const entries = [];
63
+ for (const { entry, target } of entryPatterns(manifest)) {
64
+ if (isTestSource(outputRoot, resolve(directory, target))) continue;
65
+ const path = relative(outputRoot, resolve(directory, target)).split("\\").join("/");
66
+ const pattern = targetPattern(path);
67
+ const matches = files.filter((file) => pattern.test(file));
68
+ if (!matches.length && !path.includes("*")) throw new Error(`${directory}: published target ${target} has no emitted declaration.`);
69
+ for (const file of matches) {
70
+ const captures = pattern.exec(file)?.slice(1) ?? [];
71
+ let index = 0;
72
+ const name = entry.includes("*") ? entry.replaceAll("*", () => entrySegment(captures[index++] ?? captures[0] ?? "")) : `${entry}${captures.length ? `/file/${entrySegment(file)}` : ""}`;
73
+ entries.push({
74
+ entry: `${name}.d.ts`,
75
+ file
76
+ });
77
+ }
78
+ }
79
+ return entries.sort((a, b) => a.entry.localeCompare(b.entry));
80
+ }
81
+ function statementName(compiler, statement, source) {
82
+ if (compiler.isVariableStatement(statement)) return statement.declarationList.declarations[0]?.name.getText(source) ?? "";
83
+ if (compiler.isFunctionDeclaration(statement) || compiler.isClassDeclaration(statement) || compiler.isInterfaceDeclaration(statement) || compiler.isTypeAliasDeclaration(statement) || compiler.isEnumDeclaration(statement) || compiler.isModuleDeclaration(statement) || compiler.isImportEqualsDeclaration(statement)) return statement.name?.getText(source) ?? "default";
84
+ return statement.getText(source);
85
+ }
86
+ function normalizeBundle(compiler, text) {
87
+ const source = compiler.createSourceFile("bundle.d.ts", text, compiler.ScriptTarget.Latest, true);
88
+ const statements = [...source.statements].sort((a, b) => statementName(compiler, a, source).localeCompare(statementName(compiler, b, source)));
89
+ const normalized = compiler.factory.updateSourceFile(source, statements);
90
+ return compiler.createPrinter({
91
+ removeComments: true,
92
+ newLine: compiler.NewLineKind.LineFeed
93
+ }).printFile(normalized);
94
+ }
95
+ function bundleDependencies(directory, scratch) {
96
+ let current = directory;
97
+ while (!existsSync(join(current, "node_modules"))) {
98
+ const parent = dirname(current);
99
+ if (parent === current) return;
100
+ current = parent;
101
+ }
102
+ symlinkSync(join(current, "node_modules"), join(scratch, "node_modules"), "dir");
103
+ }
104
+ /** dts-bundle-generator 9 remaps a .d.mts/.d.cts root twice; a source-named copy preserves its declaration text. */
105
+ function generatorEntry(file) {
106
+ const alias = file.replace(/\.d\.([cm]ts)$/u, ".$1");
107
+ if (alias !== file) copyFileSync(file, alias);
108
+ return alias;
109
+ }
110
+ function bundlePackage(directory, output, scratch, manifest) {
111
+ const emitted = emitPackage(directory, scratch, true);
112
+ const entries = publishedEntries(directory, manifest, emitted, scratch);
113
+ if (!entries.length) return 0;
114
+ bundleDependencies(directory, scratch);
115
+ writeFileSync(join(scratch, "package.json"), readFileSync(join(directory, "package.json")));
116
+ const config = join(scratch, "tsconfig.json");
117
+ writeFileSync(config, JSON.stringify({
118
+ extends: join(directory, "tsconfig.json"),
119
+ compilerOptions: {
120
+ composite: false,
121
+ incremental: false,
122
+ noEmit: false,
123
+ rootDir: scratch
124
+ },
125
+ files: entries.map(({ file }) => join(scratch, file)),
126
+ include: []
127
+ }));
128
+ const bundles = generateDtsBundle(entries.map(({ file }) => ({
129
+ filePath: generatorEntry(join(scratch, file)),
130
+ output: {
131
+ exportReferencedTypes: false,
132
+ noBanner: true
133
+ },
134
+ libraries: { inlinedLibraries: [] }
135
+ })), { preferredConfigPath: config });
136
+ for (const [index, { entry }] of entries.entries()) {
137
+ const path = join(output, entry);
138
+ mkdirSync(dirname(path), { recursive: true });
139
+ writeFileSync(path, normalizeBundle(emitted.compiler, bundles[index]));
140
+ }
141
+ return entries.length;
142
+ }
143
+ /** Proves published declarations match between a base commit and committed HEAD. */
144
+ function apiDiff(root, baseRef) {
145
+ return compareRevisions(root, baseRef, "API", (worktree, output, scratch) => {
146
+ const packages = packagesAt(worktree, true);
147
+ let entries = 0;
148
+ for (const { directory, manifest } of packages) entries += bundlePackage(join(worktree, directory), join(output, directory === "." ? "_root" : directory), join(scratch, directory === "." ? "_root" : directory), manifest);
149
+ return {
150
+ packages: packages.map(({ directory }) => directory),
151
+ entries
152
+ };
153
+ });
154
+ }
155
+ //#endregion
156
+ //#region src/bin/api-diff.ts
157
+ runCli(() => {
158
+ 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]);
161
+ process.stdout.write(result.output);
162
+ return result.different ? 1 : 0;
163
+ });
164
+ //#endregion
165
+ export {};
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as repositoryRoot, i as repositoryPath, o as runCli, t as authoredSizes } from "../files-BSl0U8lT.mjs";
3
- import { t as checkSize } from "../size-DRCgYCLA.mjs";
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";
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,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as repositoryRoot, o as runCli } from "../files-BSl0U8lT.mjs";
3
- import { i as parseManifest, r as packageManager } from "../package-B1aipTbn.mjs";
4
- import { readFileSync } from "node:fs";
2
+ import "../ceilings-BSw9p4YB.mjs";
3
+ import { a as runCli, i as repositoryRoot } from "../files-DAD5OtfV.mjs";
4
+ import { i as parseManifest, r as packageManager } from "../package-Ckd7e4tA.mjs";
5
+ import { existsSync, readFileSync } from "node:fs";
5
6
  import { join } from "node:path";
6
7
  import { spawnSync } from "node:child_process";
7
8
  //#region src/guardrails/checks.ts
@@ -19,7 +20,8 @@ function runChecks(root) {
19
20
  const manifest = parseManifest(readFileSync(join(root, "package.json"), "utf8"));
20
21
  const manager = packageManager(manifest);
21
22
  const failures = [];
22
- for (const step of CHECK_STEPS) {
23
+ const steps = CHECK_STEPS.filter((step) => step !== "ratchet" || existsSync(join(root, "guardrails.ratchet.json")));
24
+ for (const step of steps) {
23
25
  if (!manifest.scripts?.[step]) {
24
26
  failures.push(`[${step}] Missing package.json script "${step}". Run antelopejs-sync.\n`);
25
27
  continue;
@@ -37,7 +39,7 @@ function runChecks(root) {
37
39
  }
38
40
  return {
39
41
  failed: failures.length,
40
- output: `${failures.join("\n")}Checks: ${CHECK_STEPS.length - failures.length} passed, ${failures.length} failed.\n`
42
+ output: `${failures.join("\n")}Checks: ${steps.length - failures.length} passed, ${failures.length} failed.\n`
41
43
  };
42
44
  }
43
45
  //#endregion
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,39 @@
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";
4
+ import { copyFileSync, mkdirSync } from "node:fs";
5
+ import { dirname, join, relative } from "node:path";
6
+ //#region src/guardrails/comment-proof.ts
7
+ /** Proves comment-free JavaScript emit matches, including private packages and excluding tests. */
8
+ function commentProof(root, baseRef) {
9
+ return compareRevisions(root, baseRef, "Comment proof", (worktree, output, scratch) => {
10
+ const packages = packagesAt(worktree, false);
11
+ let entries = 0;
12
+ for (const { directory } of packages) {
13
+ const key = directory === "." ? "_root" : directory;
14
+ const emittedRoot = join(scratch, key);
15
+ const emitted = emitPackage(join(worktree, directory), emittedRoot, false);
16
+ for (const file of emitted.files) {
17
+ const path = join(output, key, relative(emittedRoot, file));
18
+ mkdirSync(dirname(path), { recursive: true });
19
+ copyFileSync(file, path);
20
+ entries++;
21
+ }
22
+ }
23
+ return {
24
+ packages: packages.map(({ directory }) => directory),
25
+ entries
26
+ };
27
+ });
28
+ }
29
+ //#endregion
30
+ //#region src/bin/comment-proof.ts
31
+ runCli(() => {
32
+ const args = process.argv.slice(2);
33
+ if (args.length !== 1) throw new Error("Usage: antelopejs-comment-proof <base-ref>");
34
+ const result = commentProof(repositoryRoot(), args[0]);
35
+ process.stdout.write(result.output);
36
+ return result.different ? 1 : 0;
37
+ });
38
+ //#endregion
39
+ export {};
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { o as readCeilings } from "../ceilings--pJmhSde.mjs";
3
- import { a as repositoryRoot, i as repositoryPath, o as runCli } from "../files-BSl0U8lT.mjs";
4
- import { n as ratchet, r as lintDiagnostics } from "../ratchet-DNsVZH9s.mjs";
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";
5
5
  import { resolve } from "node:path";
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { parseArgs } from "node:util";
@@ -55,8 +55,10 @@ runCli(() => {
55
55
  if (positionals.length && !values.staged) throw new Error("File arguments require --staged.");
56
56
  const result = values.staged ? {
57
57
  failures: stagedRatchet(root, positionals.length ? positionals.map((file) => repositoryPath(root, resolve(file))) : void 0),
58
- lowered: []
58
+ lowered: [],
59
+ retired: false
59
60
  } : ratchet(root, values.update);
61
+ if (result.retired) process.stdout.write(`Deleted ${CEILINGS_FILE}: no debt left. Run antelopejs-sync.\n`);
60
62
  for (const message of result.lowered) process.stdout.write(`Lowered ${message}\n`);
61
63
  for (const message of result.failures) process.stderr.write(`${message}\n`);
62
64
  if (!result.failures.length) process.stdout.write("Ratchet: within ceilings.\n");
package/dist/bin/sync.mjs CHANGED
@@ -1,8 +1,8 @@
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-BSl0U8lT.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-DNsVZH9s.mjs";
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";
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";
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
8
  import { parseArgs } from "node:util";
@@ -20,6 +20,9 @@ Hold every test you write or keep to this checklist:
20
20
  - Name the wrong implementation this test fails on. A test with no answer is deleted, not kept.
21
21
  - No test reads the file it tests. Drive the behaviour through the public interface.
22
22
  - An invariant over many files is a lint rule, not a test.`;
23
+ const CLEANUP_PROOF = `### Cleanup proof
24
+
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.`;
23
26
  const SHARED_RULES = [
24
27
  ["Formatting", "oxfmt"],
25
28
  ["Lint findings are errors; no rule warns", "oxlint preset"],
@@ -28,11 +31,12 @@ const SHARED_RULES = [
28
31
  ["Tests read no files outside fixtures, helpers, locales", "`antelopejs/no-fs-read-in-tests`"],
29
32
  ["No assertion that restates a constant", "`antelopejs/no-constant-value-assertion`"],
30
33
  ["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`"]
34
+ ["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>`"]
32
37
  ];
33
38
  const FULL_RULES = [
34
39
  ["Files ≤ 20,000 bytes, tests included", "`antelopejs-check-size` (pre-commit)"],
35
- ["Existing debt only shrinks", "`antelopejs-ratchet`"],
36
40
  ["Types check", "`tsc` (pre-push)"],
37
41
  ["No dead files, exports, or dependencies", "knip"],
38
42
  ["Managed files match the pack", "`antelopejs-sync --check`"],
@@ -62,7 +66,7 @@ function fullBody(context) {
62
66
  return [
63
67
  "## Guardrails",
64
68
  "",
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.",
69
+ `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
70
  "",
67
71
  ...pointers(context).flatMap((line) => [line, ""]),
68
72
  "The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks.",
@@ -71,11 +75,19 @@ function fullBody(context) {
71
75
  "",
72
76
  TESTS,
73
77
  "",
74
- "### Debt",
75
- "",
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.",
78
+ CLEANUP_PROOF,
77
79
  "",
78
- ruleTable([...SHARED_RULES, ...FULL_RULES])
80
+ ...context.ratchet ? [
81
+ "### Debt",
82
+ "",
83
+ "`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.",
84
+ ""
85
+ ] : [],
86
+ ruleTable([
87
+ ...SHARED_RULES,
88
+ ...FULL_RULES,
89
+ ...context.ratchet ? [["Existing debt only shrinks", "`antelopejs-ratchet`"]] : []
90
+ ])
79
91
  ];
80
92
  }
81
93
  function templateBody(context) {
@@ -89,6 +101,8 @@ function templateBody(context) {
89
101
  "",
90
102
  TESTS,
91
103
  "",
104
+ CLEANUP_PROOF,
105
+ "",
92
106
  ruleTable(SHARED_RULES)
93
107
  ];
94
108
  }
@@ -140,7 +154,7 @@ function presetConfig(current, preset, tools) {
140
154
  const importLine = preset.content.split("\n").filter((line) => line.includes(preset.specifier)).join("\n").replace(preset.specifier, target);
141
155
  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
156
  }
143
- function lefthook(current, manager, tools) {
157
+ function lefthook(current, manager, tools, ratchet = true) {
144
158
  const exec = execCommand(manager);
145
159
  const document = parseDocument(current ?? "{}\n");
146
160
  if (document.errors.length) throw new Error(`lefthook.yml: ${document.errors.map(({ message }) => message).join("; ")}`);
@@ -176,6 +190,11 @@ function lefthook(current, manager, tools) {
176
190
  key
177
191
  ], document.createNode(command));
178
192
  }
193
+ if (!ratchet) document.deleteIn([
194
+ "pre-commit",
195
+ "commands",
196
+ "antelopejs-ratchet"
197
+ ]);
179
198
  return document.toString({ lineWidth: 0 });
180
199
  }
181
200
  const SETUP_SCRIPT = `#!/usr/bin/env bash
@@ -241,6 +260,7 @@ function hasChecksWorkflow(root) {
241
260
  //#endregion
242
261
  //#region src/guardrails/sync.ts
243
262
  const LEGACY_COMMENT_INSTRUCTION = "- Explain non-obvious constraints and decisions in comments; avoid restating the code. Document public contracts with TSDoc.";
263
+ const RETIRED_RATCHET = `${CEILINGS_FILE} has no debt left. Run ratchet:update to delete it, then run antelopejs-sync.`;
244
264
  function managedAgents(current = "", context) {
245
265
  const start = current.indexOf(AGENTS_BLOCK_START);
246
266
  const end = current.indexOf(AGENTS_BLOCK_END);
@@ -273,7 +293,7 @@ const MANAGED_ARTIFACTS = [
273
293
  path: "lefthook.yml",
274
294
  profiles: ["full"],
275
295
  desired: (current, context) => ({
276
- content: lefthook(current, context.manager, context.tools),
296
+ content: lefthook(current, context.manager, context.tools, context.ratchet),
277
297
  notes: []
278
298
  })
279
299
  },
@@ -333,6 +353,7 @@ function planSync(current, modes, context) {
333
353
  });
334
354
  for (const artifact of MANAGED_ARTIFACTS) {
335
355
  if (!artifact.profiles.includes(context.profile)) continue;
356
+ if (artifact.path === "guardrails.ratchet.json" && !context.ratchet) continue;
336
357
  try {
337
358
  const { content, notes } = artifact.desired(current.get(artifact.path), context);
338
359
  const changed = current.get(artifact.path) !== content || artifact.mode !== void 0 && (artifact.mode & 73) !== 0 && ((modes.get(artifact.path) ?? 0) & 73) === 0;
@@ -407,6 +428,7 @@ function repositoryContext(root, current, profile) {
407
428
  const desiredLint = current.get(lint.path) ?? lint.content;
408
429
  return {
409
430
  profile,
431
+ ratchet: current.has(CEILINGS_FILE),
410
432
  manager: packageManager(manifest),
411
433
  versions: toolingVersions(),
412
434
  tools: repositoryTools(manifest),
@@ -427,6 +449,13 @@ function writeArtifacts(root, artifacts) {
427
449
  if (artifact.mode !== void 0) chmodSync(path, artifact.mode);
428
450
  }
429
451
  }
452
+ function repositoryNotices(root, context) {
453
+ const notices = [];
454
+ if (context.profile === "full" && !hasChecksWorkflow(root)) notices.push(MISSING_CHECKS_WORKFLOW);
455
+ if (context.ratchet && isRetired(readCeilings(root))) notices.push(RETIRED_RATCHET);
456
+ return notices;
457
+ }
458
+ /** Measures debt only on first adoption, before sync has written the sync:check script; retired repositories stay retired. */
430
459
  function syncRepository(root, options = {}) {
431
460
  const profile = options.profile ?? "full";
432
461
  const { current, modes } = repositorySnapshot(root);
@@ -435,14 +464,13 @@ function syncRepository(root, options = {}) {
435
464
  let plan = planSync(current, modes, context);
436
465
  const errors = plan.filter((artifact) => artifact.kind === "error").map(({ message }) => message);
437
466
  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")) {
467
+ (options.check ? errors : notes).push(...repositoryNotices(root, context));
468
+ if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json") && manifest.scripts?.["sync:check"] === void 0) {
443
469
  const lint = PRESET_CONFIGS[0];
444
470
  const desired = presetConfig(current.get(lint.path), lint, context.tools);
445
- context.baseline = formatCeilings(measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired));
471
+ const baseline = measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired);
472
+ context.ratchet = !isRetired(baseline);
473
+ context.baseline = formatCeilings(baseline);
446
474
  plan = planSync(current, modes, context);
447
475
  }
448
476
  const writes = plan.filter((artifact) => artifact.kind === "write" || artifact.kind === "migrated");
@@ -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 };
@@ -1,5 +1,6 @@
1
+ import "./ceilings-BSw9p4YB.mjs";
1
2
  import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "./shared-NC0T8ykm.mjs";
2
- import { existsSync, lstatSync } from "node:fs";
3
+ import { closeSync, existsSync, lstatSync, openSync, readSync } from "node:fs";
3
4
  import { join, matchesGlob, relative, resolve } from "node:path";
4
5
  import { execFileSync, spawnSync } from "node:child_process";
5
6
  //#region src/guardrails/cli.ts
@@ -28,6 +29,16 @@ const SIZE_EXCLUSIONS = [
28
29
  kind: "fixtures",
29
30
  patterns: ["**/fixtures/**", "**/__fixtures__/**"]
30
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
+ },
31
42
  {
32
43
  kind: "lockfiles",
33
44
  patterns: [
@@ -84,15 +95,35 @@ function fileBytes(root, file, staged = false) {
84
95
  const stat = lstatSync(path);
85
96
  return stat.isFile() ? stat.size : void 0;
86
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
+ }
87
117
  function authoredSizes(root, files = gitFiles(root), staged = false) {
88
118
  const sizes = /* @__PURE__ */ new Map();
89
119
  for (const input of files) {
90
120
  const file = repositoryPath(root, input);
91
- if (SIZE_EXCLUSIONS.some(({ patterns }) => patterns.some((pattern) => matchesGlob(file, pattern)))) continue;
121
+ if (isExcluded(file)) continue;
92
122
  const size = fileBytes(root, file, staged);
123
+ if (size !== void 0 && size > 2e4 && isBinary(root, file, staged)) continue;
93
124
  if (size !== void 0) sizes.set(file, size);
94
125
  }
95
126
  return sizes;
96
127
  }
97
128
  //#endregion
98
- export { repositoryRoot as a, repositoryPath as i, fileBytes as n, runCli as o, gitFiles as r, authoredSizes as t };
129
+ export { runCli as a, repositoryRoot as i, gitFiles as n, repositoryPath as r, authoredSizes as t };
package/dist/oxc/lint.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as findGitRoot, o as readCeilings } from "../ceilings--pJmhSde.mjs";
1
+ import { i as findGitRoot, s as readCeilings } from "../ceilings-BSw9p4YB.mjs";
2
2
  import { n as TEST_FILE_GLOBS, t as ASSERTION_FUNCTION_NAMES } from "../shared-WC0z3y9F.mjs";
3
3
  import { n as IGNORE_PATTERNS, t as AGENT_IGNORE_PATTERNS } from "../shared-NC0T8ykm.mjs";
4
4
  import { defineConfig } from "oxlint";
@@ -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;
@@ -1,8 +1,8 @@
1
- import { a as formatCeilings, n as MAX_FILE_BYTES, o as readCeilings, r as MEASURE_ENV, t as CEILINGS_FILE } from "./ceilings--pJmhSde.mjs";
2
- import { i as repositoryPath, n as fileBytes, r as gitFiles, t as authoredSizes } from "./files-BSl0U8lT.mjs";
3
- import { n as sizeFailure } from "./size-DRCgYCLA.mjs";
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";
4
4
  import { createRequire } from "node:module";
5
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { basename, dirname, join } from "node:path";
7
7
  import { execFileSync, spawnSync } from "node:child_process";
8
8
  //#region src/guardrails/oxlint.ts
@@ -77,11 +77,12 @@ function lintDiagnostics(root, config, files) {
77
77
  function baseline(root, diagnostics) {
78
78
  const rules = {};
79
79
  const oversizeFiles = {};
80
+ const sizes = authoredSizes(root);
80
81
  for (const diagnostic of diagnostics) if (diagnostic.rule === "eslint/max-lines") {
81
- const size = fileBytes(root, diagnostic.file);
82
+ const size = sizes.get(diagnostic.file);
82
83
  if (size !== void 0) oversizeFiles[diagnostic.file] = size;
83
84
  } else rules[diagnostic.rule] = (rules[diagnostic.rule] ?? 0) + 1;
84
- for (const [file, size] of authoredSizes(root)) if (size > 2e4) oversizeFiles[file] = size;
85
+ for (const [file, size] of sizes) if (size > 2e4) oversizeFiles[file] = size;
85
86
  return {
86
87
  rules,
87
88
  oversizeFiles
@@ -154,18 +155,22 @@ function ratchet(root, update = false) {
154
155
  if (shown.length < matches.length) failures.push(` ...and ${matches.length - shown.length} recorded in files this change does not touch.`);
155
156
  }
156
157
  for (const [file, size] of Object.entries(current.oversizeFiles)) if (ceilings.oversizeFiles[file] === void 0) failures.push(size > 2e4 ? sizeFailure(file, size, MAX_FILE_BYTES) : `${file}: ${size} bytes and an eslint/max-lines diagnostic; this file has no oversize allowance. Split by responsibility into sibling modules, or split tests by behaviour under test, one file per describe.`);
158
+ const sizes = authoredSizes(root, Object.keys(ceilings.oversizeFiles));
157
159
  for (const [file, limit] of Object.entries(ceilings.oversizeFiles)) {
158
- const size = fileBytes(root, file);
160
+ const size = sizes.get(file);
159
161
  if (size !== void 0 && size > limit) failures.push(sizeFailure(file, size, limit));
160
162
  }
161
163
  const lowered = update ? lowerCeilings(ceilings, current) : [];
164
+ const retired = update && isRetired(ceilings);
162
165
  if (update) {
163
166
  const path = join(root, CEILINGS_FILE);
164
- writeFileSync(path, formatCeilings(ceilings));
167
+ if (retired) unlinkSync(path);
168
+ else writeFileSync(path, formatCeilings(ceilings));
165
169
  }
166
170
  return {
167
171
  failures,
168
- lowered
172
+ lowered,
173
+ retired
169
174
  };
170
175
  }
171
176
  //#endregion
@@ -1,5 +1,5 @@
1
- import { o as readCeilings } from "./ceilings--pJmhSde.mjs";
2
- import { t as authoredSizes } from "./files-BSl0U8lT.mjs";
1
+ import { s as readCeilings } from "./ceilings-BSw9p4YB.mjs";
2
+ import { t as authoredSizes } from "./files-DAD5OtfV.mjs";
3
3
  //#region src/guardrails/size.ts
4
4
  function sizeFailure(file, size, limit) {
5
5
  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.`;
@@ -0,0 +1,202 @@
1
+ import { n as TEST_FILE_GLOBS } from "./shared-WC0z3y9F.mjs";
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
104
+ //#region src/guardrails/typescript.ts
105
+ function packagesAt(root, publishedOnly) {
106
+ return trackedManifests(root).flatMap((file) => {
107
+ const directory = dirname(file);
108
+ if (!existsSync(join(root, directory, "tsconfig.json"))) return [];
109
+ const manifest = JSON.parse(readFileSync(join(root, file), "utf8"));
110
+ if (publishedOnly && manifest.private === true) return [];
111
+ return [{
112
+ directory,
113
+ manifest
114
+ }];
115
+ });
116
+ }
117
+ /** Loads the consumer's compiler, including a package-local installation in a workspace. */
118
+ function loadTypeScript(directory) {
119
+ return createRequire(join(directory, "package.json"))("typescript");
120
+ }
121
+ function isTestSource(root, file) {
122
+ const path = relative(root, file).split("\\").join("/");
123
+ return TEST_FILE_GLOBS.some((pattern) => matchesGlob(path, pattern));
124
+ }
125
+ function failDiagnostics(compiler, diagnostics) {
126
+ const errors = diagnostics.filter((item) => item.category === compiler.DiagnosticCategory.Error);
127
+ if (errors.length) throw new Error(compiler.formatDiagnostics(errors, {
128
+ getCanonicalFileName: (file) => file,
129
+ getCurrentDirectory: () => process.cwd(),
130
+ getNewLine: () => "\n"
131
+ }));
132
+ }
133
+ /** Declaration inputs are part of the local closure but TypeScript does not emit copies of them. */
134
+ function copyDeclarations(program, directory, output) {
135
+ const sources = program.getSourceFiles().filter((source) => !program.isSourceFileDefaultLibrary(source) && !program.isSourceFileFromExternalLibrary(source) && !relative(directory, source.fileName).startsWith(`..${sep}`) && !isTestSource(directory, source.fileName));
136
+ const authored = sources.filter((source) => !source.isDeclarationFile);
137
+ let root = dirname((authored[0] ?? sources[0])?.fileName ?? join(directory, "index.ts"));
138
+ for (const source of authored.length ? authored : sources) while (relative(root, source.fileName).startsWith(`..${sep}`)) root = dirname(root);
139
+ root = program.getCompilerOptions().rootDir ?? root;
140
+ const files = [];
141
+ for (const source of sources) {
142
+ if (!source.isDeclarationFile) continue;
143
+ const path = relative(root, source.fileName);
144
+ if (path.startsWith(`..${sep}`)) continue;
145
+ const target = join(output, path);
146
+ mkdirSync(dirname(target), { recursive: true });
147
+ writeFileSync(target, source.text);
148
+ files.push(target);
149
+ }
150
+ return files;
151
+ }
152
+ /** Emits with the package's compiler and config; test roots and outputs never enter either proof. */
153
+ function emitPackage(directory, output, declarations) {
154
+ const compiler = loadTypeScript(directory);
155
+ const configPath = join(directory, "tsconfig.json");
156
+ const config = compiler.readConfigFile(configPath, compiler.sys.readFile);
157
+ if (config.error) failDiagnostics(compiler, [config.error]);
158
+ const parsed = compiler.parseJsonConfigFileContent(config.config, compiler.sys, directory);
159
+ failDiagnostics(compiler, parsed.errors);
160
+ const options = {
161
+ ...parsed.options,
162
+ rootDir: parsed.options.rootDir ?? (parsed.options.outDir ? void 0 : directory),
163
+ outDir: output,
164
+ declarationDir: declarations ? output : void 0,
165
+ declaration: declarations,
166
+ emitDeclarationOnly: declarations,
167
+ removeComments: true,
168
+ declarationMap: false,
169
+ sourceMap: false,
170
+ inlineSourceMap: false,
171
+ noEmit: false,
172
+ noEmitOnError: false,
173
+ composite: false,
174
+ incremental: false,
175
+ tsBuildInfoFile: void 0,
176
+ outFile: void 0,
177
+ allowImportingTsExtensions: declarations ? parsed.options.allowImportingTsExtensions : false
178
+ };
179
+ const program = compiler.createProgram({
180
+ rootNames: parsed.fileNames.filter((file) => !isTestSource(directory, file)),
181
+ options,
182
+ projectReferences: parsed.projectReferences
183
+ });
184
+ if (declarations) failDiagnostics(compiler, compiler.getPreEmitDiagnostics(program));
185
+ const files = [];
186
+ const result = program.emit(void 0, (file, text, _bom, _onError, sources) => {
187
+ if (sources?.some((source) => isTestSource(directory, source.fileName))) return;
188
+ if (declarations ? !/\.d\.[cm]?ts$/.test(file) : !/\.[cm]?jsx?$/.test(file)) return;
189
+ mkdirSync(dirname(file), { recursive: true });
190
+ writeFileSync(file, text);
191
+ files.push(file);
192
+ });
193
+ if (declarations) failDiagnostics(compiler, result.diagnostics);
194
+ if (declarations) files.push(...copyDeclarations(program, directory, output));
195
+ return {
196
+ compiler,
197
+ files,
198
+ options: parsed.options
199
+ };
200
+ }
201
+ //#endregion
202
+ export { compareRevisions as i, isTestSource as n, packagesAt 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.0",
3
+ "version": "0.2.2",
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",
@@ -21,9 +21,11 @@
21
21
  "url": "git+https://github.com/AntelopeJS/tooling-configs.git"
22
22
  },
23
23
  "bin": {
24
+ "antelopejs-api-diff": "./dist/bin/api-diff.mjs",
24
25
  "antelopejs-check-interface-ranges": "./dist/bin/check-interface-ranges.mjs",
25
26
  "antelopejs-check-size": "./dist/bin/check-size.mjs",
26
27
  "antelopejs-checks": "./dist/bin/checks.mjs",
28
+ "antelopejs-comment-proof": "./dist/bin/comment-proof.mjs",
27
29
  "antelopejs-ratchet": "./dist/bin/ratchet.mjs",
28
30
  "antelopejs-sync": "./dist/bin/sync.mjs"
29
31
  },
@@ -55,15 +57,14 @@
55
57
  "checks": "node src/bin/checks.ts",
56
58
  "format:check": "oxfmt --check .",
57
59
  "size": "node src/bin/check-size.ts",
58
- "ratchet": "node src/bin/ratchet.ts",
59
60
  "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
60
61
  "sync": "node src/bin/sync.ts",
61
- "ratchet:update": "node src/bin/ratchet.ts --update",
62
62
  "sync:check": "node src/bin/sync.ts --check",
63
63
  "knip": "knip"
64
64
  },
65
65
  "dependencies": {
66
66
  "@oxlint/plugins": "1.81.0",
67
+ "dts-bundle-generator": "^9.5.1",
67
68
  "yaml": "^2.9.1"
68
69
  },
69
70
  "devDependencies": {