@warpgogol/forge 5.3.4 → 6.0.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 +39 -9
- 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 +326 -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 +93 -5
- 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/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.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 +28 -5
- 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 +27 -3
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +11 -0
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +1 -0
- 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 +140 -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 +72 -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 +46 -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/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/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 +50 -9
- package/os/compass/handlers/compass-docs-validate.ts +359 -0
- package/os/compass/handlers/summary-record.ts +104 -5
- 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/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/exploration/exploration.module.ts +3 -0
- 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 +35 -5
- package/os/rfc/rfc.module.ts +29 -3
- package/os/rfc/types.ts +17 -1
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +155 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +80 -38
- package/os/session/handlers/save.ts +41 -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/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/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
|
@@ -0,0 +1,305 @@
|
|
|
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 {
|
|
9
|
+
runRfcVerificationEmit,
|
|
10
|
+
mapWithConcurrency,
|
|
11
|
+
resolveConcurrency,
|
|
12
|
+
} from "./verification-evidence.ts";
|
|
13
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../src/types.ts";
|
|
14
|
+
|
|
15
|
+
const { mockRunProbe } = vi.hoisted(() => ({
|
|
16
|
+
mockRunProbe: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("./acceptance.ts", () => ({
|
|
20
|
+
runProbe: mockRunProbe,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const RFC_FRONTMATTER = (id: string, status: string, probes: Record<string, unknown>[] = []) => {
|
|
24
|
+
const probeYaml = probes
|
|
25
|
+
.map(
|
|
26
|
+
(p) =>
|
|
27
|
+
` - probe: ${p.probe}\n path: ${p.path ?? ""}\n criterion: ${p.criterion ?? ""}`,
|
|
28
|
+
)
|
|
29
|
+
.join("\n");
|
|
30
|
+
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`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PROBE = { probe: "file-exists", path: "src/index.ts", criterion: "AC-1" };
|
|
34
|
+
|
|
35
|
+
function makeEnvelope(rfcId: string, overall: "pass" | "fail"): Record<string, unknown> {
|
|
36
|
+
return {
|
|
37
|
+
rfcId,
|
|
38
|
+
title: `Test ${rfcId}`,
|
|
39
|
+
rfcStatus: "implemented",
|
|
40
|
+
emittedAt: "2026-01-01T00:00:00.000Z",
|
|
41
|
+
commit: "abc123",
|
|
42
|
+
workingTreeDirty: false,
|
|
43
|
+
kernelVersion: "1.0.0",
|
|
44
|
+
rfcFileHash: "hash123",
|
|
45
|
+
acceptanceHash: "accHash123",
|
|
46
|
+
probes: [{ probe: PROBE, ok: overall === "pass", detail: "", durationMs: 5 }],
|
|
47
|
+
overall,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function setupWorkspace(opts: {
|
|
52
|
+
rfcs: Array<{ id: string; status: string; probes?: unknown[] }>;
|
|
53
|
+
envelopes?: Array<{ id: string; overall: "pass" | "fail" }>;
|
|
54
|
+
}): Promise<{ workspaceRoot: string; cleanup: () => Promise<void> }> {
|
|
55
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-emit-test-"));
|
|
56
|
+
const rfcDir = path.join(tmpDir, "docs", "rfcs");
|
|
57
|
+
const verificationDir = path.join(rfcDir, "verification");
|
|
58
|
+
await fs.mkdir(rfcDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
for (const rfc of opts.rfcs) {
|
|
61
|
+
const slug = rfc.id.toLowerCase();
|
|
62
|
+
const probes = rfc.probes ?? [PROBE];
|
|
63
|
+
await fs.writeFile(
|
|
64
|
+
path.join(rfcDir, `${slug}-test.md`),
|
|
65
|
+
RFC_FRONTMATTER(rfc.id, rfc.status, probes),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const env of opts.envelopes ?? []) {
|
|
70
|
+
await fs.mkdir(verificationDir, { recursive: true });
|
|
71
|
+
await fs.writeFile(
|
|
72
|
+
path.join(verificationDir, `${env.id.toLowerCase()}.generated.yaml`),
|
|
73
|
+
yamlStringify(makeEnvelope(env.id, env.overall)) + "\n",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
workspaceRoot: tmpDir,
|
|
79
|
+
cleanup: async () => {
|
|
80
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function makeContext(workspaceRoot: string, overrides: Record<string, unknown> = {}) {
|
|
86
|
+
return {
|
|
87
|
+
workspaceRoot,
|
|
88
|
+
logger: { info: () => {} },
|
|
89
|
+
outputFormat: "json" as const,
|
|
90
|
+
commandRegistry: undefined,
|
|
91
|
+
dryRun: false,
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const envelopePath = (root: string, rfcId: string) =>
|
|
97
|
+
path.join(root, "docs", "rfcs", "verification", `${rfcId.toLowerCase()}.generated.yaml`);
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
mockRunProbe.mockReset();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.restoreAllMocks();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("rfc.verification.emit — baseline emission", () => {
|
|
108
|
+
test("writes the emit-canonical structured marker header + envelope", async () => {
|
|
109
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
110
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
115
|
+
|
|
116
|
+
const result = await runRfcVerificationEmit(
|
|
117
|
+
{ flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
|
|
118
|
+
makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result.data!.emitted).toHaveLength(1);
|
|
122
|
+
expect(result.exitCode).toBe(0);
|
|
123
|
+
|
|
124
|
+
const raw = await fs.readFile(envelopePath(workspaceRoot, "RFC-0100"), "utf-8");
|
|
125
|
+
// RFC-1237 canonical format: structured marker keys, not '#' comments.
|
|
126
|
+
expect(raw).toMatch(/^generatedMarker: GENERATED\./);
|
|
127
|
+
expect(raw).toContain("doNotEdit: DO NOT EDIT THIS FILE.");
|
|
128
|
+
expect(raw).toContain("ownerCommand: rfc.verification.emit");
|
|
129
|
+
expect(raw).toContain("regenerateCommand: pnpm exec werkstatt run rfc.verification.emit");
|
|
130
|
+
expect(raw).toContain("rfcId: RFC-0100");
|
|
131
|
+
expect(raw).toContain("overall: pass");
|
|
132
|
+
expect(raw.split("\n").every((line) => !line.startsWith("#"))).toBe(true);
|
|
133
|
+
} finally {
|
|
134
|
+
await cleanup();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("rfc.verification.emit — sweep delta report", () => {
|
|
140
|
+
test("reports recovered (fail→pass) and regressed (pass→fail) transitions", async () => {
|
|
141
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
142
|
+
rfcs: [
|
|
143
|
+
{ id: "RFC-0100", status: "implemented" },
|
|
144
|
+
{ id: "RFC-0101", status: "implemented" },
|
|
145
|
+
{ id: "RFC-0102", status: "implemented" },
|
|
146
|
+
],
|
|
147
|
+
envelopes: [
|
|
148
|
+
{ id: "RFC-0100", overall: "fail" },
|
|
149
|
+
{ id: "RFC-0101", overall: "pass" },
|
|
150
|
+
{ id: "RFC-0102", overall: "fail" },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
mockRunProbe
|
|
156
|
+
.mockResolvedValueOnce({ probe: PROBE, ok: true, detail: "exists" })
|
|
157
|
+
.mockResolvedValueOnce({ probe: PROBE, ok: false, detail: "missing" })
|
|
158
|
+
.mockResolvedValueOnce({ probe: PROBE, ok: false, detail: "missing" });
|
|
159
|
+
|
|
160
|
+
const result = await runRfcVerificationEmit(
|
|
161
|
+
{ flags: { status: "implemented" } } as unknown as ForgeCommandInput,
|
|
162
|
+
makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(result.data!.delta).toEqual({
|
|
166
|
+
recovered: ["RFC-0100"],
|
|
167
|
+
regressed: ["RFC-0101"],
|
|
168
|
+
});
|
|
169
|
+
const entries = Object.fromEntries(result.data!.emitted.map((e) => [e.rfcId, e]));
|
|
170
|
+
expect(entries["RFC-0100"]?.previousOverall).toBe("fail");
|
|
171
|
+
expect(entries["RFC-0101"]?.previousOverall).toBe("pass");
|
|
172
|
+
expect(entries["RFC-0102"]?.previousOverall).toBe("fail");
|
|
173
|
+
const r101Diag = result.data!.diagnostics.find((d) => d.file?.includes("rfc-0101"));
|
|
174
|
+
expect(r101Diag?.message).toContain("regressed: was pass");
|
|
175
|
+
expect(result.summary).toContain("1 regressed, 1 recovered");
|
|
176
|
+
} finally {
|
|
177
|
+
await cleanup();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("rfc.verification.emit — RFC-1237 AC-6: dry-run", () => {
|
|
183
|
+
test("context.dryRun (the real werkstatt run --dry-run path) writes zero envelopes", async () => {
|
|
184
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
185
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
190
|
+
|
|
191
|
+
// The real CLI path: consumeCommonFlags strips --dry-run from argv into
|
|
192
|
+
// context.dryRun — input.flags carries no "dry-run" key at all.
|
|
193
|
+
const result = await runRfcVerificationEmit(
|
|
194
|
+
{ flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
|
|
195
|
+
makeContext(workspaceRoot, { dryRun: true }) as unknown as ForgeRuntimeContext,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(result.data!.emitted).toHaveLength(1);
|
|
199
|
+
expect(result.summary).toContain("dry-run");
|
|
200
|
+
await expect(fs.stat(envelopePath(workspaceRoot, "RFC-0100"))).rejects.toThrow();
|
|
201
|
+
// The verification dir itself is also not created under dry-run.
|
|
202
|
+
await expect(
|
|
203
|
+
fs.stat(path.join(workspaceRoot, "docs", "rfcs", "verification")),
|
|
204
|
+
).rejects.toThrow();
|
|
205
|
+
} finally {
|
|
206
|
+
await cleanup();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('input.flags["dry-run"] (direct/programmatic callers) writes zero envelopes', async () => {
|
|
211
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
212
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
217
|
+
|
|
218
|
+
const result = await runRfcVerificationEmit(
|
|
219
|
+
{ flags: { id: "RFC-0100", "dry-run": true } } as unknown as ForgeCommandInput,
|
|
220
|
+
makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(result.summary).toContain("dry-run");
|
|
224
|
+
await expect(fs.stat(envelopePath(workspaceRoot, "RFC-0100"))).rejects.toThrow();
|
|
225
|
+
} finally {
|
|
226
|
+
await cleanup();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("dry-run still runs probes and reports overall: fail", async () => {
|
|
231
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
232
|
+
rfcs: [{ id: "RFC-0100", status: "implemented" }],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: false, detail: "does not exist" });
|
|
237
|
+
|
|
238
|
+
const result = await runRfcVerificationEmit(
|
|
239
|
+
{ flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
|
|
240
|
+
makeContext(workspaceRoot, { dryRun: true }) as unknown as ForgeRuntimeContext,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(mockRunProbe).toHaveBeenCalledOnce();
|
|
244
|
+
expect(result.data!.emitted[0]?.overall).toBe("fail");
|
|
245
|
+
expect(result.exitCode).toBe(1);
|
|
246
|
+
await expect(fs.stat(envelopePath(workspaceRoot, "RFC-0100"))).rejects.toThrow();
|
|
247
|
+
} finally {
|
|
248
|
+
await cleanup();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("verification sweep — --concurrency", () => {
|
|
254
|
+
test("mapWithConcurrency preserves input order and bounds parallelism", async () => {
|
|
255
|
+
let inFlight = 0;
|
|
256
|
+
let maxInFlight = 0;
|
|
257
|
+
const items = Array.from({ length: 10 }, (_, i) => i);
|
|
258
|
+
const results = await mapWithConcurrency(items, 3, async (n) => {
|
|
259
|
+
inFlight += 1;
|
|
260
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
261
|
+
await new Promise((r) => setTimeout(r, Math.random() * 5));
|
|
262
|
+
inFlight -= 1;
|
|
263
|
+
return n * 2;
|
|
264
|
+
});
|
|
265
|
+
expect(results).toEqual(items.map((n) => n * 2));
|
|
266
|
+
expect(maxInFlight).toBeLessThanOrEqual(3);
|
|
267
|
+
expect(maxInFlight).toBeGreaterThan(1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("resolveConcurrency: absent→1, garbage→1+warn, >16→clamped", () => {
|
|
271
|
+
const warnings: string[] = [];
|
|
272
|
+
const warn = (m: string) => warnings.push(m);
|
|
273
|
+
expect(resolveConcurrency(undefined)).toBe(1);
|
|
274
|
+
expect(resolveConcurrency("abc", warn)).toBe(1);
|
|
275
|
+
expect(resolveConcurrency("0", warn)).toBe(1);
|
|
276
|
+
expect(resolveConcurrency("4", warn)).toBe(4);
|
|
277
|
+
expect(resolveConcurrency("99", warn)).toBe(16);
|
|
278
|
+
expect(warnings).toHaveLength(3);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("--concurrency 3 emits identical results to sequential", async () => {
|
|
282
|
+
const ids = ["RFC-0201", "RFC-0202", "RFC-0203", "RFC-0204"];
|
|
283
|
+
const { workspaceRoot, cleanup } = await setupWorkspace({
|
|
284
|
+
rfcs: ids.map((id) => ({ id, status: "implemented" })),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
|
|
289
|
+
|
|
290
|
+
const result = await runRfcVerificationEmit(
|
|
291
|
+
{ flags: { status: "implemented", concurrency: "3" } } as unknown as ForgeCommandInput,
|
|
292
|
+
makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(result.data!.emitted.map((e) => e.rfcId)).toEqual(ids);
|
|
296
|
+
expect(result.data!.emitted.every((e) => e.overall === "pass")).toBe(true);
|
|
297
|
+
expect(result.exitCode).toBe(0);
|
|
298
|
+
for (const id of ids) {
|
|
299
|
+
await expect(fs.stat(envelopePath(workspaceRoot, id))).resolves.toBeDefined();
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
await cleanup();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -14,6 +14,18 @@ a JSON evidence envelope to docs/rfcs/verification/<slug>.generated.yaml.
|
|
|
14
14
|
<CHANGE_SUMMARY>
|
|
15
15
|
<item>RFC-0330: initial implementation.</item>
|
|
16
16
|
<item>RFC-0999: exported captureGitContext, getKernelVersion, byteHashHex, VERIFICATION_DIR for reuse by verification-refresh.ts.</item>
|
|
17
|
+
<item>RFC-1237: dual-read dry-run (context.dryRun || flags["dry-run"]),
|
|
18
|
+
io.writeFile port routing, dry-run summary marker; serializeEvidenceEnvelope —
|
|
19
|
+
shared emit-canonical serializer restoring the structured YAML marker-key
|
|
20
|
+
header (generatedMarker/doNotEdit/ownerCommand/editInstead/regenerateCommand
|
|
21
|
+
with "pnpm exec werkstatt run" prefix), also consumed by refresh.</item>
|
|
22
|
+
<item>Session-retro 2026-10-08: sweep delta report — reads the committed
|
|
23
|
+
envelope baseline (previousOverall) and reports recovered/regressed
|
|
24
|
+
transitions so a fail-heavy sweep needs no manual pass/fail triage.</item>
|
|
25
|
+
<item>Session-retro 2026-10-08: --concurrency flag (default 1, garbage values
|
|
26
|
+
warn + fall back to 1, >16 clamps) fans emit out across RFC units via
|
|
27
|
+
mapWithConcurrency while probes inside one RFC stay sequential — envelope
|
|
28
|
+
ordering stays deterministic.</item>
|
|
17
29
|
</CHANGE_SUMMARY>
|
|
18
30
|
*/
|
|
19
31
|
|
|
@@ -26,9 +38,8 @@ import { byteHash } from "../../src/utils/hash.ts";
|
|
|
26
38
|
|
|
27
39
|
import { runProbe } from "./acceptance.ts";
|
|
28
40
|
import { listRfcFiles, readAndParseRfc } from "./frontmatter-io.ts";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { stringify as yamlStringify } from "yaml";
|
|
41
|
+
import { generatedHeaderFields } from "../../src/utils/generated-marker.ts";
|
|
42
|
+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
32
43
|
import { RFC_DIR } from "./types.ts";
|
|
33
44
|
|
|
34
45
|
// Ambient default for helper fns without a context param — handlers override
|
|
@@ -90,6 +101,71 @@ export function byteHashHex(content: string): string {
|
|
|
90
101
|
return byteHash(content).slice(HASH_PREFIX.length);
|
|
91
102
|
}
|
|
92
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Bounded-parallel map preserving input order — one in-flight worker per slot,
|
|
106
|
+
* results land in the same positions as their inputs. Used by the emit/refresh
|
|
107
|
+
* sweeps so `--concurrency N` overlaps probe subprocesses across RFCs.
|
|
108
|
+
*/
|
|
109
|
+
export async function mapWithConcurrency<T, R>(
|
|
110
|
+
items: readonly T[],
|
|
111
|
+
concurrency: number,
|
|
112
|
+
worker: (item: T, index: number) => Promise<R>,
|
|
113
|
+
): Promise<R[]> {
|
|
114
|
+
const results = new Array<R>(items.length);
|
|
115
|
+
let cursor = 0;
|
|
116
|
+
const lanes = Array.from({ length: Math.min(Math.max(concurrency, 1), items.length) }, () =>
|
|
117
|
+
(async () => {
|
|
118
|
+
while (cursor < items.length) {
|
|
119
|
+
const index = cursor;
|
|
120
|
+
cursor += 1;
|
|
121
|
+
results[index] = await worker(items[index]!, index);
|
|
122
|
+
}
|
|
123
|
+
})(),
|
|
124
|
+
);
|
|
125
|
+
await Promise.all(lanes);
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the `--concurrency` flag (string flag; the kernel schema has no
|
|
131
|
+
* number kind). Garbage/NaN/non-positive degrades to 1 with a warning;
|
|
132
|
+
* clamped to 16 — probe workers spawn vitest/site-kernel children and wider
|
|
133
|
+
* fan-out starves CPU rather than speeding the sweep.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveConcurrency(flag: unknown, warn?: (message: string) => void): number {
|
|
136
|
+
if (flag === undefined) return 1;
|
|
137
|
+
const raw = Array.isArray(flag) ? flag[0] : flag;
|
|
138
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
139
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
140
|
+
warn?.(`--concurrency "${String(raw)}" is not a positive integer — falling back to 1.`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
if (parsed > 16) {
|
|
144
|
+
warn?.(
|
|
145
|
+
`--concurrency ${parsed} exceeds the 16 cap — clamped (probe workers spawn subprocesses).`,
|
|
146
|
+
);
|
|
147
|
+
return 16;
|
|
148
|
+
}
|
|
149
|
+
return parsed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* RFC-1237: the emit-canonical envelope serialization — structured marker keys
|
|
154
|
+
* (generatedMarker/doNotEdit/ownerCommand/editInstead/regenerateCommand) as
|
|
155
|
+
* leading YAML fields, then the envelope body. emit and refresh MUST share
|
|
156
|
+
* this serializer: refresh owns content freshness, never envelope identity
|
|
157
|
+
* (OWN-DUP-01), so refresh output is identical-by-construction.
|
|
158
|
+
*/
|
|
159
|
+
export function serializeEvidenceEnvelope(envelope: VerificationEvidence): string {
|
|
160
|
+
return `${yamlStringify({
|
|
161
|
+
...generatedHeaderFields({
|
|
162
|
+
ownerCommand: "rfc.verification.emit",
|
|
163
|
+
commandPrefix: "pnpm exec werkstatt run",
|
|
164
|
+
}),
|
|
165
|
+
...envelope,
|
|
166
|
+
})}\n`;
|
|
167
|
+
}
|
|
168
|
+
|
|
93
169
|
function normalizeProbes(probes: AcceptanceProbe[]): string {
|
|
94
170
|
return JSON.stringify(probes, Object.keys(probes[0] ?? {}).sort());
|
|
95
171
|
}
|
|
@@ -130,6 +206,10 @@ export async function runRfcVerificationEmit(
|
|
|
130
206
|
const rfcDirPath = join(workspaceRoot, RFC_DIR);
|
|
131
207
|
const targetId = input.flags["id"] as string | undefined;
|
|
132
208
|
const targetStatus = input.flags["status"] as string | undefined;
|
|
209
|
+
// RFC-1237: consumeCommonFlags strips --dry-run into context.dryRun before
|
|
210
|
+
// dispatch — the input.flags read stays for direct/programmatic callers.
|
|
211
|
+
const dryRun = context.dryRun === true || input.flags["dry-run"] === true;
|
|
212
|
+
const concurrency = resolveConcurrency(input.flags["concurrency"], (m) => logger.warn(m));
|
|
133
213
|
|
|
134
214
|
if (!targetId && !targetStatus) {
|
|
135
215
|
return {
|
|
@@ -161,22 +241,27 @@ export async function runRfcVerificationEmit(
|
|
|
161
241
|
}
|
|
162
242
|
const kernelVersion = await getKernelVersion(workspaceRoot);
|
|
163
243
|
const verificationDirAbs = join(workspaceRoot, VERIFICATION_DIR);
|
|
164
|
-
await io.mkdir(verificationDirAbs);
|
|
244
|
+
if (!dryRun) await io.mkdir(verificationDirAbs);
|
|
165
245
|
|
|
166
|
-
|
|
246
|
+
const processFile = async (
|
|
247
|
+
fileName: string,
|
|
248
|
+
): Promise<{
|
|
249
|
+
emittedEntry?: NonNullable<RfcVerificationEmitResult["emitted"]>[number];
|
|
250
|
+
skippedEntry?: NonNullable<RfcVerificationEmitResult["skipped"]>[number];
|
|
251
|
+
diagnostic?: Diagnostic;
|
|
252
|
+
}> => {
|
|
167
253
|
const parsedFile = await readAndParseRfc(rfcDirPath, fileName);
|
|
168
|
-
if (!parsedFile)
|
|
169
|
-
if ("error" in parsedFile)
|
|
254
|
+
if (!parsedFile) return {};
|
|
255
|
+
if ("error" in parsedFile) return {};
|
|
170
256
|
const fm = parsedFile.parsed.frontmatter;
|
|
171
257
|
const rfcId = String(fm["id"] ?? "");
|
|
172
258
|
|
|
173
|
-
if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase())
|
|
174
|
-
if (targetStatus && String(fm["status"] ?? "") !== targetStatus)
|
|
259
|
+
if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) return {};
|
|
260
|
+
if (targetStatus && String(fm["status"] ?? "") !== targetStatus) return {};
|
|
175
261
|
|
|
176
262
|
const acceptance = fm["acceptance"];
|
|
177
263
|
if (!Array.isArray(acceptance) || acceptance.length === 0) {
|
|
178
|
-
|
|
179
|
-
continue;
|
|
264
|
+
return { skippedEntry: { rfcId, reason: "no-probes" } };
|
|
180
265
|
}
|
|
181
266
|
|
|
182
267
|
const probes = acceptance as AcceptanceProbe[];
|
|
@@ -213,29 +298,74 @@ export async function runRfcVerificationEmit(
|
|
|
213
298
|
const evidenceFileName = `${slug}.generated.yaml`;
|
|
214
299
|
const evidenceRelPath = join(VERIFICATION_DIR, evidenceFileName);
|
|
215
300
|
const evidenceAbsPath = join(workspaceRoot, evidenceRelPath);
|
|
216
|
-
const jsonContent = `${buildGeneratedHeader({ filePath: evidenceRelPath, ownerCommand: "rfc.verification.emit" })}${yamlStringify(envelope)}\n`;
|
|
217
|
-
await writeFileAtomic(evidenceAbsPath, jsonContent);
|
|
218
|
-
|
|
219
|
-
emitted.push({ rfcId, file: evidenceRelPath, overall: envelope.overall });
|
|
220
301
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
302
|
+
// Committed-envelope baseline for the sweep delta — lets a fail-heavy run
|
|
303
|
+
// separate still-failing drift from fresh regressions without git diff.
|
|
304
|
+
let previousOverall: "pass" | "fail" | undefined;
|
|
305
|
+
try {
|
|
306
|
+
const existing = yamlParse(await io.readFile(evidenceAbsPath)) as VerificationEvidence | null;
|
|
307
|
+
if (existing && (existing.overall === "pass" || existing.overall === "fail")) {
|
|
308
|
+
previousOverall = existing.overall;
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// No committed envelope — first emission for this RFC.
|
|
228
312
|
}
|
|
229
313
|
|
|
314
|
+
// RFC-1237: port write — the recording IO adapter intercepts under kernel
|
|
315
|
+
// --dry-run even if the explicit guard regresses.
|
|
316
|
+
if (!dryRun) await io.writeFile(evidenceAbsPath, serializeEvidenceEnvelope(envelope));
|
|
317
|
+
|
|
230
318
|
if (outputFormat === "pretty") {
|
|
319
|
+
const drift =
|
|
320
|
+
previousOverall !== undefined && previousOverall !== envelope.overall
|
|
321
|
+
? `, was ${previousOverall}`
|
|
322
|
+
: "";
|
|
231
323
|
logger.info(
|
|
232
|
-
`[evidence] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes)`,
|
|
324
|
+
`[evidence] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes${drift}${dryRun ? ", dry-run" : ""})`,
|
|
233
325
|
);
|
|
234
326
|
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
emittedEntry: {
|
|
330
|
+
rfcId,
|
|
331
|
+
file: evidenceRelPath,
|
|
332
|
+
overall: envelope.overall,
|
|
333
|
+
...(previousOverall !== undefined ? { previousOverall } : {}),
|
|
334
|
+
},
|
|
335
|
+
...(envelope.overall === "fail"
|
|
336
|
+
? {
|
|
337
|
+
diagnostic: {
|
|
338
|
+
ruleId: "RFC-EVID-02",
|
|
339
|
+
severity: "error",
|
|
340
|
+
file: evidenceRelPath,
|
|
341
|
+
message: `${rfcId}: evidence overall is "fail" — ${probeRecords.filter((r) => !r.ok).length} probe(s) failed${previousOverall === "pass" ? " (regressed: was pass)" : ""}.`,
|
|
342
|
+
} satisfies Diagnostic,
|
|
343
|
+
}
|
|
344
|
+
: {}),
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const results = await mapWithConcurrency(allFiles, concurrency, processFile);
|
|
349
|
+
for (const r of results) {
|
|
350
|
+
if (r.emittedEntry) emitted.push(r.emittedEntry);
|
|
351
|
+
if (r.skippedEntry) skipped.push(r.skippedEntry);
|
|
352
|
+
if (r.diagnostic) diagnostics.push(r.diagnostic);
|
|
235
353
|
}
|
|
236
354
|
|
|
237
355
|
const hasFailures = emitted.some((e) => e.overall === "fail");
|
|
238
356
|
const status: RfcVerificationEmitResult["status"] = hasFailures ? "fail" : "pass";
|
|
357
|
+
const delta = {
|
|
358
|
+
recovered: emitted
|
|
359
|
+
.filter((e) => e.previousOverall === "fail" && e.overall === "pass")
|
|
360
|
+
.map((e) => e.rfcId),
|
|
361
|
+
regressed: emitted
|
|
362
|
+
.filter((e) => e.previousOverall === "pass" && e.overall === "fail")
|
|
363
|
+
.map((e) => e.rfcId),
|
|
364
|
+
};
|
|
365
|
+
const deltaSummary =
|
|
366
|
+
delta.recovered.length + delta.regressed.length > 0
|
|
367
|
+
? `, ${delta.regressed.length} regressed, ${delta.recovered.length} recovered`
|
|
368
|
+
: "";
|
|
239
369
|
|
|
240
370
|
return {
|
|
241
371
|
data: {
|
|
@@ -244,8 +374,9 @@ export async function runRfcVerificationEmit(
|
|
|
244
374
|
emitted,
|
|
245
375
|
skipped,
|
|
246
376
|
diagnostics,
|
|
377
|
+
delta,
|
|
247
378
|
},
|
|
248
379
|
exitCode: hasFailures ? 1 : 0,
|
|
249
|
-
summary: `rfc.verification.emit: ${emitted.length} emitted, ${skipped.length} skipped`,
|
|
380
|
+
summary: `rfc.verification.emit: ${emitted.length} emitted, ${skipped.length} skipped${deltaSummary}${dryRun ? ", dry-run" : ""}`,
|
|
250
381
|
};
|
|
251
382
|
}
|