@warpgogol/forge 2.21.5 → 2.21.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +27 -1
  2. package/README.md +93 -0
  3. package/README.uk.md +93 -0
  4. package/os/adr/adr-0000-template.md +8 -0
  5. package/os/adr/handlers/validate.test.ts +203 -0
  6. package/os/adr/handlers/validate.ts +54 -1
  7. package/os/adr/types.ts +7 -0
  8. package/os/compass/handlers/compass-inventory-handler.ts +11 -1
  9. package/os/compass/handlers/compass-inventory.ts +10 -0
  10. package/os/core/handlers/validate.ts +56 -5
  11. package/os/naming/naming-convention.ts +9 -0
  12. package/os/plugin/plugin.module.ts +1 -1
  13. package/os/rfc/acceptance.ts +133 -4
  14. package/os/rfc/handlers/implement-stamp.ts +14 -1
  15. package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
  16. package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
  17. package/os/rfc/handlers/validate-rules.ts +450 -9
  18. package/os/rfc/handlers/validate.ts +20 -2
  19. package/os/rfc/rfc-0000-template.md +24 -8
  20. package/os/rfc/rfc.module.ts +28 -0
  21. package/os/rfc/types.ts +72 -6
  22. package/os/rfc/verification-evidence.ts +5 -4
  23. package/os/rfc/verification-refresh.test.ts +320 -0
  24. package/os/rfc/verification-refresh.ts +216 -0
  25. package/os/session/handlers/save.ts +10 -0
  26. package/os/spec/spec-validate.test.ts +59 -0
  27. package/os/spec/spec-validate.ts +6 -4
  28. package/package.json +2 -1
  29. package/skills/fo/fo-handoff/SKILL.md +15 -6
  30. package/skills/fo/fo-idea-audit/SKILL.md +1 -1
  31. package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
  32. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
  33. package/skills/fo/fo-idea-implement/SKILL.md +3 -2
  34. package/src/compass/contract-registry.ts +25 -6
  35. package/src/index.ts +1 -1
  36. package/src/onboarding/doctor.ts +1 -1
  37. package/src/registry.ts +1 -1
  38. package/src/tests/acceptance-probe-kinds.test.ts +262 -0
  39. package/src/tests/plugin-manifest.test.ts +1 -1
  40. package/src/tests/session-handlers.test.ts +29 -0
  41. package/src/types/werkstatt-engine-shims.d.ts +0 -21
  42. package/src/types/werkstatt-shared-shims.d.ts +68 -147
  43. /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
@@ -3,12 +3,13 @@
3
3
  <purpose>forge.validate — execute validate commands for all artifacts declared in the active stack profile. Supports --dry-run, --json, --artifact filtering, and violation parsing.</purpose>
4
4
  <non-goals>
5
5
  <item>Do not implement dev or build logic — those are separate handlers.</item>
6
- <item>Do not import from @warpgogol/* — this module is portable.</item>
6
+ <item>Do not import from @warpgogol/* — compass handler is intra-package within @warpgogol/forge.</item>
7
7
  </non-goals>
8
8
  </MODULE_CONTRACT>
9
9
  <CHANGE_SUMMARY>
10
10
  <item>RFC-0674: initial forge.validate handler with profile resolution, --dry-run, and per-artifact execution.</item>
11
11
  <item>RFC-0677: extended with --artifact filtering, violation parsing (outputFormat: json/plain), passed/allPassed fields.</item>
12
+ <item>Integrated compass.validate as in-process step after artifact validations for automatic Compass enforcement.</item>
12
13
  </CHANGE_SUMMARY>
13
14
  */
14
15
 
@@ -20,6 +21,7 @@ import type {
20
21
  ForgeRuntimeContext,
21
22
  } from "../../../src/types.ts";
22
23
  import { resolveActiveProfile, resolveLifecycleFlags } from "./profile-resolve.ts";
24
+ import { runCompassValidation } from "../../compass/handlers/compass-inventory-handler.ts";
23
25
 
24
26
  const execAsync = promisify(exec);
25
27
 
