@warpgogol/forge 5.3.4 → 6.2.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 +40 -10
- 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 +328 -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 +95 -15
- 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/core/handlers/file-size-lint.d.ts.map +1 -1
- package/dist/os/core/handlers/file-size-lint.js +2 -0
- package/dist/os/core/handlers/file-size-lint.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-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +11 -1
- package/dist/os/naming/naming-convention.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 +30 -8
- 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 +25 -6
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +12 -2
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +2 -2
- 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 +134 -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 +66 -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 +42 -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/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +2 -3
- package/dist/src/onboarding/doctor.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/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +2 -4
- package/dist/src/validators/skill-validate.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 +51 -10
- package/os/compass/handlers/compass-docs-validate.ts +361 -0
- package/os/compass/handlers/summary-record.ts +106 -15
- 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/compass-policy.test.ts +7 -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/core/handlers/file-size-lint.ts +2 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/naming/naming-convention.ts +11 -1
- 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 +37 -8
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
- package/os/rfc/rfc.module.ts +27 -6
- package/os/rfc/types.ts +18 -3
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +149 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +74 -38
- package/os/session/handlers/save.ts +37 -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/doctor.ts +2 -3
- 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/src/validators/skill-validate.ts +2 -4
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
</MODULE_CONTRACT>
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0396: initial spec.materialize handler — front computation, scaffold via rfc.create, materializedAs write-back.</item>
|
|
11
|
+
<item>RFC-1240: consumer-aware front — consumers ownership gate, foreign-dep satisfaction, foreignOwned result field, --consumer flag resolution.</item>
|
|
11
12
|
</CHANGE_SUMMARY>
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -29,6 +30,9 @@ import {
|
|
|
29
30
|
type ForgeSpec,
|
|
30
31
|
type SpecRfcNode,
|
|
31
32
|
type SpecAmendment,
|
|
33
|
+
ownsSpecNode,
|
|
34
|
+
isForeignOwnedNode,
|
|
35
|
+
isForeignConsumerDep,
|
|
32
36
|
resolveAmendedNode,
|
|
33
37
|
} from "./spec-schema.ts";
|
|
34
38
|
import { listRfcFiles } from "../rfc/frontmatter-io.ts";
|
|
@@ -55,6 +59,8 @@ interface SpecMaterializeResult {
|
|
|
55
59
|
created: MaterializedNode[];
|
|
56
60
|
skipped: SkippedNode[];
|
|
57
61
|
front: string[];
|
|
62
|
+
/** RFC-1240: nodes excluded from the front by consumer ownership. */
|
|
63
|
+
foreignOwned: string[];
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
// ---------------------------------------------------------------------------
|
|
@@ -94,6 +100,7 @@ export async function runSpecMaterialize(
|
|
|
94
100
|
created: [],
|
|
95
101
|
skipped: [],
|
|
96
102
|
front: [],
|
|
103
|
+
foreignOwned: [],
|
|
97
104
|
},
|
|
98
105
|
exitCode: 1,
|
|
99
106
|
summary: "spec.materialize: --spec=<id> is required",
|
|
@@ -101,12 +108,16 @@ export async function runSpecMaterialize(
|
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
let specsDir = "docs/specs";
|
|
111
|
+
let configConsumer: string | undefined;
|
|
104
112
|
try {
|
|
105
113
|
const config = loadForgeConfig(workspaceRoot);
|
|
106
114
|
specsDir = config.paths.specsDir;
|
|
115
|
+
configConsumer = config.project.consumer;
|
|
107
116
|
} catch {
|
|
108
117
|
// Use default
|
|
109
118
|
}
|
|
119
|
+
// RFC-1240: --consumer flag wins; otherwise forge.yaml project.consumer; else no identity.
|
|
120
|
+
const localConsumer = (input.flags["consumer"] as string | undefined) ?? configConsumer;
|
|
110
121
|
|
|
111
122
|
const specDir = path.join(workspaceRoot, specsDir, specId);
|
|
112
123
|
const forgeSpecPath = path.join(specDir, "forge-spec.yaml");
|
|
@@ -120,6 +131,7 @@ export async function runSpecMaterialize(
|
|
|
120
131
|
created: [],
|
|
121
132
|
skipped: [],
|
|
122
133
|
front: [],
|
|
134
|
+
foreignOwned: [],
|
|
123
135
|
},
|
|
124
136
|
exitCode: 1,
|
|
125
137
|
summary: `spec.materialize: forge-spec.yaml not found for spec '${specId}'`,
|
|
@@ -136,6 +148,7 @@ export async function runSpecMaterialize(
|
|
|
136
148
|
created: [],
|
|
137
149
|
skipped: [],
|
|
138
150
|
front: [],
|
|
151
|
+
foreignOwned: [],
|
|
139
152
|
},
|
|
140
153
|
exitCode: 1,
|
|
141
154
|
summary: `spec.materialize: forge-spec.yaml schema violation for spec '${specId}'`,
|
|
@@ -153,6 +166,7 @@ export async function runSpecMaterialize(
|
|
|
153
166
|
created: [],
|
|
154
167
|
skipped: [],
|
|
155
168
|
front: [],
|
|
169
|
+
foreignOwned: [],
|
|
156
170
|
},
|
|
157
171
|
exitCode: 1,
|
|
158
172
|
summary: `spec.materialize: spec '${specId}' is not accepted (status: ${spec.status}). Run /fo-spec-ingest to complete acceptance.`,
|
|
@@ -220,10 +234,27 @@ export async function runSpecMaterialize(
|
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
236
|
|
|
223
|
-
//
|
|
237
|
+
// RFC-1240: consumer ownership — a node with `consumers` is ours only when the
|
|
238
|
+
// resolved local consumer identity is a member. Without local identity every
|
|
239
|
+
// consumer-declared node is treated as not locally owned.
|
|
240
|
+
const nodeById = new Map(spec.rfcs.map((n) => [n.id, n]));
|
|
241
|
+
const ownsNode = (node: SpecRfcNode): boolean =>
|
|
242
|
+
ownsSpecNode(node, localConsumer);
|
|
243
|
+
const depSatisfied = (depId: string): boolean => {
|
|
244
|
+
if (nodeState.get(depId) === "implemented") return true;
|
|
245
|
+
// Foreign-consumer dependency: an implementation duty of another consumer.
|
|
246
|
+
return isForeignConsumerDep(nodeById.get(depId), localConsumer);
|
|
247
|
+
};
|
|
248
|
+
const foreignOwned = spec.rfcs
|
|
249
|
+
.filter((n) => isForeignOwnedNode(n, localConsumer))
|
|
250
|
+
.map((n) => n.id);
|
|
251
|
+
|
|
252
|
+
// Compute front: unmaterialized, locally-owned nodes whose deps are all
|
|
253
|
+
// implemented or foreign-owned (RFC-1240).
|
|
224
254
|
function isFront(node: SpecRfcNode): boolean {
|
|
225
255
|
if (node.materializedAs) return false;
|
|
226
|
-
|
|
256
|
+
if (!ownsNode(node)) return false;
|
|
257
|
+
return node.dependsOn.every(depSatisfied);
|
|
227
258
|
}
|
|
228
259
|
|
|
229
260
|
const frontNodes = spec.rfcs.filter(isFront);
|
|
@@ -241,8 +272,15 @@ export async function runSpecMaterialize(
|
|
|
241
272
|
if (!node) {
|
|
242
273
|
skipped.push({ node: nodeId, reason: "node not found in spec" });
|
|
243
274
|
} else if (!isFront(node)) {
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
if (!ownsSpecNode(node, localConsumer)) {
|
|
276
|
+
const reason = localConsumer == null
|
|
277
|
+
? "consumer identity not configured"
|
|
278
|
+
: `foreign-owned by ${(node.consumers ?? []).join(", ")}`;
|
|
279
|
+
skipped.push({ node: nodeId, reason });
|
|
280
|
+
} else {
|
|
281
|
+
const blockers = node.dependsOn.filter((d) => !depSatisfied(d));
|
|
282
|
+
skipped.push({ node: nodeId, reason: `blocked by ${blockers.join(", ")}` });
|
|
283
|
+
}
|
|
246
284
|
} else {
|
|
247
285
|
toMaterialize.push(node);
|
|
248
286
|
}
|
|
@@ -256,6 +294,7 @@ export async function runSpecMaterialize(
|
|
|
256
294
|
created: [],
|
|
257
295
|
skipped,
|
|
258
296
|
front: frontNodes.map((n) => n.id),
|
|
297
|
+
foreignOwned,
|
|
259
298
|
},
|
|
260
299
|
exitCode: 1,
|
|
261
300
|
summary: `spec.materialize: all requested nodes blocked or not found`,
|
|
@@ -293,6 +332,7 @@ export async function runSpecMaterialize(
|
|
|
293
332
|
created: [],
|
|
294
333
|
skipped: [],
|
|
295
334
|
front: frontNodes.map((n) => n.id),
|
|
335
|
+
foreignOwned,
|
|
296
336
|
},
|
|
297
337
|
exitCode: 1,
|
|
298
338
|
summary: `spec.materialize: RFC template not found at ${templatePath}`,
|
|
@@ -373,6 +413,9 @@ export async function runSpecMaterialize(
|
|
|
373
413
|
if (remainingFront.length > 0) {
|
|
374
414
|
logger.info(`Remaining front: ${remainingFront.join(", ")}`);
|
|
375
415
|
}
|
|
416
|
+
if (foreignOwned.length > 0) {
|
|
417
|
+
logger.info(`Foreign-owned nodes (excluded from front): ${foreignOwned.join(", ")}`);
|
|
418
|
+
}
|
|
376
419
|
}
|
|
377
420
|
|
|
378
421
|
return {
|
|
@@ -383,6 +426,7 @@ export async function runSpecMaterialize(
|
|
|
383
426
|
created,
|
|
384
427
|
skipped,
|
|
385
428
|
front: remainingFront,
|
|
429
|
+
foreignOwned,
|
|
386
430
|
},
|
|
387
431
|
exitCode: 0,
|
|
388
432
|
summary: `spec.materialize: created ${created.length} RFC(s) from spec '${specId}'`,
|
package/os/spec/spec-schema.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
</MODULE_CONTRACT>
|
|
9
9
|
<CHANGE_SUMMARY>
|
|
10
10
|
<item>RFC-0394: initial spec schema — forge/spec@1, integrity manifest, decision/rfc/wave types.</item>
|
|
11
|
+
<item>RFC-1240: consumers field on roadmap nodes (consumer ownership filtering); severity field on SpecViolation for warning diagnostics.</item>
|
|
11
12
|
</CHANGE_SUMMARY>
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -38,6 +39,8 @@ export const specRfcNodeSchema = z.object({
|
|
|
38
39
|
wave: z.number().int().min(1),
|
|
39
40
|
sources: z.array(z.string()).default([]),
|
|
40
41
|
materializedAs: z.string().optional(),
|
|
42
|
+
/** RFC-1240: consumer ownership — absent means the node materializes everywhere. */
|
|
43
|
+
consumers: z.array(z.string().min(1)).optional(),
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
export type SpecRfcNode = z.infer<typeof specRfcNodeSchema>;
|
|
@@ -77,6 +80,50 @@ export const forgeSpecSchema = z.object({
|
|
|
77
80
|
|
|
78
81
|
export type ForgeSpec = z.infer<typeof forgeSpecSchema>;
|
|
79
82
|
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Consumer ownership predicates (RFC-1240)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* True when the node may materialize for this workspace's consumer identity.
|
|
89
|
+
* A node without `consumers` is owned by every consumer.
|
|
90
|
+
*/
|
|
91
|
+
export function ownsSpecNode(
|
|
92
|
+
node: SpecRfcNode,
|
|
93
|
+
localConsumer: string | undefined,
|
|
94
|
+
): boolean {
|
|
95
|
+
return (
|
|
96
|
+
!node.consumers ||
|
|
97
|
+
(localConsumer != null && node.consumers.includes(localConsumer))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* True when the node declares `consumers` but is not locally owned —
|
|
103
|
+
* either the local identity is absent or it is not a member.
|
|
104
|
+
*/
|
|
105
|
+
export function isForeignOwnedNode(
|
|
106
|
+
node: SpecRfcNode,
|
|
107
|
+
localConsumer: string | undefined,
|
|
108
|
+
): boolean {
|
|
109
|
+
return node.consumers != null && !ownsSpecNode(node, localConsumer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* True when a dependency node's implementation duty belongs to other
|
|
114
|
+
* consumers — it satisfies dependents without a local implementation.
|
|
115
|
+
*/
|
|
116
|
+
export function isForeignConsumerDep(
|
|
117
|
+
depNode: SpecRfcNode | undefined,
|
|
118
|
+
localConsumer: string | undefined,
|
|
119
|
+
): boolean {
|
|
120
|
+
return (
|
|
121
|
+
depNode?.consumers != null &&
|
|
122
|
+
localConsumer != null &&
|
|
123
|
+
!depNode.consumers.includes(localConsumer)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
// ---------------------------------------------------------------------------
|
|
81
128
|
// Integrity manifest (forge/spec-integrity@1)
|
|
82
129
|
// ---------------------------------------------------------------------------
|
|
@@ -95,6 +142,8 @@ export type SpecIntegrity = z.infer<typeof specIntegritySchema>;
|
|
|
95
142
|
export interface SpecViolation {
|
|
96
143
|
rule: string;
|
|
97
144
|
message: string;
|
|
145
|
+
/** RFC-1240: warnings (e.g. missing consumer identity) do not fail validation. */
|
|
146
|
+
severity?: "error" | "warning";
|
|
98
147
|
}
|
|
99
148
|
|
|
100
149
|
export interface SpecValidateResult {
|
package/os/spec/spec-status.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
</MODULE_CONTRACT>
|
|
8
8
|
<CHANGE_SUMMARY>
|
|
9
9
|
<item>RFC-0396: initial spec.status handler — per-node states, blockers, front, progress.</item>
|
|
10
|
+
<item>RFC-1240: consumer-aware front — foreignOwned flag/list, foreign deps are not local blockers, --consumer flag resolution.</item>
|
|
10
11
|
</CHANGE_SUMMARY>
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -23,6 +24,8 @@ import type {
|
|
|
23
24
|
import {
|
|
24
25
|
forgeSpecSchema,
|
|
25
26
|
specAmendmentSchema,
|
|
27
|
+
isForeignConsumerDep,
|
|
28
|
+
isForeignOwnedNode,
|
|
26
29
|
type ForgeSpec,
|
|
27
30
|
type SpecAmendment,
|
|
28
31
|
} from "./spec-schema.ts";
|
|
@@ -37,6 +40,8 @@ interface SpecNodeStatus {
|
|
|
37
40
|
materializedAs?: string;
|
|
38
41
|
wave: number;
|
|
39
42
|
blockedBy: string[];
|
|
43
|
+
/** RFC-1240: true when the node is owned by other consumers (excluded from front). */
|
|
44
|
+
foreignOwned: boolean;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
interface SpecProgress {
|
|
@@ -49,6 +54,8 @@ interface SpecStatusEntry {
|
|
|
49
54
|
status: string;
|
|
50
55
|
nodes: SpecNodeStatus[];
|
|
51
56
|
front: string[];
|
|
57
|
+
/** RFC-1240: nodes excluded from the front by consumer ownership. */
|
|
58
|
+
foreignOwned: string[];
|
|
52
59
|
progress: SpecProgress;
|
|
53
60
|
amendments?: {
|
|
54
61
|
proposed: number;
|
|
@@ -138,6 +145,8 @@ function computeFront(
|
|
|
138
145
|
): string[] {
|
|
139
146
|
return nodes
|
|
140
147
|
.filter((n) => n.state === "unmaterialized")
|
|
148
|
+
// RFC-1240: foreign-owned nodes are never on the local front.
|
|
149
|
+
.filter((n) => !n.foreignOwned)
|
|
141
150
|
.filter((n) => n.blockedBy.every((dep) => {
|
|
142
151
|
const depNode = nodeMap.get(dep);
|
|
143
152
|
return depNode?.state === "implemented";
|
|
@@ -156,12 +165,16 @@ export async function runSpecStatus(
|
|
|
156
165
|
const { workspaceRoot, logger, outputFormat } = context;
|
|
157
166
|
|
|
158
167
|
let specsDir = "docs/specs";
|
|
168
|
+
let configConsumer: string | undefined;
|
|
159
169
|
try {
|
|
160
170
|
const config = loadForgeConfig(workspaceRoot);
|
|
161
171
|
specsDir = config.paths.specsDir;
|
|
172
|
+
configConsumer = config.project.consumer;
|
|
162
173
|
} catch {
|
|
163
174
|
// Use default
|
|
164
175
|
}
|
|
176
|
+
// RFC-1240: --consumer flag wins; otherwise forge.yaml project.consumer; else no identity.
|
|
177
|
+
const localConsumer = (input.flags["consumer"] as string | undefined) ?? configConsumer;
|
|
165
178
|
|
|
166
179
|
const specsRoot = path.join(workspaceRoot, specsDir);
|
|
167
180
|
const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
|
|
@@ -198,9 +211,15 @@ export async function runSpecStatus(
|
|
|
198
211
|
else state = "draft";
|
|
199
212
|
}
|
|
200
213
|
|
|
214
|
+
// RFC-1240: a node with `consumers` not containing the local consumer
|
|
215
|
+
// identity (or any `consumers` when identity is absent) is foreign-owned.
|
|
216
|
+
const foreignOwned = isForeignOwnedNode(node, localConsumer);
|
|
217
|
+
|
|
201
218
|
const blockedBy = node.dependsOn.filter((dep) => {
|
|
202
219
|
const depNode = spec.rfcs.find((n) => n.id === dep);
|
|
203
220
|
if (!depNode) return false;
|
|
221
|
+
// RFC-1240: a dependency owned by other consumers is not a local blocker.
|
|
222
|
+
if (isForeignConsumerDep(depNode, localConsumer)) return false;
|
|
204
223
|
if (!depNode.materializedAs) return true;
|
|
205
224
|
const depStatus = rfcStatuses.get(depNode.materializedAs);
|
|
206
225
|
return depStatus !== "implemented";
|
|
@@ -212,6 +231,7 @@ export async function runSpecStatus(
|
|
|
212
231
|
materializedAs: node.materializedAs,
|
|
213
232
|
wave: node.wave,
|
|
214
233
|
blockedBy,
|
|
234
|
+
foreignOwned,
|
|
215
235
|
};
|
|
216
236
|
nodes.push(nodeStatus);
|
|
217
237
|
nodeMap.set(node.id, nodeStatus);
|
|
@@ -240,6 +260,7 @@ export async function runSpecStatus(
|
|
|
240
260
|
status: spec.status,
|
|
241
261
|
nodes,
|
|
242
262
|
front,
|
|
263
|
+
foreignOwned: nodes.filter((n) => n.foreignOwned).map((n) => n.id),
|
|
243
264
|
progress,
|
|
244
265
|
amendments: {
|
|
245
266
|
proposed: amendments.filter((a) => a.status === "proposed").length,
|
|
@@ -261,6 +282,9 @@ export async function runSpecStatus(
|
|
|
261
282
|
`Progress: ${entry.progress.implemented}/${entry.progress.total} implemented`,
|
|
262
283
|
);
|
|
263
284
|
logger.info(`Front: ${entry.front.join(", ") || "(none)"}`);
|
|
285
|
+
if (entry.foreignOwned.length > 0) {
|
|
286
|
+
logger.info(`Foreign-owned: ${entry.foreignOwned.join(", ")}`);
|
|
287
|
+
}
|
|
264
288
|
for (const node of entry.nodes) {
|
|
265
289
|
const blocked = node.blockedBy.length > 0
|
|
266
290
|
? ` [blocked by ${node.blockedBy.join(", ")}]`
|
|
@@ -19,6 +19,72 @@ afterEach(async () => {
|
|
|
19
19
|
await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
describe("spec.validate consumer rules (RFC-1240)", () => {
|
|
23
|
+
async function writeConsumerSpec(workspaceRoot: string, consumersYaml: string): Promise<void> {
|
|
24
|
+
const specDir = path.join(workspaceRoot, "docs/specs/example");
|
|
25
|
+
await fs.mkdir(specDir, { recursive: true });
|
|
26
|
+
await fs.writeFile(path.join(specDir, "forge-spec.yaml"), `schema: forge/spec@1
|
|
27
|
+
id: example
|
|
28
|
+
title: Example
|
|
29
|
+
version: 1.0.0
|
|
30
|
+
status: accepted
|
|
31
|
+
reviewers: []
|
|
32
|
+
sourceNote: test fixture
|
|
33
|
+
vendoredAt: 2026-10-09
|
|
34
|
+
documents: {}
|
|
35
|
+
decisions: []
|
|
36
|
+
rfcs:
|
|
37
|
+
- id: EX-001
|
|
38
|
+
title: Consumer node
|
|
39
|
+
dependsOn: []
|
|
40
|
+
wave: 1
|
|
41
|
+
sources: []
|
|
42
|
+
${consumersYaml}waves:
|
|
43
|
+
- id: 1
|
|
44
|
+
name: Test
|
|
45
|
+
goal: Exercise consumer rules
|
|
46
|
+
`);
|
|
47
|
+
await fs.writeFile(path.join(specDir, "integrity.yaml"), "schema: forge/spec-integrity@1\nfiles: {}\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ctx(workspaceRoot: string): ForgeRuntimeContext {
|
|
51
|
+
return { workspaceRoot, dryRun: false, outputFormat: "json",
|
|
52
|
+
logger: { section() {}, info() {}, warn() {}, error() {}, success() {} } } as ForgeRuntimeContext;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it("emits SPEC-13 warning without failing when consumers exist but no local identity is configured", async () => {
|
|
56
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-consumer-"));
|
|
57
|
+
roots.push(workspaceRoot);
|
|
58
|
+
await writeConsumerSpec(workspaceRoot, " consumers:\n - personal-agent\n");
|
|
59
|
+
const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, ctx(workspaceRoot));
|
|
60
|
+
expect(result.exitCode).toBe(0);
|
|
61
|
+
expect(result.data.status).toBe("pass");
|
|
62
|
+
const warnings = result.data.specs[0]!.violations.filter((v) => v.severity === "warning");
|
|
63
|
+
expect(warnings.map((v) => v.rule)).toEqual(["SPEC-13"]);
|
|
64
|
+
expect(warnings[0]!.message).toContain("EX-001");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not emit SPEC-13 when project.consumer is configured", async () => {
|
|
68
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-consumer-"));
|
|
69
|
+
roots.push(workspaceRoot);
|
|
70
|
+
await fs.writeFile(path.join(workspaceRoot, "forge.yaml"),
|
|
71
|
+
"schema: forge/config@1\nproject:\n name: test-ws\n consumer: werkstatt\npaths: {}\n");
|
|
72
|
+
await writeConsumerSpec(workspaceRoot, " consumers:\n - personal-agent\n");
|
|
73
|
+
const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, ctx(workspaceRoot));
|
|
74
|
+
expect(result.exitCode).toBe(0);
|
|
75
|
+
expect(result.data.specs[0]!.violations).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("fails with SPEC-12 on duplicate consumer entries", async () => {
|
|
79
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-consumer-"));
|
|
80
|
+
roots.push(workspaceRoot);
|
|
81
|
+
await writeConsumerSpec(workspaceRoot, " consumers:\n - werkstatt\n - werkstatt\n");
|
|
82
|
+
const result = await runSpecValidate({ argv: [], flags: { spec: "example" } }, ctx(workspaceRoot));
|
|
83
|
+
expect(result.exitCode).toBe(1);
|
|
84
|
+
expect(result.data.specs[0]!.violations.map((v) => v.rule)).toContain("SPEC-12");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
22
88
|
describe("spec.validate SPEC-07", () => {
|
|
23
89
|
it("accepts a materialized RFC after docs.archive moves it below archive/implemented", async () => {
|
|
24
90
|
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "spec-validate-archive-"));
|
package/os/spec/spec-validate.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<CHANGE_SUMMARY>
|
|
11
11
|
<item>RFC-0394: initial spec.validate handler with SPEC-01..07 rules.</item>
|
|
12
12
|
<item>Gap fix: resolve materialized RFCs recursively so terminal RFC archival does not invalidate accepted specs.</item>
|
|
13
|
+
<item>RFC-1240: SPEC-12 consumers list hygiene, SPEC-13 consumer-identity warning; warnings no longer fail validation.</item>
|
|
13
14
|
</CHANGE_SUMMARY>
|
|
14
15
|
*/
|
|
15
16
|
|
|
@@ -280,6 +281,45 @@ async function checkMaterializedAs(
|
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// SPEC-12: consumers list hygiene / SPEC-13: consumer identity configured (RFC-1240)
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
function checkConsumers(
|
|
289
|
+
spec: ForgeSpec,
|
|
290
|
+
localConsumer: string | undefined,
|
|
291
|
+
violations: SpecViolation[],
|
|
292
|
+
): void {
|
|
293
|
+
const identityNodes: string[] = [];
|
|
294
|
+
for (const node of spec.rfcs) {
|
|
295
|
+
if (!node.consumers) continue;
|
|
296
|
+
if (node.consumers.length === 0) {
|
|
297
|
+
violations.push({
|
|
298
|
+
rule: "SPEC-12",
|
|
299
|
+
message: `node ${node.id} declares an empty consumers list`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const seen = new Set<string>();
|
|
303
|
+
for (const consumer of node.consumers) {
|
|
304
|
+
if (seen.has(consumer)) {
|
|
305
|
+
violations.push({
|
|
306
|
+
rule: "SPEC-12",
|
|
307
|
+
message: `node ${node.id} declares duplicate consumer '${consumer}'`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
seen.add(consumer);
|
|
311
|
+
}
|
|
312
|
+
if (localConsumer == null) identityNodes.push(node.id);
|
|
313
|
+
}
|
|
314
|
+
if (identityNodes.length > 0) {
|
|
315
|
+
violations.push({
|
|
316
|
+
rule: "SPEC-13",
|
|
317
|
+
severity: "warning",
|
|
318
|
+
message: `consumer identity not configured — nodes declaring consumers cannot be owned locally: ${identityNodes.join(", ")} (set project.consumer in forge.yaml or pass --consumer)`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
283
323
|
// ---------------------------------------------------------------------------
|
|
284
324
|
// SPEC-08..11: Amendment validation (RFC-0397)
|
|
285
325
|
// ---------------------------------------------------------------------------
|
|
@@ -401,12 +441,16 @@ export async function runSpecValidate(
|
|
|
401
441
|
|
|
402
442
|
// Resolve specs directory from forge.yaml
|
|
403
443
|
let specsDir = "docs/specs";
|
|
444
|
+
let configConsumer: string | undefined;
|
|
404
445
|
try {
|
|
405
446
|
const config = loadForgeConfig(workspaceRoot);
|
|
406
447
|
specsDir = config.paths.specsDir;
|
|
448
|
+
configConsumer = config.project.consumer;
|
|
407
449
|
} catch {
|
|
408
450
|
// Use default
|
|
409
451
|
}
|
|
452
|
+
// RFC-1240: --consumer flag wins; otherwise forge.yaml project.consumer; else no identity.
|
|
453
|
+
const localConsumer = (input.flags["consumer"] as string | undefined) ?? configConsumer;
|
|
410
454
|
|
|
411
455
|
const specsRoot = path.join(workspaceRoot, specsDir);
|
|
412
456
|
const filterSpec = input.flags["spec"] as string | undefined;
|
|
@@ -475,6 +519,9 @@ export async function runSpecValidate(
|
|
|
475
519
|
const amendments = await loadAmendments(specDir);
|
|
476
520
|
checkAmendments(spec, amendments, violations);
|
|
477
521
|
|
|
522
|
+
// SPEC-12/13: consumers hygiene + identity warning (RFC-1240)
|
|
523
|
+
checkConsumers(spec, localConsumer, violations);
|
|
524
|
+
|
|
478
525
|
results.push({
|
|
479
526
|
id: spec.id,
|
|
480
527
|
status: spec.status,
|
|
@@ -482,20 +529,27 @@ export async function runSpecValidate(
|
|
|
482
529
|
});
|
|
483
530
|
}
|
|
484
531
|
|
|
485
|
-
const hasFailures = results.some((r) =>
|
|
532
|
+
const hasFailures = results.some((r) =>
|
|
533
|
+
r.violations.some((v) => v.severity !== "warning"),
|
|
534
|
+
);
|
|
486
535
|
|
|
487
536
|
if (outputFormat === "pretty") {
|
|
488
537
|
if (results.length === 0) {
|
|
489
538
|
logger.info("No vendored specs found.");
|
|
490
539
|
} else {
|
|
491
540
|
for (const result of results) {
|
|
492
|
-
|
|
493
|
-
|
|
541
|
+
const errors = result.violations.filter((v) => v.severity !== "warning");
|
|
542
|
+
const warnings = result.violations.filter((v) => v.severity === "warning");
|
|
543
|
+
if (errors.length === 0) {
|
|
544
|
+
logger.success(`spec.validate: ${result.id} — pass${warnings.length > 0 ? ` (${warnings.length} warning${warnings.length === 1 ? "" : "s"})` : ""}`);
|
|
494
545
|
} else {
|
|
495
|
-
logger.error(`spec.validate: ${result.id} — ${
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
546
|
+
logger.error(`spec.validate: ${result.id} — ${errors.length} violation${errors.length === 1 ? "" : "s"}`);
|
|
547
|
+
}
|
|
548
|
+
for (const v of errors) {
|
|
549
|
+
logger.error(` ${v.rule}: ${v.message}`);
|
|
550
|
+
}
|
|
551
|
+
for (const v of warnings) {
|
|
552
|
+
logger.warn(` ${v.rule}: ${v.message}`);
|
|
499
553
|
}
|
|
500
554
|
}
|
|
501
555
|
}
|
|
@@ -509,7 +563,7 @@ export async function runSpecValidate(
|
|
|
509
563
|
},
|
|
510
564
|
exitCode: hasFailures ? 1 : 0,
|
|
511
565
|
summary: hasFailures
|
|
512
|
-
? `spec.validate: ${results.filter((r) => r.violations.
|
|
566
|
+
? `spec.validate: ${results.filter((r) => r.violations.some((v) => v.severity !== "warning")).length} spec(s) with violations`
|
|
513
567
|
: `spec.validate: all ${results.length} spec(s) pass`,
|
|
514
568
|
};
|
|
515
569
|
}
|