@warpgogol/forge 5.3.4 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/cli.ts +7 -2
  3. package/dist/bin/cli.js +7 -2
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/os/adr/adr.module.d.ts.map +1 -1
  6. package/dist/os/adr/adr.module.js +2 -0
  7. package/dist/os/adr/adr.module.js.map +1 -1
  8. package/dist/os/audit/audit.module.d.ts.map +1 -1
  9. package/dist/os/audit/audit.module.js +2 -0
  10. package/dist/os/audit/audit.module.js.map +1 -1
  11. package/dist/os/compass/compass.module.d.ts.map +1 -1
  12. package/dist/os/compass/compass.module.js +47 -15
  13. package/dist/os/compass/compass.module.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
  15. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  16. package/dist/os/compass/handlers/compass-audit-handler.js +39 -9
  17. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  18. package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
  19. package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
  20. package/dist/os/compass/handlers/compass-docs-validate.js +326 -0
  21. package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
  22. package/dist/os/compass/handlers/summary-record.d.ts +11 -2
  23. package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
  24. package/dist/os/compass/handlers/summary-record.js +93 -5
  25. package/dist/os/compass/handlers/summary-record.js.map +1 -1
  26. package/dist/os/core/core.module.d.ts.map +1 -1
  27. package/dist/os/core/core.module.js +124 -73
  28. package/dist/os/core/core.module.js.map +1 -1
  29. package/dist/os/exploration/exploration.module.d.ts.map +1 -1
  30. package/dist/os/exploration/exploration.module.js +3 -0
  31. package/dist/os/exploration/exploration.module.js.map +1 -1
  32. package/dist/os/naming/naming.module.d.ts.map +1 -1
  33. package/dist/os/naming/naming.module.js +9 -2
  34. package/dist/os/naming/naming.module.js.map +1 -1
  35. package/dist/os/notes/notes.module.d.ts.map +1 -1
  36. package/dist/os/notes/notes.module.js +17 -6
  37. package/dist/os/notes/notes.module.js.map +1 -1
  38. package/dist/os/plan/plan.module.d.ts.map +1 -1
  39. package/dist/os/plan/plan.module.js +2 -0
  40. package/dist/os/plan/plan.module.js.map +1 -1
  41. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  42. package/dist/os/plugin/plugin.module.js +9 -2
  43. package/dist/os/plugin/plugin.module.js.map +1 -1
  44. package/dist/os/program/program.module.d.ts.map +1 -1
  45. package/dist/os/program/program.module.js +19 -4
  46. package/dist/os/program/program.module.js.map +1 -1
  47. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  48. package/dist/os/queue/handlers/queue-validate.js +154 -7
  49. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  50. package/dist/os/queue/manifest.d.ts +17 -1
  51. package/dist/os/queue/manifest.d.ts.map +1 -1
  52. package/dist/os/queue/manifest.js +127 -6
  53. package/dist/os/queue/manifest.js.map +1 -1
  54. package/dist/os/queue/queue.module.d.ts.map +1 -1
  55. package/dist/os/queue/queue.module.js +32 -4
  56. package/dist/os/queue/queue.module.js.map +1 -1
  57. package/dist/os/queue/types.d.ts +100 -1
  58. package/dist/os/queue/types.d.ts.map +1 -1
  59. package/dist/os/queue/types.js +44 -0
  60. package/dist/os/queue/types.js.map +1 -1
  61. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  62. package/dist/os/rfc/handlers/implement-stamp.js +28 -5
  63. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  64. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  65. package/dist/os/rfc/rfc.module.js +27 -3
  66. package/dist/os/rfc/rfc.module.js.map +1 -1
  67. package/dist/os/rfc/types.d.ts +11 -0
  68. package/dist/os/rfc/types.d.ts.map +1 -1
  69. package/dist/os/rfc/types.js +1 -0
  70. package/dist/os/rfc/types.js.map +1 -1
  71. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  72. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  73. package/dist/os/rfc/verification-evidence.js +140 -23
  74. package/dist/os/rfc/verification-evidence.js.map +1 -1
  75. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  76. package/dist/os/rfc/verification-refresh.js +72 -37
  77. package/dist/os/rfc/verification-refresh.js.map +1 -1
  78. package/dist/os/session/handlers/save.d.ts.map +1 -1
  79. package/dist/os/session/handlers/save.js +46 -5
  80. package/dist/os/session/handlers/save.js.map +1 -1
  81. package/dist/os/session/session.module.d.ts.map +1 -1
  82. package/dist/os/session/session.module.js +5 -0
  83. package/dist/os/session/session.module.js.map +1 -1
  84. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  85. package/dist/os/spec/live-spec-merge.js +55 -134
  86. package/dist/os/spec/live-spec-merge.js.map +1 -1
  87. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  88. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  89. package/dist/os/spec/live-spec-rebuild.js +272 -0
  90. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  91. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  92. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  93. package/dist/os/spec/live-spec-shared.js +315 -0
  94. package/dist/os/spec/live-spec-shared.js.map +1 -0
  95. package/dist/os/spec/live-spec-types.d.ts +17 -1
  96. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  97. package/dist/os/spec/live-spec-types.js +3 -0
  98. package/dist/os/spec/live-spec-types.js.map +1 -1
  99. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  100. package/dist/os/spec/live-spec-validate.js +105 -2
  101. package/dist/os/spec/live-spec-validate.js.map +1 -1
  102. package/dist/os/spec/spec-materialize.d.ts +2 -0
  103. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  104. package/dist/os/spec/spec-materialize.js +45 -5
  105. package/dist/os/spec/spec-materialize.js.map +1 -1
  106. package/dist/os/spec/spec-schema.d.ts +19 -0
  107. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  108. package/dist/os/spec/spec-schema.js +30 -0
  109. package/dist/os/spec/spec-schema.js.map +1 -1
  110. package/dist/os/spec/spec-status.d.ts +4 -0
  111. package/dist/os/spec/spec-status.d.ts.map +1 -1
  112. package/dist/os/spec/spec-status.js +19 -1
  113. package/dist/os/spec/spec-status.js.map +1 -1
  114. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  115. package/dist/os/spec/spec-validate.js +55 -8
  116. package/dist/os/spec/spec-validate.js.map +1 -1
  117. package/dist/os/spec/spec.module.d.ts.map +1 -1
  118. package/dist/os/spec/spec.module.js +84 -8
  119. package/dist/os/spec/spec.module.js.map +1 -1
  120. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  121. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  122. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  123. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  124. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  125. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  126. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  127. package/dist/os/workflow/workflow.module.js +13 -5
  128. package/dist/os/workflow/workflow.module.js.map +1 -1
  129. package/dist/src/cli-flags.d.ts.map +1 -1
  130. package/dist/src/cli-flags.js +25 -5
  131. package/dist/src/cli-flags.js.map +1 -1
  132. package/dist/src/config/forge-config.d.ts +3 -0
  133. package/dist/src/config/forge-config.d.ts.map +1 -1
  134. package/dist/src/config/forge-config.js +2 -0
  135. package/dist/src/config/forge-config.js.map +1 -1
  136. package/dist/src/onboarding/init.d.ts.map +1 -1
  137. package/dist/src/onboarding/init.js +8 -3
  138. package/dist/src/onboarding/init.js.map +1 -1
  139. package/dist/src/pipeline-status.d.ts +14 -1
  140. package/dist/src/pipeline-status.d.ts.map +1 -1
  141. package/dist/src/pipeline-status.js +28 -8
  142. package/dist/src/pipeline-status.js.map +1 -1
  143. package/dist/src/types.d.ts +7 -1
  144. package/dist/src/types.d.ts.map +1 -1
  145. package/dist/src/types.js +6 -0
  146. package/dist/src/types.js.map +1 -1
  147. package/dist/src/utils/generated-marker.d.ts +8 -0
  148. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  149. package/dist/src/utils/generated-marker.js +25 -0
  150. package/dist/src/utils/generated-marker.js.map +1 -1
  151. package/os/adr/adr.module.ts +2 -0
  152. package/os/audit/audit.module.ts +2 -0
  153. package/os/compass/compass.module.ts +49 -15
  154. package/os/compass/handlers/compass-audit-handler.ts +50 -9
  155. package/os/compass/handlers/compass-docs-validate.ts +359 -0
  156. package/os/compass/handlers/summary-record.ts +104 -5
  157. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  158. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  159. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  160. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  161. package/os/compass/handlers/tests/summary-record.test.ts +127 -0
  162. package/os/core/core.module.ts +147 -75
  163. package/os/core/docs-archive.test.ts +136 -0
  164. package/os/exploration/exploration.module.ts +3 -0
  165. package/os/naming/naming.module.ts +9 -2
  166. package/os/notes/notes.module.ts +17 -6
  167. package/os/plan/plan-0000-template.md +150 -0
  168. package/os/plan/plan.module.ts +2 -0
  169. package/os/plugin/plugin.module.ts +9 -2
  170. package/os/program/program.module.ts +19 -4
  171. package/os/queue/decision-ledger.test.ts +163 -0
  172. package/os/queue/handlers/queue-validate.ts +190 -8
  173. package/os/queue/manifest.ts +201 -7
  174. package/os/queue/queue-validate.test.ts +303 -0
  175. package/os/queue/queue.module.ts +32 -4
  176. package/os/queue/types.ts +56 -1
  177. package/os/rfc/handlers/implement-stamp.ts +35 -5
  178. package/os/rfc/rfc.module.ts +29 -3
  179. package/os/rfc/types.ts +17 -1
  180. package/os/rfc/verification-emit.test.ts +305 -0
  181. package/os/rfc/verification-evidence.ts +155 -24
  182. package/os/rfc/verification-refresh.test.ts +222 -1
  183. package/os/rfc/verification-refresh.ts +80 -38
  184. package/os/session/handlers/save.ts +41 -5
  185. package/os/session/session.module.ts +5 -0
  186. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  187. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  188. package/os/spec/live-spec-merge.test.ts +187 -7
  189. package/os/spec/live-spec-merge.ts +71 -155
  190. package/os/spec/live-spec-rebuild.test.ts +317 -0
  191. package/os/spec/live-spec-rebuild.ts +356 -0
  192. package/os/spec/live-spec-shared.ts +378 -0
  193. package/os/spec/live-spec-types.ts +21 -1
  194. package/os/spec/live-spec-validate.test.ts +317 -0
  195. package/os/spec/live-spec-validate.ts +106 -2
  196. package/os/spec/spec-materialize.test.ts +192 -0
  197. package/os/spec/spec-materialize.ts +48 -4
  198. package/os/spec/spec-schema.ts +49 -0
  199. package/os/spec/spec-status.ts +24 -0
  200. package/os/spec/spec-validate.test.ts +66 -0
  201. package/os/spec/spec-validate.ts +62 -8
  202. package/os/spec/spec.module.ts +85 -8
  203. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  204. package/os/werkstatt/werkstatt.module.ts +13 -4
  205. package/os/workflow/workflow.module.ts +13 -5
  206. package/package.json +1 -1
  207. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  208. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  209. package/skills/fo/fo-explore/SKILL.md +2 -1
  210. package/skills/fo/fo-fix/SKILL.md +11 -3
  211. package/skills/fo/fo-handoff/SKILL.md +4 -6
  212. package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
  213. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  214. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  215. package/skills/fo/fo-idea-implement/SKILL.md +53 -10
  216. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  217. package/skills/fo/fo-review/SKILL.md +69 -8
  218. package/skills/fo/fo-session-retro/SKILL.md +3 -2
  219. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  220. package/skills/shared/grilling/SKILL.md +12 -0
  221. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  222. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  223. package/src/cli-flags.ts +23 -5
  224. package/src/config/forge-config.ts +4 -0
  225. package/src/onboarding/init.ts +8 -3
  226. package/src/pipeline-status.ts +32 -8
  227. package/src/tests/cli-flags.test.ts +20 -0
  228. package/src/tests/cli-output.test.ts +13 -7
  229. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  230. package/src/tests/implement-stamp.test.ts +38 -0
  231. package/src/tests/session-handlers.test.ts +33 -0
  232. package/src/types.ts +13 -1
  233. package/src/utils/generated-marker.ts +28 -0
  234. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  235. package/skills/fo/fo-review/AXES.md +0 -70
