@warpgogol/forge 5.3.4 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/cli.ts +7 -2
  3. package/dist/bin/cli.js +7 -2
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/os/adr/adr.module.d.ts.map +1 -1
  6. package/dist/os/adr/adr.module.js +2 -0
  7. package/dist/os/adr/adr.module.js.map +1 -1
  8. package/dist/os/audit/audit.module.d.ts.map +1 -1
  9. package/dist/os/audit/audit.module.js +2 -0
  10. package/dist/os/audit/audit.module.js.map +1 -1
  11. package/dist/os/compass/compass.module.d.ts.map +1 -1
  12. package/dist/os/compass/compass.module.js +47 -15
  13. package/dist/os/compass/compass.module.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
  15. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  16. package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
  17. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  18. package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
  19. package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
  20. package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
  21. package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
  22. package/dist/os/compass/handlers/summary-record.d.ts +11 -2
  23. package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
  24. package/dist/os/compass/handlers/summary-record.js +95 -15
  25. package/dist/os/compass/handlers/summary-record.js.map +1 -1
  26. package/dist/os/core/core.module.d.ts.map +1 -1
  27. package/dist/os/core/core.module.js +124 -73
  28. package/dist/os/core/core.module.js.map +1 -1
  29. package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
  30. package/dist/os/core/handlers/file-size-lint.js +2 -0
  31. package/dist/os/core/handlers/file-size-lint.js.map +1 -1
  32. package/dist/os/exploration/exploration.module.d.ts.map +1 -1
  33. package/dist/os/exploration/exploration.module.js +3 -0
  34. package/dist/os/exploration/exploration.module.js.map +1 -1
  35. package/dist/os/naming/naming-convention.d.ts.map +1 -1
  36. package/dist/os/naming/naming-convention.js +11 -1
  37. package/dist/os/naming/naming-convention.js.map +1 -1
  38. package/dist/os/naming/naming.module.d.ts.map +1 -1
  39. package/dist/os/naming/naming.module.js +9 -2
  40. package/dist/os/naming/naming.module.js.map +1 -1
  41. package/dist/os/notes/notes.module.d.ts.map +1 -1
  42. package/dist/os/notes/notes.module.js +17 -6
  43. package/dist/os/notes/notes.module.js.map +1 -1
  44. package/dist/os/plan/plan.module.d.ts.map +1 -1
  45. package/dist/os/plan/plan.module.js +2 -0
  46. package/dist/os/plan/plan.module.js.map +1 -1
  47. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  48. package/dist/os/plugin/plugin.module.js +9 -2
  49. package/dist/os/plugin/plugin.module.js.map +1 -1
  50. package/dist/os/program/program.module.d.ts.map +1 -1
  51. package/dist/os/program/program.module.js +19 -4
  52. package/dist/os/program/program.module.js.map +1 -1
  53. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  54. package/dist/os/queue/handlers/queue-validate.js +154 -7
  55. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  56. package/dist/os/queue/manifest.d.ts +17 -1
  57. package/dist/os/queue/manifest.d.ts.map +1 -1
  58. package/dist/os/queue/manifest.js +127 -6
  59. package/dist/os/queue/manifest.js.map +1 -1
  60. package/dist/os/queue/queue.module.d.ts.map +1 -1
  61. package/dist/os/queue/queue.module.js +32 -4
  62. package/dist/os/queue/queue.module.js.map +1 -1
  63. package/dist/os/queue/types.d.ts +100 -1
  64. package/dist/os/queue/types.d.ts.map +1 -1
  65. package/dist/os/queue/types.js +44 -0
  66. package/dist/os/queue/types.js.map +1 -1
  67. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  68. package/dist/os/rfc/handlers/implement-stamp.js +30 -8
  69. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  70. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  71. package/dist/os/rfc/rfc.module.js +25 -6
  72. package/dist/os/rfc/rfc.module.js.map +1 -1
  73. package/dist/os/rfc/types.d.ts +12 -2
  74. package/dist/os/rfc/types.d.ts.map +1 -1
  75. package/dist/os/rfc/types.js +2 -2
  76. package/dist/os/rfc/types.js.map +1 -1
  77. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  78. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  79. package/dist/os/rfc/verification-evidence.js +134 -23
  80. package/dist/os/rfc/verification-evidence.js.map +1 -1
  81. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  82. package/dist/os/rfc/verification-refresh.js +66 -37
  83. package/dist/os/rfc/verification-refresh.js.map +1 -1
  84. package/dist/os/session/handlers/save.d.ts.map +1 -1
  85. package/dist/os/session/handlers/save.js +42 -5
  86. package/dist/os/session/handlers/save.js.map +1 -1
  87. package/dist/os/session/session.module.d.ts.map +1 -1
  88. package/dist/os/session/session.module.js +5 -0
  89. package/dist/os/session/session.module.js.map +1 -1
  90. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  91. package/dist/os/spec/live-spec-merge.js +55 -134
  92. package/dist/os/spec/live-spec-merge.js.map +1 -1
  93. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  94. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  95. package/dist/os/spec/live-spec-rebuild.js +272 -0
  96. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  97. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  98. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  99. package/dist/os/spec/live-spec-shared.js +315 -0
  100. package/dist/os/spec/live-spec-shared.js.map +1 -0
  101. package/dist/os/spec/live-spec-types.d.ts +17 -1
  102. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  103. package/dist/os/spec/live-spec-types.js +3 -0
  104. package/dist/os/spec/live-spec-types.js.map +1 -1
  105. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  106. package/dist/os/spec/live-spec-validate.js +105 -2
  107. package/dist/os/spec/live-spec-validate.js.map +1 -1
  108. package/dist/os/spec/spec-materialize.d.ts +2 -0
  109. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  110. package/dist/os/spec/spec-materialize.js +45 -5
  111. package/dist/os/spec/spec-materialize.js.map +1 -1
  112. package/dist/os/spec/spec-schema.d.ts +19 -0
  113. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  114. package/dist/os/spec/spec-schema.js +30 -0
  115. package/dist/os/spec/spec-schema.js.map +1 -1
  116. package/dist/os/spec/spec-status.d.ts +4 -0
  117. package/dist/os/spec/spec-status.d.ts.map +1 -1
  118. package/dist/os/spec/spec-status.js +19 -1
  119. package/dist/os/spec/spec-status.js.map +1 -1
  120. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  121. package/dist/os/spec/spec-validate.js +55 -8
  122. package/dist/os/spec/spec-validate.js.map +1 -1
  123. package/dist/os/spec/spec.module.d.ts.map +1 -1
  124. package/dist/os/spec/spec.module.js +84 -8
  125. package/dist/os/spec/spec.module.js.map +1 -1
  126. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  127. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  128. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  129. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  130. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  131. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  132. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  133. package/dist/os/workflow/workflow.module.js +13 -5
  134. package/dist/os/workflow/workflow.module.js.map +1 -1
  135. package/dist/src/cli-flags.d.ts.map +1 -1
  136. package/dist/src/cli-flags.js +25 -5
  137. package/dist/src/cli-flags.js.map +1 -1
  138. package/dist/src/config/forge-config.d.ts +3 -0
  139. package/dist/src/config/forge-config.d.ts.map +1 -1
  140. package/dist/src/config/forge-config.js +2 -0
  141. package/dist/src/config/forge-config.js.map +1 -1
  142. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  143. package/dist/src/onboarding/doctor.js +2 -3
  144. package/dist/src/onboarding/doctor.js.map +1 -1
  145. package/dist/src/onboarding/init.d.ts.map +1 -1
  146. package/dist/src/onboarding/init.js +8 -3
  147. package/dist/src/onboarding/init.js.map +1 -1
  148. package/dist/src/pipeline-status.d.ts +14 -1
  149. package/dist/src/pipeline-status.d.ts.map +1 -1
  150. package/dist/src/pipeline-status.js +28 -8
  151. package/dist/src/pipeline-status.js.map +1 -1
  152. package/dist/src/types.d.ts +7 -1
  153. package/dist/src/types.d.ts.map +1 -1
  154. package/dist/src/types.js +6 -0
  155. package/dist/src/types.js.map +1 -1
  156. package/dist/src/utils/generated-marker.d.ts +8 -0
  157. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  158. package/dist/src/utils/generated-marker.js +25 -0
  159. package/dist/src/utils/generated-marker.js.map +1 -1
  160. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  161. package/dist/src/validators/skill-validate.js +2 -4
  162. package/dist/src/validators/skill-validate.js.map +1 -1
  163. package/os/adr/adr.module.ts +2 -0
  164. package/os/audit/audit.module.ts +2 -0
  165. package/os/compass/compass.module.ts +49 -15
  166. package/os/compass/handlers/compass-audit-handler.ts +51 -10
  167. package/os/compass/handlers/compass-docs-validate.ts +361 -0
  168. package/os/compass/handlers/summary-record.ts +106 -15
  169. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  170. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  171. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  172. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  173. package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
  174. package/os/compass/handlers/tests/summary-record.test.ts +127 -0
  175. package/os/core/core.module.ts +147 -75
  176. package/os/core/docs-archive.test.ts +136 -0
  177. package/os/core/handlers/file-size-lint.ts +2 -0
  178. package/os/exploration/exploration.module.ts +3 -0
  179. package/os/naming/naming-convention.ts +11 -1
  180. package/os/naming/naming.module.ts +9 -2
  181. package/os/notes/notes.module.ts +17 -6
  182. package/os/plan/plan-0000-template.md +150 -0
  183. package/os/plan/plan.module.ts +2 -0
  184. package/os/plugin/plugin.module.ts +9 -2
  185. package/os/program/program.module.ts +19 -4
  186. package/os/queue/decision-ledger.test.ts +163 -0
  187. package/os/queue/handlers/queue-validate.ts +190 -8
  188. package/os/queue/manifest.ts +201 -7
  189. package/os/queue/queue-validate.test.ts +303 -0
  190. package/os/queue/queue.module.ts +32 -4
  191. package/os/queue/types.ts +56 -1
  192. package/os/rfc/handlers/implement-stamp.ts +37 -8
  193. package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
  194. package/os/rfc/rfc.module.ts +27 -6
  195. package/os/rfc/types.ts +18 -3
  196. package/os/rfc/verification-emit.test.ts +305 -0
  197. package/os/rfc/verification-evidence.ts +149 -24
  198. package/os/rfc/verification-refresh.test.ts +222 -1
  199. package/os/rfc/verification-refresh.ts +74 -38
  200. package/os/session/handlers/save.ts +37 -5
  201. package/os/session/session.module.ts +5 -0
  202. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  203. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  204. package/os/spec/live-spec-merge.test.ts +187 -7
  205. package/os/spec/live-spec-merge.ts +71 -155
  206. package/os/spec/live-spec-rebuild.test.ts +317 -0
  207. package/os/spec/live-spec-rebuild.ts +356 -0
  208. package/os/spec/live-spec-shared.ts +378 -0
  209. package/os/spec/live-spec-types.ts +21 -1
  210. package/os/spec/live-spec-validate.test.ts +317 -0
  211. package/os/spec/live-spec-validate.ts +106 -2
  212. package/os/spec/spec-materialize.test.ts +192 -0
  213. package/os/spec/spec-materialize.ts +48 -4
  214. package/os/spec/spec-schema.ts +49 -0
  215. package/os/spec/spec-status.ts +24 -0
  216. package/os/spec/spec-validate.test.ts +66 -0
  217. package/os/spec/spec-validate.ts +62 -8
  218. package/os/spec/spec.module.ts +85 -8
  219. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  220. package/os/werkstatt/werkstatt.module.ts +13 -4
  221. package/os/workflow/workflow.module.ts +13 -5
  222. package/package.json +1 -1
  223. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  224. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  225. package/skills/fo/fo-explore/SKILL.md +2 -1
  226. package/skills/fo/fo-fix/SKILL.md +11 -3
  227. package/skills/fo/fo-handoff/SKILL.md +4 -6
  228. package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
  229. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  230. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  231. package/skills/fo/fo-idea-implement/SKILL.md +53 -10
  232. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  233. package/skills/fo/fo-review/SKILL.md +69 -8
  234. package/skills/fo/fo-session-retro/SKILL.md +3 -2
  235. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  236. package/skills/shared/grilling/SKILL.md +12 -0
  237. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  238. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  239. package/src/cli-flags.ts +23 -5
  240. package/src/config/forge-config.ts +4 -0
  241. package/src/onboarding/doctor.ts +2 -3
  242. package/src/onboarding/init.ts +8 -3
  243. package/src/pipeline-status.ts +32 -8
  244. package/src/tests/cli-flags.test.ts +20 -0
  245. package/src/tests/cli-output.test.ts +13 -7
  246. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  247. package/src/tests/implement-stamp.test.ts +38 -0
  248. package/src/tests/session-handlers.test.ts +33 -0
  249. package/src/types.ts +13 -1
  250. package/src/utils/generated-marker.ts +28 -0
  251. package/src/validators/skill-validate.ts +2 -4
  252. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  253. package/skills/fo/fo-review/AXES.md +0 -70
