@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.
- package/CHANGELOG.md +8 -0
- package/bin/cli.ts +7 -2
- package/dist/bin/cli.js +7 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +2 -0
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/audit/audit.module.d.ts.map +1 -1
- package/dist/os/audit/audit.module.js +2 -0
- package/dist/os/audit/audit.module.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +47 -15
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +39 -9
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
- package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-docs-validate.js +326 -0
- package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
- package/dist/os/compass/handlers/summary-record.d.ts +11 -2
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +93 -5
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +124 -73
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +3 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +9 -2
- package/dist/os/naming/naming.module.js.map +1 -1
- package/dist/os/notes/notes.module.d.ts.map +1 -1
- package/dist/os/notes/notes.module.js +17 -6
- package/dist/os/notes/notes.module.js.map +1 -1
- package/dist/os/plan/plan.module.d.ts.map +1 -1
- package/dist/os/plan/plan.module.js +2 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts.map +1 -1
- package/dist/os/plugin/plugin.module.js +9 -2
- package/dist/os/plugin/plugin.module.js.map +1 -1
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +19 -4
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
- package/dist/os/queue/handlers/queue-validate.js +154 -7
- package/dist/os/queue/handlers/queue-validate.js.map +1 -1
- package/dist/os/queue/manifest.d.ts +17 -1
- package/dist/os/queue/manifest.d.ts.map +1 -1
- package/dist/os/queue/manifest.js +127 -6
- package/dist/os/queue/manifest.js.map +1 -1
- package/dist/os/queue/queue.module.d.ts.map +1 -1
- package/dist/os/queue/queue.module.js +32 -4
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/queue/types.d.ts +100 -1
- package/dist/os/queue/types.d.ts.map +1 -1
- package/dist/os/queue/types.js +44 -0
- package/dist/os/queue/types.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +28 -5
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +27 -3
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +11 -0
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +1 -0
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/rfc/verification-evidence.d.ts +21 -0
- package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
- package/dist/os/rfc/verification-evidence.js +140 -23
- package/dist/os/rfc/verification-evidence.js.map +1 -1
- package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
- package/dist/os/rfc/verification-refresh.js +72 -37
- package/dist/os/rfc/verification-refresh.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +46 -5
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +5 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
- package/dist/os/spec/live-spec-merge.js +55 -134
- package/dist/os/spec/live-spec-merge.js.map +1 -1
- package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
- package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
- package/dist/os/spec/live-spec-rebuild.js +272 -0
- package/dist/os/spec/live-spec-rebuild.js.map +1 -0
- package/dist/os/spec/live-spec-shared.d.ts +62 -0
- package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
- package/dist/os/spec/live-spec-shared.js +315 -0
- package/dist/os/spec/live-spec-shared.js.map +1 -0
- package/dist/os/spec/live-spec-types.d.ts +17 -1
- package/dist/os/spec/live-spec-types.d.ts.map +1 -1
- package/dist/os/spec/live-spec-types.js +3 -0
- package/dist/os/spec/live-spec-types.js.map +1 -1
- package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
- package/dist/os/spec/live-spec-validate.js +105 -2
- package/dist/os/spec/live-spec-validate.js.map +1 -1
- package/dist/os/spec/spec-materialize.d.ts +2 -0
- package/dist/os/spec/spec-materialize.d.ts.map +1 -1
- package/dist/os/spec/spec-materialize.js +45 -5
- package/dist/os/spec/spec-materialize.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +19 -0
- package/dist/os/spec/spec-schema.d.ts.map +1 -1
- package/dist/os/spec/spec-schema.js +30 -0
- package/dist/os/spec/spec-schema.js.map +1 -1
- package/dist/os/spec/spec-status.d.ts +4 -0
- package/dist/os/spec/spec-status.d.ts.map +1 -1
- package/dist/os/spec/spec-status.js +19 -1
- package/dist/os/spec/spec-status.js.map +1 -1
- package/dist/os/spec/spec-validate.d.ts.map +1 -1
- package/dist/os/spec/spec-validate.js +55 -8
- package/dist/os/spec/spec-validate.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +84 -8
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +13 -4
- package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
- package/dist/os/workflow/workflow.module.d.ts.map +1 -1
- package/dist/os/workflow/workflow.module.js +13 -5
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/cli-flags.d.ts.map +1 -1
- package/dist/src/cli-flags.js +25 -5
- package/dist/src/cli-flags.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +3 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +8 -3
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +14 -1
- package/dist/src/pipeline-status.d.ts.map +1 -1
- package/dist/src/pipeline-status.js +28 -8
- package/dist/src/pipeline-status.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/generated-marker.d.ts +8 -0
- package/dist/src/utils/generated-marker.d.ts.map +1 -1
- package/dist/src/utils/generated-marker.js +25 -0
- package/dist/src/utils/generated-marker.js.map +1 -1
- package/os/adr/adr.module.ts +2 -0
- package/os/audit/audit.module.ts +2 -0
- package/os/compass/compass.module.ts +49 -15
- package/os/compass/handlers/compass-audit-handler.ts +50 -9
- package/os/compass/handlers/compass-docs-validate.ts +359 -0
- package/os/compass/handlers/summary-record.ts +104 -5
- package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
- package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
- package/os/compass/handlers/tests/summary-record.test.ts +127 -0
- package/os/core/core.module.ts +147 -75
- package/os/core/docs-archive.test.ts +136 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/naming/naming.module.ts +9 -2
- package/os/notes/notes.module.ts +17 -6
- package/os/plan/plan-0000-template.md +150 -0
- package/os/plan/plan.module.ts +2 -0
- package/os/plugin/plugin.module.ts +9 -2
- package/os/program/program.module.ts +19 -4
- package/os/queue/decision-ledger.test.ts +163 -0
- package/os/queue/handlers/queue-validate.ts +190 -8
- package/os/queue/manifest.ts +201 -7
- package/os/queue/queue-validate.test.ts +303 -0
- package/os/queue/queue.module.ts +32 -4
- package/os/queue/types.ts +56 -1
- package/os/rfc/handlers/implement-stamp.ts +35 -5
- package/os/rfc/rfc.module.ts +29 -3
- package/os/rfc/types.ts +17 -1
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +155 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +80 -38
- package/os/session/handlers/save.ts +41 -5
- package/os/session/session.module.ts +5 -0
- package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
- package/os/spec/live-spec-list-show-validate.test.ts +10 -6
- package/os/spec/live-spec-merge.test.ts +187 -7
- package/os/spec/live-spec-merge.ts +71 -155
- package/os/spec/live-spec-rebuild.test.ts +317 -0
- package/os/spec/live-spec-rebuild.ts +356 -0
- package/os/spec/live-spec-shared.ts +378 -0
- package/os/spec/live-spec-types.ts +21 -1
- package/os/spec/live-spec-validate.test.ts +317 -0
- package/os/spec/live-spec-validate.ts +106 -2
- package/os/spec/spec-materialize.test.ts +192 -0
- package/os/spec/spec-materialize.ts +48 -4
- package/os/spec/spec-schema.ts +49 -0
- package/os/spec/spec-status.ts +24 -0
- package/os/spec/spec-validate.test.ts +66 -0
- package/os/spec/spec-validate.ts +62 -8
- package/os/spec/spec.module.ts +85 -8
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
- package/os/werkstatt/werkstatt.module.ts +13 -4
- package/os/workflow/workflow.module.ts +13 -5
- package/package.json +1 -1
- package/skills/_shared/fo-pipeline-conventions.md +126 -1
- package/skills/fo/fo-doc-audit/SKILL.md +4 -4
- package/skills/fo/fo-explore/SKILL.md +2 -1
- package/skills/fo/fo-fix/SKILL.md +11 -3
- package/skills/fo/fo-handoff/SKILL.md +4 -6
- package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
- package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
- package/skills/fo/fo-idea-implement/SKILL.md +53 -10
- package/skills/fo/fo-idea-plan/SKILL.md +28 -6
- package/skills/fo/fo-review/SKILL.md +69 -8
- package/skills/fo/fo-session-retro/SKILL.md +3 -2
- package/skills/fo/fo-step-commit/SKILL.md +2 -0
- package/skills/shared/grilling/SKILL.md +12 -0
- package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
- package/skills/shared/writing-great-skills/SKILL.md +2 -1
- package/src/cli-flags.ts +23 -5
- package/src/config/forge-config.ts +4 -0
- package/src/onboarding/init.ts +8 -3
- package/src/pipeline-status.ts +32 -8
- package/src/tests/cli-flags.test.ts +20 -0
- package/src/tests/cli-output.test.ts +13 -7
- package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
- package/src/tests/implement-stamp.test.ts +38 -0
- package/src/tests/session-handlers.test.ts +33 -0
- package/src/types.ts +13 -1
- package/src/utils/generated-marker.ts +28 -0
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- 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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
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
|
+
}
|