@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,141 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Defines types and constants for managing Architectural Decision Records (ADRs):
5
+ statuses, scopes, frontmatter shape, validation results, and list output.
6
+ </purpose>
7
+ <non-goals>
8
+ <item>Do not implement ADR processing or validation logic here.</item>
9
+ <item>Do not handle user input or command execution related to ADRs.</item>
10
+ <item>Do not manage the storage or retrieval of ADR documents.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-0366: introduce ADR types and constants mirroring the RFC domain contract.</item>
15
+ <item>RFC-0367: extend AdrStatus with reviewing and implemented; add implementedAt, closedAt, reviewers fields.</item>
16
+ <item>Post-refactor hardening: document that an ADR may be superseded by a broader RFC.</item>
17
+ <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
18
+ <item>RFC-0727: add AdrImplementStamp types for atomic ADR status transition.</item>
19
+ </CHANGE_SUMMARY>
20
+ */
21
+
22
+ export type AdrStatus =
23
+ "proposed" | "reviewing" | "accepted" | "implemented" | "superseded" | "rejected";
24
+
25
+ export const ADR_STATUSES: readonly AdrStatus[] = [
26
+ "proposed",
27
+ "reviewing",
28
+ "accepted",
29
+ "implemented",
30
+ "superseded",
31
+ "rejected",
32
+ ] as const;
33
+
34
+ export type AdrScope = "package" | "app" | "workspace";
35
+
36
+ export const ADR_SCOPES: readonly AdrScope[] = ["package", "app", "workspace"] as const;
37
+
38
+ export interface AdrFrontmatter {
39
+ id: string;
40
+ title: string;
41
+ status: AdrStatus;
42
+ scope: AdrScope;
43
+ decider: string;
44
+ createdAt: string;
45
+ updatedAt: string;
46
+ supersedes?: string[];
47
+ supersededBy?: string;
48
+ related?: string[];
49
+ implementedAt?: string;
50
+ closedAt?: string;
51
+ reviewers?: string[];
52
+ }
53
+
54
+ export interface AdrListEntry {
55
+ id: string;
56
+ title: string;
57
+ status: AdrStatus;
58
+ scope: AdrScope;
59
+ decider: string;
60
+ updatedAt: string;
61
+ file: string;
62
+ }
63
+
64
+ export interface AdrListResult {
65
+ command: "adr.list";
66
+ status: "ok";
67
+ count: number;
68
+ entries: AdrListEntry[];
69
+ }
70
+
71
+ export interface AdrValidationViolation {
72
+ adrId: string;
73
+ file: string;
74
+ rule: string;
75
+ message: string;
76
+ severity: "error" | "warning";
77
+ }
78
+
79
+ export interface AdrValidationResult {
80
+ command: "adr.validate";
81
+ status: "pass" | "fail";
82
+ count: number;
83
+ violations: AdrValidationViolation[];
84
+ }
85
+
86
+ export interface AdrCreateResult {
87
+ command: "adr.create";
88
+ status: "ok";
89
+ file: string;
90
+ id: string;
91
+ }
92
+
93
+ export const ADR_DIR = "docs/adrs";
94
+ export const ADR_TEMPLATE_FILE = "docs/adrs/adr-0000-template.md";
95
+ export const ADR_ID_PATTERN = /^ADR-\d{4}$/;
96
+
97
+ export const ADR_REQUIRED_SECTIONS = [
98
+ "Context",
99
+ "Decision",
100
+ "Justification",
101
+ "Consequences",
102
+ "Evolution",
103
+ ] as const;
104
+
105
+ export const ADR_KNOWN_KEYS: readonly string[] = [
106
+ "id",
107
+ "title",
108
+ "status",
109
+ "scope",
110
+ "decider",
111
+ "createdAt",
112
+ "updatedAt",
113
+ "supersedes",
114
+ "supersededBy",
115
+ "related",
116
+ "implementedAt",
117
+ "closedAt",
118
+ "reviewers",
119
+ ] as const;
120
+
121
+ // ─── RFC-0727: adr.implement.stamp types ─────────────────────────────────────
122
+
123
+ export type AdrImplementStampRule = "ADR-IMP-01" | "ADR-IMP-03" | "ADR-IMP-04" | "ADR-IMP-05";
124
+
125
+ export interface AdrImplementStampData {
126
+ adrId: string;
127
+ implementationCommit: string;
128
+ stampedAt: string;
129
+ }
130
+
131
+ export interface AdrImplementStampViolation {
132
+ rule: AdrImplementStampRule;
133
+ message: string;
134
+ }
135
+
136
+ export interface AdrImplementStampResult {
137
+ command: "adr.implement.stamp";
138
+ status: "pass" | "fail";
139
+ data?: AdrImplementStampData;
140
+ violations: AdrImplementStampViolation[];
141
+ }
@@ -0,0 +1,48 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Register the audit archive command with the forge kernel registry.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement handler logic here — delegate to handlers/archive.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0521: initial forgeAuditModule registering audit.archive command.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import type { ForgeModule } from "../../src/forge-module.ts";
14
+
15
+ export const forgeAuditModule: ForgeModule = {
16
+ name: "forge-audit",
17
+ version: "0.1.0",
18
+ async register(registry) {
19
+ const { runAuditArchive } = await import("./handlers/archive.ts");
20
+
21
+ registry.registerCommand({
22
+ name: "audit.archive",
23
+ description:
24
+ "Move audit files whose parent RFC has terminal status " +
25
+ "(implemented, rejected, superseded) into docs/audits/archive/<status>/ " +
26
+ "subdirectories. Bidirectional: moves non-terminal files found in " +
27
+ "subdirectories back to root. Use --dry-run to preview. " +
28
+ "Use --status to filter to a single terminal status. " +
29
+ "Prefer the docs.archive umbrella command unless you need to archive only audits.",
30
+ scope: "workspace",
31
+ mutatesState: true,
32
+ writes: ["docs/audits/*.md", "docs/audits/archive/**"],
33
+ reads: ["docs/audits/**/*.md", "docs/rfcs/**/*.md"],
34
+ cacheable: false,
35
+ flags: {
36
+ "dry-run": {
37
+ kind: "boolean",
38
+ description: "Preview what would be moved without touching the filesystem.",
39
+ },
40
+ status: {
41
+ kind: "string",
42
+ description: "Filter to a single terminal status (implemented, rejected, superseded).",
43
+ },
44
+ },
45
+ execute: runAuditArchive,
46
+ });
47
+ },
48
+ };
@@ -0,0 +1,84 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Audit frontmatter file I/O — recursive file discovery and YAML frontmatter
5
+ parsing for audit files under docs/audits/. Only files matching the
6
+ audit-rfc-XXXX-* pattern are candidates for archiving; standalone audits
7
+ are silently excluded.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Do not validate audit content.</item>
11
+ </non-goals>
12
+ </MODULE_CONTRACT>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-0521: initial audit frontmatter I/O.</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ import fs from "node:fs/promises";
19
+ import path from "node:path";
20
+ import YAML from "yaml";
21
+ import { AUDIT_RFC_FILE_PATTERN } from "./types.ts";
22
+
23
+ export interface ParsedAudit {
24
+ frontmatter: Record<string, unknown>;
25
+ body: string;
26
+ }
27
+
28
+ export function parseAuditFile(source: string): ParsedAudit {
29
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
30
+ if (!match) {
31
+ return { frontmatter: {}, body: source };
32
+ }
33
+ return {
34
+ frontmatter: (YAML.parse(match[1]!) ?? {}) as Record<string, unknown>,
35
+ body: match[2] ?? "",
36
+ };
37
+ }
38
+
39
+ export async function listAuditFiles(auditDirPath: string): Promise<string[]> {
40
+ const results: string[] = [];
41
+
42
+ async function scanDir(dirPath: string, relativePrefix: string): Promise<void> {
43
+ try {
44
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
47
+ if (entry.isDirectory()) {
48
+ await scanDir(path.join(dirPath, entry.name), relativePath);
49
+ } else if (
50
+ entry.isFile() &&
51
+ entry.name.endsWith(".md") &&
52
+ !entry.name.startsWith("audit-0000") &&
53
+ entry.name !== "README.md" &&
54
+ AUDIT_RFC_FILE_PATTERN.test(entry.name)
55
+ ) {
56
+ results.push(relativePath);
57
+ }
58
+ }
59
+ } catch {
60
+ // Directory doesn't exist or is unreadable — return empty
61
+ }
62
+ }
63
+
64
+ await scanDir(auditDirPath, "");
65
+ return results.sort();
66
+ }
67
+
68
+ export async function readAndParseAudit(
69
+ auditDirPath: string,
70
+ fileName: string,
71
+ ): Promise<{ fileName: string; parsed: ParsedAudit } | undefined> {
72
+ try {
73
+ const filePath = path.join(auditDirPath, fileName);
74
+ const content = await fs.readFile(filePath, "utf-8");
75
+ return { fileName, parsed: parseAuditFile(content) };
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
80
+
81
+ export function extractRfcIdFromAuditFile(fileName: string): string | undefined {
82
+ const match = path.basename(fileName).match(/^audit-(rfc-\d{4})-/i);
83
+ return match ? match[1]!.toUpperCase() : undefined;
84
+ }
@@ -0,0 +1,239 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Audit archive handler — moves audit files whose parent RFC has terminal status
5
+ into status-specific subdirectories under docs/audits/archive/ and moves
6
+ non-terminal files found in subdirectories back to the root. Standalone audit
7
+ files (not matching audit-rfc-XXXX-*) are silently excluded.
8
+ </purpose>
9
+ <non-goals>
10
+ <item>Does not validate audit content.</item>
11
+ <item>Does not change audit frontmatter — only moves files on disk.</item>
12
+ </non-goals>
13
+ </MODULE_CONTRACT>
14
+ <CHANGE_SUMMARY>
15
+ <item>RFC-0521: implement audit.archive command.</item>
16
+ <item>RFC-0733: add pinned-files pre-check — skip pinned files with warning instead of moving them.</item>
17
+ </CHANGE_SUMMARY>
18
+ */
19
+
20
+ import fs from "node:fs/promises";
21
+ import path from "node:path";
22
+ import { listAuditFiles, extractRfcIdFromAuditFile } from "../frontmatter-io.ts";
23
+ import { loadRfcStatusMap } from "../../rfc/frontmatter-io.ts";
24
+ import type { ArchiveMove, ArchiveSkip } from "../../rfc/handlers/archive.ts";
25
+ import type {
26
+ ForgeCommandInput,
27
+ ForgeCommandResult,
28
+ ForgeRuntimeContext,
29
+ } from "../../../src/types.ts";
30
+ import { AUDIT_DIR } from "../types.ts";
31
+ import type { AuditArchiveResult } from "../types.ts";
32
+ import { loadPinnedManifest, isPinned, isIntraDirMove } from "../../core/handlers/pinned-check.ts";
33
+
34
+ const TERMINAL_STATUSES = ["implemented", "rejected", "superseded"] as const;
35
+
36
+ export async function runAuditArchive(
37
+ input: ForgeCommandInput,
38
+ context: ForgeRuntimeContext,
39
+ ): Promise<ForgeCommandResult<AuditArchiveResult>> {
40
+ const { workspaceRoot, logger, outputFormat } = context;
41
+ const auditDirPath = path.join(workspaceRoot, AUDIT_DIR);
42
+ const rfcDirPath = path.join(workspaceRoot, "docs", "rfcs");
43
+
44
+ const dryRun = context.dryRun || input.flags["dry-run"] === true;
45
+ const statusFilter = input.flags["status"] as string | undefined;
46
+
47
+ if (statusFilter && !TERMINAL_STATUSES.includes(statusFilter as never)) {
48
+ throw new Error(
49
+ `Invalid --status "${statusFilter}". Must be one of: ${TERMINAL_STATUSES.join(", ")}`,
50
+ );
51
+ }
52
+
53
+ const files = await listAuditFiles(auditDirPath);
54
+ const rfcStatusMap = await loadRfcStatusMap(rfcDirPath);
55
+ const moved: ArchiveMove[] = [];
56
+ const skipped: ArchiveSkip[] = [];
57
+
58
+ // RFC-0733: Load pinned manifest once per invocation
59
+ let pinnedManifest = null;
60
+ try {
61
+ pinnedManifest = await loadPinnedManifest(workspaceRoot);
62
+ } catch {
63
+ // Malformed manifest — skip pre-check
64
+ }
65
+
66
+ for (const fileName of files) {
67
+ const rfcId = extractRfcIdFromAuditFile(fileName);
68
+ const relFile = path.join(AUDIT_DIR, fileName);
69
+ const basename = path.basename(fileName);
70
+
71
+ if (!rfcId) {
72
+ skipped.push({ id: "UNKNOWN", file: relFile, reason: "no RFC id in filename" });
73
+ continue;
74
+ }
75
+
76
+ const rfcStatus = rfcStatusMap.get(rfcId);
77
+ if (!rfcStatus) {
78
+ skipped.push({ id: rfcId, file: relFile, reason: "parent RFC not found" });
79
+ continue;
80
+ }
81
+
82
+ const isTerminal =
83
+ rfcStatus === "implemented" || rfcStatus === "rejected" || rfcStatus === "superseded";
84
+ const isInArchive = fileName.includes("/");
85
+
86
+ if (statusFilter && rfcStatus !== statusFilter) {
87
+ skipped.push({
88
+ id: rfcId,
89
+ file: relFile,
90
+ reason: `parent RFC status ${rfcStatus} does not match --status ${statusFilter}`,
91
+ });
92
+ continue;
93
+ }
94
+
95
+ if (isTerminal && !isInArchive) {
96
+ const targetDir = path.join(auditDirPath, "archive", rfcStatus);
97
+ const targetPath = path.join(targetDir, basename);
98
+ const targetRel = path.join(AUDIT_DIR, "archive", rfcStatus, basename);
99
+
100
+ // RFC-0733: Check if file is pinned before moving
101
+ // Gap fix: exempt intra-directory moves (file stays within the same pinned dir)
102
+ if (
103
+ pinnedManifest &&
104
+ isPinned(pinnedManifest, relFile) &&
105
+ !isIntraDirMove(pinnedManifest, relFile, targetRel)
106
+ ) {
107
+ skipped.push({
108
+ id: rfcId,
109
+ file: relFile,
110
+ reason: "pinned (protected by .forge/pinned.yaml)",
111
+ });
112
+ if (outputFormat === "pretty") {
113
+ logger.warn(` pinned: skipping ${relFile} (protected)`);
114
+ }
115
+ continue;
116
+ }
117
+
118
+ try {
119
+ await fs.access(targetPath);
120
+ skipped.push({ id: rfcId, file: relFile, reason: "destination exists" });
121
+ continue;
122
+ } catch {
123
+ // destination doesn't exist — proceed
124
+ }
125
+
126
+ if (!dryRun) {
127
+ await fs.mkdir(targetDir, { recursive: true });
128
+ try {
129
+ await fs.rename(path.join(auditDirPath, fileName), targetPath);
130
+ } catch (err) {
131
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
132
+ skipped.push({ id: rfcId, file: relFile, reason: "already moved by another process" });
133
+ continue;
134
+ }
135
+ throw err;
136
+ }
137
+ }
138
+
139
+ moved.push({
140
+ id: rfcId,
141
+ file: targetRel,
142
+ status: rfcStatus,
143
+ from: relFile,
144
+ to: targetRel,
145
+ direction: "into-archive",
146
+ });
147
+ } else if (!isTerminal && isInArchive) {
148
+ const targetPath = path.join(auditDirPath, basename);
149
+ const targetRel = path.join(AUDIT_DIR, basename);
150
+
151
+ // RFC-0733: Check if file is pinned before moving
152
+ // Gap fix: exempt intra-directory moves (file stays within the same pinned dir)
153
+ if (
154
+ pinnedManifest &&
155
+ isPinned(pinnedManifest, relFile) &&
156
+ !isIntraDirMove(pinnedManifest, relFile, targetRel)
157
+ ) {
158
+ skipped.push({
159
+ id: rfcId,
160
+ file: relFile,
161
+ reason: "pinned (protected by .forge/pinned.yaml)",
162
+ });
163
+ if (outputFormat === "pretty") {
164
+ logger.warn(` pinned: skipping ${relFile} (protected)`);
165
+ }
166
+ continue;
167
+ }
168
+
169
+ try {
170
+ await fs.access(targetPath);
171
+ skipped.push({ id: rfcId, file: relFile, reason: "destination exists" });
172
+ continue;
173
+ } catch {
174
+ // destination doesn't exist — proceed
175
+ }
176
+
177
+ if (!dryRun) {
178
+ try {
179
+ await fs.rename(path.join(auditDirPath, fileName), targetPath);
180
+ } catch (err) {
181
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
182
+ skipped.push({ id: rfcId, file: relFile, reason: "already moved by another process" });
183
+ continue;
184
+ }
185
+ throw err;
186
+ }
187
+ }
188
+
189
+ moved.push({
190
+ id: rfcId,
191
+ file: targetRel,
192
+ status: rfcStatus,
193
+ from: relFile,
194
+ to: targetRel,
195
+ direction: "out-of-archive",
196
+ });
197
+ } else if (isTerminal && isInArchive) {
198
+ skipped.push({ id: rfcId, file: relFile, reason: `already archived (${rfcStatus})` });
199
+ } else {
200
+ skipped.push({
201
+ id: rfcId,
202
+ file: relFile,
203
+ reason: `parent RFC status ${rfcStatus} is non-terminal`,
204
+ });
205
+ }
206
+ }
207
+
208
+ if (outputFormat === "pretty") {
209
+ if (dryRun) {
210
+ logger.info(
211
+ `[dry-run] audit.archive: would move ${moved.length} file(s), skip ${skipped.length}`,
212
+ );
213
+ } else {
214
+ logger.success(`audit.archive: moved ${moved.length} file(s), skipped ${skipped.length}`);
215
+ }
216
+ for (const m of moved) {
217
+ logger.info(` ${m.direction}: ${m.id} (${m.status}) ${m.from} → ${m.to}`);
218
+ }
219
+ }
220
+
221
+ return {
222
+ data: {
223
+ command: "audit.archive",
224
+ status: "ok",
225
+ moved,
226
+ skipped,
227
+ dryRun,
228
+ },
229
+ summary: dryRun
230
+ ? `[dry-run] Would move ${moved.length} file(s), skip ${skipped.length}`
231
+ : `Moved ${moved.length} file(s), skipped ${skipped.length}`,
232
+ nextSteps: [
233
+ {
234
+ action: "Run: pnpm exec werkstatt run audit.list --json to verify archive status",
235
+ kind: "optional",
236
+ },
237
+ ],
238
+ };
239
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Barrel export for the audit domain — types, module, and handlers.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement audit logic here; delegate to handlers/ and audit.module.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0521: initial audit module barrel.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ export { forgeAuditModule } from "./audit.module.ts";
14
+ export { runAuditArchive } from "./handlers/archive.ts";
15
+ export {
16
+ listAuditFiles,
17
+ parseAuditFile,
18
+ readAndParseAudit,
19
+ extractRfcIdFromAuditFile,
20
+ type ParsedAudit,
21
+ } from "./frontmatter-io.ts";
22
+ export type { AuditArchiveResult, AuditArchiveMove, AuditArchiveSkip } from "./types.ts";
23
+ export { AUDIT_DIR, AUDIT_RFC_FILE_PATTERN } from "./types.ts";
@@ -0,0 +1,29 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>
4
+ Type definitions for the audit archive domain — audit file discovery, parsing,
5
+ and archive result shapes.
6
+ </purpose>
7
+ <non-goals>
8
+ <item>Do not define archive handler logic here — that lives in handlers/archive.ts.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-0521: initial audit archive types.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import type { ArchiveMove, ArchiveSkip } from "../rfc/handlers/archive.ts";
17
+
18
+ export const AUDIT_DIR = "docs/audits";
19
+ export const AUDIT_RFC_FILE_PATTERN = /^audit-rfc-\d{4}-/;
20
+
21
+ export interface AuditArchiveResult {
22
+ command: "audit.archive";
23
+ status: "ok";
24
+ moved: ArchiveMove[];
25
+ skipped: ArchiveSkip[];
26
+ dryRun: boolean;
27
+ }
28
+
29
+ export type { ArchiveMove as AuditArchiveMove, ArchiveSkip as AuditArchiveSkip };