@warpgogol/forge 4.2.1 → 4.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.
Files changed (52) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +2 -0
  3. package/README.uk.md +2 -0
  4. package/bin/cli.ts +8 -0
  5. package/dist/bin/cli.js +8 -0
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/os/compass/compass.module.d.ts.map +1 -1
  8. package/dist/os/compass/compass.module.js +9 -4
  9. package/dist/os/compass/compass.module.js.map +1 -1
  10. package/dist/os/compass/handlers/compass-audit-handler.d.ts +1 -0
  11. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  12. package/dist/os/compass/handlers/compass-audit-handler.js +3 -0
  13. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-change-summary-handler.d.ts.map +1 -1
  15. package/dist/os/compass/handlers/compass-change-summary-handler.js +36 -6
  16. package/dist/os/compass/handlers/compass-change-summary-handler.js.map +1 -1
  17. package/dist/os/compass/handlers/compass-inventory-handler.d.ts +2 -1
  18. package/dist/os/compass/handlers/compass-inventory-handler.d.ts.map +1 -1
  19. package/dist/os/compass/handlers/compass-inventory-handler.js +54 -7
  20. package/dist/os/compass/handlers/compass-inventory-handler.js.map +1 -1
  21. package/dist/os/compass/handlers/compass-inventory.d.ts +17 -0
  22. package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
  23. package/dist/os/compass/handlers/compass-inventory.js +277 -12
  24. package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
  25. package/dist/os/core/core.module.d.ts.map +1 -1
  26. package/dist/os/core/core.module.js +26 -0
  27. package/dist/os/core/core.module.js.map +1 -1
  28. package/dist/os/core/handlers/file-size-lint.d.ts +8 -0
  29. package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -0
  30. package/dist/os/core/handlers/file-size-lint.js +166 -0
  31. package/dist/os/core/handlers/file-size-lint.js.map +1 -0
  32. package/dist/os/core/handlers/public-surface.d.ts.map +1 -1
  33. package/dist/os/core/handlers/public-surface.js +4 -3
  34. package/dist/os/core/handlers/public-surface.js.map +1 -1
  35. package/os/compass/compass.module.ts +12 -7
  36. package/os/compass/handlers/compass-audit-handler.ts +4 -0
  37. package/os/compass/handlers/compass-change-summary-handler.ts +45 -6
  38. package/os/compass/handlers/compass-inventory-handler.ts +78 -9
  39. package/os/compass/handlers/compass-inventory.ts +339 -13
  40. package/os/compass/handlers/tests/compass-v2-contract.test.ts +239 -0
  41. package/os/core/core.module.ts +28 -0
  42. package/os/core/handlers/file-size-lint.ts +200 -0
  43. package/os/core/handlers/public-surface.ts +4 -3
  44. package/package.json +12 -12
  45. package/skills/fo/fo-compass-annotate/SKILL.md +13 -11
  46. package/skills/fo/fo-compass-annotate/templates/audit-prompt.md +7 -1
  47. package/skills/fo/fo-compass-annotate/templates/header-format.md +33 -11
  48. package/skills/fo/fo-compass-annotate/templates/header-prompt.md +12 -1
  49. package/src/tests/create.test.ts +17 -17
  50. package/src/tests/file-size-lint.test.ts +181 -0
  51. package/src/tests/public-surface.test.ts +13 -4
  52. package/src/tests/scaffold-project.test.ts +1 -1
@@ -1,13 +1,16 @@
1
- # Canonical Compass Header Format
1
+ # Canonical Compass Header Format (v2, RFC-1094)
2
2
 
3
3
  This is the canonical format for Compass source headers. Check the project's invariants file for the canonical Compass markup rule.
4
4
 
5
- ## Two-block contract
5
+ ## Three-block contract
6
6
 
7
- Every non-trivial source file in `apps/`, `packages/`, or `services/` must carry:
7
+ Every non-trivial source file in `apps/`, `packages/`, or `services/` must carry, in this canonical order:
8
8
 