@@ -41,11 +43,25 @@ export interface ForgeValidateArtifactResult {
41
43
  stderr: string;
42
44
  }
43
45
 
46
+ export interface ForgeValidateCompassResult {
47
+ passed: boolean;
48
+ checkedFiles: number;
49
+ failures: number;
50
+ diagnostics: Array<{
51
+ ruleId: string;
52
+ severity: string;
53
+ file: string;
54
+ message: string;
55
+ fix: string;
56
+ }>;
57
+ }
58
+
44
59
  export interface ForgeValidateResult {
45
60
  command: "forge.validate";
46
61
  profileId: string;
47
62
  artifacts: ForgeValidateArtifactResult[];
48
63
  allPassed: boolean;
64
+ compass?: ForgeValidateCompassResult;
49
65
  }
50
66
 
51
67
  export function parseViolations(
@@ -185,6 +201,12 @@ export async function runValidate(
185
201
  stderr: "",
186
202
  })),
187
203
  allPassed: true,
204
+ compass: {
205
+ passed: true,
206
+ checkedFiles: 0,
207
+ failures: 0,
208
+ diagnostics: [],
209
+ },
188
210
  },
189
211
  summary: `[dry-run] ${commands.length} validate command(s) resolved`,
190
212
  };
@@ -245,7 +267,37 @@ export async function runValidate(
245
267
  }
246
268
  }
247
269
 
248
- const allPassed = results.every((r) => r.passed);
270
+ const artifactsAllPassed = results.every((r) => r.passed);
271
+
272
+ let compassResult: ForgeValidateCompassResult | undefined;
273
+ try {
274
+ const compassResponse = await runCompassValidation({ argv: [], flags: {} }, context);
275
+ if (compassResponse.data) {
276
+ compassResult = {
277
+ passed: (compassResponse.exitCode ?? 0) === 0,
278
+ checkedFiles: compassResponse.data.checkedFiles,
279
+ failures: compassResponse.data.failures,
280
+ diagnostics: compassResponse.data.diagnostics,
281
+ };
282
+ }
283
+ } catch {
284
+ // compass.validate not available in this workspace — skip
285
+ }
286
+
287
+ const allPassed = artifactsAllPassed && (compassResult?.passed ?? true);
288
+
289
+ const summaryParts: string[] = [
290
+ allPassed
291
+ ? `forge.validate: all ${results.length} artifact(s) passed`
292
+ : `forge.validate: ${results.filter((r) => !r.passed).length} artifact(s) failed`,
293
+ ];
294
+ if (compassResult) {
295
+ summaryParts.push(
296
+ compassResult.passed
297
+ ? `compass: OK (${compassResult.checkedFiles} files)`
298
+ : `compass: ${compassResult.failures} failure(s)`,
299
+ );
300
+ }
249
301
 
250
302
  return {
251
303
  data: {
@@ -253,10 +305,9 @@ export async function runValidate(
253
305
  profileId: profile.id,
254
306
  artifacts: results,
255
307
  allPassed,
308
+ compass: compassResult,
256
309
  },
257
310
  exitCode: allPassed ? 0 : 1,
258
- summary: allPassed
259
- ? `forge.validate: all ${results.length} artifact(s) passed`
260
- : `forge.validate: ${results.filter((r) => !r.passed).length} artifact(s) failed`,
311
+ summary: summaryParts.join("; "),
261
312
  };
262
313
  }
@@ -63,6 +63,12 @@ const NAMING_CONVENTION_IGNORED_TOP_LEVEL = new Set([
63
63
  ".windsurf",
64
64
  ".cache",
65
65
  "tmp",
66
+ ".adr-locks",
67
+ ".rfc-locks",
68
+ ".devin",
69
+ ".forge",
70
+ "patches",
71
+ "storage",
66
72
  ]);
67
73
 
68
74
  // Tool-mandated filenames exempt from kebab-case (Docker, Caddy use these exact names).
@@ -93,6 +99,9 @@ const NAMING_CONVENTION_EXEMPT_KEYWORDS = ["config", "module"] as const;
93
99
  // Set of directory paths that are exempt from naming convention (generated files)