@@ -6,14 +6,19 @@
6
6
  </non-goals>
7
7
  </MODULE_CONTRACT>
8
8
  <CHANGE_SUMMARY>
9
- <item>RFC-0556: removed dynamic import of @warpgogol/site-kernel-checks, all handlers now inlined in forge/os/compass/handlers/.</item>
10
- <item>RFC-1095: compass.summary.record, trim repair rewrite, commit integration</item>
11
9
  <item>RFC-1097: steps 1-4 — compass.migrate codemod
12
10
 
13
11
  Add the v1 to v2 Compass header codemod: migrateFile pure transform (collapse, strip, seed, reorder, purpose-flag actions), migrateWorkspace walker, runCompassMigrate handler with dirty-tree refusal and --force/--files/--dry-run flags, module registration, and 15 unit tests.</item>
14
12
  <item>RFC-1173: declare mutatesState on all kernel commands — collectDeclarationDiagnostics emits error-severity MUTATES-STATE-DECLARED, command.manifest.validate is the blocking consumer in packages.check, sweep declares the flag on every command definition (factories hardcode false for read-only check specs)</item>
15
13
  <item>KERNEL-FLAG-02: Declare compass --root as kind "string[]" — resolveScanRoots reads it as multi-value via getFlagValues, so repeated --root stays legal now that repeated kind-"string" flags are rejected at flag resolution.</item>