9
9
  1. `MODULE_CONTRACT` — describes the file's purpose and boundaries.
10
- 2. `CHANGE_SUMMARY` — chronological list of changes with RFC/ticket references.
10
+ 2. `KEY_DECISIONS` — current-state design truths (required on medium/high riskClass files, optional on low).
11
+ 3. `CHANGE_SUMMARY` — compressed change history: the 5 newest governance-ID items plus a `<history>` ID tail.
12
+
13
+ Blocks out of this order are a `COMPASS-ORDER-01` violation.
11
14
 
12
15
  ## MODULE_CONTRACT
13
16
 
@@ -22,24 +25,42 @@ Every non-trivial source file in `apps/`, `packages/`, or `services/` must carry
22
25
 
23
26
  ### Rules
24
27
 
25
- - `<purpose>` must be specific to this file, under 100 characters.
28
+ - `<purpose>` must be specific to this file, under 100 characters, and contain at least one file-derived token (filename stem segment, parent-directory segment for generic stems, or an exported symbol name). Boilerplate openers ("This file…", "Utility…", "TODO…") are `COMPASS-PURPOSE-01`; a purpose with no file-derived token is `COMPASS-PURPOSE-02`.
26
29
  - `<non-goals>` must have at least one `<item>`.
27
30
  - Do not include `@ai-invariant` lines in `MODULE_CONTRACT` — they are inline comments in the file body.
28
31
 
32
+ ## KEY_DECISIONS
33
+
34
+ ```
35
+ <KEY_DECISIONS>
36
+ <item>Validation is deterministic regex parsing — no LLM in the check path.</item>
37
+ <item>Warnings never fail the build during the migration window.</item>
38
+ </KEY_DECISIONS>
39
+ ```
40
+
41
+ ### Rules
42
+
43
+ - Content: current-state design truths of the file — invariants, chosen mechanisms, forbidden approaches. Rewritten in place when behavior changes; never appended chronologically.
44
+ - Required on files whose `riskClass` is `medium` or `high` (`COMPASS-KD-01`); optional on `low`. When present on a low-risk file, all item rules still apply.
45
+ - Limits: 1–7 `<item>` entries (`COMPASS-KD-04`), each ≤ 20 words (`COMPASS-KD-03`).
46
+ - An item MUST NOT begin with a governance-ID prefix (`RFC-`, `ADR-`, ticket pattern) — history belongs in `CHANGE_SUMMARY` (`COMPASS-KD-05`).
47
+ - An empty block or `TODO` placeholder items are a violation (`COMPASS-KD-02`).
48
+
29
49
  ## CHANGE_SUMMARY
30
50
 
31
51
  ```
32
52
  <CHANGE_SUMMARY>
33
- <item>RFC-XXXX: one-line description of the change.</item>
53
+ <item>RFC-1094: adopted v2 three-block contract.</item>
54
+ <history>RFC-0348, RFC-0349, RFC-0538</history>
34
55
  </CHANGE_SUMMARY>
35
56
  ```
36
57
 
37
58
  ### Rules
38
59
 
39
- - Each `<item>` should reference an RFC id, ticket id, or ADR id (protected items).
40
- - Items without an RFC/ticket reference are "unprotected" and may be trimmed by `compass.summary.trim`.
41
- - Maximum 30 total items per file (enforced by `compass.changesummary.validate`).
42
- - Boilerplate items (e.g., "Initial creation", "Compass scaffolding") are automatically removed by `compass.summary.trim`.
60
+ - At most 5 `<item>` entries, each `ID: one-line description` (`COMPASS-CS-05`). The retained items are the 5 newest — the chronological tail.
61
+ - Every `<item>` MUST carry a governance ID (`RFC-XXXX`, `ADR-XXXX`, ticket pattern); items without an ID are violations (`COMPASS-CS-06`) and will be deleted by `compass.migrate` (sibling RFC, not yet implemented).
62
+ - Older IDs collapse into a single `<history>` element: comma-separated, deduplicated, per-namespace ascending numeric order (`COMPASS-CS-07`). `<history>` carries IDs only — no descriptions — and has no length cap.
63
+ - An empty `CHANGE_SUMMARY` (no items, no history) is legal for files never touched by a governance-referencing commit.
43
64
 