@@ -16,6 +16,7 @@ import { join } from "node:path";
16
16
  import { execFile } from "node:child_process";
17
17
  import { promisify } from "node:util";
18
18
  import { runCompassAuditValidate } from "../compass-audit-handler.ts";
19
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
20
  import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
21
 
21
22
  const execFileAsync = promisify(execFile);
@@ -31,15 +32,15 @@ const logger = {
31
32
  },
32
33
  };
33
34
 
34
- function makeContext(workspaceRoot: string): ForgeRuntimeContext {
35
+ function makeContext(workspaceRoot: string, dryRun = false): ForgeRuntimeContext {
35
36
  return {
36
37
  workspaceRoot,
37
38
  site: undefined,
38
39
  siteExplicit: false,
39
40
  logger: logger as never,
40
- dryRun: false,
41
+ dryRun,
41
42
  outputFormat: "json",
42
- io: {} as never,
43
+ io: ambientIo,
43
44
  actualState: undefined as never,
44
45
  fileIntents: [],
45
46
  } as unknown as ForgeRuntimeContext;
@@ -131,3 +132,91 @@ describe("compass.audit.validate — RFC-1143 ledger-ineligible skips", () => {
131
132
  expect(data.skippedIneligible).toBeGreaterThan(0);
132
133
  });
133
134
  });
