@warpgogol/forge 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>spec.validate handler — validates vendored spec packages under docs/specs/ (RFC-0394).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not modify spec files — validate is read-only.</item>
|
|
6
|
+
<item>Do not implement materialization — that is RFC-0396.</item>
|
|
7
|
+
<item>Do not implement amendment validation — that is RFC-0397.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0394: initial spec.validate handler with SPEC-01..07 rules.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { parse as parseYaml } from "yaml";
|
|
19
|
+
import { byteHash } from "../../src/utils/hash.ts";
|
|
20
|
+
import { collectFiles } from "../../src/utils/fs.ts";
|
|
21
|
+
import { loadForgeConfig } from "../../src/config/forge-config.ts";
|
|
22
|
+
import type {
|
|
23
|
+
ForgeCommandInput,
|
|
24
|
+
ForgeCommandResult,
|
|
25
|
+
ForgeRuntimeContext,
|
|
26
|
+
} from "../../src/types.ts";
|
|
27
|
+
import {
|
|
28
|
+
forgeSpecSchema,
|
|
29
|
+
specIntegritySchema,
|
|
30
|
+
specAmendmentSchema,
|
|
31
|
+
type ForgeSpec,
|
|
32
|
+
type SpecIntegrity,
|
|
33
|
+
type SpecAmendment,
|
|
34
|
+
type SpecViolation,
|
|
35
|
+
type SpecValidateResult,
|
|
36
|
+
} from "./spec-schema.ts";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
async function readYaml<T>(filePath: string): Promise<T | null> {
|
|
43
|
+
try {
|
|
44
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
45
|
+
return parseYaml(content) as T;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function listSpecDirs(specsDir: string): Promise<string[]> {
|
|
52
|
+
if (!existsSync(specsDir)) return [];
|
|
53
|
+
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
|
54
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listSnapshotFiles(
|
|
58
|
+
specDir: string,
|
|
59
|
+
exclude: Set<string>,
|
|
60
|
+
): Promise<string[]> {
|
|
61
|
+
const files = await collectFiles(specDir, {
|
|
62
|
+
ignore: (name) => exclude.has(name),
|
|
63
|
+
});
|
|
64
|
+
return files.map((abs) => path.relative(specDir, abs).replace(/\\/g, "/"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// SPEC-01: Integrity check
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
async function checkIntegrity(
|
|
72
|
+
specDir: string,
|
|
73
|
+
violations: SpecViolation[],
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const integrityPath = path.join(specDir, "integrity.yaml");
|
|
76
|
+
const integrity = await readYaml<SpecIntegrity>(integrityPath);
|
|
77
|
+
if (!integrity) {
|
|
78
|
+
violations.push({ rule: "SPEC-01", message: "integrity.yaml not found" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parsed = specIntegritySchema.safeParse(integrity);
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
violations.push({
|
|
85
|
+
rule: "SPEC-01",
|
|
86
|
+
message: `integrity.yaml schema violation: ${parsed.error.message}`,
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const manifest = parsed.data;
|
|
92
|
+
const exclude = new Set(["forge-spec.yaml", "integrity.yaml", "amendments"]);
|
|
93
|
+
|
|
94
|
+
for (const [relPath, expectedHash] of Object.entries(manifest.files)) {
|
|
95
|
+
const fullPath = path.join(specDir, relPath);
|
|
96
|
+
try {
|
|
97
|
+
const content = await fs.readFile(fullPath);
|
|
98
|
+
const actualHash = byteHash(content);
|
|
99
|
+
if (actualHash !== expectedHash) {
|
|
100
|
+
violations.push({
|
|
101
|
+
rule: "SPEC-01",
|
|
102
|
+
message: `integrity mismatch: ${relPath}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
violations.push({
|
|
107
|
+
rule: "SPEC-01",
|
|
108
|
+
message: `file in manifest not found: ${relPath}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check for files in snapshot not in manifest
|
|
114
|
+
const snapshotFiles = await listSnapshotFiles(specDir, exclude);
|
|
115
|
+
for (const file of snapshotFiles) {
|
|
116
|
+
if (!(file in manifest.files)) {
|
|
117
|
+
violations.push({
|
|
118
|
+
rule: "SPEC-01",
|
|
119
|
+
message: `file not in integrity manifest: ${file}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// SPEC-02: Schema validation
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function checkSchema(
|
|
130
|
+
raw: unknown,
|
|
131
|
+
violations: SpecViolation[],
|
|
132
|
+
): ForgeSpec | null {
|
|
133
|
+
const parsed = forgeSpecSchema.safeParse(raw);
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
violations.push({
|
|
136
|
+
rule: "SPEC-02",
|
|
137
|
+
message: `forge-spec.yaml schema violation: ${parsed.error.issues.map((i) => i.message).join("; ")}`,
|
|
138
|
+
});
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return parsed.data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// SPEC-03: Dependency cycle detection
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
function checkCycles(spec: ForgeSpec, violations: SpecViolation[]): void {
|
|
149
|
+
const nodes = new Map<string, string[]>();
|
|
150
|
+
for (const node of spec.rfcs) {
|
|
151
|
+
nodes.set(node.id, node.dependsOn);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const visited = new Set<string>();
|
|
155
|
+
const stack = new Set<string>();
|
|
156
|
+
|
|
157
|
+
function dfs(id: string, path: string[]): boolean {
|
|
158
|
+
if (stack.has(id)) {
|
|
159
|
+
const cycle = [...path, id].join(" -> ");
|
|
160
|
+
violations.push({
|
|
161
|
+
rule: "SPEC-03",
|
|
162
|
+
message: `dependency cycle: ${cycle}`,
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (visited.has(id)) return false;
|
|
167
|
+
visited.add(id);
|
|
168
|
+
stack.add(id);
|
|
169
|
+
const deps = nodes.get(id) ?? [];
|
|
170
|
+
for (const dep of deps) {
|
|
171
|
+
if (dfs(dep, [...path, id])) return true;
|
|
172
|
+
}
|
|
173
|
+
stack.delete(id);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const node of spec.rfcs) {
|
|
178
|
+
if (!visited.has(node.id)) {
|
|
179
|
+
dfs(node.id, []);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// SPEC-04: Unresolvable references
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function checkReferences(spec: ForgeSpec, violations: SpecViolation[]): void {
|
|
189
|
+
const nodeIds = new Set(spec.rfcs.map((n) => n.id));
|
|
190
|
+
const docNames = new Set(Object.keys(spec.documents));
|
|
191
|
+
|
|
192
|
+
for (const node of spec.rfcs) {
|
|
193
|
+
for (const dep of node.dependsOn) {
|
|
194
|
+
if (!nodeIds.has(dep)) {
|
|
195
|
+
violations.push({
|
|
196
|
+
rule: "SPEC-04",
|
|
197
|
+
message: `dependsOn reference does not resolve: ${dep} (in node ${node.id})`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const source of node.sources) {
|
|
202
|
+
const docName = source.split("#")[0]!;
|
|
203
|
+
if (!docNames.has(docName)) {
|
|
204
|
+
violations.push({
|
|
205
|
+
rule: "SPEC-04",
|
|
206
|
+
message: `source reference does not resolve: ${source} (in node ${node.id})`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// SPEC-05: Roadmap node without a wave
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
function checkWaveCoverage(spec: ForgeSpec, violations: SpecViolation[]): void {
|
|
218
|
+
const waveIds = new Set(spec.waves.map((w) => w.id));
|
|
219
|
+
for (const node of spec.rfcs) {
|
|
220
|
+
if (!waveIds.has(node.wave)) {
|
|
221
|
+
violations.push({
|
|
222
|
+
rule: "SPEC-05",
|
|
223
|
+
message: `node ${node.id} references wave ${node.wave} which does not exist`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// SPEC-06: Duplicate node or decision id
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function checkDuplicates(spec: ForgeSpec, violations: SpecViolation[]): void {
|
|
234
|
+
const nodeIds = new Set<string>();
|
|
235
|
+
for (const node of spec.rfcs) {
|
|
236
|
+
if (nodeIds.has(node.id)) {
|
|
237
|
+
violations.push({
|
|
238
|
+
rule: "SPEC-06",
|
|
239
|
+
message: `duplicate node id: ${node.id}`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
nodeIds.add(node.id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const decisionIds = new Set<string>();
|
|
246
|
+
for (const decision of spec.decisions) {
|
|
247
|
+
if (decisionIds.has(decision.id)) {
|
|
248
|
+
violations.push({
|
|
249
|
+
rule: "SPEC-06",
|
|
250
|
+
message: `duplicate decision id: ${decision.id}`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
decisionIds.add(decision.id);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// SPEC-07: materializedAs points to missing RFC file
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
async function checkMaterializedAs(
|
|
262
|
+
spec: ForgeSpec,
|
|
263
|
+
workspaceRoot: string,
|
|
264
|
+
rfcDir: string,
|
|
265
|
+
violations: SpecViolation[],
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
for (const node of spec.rfcs) {
|
|
268
|
+
if (!node.materializedAs) continue;
|
|
269
|
+
const rfcId = node.materializedAs;
|
|
270
|
+
const rfcFiles = await fs.readdir(rfcDir).catch(() => []);
|
|
271
|
+
const found = rfcFiles.some((f) => f.startsWith(rfcId.toLowerCase().replace(/^RFC-/, "rfc-")));
|
|
272
|
+
if (!found) {
|
|
273
|
+
violations.push({
|
|
274
|
+
rule: "SPEC-07",
|
|
275
|
+
message: `materializedAs ${rfcId} for node ${node.id} not found in ${rfcDir}`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// SPEC-08..11: Amendment validation (RFC-0397)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
async function loadAmendments(specDir: string): Promise<SpecAmendment[]> {
|
|
286
|
+
const amendmentsDir = path.join(specDir, "amendments");
|
|
287
|
+
if (!existsSync(amendmentsDir)) return [];
|
|
288
|
+
|
|
289
|
+
const entries = await fs.readdir(amendmentsDir);
|
|
290
|
+
const amendments: SpecAmendment[] = [];
|
|
291
|
+
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
if (!entry.endsWith(".md")) continue;
|
|
294
|
+
try {
|
|
295
|
+
const content = await fs.readFile(path.join(amendmentsDir, entry), "utf8");
|
|
296
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
297
|
+
if (!fmMatch) continue;
|
|
298
|
+
const raw = parseYaml(fmMatch[1]) as unknown;
|
|
299
|
+
const parsed = specAmendmentSchema.safeParse(raw);
|
|
300
|
+
if (parsed.success) {
|
|
301
|
+
amendments.push(parsed.data);
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
// skip unreadable files
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return amendments;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function checkAmendments(
|
|
312
|
+
spec: ForgeSpec,
|
|
313
|
+
amendments: SpecAmendment[],
|
|
314
|
+
violations: SpecViolation[],
|
|
315
|
+
): void {
|
|
316
|
+
const nodeIds = new Set(spec.rfcs.map((n) => n.id));
|
|
317
|
+
const decisionIds = new Set(spec.decisions.map((d) => d.id));
|
|
318
|
+
const docNames = new Set(Object.keys(spec.documents));
|
|
319
|
+
|
|
320
|
+
const seenIds = new Set<string>();
|
|
321
|
+
|
|
322
|
+
for (const amendment of amendments) {
|
|
323
|
+
// SPEC-08: schema/id violations
|
|
324
|
+
if (seenIds.has(amendment.id)) {
|
|
325
|
+
violations.push({
|
|
326
|
+
rule: "SPEC-08",
|
|
327
|
+
message: `duplicate amendment id: ${amendment.id}`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
seenIds.add(amendment.id);
|
|
331
|
+
|
|
332
|
+
// SPEC-09: unresolvable targets
|
|
333
|
+
for (const target of amendment.targets) {
|
|
334
|
+
if (target.kind === "section") {
|
|
335
|
+
if (!docNames.has(target.document)) {
|
|
336
|
+
violations.push({
|
|
337
|
+
rule: "SPEC-09",
|
|
338
|
+
message: `amendment ${amendment.id} targets document not found: ${target.document}`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} else if (target.kind === "decision") {
|
|
342
|
+
if (!decisionIds.has(target.id)) {
|
|
343
|
+
violations.push({
|
|
344
|
+
rule: "SPEC-09",
|
|
345
|
+
message: `amendment ${amendment.id} targets decision not found: ${target.id}`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
} else if (target.kind === "node") {
|
|
349
|
+
if (!nodeIds.has(target.id)) {
|
|
350
|
+
violations.push({
|
|
351
|
+
rule: "SPEC-09",
|
|
352
|
+
message: `amendment ${amendment.id} targets node not found: ${target.id}`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// SPEC-11: accepted amendment without reviewers
|
|
359
|
+
if (amendment.status === "accepted" && amendment.reviewers.length === 0) {
|
|
360
|
+
violations.push({
|
|
361
|
+
rule: "SPEC-11",
|
|
362
|
+
message: `accepted amendment ${amendment.id} has no reviewers`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// SPEC-10: two accepted amendments target the same anchor with conflicting Becomes
|
|
368
|
+
const acceptedAmendments = amendments.filter((a) => a.status === "accepted");
|
|
369
|
+
const targetCounts = new Map<string, number>();
|
|
370
|
+
for (const amendment of acceptedAmendments) {
|
|
371
|
+
for (const target of amendment.targets) {
|
|
372
|
+
const key = target.kind === "section"
|
|
373
|
+
? `section:${target.document}:${target.anchor}`
|
|
374
|
+
: target.kind === "decision"
|
|
375
|
+
? `decision:${target.id}`
|
|
376
|
+
: `node:${target.id}`;
|
|
377
|
+
targetCounts.set(key, (targetCounts.get(key) ?? 0) + 1);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const [key, count] of targetCounts) {
|
|
381
|
+
if (count > 1) {
|
|
382
|
+
violations.push({
|
|
383
|
+
rule: "SPEC-10",
|
|
384
|
+
message: `conflicting accepted amendments targeting ${key} — manual review required`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Main handler
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
export async function runSpecValidate(
|
|
395
|
+
input: ForgeCommandInput,
|
|
396
|
+
context: ForgeRuntimeContext,
|
|
397
|
+
): Promise<ForgeCommandResult<SpecValidateResult>> {
|
|
398
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
399
|
+
|
|
400
|
+
// Resolve specs directory from forge.yaml
|
|
401
|
+
let specsDir = "docs/specs";
|
|
402
|
+
try {
|
|
403
|
+
const config = loadForgeConfig(workspaceRoot);
|
|
404
|
+
specsDir = config.paths.specsDir;
|
|
405
|
+
} catch {
|
|
406
|
+
// Use default
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const specsRoot = path.join(workspaceRoot, specsDir);
|
|
410
|
+
const filterSpec = input.flags["spec"] as string | undefined;
|
|
411
|
+
|
|
412
|
+
let specDirs = await listSpecDirs(specsRoot);
|
|
413
|
+
if (filterSpec) {
|
|
414
|
+
if (!specDirs.includes(filterSpec)) {
|
|
415
|
+
return {
|
|
416
|
+
data: {
|
|
417
|
+
command: "spec.validate",
|
|
418
|
+
status: "fail",
|
|
419
|
+
specs: [],
|
|
420
|
+
},
|
|
421
|
+
exitCode: 1,
|
|
422
|
+
summary: `spec.validate: spec '${filterSpec}' not found. Available: ${specDirs.join(", ") || "(none)"}`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
specDirs = [filterSpec];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
|
|
429
|
+
const results: SpecValidateResult["specs"] = [];
|
|
430
|
+
|
|
431
|
+
for (const specId of specDirs) {
|
|
432
|
+
const specDir = path.join(specsRoot, specId);
|
|
433
|
+
const violations: SpecViolation[] = [];
|
|
434
|
+
|
|
435
|
+
// Load forge-spec.yaml
|
|
436
|
+
const forgeSpecPath = path.join(specDir, "forge-spec.yaml");
|
|
437
|
+
const rawSpec = await readYaml<unknown>(forgeSpecPath);
|
|
438
|
+
if (!rawSpec) {
|
|
439
|
+
results.push({
|
|
440
|
+
id: specId,
|
|
441
|
+
status: "unknown",
|
|
442
|
+
violations: [{ rule: "SPEC-02", message: "forge-spec.yaml not found" }],
|
|
443
|
+
});
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// SPEC-02: Schema
|
|
448
|
+
const spec = checkSchema(rawSpec, violations);
|
|
449
|
+
if (!spec) {
|
|
450
|
+
results.push({ id: specId, status: "unknown", violations });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// SPEC-01: Integrity
|
|
455
|
+
await checkIntegrity(specDir, violations);
|
|
456
|
+
|
|
457
|
+
// SPEC-03: Cycles
|
|
458
|
+
checkCycles(spec, violations);
|
|
459
|
+
|
|
460
|
+
// SPEC-04: References
|
|
461
|
+
checkReferences(spec, violations);
|
|
462
|
+
|
|
463
|
+
// SPEC-05: Wave coverage
|
|
464
|
+
checkWaveCoverage(spec, violations);
|
|
465
|
+
|
|
466
|
+
// SPEC-06: Duplicates
|
|
467
|
+
checkDuplicates(spec, violations);
|
|
468
|
+
|
|
469
|
+
// SPEC-07: materializedAs
|
|
470
|
+
await checkMaterializedAs(spec, workspaceRoot, rfcDir, violations);
|
|
471
|
+
|
|
472
|
+
// SPEC-08..11: Amendments (RFC-0397)
|
|
473
|
+
const amendments = await loadAmendments(specDir);
|
|
474
|
+
checkAmendments(spec, amendments, violations);
|
|
475
|
+
|
|
476
|
+
results.push({
|
|
477
|
+
id: spec.id,
|
|
478
|
+
status: spec.status,
|
|
479
|
+
violations,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const hasFailures = results.some((r) => r.violations.length > 0);
|
|
484
|
+
|
|
485
|
+
if (outputFormat === "pretty") {
|
|
486
|
+
if (results.length === 0) {
|
|
487
|
+
logger.info("No vendored specs found.");
|
|
488
|
+
} else {
|
|
489
|
+
for (const result of results) {
|
|
490
|
+
if (result.violations.length === 0) {
|
|
491
|
+
logger.success(`spec.validate: ${result.id} — pass`);
|
|
492
|
+
} else {
|
|
493
|
+
logger.error(`spec.validate: ${result.id} — ${result.violations.length} violation(s)`);
|
|
494
|
+
for (const v of result.violations) {
|
|
495
|
+
logger.error(` ${v.rule}: ${v.message}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
data: {
|
|
504
|
+
command: "spec.validate",
|
|
505
|
+
status: hasFailures ? "fail" : "pass",
|
|
506
|
+
specs: results,
|
|
507
|
+
},
|
|
508
|
+
exitCode: hasFailures ? 1 : 0,
|
|
509
|
+
summary: hasFailures
|
|
510
|
+
? `spec.validate: ${results.filter((r) => r.violations.length > 0).length} spec(s) with violations`
|
|
511
|
+
: `spec.validate: all ${results.length} spec(s) pass`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Register the forge spec module — spec.validate (RFC-0394), spec.status + spec.materialize (RFC-0396), spec.live.merge/list/show/validate (RFC-0711).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement skill logic — skills live in skills/.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0394: initial forgeSpecModule registering spec.validate.</item>
|
|
10
|
+
<item>RFC-0396: added spec.status and spec.materialize commands.</item>
|
|
11
|
+
<item>RFC-0711: added spec.live.merge, spec.live.list, spec.live.show, spec.live.validate commands.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ForgeModule } from "../../src/forge-module.ts";
|
|
16
|
+
|
|
17
|
+
export const forgeSpecModule: ForgeModule = {
|
|
18
|
+
name: "forge-spec",
|
|
19
|
+
version: "0.2.0",
|
|
20
|
+
async register(registry) {
|
|
21
|
+
const { runSpecValidate } = await import("./spec-validate.ts");
|
|
22
|
+
const { runSpecStatus } = await import("./spec-status.ts");
|
|
23
|
+
const { runSpecMaterialize } = await import("./spec-materialize.ts");
|
|
24
|
+
const { runSpecLiveMerge } = await import("./live-spec-merge.ts");
|
|
25
|
+
const { runSpecLiveList } = await import("./live-spec-list.ts");
|
|
26
|
+
const { runSpecLiveShow } = await import("./live-spec-show.ts");
|
|
27
|
+
const { runSpecLiveValidate } = await import("./live-spec-validate.ts");
|
|
28
|
+
|
|
29
|
+
registry.registerCommand({
|
|
30
|
+
name: "spec.validate",
|
|
31
|
+
description:
|
|
32
|
+
"Validate vendored spec packages under docs/specs/. " +
|
|
33
|
+
"Checks integrity (SHA-256), schema, dependency graph (acyclic), " +
|
|
34
|
+
"reference resolution, wave coverage, duplicate ids, and materializedAs links. " +
|
|
35
|
+
"Use --spec=<id> to validate a single spec.",
|
|
36
|
+
scope: "workspace",
|
|
37
|
+
flags: {
|
|
38
|
+
spec: {
|
|
39
|
+
kind: "string",
|
|
40
|
+
description: "Validate only the named spec.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
reads: ["docs/specs/**/*"],
|
|
44
|
+
execute: runSpecValidate,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
registry.registerCommand({
|
|
48
|
+
name: "spec.status",
|
|
49
|
+
description:
|
|
50
|
+
"Show roadmap progress for vendored specs. " +
|
|
51
|
+
"Without --spec, summarizes all specs; with it, full per-node table + computed front.",
|
|
52
|
+
scope: "workspace",
|
|
53
|
+
flags: {
|
|
54
|
+
spec: {
|
|
55
|
+
kind: "string",
|
|
56
|
+
description: "Show status for a single spec.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
reads: ["docs/specs/**/*", "docs/rfcs/**/*.md"],
|
|
60
|
+
execute: runSpecStatus,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
registry.registerCommand({
|
|
64
|
+
name: "spec.materialize",
|
|
65
|
+
description:
|
|
66
|
+
"Scaffold RFC files for the next N front nodes of a spec roadmap. " +
|
|
67
|
+
"Requires --spec=<id>. Optional: --next=<N> (default 8, max 12), --nodes=<id,id> explicit selection.",
|
|
68
|
+
scope: "workspace",
|
|
69
|
+
mutatesState: true,
|
|
70
|
+
writes: ["docs/rfcs/rfc-*.md", "docs/specs/*/forge-spec.yaml"],
|
|
71
|
+
reads: ["docs/specs/**/*", "docs/rfcs/**/*.md"],
|
|
72
|
+
flags: {
|
|
73
|
+
spec: { kind: "string", required: true, description: "Spec id to materialize from." },
|
|
74
|
+
next: { kind: "string", description: "Number of front nodes to materialize (default 8, max 12)." },
|
|
75
|
+
nodes: { kind: "string", description: "Comma-separated explicit node ids to materialize." },
|
|
76
|
+
},
|
|
77
|
+
execute: runSpecMaterialize,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
registry.registerCommand({
|
|
81
|
+
name: "spec.live.merge",
|
|
82
|
+
description:
|
|
83
|
+
"Merge deltas from an implemented RFC's ## Design section into a living feature spec " +
|
|
84
|
+
"under docs/specs/live/<domain>.md. Requires --id=<RFC-XXXX>. " +
|
|
85
|
+
"Domain is auto-derived from packagesImpacted[0] when liveSpec: true, or uses " +
|
|
86
|
+
"the string value when liveSpec: <domain>. " +
|
|
87
|
+
"All-or-nothing: aborts on any heading conflict without writing. " +
|
|
88
|
+
"Use --dry-run to preview deltas without writing.",
|
|
89
|
+
scope: "workspace",
|
|
90
|
+
mutatesState: true,
|
|
91
|
+
writes: ["docs/specs/live/*.md"],
|
|
92
|
+
reads: ["docs/rfcs/**/*.md", "docs/specs/live/*.md"],
|
|
93
|
+
flags: {
|
|
94
|
+
id: { kind: "string", required: true, description: "RFC id to merge (e.g. RFC-0711)." },
|
|
95
|
+
"dry-run": { kind: "boolean", description: "Preview deltas without writing files." },
|
|
96
|
+
},
|
|
97
|
+
execute: runSpecLiveMerge,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
registry.registerCommand({
|
|
101
|
+
name: "spec.live.list",
|
|
102
|
+
description:
|
|
103
|
+
"List all living feature specs in docs/specs/live/. " +
|
|
104
|
+
"Returns domain, title, lastMergedRfc, updatedAt, and historyCount for each spec.",
|
|
105
|
+
scope: "workspace",
|
|
106
|
+
reads: ["docs/specs/live/*.md"],
|
|
107
|
+
execute: runSpecLiveList,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
registry.registerCommand({
|
|
111
|
+
name: "spec.live.show",
|
|
112
|
+
description:
|
|
113
|
+
"Show a single living feature spec by domain. " +
|
|
114
|
+
"Requires --domain=<name>. Returns full frontmatter and body content.",
|
|
115
|
+
scope: "workspace",
|
|
116
|
+
reads: ["docs/specs/live/*.md"],
|
|
117
|
+
flags: {
|
|
118
|
+
domain: { kind: "string", required: true, description: "Domain name (filename without .md)." },
|
|
119
|
+
},
|
|
120
|
+
execute: runSpecLiveShow,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
registry.registerCommand({
|
|
124
|
+
name: "spec.live.validate",
|
|
125
|
+
description:
|
|
126
|
+
"Validate all living feature specs in docs/specs/live/. " +
|
|
127
|
+
"Checks V-LS-01 (frontmatter), V-LS-02 (domain/filename match), " +
|
|
128
|
+
"V-LS-03 (lastMergedRfc is archived), V-LS-04 (history entries are archived), " +
|
|
129
|
+
"V-LS-05 (no duplicate domains).",
|
|
130
|
+
scope: "workspace",
|
|
131
|
+
reads: ["docs/specs/live/*.md", "docs/rfcs/**/*.md"],
|
|
132
|
+
execute: runSpecLiveValidate,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|