44
65
  ## Comment syntax by extension
45
66
 
@@ -47,8 +68,9 @@ See `reference/comment-styles.md` for the correct comment syntax per file extens
47
68
 
48
69
  ## Forbidden blocks
49
70
 
50
- The following legacy Compass blocks are forbidden and must be removed:
71
+ The following legacy Compass blocks are forbidden and must be removed (`COMPASS-FORBIDDEN-01`, always an error regardless of `--mode`):
51
72
 
73
+ - `MODULE_MAP`, `keywords`, `responsibilities`, `COMPASS_BLOCK` — removed by RFC-0348.
52
74
  - `GRACE_MODULE_CONTRACT` — use `MODULE_CONTRACT` instead.
53
75
  - `GRACE_CHANGE_SUMMARY` — use `CHANGE_SUMMARY` instead.
54
76
  - `AI_INVARIANTS` — use inline `// @ai-invariant` comments instead.
@@ -19,9 +19,17 @@ Generate a `MODULE_CONTRACT` block with:
19
19
  - The purpose must be specific to this file, not generic.
20
20
  - Non-goals must describe real boundaries the file respects.
21
21
  - Do not use vague phrases like "utility functions" or "helper code".
22
- - Do not reference the file name in the purpose.
22
+ - The purpose MUST contain at least one file-derived token — a filename stem segment, a parent-directory segment for generic stems, or an exported symbol name (COMPASS-PURPOSE-02). Do not open with boilerplate like "This file" or "Utility" (COMPASS-PURPOSE-01).
23
23
  - Keep the purpose under 100 characters.
24
24
 
25
+ ## KEY_DECISIONS (v2, RFC-1094)
26
+
27
+ When the file's `riskClass` is `medium` or `high`, also generate a `KEY_DECISIONS` block between `MODULE_CONTRACT` and `CHANGE_SUMMARY`:
28
+
29
+ - 1–7 `<item>` entries, each ≤ 20 words.
30
+ - Content is current-state design truth: invariants, chosen mechanisms, forbidden approaches — "current truth, not history".
31
+ - Items MUST NOT start with a governance-ID prefix (`RFC-`, `ADR-`, ticket) — history belongs in `CHANGE_SUMMARY`.
32
+
25
33
  ## Example
26
34
 
27
35
  ```
@@ -32,4 +40,7 @@ Generate a `MODULE_CONTRACT` block with:
32
40
  <item>Do not delete protected (RFC/code-referencing) items under any circumstance.</item>
33
41
  </non-goals>
34
42
  </MODULE_CONTRACT>
43
+ <KEY_DECISIONS>
44
+ <item>Classification is deterministic regex — no LLM in the validate path.</item>
45
+ </KEY_DECISIONS>
35
46
  ```
@@ -64,7 +64,7 @@ test("forge create --in-place scaffolds in cwd with forge.yaml and docs dirs", a
64
64
  expect(existsSync(join(tempDir, "package.json"))).toBe(true);
65
65
  expect(existsSync(join(tempDir, "scripts", "clean.mjs"))).toBe(true);
66
66
  expect(existsSync(join(tempDir, "hooks", "pre-user-prompt-wrapper.mjs"))).toBe(true);
67
- }, 30000);
67
+ }, 120000);
68
68
 
69
69
  test("forge create --in-place refuses when forge artifacts already exist", async () => {
70
70
  await writeFile(join(tempDir, "forge.yaml"), "existing: true", "utf8");
@@ -102,7 +102,7 @@ test("forge create --in-place tolerates only .git directory in target directory"
102
102
  expect(result.data?.status).toBe("pass");
103
103
  expect(existsSync(join(tempDir, "forge.yaml"))).toBe(true);
104
104
  expect(existsSync(join(tempDir, ".git"))).toBe(true);
105
- }, 30000);
105
+ }, 120000);
106
106
 