94
100
  const NAMING_CONVENTION_EXEMPT_DIRS = new Set([
95
101
  "components/icons/gen", // Generated icon components — naming controlled by generator
102
+ "docs/performance", // Tool-generated screenshots and Lighthouse reports with timestamps
103
+ "docs/specs", // Imported specification documents with their own naming convention
104
+ "docs/rfcs/archive", // Archived RFCs — historical artifacts, cannot be renamed
96
105
  ]);
97
106
 
98
107
  /**
@@ -21,7 +21,7 @@ import type {
21
21
  ForgeRuntimeContext,
22
22
  } from "../../src/types.ts";
23
23
  import { loadForgeConfig } from "../../src/config/forge-config.ts";
24
- import { forgePluginManifestSchema } from "../../src/plugin/ForgePluginManifest.ts";
24
+ import { forgePluginManifestSchema } from "../../src/plugin/forge-plugin-manifest.ts";
25
25
 
26
26
  export interface PluginValidateResult {
27
27
  command: "forge.plugin.validate";
@@ -14,6 +14,7 @@ prose checklist.
14
14
  </MODULE_CONTRACT>
15
15
  <CHANGE_SUMMARY>
16
16
  <item>RFC-0268: initial implementation.</item>
17
+ <item>RFC-0998: added test and json-schema probe kinds — spawnVitest helper, Ajv validation, shape validation.</item>
17
18
  </CHANGE_SUMMARY>
18
19
  */
19
20
 
@@ -21,6 +22,7 @@ import { spawn } from "node:child_process";
21
22
  import { stat, readFile } from "node:fs/promises";
22
23
  import path from "node:path";
23
24
  import { parse as yamlParse } from "yaml";
25
+ import { Ajv } from "ajv";
24
26
  import type {
25
27
  Diagnostic,
26
28
  ForgeCommandInput,
@@ -28,9 +30,15 @@ import type {
28
30
  ForgeRuntimeContext,
29
31
  CommandRegistry,
30
32
  } from "../../src/types.ts";
31
- import type { AcceptanceProbe, ProbeResult, RfcAcceptanceRunResult } from "./types.ts";
32
- import { RFC_DIR } from "./types.ts";
33
+ import type {
34
+ AcceptanceProbe,
35
+ ProbeResult,
36
+ RfcAcceptanceRunResult,
37
+ ProbeCoverageReport,
38
+ } from "./types.ts";
39
+ import { RFC_DIR, RFC_PROBE_BINDING_CUTOFF } from "./types.ts";
33
40
  import { listRfcFiles, readAndParseRfc } from "./frontmatter-io.ts";
41
+ import { evaluateAcceptanceCriteria, computeProbeCoverage } from "./handlers/validate-rules.ts";
34
42
 
35
43
  const RUN_PROBE_ALLOWED_PREFIX = "werkstatt ";
36
44
 
@@ -138,10 +146,40 @@ export function validateAcceptanceShape(value: unknown): AcceptanceShapeIssue[]
138
146
  }
139
147
  break;
140
148
  }
149
+ case "test": {
150
+ const file = (entry as Record<string, unknown>)["file"];
151
+ if (typeof file !== "string") {
152
+ issues.push({ index, message: 'probe "test" requires a string "file"' });
153
+ }
154
+ const testName = (entry as Record<string, unknown>)["testName"];
155
+ if (testName !== undefined && typeof testName !== "string") {
156
+ issues.push({ index, message: 'probe "test" testName must be a string if present' });
157
+ }
158
+ const expect = (entry as Record<string, unknown>)["expect"];
159
+ if (
160
+ !expect ||
161
+ typeof expect !== "object" ||
162
+ typeof (expect as Record<string, unknown>)["exitCode"] !== "number"
163
+ ) {
164
+ issues.push({ index, message: 'probe "test" requires expect: { exitCode: <number> }' });
165
+ }
166
+ break;
167
+ }
168
+ case "json-schema": {
169
+ const artifact = (entry as Record<string, unknown>)["artifact"];
170
+ if (typeof artifact !== "string") {
171
+ issues.push({ index, message: 'probe "json-schema" requires a string "artifact"' });
172
+ }
173
+ const schemaInline = (entry as Record<string, unknown>)["schemaInline"];
174
+ if (!schemaInline || typeof schemaInline !== "object") {
175
+ issues.push({ index, message: 'probe "json-schema" requires an object "schemaInline"' });
176
+ }
177
+ break;
178
+ }
141
179
  default:
