@warpgogol/forge 2.21.6 → 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.
- package/AGENTS.md +27 -1
- package/os/adr/adr-0000-template.md +8 -0
- package/os/adr/handlers/validate.test.ts +203 -0
- package/os/adr/handlers/validate.ts +54 -1
- package/os/adr/types.ts +7 -0
- package/os/compass/handlers/compass-inventory-handler.ts +11 -1
- package/os/compass/handlers/compass-inventory.ts +10 -0
- package/os/core/handlers/validate.ts +56 -5
- package/os/naming/naming-convention.ts +9 -0
- package/os/plugin/plugin.module.ts +1 -1
- package/os/rfc/acceptance.ts +133 -4
- package/os/rfc/handlers/implement-stamp.ts +14 -1
- package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
- package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
- package/os/rfc/handlers/validate-rules.ts +450 -9
- package/os/rfc/handlers/validate.ts +20 -2
- package/os/rfc/rfc-0000-template.md +24 -8
- package/os/rfc/rfc.module.ts +28 -0
- package/os/rfc/types.ts +72 -6
- package/os/rfc/verification-evidence.ts +5 -4
- package/os/rfc/verification-refresh.test.ts +320 -0
- package/os/rfc/verification-refresh.ts +216 -0
- package/os/session/handlers/save.ts +10 -0
- package/os/spec/spec-validate.test.ts +59 -0
- package/os/spec/spec-validate.ts +6 -4
- package/package.json +2 -1
- package/skills/fo/fo-handoff/SKILL.md +15 -6
- package/skills/fo/fo-idea-audit/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
- package/skills/fo/fo-idea-implement/SKILL.md +3 -2
- package/src/compass/contract-registry.ts +25 -6
- package/src/index.ts +1 -1
- package/src/onboarding/doctor.ts +1 -1
- package/src/registry.ts +1 -1
- package/src/tests/acceptance-probe-kinds.test.ts +262 -0
- package/src/tests/plugin-manifest.test.ts +1 -1
- package/src/tests/session-handlers.test.ts +29 -0
- package/src/types/werkstatt-engine-shims.d.ts +0 -21
- package/src/types/werkstatt-shared-shims.d.ts +68 -147
- /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
package/os/rfc/types.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
<item>RFC-0478: added versionBump field to RfcFrontmatter interface and RFC_KNOWN_KEYS array for platform versioning enforcement.</item>
|
|
18
18
|
<item>RFC-0480: added breaksC field to RfcFrontmatter interface and RFC_KNOWN_KEYS array for Layer C protection.</item>
|
|
19
19
|
<item>RFC-0795: added dependsOn, batch fields to RfcFrontmatter and RFC_KNOWN_KEYS; added RFC-IMP-07 to RfcImplementStampRule; added batch/dependsOn to RfcListEntry.</item>
|
|
20
|
+
<item>RFC-0998: added TestProbe and JsonSchemaProbe to AcceptanceProbe union for test and json-schema acceptance probe kinds.</item>
|
|
21
|
+
<item>RFC-0999: added lastRefreshedAt to VerificationEvidence; added RfcVerificationRefreshResult interface.</item>
|
|
20
22
|
</CHANGE_SUMMARY>
|
|
21
23
|
***************************************************************/
|
|
22
24
|
|
|
@@ -265,10 +267,10 @@ export interface RfcFrontmatter {
|
|
|
265
267
|
* arbitrary shell, keeping the probe surface auditable and platform-independent.
|
|
266
268
|
*/
|
|
267
269
|
export type AcceptanceProbe =
|
|
268
|
-
| { probe: "run"; command: string; expect: { exitCode: number } }
|
|
269
|
-
| { probe: "file-exists"; path: string }
|
|
270
|
-
| { probe: "file-contains"; path: string; pattern: string }
|
|
271
|
-
| { probe: "command-registered"; name: string }
|
|
270
|
+
| { probe: "run"; command: string; expect: { exitCode: number }; criterion?: string }
|
|
271
|
+
| { probe: "file-exists"; path: string; criterion?: string }
|
|
272
|
+
| { probe: "file-contains"; path: string; pattern: string; criterion?: string }
|
|
273
|
+
| { probe: "command-registered"; name: string; criterion?: string }
|
|
272
274
|
// RFC-0333: black-box expectation against the rendered site. Executed
|
|
273
275
|
// ONLY by qa.independent.run (needs a built dist); rfc.acceptance.run
|
|
274
276
|
// reports it as skipped.
|
|
@@ -279,6 +281,22 @@ export type AcceptanceProbe =
|
|
|
279
281
|
selector?: string;
|
|
280
282
|
textPattern?: string;
|
|
281
283
|
allowConsoleErrors?: boolean;
|
|
284
|
+
criterion?: string;
|
|
285
|
+
}
|
|
286
|
+
// RFC-0998: run a specific vitest file or test name and check exit code.
|
|
287
|
+
| {
|
|
288
|
+
probe: "test";
|
|
289
|
+
file: string;
|
|
290
|
+
testName?: string;
|
|
291
|
+
expect: { exitCode: number };
|
|
292
|
+
criterion?: string;
|
|
293
|
+
}
|
|
294
|
+
// RFC-0998: validate a JSON/YAML artifact against an inline JSON Schema using Ajv.
|
|
295
|
+
| {
|
|
296
|
+
probe: "json-schema";
|
|
297
|
+
artifact: string;
|
|
298
|
+
schemaInline: Record<string, unknown>;
|
|
299
|
+
criterion?: string;
|
|
282
300
|
};
|
|
283
301
|
|
|
284
302
|
export interface ProbeResult {
|
|
@@ -292,7 +310,7 @@ export interface RfcAcceptanceRunResult {
|
|
|
292
310
|
command: "rfc.acceptance.run";
|
|
293
311
|
status: "pass" | "fail";
|
|
294
312
|
diagnostics: Diagnostic[];
|
|
295
|
-
results: Array<{ rfcId: string; probeResults: ProbeResult[] }>;
|
|
313
|
+
results: Array<{ rfcId: string; probeResults: ProbeResult[]; coverage?: ProbeCoverageReport }>;
|
|
296
314
|
} // ─── RFC-0330: verification evidence ─────────────────────────────────────────
|
|
297
315
|
export interface VerificationEvidenceProbeRecord {
|
|
298
316
|
probe: AcceptanceProbe;
|
|
@@ -306,6 +324,7 @@ export interface VerificationEvidence {
|
|
|
306
324
|
title: string;
|
|
307
325
|
rfcStatus: RfcStatus;
|
|
308
326
|
emittedAt: string;
|
|
327
|
+
lastRefreshedAt?: string;
|
|
309
328
|
commit: string;
|
|
310
329
|
workingTreeDirty: boolean;
|
|
311
330
|
kernelVersion: string;
|
|
@@ -322,6 +341,21 @@ export interface RfcVerificationEmitResult {
|
|
|
322
341
|
emitted: Array<{ rfcId: string; file: string; overall: "pass" | "fail" }>;
|
|
323
342
|
skipped: Array<{ rfcId: string; reason: "no-probes" }>;
|
|
324
343
|
diagnostics: Diagnostic[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface RfcVerificationRefreshResult {
|
|
347
|
+
command: "rfc.verification.refresh";
|
|
348
|
+
status: "pass" | "fail";
|
|
349
|
+
refreshed: Array<{
|
|
350
|
+
rfcId: string;
|
|
351
|
+
file: string;
|
|
352
|
+
overall: "pass" | "fail";
|
|
353
|
+
probesTotal: number;
|
|
354
|
+
probesFailed: number;
|
|
355
|
+
}>;
|
|
356
|
+
skipped: Array<{ rfcId: string; reason: "no evidence envelope" | "not implemented" }>;
|
|
357
|
+
diagnostics: Diagnostic[];
|
|
358
|
+
summary: { total: number; passed: number; failed: number; skipped: number };
|
|
325
359
|
} // ─── Validation ──────────────────────────────────────────────────────────────
|
|
326
360
|
export interface RfcValidationViolation {
|
|
327
361
|
rfcId: string;
|
|
@@ -343,6 +377,7 @@ export interface RfcValidationResult {
|
|
|
343
377
|
count: number;
|
|
344
378
|
violations: RfcValidationViolation[];
|
|
345
379
|
markers?: Marker[];
|
|
380
|
+
coverage?: Array<{ rfcId: string; report: ProbeCoverageReport }>;
|
|
346
381
|
} // ─── Command results ─────────────────────────────────────────────────────────
|
|
347
382
|
|
|
348
383
|
export interface RfcCommandLifecycleDiagnosticData {
|
|
@@ -515,7 +550,8 @@ export type RfcImplementStampRule =
|
|
|
515
550
|
| "RFC-IMP-04"
|
|
516
551
|
| "RFC-IMP-05"
|
|
517
552
|
| "RFC-IMP-06"
|
|
518
|
-
| "RFC-IMP-07"
|
|
553
|
+
| "RFC-IMP-07"
|
|
554
|
+
| "RFC-IMP-08";
|
|
519
555
|
|
|
520
556
|
export interface RfcImplementStampViolation {
|
|
521
557
|
rule: RfcImplementStampRule;
|
|
@@ -594,3 +630,33 @@ export const RFC_METADATA_CUTOFF = "2026-07-07";
|
|
|
594
630
|
* a versionBump field. RFCs created before are exempt.
|
|
595
631
|
*/
|
|
596
632
|
export const RFC_VERSION_BUMP_CUTOFF = "2026-07-21";
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* RFC-0997: cutoff date for probe-criterion binding rules.
|
|
636
|
+
* RFCs created on or after this date are subject to V-35 (probe→criterion
|
|
637
|
+
* referential integrity), V-36 (criterion identifier discipline), V-37
|
|
638
|
+
* (evidence mechanism validity), and RFC-IMP-08 (minimum-one-probe stamp gate).
|
|
639
|
+
* RFCs created before are exempt.
|
|
640
|
+
*/
|
|
641
|
+
export const RFC_PROBE_BINDING_CUTOFF = "2026-09-01";
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* RFC-1006: cutoff date for criteria content rules (phase separation, reject
|
|
645
|
+
* checklist, criterion versioning). RFCs created on or after this date are
|
|
646
|
+
* subject to V-38 (document readiness completeness), V-39 (non-atomic
|
|
647
|
+
* criterion), V-40 (unbounded quantity), V-41 (weasel verb), and V-42
|
|
648
|
+
* (criterion versioning annotation format). RFCs created before are exempt.
|
|
649
|
+
*/
|
|
650
|
+
export const RFC_CRITERIA_CONTENT_CUTOFF = "2026-09-03";
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* RFC-0997: non-blocking probe coverage report per RFC.
|
|
654
|
+
* Emitted in `rfc.validate --json` and `rfc.acceptance.run` outputs.
|
|
655
|
+
*/
|
|
656
|
+
export interface ProbeCoverageReport {
|
|
657
|
+
totalCriteria: number;
|
|
658
|
+
probeBackedCriteria: number;
|
|
659
|
+
coverageRatio: number;
|
|
660
|
+
unboundProbes: string[];
|
|
661
|
+
uncoveredCriteria: string[];
|
|
662
|
+
}
|
|
@@ -13,6 +13,7 @@ a JSON evidence envelope to docs/rfcs/verification/<slug>.generated.yaml.
|
|
|
13
13
|
</MODULE_CONTRACT>
|
|
14
14
|
<CHANGE_SUMMARY>
|
|
15
15
|
<item>RFC-0330: initial implementation.</item>
|
|
16
|
+
<item>RFC-0999: exported captureGitContext, getKernelVersion, byteHashHex, VERIFICATION_DIR for reuse by verification-refresh.ts.</item>
|
|
16
17
|
</CHANGE_SUMMARY>
|
|
17
18
|
*/
|
|
18
19
|
|
|
@@ -42,7 +43,7 @@ import type {
|
|
|
42
43
|
ForgeRuntimeContext,
|
|
43
44
|
} from "../../src/types.ts";
|
|
44
45
|
|
|
45
|
-
const VERIFICATION_DIR = join(RFC_DIR, "verification");
|
|
46
|
+
export const VERIFICATION_DIR = join(RFC_DIR, "verification");
|
|
46
47
|
const HASH_PREFIX = "sha" + "256:";
|
|
47
48
|
|
|
48
49
|
function execGit(workspaceRoot: string, args: string[]): Promise<string> {
|
|
@@ -57,7 +58,7 @@ function execGit(workspaceRoot: string, args: string[]): Promise<string> {
|
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
async function captureGitContext(
|
|
61
|
+
export async function captureGitContext(
|
|
61
62
|
workspaceRoot: string,
|
|
62
63
|
): Promise<{ commit: string; workingTreeDirty: boolean }> {
|
|
63
64
|
const [commitOutput, statusOutput] = await Promise.all([
|
|
@@ -70,7 +71,7 @@ async function captureGitContext(
|
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
async function getKernelVersion(workspaceRoot: string): Promise<string> {
|
|
74
|
+
export async function getKernelVersion(workspaceRoot: string): Promise<string> {
|
|
74
75
|
try {
|
|
75
76
|
const pkgPath = join(workspaceRoot, "packages", "os", "site-kernel", "package.json");
|
|
76
77
|
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
@@ -80,7 +81,7 @@ async function getKernelVersion(workspaceRoot: string): Promise<string> {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
function byteHashHex(content: string): string {
|
|
84
|
+
export function byteHashHex(content: string): string {
|
|
84
85
|
return byteHash(content).slice(HASH_PREFIX.length);
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { test, expect, describe, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { stringify as yamlStringify } from "yaml";
|
|
7
|
+
|
|
8
|
+
import { runRfcVerificationRefresh } from "./verification-refresh.ts";
|
|
9
|
+
|
|
10
|
+
const { mockRunProbe, mockCaptureGitContext, mockGetKernelVersion } = vi.hoisted(() => ({
|
|
11
|
+
mockRunProbe: vi.fn(),
|
|
12
|
+
mockCaptureGitContext: vi.fn(),
|
|
13
|
+
mockGetKernelVersion: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("./acceptance.ts", () => ({
|
|
17
|
+
runProbe: mockRunProbe,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("./verification-evidence.ts", async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof import("./verification-evidence.ts")>();
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
captureGitContext: mockCaptureGitContext,
|
|
25
|
+
getKernelVersion: mockGetKernelVersion,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const RFC_FRONTMATTER = (id: string, status: string, probes: unknown[] = []) => {
|
|
30
|
+
const probeYaml = probes
|
|
31
|
+
.map(
|
|
32
|
+
(p) =>
|
|
33
|
+
` - probe: ${(p as any).probe}\n path: ${(p as any).path ?? ""}\n criterion: ${(p as any).criterion ?? ""}`,
|
|
34
|
+
)
|
|
35
|
+
.join("\n");
|
|
36
|
+
return `---\nid: ${id}\ntitle: "Test ${id}"\nstatus: ${status}\nkind: command\nscope: workspace\nowners:\n - architecture\ncreatedAt: 2026-01-01\nupdatedAt: 2026-01-01\nacceptance:\n${probeYaml}\n---\n\n# ${id}\n## Context\nTest.\n`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const PROBE = { probe: "file-exists", path: "src/index.ts", criterion: "AC-1" };
|
|
40
|
+
|
|
41
|
+
function makeEnvelope(
|
|
42
|
+
rfcId: string,
|
|
43
|
+
emittedAt: string,
|
|
44
|
+
overall: "pass" | "fail" = "pass",
|
|
45
|
+
): Record<string, unknown> {
|
|
46
|
+
return {
|
|
47
|
+
rfcId,
|
|
48
|
+
title: `Test ${rfcId}`,
|
|
49
|
+
rfcStatus: "implemented",
|
|
50
|
+
emittedAt,
|
|
51
|
+
commit: "abc123",
|
|
52
|
+
workingTreeDirty: false,
|
|
53
|
+
kernelVersion: "1.0.0",
|
|
54
|
+
rfcFileHash: "hash123",
|
|
55
|
+
acceptanceHash: "accHash123",
|
|
56
|
+
probes: [
|
|
57
|
+
{
|
|
58
|
+
probe: PROBE,
|
|
59
|
+
ok: overall === "pass",
|
|
60
|
+
detail: overall === "pass" ? "exists" : "does not exist",
|
|
61
|
+
durationMs: 5,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
overall,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function setupWorkspace(opts: {
|
|
69
|
+
rfcs: Array<{ id: string; status: string; probes?: unknown[] }>;
|
|
70
|
+
envelopes?: Array<{ id: string; emittedAt?: string; overall?: "pass" | "fail" }>;
|
|
71
|
+
}): Promise<{ workspaceRoot: string; cleanup: () => Promise<void> }> {
|
|
72
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-refresh-test-"));
|
|
73
|
+
const rfcDir = path.join(tmpDir, "docs", "rfcs");
|
|
74
|
+
const verificationDir = path.join(rfcDir, "verification");
|
|
75
|
+
await fs.mkdir(verificationDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
for (const rfc of opts.rfcs) {
|
|
78
|
+
const slug = rfc.id.toLowerCase();
|
|
79
|
+
const probes = rfc.probes ?? [PROBE];
|
|
80
|
+
await fs.writeFile(
|
|
81
|
+
path.join(rfcDir, `${slug}-test.md`),
|
|
82
|
+
RFC_FRONTMATTER(rfc.id, rfc.status, probes),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const env of opts.envelopes ?? []) {
|
|
87
|
+
const slug = env.id.toLowerCase();
|
|
88
|
+
const envelope = makeEnvelope(env.id, env.emittedAt ?? "2026-01-01T00:00:00.000Z", env.overall);
|
|
89
|
+
const header = `# Generated by rfc.verification.emit\n# Do not edit by hand.\n`;
|
|
90
|
+
await fs.writeFile(
|
|
91
|
+
path.join(verificationDir, `${slug}.generated.yaml`),
|
|
92
|
+
header + yamlStringify(envelope) + "\n",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mockCaptureGitContext.mockResolvedValue({ commit: "abc123", workingTreeDirty: false });
|
|
97
|
+
mockGetKernelVersion.mockResolvedValue("1.0.0");
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
workspaceRoot: tmpDir,
|
|
101
|
+
cleanup: async () => {
|
|
102
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function makeContext(workspaceRoot: string) {
|
|
108
|
+
return {
|
|
109
|
+
workspaceRoot,
|
|
110
|
+
logger: { info: () => {} },
|
|
111
|
+
outputFormat: "json" as const,
|
|
112
|
+
commandRegistry: undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
mockRunProbe.mockReset();
|
|
118
|
+
mockCaptureGitContext.mockReset();
|
|
119
|
+
mockGetKernelVersion.mockReset();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
vi.restoreAllMocks();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("rfc.verification.refresh — AC-1: single-RFC refresh", () => {
|
|
127
|
+
test("updates lastRefreshedAt, replaces probes[], preserves emittedAt", async () => {
|
|
128
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
129
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
130
|
+
envelopes: [{ id: "RFC-0100", emittedAt: "2026-01-01T00:00:00.000Z" }],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
135
|
+
|
|
136
|
+
const result = await runRfcVerificationRefresh(
|
|
137
|
+
{ flags: { id: "RFC-0100" } } as any,
|
|
138
|
+
makeContext(workspaceRoot) as any,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(result.data!.refreshed).toHaveLength(1);
|
|
142
|
+
expect(result.data!.refreshed[0].rfcId).toBe("RFC-0100");
|
|
143
|
+
expect(result.data!.refreshed[0].overall).toBe("pass");
|
|
144
|
+
expect(result.exitCode).toBe(0);
|
|
145
|
+
|
|
146
|
+
// Verify envelope was written with lastRefreshedAt
|
|
147
|
+
const envelopePath = path.join(
|
|
148
|
+
workspaceRoot,
|
|
149
|
+
"docs",
|
|
150
|
+
"rfcs",
|
|
151
|
+
"verification",
|
|
152
|
+
"rfc-0100.generated.yaml",
|
|
153
|
+
);
|
|
154
|
+
const raw = await fs.readFile(envelopePath, "utf-8");
|
|
155
|
+
expect(raw).toContain("lastRefreshedAt");
|
|
156
|
+
expect(raw).toContain("emittedAt: 2026-01-01T00:00:00.000Z");
|
|
157
|
+
} finally {
|
|
158
|
+
await cleanup();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("rfc.verification.refresh — AC-2: --all mode", () => {
|
|
164
|
+
test("iterates over all implemented RFCs with evidence envelopes", async () => {
|
|
165
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
166
|
+
rfcs: [
|
|
167
|
+
{ id: "RFC-0100", status: "implemented" },
|
|
168
|
+
{ id: "RFC-0200", status: "implemented" },
|
|
169
|
+
{ id: "RFC-0300", status: "accepted" },
|
|
170
|
+
],
|
|
171
|
+
envelopes: [{ id: "RFC-0100" }, { id: "RFC-0200" }],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
176
|
+
|
|
177
|
+
const result = await runRfcVerificationRefresh(
|
|
178
|
+
{ flags: { all: true } } as any,
|
|
179
|
+
makeContext(workspaceRoot) as any,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result.data!.refreshed).toHaveLength(2);
|
|
183
|
+
expect(result.data!.skipped).toHaveLength(1);
|
|
184
|
+
expect(result.data!.skipped[0].reason).toBe("not implemented");
|
|
185
|
+
expect(result.exitCode).toBe(0);
|
|
186
|
+
} finally {
|
|
187
|
+
await cleanup();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("rfc.verification.refresh — AC-3: probe failure still writes envelope", () => {
|
|
193
|
+
test("writes envelope with overall: fail and exits non-zero", async () => {
|
|
194
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
195
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
196
|
+
envelopes: [{ id: "RFC-0100" }],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: false, detail: "does not exist" });
|
|
201
|
+
|
|
202
|
+
const result = await runRfcVerificationRefresh(
|
|
203
|
+
{ flags: { id: "RFC-0100" } } as any,
|
|
204
|
+
makeContext(workspaceRoot) as any,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result.data!.refreshed).toHaveLength(1);
|
|
208
|
+
expect(result.data!.refreshed[0].overall).toBe("fail");
|
|
209
|
+
expect(result.data!.refreshed[0].probesFailed).toBe(1);
|
|
210
|
+
expect(result.exitCode).toBe(1);
|
|
211
|
+
|
|
212
|
+
// Envelope was still written
|
|
213
|
+
const envelopePath = path.join(
|
|
214
|
+
workspaceRoot,
|
|
215
|
+
"docs",
|
|
216
|
+
"rfcs",
|
|
217
|
+
"verification",
|
|
218
|
+
"rfc-0100.generated.yaml",
|
|
219
|
+
);
|
|
220
|
+
const raw = await fs.readFile(envelopePath, "utf-8");
|
|
221
|
+
expect(raw).toContain("overall: fail");
|
|
222
|
+
} finally {
|
|
223
|
+
await cleanup();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("rfc.verification.refresh — AC-4: --dry-run does not write files", () => {
|
|
229
|
+
test("runs probes but does not write envelope files", async () => {
|
|
230
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
231
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
232
|
+
envelopes: [{ id: "RFC-0100", emittedAt: "2026-01-01T00:00:00.000Z" }],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
237
|
+
|
|
238
|
+
const result = await runRfcVerificationRefresh(
|
|
239
|
+
{ flags: { id: "RFC-0100", "dry-run": true } } as any,
|
|
240
|
+
makeContext(workspaceRoot) as any,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(result.data!.refreshed).toHaveLength(1);
|
|
244
|
+
expect(result.data!.refreshed[0].overall).toBe("pass");
|
|
245
|
+
|
|
246
|
+
// Envelope file should still have the old content (no lastRefreshedAt)
|
|
247
|
+
const envelopePath = path.join(
|
|
248
|
+
workspaceRoot,
|
|
249
|
+
"docs",
|
|
250
|
+
"rfcs",
|
|
251
|
+
"verification",
|
|
252
|
+
"rfc-0100.generated.yaml",
|
|
253
|
+
);
|
|
254
|
+
const raw = await fs.readFile(envelopePath, "utf-8");
|
|
255
|
+
expect(raw).not.toContain("lastRefreshedAt");
|
|
256
|
+
} finally {
|
|
257
|
+
await cleanup();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("rfc.verification.refresh — AC-5: no evidence envelope", () => {
|
|
263
|
+
test("skips RFC with reason 'no evidence envelope' and exits non-zero", async () => {
|
|
264
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
265
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
266
|
+
envelopes: [],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
271
|
+
|
|
272
|
+
const result = await runRfcVerificationRefresh(
|
|
273
|
+
{ flags: { id: "RFC-0100" } } as any,
|
|
274
|
+
makeContext(workspaceRoot) as any,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(result.data!.refreshed).toHaveLength(0);
|
|
278
|
+
expect(result.data!.skipped).toHaveLength(1);
|
|
279
|
+
expect(result.data!.skipped[0].reason).toBe("no evidence envelope");
|
|
280
|
+
expect(result.exitCode).toBe(1);
|
|
281
|
+
} finally {
|
|
282
|
+
await cleanup();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("rfc.verification.refresh — AC-6: emittedAt preservation", () => {
|
|
288
|
+
test("emittedAt is preserved across refreshes", async () => {
|
|
289
|
+
const originalEmittedAt = "2026-01-01T00:00:00.000Z";
|
|
290
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
291
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
292
|
+
envelopes: [{ id: "RFC-0100", emittedAt: originalEmittedAt }],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
297
|
+
|
|
298
|
+
await runRfcVerificationRefresh(
|
|
299
|
+
{ flags: { id: "RFC-0100" } } as any,
|
|
300
|
+
makeContext(workspaceRoot) as any,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const envelopePath = path.join(
|
|
304
|
+
workspaceRoot,
|
|
305
|
+
"docs",
|
|
306
|
+
"rfcs",
|
|
307
|
+
"verification",
|
|
308
|
+
"rfc-0100.generated.yaml",
|
|
309
|
+
);
|
|
310
|
+
const raw = await fs.readFile(envelopePath, "utf-8");
|
|
311
|
+
expect(raw).toContain(`emittedAt: ${originalEmittedAt}`);
|
|
312
|
+
// lastRefreshedAt should be different (set to now)
|
|
313
|
+
expect(raw).toContain("lastRefreshedAt:");
|
|
314
|
+
// lastRefreshedAt should not equal emittedAt
|
|
315
|
+
expect(raw).not.toMatch(/lastRefreshedAt: 2026-01-01T00:00:00\.000Z/);
|
|
316
|
+
} finally {
|
|
317
|
+
await cleanup();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|