135
+
136
+ // RFC-1242 AC-5/AC-6: --prune drops ledger entries for paths that no longer
137
+ // exist while preserving every surviving entry's verdict fields.
138
+ const DEAD_PATH = "packages/gone/deleted.ts";
139
+ const LEDGER_ENTRY = (path: string) =>
140
+ ` - path: ${path}\n entityId: "eid-${path}"\n auditedRevision: 7\n auditedHash: "hash-${path}"\n auditedAt: "2026-01-01T00:00:00Z"\n verdict: pass\n agent: human:test\n`;
141
+
142
+ describe("compass.audit.validate --prune — RFC-1242", () => {
143
+ let root: string;
144
+
145
+ beforeEach(async () => {
146
+ root = await mkdtemp(join(tmpdir(), "compass-prune-"));
147
+ await mkdir(join(root, "packages", "foo"), { recursive: true });
148
+ await mkdir(join(root, "docs"), { recursive: true });
149
+ await writeFile(join(root, "packages", "foo", "index.ts"), "export const x = 1;\n", "utf8");
150
+ await writeFile(
151
+ join(root, "docs", "compass-audit-ledger.generated.yaml"),
152
+ `entries:\n${LEDGER_ENTRY(ELIGIBLE_FILE)}${LEDGER_ENTRY(DEAD_PATH)}`,
153
+ "utf8",
154
+ );
155
+ await execFileAsync("git", ["init"], { cwd: root });
156
+ await execFileAsync("git", ["config", "user.email", "test@test.com"], { cwd: root });
157
+ await execFileAsync("git", ["config", "user.name", "Test"], { cwd: root });
158
+ await execFileAsync("git", ["add", "."], { cwd: root });
159
+ await execFileAsync("git", ["commit", "-m", "init"], { cwd: root });
160
+ });
161
+
162
+ afterEach(async () => {
163
+ await rm(root, { recursive: true, force: true });
164
+ });
165
+
166
+ it("AC-5: ledger contains no entry whose path does not exist after prune", async () => {
167
+ const result = await runCompassAuditValidate(
168
+ makeInput({ prune: true, root: ["packages"] }),
169
+ makeContext(root),
170
+ );
171
+ const data = result.data as { pruned: number; prunedPaths: string[] };
172
+ expect(data.pruned).toBe(1);
173
+ expect(data.prunedPaths).toEqual([DEAD_PATH]);
174
+ const { readFile } = await import("node:fs/promises");
175
+ const ledgerText = await readFile(
176
+ join(root, "docs", "compass-audit-ledger.generated.yaml"),
177
+ "utf8",
178
+ );
179
+ expect(ledgerText).not.toContain(DEAD_PATH);
180
+ expect(ledgerText).toContain(ELIGIBLE_FILE);
181
+ });
182
+
183
+ it("AC-6: surviving entries keep their verdict fields unchanged", async () => {
184
+ await runCompassAuditValidate(
185
+ makeInput({ prune: true, root: ["packages"] }),
186
+ makeContext(root),
187
+ );
188
+ const { readFile } = await import("node:fs/promises");
189
+ const { parse } = await import("yaml");
190
+ const ledger = parse(
191
+ await readFile(join(root, "docs", "compass-audit-ledger.generated.yaml"), "utf8"),
192
+ ) as { entries: Array<Record<string, unknown>> };
193
+ const survivor = ledger.entries.find((e) => e.path === ELIGIBLE_FILE);
194
+ expect(
195
+ survivor,
196
+ "surviving ledger entry must keep its verdict fields — check the keep/drop partition",
197
+ ).toMatchObject({
198
+ entityId: `eid-${ELIGIBLE_FILE}`,
199
+ auditedRevision: 7,
200
+ auditedHash: `hash-${ELIGIBLE_FILE}`,
201
+ auditedAt: "2026-01-01T00:00:00Z",
202
+ verdict: "pass",
203
+ agent: "human:test",
204
+ });
205
+ });
206
+
207
+ it("--prune --dry-run reports the drop set without rewriting the ledger", async () => {
208
+ const { readFile } = await import("node:fs/promises");
209
+ const ledgerPath = join(root, "docs", "compass-audit-ledger.generated.yaml");
210
+ const before = await readFile(ledgerPath, "utf8");
211
+ const result = await runCompassAuditValidate(
212
+ makeInput({ prune: true, root: ["packages"] }),
213
+ makeContext(root, true),
214
+ );
215
+ const data = result.data as { pruned: number };
216
+ expect(data.pruned).toBe(1);
217
+ expect(
218
+ await readFile(ledgerPath, "utf8"),
219
+ "dry-run must not rewrite the ledger — writes must route through context.io honoring context.dryRun",
220
+ ).toBe(before);
221
+ });
222
+ });
@@ -0,0 +1,203 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ RFC-1242 AC-1..AC-4: compass.docs.validate scans the docs/*.xml corpus and
5
+ must fail on unresolvable <path> entries (COMPASS-DOC-01) and dead workspace
6
+ ids (COMPASS-DOC-02), warn on unresolvable link targets (COMPASS-DOC-03),
7
+ and flag malformed XML (COMPASS-DOC-00). A green corpus exits 0 with
8
+ status-shaped data.
9
+ </purpose>
10
+ </MODULE_CONTRACT>
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
14
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { runCompassDocsValidate } from "../compass-docs-validate.ts";
18
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
+ import type { Diagnostic, ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
+
21
+ const logger = {
22
+ section() {},
23
+ info() {},
24
+ warn() {},
25
+ error() {},
26
+ success() {},
27
+ getEvents() {
28
+ return [];
29
+ },
30
+ };
31
+
32
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
33
+ return {
34
+ workspaceRoot,
35
+ site: undefined,
36
+ siteExplicit: false,
37
+ logger: logger as never,
38
+ dryRun: false,
39
+ outputFormat: "json",
40
+ io: ambientIo,
41
+ actualState: undefined as never,
42
+ fileIntents: [],
43
+ } as unknown as ForgeRuntimeContext;
44
+ }
45
+
46
+ function makeInput(): ForgeCommandInput {
47
+ return { argv: [], flags: {} } as unknown as ForgeCommandInput;
48
+ }
49
+
50
+ const GREEN_DOC = `<root>
51
+ <path>\`docs/other.xml\`</path>
52
+ <workspace id="pkg-foo"><path>\`packages/foo\`</path></workspace>
53
+ <node id="packages.foo" type="package"><path>\`packages/foo\`</path></node>
54
+ <node id="root.a" type="compass-doc"><link rel="binds-to" target="root.b" /></node>
55
+ <node id="root.b" type="compass-doc" />
56
+ </root>
57
+ `;
58
+
59
+ describe("compass.docs.validate — RFC-1242", () => {
60
+ let root: string;
61
+
62
+ beforeEach(async () => {
63
+ root = await mkdtemp(join(tmpdir(), "compass-docs-"));
64
+ await mkdir(join(root, "docs"), { recursive: true });
65
+ await mkdir(join(root, "packages", "foo"), { recursive: true });
66
+ await writeFile(join(root, "packages", "foo", "index.ts"), "export {};\n", "utf8");
67
+ });
68
+
69
+ afterEach(async () => {
70
+ await rm(root, { recursive: true, force: true });
71
+ });
72
+
73
+ it("AC-2: green corpus exits 0 and reports scanned counts", async () => {
74
+ await writeFile(join(root, "docs", "a.xml"), GREEN_DOC, "utf8");
75
+ await writeFile(join(root, "docs", "other.xml"), "<root/>", "utf8");
76
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
77
+ expect(result.exitCode).toBe(0);
78
+ const data = result.data as {
79
+ scanned: { xmlFiles: number; pathsChecked: number; idsChecked: number };
80
+ diagnostics: Diagnostic[];
81
+ };
82
+ expect(data.scanned.xmlFiles).toBe(2);
83
+ expect(data.diagnostics).toEqual([]);
84
+ });
85
+
86
+ it("AC-3: dead <path> emits COMPASS-DOC-01 error", async () => {
87
+ await writeFile(
88
+ join(root, "docs", "a.xml"),
89
+ `<root><path>\`packages/gone\`</path></root>`,
90
+ "utf8",
91
+ );
92
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
93
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
94
+ expect(
95
+ diagnostics.some((d) => d.ruleId === "COMPASS-DOC-01" && d.severity === "error"),
96
+ "dead <path> must emit COMPASS-DOC-01 — check pathTokens/pathCheckTarget resolution",
97
+ ).toBe(true);
98
+ });
99
+
100
+ it("AC-4: dead <path> produces a non-zero exit", async () => {
101
+ await writeFile(
102
+ join(root, "docs", "a.xml"),
103
+ `<root><path>\`packages/gone\`</path></root>`,
104
+ "utf8",
105
+ );
106
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
107
+ expect(result.exitCode).toBe(1);
108
+ });
109
+
110
+ it("dead workspace id emits COMPASS-DOC-02 error", async () => {
111
+ await writeFile(join(root, "docs", "a.xml"), `<root><workspace id="pkg-gone"/></root>`, "utf8");
112
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
113
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
114
+ expect(
115
+ diagnostics.some((d) => d.ruleId === "COMPASS-DOC-02" && d.message.includes("pkg-gone")),
116
+ "dead workspace id must emit COMPASS-DOC-02 — check workspaceIdTarget mapping",
117
+ ).toBe(true);
118
+ expect(result.exitCode).toBe(1);
119
+ });
120
+
121
+ it("glob-tail <path> resolves against the parent directory", async () => {
122
+ await writeFile(
123
+ join(root, "docs", "a.xml"),
124
+ `<root><path>\`packages/*\`</path></root>`,
125
+ "utf8",
126
+ );
127
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
128
+ expect(result.exitCode).toBe(0);
129
+ });
130
+
131
+ it("unresolvable link target emits COMPASS-DOC-03 warning, never an error", async () => {
132
+ await writeFile(
133
+ join(root, "docs", "a.xml"),
134
+ `<root><node id="x"><link rel="binds-to" target="docs.rfcs.rfc-9999" /></node></root>`,
135
+ "utf8",
136
+ );
137
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
138
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
139
+ expect(
140
+ diagnostics.some((d) => d.ruleId === "COMPASS-DOC-03" && d.severity === "warning"),
141
+ "dangling link target must warn via COMPASS-DOC-03",
142
+ ).toBe(true);
143
+ expect(result.exitCode).toBe(0);
144
+ });
145
+
146
+ it("link target resolving to an archived RFC file passes", async () => {
147
+ await mkdir(join(root, "docs", "rfcs", "archive"), { recursive: true });
148
+ await writeFile(join(root, "docs", "rfcs", "archive", "rfc-0097-old.md"), "x", "utf8");
149
+ await writeFile(
150
+ join(root, "docs", "a.xml"),
151
+ `<root><node id="x"><link rel="binds-to" target="docs.rfcs.rfc-0097" /></node></root>`,
152
+ "utf8",
153
+ );
154
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
155
+ expect(result.exitCode).toBe(0);
156
+ });
157
+
158
+ it("malformed XML emits COMPASS-DOC-00 and skips ref checks for that file", async () => {
159
+ await writeFile(join(root, "docs", "a.xml"), `<root><name>oops</root>`, "utf8");
160
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
161
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
162
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-00")).toBe(true);
163
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-01")).toBe(false);
164
+ expect(result.exitCode).toBe(1);
165
+ });
166
+
167
+ it("quoted attribute value ending in / is not misread as self-closing", async () => {
168
+ await writeFile(
169
+ join(root, "docs", "a.xml"),
170
+ `<root><node id="x" type="compass-doc" path="docs/"><link rel="binds-to" target="x" /></node></root>`,
171
+ "utf8",
172
+ );
173
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
174
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
175
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-00")).toBe(false);
176
+ expect(result.exitCode).toBe(0);
177
+ });
178
+
179
+ it("docs.plans.plan-rfc-* link target resolves to a plan-*.md basename", async () => {
180
+ await mkdir(join(root, "docs", "plans"), { recursive: true });
181
+ await writeFile(join(root, "docs", "plans", "plan-rfc-1242-x.md"), "x", "utf8");
182
+ await writeFile(
183
+ join(root, "docs", "a.xml"),
184
+ `<root><node id="x"><link rel="binds-to" target="docs.plans.plan-rfc-1242-x" /></node></root>`,
185
+ "utf8",
186
+ );
187
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
188
+ const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
189
+ expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-03")).toBe(false);
190
+ });
191
+
192
+ it("AC-1: result data carries command-shaped payload fields", async () => {
193
+ await writeFile(join(root, "docs", "a.xml"), GREEN_DOC, "utf8");
194
+ await writeFile(join(root, "docs", "other.xml"), "<root/>", "utf8");
195
+ const result = await runCompassDocsValidate(makeInput(), makeContext(root));
196
+ const data = result.data as Record<string, unknown>;
197
+ expect(data).toHaveProperty("command", "compass.docs.validate");
198
+ expect(data).toHaveProperty("status", "pass");
199
+ expect(data).toHaveProperty("scanned");
200
+ expect(data).toHaveProperty("diagnostics");
201
+ expect(Array.isArray(data.diagnostics)).toBe(true);
202
+ });
203
+ });
@@ -16,6 +16,7 @@ import { execFile } from "node:child_process";
16
16
  import { promisify } from "node:util";
17
17
  import { parse as yamlParse } from "yaml";
18
18
  import { runCompassAuditBaseline } from "../compass-audit-handler.ts";
19
+ import { ambientIo } from "../../../../src/utils/io.ts";
19
20
  import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
20
21
 
21
22
  const execFileAsync = promisify(execFile);
@@ -39,7 +40,7 @@ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
39
40
  logger: logger as never,
40
41
  dryRun: false,
41
42
  outputFormat: "json",
42
- io: {} as never,
43
+ io: ambientIo,
43
44
  actualState: undefined as never,
44
45
  fileIntents: [],
45
46
  } as unknown as ForgeRuntimeContext;
@@ -11,6 +11,7 @@ the named-config-error contract.</purpose>
11
11
  <item>RFC-1096: initial coverage for resolveCompassPolicy (AC-1..AC-5).</item>
12
12
  <item>RFC-1220: step 1 — widen governance-ID tail regex</item>
13
13
  <item>RFC-1220: fo-review — F-1 shared forge-yaml fixture, F-2 temp-dir cleanup</item>
14
+ <item>RFC-1252: harden export pipeline — CI-parity verify-extract, --from-head publish-forge, EC-20 staged lint gate</item>
14
15
  </CHANGE_SUMMARY>
15
16
  */