142
180
  issues.push({
143
181
  index,
144
- message: `unknown probe kind "${String(probe)}" — expected one of: run, file-exists, file-contains, command-registered, page`,
182
+ message: `unknown probe kind "${String(probe)}" — expected one of: run, file-exists, file-contains, command-registered, page, test, json-schema`,
145
183
  });
146
184
  }
147
185
  });
@@ -189,6 +227,45 @@ async function spawnSiteKernel(
189
227
  });
190
228
  }
191
229
 
230
+ async function spawnVitest(
231
+ workspaceRoot: string,
232
+ file: string,
233
+ testName?: string,
234
+ ): Promise<{ exitCode: number | null; timedOut: boolean }> {
235
+ const args = ["exec", "vitest", "run", file];
236
+ if (testName) {
237
+ args.push("-t", testName);
238
+ }
239
+
240
+ return new Promise((resolve) => {
241
+ const child = spawn("pnpm", args, {
242
+ cwd: workspaceRoot,
243
+ stdio: "ignore",
244
+ });
245
+ let settled = false;
246
+ const timer = setTimeout(() => {
247
+ if (settled) return;
248
+ settled = true;
249
+ child.kill();
250
+ resolve({ exitCode: null, timedOut: true });
251
+ }, RUN_PROBE_TIMEOUT_MS);
252
+ timer.unref?.();
253
+
254
+ child.on("close", (code) => {
255
+ if (settled) return;
256
+ settled = true;
257
+ clearTimeout(timer);
258
+ resolve({ exitCode: code, timedOut: false });
259
+ });
260
+ child.on("error", () => {
261
+ if (settled) return;
262
+ settled = true;
263
+ clearTimeout(timer);
264
+ resolve({ exitCode: null, timedOut: false });
265
+ });
266
+ });
267
+ }
268
+
192
269
  /** Executes a single probe. Pure aside from its declared filesystem/process/registry effects. */
193
270
  export async function runProbe(
194
271
  probe: AcceptanceProbe,
@@ -247,6 +324,48 @@ export async function runProbe(
247
324
  detail: "page probe skipped — run qa.independent.run against a built dist",
248
325
  };
249
326
  }
327
+ case "test": {
328
+ const { exitCode, timedOut } = await spawnVitest(workspaceRoot, probe.file, probe.testName);
329
+ if (timedOut) {
330
+ return { probe, ok: false, detail: `timed out after ${RUN_PROBE_TIMEOUT_MS}ms` };
331
+ }
332
+ const ok = exitCode === probe.expect.exitCode;
333
+ return { probe, ok, detail: `exitCode=${exitCode} (expected ${probe.expect.exitCode})` };
334
+ }
335
+ case "json-schema": {
336
+ const artifactPath = path.join(workspaceRoot, probe.artifact);
337
+ let raw: string;
338
+ try {
339
+ raw = await readFile(artifactPath, "utf8");
340
+ } catch {
341
+ return { probe, ok: false, detail: "artifact file not found" };
342
+ }
343
+ let parsed: unknown;
344
+ const ext = path.extname(probe.artifact).toLowerCase();
345
+ try {
346
+ if (ext === ".yaml" || ext === ".yml") {
347
+ parsed = yamlParse(raw);
348
+ } else {
349
+ parsed = JSON.parse(raw);
350
+ }
351
+ } catch {
352
+ return { probe, ok: false, detail: "artifact file could not be parsed" };
353
+ }
354
+ try {
355
+ const ajv = new Ajv({ allErrors: false });
356
+ const validate = ajv.compile(probe.schemaInline);
357
+ const valid = validate(parsed);
358
+ if (valid) {
359
+ return { probe, ok: true, detail: "schema valid" };
360
+ }
361
+ const firstError = validate.errors?.[0];
362
+ const errorPath = firstError?.instancePath || "(root)";
363
+ const errorMessage = firstError?.message || "validation failed";
364
+ return { probe, ok: false, detail: `Ajv error at ${errorPath}: ${errorMessage}` };
365
+ } catch {
366
+ return { probe, ok: false, detail: "schema compilation failed" };
367
+ }
368
+ }
250
369
  }
