@warpgogol/forge 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -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 +54 -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 +221 -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/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,382 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Compass audit command handlers (plan, record, baseline, validate).
4
+ Moved from @warpgogol/site-kernel-checks to @warpgogol/forge for full autonomous
5
+ mode (RFC-0556). Drives per-file semantic-truth auditing on a revision cadence (RFC-0352).</purpose>
6
+ <non-goals>
7
+ <item>Do not perform semantic comparison inside a command — commands are deterministic; the code-vs-prose judgment is the agent's.</item>
8
+ <item>Do not call an LLM or read any API key.</item>
9
+ <item>Do not replace compass.validate — this adds a heavy truth audit on top.</item>
10
+ </non-goals>
11
+ </MODULE_CONTRACT>
12
+ <CHANGE_SUMMARY>
13
+ <item>RFC-0352: initial implementation of compass.audit.plan, compass.audit.record, compass.audit.baseline, compass.audit.validate.</item>
14
+ <item>RFC-0556: moved from @warpgogol/site-kernel-checks to @warpgogol/forge for autonomous mode.</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ import { readFile } from "node:fs/promises";
19
+ import { resolve, relative } from "node:path";
20
+ import { execFile } from "node:child_process";
21
+ import { promisify } from "node:util";
22
+ import { createCompassInventoryEntries } from "./compass-inventory.ts";
23
+ import { resolveCompassScanRoot } from "./resolve-scan-root.ts";
24
+ import { getRevisionByPath } from "./git-revision.ts";
25
+ import type { CompassInventoryEntry } from "./compass-inventory.ts";
26
+ import { writeFileAtomic } from "../../../src/utils/fs-atomic.ts";
27
+ import { buildGeneratedHeader } from "../../../src/utils/generated-marker.ts";
28
+ import type {
29
+ ForgeCommandInput,
30
+ ForgeCommandResult,
31
+ ForgeRuntimeContext,
32
+ } from "../../../src/types.ts";
33
+ import { parse as yamlParse, stringify as yamlStringify } from "yaml";
34
+
35
+ const execFileAsync = promisify(execFile);
36
+ const LEDGER_PATH = "docs/compass-audit-ledger.generated.yaml";
37
+ const DEFAULT_THRESHOLD = 30;
38
+
39
+ function resolveRevisionThreshold(value: unknown, fallback: number): number {
40
+ if (value === undefined) return fallback;
41
+ const parsed = typeof value === "number" ? value : Number(String(value));
42
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
43
+ }
44
+
45
+ type CompassAuditVerdict = "pass" | "repaired" | "baseline";
46
+
47
+ interface CompassAuditLedgerEntry {
48
+ path: string;
49
+ entityId: string;
50
+ auditedRevision: number;
51
+ auditedHash: string;
52
+ auditedAt: string;
53
+ verdict: CompassAuditVerdict;
54
+ agent: string;
55
+ }
56
+
57
+ interface CompassAuditLedger {
58
+ revisionThreshold: number;
59
+ entries: CompassAuditLedgerEntry[];
60
+ }
61
+
62
+ interface CompassAuditWorkOrderItem {
63
+ path: string;
64
+ currentRevision: number;
65
+ auditedRevision: number | null;
66
+ reason: "never-audited" | "revision-threshold-crossed";
67
+ moduleContract: string;
68
+ changeSummary: string;
69
+ }
70
+
71
+ export function isAuditDue(current: number, audited: number | null, threshold: number): boolean {
72
+ if (audited === null) return true;
73
+ return current - audited >= threshold;
74
+ }
75
+
76
+ const _LEDGER_ADVISORY = { ownerCommand: "compass.audit.record" };
77
+
78
+ function withLedgerAdvisory(ledger: Partial<CompassAuditLedger>): CompassAuditLedger {
79
+ return {
80
+ revisionThreshold: ledger.revisionThreshold ?? DEFAULT_THRESHOLD,
81
+ entries: Array.isArray(ledger.entries) ? ledger.entries : [],
82
+ };
83
+ }
84
+
85
+ async function loadLedger(workspaceRoot: string): Promise<CompassAuditLedger> {
86
+ const abs = resolve(workspaceRoot, LEDGER_PATH);
87
+ try {
88
+ const content = await readFile(abs, "utf8");
89
+ return withLedgerAdvisory(yamlParse(content) as Partial<CompassAuditLedger>);
90
+ } catch {
91
+ return withLedgerAdvisory({});
92
+ }
93
+ }
94
+
95
+ async function saveLedger(workspaceRoot: string, ledger: CompassAuditLedger): Promise<void> {
96
+ const normalized = withLedgerAdvisory(ledger);
97
+ normalized.entries.sort((a, b) => a.path.localeCompare(b.path));
98
+ const abs = resolve(workspaceRoot, LEDGER_PATH);
99
+ const header = buildGeneratedHeader({
100
+ ownerCommand: "compass.audit.record",
101
+ filePath: LEDGER_PATH,
102
+ });
103
+ const yaml = header + yamlStringify(normalized) + "\n";
104
+ await writeFileAtomic(abs, yaml);
105
+ }
106
+
107
+ function extractBlock(source: string, tagName: string): string {
108
+ const match = source.match(new RegExp(`<${tagName}>[\\s\\S]*?<\\/${tagName}>`));
109
+ return match?.[0] ?? "";
110
+ }
111
+
112
+ async function getGitUser(workspaceRoot: string): Promise<string> {
113
+ try {
114
+ const { stdout } = await execFileAsync("git", ["config", "user.name"], {
115
+ cwd: workspaceRoot,
116
+ });
117
+ return `human:${stdout.trim()}`;
118
+ } catch {
119
+ return "human:unknown";
120
+ }
121
+ }
122
+
123
+ function getAuthoredEntries(entries: CompassInventoryEntry[]): CompassInventoryEntry[] {
124
+ return entries.filter(
125
+ (e) => e.authoringStatus === "authored" && e.requiredScaffolding !== "none",
126
+ );
127
+ }
128
+
129
+ export async function runCompassAuditPlan(
130
+ input: ForgeCommandInput,
131
+ context: ForgeRuntimeContext,
132
+ ): Promise<
133
+ ForgeCommandResult<{
134
+ threshold: number;
135
+ dueCount: number;
136
+ items: CompassAuditWorkOrderItem[];
137
+ }>
138
+ > {
139
+ const scanRoot = resolveCompassScanRoot(input, context);
140
+ const entries = await createCompassInventoryEntries(context.workspaceRoot, input, scanRoot);
141
+ const authored = getAuthoredEntries(entries);
142
+ const ledger = await loadLedger(context.workspaceRoot);
143
+
144
+ const threshold = resolveRevisionThreshold(input.flags["threshold"], ledger.revisionThreshold);
145
+
146
+ const ledgerMap = new Map<string, CompassAuditLedgerEntry>();
147
+ for (const e of ledger.entries) {
148
+ ledgerMap.set(e.path, e);
149
+ }
150
+
151
+ const items: CompassAuditWorkOrderItem[] = [];
152
+
153
+ for (const entry of authored) {
154
+ const ledgerEntry = ledgerMap.get(entry.path);
155
+ const auditedRevision = ledgerEntry?.auditedRevision ?? null;
156
+
157
+ const { revision: currentRevision } = await getRevisionByPath(
158
+ context.workspaceRoot,
159
+ entry.path,
160
+ );
161
+
162
+ if (!isAuditDue(currentRevision, auditedRevision, threshold)) {
163
+ continue;
164
+ }
165
+
166
+ const absPath = resolve(context.workspaceRoot, entry.path);
167
+ const source = await readFile(absPath, "utf8");
168
+ const moduleContract = extractBlock(source, "MODULE_CONTRACT");
169
+ const changeSummary = extractBlock(source, "CHANGE_SUMMARY");
170
+
171
+ items.push({
172
+ path: entry.path,
173
+ currentRevision,
174
+ auditedRevision,
175
+ reason: auditedRevision === null ? "never-audited" : "revision-threshold-crossed",
176
+ moduleContract,
177
+ changeSummary,
178
+ });
179
+ }
180
+
181
+ items.sort((a, b) => a.path.localeCompare(b.path));
182
+
183
+ context.logger.info(`[compass.audit.plan] threshold=${threshold}, due=${items.length}`);
184
+
185
+ return {
186
+ data: { threshold, dueCount: items.length, items },
187
+ exitCode: 0,
188
+ summary: `[compass.audit.plan] ${items.length} files due for audit (threshold=${threshold})`,
189
+ };
190
+ }
191
+
192
+ export async function runCompassAuditRecord(
193
+ input: ForgeCommandInput,
194
+ context: ForgeRuntimeContext,
195
+ ): Promise<
196
+ ForgeCommandResult<{
197
+ path: string;
198
+ action: "recorded";
199
+ }>
200
+ > {
201
+ const rawFilePath = input.flags["file"] as string | undefined;
202
+ const verdict = input.flags["verdict"] as CompassAuditVerdict | undefined;
203
+ const agentFlag = input.flags["agent"] as string | undefined;
204
+
205
+ if (!rawFilePath) {
206
+ context.logger.error("[compass.audit.record] --file is required");
207
+ return { exitCode: 1, data: { path: "", action: "recorded" } };
208
+ }
209
+
210
+ if (!verdict || !["pass", "repaired", "baseline"].includes(verdict)) {
211
+ context.logger.error("[compass.audit.record] --verdict must be pass|repaired|baseline");
212
+ return { exitCode: 1, data: { path: rawFilePath, action: "recorded" } };
213
+ }
214
+
215
+ const filePath = relative(context.workspaceRoot, resolve(process.cwd(), rawFilePath));
216
+
217
+ const { revision, entityId, contentHash } = await getRevisionByPath(
218
+ context.workspaceRoot,
219
+ filePath,
220
+ );
221
+
222
+ const agent = agentFlag ?? (await getGitUser(context.workspaceRoot));
223
+ const ledger = await loadLedger(context.workspaceRoot);
224
+
225
+ const existingIdx = ledger.entries.findIndex((e) => e.path === filePath);
226
+ const entry: CompassAuditLedgerEntry = {
227
+ path: filePath,
228
+ entityId: entityId ?? "",
229
+ auditedRevision: revision,
230
+ auditedHash: contentHash,
231
+ auditedAt: new Date().toISOString(),
232
+ verdict,
233
+ agent,
234
+ };
235
+
236
+ if (existingIdx >= 0) {
237
+ ledger.entries[existingIdx] = entry;
238
+ } else {
239
+ ledger.entries.push(entry);
240
+ }
241
+
242
+ if (!context.dryRun) {
243
+ await saveLedger(context.workspaceRoot, ledger);
244
+ }
245
+
246
+ context.logger.info(
247
+ `[compass.audit.record] ${filePath}: verdict=${verdict}, revision=${revision}, agent=${agent}`,
248
+ );
249
+
250
+ return {
251
+ data: { path: filePath, action: "recorded" },
252
+ exitCode: 0,
253
+ summary: `[compass.audit.record] ${filePath}: ${verdict} at revision ${revision}`,
254
+ };
255
+ }
256
+
257
+ export async function runCompassAuditBaseline(
258
+ input: ForgeCommandInput,
259
+ context: ForgeRuntimeContext,
260
+ ): Promise<
261
+ ForgeCommandResult<{
262
+ seeded: number;
263
+ total: number;
264
+ }>
265
+ > {
266
+ const scanRoot = resolveCompassScanRoot(input, context);
267
+ const entries = await createCompassInventoryEntries(context.workspaceRoot, input, scanRoot);
268
+ const authored = getAuthoredEntries(entries);
269
+ const ledger = await loadLedger(context.workspaceRoot);
270
+
271
+ const existingPaths = new Set(ledger.entries.map((e) => e.path));
272
+ let seededCount = 0;
273
+
274
+ for (const entry of authored) {
275
+ if (existingPaths.has(entry.path)) {
276
+ continue;
277
+ }
278
+
279
+ const { revision, entityId, contentHash } = await getRevisionByPath(
280
+ context.workspaceRoot,
281
+ entry.path,
282
+ );
283
+
284
+ ledger.entries.push({
285
+ path: entry.path,
286
+ entityId: entityId ?? "",
287
+ auditedRevision: revision,
288
+ auditedHash: contentHash,
289
+ auditedAt: new Date().toISOString(),
290
+ verdict: "baseline",
291
+ agent: "system:baseline",
292
+ });
293
+ seededCount++;
294
+ }
295
+
296
+ ledger.entries.sort((a, b) => a.path.localeCompare(b.path));
297
+
298
+ if (!context.dryRun) {
299
+ await saveLedger(context.workspaceRoot, ledger);
300
+ }
301
+
302
+ context.logger.info(
303
+ `[compass.audit.baseline] seeded=${seededCount}, total=${ledger.entries.length}`,
304
+ );
305
+
306
+ return {
307
+ data: { seeded: seededCount, total: ledger.entries.length },
308
+ exitCode: 0,
309
+ summary: `[compass.audit.baseline] seeded ${seededCount} entries (total: ${ledger.entries.length})`,
310
+ };
311
+ }
312
+
313
+ export async function runCompassAuditValidate(
314
+ input: ForgeCommandInput,
315
+ context: ForgeRuntimeContext,
316
+ ): Promise<
317
+ ForgeCommandResult<{
318
+ strict: boolean;
319
+ dueCount: number;
320
+ diagnostics: Array<{
321
+ ruleId: string;
322
+ severity: string;
323
+ file: string;
324
+ message: string;
325
+ fix: string;
326
+ }>;
327
+ }>
328
+ > {
329
+ const strict = input.flags["strict"] === true;
330
+ const scanRoot = resolveCompassScanRoot(input, context);
331
+ const entries = await createCompassInventoryEntries(context.workspaceRoot, input, scanRoot);
332
+ const authored = getAuthoredEntries(entries);
333
+ const ledger = await loadLedger(context.workspaceRoot);
334
+
335
+ const ledgerMap = new Map<string, CompassAuditLedgerEntry>();
336
+ for (const e of ledger.entries) {
337
+ ledgerMap.set(e.path, e);
338
+ }
339
+
340
+ const diagnostics: Array<{
341
+ ruleId: string;
342
+ severity: string;
343
+ file: string;
344
+ message: string;
345
+ fix: string;
346
+ }> = [];
347
+
348
+ for (const entry of authored) {
349
+ const ledgerEntry = ledgerMap.get(entry.path);
350
+ const auditedRevision = ledgerEntry?.auditedRevision ?? null;
351
+
352
+ const { revision: currentRevision } = await getRevisionByPath(
353
+ context.workspaceRoot,
354
+ entry.path,
355
+ );
356
+
357
+ if (!isAuditDue(currentRevision, auditedRevision, ledger.revisionThreshold)) {
358
+ continue;
359
+ }
360
+
361
+ const severity = strict ? "error" : "warning";
362
+ context.logger[strict ? "error" : "warn"](
363
+ `[compass.audit.validate] COMPASS-AUDIT-01: ${entry.path}: audit overdue (current=${currentRevision}, audited=${auditedRevision ?? "never"}, threshold=${ledger.revisionThreshold})`,
364
+ );
365
+ diagnostics.push({
366
+ ruleId: "COMPASS-AUDIT-01",
367
+ severity,
368
+ file: entry.path,
369
+ message: `Compass audit overdue: ${currentRevision - (auditedRevision ?? 0)} revisions since last audit (threshold=${ledger.revisionThreshold})`,
370
+ fix: "fix: run compass.audit.plan, reconcile the blocks with the code, then compass.audit.record",
371
+ });
372
+ }
373
+
374
+ const dueCount = diagnostics.length;
375
+ const hasErrors = strict && dueCount > 0;
376
+
377
+ return {
378
+ data: { strict, dueCount, diagnostics },
379
+ exitCode: hasErrors ? 1 : 0,
380
+ summary: dueCount > 0 ? undefined : `[compass.audit.validate] OK (0 files overdue)`,
381
+ };
382
+ }
@@ -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
+ }