16
17
 
@@ -163,8 +164,13 @@ describe("resolveCompassPolicy (RFC-1096)", () => {
163
164
  const inventoryFile = join(COMPASS_DIR, "handlers", "compass-inventory.ts");
164
165
 
165
166
  const offenders: string[] = [];
167
+ // Compass contract blocks are mechanical governance metadata — a recorded
168
+ // item text may name a consumer command and still be a name, not a path
169
+ // literal. The documented exemption above only holds if those blocks are
170
+ // stripped before the substring scan.
171
+ const compassBlockRe = /<(MODULE_CONTRACT|KEY_DECISIONS|CHANGE_SUMMARY)>[\s\S]*?<\/\1>/g;
166
172
  for (const file of collectTsFiles(COMPASS_DIR)) {
167
- const source = readFileSync(file, "utf8");
173
+ const source = readFileSync(file, "utf8").replace(compassBlockRe, "");
168
174
  const banned = file === inventoryFile ? bannedInInventory : bannedPathLiterals;
169
175
  for (const literal of banned) {
170
176
  if (source.includes(literal)) {
@@ -2,12 +2,14 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { execFileSync } from "node:child_process";
5
6
  import {
6
7
  runCompassSummaryRecord,
7
8
  parseChangeSummary,
8
9
  mergeHistoryIds,
9
10
  buildChangeSummaryBlock,
10
11
  stripConventionalPrefix,
12
+ stripGitTrailers,
11
13
  isValidGovernanceId,
12
14
  CHANGE_SUMMARY_WINDOW,
13
15
  } from "../summary-record.ts";
@@ -160,6 +162,51 @@ describe("compass.summary.record", () => {
160
162
  expect(missing.exitCode).toBe(1);
161
163
  });
162
164
 
165
+ it("skips auto-inject when the worktree diff already adds a same-ID item (id-in-diff)", async () => {
166
+ execFileSync("git", ["init"], { cwd: root });
167
+ execFileSync("git", ["config", "user.email", "t@t"], { cwd: root });
168
+ execFileSync("git", ["config", "user.name", "T"], { cwd: root });
169
+ await writeFile(join(root, "src", "a.ts"), TS_HEADER(["RFC-0001: existing item."]));
170
+ execFileSync("git", ["add", "-A"], { cwd: root });
171
+ execFileSync("git", ["commit", "-m", "baseline"], { cwd: root });
172
+ // The pending change carries a hand-authored descriptive item for the ID.
173
+ await writeFile(
174
+ join(root, "src", "a.ts"),
175
+ TS_HEADER(["RFC-0001: existing item.", "RFC-1095: descriptive hand-written entry"]),
176
+ );
177
+
178
+ const result = await runCompassSummaryRecord(
179
+ makeInput({ id: "RFC-1095", files: ["src/a.ts"], text: "generic subject line" }),
180
+ makeContext(root),
181
+ );
182
+
183
+ expect(result.data?.skipped).toEqual([{ file: "src/a.ts", reason: "id-in-diff" }]);
184
+ expect(result.data?.recorded).toEqual([]);
185
+ const source = await readFile(join(root, "src", "a.ts"), "utf8");
186
+ expect(source).not.toContain("generic subject line");
187
+ });
188
+
189
+ it("still records when the same-ID item already exists at HEAD (consecutive-commit stack)", async () => {
190
+ execFileSync("git", ["init"], { cwd: root });
191
+ execFileSync("git", ["config", "user.email", "t@t"], { cwd: root });
192
+ execFileSync("git", ["config", "user.name", "T"], { cwd: root });
193
+ await writeFile(
194
+ join(root, "src", "a.ts"),
195
+ TS_HEADER(["RFC-1095: step 1 — recorded by the previous commit"]),
196
+ );
197
+ execFileSync("git", ["add", "-A"], { cwd: root });
198
+ execFileSync("git", ["commit", "-m", "baseline"], { cwd: root });
199
+
200
+ const result = await runCompassSummaryRecord(
201
+ makeInput({ id: "RFC-1095", files: ["src/a.ts"], text: "step 2" }),
202
+ makeContext(root),
203
+ );
204
+
205
+ expect(result.data?.recorded).toEqual(["src/a.ts"]);
206
+ const source = await readFile(join(root, "src", "a.ts"), "utf8");
207
+ expect(source).toContain("<item>RFC-1095: step 2</item>");
208
+ });
209
+
163
210
  it("defaults item text to the bare ID when --text is absent", async () => {
164
211
  await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
165
212
  await runCompassSummaryRecord(
@@ -191,6 +238,65 @@ describe("compass.summary.record", () => {
191
238
  expect(source).toContain("<item>RFC-1095: record command</item>");
192
239
  expect(source).not.toContain("RFC-1095: RFC-1095");
193
240
  });
241
+
242
+ it("strips Generated-with/Co-Authored-By trailer blocks from the recorded item (RFC-1233 AC-1)", async () => {
243
+ await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
244
+ await runCompassSummaryRecord(
245
+ makeInput({
246
+ id: "RFC-1233",
247
+ files: ["src/a.ts"],
248
+ text: "trailer stripping\n\nGenerated with [Devin](https://devin.ai)\nCo-Authored-By: Devin <bot@users.noreply.github.com>",
249
+ }),
250
+ makeContext(root),
251
+ );
252
+ const source = await readFile(join(root, "src", "a.ts"), "utf8");
253
+ expect(source).toContain("<item>RFC-1233: trailer stripping</item>");
254
+ expect(source).not.toContain("Co-Authored-By");
255
+ expect(source).not.toContain("Generated with");
256
+ expect(source).not.toContain("devin.ai");
257
+ });
258
+
259
+ it("neutralizes a literal star-slash in --text so the item cannot terminate the block comment (RFC-1233 AC-2)", async () => {
260
+ await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
261
+ await runCompassSummaryRecord(
262
+ makeInput({
263
+ id: "RFC-1233",
264
+ files: ["src/a.ts"],
265
+ text: "glob src" + "/**/*.ts in comments",
266
+ }),
267
+ makeContext(root),
268
+ );
269
+ const source = await readFile(join(root, "src", "a.ts"), "utf8");
270
+ expect(source).toContain("RFC-1233: glob src");
271
+ expect(source.match(CS_BLOCK_RE)).not.toBeNull();
272
+ // The neutralized form keeps both characters readable but cannot close /*.
273
+ expect(source).toContain("glob src/**" + "∕*.ts in comments");
274
+ });
275
+
276
+ it("keeps a single-line subject identical — negative regression (RFC-1233 AC-3)", async () => {
277
+ await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
278
+ await runCompassSummaryRecord(
279
+ makeInput({ id: "RFC-1233", files: ["src/a.ts"], text: "plain subject line" }),
280
+ makeContext(root),
281
+ );
282
+ const source = await readFile(join(root, "src", "a.ts"), "utf8");
283
+ expect(source).toContain("<item>RFC-1233: plain subject line</item>");
284
+ });
285
+
286
+ it("preserves a Key: value-shaped line when it is not in the trailing block (RFC-1233 AC-4)", async () => {
287
+ await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
288
+ await runCompassSummaryRecord(
289
+ makeInput({
290
+ id: "RFC-1233",
291
+ files: ["src/a.ts"],
292
+ text: "fix the parser\n\nNote: this line is prose about the fix\nfinal summary line",
293
+ }),
294
+ makeContext(root),
295
+ );
296
+ const source = await readFile(join(root, "src", "a.ts"), "utf8");
297
+ expect(source).toContain("Note: this line is prose about the fix");
298
+ expect(source).toContain("final summary line");
299
+ });
194
300
  });
195
301
 
196
302
  describe("summary-record helpers", () => {
@@ -246,4 +352,25 @@ describe("summary-record helpers", () => {
246
352
  expect(block).not.toContain("<history>");
247
353
  expect(block).toContain("<item>RFC-1: a</item>");
248
354
  });
355
+
356
+ it("stripGitTrailers strips a Token:-shaped tail block (RFC-1233)", () => {
357
+ expect(
358
+ stripGitTrailers("subject\n\nbody line\nCo-Authored-By: Devin <bot@x>\nX-RFC: RFC-1233"),
359
+ ).toBe("subject\n\nbody line");
360
+ });
361
+
362
+ it("stripGitTrailers keeps a lone markdown-link tail (no strict trailer line)", () => {
363
+ const msg = "subject\n\nsee [the docs](https://example.com)";
364
+ expect(stripGitTrailers(msg)).toBe(msg);
365
+ });
366
+
367
+ it("stripGitTrailers on a trailer-only message returns empty", () => {
368
+ expect(stripGitTrailers("Co-Authored-By: Devin <bot@x>")).toBe("");
369
+ });
370
+
371
+ it("stripGitTrailers strips empty-value trailers (Token:) — interpret-trailers allows them", () => {
372
+ expect(stripGitTrailers("subject\n\nReviewed-by:\nSigned-off-by: Devin <bot@x>")).toBe(
373
+ "subject",
374
+ );
375
+ });
249
376
  });