@warpgogol/forge 2.7.0 → 2.8.1

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 (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +51 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +217 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,272 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>CHANGE_SUMMARY classification, validation, and deterministic tidy logic
4
+ per RFC-0349. Moved from @warpgogol/site-kernel-checks to @warpgogol/forge for full
5
+ autonomous mode (RFC-0556). Provides compass.changesummary.validate and compass.summary.trim.</purpose>
6
+ <non-goals>
7
+ <item>Do not audit truthfulness of CHANGE_SUMMARY items against code — that is RFC-0352.</item>
8
+ <item>Do not delete protected (RFC/code-referencing) items under any circumstance.</item>
9
+ <item>Do not use an LLM — classification and tidy are purely deterministic.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0349: initial implementation of classifyChangeSummaryItem, compass.changesummary.validate, and compass.changesummary.tidy.</item>
14
+ <item>RFC-0538: renamed compass.changesummary.tidy to compass.summary.trim, raised cap from 3 unprotected to 30 total items, aligned validate cap to 30 total.</item>
15
+ <item>RFC-0556: moved from @warpgogol/site-kernel-checks to @warpgogol/forge for autonomous mode.</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import { resolve } from "node:path";
20
+ import { readFile } from "node:fs/promises";
21
+ import { createCompassInventoryEntries } from "./compass-inventory.ts";
22
+ import { resolveCompassScanRoot } from "./resolve-scan-root.ts";
23
+ import { writeFileIfChanged } from "../../../src/utils/fs-idempotent.ts";
24
+ import type {
25
+ Diagnostic,
26
+ ForgeCommandInput,
27
+ ForgeCommandResult,
28
+ ForgeRuntimeContext,
29
+ } from "../../../src/types.ts";
30
+
31
+ const PROTECTED_RE = /\b([A-Z][A-Z0-9]*-)+\d+\b/;
32
+ const BOILERPLATE_RE =
33
+ /^(Wave\s+\d|Backfill\b|Annotate Compass|Annotation revision|Initial creation|Compass scaffolding|Created as part of|Enhance .* with Compass)/i;
34
+
35
+ export type ChangeSummaryItemClass = "protected" | "boilerplate" | "unprotected";
36
+
37
+ export function classifyChangeSummaryItem(text: string): ChangeSummaryItemClass {
38
+ if (PROTECTED_RE.test(text)) return "protected";
39
+ if (BOILERPLATE_RE.test(text.trim())) return "boilerplate";
40
+ return "unprotected";
41
+ }
42
+
43
+ function extractChangeSummaryBlock(source: string): string | null {
44
+ const match = source.match(/<CHANGE_SUMMARY>[\s\S]*?<\/CHANGE_SUMMARY>/);
45
+ return match?.[0] ?? null;
46
+ }
47
+
48
+ function extractItems(block: string): string[] {
49
+ const items: string[] = [];
50
+ const re = /<item>([\s\S]*?)<\/item>/g;
51
+ let m: RegExpExecArray | null;
52
+ while ((m = re.exec(block)) !== null) {
53
+ items.push(m[1]!.trim());
54
+ }
55
+ return items;
56
+ }
57
+
58
+ function rebuildChangeSummaryBlock(items: string[]): string {
59
+ const lines = ["<CHANGE_SUMMARY>"];
60
+ for (const item of items) {
61
+ lines.push(` <item>${item}</item>`);
62
+ }
63
+ lines.push("</CHANGE_SUMMARY>");
64
+ return lines.join("\n");
65
+ }
66
+
67
+ function replaceChangeSummaryBlock(source: string, newBlock: string): string {
68
+ return source.replace(/<CHANGE_SUMMARY>[\s\S]*?<\/CHANGE_SUMMARY>/, newBlock);
69
+ }
70
+
71
+ const MAX_TOTAL_ITEMS = 30;
72
+
73
+ export async function runCompassChangeSummaryValidate(
74
+ input: ForgeCommandInput,
75
+ context: ForgeRuntimeContext,
76
+ ): Promise<
77
+ ForgeCommandResult<{
78
+ command: string;
79
+ status: "pass" | "fail";
80
+ diagnostics: Diagnostic[];
81
+ checkedFiles: number;
82
+ }>
83
+ > {
84
+ const scanRoot = resolveCompassScanRoot(input, context);
85
+ const entries = await createCompassInventoryEntries(context.workspaceRoot, input, scanRoot);
86
+
87
+ const diagnostics: Diagnostic[] = [];
88
+ let checkedFiles = 0;
89
+
90
+ for (const entry of entries) {
91
+ if (entry.authoringStatus !== "authored" || entry.requiredScaffolding === "none") {
92
+ continue;
93
+ }
94
+
95
+ checkedFiles++;
96
+
97
+ if (!entry.hasChangeSummary) {
98
+ continue;
99
+ }
100
+
101
+ const absPath = resolve(context.workspaceRoot, entry.path);
102
+ const source = await readFile(absPath, "utf8");
103
+ const block = extractChangeSummaryBlock(source);
104
+ if (!block) continue;
105
+
106
+ const items = extractItems(block);
107
+ let hasBoilerplate = false;
108
+ let totalItems = 0;
109
+
110
+ for (const item of items) {
111
+ const cls = classifyChangeSummaryItem(item);
112
+ if (cls === "boilerplate") {
113
+ hasBoilerplate = true;
114
+ }
115
+ totalItems++;
116
+ }
117
+
118
+ if (hasBoilerplate) {
119
+ diagnostics.push({
120
+ ruleId: "COMPASS-CS-01",
121
+ severity: "error",
122
+ file: entry.path,
123
+ message: "CHANGE_SUMMARY contains a boilerplate item",
124
+ fixHint: "fix: run compass.summary.trim",
125
+ });
126
+ }
127
+
128
+ if (totalItems > MAX_TOTAL_ITEMS) {
129
+ diagnostics.push({
130
+ ruleId: "COMPASS-CS-02",
131
+ severity: "error",
132
+ file: entry.path,
133
+ message: `CHANGE_SUMMARY has ${totalItems} total items (cap is ${MAX_TOTAL_ITEMS})`,
134
+ fixHint: `fix: run compass.summary.trim (cap is ${MAX_TOTAL_ITEMS} total items)`,
135
+ });
136
+ }
137
+ }
138
+
139
+ const failed = diagnostics.length > 0;
140
+
141
+ for (const d of diagnostics) {
142
+ context.logger.error(`[compass.changesummary.validate] ${d.ruleId}: ${d.file}: ${d.message}`);
143
+ }
144
+
145
+ return {
146
+ data: {
147
+ command: "compass.changesummary.validate",
148
+ status: failed ? "fail" : "pass",
149
+ diagnostics,
150
+ checkedFiles,
151
+ },
152
+ exitCode: failed ? 1 : 0,
153
+ summary: failed
154
+ ? undefined
155
+ : `[compass.changesummary.validate] OK (${checkedFiles} authored files checked)`,
156
+ };
157
+ }
158
+
159
+ const TRIM_FALLBACK_ITEM = "Tidied by compass.summary.trim; see git history for prior entries.";
160
+
161
+ export async function runCompassSummaryTrim(
162
+ input: ForgeCommandInput,
163
+ context: ForgeRuntimeContext,
164
+ ): Promise<
165
+ ForgeCommandResult<{
166
+ command: string;
167
+ status: "ok";
168
+ files: Array<{ path: string; removed: string[]; kept: number }>;
169
+ }>
170
+ > {
171
+ if (context.dryRun) {
172
+ context.logger.info(`[compass.summary.trim] dry-run active — will not apply changes`);
173
+ }
174
+
175
+ const scanRoot = resolveCompassScanRoot(input, context);
176
+ const entries = await createCompassInventoryEntries(context.workspaceRoot, input, scanRoot);
177
+
178
+ const results: Array<{ path: string; removed: string[]; kept: number }> = [];
179
+
180
+ for (const entry of entries) {
181
+ if (entry.authoringStatus !== "authored" || entry.requiredScaffolding === "none") {
182
+ continue;
183
+ }
184
+
185
+ if (!entry.hasChangeSummary) {
186
+ continue;
187
+ }
188
+
189
+ const absPath = resolve(context.workspaceRoot, entry.path);
190
+ const source = await readFile(absPath, "utf8");
191
+ const block = extractChangeSummaryBlock(source);
192
+ if (!block) continue;
193
+
194
+ const items = extractItems(block);
195
+ const keptItems: string[] = [];
196
+ const removedItems: string[] = [];
197
+
198
+ const unprotectedItems: Array<{ index: number; text: string }> = [];
199
+
200
+ for (let i = 0; i < items.length; i++) {
201
+ const text = items[i]!;
202
+ const cls = classifyChangeSummaryItem(text);
203
+
204
+ if (cls === "protected") {
205
+ keptItems.push(text);
206
+ } else if (cls === "boilerplate") {
207
+ removedItems.push(text);
208
+ } else {
209
+ unprotectedItems.push({ index: i, text });
210
+ }
211
+ }
212
+
213
+ const protectedCount = keptItems.length;
214
+ const maxUnprotectedToKeep = Math.max(0, MAX_TOTAL_ITEMS - protectedCount);
215
+ const unprotectedToKeep = unprotectedItems.slice(-maxUnprotectedToKeep);
216
+ const unprotectedToRemove = unprotectedItems.slice(0, -maxUnprotectedToKeep);
217
+
218
+ for (const item of unprotectedToRemove) {
219
+ removedItems.push(item.text);
220
+ }
221
+
222
+ const keepSet = new Set<string>();
223
+ for (const item of unprotectedToKeep) {
224
+ keepSet.add(item.text);
225
+ }
226
+
227
+ const finalItems: string[] = [];
228
+ const protectedTexts = new Set(keptItems);
229
+ const unprotectedTexts = new Set(unprotectedToKeep.map((u) => u.text));
230
+
231
+ for (const originalItem of items) {
232
+ if (protectedTexts.has(originalItem) || unprotectedTexts.has(originalItem)) {
233
+ finalItems.push(originalItem);
234
+ }
235
+ }
236
+
237
+ if (finalItems.length === 0) {
238
+ finalItems.push(TRIM_FALLBACK_ITEM);
239
+ }
240
+
241
+ const newBlock = rebuildChangeSummaryBlock(finalItems);
242
+ const transformed = replaceChangeSummaryBlock(source, newBlock);
243
+
244
+ if (transformed === source) {
245
+ continue;
246
+ }
247
+
248
+ if (!context.dryRun) {
249
+ await writeFileIfChanged(absPath, transformed);
250
+ }
251
+
252
+ context.logger.info(
253
+ `[compass.summary.trim] trimmed: ${entry.path} (removed ${removedItems.length}, kept ${finalItems.length})`,
254
+ );
255
+
256
+ results.push({
257
+ path: entry.path,
258
+ removed: removedItems,
259
+ kept: finalItems.length,
260
+ });
261
+ }
262
+
263
+ return {
264
+ data: {
265
+ command: "compass.summary.trim",
266
+ status: "ok",
267
+ files: results,
268
+ },
269
+ exitCode: 0,
270
+ summary: `[compass.summary.trim] files=${results.length}, removed=${results.reduce((sum, r) => sum + r.removed.length, 0)}`,
271
+ };
272
+ }
@@ -0,0 +1,294 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Compass inventory and validation command handlers. Moved from
4
+ @warpgogol/site-kernel-checks to @warpgogol/forge for full autonomous mode (RFC-0556).
5
+ Provides runCompassInventory (XML report generation) and runCompassValidation
6
+ (compliance diagnostics with COMPASS-* rules).</purpose>
7
+ <non-goals>
8
+ <item>Do not handle raw file parsing or content analysis.</item>
9
+ <item>Do not manage configuration or orchestration of external services.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0348: v2 two-block contract — XML output updated, compass.validate emits COMPASS-* diagnostics, summary uses standard-required-files.</item>
14
+ <item>RFC-0350: added COMPASS-TODO-01 diagnostic for unfilled Compass TODO sentinels.</item>
15
+ <item>RFC-0556: moved from @warpgogol/site-kernel-checks to @warpgogol/forge for autonomous mode.</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import { resolve } from "node:path";
20
+ import { readFile } from "node:fs/promises";
21
+ import { createCompassInventoryEntries, type CompassInventoryEntry } from "./compass-inventory.ts";
22
+ import { resolveCompassScanRoot } from "./resolve-scan-root.ts";
23
+ import { writeFileIfChanged } from "../../../src/utils/fs-idempotent.ts";
24
+ import type {
25
+ ForgeCommandInput,
26
+ ForgeCommandResult,
27
+ ForgeRuntimeContext,
28
+ } from "../../../src/types.ts";
29
+
30
+ const INVENTORY_OUTPUT_PATH = "docs/compass-inventory.xml";
31
+
32
+ interface CompassInventorySummary {
33
+ scannedFiles: number;
34
+ authoredFiles: number;
35
+ excludedFiles: number;
36
+ standardRequiredFiles: number;
37
+ compliantFiles: number;
38
+ nonCompliantFiles: number;
39
+ }
40
+
41
+ function summarizeInventory(entries: CompassInventoryEntry[]): CompassInventorySummary {
42
+ let authoredFiles = 0;
43
+ let excludedFiles = 0;
44
+ let standardRequiredFiles = 0;
45
+ let compliantFiles = 0;
46
+ let nonCompliantFiles = 0;
47
+
48
+ for (const entry of entries) {
49
+ if (entry.authoringStatus === "excluded") {
50
+ excludedFiles += 1;
51
+ continue;
52
+ }
53
+
54
+ authoredFiles += 1;
55
+ if (entry.requiredScaffolding === "standard") standardRequiredFiles += 1;
56
+
57
+ if (entry.compliant) {
58
+ compliantFiles += 1;
59
+ } else {
60
+ nonCompliantFiles += 1;
61
+ }
62
+ }
63
+
64
+ return {
65
+ scannedFiles: entries.length,
66
+ authoredFiles,
67
+ excludedFiles,
68
+ standardRequiredFiles,
69
+ compliantFiles,
70
+ nonCompliantFiles,
71
+ };
72
+ }
73
+
74
+ function escapeXml(value: string): string {
75
+ return value
76
+ .replace(/&/g, "&amp;")
77
+ .replace(/</g, "&lt;")
78
+ .replace(/>/g, "&gt;")
79
+ .replace(/"/g, "&quot;")
80
+ .replace(/'/g, "&apos;");
81
+ }
82
+
83
+ function renderInventoryXml(
84
+ entries: CompassInventoryEntry[],
85
+ summary: CompassInventorySummary,
86
+ ): string {
87
+ const lines: string[] = [];
88
+ lines.push("<compass-inventory>");
89
+ lines.push(" <meta>");
90
+ lines.push(" <document-id>compass-inventory</document-id>");
91
+ lines.push(" <version>2.0.0</version>");
92
+ lines.push(" <status>generated</status>");
93
+ lines.push(" <scope>repository-root</scope>");
94
+ lines.push(` <generated-at>null</generated-at>`);
95
+ lines.push(" <generator>@warpgogol/forge:compass.inventory</generator>");
96
+ lines.push(" </meta>");
97
+ lines.push(" <summary>");
98
+ lines.push(` <scanned-files>${summary.scannedFiles}</scanned-files>`);
99
+ lines.push(` <authored-files>${summary.authoredFiles}</authored-files>`);
100
+ lines.push(` <excluded-files>${summary.excludedFiles}</excluded-files>`);
101
+ lines.push(
102
+ ` <standard-required-files>${summary.standardRequiredFiles}</standard-required-files>`,
103
+ );
104
+ lines.push(` <compliant-files>${summary.compliantFiles}</compliant-files>`);
105
+ lines.push(` <non-compliant-files>${summary.nonCompliantFiles}</non-compliant-files>`);
106
+ lines.push(" </summary>");
107
+ lines.push(" <entries>");
108
+
109
+ for (const entry of entries) {
110
+ lines.push(
111
+ ` <entry path="${escapeXml(entry.path)}" workspace-kind="${entry.workspaceKind}" workspace-name="${escapeXml(entry.workspaceName)}" layer="${escapeXml(entry.layer)}" extension="${escapeXml(entry.extension)}" authoring-status="${entry.authoringStatus}" risk-class="${entry.riskClass}" complexity="${entry.complexity}" required-scaffolding="${entry.requiredScaffolding}" compliant="${entry.compliant ? "true" : "false"}">`,
112
+ );
113
+ lines.push(` <non-empty-lines>${entry.nonEmptyLineCount}</non-empty-lines>`);
114
+ if (entry.exclusionReason) {
115
+ lines.push(` <exclusion-reason>${escapeXml(entry.exclusionReason)}</exclusion-reason>`);
116
+ }
117
+ lines.push(
118
+ ` <has-module-contract>${entry.hasModuleContract ? "true" : "false"}</has-module-contract>`,
119
+ );
120
+ lines.push(
121
+ ` <has-change-summary>${entry.hasChangeSummary ? "true" : "false"}</has-change-summary>`,
122
+ );
123
+ lines.push(
124
+ ` <has-ai-invariant>${entry.hasAiInvariant ? "true" : "false"}</has-ai-invariant>`,
125
+ );
126
+ lines.push(` <has-purpose>${entry.hasPurpose ? "true" : "false"}</has-purpose>`);
127
+ lines.push(` <has-non-goals>${entry.hasNonGoals ? "true" : "false"}</has-non-goals>`);
128
+ if (entry.forbiddenPresent.length > 0) {
129
+ lines.push(" <forbidden-markers>");
130
+ for (const marker of entry.forbiddenPresent) {
131
+ lines.push(` <marker>${escapeXml(marker)}</marker>`);
132
+ }
133
+ lines.push(" </forbidden-markers>");
134
+ }
135
+ if (entry.violations.length > 0) {
136
+ lines.push(" <violations>");
137
+ for (const violation of entry.violations) {
138
+ lines.push(` <violation>${escapeXml(violation)}</violation>`);
139
+ }
140
+ lines.push(" </violations>");
141
+ }
142
+ lines.push(" </entry>");
143
+ }
144
+
145
+ lines.push(" </entries>");
146
+ lines.push("</compass-inventory>");
147
+ lines.push("");
148
+ return lines.join("\n");
149
+ }
150
+
151
+ export async function runCompassInventory(
152
+ input: ForgeCommandInput,
153
+ context: ForgeRuntimeContext,
154
+ ): Promise<
155
+ ForgeCommandResult<{ entries: number; outputPath: string; summary: CompassInventorySummary }>
156
+ > {
157
+ const scanRoot = resolveCompassScanRoot(input, context) ?? context.workspaceRoot;
158
+ const entries = await createCompassInventoryEntries(scanRoot, input);
159
+ const summary = summarizeInventory(entries);
160
+
161
+ context.logger.info(
162
+ `[compass.inventory] scanned=${summary.scannedFiles} authored=${summary.authoredFiles} excluded=${summary.excludedFiles} standard=${summary.standardRequiredFiles}`,
163
+ );
164
+
165
+ if (context.dryRun) {
166
+ context.logger.warn(
167
+ `[compass.inventory] dry-run active — skipped writing ${INVENTORY_OUTPUT_PATH}`,
168
+ );
169
+ return {
170
+ data: { entries: entries.length, outputPath: INVENTORY_OUTPUT_PATH, summary },
171
+ summary: `[compass.inventory] previewed ${INVENTORY_OUTPUT_PATH}`,
172
+ };
173
+ }
174
+
175
+ const xml = renderInventoryXml(entries, summary);
176
+ const outputPath = resolve(context.workspaceRoot, INVENTORY_OUTPUT_PATH);
177
+ await writeFileIfChanged(outputPath, xml);
178
+
179
+ return {
180
+ data: { entries: entries.length, outputPath, summary },
181
+ summary: `[compass.inventory] ${context.dryRun ? "previewed" : "wrote"} ${INVENTORY_OUTPUT_PATH}`,
182
+ };
183
+ }
184
+
185
+ export async function runCompassValidation(
186
+ input: ForgeCommandInput,
187
+ context: ForgeRuntimeContext,
188
+ ): Promise<
189
+ ForgeCommandResult<{
190
+ checkedFiles: number;
191
+ failures: number;
192
+ summary: CompassInventorySummary;
193
+ diagnostics: Array<{
194
+ ruleId: string;
195
+ severity: string;
196
+ file: string;
197
+ message: string;
198
+ fix: string;
199
+ }>;
200
+ }>
201
+ > {
202
+ const scanRoot = resolveCompassScanRoot(input, context);
203
+ const entries = await createCompassInventoryEntries(context.workspaceRoot, input, scanRoot);
204
+ const summary = summarizeInventory(entries);
205
+ const failures = entries.filter(
206
+ (entry) =>
207
+ entry.authoringStatus === "authored" &&
208
+ entry.requiredScaffolding !== "none" &&
209
+ !entry.compliant,
210
+ );
211
+
212
+ const diagnostics: Array<{
213
+ ruleId: string;
214
+ severity: string;
215
+ file: string;
216
+ message: string;
217
+ fix: string;
218
+ }> = [];
219
+
220
+ for (const failure of failures) {
221
+ for (const violation of failure.violations) {
222
+ let ruleId = "COMPASS-CONTRACT-01";
223
+ let fix = "fix: add a MODULE_CONTRACT block with <purpose> and <non-goals>";
224
+
225
+ if (violation.includes("purpose missing")) {
226
+ ruleId = "COMPASS-CONTRACT-02";
227
+ fix = "fix: write a <purpose> of at least 10 words";
228
+ } else if (violation.includes("non-goals")) {
229
+ ruleId = "COMPASS-CONTRACT-03";
230
+ fix = "fix: add at least one <non-goals><item>";
231
+ } else if (violation.includes("CHANGE_SUMMARY")) {
232
+ ruleId = "COMPASS-CONTRACT-04";
233
+ fix = "fix: add a CHANGE_SUMMARY with at least one <item>";
234
+ } else if (violation.includes("@ai-invariant")) {
235
+ ruleId = "COMPASS-INVARIANT-01";
236
+ fix = "fix: add // @ai-invariant capturing the non-obvious constraint";
237
+ } else if (violation.includes("forbidden")) {
238
+ ruleId = "COMPASS-FORBIDDEN-01";
239
+ const marker = violation.replace(/forbidden: ([^ ]+) present/, "$1");
240
+ fix = `fix: remove ${marker}; run fo-compass-annotate skill`;
241
+ }
242
+
243
+ context.logger.error(`[compass.validate] ${ruleId}: ${failure.path}: ${violation}`);
244
+ diagnostics.push({
245
+ ruleId,
246
+ severity: "error",
247
+ file: failure.path,
248
+ message: violation,
249
+ fix,
250
+ });
251
+ }
252
+ }
253
+
254
+ const compassTodoLabel = "TODO" + "(compass)";
255
+ const TODO_COMPASS_RE = new RegExp("TODO" + "\\\\(compass\\\\)");
256
+ for (const entry of entries) {
257
+ if (entry.authoringStatus !== "authored" || entry.requiredScaffolding === "none") {
258
+ continue;
259
+ }
260
+ if (!entry.hasModuleContract && !entry.hasChangeSummary) {
261
+ continue;
262
+ }
263
+
264
+ const absPath = resolve(context.workspaceRoot, entry.path);
265
+ const source = await readFile(absPath, "utf8");
266
+ if (TODO_COMPASS_RE.test(source)) {
267
+ context.logger.error(
268
+ `[compass.validate] COMPASS-TODO-01: ${entry.path}: unfilled ${compassTodoLabel} sentinel in Compass block`,
269
+ );
270
+ diagnostics.push({
271
+ ruleId: "COMPASS-TODO-01",
272
+ severity: "error",
273
+ file: entry.path,
274
+ message: `Unfilled ${compassTodoLabel} sentinel in Compass block`,
275
+ fix: `fix: replace the ${compassTodoLabel} sentinel with a real value`,
276
+ });
277
+ }
278
+ }
279
+
280
+ const hasFailures = diagnostics.length > 0;
281
+
282
+ return {
283
+ data: {
284
+ checkedFiles: summary.authoredFiles,
285
+ failures: failures.length,
286
+ summary,
287
+ diagnostics,
288
+ },
289
+ exitCode: hasFailures ? 1 : 0,
290
+ summary: hasFailures
291
+ ? undefined
292
+ : `[compass.validate] OK (${summary.authoredFiles} authored files checked)`,
293
+ };
294
+ }