@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,390 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>spec.materialize handler — scaffolds RFC files for front nodes of a spec roadmap (RFC-0396).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not fill RFC content — the agent fills sections during the fo pipeline.</item>
|
|
6
|
+
<item>Do not bulk-materialize past the front or above the --next cap.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0396: initial spec.materialize handler — front computation, scaffold via rfc.create, materializedAs write-back.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
18
|
+
import { toKebabCase } from "../../src/utils/string-utils.ts";
|
|
19
|
+
import { loadForgeConfig } from "../../src/config/forge-config.ts";
|
|
20
|
+
import { resolveRfcTemplate } from "../rfc/handlers/shared.ts";
|
|
21
|
+
import type {
|
|
22
|
+
ForgeCommandInput,
|
|
23
|
+
ForgeCommandResult,
|
|
24
|
+
ForgeRuntimeContext,
|
|
25
|
+
} from "../../src/types.ts";
|
|
26
|
+
import {
|
|
27
|
+
forgeSpecSchema,
|
|
28
|
+
specAmendmentSchema,
|
|
29
|
+
type ForgeSpec,
|
|
30
|
+
type SpecRfcNode,
|
|
31
|
+
type SpecAmendment,
|
|
32
|
+
resolveAmendedNode,
|
|
33
|
+
} from "./spec-schema.ts";
|
|
34
|
+
import { listRfcFiles } from "../rfc/frontmatter-io.ts";
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Types
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
interface MaterializedNode {
|
|
41
|
+
node: string;
|
|
42
|
+
rfc: string;
|
|
43
|
+
file: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SkippedNode {
|
|
47
|
+
node: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SpecMaterializeResult {
|
|
52
|
+
command: "spec.materialize";
|
|
53
|
+
status: "pass" | "fail";
|
|
54
|
+
spec: string;
|
|
55
|
+
created: MaterializedNode[];
|
|
56
|
+
skipped: SkippedNode[];
|
|
57
|
+
front: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
async function readYaml<T>(filePath: string): Promise<T | null> {
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
67
|
+
return parseYaml(content) as T;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toIsoDate(d: Date): string {
|
|
74
|
+
return d.toISOString().slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Main handler
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export async function runSpecMaterialize(
|
|
82
|
+
input: ForgeCommandInput,
|
|
83
|
+
context: ForgeRuntimeContext,
|
|
84
|
+
): Promise<ForgeCommandResult<SpecMaterializeResult>> {
|
|
85
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
86
|
+
|
|
87
|
+
const specId = input.flags["spec"] as string | undefined;
|
|
88
|
+
if (!specId) {
|
|
89
|
+
return {
|
|
90
|
+
data: {
|
|
91
|
+
command: "spec.materialize",
|
|
92
|
+
status: "fail",
|
|
93
|
+
spec: "",
|
|
94
|
+
created: [],
|
|
95
|
+
skipped: [],
|
|
96
|
+
front: [],
|
|
97
|
+
},
|
|
98
|
+
exitCode: 1,
|
|
99
|
+
summary: "spec.materialize: --spec=<id> is required",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let specsDir = "docs/specs";
|
|
104
|
+
try {
|
|
105
|
+
const config = loadForgeConfig(workspaceRoot);
|
|
106
|
+
specsDir = config.paths.specsDir;
|
|
107
|
+
} catch {
|
|
108
|
+
// Use default
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const specDir = path.join(workspaceRoot, specsDir, specId);
|
|
112
|
+
const forgeSpecPath = path.join(specDir, "forge-spec.yaml");
|
|
113
|
+
const rawSpec = await readYaml<unknown>(forgeSpecPath);
|
|
114
|
+
if (!rawSpec) {
|
|
115
|
+
return {
|
|
116
|
+
data: {
|
|
117
|
+
command: "spec.materialize",
|
|
118
|
+
status: "fail",
|
|
119
|
+
spec: specId,
|
|
120
|
+
created: [],
|
|
121
|
+
skipped: [],
|
|
122
|
+
front: [],
|
|
123
|
+
},
|
|
124
|
+
exitCode: 1,
|
|
125
|
+
summary: `spec.materialize: forge-spec.yaml not found for spec '${specId}'`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const parsed = forgeSpecSchema.safeParse(rawSpec);
|
|
130
|
+
if (!parsed.success) {
|
|
131
|
+
return {
|
|
132
|
+
data: {
|
|
133
|
+
command: "spec.materialize",
|
|
134
|
+
status: "fail",
|
|
135
|
+
spec: specId,
|
|
136
|
+
created: [],
|
|
137
|
+
skipped: [],
|
|
138
|
+
front: [],
|
|
139
|
+
},
|
|
140
|
+
exitCode: 1,
|
|
141
|
+
summary: `spec.materialize: forge-spec.yaml schema violation for spec '${specId}'`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const spec: ForgeSpec = parsed.data;
|
|
146
|
+
|
|
147
|
+
if (spec.status !== "accepted") {
|
|
148
|
+
return {
|
|
149
|
+
data: {
|
|
150
|
+
command: "spec.materialize",
|
|
151
|
+
status: "fail",
|
|
152
|
+
spec: specId,
|
|
153
|
+
created: [],
|
|
154
|
+
skipped: [],
|
|
155
|
+
front: [],
|
|
156
|
+
},
|
|
157
|
+
exitCode: 1,
|
|
158
|
+
summary: `spec.materialize: spec '${specId}' is not accepted (status: ${spec.status}). Run /fo-spec-ingest to complete acceptance.`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Load amendments (RFC-0397)
|
|
163
|
+
const amendmentsDir = path.join(specDir, "amendments");
|
|
164
|
+
const amendments: SpecAmendment[] = [];
|
|
165
|
+
if (existsSync(amendmentsDir)) {
|
|
166
|
+
const amdEntries = await fs.readdir(amendmentsDir);
|
|
167
|
+
for (const entry of amdEntries) {
|
|
168
|
+
if (!entry.endsWith(".md")) continue;
|
|
169
|
+
try {
|
|
170
|
+
const content = await fs.readFile(path.join(amendmentsDir, entry), "utf8");
|
|
171
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
172
|
+
if (!fmMatch) continue;
|
|
173
|
+
const raw = parseYaml(fmMatch[1]) as unknown;
|
|
174
|
+
const amdParsed = specAmendmentSchema.safeParse(raw);
|
|
175
|
+
if (amdParsed.success) {
|
|
176
|
+
amendments.push(amdParsed.data);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// skip
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const proposedAmendmentNodes = new Set<string>();
|
|
185
|
+
for (const amendment of amendments) {
|
|
186
|
+
if (amendment.status === "proposed") {
|
|
187
|
+
for (const target of amendment.targets) {
|
|
188
|
+
if (target.kind === "node") proposedAmendmentNodes.add(target.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
|
|
194
|
+
const rfcFiles = await listRfcFiles(rfcDir);
|
|
195
|
+
|
|
196
|
+
// Load RFC statuses
|
|
197
|
+
const rfcStatuses = new Map<string, string>();
|
|
198
|
+
for (const file of rfcFiles) {
|
|
199
|
+
try {
|
|
200
|
+
const content = await fs.readFile(path.join(rfcDir, file), "utf8");
|
|
201
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
202
|
+
if (!fmMatch) continue;
|
|
203
|
+
const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
|
|
204
|
+
const id = String(fm["id"] ?? "");
|
|
205
|
+
const status = String(fm["status"] ?? "");
|
|
206
|
+
if (id && status) rfcStatuses.set(id, status);
|
|
207
|
+
} catch {
|
|
208
|
+
// skip
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Compute node states
|
|
213
|
+
const nodeState = new Map<string, "unmaterialized" | "implemented">();
|
|
214
|
+
for (const node of spec.rfcs) {
|
|
215
|
+
if (node.materializedAs) {
|
|
216
|
+
const status = rfcStatuses.get(node.materializedAs);
|
|
217
|
+
nodeState.set(node.id, status === "implemented" ? "implemented" : "unmaterialized");
|
|
218
|
+
} else {
|
|
219
|
+
nodeState.set(node.id, "unmaterialized");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Compute front: unmaterialized nodes whose deps are all implemented
|
|
224
|
+
function isFront(node: SpecRfcNode): boolean {
|
|
225
|
+
if (node.materializedAs) return false;
|
|
226
|
+
return node.dependsOn.every((dep) => nodeState.get(dep) === "implemented");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const frontNodes = spec.rfcs.filter(isFront);
|
|
230
|
+
|
|
231
|
+
// Determine which nodes to materialize
|
|
232
|
+
const explicitNodes = input.flags["nodes"] as string | undefined;
|
|
233
|
+
let toMaterialize: SpecRfcNode[];
|
|
234
|
+
|
|
235
|
+
if (explicitNodes) {
|
|
236
|
+
const requested = explicitNodes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
237
|
+
toMaterialize = [];
|
|
238
|
+
const skipped: SkippedNode[] = [];
|
|
239
|
+
for (const nodeId of requested) {
|
|
240
|
+
const node = spec.rfcs.find((n) => n.id === nodeId);
|
|
241
|
+
if (!node) {
|
|
242
|
+
skipped.push({ node: nodeId, reason: "node not found in spec" });
|
|
243
|
+
} else if (!isFront(node)) {
|
|
244
|
+
const blockers = node.dependsOn.filter((d) => nodeState.get(d) !== "implemented");
|
|
245
|
+
skipped.push({ node: nodeId, reason: `blocked by ${blockers.join(", ")}` });
|
|
246
|
+
} else {
|
|
247
|
+
toMaterialize.push(node);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (skipped.length > 0 && toMaterialize.length === 0) {
|
|
251
|
+
return {
|
|
252
|
+
data: {
|
|
253
|
+
command: "spec.materialize",
|
|
254
|
+
status: "fail",
|
|
255
|
+
spec: specId,
|
|
256
|
+
created: [],
|
|
257
|
+
skipped,
|
|
258
|
+
front: frontNodes.map((n) => n.id),
|
|
259
|
+
},
|
|
260
|
+
exitCode: 1,
|
|
261
|
+
summary: `spec.materialize: all requested nodes blocked or not found`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const nextStr = input.flags["next"] as string | undefined;
|
|
266
|
+
let next = nextStr ? parseInt(nextStr, 10) : 8;
|
|
267
|
+
if (isNaN(next) || next < 1) next = 8;
|
|
268
|
+
if (next > 12) next = 12;
|
|
269
|
+
toMaterialize = frontNodes.slice(0, next);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Find max RFC number
|
|
273
|
+
let maxId = 0;
|
|
274
|
+
for (const file of rfcFiles) {
|
|
275
|
+
const match = path.basename(file).match(/^rfc-(\d{4})/);
|
|
276
|
+
if (match) {
|
|
277
|
+
const num = parseInt(match[1]!, 10);
|
|
278
|
+
if (num > maxId) maxId = num;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Load template via resolveRfcTemplate (project-level first, then forge package fallback)
|
|
283
|
+
const templatePath = resolveRfcTemplate(workspaceRoot);
|
|
284
|
+
let templateContent: string;
|
|
285
|
+
try {
|
|
286
|
+
templateContent = await fs.readFile(templatePath, "utf-8");
|
|
287
|
+
} catch {
|
|
288
|
+
return {
|
|
289
|
+
data: {
|
|
290
|
+
command: "spec.materialize",
|
|
291
|
+
status: "fail",
|
|
292
|
+
spec: specId,
|
|
293
|
+
created: [],
|
|
294
|
+
skipped: [],
|
|
295
|
+
front: frontNodes.map((n) => n.id),
|
|
296
|
+
},
|
|
297
|
+
exitCode: 1,
|
|
298
|
+
summary: `spec.materialize: RFC template not found at ${templatePath}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const today = toIsoDate(new Date());
|
|
303
|
+
const created: MaterializedNode[] = [];
|
|
304
|
+
const skipped: SkippedNode[] = [];
|
|
305
|
+
|
|
306
|
+
for (const node of toMaterialize) {
|
|
307
|
+
// Use amended node if amendments exist (RFC-0397)
|
|
308
|
+
const effectiveNode = resolveAmendedNode(spec, node.id, amendments);
|
|
309
|
+
const nextNum = maxId + 1;
|
|
310
|
+
const paddedNum = String(nextNum).padStart(4, "0");
|
|
311
|
+
const nextRfcId = `RFC-${paddedNum}`;
|
|
312
|
+
const kebabTitle = toKebabCase(effectiveNode.title);
|
|
313
|
+
const fileName = `rfc-${paddedNum}-${kebabTitle}.md`;
|
|
314
|
+
|
|
315
|
+
// Warn if node has a proposed amendment (RFC-0397)
|
|
316
|
+
if (proposedAmendmentNodes.has(node.id)) {
|
|
317
|
+
logger.warn(`Warning: node ${node.id} has a proposed amendment — node knowledge may be about to change`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let content = templateContent;
|
|
321
|
+
content = content.replace(/^id: RFC-0000$/m, `id: ${nextRfcId}`);
|
|
322
|
+
content = content.replace(/^title: ".*"$/m, `title: "${effectiveNode.title}"`);
|
|
323
|
+
content = content.replace(/^status: draft$/m, `status: draft`);
|
|
324
|
+
content = content.replace(/^kind: \w+$/m, `kind: contract`);
|
|
325
|
+
content = content.replace(/^createdAt: YYYY-MM-DD$/m, `createdAt: ${today}`);
|
|
326
|
+
content = content.replace(/^updatedAt: YYYY-MM-DD$/m, `updatedAt: ${today}`);
|
|
327
|
+
content = content.replace(/^# specRef:$/m, `specRef: "${specId}/${node.id}"`);
|
|
328
|
+
content = content.replace(/^# RFC-0000: .+$/m, `# ${nextRfcId}: ${effectiveNode.title}`);
|
|
329
|
+
|
|
330
|
+
// Add normative source references in the body
|
|
331
|
+
const sourceRefs = effectiveNode.sources.length > 0
|
|
332
|
+
? effectiveNode.sources.map((s) => `- ${s}`).join("\n")
|
|
333
|
+
: "(none)";
|
|
334
|
+
const designSection = `\n## Design\n\n**Normative source references:**\n${sourceRefs}\n\n_Fill in the implementation design for this repository. Reference vendored snapshot sections — do not copy model content._\n`;
|
|
335
|
+
content = content.replace(/^---\n\n# /m, `---\n${designSection}\n# `);
|
|
336
|
+
|
|
337
|
+
const targetPath = path.join(rfcDir, fileName);
|
|
338
|
+
await fs.writeFile(targetPath, content, "utf-8");
|
|
339
|
+
maxId = nextNum;
|
|
340
|
+
|
|
341
|
+
created.push({
|
|
342
|
+
node: node.id,
|
|
343
|
+
rfc: nextRfcId,
|
|
344
|
+
file: path.join("docs", "rfcs", fileName),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Write back materializedAs into forge-spec.yaml
|
|
349
|
+
if (created.length > 0) {
|
|
350
|
+
const updatedSpec: ForgeSpec = {
|
|
351
|
+
...spec,
|
|
352
|
+
rfcs: spec.rfcs.map((node) => {
|
|
353
|
+
const created_entry = created.find((c) => c.node === node.id);
|
|
354
|
+
return created_entry ? { ...node, materializedAs: created_entry.rfc } : node;
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
const yamlContent = stringifyYaml(updatedSpec);
|
|
358
|
+
await fs.writeFile(forgeSpecPath, yamlContent, "utf-8");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Report skipped front nodes (beyond the materialized batch)
|
|
362
|
+
const remainingFront = frontNodes
|
|
363
|
+
.filter((n) => !created.some((c) => c.node === n.id))
|
|
364
|
+
.map((n) => n.id);
|
|
365
|
+
|
|
366
|
+
if (outputFormat === "pretty") {
|
|
367
|
+
for (const c of created) {
|
|
368
|
+
logger.success(`Materialized ${c.node} → ${c.rfc} (${c.file})`);
|
|
369
|
+
}
|
|
370
|
+
for (const s of skipped) {
|
|
371
|
+
logger.warn(`Skipped ${s.node}: ${s.reason}`);
|
|
372
|
+
}
|
|
373
|
+
if (remainingFront.length > 0) {
|
|
374
|
+
logger.info(`Remaining front: ${remainingFront.join(", ")}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
data: {
|
|
380
|
+
command: "spec.materialize",
|
|
381
|
+
status: "pass",
|
|
382
|
+
spec: specId,
|
|
383
|
+
created,
|
|
384
|
+
skipped,
|
|
385
|
+
front: remainingFront,
|
|
386
|
+
},
|
|
387
|
+
exitCode: 0,
|
|
388
|
+
summary: `spec.materialize: created ${created.length} RFC(s) from spec '${specId}'`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Zod schemas and TypeScript types for the forge spec vendoring contract (RFC-0394).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not implement validation logic — that lives in spec-validate.ts.</item>
|
|
6
|
+
<item>Do not implement materialization or status logic — that is RFC-0396.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0394: initial spec schema — forge/spec@1, integrity manifest, decision/rfc/wave types.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Spec decision
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const specDecisionSchema = z.object({
|
|
21
|
+
id: z.string().min(1),
|
|
22
|
+
title: z.string().min(1),
|
|
23
|
+
status: z.enum(["accepted", "amended", "rejected"]),
|
|
24
|
+
rationale: z.string().min(1),
|
|
25
|
+
promotedTo: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type SpecDecision = z.infer<typeof specDecisionSchema>;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Spec RFC node
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export const specRfcNodeSchema = z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
title: z.string().min(1),
|
|
37
|
+
dependsOn: z.array(z.string()).default([]),
|
|
38
|
+
wave: z.number().int().min(1),
|
|
39
|
+
sources: z.array(z.string()).default([]),
|
|
40
|
+
materializedAs: z.string().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type SpecRfcNode = z.infer<typeof specRfcNodeSchema>;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Spec wave
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export const specWaveSchema = z.object({
|
|
50
|
+
id: z.number().int().min(1),
|
|
51
|
+
name: z.string().min(1),
|
|
52
|
+
goal: z.string().min(1),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type SpecWave = z.infer<typeof specWaveSchema>;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Forge spec (forge/spec@1)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export const forgeSpecSchema = z.object({
|
|
62
|
+
schema: z.literal("forge/spec@1"),
|
|
63
|
+
id: z.string().min(1),
|
|
64
|
+
title: z.string().min(1),
|
|
65
|
+
version: z.string().min(1),
|
|
66
|
+
status: z.enum(["vendored", "accepted", "superseded"]),
|
|
67
|
+
reviewers: z.array(z.string()).default([]),
|
|
68
|
+
sourceNote: z.string().min(1),
|
|
69
|
+
vendoredAt: z.string().min(1),
|
|
70
|
+
documents: z.record(z.string(), z.string()).default({}),
|
|
71
|
+
decisions: z.array(specDecisionSchema).default([]),
|
|
72
|
+
rfcs: z.array(specRfcNodeSchema).default([]),
|
|
73
|
+
waves: z.array(specWaveSchema).default([]),
|
|
74
|
+
/** Set when this spec supersedes an older version (RFC-0397). */
|
|
75
|
+
supersedes: z.string().optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type ForgeSpec = z.infer<typeof forgeSpecSchema>;
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Integrity manifest (forge/spec-integrity@1)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export const specIntegritySchema = z.object({
|
|
85
|
+
schema: z.literal("forge/spec-integrity@1"),
|
|
86
|
+
files: z.record(z.string(), z.string()).default({}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type SpecIntegrity = z.infer<typeof specIntegritySchema>;
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Validation result types
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface SpecViolation {
|
|
96
|
+
rule: string;
|
|
97
|
+
message: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SpecValidateResult {
|
|
101
|
+
command: "spec.validate";
|
|
102
|
+
status: "pass" | "fail";
|
|
103
|
+
specs: Array<{
|
|
104
|
+
id: string;
|
|
105
|
+
status: string;
|
|
106
|
+
violations: SpecViolation[];
|
|
107
|
+
}>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Spec amendment (RFC-0397)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export const specAmendmentTargetSchema = z.union([
|
|
115
|
+
z.object({
|
|
116
|
+
kind: z.literal("section"),
|
|
117
|
+
document: z.string().min(1),
|
|
118
|
+
anchor: z.string().min(1),
|
|
119
|
+
}),
|
|
120
|
+
z.object({
|
|
121
|
+
kind: z.literal("decision"),
|
|
122
|
+
id: z.string().min(1),
|
|
123
|
+
}),
|
|
124
|
+
z.object({
|
|
125
|
+
kind: z.literal("node"),
|
|
126
|
+
id: z.string().min(1),
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
export type SpecAmendmentTarget = z.infer<typeof specAmendmentTargetSchema>;
|
|
131
|
+
|
|
132
|
+
export const specAmendmentSchema = z.object({
|
|
133
|
+
schema: z.literal("forge/spec-amendment@1"),
|
|
134
|
+
id: z.string().min(1),
|
|
135
|
+
title: z.string().min(1),
|
|
136
|
+
status: z.enum(["proposed", "accepted", "rejected"]),
|
|
137
|
+
createdAt: z.string().min(1),
|
|
138
|
+
reviewers: z.array(z.string()).default([]),
|
|
139
|
+
targets: z.array(specAmendmentTargetSchema).min(1),
|
|
140
|
+
discoveredBy: z.string().min(1),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export type SpecAmendment = z.infer<typeof specAmendmentSchema>;
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// resolveAmendedNode — apply accepted amendments to a roadmap node
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export function resolveAmendedNode(
|
|
150
|
+
spec: ForgeSpec,
|
|
151
|
+
nodeId: string,
|
|
152
|
+
amendments: SpecAmendment[],
|
|
153
|
+
): SpecRfcNode {
|
|
154
|
+
const node = spec.rfcs.find((n) => n.id === nodeId);
|
|
155
|
+
if (!node) throw new Error(`Node not found: ${nodeId}`);
|
|
156
|
+
|
|
157
|
+
const acceptedAmendments = amendments.filter(
|
|
158
|
+
(a) => a.status === "accepted" && a.targets.some((t) => t.kind === "node" && t.id === nodeId),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (acceptedAmendments.length === 0) return node;
|
|
162
|
+
|
|
163
|
+
// Apply amendments — later amendments override earlier ones
|
|
164
|
+
let resolved = { ...node };
|
|
165
|
+
for (const amendment of acceptedAmendments) {
|
|
166
|
+
// Amendments compose at read time; the node's title gets an annotation
|
|
167
|
+
resolved = {
|
|
168
|
+
...resolved,
|
|
169
|
+
title: `${resolved.title} (as amended by ${amendment.id})`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return resolved;
|
|
174
|
+
}
|