@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.
Files changed (41) hide show
  1. package/AGENTS.md +27 -1
  2. package/os/adr/adr-0000-template.md +8 -0
  3. package/os/adr/handlers/validate.test.ts +203 -0
  4. package/os/adr/handlers/validate.ts +54 -1
  5. package/os/adr/types.ts +7 -0
  6. package/os/compass/handlers/compass-inventory-handler.ts +11 -1
  7. package/os/compass/handlers/compass-inventory.ts +10 -0
  8. package/os/core/handlers/validate.ts +56 -5
  9. package/os/naming/naming-convention.ts +9 -0
  10. package/os/plugin/plugin.module.ts +1 -1
  11. package/os/rfc/acceptance.ts +133 -4
  12. package/os/rfc/handlers/implement-stamp.ts +14 -1
  13. package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
  14. package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
  15. package/os/rfc/handlers/validate-rules.ts +450 -9
  16. package/os/rfc/handlers/validate.ts +20 -2
  17. package/os/rfc/rfc-0000-template.md +24 -8
  18. package/os/rfc/rfc.module.ts +28 -0
  19. package/os/rfc/types.ts +72 -6
  20. package/os/rfc/verification-evidence.ts +5 -4
  21. package/os/rfc/verification-refresh.test.ts +320 -0
  22. package/os/rfc/verification-refresh.ts +216 -0
  23. package/os/session/handlers/save.ts +10 -0
  24. package/os/spec/spec-validate.test.ts +59 -0
  25. package/os/spec/spec-validate.ts +6 -4
  26. package/package.json +2 -1
  27. package/skills/fo/fo-handoff/SKILL.md +15 -6
  28. package/skills/fo/fo-idea-audit/SKILL.md +1 -1
  29. package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
  30. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
  31. package/skills/fo/fo-idea-implement/SKILL.md +3 -2
  32. package/src/compass/contract-registry.ts +25 -6
  33. package/src/index.ts +1 -1
  34. package/src/onboarding/doctor.ts +1 -1
  35. package/src/registry.ts +1 -1
  36. package/src/tests/acceptance-probe-kinds.test.ts +262 -0
  37. package/src/tests/plugin-manifest.test.ts +1 -1
  38. package/src/tests/session-handlers.test.ts +29 -0
  39. package/src/types/werkstatt-engine-shims.d.ts +0 -21
  40. package/src/types/werkstatt-shared-shims.d.ts +68 -147
  41. /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
+ });