107
107
  test("forge create fails on non-kebab-case name", async () => {
108
108
  const result = await runCreate(
@@ -141,7 +141,7 @@ test("forge create --in-place --profile forge-shell uses forge-shell profile", a
141
141
  );
142
142
  expect(result.exitCode).toBe(0);
143
143
  expect(result.data?.profile).toBe("forge-shell");
144
- }, 30000);
144
+ }, 120000);
145
145
 
146
146
  test("forge create --package-manager npm writes npm into forge.yaml with npx bindings", async () => {
147
147
  const result = await runCreate(
@@ -162,7 +162,7 @@ test("forge create --package-manager npm writes npm into forge.yaml with npx bin
162
162
  expect(config.project.packageManager).toBe("npm");
163
163
  expect(config.bindings?.commands.validateRfc).toContain("npx");
164
164
  expect(config.bindings?.commands.validateRfc).toContain("forge rfc.validate");
165
- }, 30000);
165
+ }, 120000);
166
166
 
167
167
  test("forge create --package-manager pnpm writes pnpm into forge.yaml", async () => {
168
168
  const result = await runCreate(
@@ -182,7 +182,7 @@ test("forge create --package-manager pnpm writes pnpm into forge.yaml", async ()
182
182
  const config = loadForgeConfig(tempDir);
183
183
  expect(config.project.packageManager).toBe("pnpm");
184
184
  expect(config.bindings?.commands.validateRfc).toContain("pnpm exec");
185
- }, 30000);
185
+ }, 120000);
186
186
 
187
187
  test("forge.yaml has non-null forge-CLI bindings and null stack bindings", async () => {
188
188
  await runCreate(
@@ -199,7 +199,7 @@ test("forge.yaml has non-null forge-CLI bindings and null stack bindings", async
199
199
  expect(config.bindings?.commands.typecheck).toBeNull();
200
200
  expect(config.bindings?.commands.test).toBeNull();
201
201
  expect(config.bindings?.commands.scopedBuild).toBeNull();
202
- }, 30000);
202
+ }, 120000);
203
203
 