16
- <history>RFC-0374, RFC-0538</history>
14
+ <item>RFC-1231: step 1 — rename supportsAllSites to acceptsAllFlag
15
+
16
+ Mechanical sweep: the field only ever gated --all argv acceptance; fan-out
17
+ follows the parsed selector. Guard renamed assertAllSitesAllowed ->
18
+ assertAllFlagAccepted, message updated. 417 declaration sites + type
19
+ surfaces (KernelCommandMetadata, ForgeCommandMetadata) in one atomic pass.</item>
20
+ <item>RFC-1248: VITE-CLIENT-DEP-02 lazy-import gate + SCAN-02 validator reads-root existence check (RFC-1248)</item>
21
+ <history>RFC-0374, RFC-0538, RFC-0556, RFC-1095, RFC-1242</history>
17
22
  </CHANGE_SUMMARY>
18
23
  */
19
24
 
@@ -29,6 +34,7 @@ import {
29
34
  import { runCompassSummaryTrim } from "./handlers/compass-change-summary-handler.ts";
30
35
  import { runCompassSummaryRecord } from "./handlers/summary-record.ts";
31
36
  import { runCompassMigrate } from "./handlers/compass-migrate-handler.ts";
37
+ import { runCompassDocsValidate } from "./handlers/compass-docs-validate.ts";
32
38
 
33
39
  const compassScanFlags = {
34
40
  packages: {
@@ -68,7 +74,7 @@ export const forgeCompassModule: ForgeModule = {
68
74
  mutatesState: true,
69
75
  description: "Generate the repository-wide Compass source inventory XML report.",
70
76
  scope: "workspace",
71
- supportsAllSites: true,
77
+ acceptsAllFlag: true,
72
78
  flags: { ...compassScanFlags },
73
79
  reads: [
74
80
  "packages/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
@@ -85,11 +91,11 @@ export const forgeCompassModule: ForgeModule = {
85
91
  description:
86
92
  "Validate authored source files against current Compass scaffolding requirements.",
87
93
  scope: "workspace",
88
- supportsAllSites: true,
94
+ acceptsAllFlag: true,
89
95
  flags: { ...compassScanFlags, mode: compassModeFlag },
90
96
  reads: [
91
97
  "packages/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
92
- "apps/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
98
+ "apps/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}", // scan-coverage: consumer-workshop root — apps/ exists in consumer workshops, not this monorepo
93
99
  "services/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
94
100
  "docs/source-markup.xml",
95
101
  ],
@@ -97,11 +103,12 @@ export const forgeCompassModule: ForgeModule = {
97
103
  },
98
104
  {
99
105
  name: "compass.summary.record",
106
+ coverage: "operator" as const,
100
107
  description:
101
108
  "Append a governance-referencing item to each target file's CHANGE_SUMMARY and collapse the 5-item window into <history> (RFC-1095). Invoked by commit commands when a commit carries an RFC/ADR reference. Required flags: `--id`.",
102
109
  scope: "workspace",
103
110
  mutatesState: true,
104
- supportsAllSites: true,
111
+ acceptsAllFlag: true,
105
112
  writes: [
106
113
  "apps/**/*.{astro,ts,tsx,js,mjs,css,cs,tscn,tres,gd,md}",
107
114
  "packages/**/*.{astro,ts,tsx,js,mjs,css,cs,tscn,tres,gd,md}",
@@ -136,11 +143,12 @@ export const forgeCompassModule: ForgeModule = {
136
143
  },
137
144
  {
138
145
  name: "compass.summary.trim",
146
+ coverage: "operator" as const,
139
147
  description:
140
148
  "Repair CHANGE_SUMMARY blocks to the v2 shape: collapse described items past 5 into <history>, remove ID-less items, normalize <history> (RFC-1095).",
141
149
  scope: "workspace",
142
150
  mutatesState: true,
143
- supportsAllSites: true,
151
+ acceptsAllFlag: true,
144
152
  writes: [
145
153
  "apps/**/*.{astro,ts,tsx,js,mjs,css,cs,tscn,tres,gd,md}",
146
154
  "packages/**/*.{astro,ts,tsx,js,mjs,css,cs,tscn,tres,gd,md}",
@@ -168,7 +176,7 @@ export const forgeCompassModule: ForgeModule = {
168
176
  "Rewrite authored file headers to the Compass v2 shape (RFC-1097): collapse CHANGE_SUMMARY into <history>, strip forbidden v1 blocks, seed KEY_DECISIONS from @ai-invariant comments, reorder blocks canonically. Refuses a dirty git tree unless --force.",
169
177
  scope: "workspace",
170
178
  mutatesState: true,
171
- supportsAllSites: true,
179
+ acceptsAllFlag: true,
172
180
  writes: [
173
181
  "apps/**/*.{astro,ts,tsx,js,mjs,css,cs,tscn,tres,gd,md}",
174
182
  "packages/**/*.{astro,ts,tsx,js,mjs,css,cs,tscn,tres,gd,md}",
@@ -200,12 +208,13 @@ export const forgeCompassModule: ForgeModule = {
200
208
  },
201
209
  {
202
210
  name: "compass.audit.plan",
211
+ coverage: "operator" as const,
203
212
  mutatesState: false,
204
213
  execOnReadOnly: true,
205
214
  description:
206
215
  "Emit a deterministic work-order of files whose revision has advanced past the threshold since their last Compass audit (RFC-0352). Read-only, no LLM.",
207
216
  scope: "workspace",
208
- supportsAllSites: true,
217
+ acceptsAllFlag: true,
209
218
  flags: {
210
219
  ...compassScanFlags,
211
220
  threshold: {
@@ -223,12 +232,13 @@ export const forgeCompassModule: ForgeModule = {
223
232
  },
224
233
  {
225
234
  name: "compass.audit.record",
235
+ coverage: "operator" as const,
226
236
  modulePath: "packages/forge/os/compass/compass.module.ts",
227
237
  description:
228
238
  "Stamp a file's audit verdict and current revision into the compass-audit ledger (RFC-0352). Mutating. Required flags: `--file`, `--verdict`.",
229
239
  scope: "workspace",
230
240
  mutatesState: true,
231
- supportsAllSites: true,
241
+ acceptsAllFlag: true,
232
242
  writes: ["docs/compass-audit-ledger.generated.yaml"],
233
243
  generates: [{ path: "docs/compass-audit-ledger.generated.yaml", phase: "on-demand" }],
234
244
  reads: ["docs/compass-audit-ledger.generated.yaml"],
@@ -251,7 +261,7 @@ export const forgeCompassModule: ForgeModule = {
251
261
  "Seed the compass-audit ledger for every authored file at its current revision with verdict=baseline (RFC-0352). One-time bootstrap.",
252
262
  scope: "workspace",
253
263
  mutatesState: true,
254
- supportsAllSites: true,
264
+ acceptsAllFlag: true,
255
265
  writes: ["docs/compass-audit-ledger.generated.yaml"],
256
266
  // Artifact ownership is claimed by compass.audit.record (GENERATES-PATH-
257
267
  // COLLISION forbids two owners on a non-glob path); baseline only writes it.
@@ -268,23 +278,29 @@ export const forgeCompassModule: ForgeModule = {
268
278
  {
269
279
  name: "compass.audit.validate",
270
280
  mutatesState: false,
281
+ mutatingFlags: ["prune"],
271
282
  execOnReadOnly: true,
272
283
  contract: "compass",
273
284
  rules: [],
274
285
  description:
275
- "Validate that no authored file is audit-overdue per the revision threshold (RFC-0352). Warns by default, fails with --strict.",
286
+ "Validate that no authored file is audit-overdue per the revision threshold (RFC-0352). Warns by default, fails with --strict. --prune drops ledger entries for paths that no longer exist (RFC-1242).",
276
287
  scope: "workspace",
277
- supportsAllSites: true,
288
+ acceptsAllFlag: true,
278
289
  flags: {
279
290
  ...compassScanFlags,
280
291
  strict: {
281
292
  kind: "boolean",
282
293
  description: "Fail when audit-overdue authored files are found.",
283
294
  },
295
+ prune: {
296
+ kind: "boolean",
297
+ description:
298
+ "Drop ledger entries whose path no longer exists (RFC-1242). Mutating; honors --dry-run.",
299
+ },
284
300
  },
285
301
  reads: [
286
302
  "packages/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
287
- "apps/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
303
+ "apps/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}", // scan-coverage: consumer-workshop root — apps/ exists in consumer workshops, not this monorepo
288
304
  "services/**/*.{ts,tsx,astro,js,mjs,css,cs,tscn,tres,gd,md}",
289
305
  "docs/compass-audit-ledger.generated.yaml",
290
306
  ],
@@ -299,6 +315,24 @@ export const forgeCompassModule: ForgeModule = {
299
315
  },
300
316
  },
301
317
  },
318
+ {
319
+ name: "compass.docs.validate",
320
+ modulePath: "packages/forge/os/compass/compass.module.ts",
321
+ mutatesState: false,
322
+ contract: "compass",
323
+ rules: ["COMPASS-DOC-00", "COMPASS-DOC-01", "COMPASS-DOC-02", "COMPASS-DOC-03"],
324
+ description:
325
+ "Validate that the live docs/*.xml corpus resolves every path, workspace id, and knowledge-graph link target it references (RFC-1242). Read-only.",
326
+ scope: "workspace",
327
+ acceptsAllFlag: true,
328
+ flags: {},
329
+ reads: ["docs/*.xml", "docs/command-manifest.generated.yaml", "docs/architecture-dna.md"],
330
+ execute: runCompassDocsValidate,
331
+ gate: {
332
+ severity: "mixed",
333
+ phase: "workspace",
334
+ },
335
+ },
302
336
  ],
303
337
  pipelines: [],
304
338
  };
@@ -10,6 +10,7 @@ mode (RFC-0556). Drives per-file semantic-truth auditing on a revision cadence (
10
10
  </non-goals>
11
11
  </MODULE_CONTRACT>
12
12
  <CHANGE_SUMMARY>
13
+ <item>RFC-1242: compass.audit.validate --prune — drops ledger entries whose path no longer exists via mutatingFlags upgrade; saveLedger routes writes through the injected io adapter so dry-run recording intercepts them.</item>
13
14
  <item>RFC-0352: initial implementation of compass.audit.plan, compass.audit.record, compass.audit.baseline, compass.audit.validate.</item>
14
15
  <item>RFC-0556: moved from @warpgogol/site-kernel-checks to @warpgogol/forge for autonomous mode.</item>
15
16
  <item>RFC-1094: audit work orders now carry the KEY_DECISIONS block alongside MODULE_CONTRACT and CHANGE_SUMMARY.</item>
@@ -27,12 +28,13 @@ import { resolveCompassPolicy } from "../policy.ts";
27
28
  import { resolveCompassScanRoot } from "./resolve-scan-root.ts";
28
29
  import { getRevisionByPath } from "./git-revision.ts";
29
30
  import type { CompassInventoryEntry } from "./compass-inventory.ts";
30
- import { writeFileAtomic } from "../../../src/utils/fs-atomic.ts";
31
+
31
32
  import { buildGeneratedHeader } from "../../../src/utils/generated-marker.ts";
32
33
  import type {
33
34
  ForgeCommandInput,
34
35
  ForgeCommandResult,
35
36
  ForgeRuntimeContext,
37
+ WorkspaceIO,
36
38
  } from "../../../src/types.ts";
37
39
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
38
40
 
@@ -104,17 +106,30 @@ async function loadLedger(workspaceRoot: string): Promise<CompassAuditLedger> {
104
106
  }
105
107
  }
106
108
 
107
- async function saveLedger(workspaceRoot: string, ledger: CompassAuditLedger): Promise<void> {
109
+ function serializeLedger(ledger: CompassAuditLedger): string {
108
110
  const normalized = withLedgerAdvisory(ledger);
109
111
  normalized.entries.sort((a, b) => a.path.localeCompare(b.path));
110
- const abs = resolve(workspaceRoot, LEDGER_PATH);
111
- await io.mkdir(resolve(abs, ".."));
112
112
  const header = buildGeneratedHeader({
113
113
  ownerCommand: "compass.audit.record",
114
114
  filePath: LEDGER_PATH,
115
115
  });
116
- const yaml = header + yamlStringify(normalized) + "\n";
117
- await writeFileAtomic(abs, yaml);
116
+ return header + yamlStringify(normalized) + "\n";
117
+ }
118
+
119
+ // RFC-1242: writes route through the injected adapter so --dry-run recording
120
+ // IO (mutatingFlags: ["prune"]) intercepts them — ambient writeFileAtomic would
121
+ // bypass the recording adapter entirely (DNA-110). The param is named `io` so
122
+ // the workspace-write-boundary lint recognizes the io.writeFile port call
123
+ // (WS-WRITE-02 matches io.writeFile/context.io.writeFile member expressions).
124
+ // Callers under a mutating-flag gate (compass.audit.validate --prune) write
125
+ // via io.writeFile directly inside the gated block — registry-integrity sees
126
+ // mutations in shared callees but not the caller's flag condition.
127
+ async function saveLedger(
128
+ workspaceRoot: string,
129
+ ledger: CompassAuditLedger,
130
+ io: WorkspaceIO = ambientIo,
131
+ ): Promise<void> {
132
+ await io.writeFile(resolve(workspaceRoot, LEDGER_PATH), serializeLedger(ledger));
118
133
  }
119
134
 
120
135
  function extractBlock(source: string, tagName: string): string {
@@ -343,7 +358,7 @@ export async function runCompassAuditRecord(
343
358
  }
344
359
 
345
360
  if (!context.dryRun) {
346
- await saveLedger(context.workspaceRoot, ledger);
361
+ await saveLedger(context.workspaceRoot, ledger, _io);
347
362
  }
348
363
 
349
364
  context.logger.info(
@@ -419,7 +434,7 @@ export async function runCompassAuditBaseline(
419
434
  ledger.entries.sort((a, b) => a.path.localeCompare(b.path));
420
435
 
421
436
  if (!context.dryRun) {
422
- await saveLedger(context.workspaceRoot, ledger);
437
+ await saveLedger(context.workspaceRoot, ledger, _io);
423
438
  }
424
439
 
425
440
  context.logger.info(
@@ -449,10 +464,13 @@ export async function runCompassAuditValidate(
449
464
  }>;
450
465
  skippedIneligible: number;
451
466
  skippedPaths: string[];
467
+ pruned: number;
468
+ prunedPaths: string[];
452
469
  }>
453
470
  > {
454
- const _io = resolveIo(context.io);
471
+ const io = resolveIo(context.io);
455
472
  const strict = input.flags["strict"] === true;
473
+ const prune = input.flags["prune"] === true;
456
474
  const scanRoot = resolveCompassScanRoot(input, context);
457
475
  const policy = resolveCompassPolicy(context.workspaceRoot, context.forgeRoot);
458
476
  const entries = await createCompassInventoryEntries(
@@ -465,6 +483,27 @@ export async function runCompassAuditValidate(
465
483
  const authored = getAuthoredEntries(entries);
466
484
  const ledger = await loadLedger(context.workspaceRoot);
467
485
 
486
+ // RFC-1242: --prune drops ledger entries whose path no longer exists.
487
+ // Opt-in only — verdict history for live paths is preserved untouched.
488
+ const prunedPaths: string[] = [];
489
+ if (prune) {
490
+ const kept: CompassAuditLedgerEntry[] = [];
491
+ for (const entry of ledger.entries) {
492
+ if (await io.exists(resolve(context.workspaceRoot, entry.path))) {
493
+ kept.push(entry);
494
+ } else {
495
+ prunedPaths.push(entry.path);
496
+ }
497
+ }
498
+ ledger.entries = kept;
499
+ if (prunedPaths.length > 0 && !context.dryRun) {
500
+ await io.writeFile(resolve(context.workspaceRoot, LEDGER_PATH), serializeLedger(ledger));
501
+ }
502
+ context.logger.info(
503
+ `[compass.audit.validate] pruned=${prunedPaths.length} ledger entr${prunedPaths.length === 1 ? "y" : "ies"} for non-existent paths${context.dryRun ? " (dry-run — not written)" : ""}`,
504
+ );
505
+ }
506
+
468
507
  // RFC-1143: drop ledger-ineligible authored paths (missions/, gitignored)
469
508
  // before the per-entry revision loop — baseline is forbidden to seed them,
470
509
  // so demanding entries is a guaranteed false-positive (COMPASS-AUDIT-01).
@@ -532,6 +571,8 @@ export async function runCompassAuditValidate(
532
571
  diagnostics,
533
572
  skippedIneligible,
534
573
  skippedPaths: skippedPaths.slice(0, SKIPPED_PATHS_CAP),
574
+ pruned: prunedPaths.length,
575
+ prunedPaths: prunedPaths.slice(0, SKIPPED_PATHS_CAP),
535
576
  },
536
577
  exitCode: hasErrors ? 1 : 0,
537
578
  summary: dueCount > 0 ? undefined : `[compass.audit.validate] OK (0 files overdue)`,
@@ -0,0 +1,359 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC-1242: validate that the live root docs/*.xml corpus describes the repository as it exists — every path token, workspace id, and knowledge-graph link target must resolve to a real file, directory, or declared node.</purpose>
4
+ <non-goals>
5
+ <item>Do not scan archived trees, vendored specs, sessions/metrics, or authored RFC/ADR/plan bodies — frozen history and forward-looking prose legitimately name paths that do not exist.</item>
6
+ <item>Do not validate markdown link targets — only the XML semantic layer is gated (markdown sweep is manual).</item>
7
+ <item>Do not mutate anything — this handler is read-only; pruning lives on compass.audit.validate --prune.</item>
8
+ </non-goals>
9
+ <KEY_DECISIONS>
10
+ <item>Tag-stack tokenizer instead of an XML dependency — the corpus is small authored XML; a balance check catches the literal-tag breakage class (COMPASS-DOC-00) without pulling fast-xml-parser.</item>
11
+ <item>DOC-03 link targets resolve to declared node ids OR file/dir conventions — dangling-by-convention stays a warning, never an error.</item>
12
+ </KEY_DECISIONS>
13
+ <CHANGE_SUMMARY>
14
+ <item>RFC-1242: review fixes — self-close detection strips quoted attr values first (a="foo/" no longer fakes a self-close), non-backticked path tokens get the same ellipsis strip, docs.* link slugs keep their basename prefix (docs.plans.plan-rfc-* resolves).</item>
15
+ <item>RFC-1242: created — scan docs/*.xml for unresolvable paths, workspace ids, and link targets (COMPASS-DOC-00..03).</item>
16
+ </CHANGE_SUMMARY>
17
+ */
18
+
19
+ import { resolve } from "node:path";
20
+ import { resolveIo } from "../../../src/utils/io.ts";
21
+ import type { Diagnostic, WorkspaceIO } from "../../../src/types.ts";
22
+ import type {
23
+ ForgeCommandInput,
24
+ ForgeCommandResult,
25
+ ForgeRuntimeContext,
26
+ } from "../../../src/types.ts";
27
+
28
+ const DOCS_GLOB = "*.xml";
29
+
30
+ // Well-formedness: minimal tag-stack check. Skips comments, CDATA, PIs and
31
+ // declarations; a literal `<name` inside prose opens a tag that never balances
32
+ // → COMPASS-DOC-00. The corpus is authored XML without < in attribute values.
33
+ function checkWellFormed(file: string, source: string): { line: number; message: string } | null {
34
+ const stack: Array<{ name: string; line: number }> = [];
35
+ let i = 0;
36
+ let line = 1;
37
+ const advanceLine = (chunk: string) => {
38
+ for (const c of chunk) if (c === "\n") line++;
39
+ };
40
+ while (i < source.length) {
41
+ const open = source.indexOf("<", i);
42
+ if (open === -1) break;
43
+ advanceLine(source.slice(i, open));
44
+ const rest = source.slice(open);
45
+ // comments / CDATA / PI / declarations
46
+ if (rest.startsWith("<!--")) {
47
+ const end = source.indexOf("-->", open + 4);
48
+ if (end === -1) return { line, message: "unterminated comment" };
49
+ advanceLine(source.slice(open, end + 3));
50
+ i = end + 3;
51
+ continue;
52
+ }
53
+ if (rest.startsWith("<![CDATA[")) {
54
+ const end = source.indexOf("]]>", open + 9);
55
+ if (end === -1) return { line, message: "unterminated CDATA" };
56
+ advanceLine(source.slice(open, end + 3));
57
+ i = end + 3;
58
+ continue;
59
+ }
60
+ if (rest.startsWith("<?")) {
61
+ const end = source.indexOf("?>", open + 2);
62
+ if (end === -1) return { line, message: "unterminated processing instruction" };
63
+ advanceLine(source.slice(open, end + 2));
64
+ i = end + 2;
65
+ continue;
66
+ }
67
+ if (rest.startsWith("<!")) {
68
+ const end = source.indexOf(">", open + 2);
69
+ if (end === -1) return { line, message: "unterminated declaration" };
70
+ advanceLine(source.slice(open, end + 1));
71
+ i = end + 1;
72
+ continue;
73
+ }
74
+ const tagMatch = /^<(\/?)([A-Za-z_][\w:.-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)?(\/?)>/.exec(rest);
75
+ if (!tagMatch) {
76
+ // `<` not starting a tag — literal text, tolerated.
77
+ advanceLine(source.slice(open, open + 1));
78
+ i = open + 1;
79
+ continue;
80
+ }
81
+ const [, closing, name, attrs = "", selfClose] = tagMatch;
82
+ advanceLine(tagMatch[0]);
83
+ i = open + tagMatch[0].length;
84
+ if (closing === "/") {
85
+ const top = stack.pop();
86
+ if (!top || top.name !== name) {
87
+ return {
88
+ line,
89
+ message: `mismatched closing tag </${name}>${top ? ` — expected </${top.name}> opened at line ${top.line}` : " — no open tag"}`,
90
+ };
91
+ }
92
+ continue;
93
+ }
94
+ const attrsUnquoted = attrs.replace(/"[^"]*"|'[^']*'/g, "");
95
+ if (selfClose !== "/" && !attrsUnquoted.trimEnd().endsWith("/")) {
96
+ stack.push({ name, line });
97
+ }
98
+ }
99
+ if (stack.length > 0) {
100
+ const top = stack[stack.length - 1]!;
101
+ return { line: top.line, message: `unclosed tag <${top.name}>` };
102
+ }
103
+ return null;
104
+ }
105
+
106
+ interface ExtractedRefs {
107
+ paths: Array<{ value: string; line: number }>;
108
+ workspaceIds: Array<{ id: string; line: number }>;
109
+ linkTargets: Array<{ target: string; line: number }>;
110
+ }
111
+
112
+ function extractRefs(source: string): ExtractedRefs {
113
+ const refs: ExtractedRefs = { paths: [], workspaceIds: [], linkTargets: [] };
114
+ const lineOf = (index: number) => source.slice(0, index).split("\n").length;
115
+ for (const m of source.matchAll(/<path>([\s\S]*?)<\/path>/g)) {
116
+ refs.paths.push({ value: m[1] ?? "", line: lineOf(m.index) });
117
+ }
118
+ for (const m of source.matchAll(/<workspace\s[^>]*?id="([^"]+)"/g)) {
119
+ refs.workspaceIds.push({ id: m[1]!, line: lineOf(m.index) });
120
+ }
121
+ for (const m of source.matchAll(/<node\s[^>]*>/g)) {
122
+ // DOC-02 only applies to ids that claim a workspace: <workspace> entries
123
+ // and workspace-typed nodes. Layer/symbolic ids (apps.rules,
124
+ // packages.rules, changelog.*) name semantic handles, not directories.
125
+ const id = /\bid="([^"]+)"/.exec(m[0])?.[1];
126
+ const type = /\btype="([^"]+)"/.exec(m[0])?.[1];
127
+ if (id && type === "workspace") {
128
+ refs.workspaceIds.push({ id, line: lineOf(m.index) });
129
+ }
130
+ }
131
+ for (const m of source.matchAll(/<link\s[^>]*?target="([^"]+)"/g)) {
132
+ refs.linkTargets.push({ target: m[1]!, line: lineOf(m.index) });
133
+ }
134
+ return refs;
135
+ }
136
+
137
+ // Normalize a <path> payload into checkable path tokens. Each backticked
138
+ // fragment is one candidate — splitting on commas inside would shred brace
139
+ // expansions (`{a,b}`) and ellipsis ranges (`docs/x/…` … `leaf`). A bare tail
140
+ // fragment after an `…` outside the backticks resolves under the previous
141
+ // fragment's directory (range shorthand for two versions of the same dir).
142
+ function pathTokens(raw: string): string[] {
143
+ const tokens: string[] = [];
144
+ const fragments = [...raw.matchAll(/`([^`]*)`/g)].map((m) => m[1]!.trim());
145
+ if (fragments.length === 0) {
146
+ return raw
147
+ .split(/[\s,]+/)
148
+ .map((t) =>
149
+ t
150
+ .trim()
151
+ .replace(/(?:…|\.\.\.).*$/, "")
152
+ .replace(/\/+$/, ""),
153
+ )
154
+ .filter((t) => t.length > 0 && !t.startsWith("<") && t !== "…" && t !== "+");
155
+ }
156
+ const joinsWithEllipsis = raw.replace(/`[^`]*`/g, "").includes("…");
157
+ let parentDir: string | null = null;
158
+ for (const frag of fragments) {
159
+ const cleaned = frag.replace(/(?:…|\.\.\.).*$/, "").replace(/\/+$/, "");
160
+ if (!cleaned || cleaned === "+") continue;
161
+ if (joinsWithEllipsis && !cleaned.includes("/") && parentDir) {
162
+ tokens.push(`${parentDir}/${cleaned}`);
163
+ continue;
164
+ }
165
+ tokens.push(cleaned);
166
+ parentDir = cleaned.split("/").slice(0, -1).join("/") || parentDir;
167
+ }
168
+ return tokens;
169
+ }
170
+
171
+ /** Directory prefix that must exist for a path token (glob tails trimmed). */
172
+ function pathCheckTarget(token: string): string {
173
+ const globIdx = token.search(/[*{[]/);
174
+ if (globIdx === -1) return token;
175
+ const prefix = token.slice(0, globIdx);
176
+ return prefix.replace(/\/[^/]*$/, "") || ".";
177
+ }
178
+
179
+ /** Resolve a workspace-style id to the package/service directory it names. */
180
+ function workspaceIdTarget(id: string): string | null {
181
+ const pkg = /^pkg-([a-z0-9-]+)$/.exec(id);
182
+ if (pkg) return `packages/${pkg[1]}`;
183
+ const svc = /^svc-([a-z0-9-]+)$/.exec(id);
184
+ if (svc) return `services/${svc[1]}`;
185
+ const app = /^app-([a-z0-9-]+)$/.exec(id);
186
+ if (app) return `apps/${app[1]}`;
187
+ const dotted = /^(packages|apps|services)\.([a-z0-9-]+)/.exec(id);
188
+ if (dotted) return `${dotted[1]}/${dotted[2]}`;
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * DOC-03 resolution for dotted link targets: declared node id (checked by the
194
+ * caller) or a file/dir convention — docs.<dir>.<slug> → docs/<dir>/<slug>*,
195
+ * packages./apps./services. → workspace dir, spec.<x> → docs/specs/<x>*,
196
+ * dna-N → docs/architecture-dna.md, dotted command names → command manifest.
197
+ */
198
+ async function linkTargetResolves(
199
+ target: string,
200
+ declaredIds: Set<string>,
201
+ io: WorkspaceIO,
202
+ docsDir: string,
203
+ commandNames: Set<string>,
204
+ ): Promise<"node" | "file" | "none"> {
205
+ if (declaredIds.has(target)) return "node";
206
+ if (commandNames.has(target)) return "file";
207
+ if (/^dna-\d+$/.test(target)) {
208
+ return (await io.exists(resolve(docsDir, "architecture-dna.md"))) ? "file" : "none";
209
+ }
210
+ const docsId = /^docs\.([a-z-]+)\.(.+)$/.exec(target);
211
+ if (docsId) {
212
+ const [, dir, slug] = docsId;
213
+ const hits = await io.glob(`${dir}/**/*${slug}*.md`, { cwd: docsDir }).catch(() => []);
214
+ if (hits.length > 0) return "file";
215
+ // generic fallback: any file whose basename contains the slug under docs/
216
+ const broad = await io.glob(`**/*${slug}*`, { cwd: docsDir }).catch(() => []);
217
+ if (broad.length > 0) return "file";
218
+ return "none";
219
+ }
220
+ const ws = workspaceIdTarget(target);
221
+ if (ws) return (await io.exists(ws)) ? "file" : "none";
222
+ const spec = /^spec\.([a-z0-9-]+)$/.exec(target);
223
+ if (spec) {
224
+ const hits = await io.glob(`specs/${spec[1]}*/**`, { cwd: docsDir }).catch(() => []);
225
+ const dirs = await io.glob(`specs/${spec[1]}*`, { cwd: docsDir }).catch(() => []);
226
+ return hits.length + dirs.length > 0 ? "file" : "none";
227
+ }
228
+ return "none";
229
+ }
230
+
231
+ export async function runCompassDocsValidate(
232
+ input: ForgeCommandInput,
233
+ context: ForgeRuntimeContext,
234
+ ): Promise<
235
+ ForgeCommandResult<{
236
+ command: "compass.docs.validate";
237
+ status: "pass" | "fail";
238
+ scanned: { xmlFiles: number; pathsChecked: number; idsChecked: number };
239
+ diagnostics: Diagnostic[];
240
+ }>
241
+ > {
242
+ const io = resolveIo(context.io);
243
+ const docsDir = resolve(context.workspaceRoot, "docs");
244
+ const diagnostics: Diagnostic[] = [];
245
+ let pathsChecked = 0;
246
+ let idsChecked = 0;
247
+
248
+ // Command-name convention for DOC-03: dotted targets like ratgeber.hub.validate
249
+ // resolve when the command registry lists them.
250
+ const commandNames = new Set<string>();
251
+ const manifestPath = resolve(docsDir, "command-manifest.generated.yaml");
252
+ if (await io.exists(manifestPath)) {
253
+ const manifest = await io.readFile(manifestPath);
254
+ for (const m of manifest.matchAll(/^\s*- name: ([\w.-]+)\s*$/gm)) {
255
+ commandNames.add(m[1]!);
256
+ }
257
+ }
258
+
259
+ const xmlFiles = (await io.glob(DOCS_GLOB, { cwd: docsDir })).sort();
260
+ const declaredNodeIds = new Set<string>();
261
+
262
+ // Pass 1: collect declared node ids so link targets can resolve across files.
263
+ const sources = new Map<string, string>();
264
+ for (const rel of xmlFiles) {
265
+ const abs = resolve(docsDir, rel);
266
+ const source = await io.readFile(abs);
267
+ sources.set(rel, source);
268
+ for (const m of source.matchAll(/<node\s[^>]*?id="([^"]+)"/g)) {
269
+ declaredNodeIds.add(m[1]!);
270
+ }
271
+ }
272
+
273
+ for (const rel of xmlFiles) {
274
+ const file = `docs/${rel}`;
275
+ const source = sources.get(rel)!;
276
+
277
+ const malformed = checkWellFormed(file, source);
278
+ if (malformed) {
279
+ diagnostics.push({
280
+ ruleId: "COMPASS-DOC-00",
281
+ severity: "error",
282
+ file,
283
+ line: malformed.line,
284
+ message: `file is not well-formed XML: ${malformed.message}`,
285
+ });
286
+ continue; // refs from a broken file are noise
287
+ }
288
+
289
+ const refs = extractRefs(source);
290
+
291
+ for (const { value, line } of refs.paths) {
292
+ for (const token of pathTokens(value)) {
293
+ pathsChecked++;
294
+ const target = pathCheckTarget(token);
295
+ if (!(await io.exists(resolve(context.workspaceRoot, target)))) {
296
+ diagnostics.push({
297
+ ruleId: "COMPASS-DOC-01",
298
+ severity: "error",
299
+ file,
300
+ line,
301
+ message: `<path> references '${token}' — does not exist under the workspace root`,
302
+ fixHint: "Repoint to the live path or remove the stale reference",
303
+ });
304
+ }
305
+ }
306
+ }
307
+
308
+ for (const { id, line } of refs.workspaceIds) {
309
+ const target = workspaceIdTarget(id);
310
+ if (!target) continue;
311
+ idsChecked++;
312
+ if (!(await io.exists(resolve(context.workspaceRoot, target)))) {
313
+ diagnostics.push({
314
+ ruleId: "COMPASS-DOC-02",
315
+ severity: "error",
316
+ file,
317
+ line,
318
+ message: `id '${id}' names workspace path '${target}' — does not exist`,
319
+ fixHint: "Repoint to the live package or remove the stale entry",
320
+ });
321
+ }
322
+ }
323
+
324
+ for (const { target, line } of refs.linkTargets) {
325
+ idsChecked++;
326
+ const resolved = await linkTargetResolves(target, declaredNodeIds, io, docsDir, commandNames);
327
+ if (resolved === "none") {
328
+ diagnostics.push({
329
+ ruleId: "COMPASS-DOC-03",
330
+ severity: "warning",
331
+ file,
332
+ line,
333
+ message: `link target '${target}' resolves to no declared node and no file`,
334
+ });
335
+ }
336
+ }
337
+ }
338
+
339
+ const errors = diagnostics.filter((d) => d.severity === "error");
340
+ for (const d of diagnostics) {
341
+ context.logger[d.severity === "error" ? "error" : "warn"](
342
+ `[compass.docs.validate] ${d.ruleId}: ${d.file}${d.line ? `:${d.line}` : ""}: ${d.message}`,
343
+ );
344
+ }
345
+
346
+ return {
347
+ data: {
348
+ command: "compass.docs.validate",
349
+ status: errors.length === 0 ? "pass" : "fail",
350
+ scanned: { xmlFiles: xmlFiles.length, pathsChecked, idsChecked },
351
+ diagnostics,
352
+ },
353
+ exitCode: errors.length > 0 ? 1 : 0,
354
+ summary:
355
+ errors.length === 0
356
+ ? `[compass.docs.validate] OK (${xmlFiles.length} files, ${pathsChecked} paths, ${idsChecked} ids)`
357
+ : `[compass.docs.validate] ${errors.length} error(s)`,
358
+ };
359
+ }