251
370
  }
252
371
 
@@ -330,7 +449,17 @@ export async function runRfcAcceptanceRun(
330
449
  message: `${pageProbeCount} page probe(s) skipped — run \`qa.independent.run --site <app>\` against a built dist.`,
331
450
  });
332
451
  }
333
- results.push({ rfcId, probeResults });
452
+
453
+ let rfcCoverage: ProbeCoverageReport | undefined;
454
+ const createdAt = String(fm["createdAt"] ?? "");
455
+ const isArchived = fileName.startsWith("archive/");
456
+ if (createdAt >= RFC_PROBE_BINDING_CUTOFF && !isArchived) {
457
+ const parsedBody = parsedFile.parsed.body;
458
+ const criteriaEval = evaluateAcceptanceCriteria(parsedBody);
459
+ rfcCoverage = computeProbeCoverage(criteriaEval.criterionIds, acceptance);
460
+ }
461
+
462
+ results.push({ rfcId, probeResults, coverage: rfcCoverage });
334
463
  }
335
464
 
336
465
  const failedCount = diagnostics.filter((d) => d.severity === "error").length;
@@ -15,6 +15,7 @@ verification evidence, and atomically mutates RFC frontmatter.
15
15
  <item>RFC-0476: initial implementation.</item>
16
16
  <item>RFC-0756: auto-detect implementation commit when --implementation-commit is omitted.</item>
17
17
  <item>RFC-0795: add RFC-IMP-07 dependsOn dependency gate — blocks stamping when any dependsOn entry is not implemented.</item>
18
+ <item>RFC-0997: add RFC-IMP-08 minimum-one-probe gate — blocks stamping for post-cutoff architecture/contract/command RFCs with no acceptance probes.</item>
18
19
  </CHANGE_SUMMARY>
19
20
  */
20
21
 
@@ -33,7 +34,7 @@ import {
33
34
  } from "../frontmatter-io.ts";
34
35
  import { evaluateAcceptanceCriteria } from "./validate-rules.ts";
35
36
  import { toIsoDate } from "./shared.ts";
36
- import { RFC_DIR, RFC_METADATA_CUTOFF } from "../types.ts";
37
+ import { RFC_DIR, RFC_METADATA_CUTOFF, RFC_PROBE_BINDING_CUTOFF } from "../types.ts";
37
38
  import type { RfcStatus, RfcImplementStampViolation, RfcImplementStampResult } from "../types.ts";
38
39
  import type {
39
40
  ForgeCommandInput,
@@ -399,6 +400,18 @@ export async function runRfcImplementStamp(
399
400
  }
400
401
  }
401
402
 
403
+ // ── RFC-IMP-08: minimum-one-probe gate (RFC-0997) ─────────────────────────
404
+ // Post-cutoff architecture/contract/command RFCs must declare at least one
405
+ // acceptance probe. Policy and deprecation kinds are exempt.
406
+ const rfcKind = String(fm["kind"] ?? "");
407
+ const probeGateKinds = new Set(["architecture", "contract", "command"]);
408
+ if (createdAtStr >= RFC_PROBE_BINDING_CUTOFF && probeGateKinds.has(rfcKind) && !hasProbes) {
409
+ violations.push({
410
+ rule: "RFC-IMP-08",
411
+ message: `RFC ${targetId} (kind: ${rfcKind}) has no acceptance probes. Post-cutoff architecture/contract/command RFCs require at least one probe (RFC-0997). See RFC-0996 for the authoring standard.`,
412
+ });
413
+ }
414
+
402
415
  // If any violations found, return without mutation
403
416
  if (violations.length > 0) {
404
417
  return stampFailResult(violations, isDryRun, outputFormat, logger);