204
204
  test("forge.yaml has forge.syncedVersion set", async () => {
205
205
  await runCreate(
@@ -209,7 +209,7 @@ test("forge.yaml has forge.syncedVersion set", async () => {
209
209
 
210
210
  const config = loadForgeConfig(tempDir);
211
211
  expect(config.forge?.syncedVersion).toBeTruthy();
212
- }, 30000);
212
+ }, 120000);
213
213
 
214
214
  test("result includes nextSteps with forge-bootstrap", async () => {
215
215
  const result = await runCreate(
@@ -220,7 +220,7 @@ test("result includes nextSteps with forge-bootstrap", async () => {
220
220
  expect(result.nextSteps).toBeDefined();
221
221
  expect(result.nextSteps?.some((s) => s.action.includes("forge-bootstrap"))).toBe(true);
222
222
  expect(result.nextSteps?.some((s) => s.kind === "required")).toBe(true);
223
- }, 30000);
223
+ }, 120000);
224
224
 
225
225
  test("forge create generates AGENTS.md with behavioral layer (RFC-0548)", async () => {
226
226
  const result = await runCreate(
@@ -237,7 +237,7 @@ test("forge create generates AGENTS.md with behavioral layer (RFC-0548)", async
237
237
  expect(agentsMd).toContain("<!-- forge:begin behavioral-layer -->");
238
238
  expect(agentsMd).toContain("<!-- forge:end behavioral-layer -->");
239
239
  expect(agentsMd).toContain("### Intent-to-skill routing");
240
- }, 30000);
240
+ }, 120000);
241
241
 
242
242
  test("forge create writes NEXT_STEPS.md with greenfield and transplant guidance (RFC-0550)", async () => {
243
243
  const result = await runCreate(
@@ -255,7 +255,7 @@ test("forge create writes NEXT_STEPS.md with greenfield and transplant guidance
255
255
  expect(nextSteps).toContain("forge-bootstrap");
256
256
  expect(nextSteps).toContain("language");
257
257
  expect(result.data?.filesCreated).toContain("NEXT_STEPS.md");
258
- }, 30000);
258
+ }, 120000);
259
259
 
260
260
  test("forge create root package.json has scripts and replaced project name", async () => {
261
261
  const result = await runCreate(
@@ -272,7 +272,7 @@ test("forge create root package.json has scripts and replaced project name", asy
272
272
  expect(pkgJson.scripts["format:check"]).toBe("prettier --check .");
273
273
  expect(pkgJson.scripts.test).toBe("vitest run --passWithNoTests");
274
274
  expect(pkgJson.scripts["upgrade-packages"]).toBe("pnpm up");
275
- }, 30000);
275
+ }, 120000);
276
276
 
277
277
  test("forge create --in-place derives name from folder name when --name omitted", async () => {
278
278
  const namedDir = join(tempDir, "my-derived-project");
@@ -288,7 +288,7 @@ test("forge create --in-place derives name from folder name when --name omitted"
288
288
  const { readFile: readFileAsync } = await import("node:fs/promises");
289
289
  const pkgJson = JSON.parse(await readFileAsync(join(namedDir, "package.json"), "utf8"));
290
290
  expect(pkgJson.name).toBe("my-derived-project");
291
- }, 30000);
291
+ }, 120000);
292
292
 
293
293
  test("forge create --profile godot-csharp writes compass.fileExtensions into forge.yaml", async () => {
294
294
  const result = await runCreate(
@@ -305,7 +305,7 @@ test("forge create --profile godot-csharp writes compass.fileExtensions into for
305
305
  expect(forgeYaml).toContain(".tscn");
306
306
  expect(forgeYaml).toContain(".tres");
307
307
  expect(forgeYaml).toContain(".gd");
308
- }, 30000);
308
+ }, 120000);
309
309
 
310
310
  test("forge create --in-place setup-git-guards.sh contains shell auto-detection (RFC-1019)", async () => {
311
311
  const result = await runCreate(
@@ -319,7 +319,7 @@ test("forge create --in-place setup-git-guards.sh contains shell auto-detection
319
319
  expect(setupScript).toContain(".zshenv");
320
320
  expect(setupScript).toContain(".bashrc");
321
321
  expect(setupScript).toContain("profile_file");
322
- }, 30000);
322
+ }, 120000);
323
323
 
324
324
  test("forge create --in-place .windsurf/hooks.json calls wrapper not bash directly (RFC-1019)", async () => {
325
325
  const result = await runCreate(
@@ -332,7 +332,7 @@ test("forge create --in-place .windsurf/hooks.json calls wrapper not bash direct
332
332
  const hooksJson = await readFileAsync(join(tempDir, ".windsurf", "hooks.json"), "utf8");
333
333
  expect(hooksJson).toContain("pre-user-prompt-wrapper.mjs");
334
334
  expect(hooksJson).not.toContain("2>/dev/null || true");
335
- }, 30000);
335
+ }, 120000);
336
336
 
337
337
  test("forge create --in-place pins pre-user-prompt-wrapper.mjs in pinned.yaml (RFC-1019)", async () => {
338
338
  const result = await runCreate(
@@ -344,7 +344,7 @@ test("forge create --in-place pins pre-user-prompt-wrapper.mjs in pinned.yaml (R
344
344
  const { readFile: readFileAsync } = await import("node:fs/promises");
345
345
  const pinnedYaml = await readFileAsync(join(tempDir, ".forge", "pinned.yaml"), "utf8");
346
346
  expect(pinnedYaml).toContain("pre-user-prompt-wrapper.mjs");
347
- }, 30000);
347
+ }, 120000);
348
348
 
349
349
  test("forge create --profile phaser-turborepo writes compass.fileExtensions into forge.yaml", async () => {
350
350
  const result = await runCreate(
@@ -357,4 +357,4 @@ test("forge create --profile phaser-turborepo writes compass.fileExtensions into
357
357
  const forgeYaml = await readFileAsync(join(tempDir, "forge.yaml"), "utf8");
358
358
  expect(forgeYaml).toContain("compass:");
359
359
  expect(forgeYaml).toContain("fileExtensions:");
360
- }, 30000);
360
+ }, 120000);
@@ -0,0 +1,181 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge.file-size.lint — SIZE-01 rule from RFC-1088.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-1088: initial tests for portable file-size lint handler.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
11
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { runFileSizeLint, countLines } from "../../os/core/handlers/file-size-lint.ts";
15
+ import type { ForgeRuntimeContext, ForgeLogger } from "../types.ts";
16
+
17
+ const logger: ForgeLogger = {
18
+ section() {},
19
+ info() {},
20
+ warn() {},
21
+ error() {},
22
+ success() {},
23
+ };
24
+
25
+ function mockContext(workspaceRoot: string): ForgeRuntimeContext {
26
+ return {
27
+ workspaceRoot,
28
+ logger,
29
+ dryRun: false,
30
+ outputFormat: "json",
31
+ };
32
+ }
33
+
34
+ function input(flags: Record<string, unknown> = {}): { argv: string[]; flags: Record<string, unknown> } {
35
+ return { argv: [], flags };
36
+ }
37
+
38
+ describe("countLines (RFC-1088)", () => {
39
+ it("counts physical lines including a trailing newline", () => {
40
+ expect(countLines("a\nb\nc\n")).toBe(4);
41
+ });
42
+
43
+ it("returns 0 for an empty source", () => {
44
+ expect(countLines("")).toBe(0);
45
+ });
46
+ });
47
+
48
+ describe("runFileSizeLint (RFC-1088, command-level)", () => {
49
+ let tempDir: string;
50
+
51
+ beforeEach(async () => {
52
+ tempDir = await mkdtemp(join(tmpdir(), "forge-file-size-lint-"));
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await rm(tempDir, { recursive: true, force: true });
57
+ });
58
+
59
+ it("SIZE-01 (warning): flags a 700-line file as warning (601-1200 tier)", async () => {
60
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
61
+ const big = Array.from({ length: 700 }, (_, i) => `const x${i} = ${i};`).join("\n");
62
+ await writeFile(join(tempDir, "packages", "some-pkg", "src", "big.ts"), big, "utf8");
63
+
64
+ const result = await runFileSizeLint(input(), mockContext(tempDir));
65
+ expect(result.exitCode ?? 0).toBe(0);
66
+
67
+ const diagnostics = (
68
+ result.data as { diagnostics: Array<{ ruleId: string; severity: string; file: string }> }
69
+ ).diagnostics;
70
+ const hit = diagnostics.find((d) => d.ruleId === "SIZE-01" && d.file.endsWith("big.ts"));
71
+ expect(hit).toBeDefined();
72
+ expect(hit!.severity).toBe("warning");
73
+ });
74
+
75
+ it("SIZE-01 (error): flags a 1300-line file as error (1200+ tier)", async () => {
76
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
77
+ const huge = Array.from({ length: 1300 }, (_, i) => `const x${i} = ${i};`).join("\n");
78
+ await writeFile(join(tempDir, "packages", "some-pkg", "src", "huge.ts"), huge, "utf8");
79
+
80
+ const result = await runFileSizeLint(input(), mockContext(tempDir));
81
+ expect(result.exitCode).toBe(1);
82
+
83
+ const diagnostics = (
84
+ result.data as { diagnostics: Array<{ ruleId: string; severity: string; file: string }> }
85
+ ).diagnostics;
86
+ const hit = diagnostics.find((d) => d.ruleId === "SIZE-01" && d.file.endsWith("huge.ts"));
87
+ expect(hit).toBeDefined();
88
+ expect(hit!.severity).toBe("error");
89
+ });
90
+
91
+ it("passes without diagnostics when every file is under the threshold", async () => {
92
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
93
+ await writeFile(
94
+ join(tempDir, "packages", "some-pkg", "src", "small.ts"),
95
+ "export const x = 1;\n",
96
+ "utf8",
97
+ );
98
+
99
+ const result = await runFileSizeLint(input(), mockContext(tempDir));
100
+ expect(result.exitCode ?? 0).toBe(0);
101
+
102
+ const diagnostics = (result.data as { diagnostics: Array<{ ruleId: string }> }).diagnostics;
103
+ expect(diagnostics.length).toBe(0);
104
+ });
105
+
106
+ it("--write-baseline writes a YAML baseline to workspace root", async () => {
107
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
108
+ const big = Array.from({ length: 700 }, (_, i) => `const x${i} = ${i};`).join("\n");
109
+ await writeFile(join(tempDir, "packages", "some-pkg", "src", "big.ts"), big, "utf8");
110
+
111
+ const result = await runFileSizeLint(
112
+ input({ "write-baseline": true }),
113
+ mockContext(tempDir),
114
+ );
115
+ expect(result.exitCode ?? 0).toBe(0);
116
+
117
+ const baselineRaw = await readFile(
118
+ join(tempDir, "file-size-lint.baseline.yaml"),
119
+ "utf8",
120
+ );
121
+ expect(baselineRaw).toContain("schemaVersion");
122
+ expect(baselineRaw).toContain("threshold: 600");
123
+ expect(baselineRaw).toContain("big.ts");
124
+ });
125
+
126
+ it("baseline suppresses files within their ceiling", async () => {
127
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
128
+ const big = Array.from({ length: 700 }, (_, i) => `const x${i} = ${i};`).join("\n");
129
+ await writeFile(join(tempDir, "packages", "some-pkg", "src", "big.ts"), big, "utf8");
130
+
131
+ await writeFile(
132
+ join(tempDir, "file-size-lint.baseline.yaml"),
133
+ `meta:\n schemaVersion: 1\n threshold: 600\nceilings:\n packages/some-pkg/src/big.ts: 700\n`,
134
+ "utf8",
135
+ );
136
+
137
+ const result = await runFileSizeLint(input(), mockContext(tempDir));
138
+ expect(result.exitCode ?? 0).toBe(0);
139
+
140
+ const diagnostics = (result.data as { diagnostics: Array<{ ruleId: string }> }).diagnostics;
141
+ expect(diagnostics.length).toBe(0);
142
+ });
143
+
144
+ it("baseline flags files that grew beyond their ceiling", async () => {
145
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
146
+ const big = Array.from({ length: 800 }, (_, i) => `const x${i} = ${i};`).join("\n");
147
+ await writeFile(join(tempDir, "packages", "some-pkg", "src", "big.ts"), big, "utf8");
148
+
149
+ await writeFile(
150
+ join(tempDir, "file-size-lint.baseline.yaml"),
151
+ `meta:\n schemaVersion: 1\n threshold: 600\nceilings:\n packages/some-pkg/src/big.ts: 700\n`,
152
+ "utf8",
153
+ );
154
+
155
+ const result = await runFileSizeLint(input(), mockContext(tempDir));
156
+ expect(result.exitCode ?? 0).toBe(0);
157
+
158
+ const diagnostics = (
159
+ result.data as { diagnostics: Array<{ ruleId: string; message: string }> }
160
+ ).diagnostics;
161
+ const hit = diagnostics.find((d) => d.ruleId === "SIZE-01");
162
+ expect(hit).toBeDefined();
163
+ expect(hit!.message).toContain("grew from the baselined 700 to 800");
164
+ });
165
+
166
+ it("--baseline-path overrides the default baseline location", async () => {
167
+ await mkdir(join(tempDir, "packages", "some-pkg", "src"), { recursive: true });
168
+ const big = Array.from({ length: 700 }, (_, i) => `const x${i} = ${i};`).join("\n");
169
+ await writeFile(join(tempDir, "packages", "some-pkg", "src", "big.ts"), big, "utf8");
170
+
171
+ const customPath = "custom-baseline.yaml";
172
+ const result = await runFileSizeLint(
173
+ input({ "write-baseline": true, "baseline-path": customPath }),
174
+ mockContext(tempDir),
175
+ );
176
+ expect(result.exitCode ?? 0).toBe(0);
177
+
178
+ const baselineRaw = await readFile(join(tempDir, customPath), "utf8");
179
+ expect(baselineRaw).toContain("big.ts");
180
+ });
181
+ });
@@ -4,6 +4,7 @@
4
4
  </MODULE_CONTRACT>
5
5
  <CHANGE_SUMMARY>
6
6
  <item>RFC-1080: initial public-surface validator tests covering all 5 rules, exit codes, and monorepo resolution.</item>
7
+ <item>RFC-1088: raise SURFACE-01 threshold from 300 to 600 lines.</item>
7
8
  </CHANGE_SUMMARY>
8
9
  */
9
10
 
@@ -101,7 +102,7 @@ async function setupMonorepo(
101
102
  await setupStandalone(join(dir, "packages", "forge"), opts);
102
103
  }
103
104
 
104
- test("SURFACE-01: README under 300 lines passes", async () => {
105
+ test("SURFACE-01: README under 600 lines passes", async () => {
105
106
  await setupStandalone(tempDir, { readmeLines: 100 });
106
107
  const result = await runPublicSurfaceValidate({} as never, mockContext(tempDir));
107
108
  const checks = result.data?.checks ?? [];
@@ -109,12 +110,20 @@ test("SURFACE-01: README under 300 lines passes", async () => {
109
110
  expect(surface01?.status, "README with 100 lines should pass SURFACE-01").toBe("pass");
110
111
  });
111
112
 
112
- test("SURFACE-01: README over 300 lines warns", async () => {
113
- await setupStandalone(tempDir, { readmeLines: 301 });
113
+ test("SURFACE-01: README at exactly 600 lines passes", async () => {
114
+ await setupStandalone(tempDir, { readmeLines: 600 });
114
115
  const result = await runPublicSurfaceValidate({} as never, mockContext(tempDir));
115
116
  const checks = result.data?.checks ?? [];
116
117
  const surface01 = checks.find((c) => c.rule === "SURFACE-01");
117
- expect(surface01?.status, "README with 301 lines should warn on SURFACE-01").toBe("warn");
118
+ expect(surface01?.status, "README with 600 lines should pass SURFACE-01").toBe("pass");
119
+ });
120
+
121
+ test("SURFACE-01: README over 600 lines warns", async () => {
122
+ await setupStandalone(tempDir, { readmeLines: 601 });
123
+ const result = await runPublicSurfaceValidate({} as never, mockContext(tempDir));
124
+ const checks = result.data?.checks ?? [];
125
+ const surface01 = checks.find((c) => c.rule === "SURFACE-01");
126
+ expect(surface01?.status, "README with 601 lines should warn on SURFACE-01").toBe("warn");
118
127
  });
119
128
 
120
129
  test("SURFACE-02: Node version match passes", async () => {
@@ -60,7 +60,7 @@ test("forge.scaffold tolerates non-empty directory (RFC-0877 in-place mode)", as
60
60
  expect(result.data?.status).toBe("pass");
61
61
  expect(existsSync(join(tempDir, "package.json"))).toBe(true);
62
62
  expect(existsSync(join(tempDir, "some-file.txt"))).toBe(true);
63
- }, 30000);
63
+ }, 120000);
64
64
 
65
65
  test("forge.scaffold fails on missing --profile", async () => {
66
66
  const result = await runScaffoldProject(