@warpgogol/forge 5.3.4 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/bin/cli.ts +7 -2
- package/dist/bin/cli.js +7 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +2 -0
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/audit/audit.module.d.ts.map +1 -1
- package/dist/os/audit/audit.module.js +2 -0
- package/dist/os/audit/audit.module.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +47 -15
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
- package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
- package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
- package/dist/os/compass/handlers/summary-record.d.ts +11 -2
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +95 -15
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +124 -73
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
- package/dist/os/core/handlers/file-size-lint.js +2 -0
- package/dist/os/core/handlers/file-size-lint.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +3 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/naming/naming-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +11 -1
- package/dist/os/naming/naming-convention.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +9 -2
- package/dist/os/naming/naming.module.js.map +1 -1
- package/dist/os/notes/notes.module.d.ts.map +1 -1
- package/dist/os/notes/notes.module.js +17 -6
- package/dist/os/notes/notes.module.js.map +1 -1
- package/dist/os/plan/plan.module.d.ts.map +1 -1
- package/dist/os/plan/plan.module.js +2 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts.map +1 -1
- package/dist/os/plugin/plugin.module.js +9 -2
- package/dist/os/plugin/plugin.module.js.map +1 -1
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +19 -4
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
- package/dist/os/queue/handlers/queue-validate.js +154 -7
- package/dist/os/queue/handlers/queue-validate.js.map +1 -1
- package/dist/os/queue/manifest.d.ts +17 -1
- package/dist/os/queue/manifest.d.ts.map +1 -1
- package/dist/os/queue/manifest.js +127 -6
- package/dist/os/queue/manifest.js.map +1 -1
- package/dist/os/queue/queue.module.d.ts.map +1 -1
- package/dist/os/queue/queue.module.js +32 -4
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/queue/types.d.ts +100 -1
- package/dist/os/queue/types.d.ts.map +1 -1
- package/dist/os/queue/types.js +44 -0
- package/dist/os/queue/types.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +30 -8
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +25 -6
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +12 -2
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +2 -2
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/rfc/verification-evidence.d.ts +21 -0
- package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
- package/dist/os/rfc/verification-evidence.js +134 -23
- package/dist/os/rfc/verification-evidence.js.map +1 -1
- package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
- package/dist/os/rfc/verification-refresh.js +66 -37
- package/dist/os/rfc/verification-refresh.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +42 -5
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +5 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
- package/dist/os/spec/live-spec-merge.js +55 -134
- package/dist/os/spec/live-spec-merge.js.map +1 -1
- package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
- package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
- package/dist/os/spec/live-spec-rebuild.js +272 -0
- package/dist/os/spec/live-spec-rebuild.js.map +1 -0
- package/dist/os/spec/live-spec-shared.d.ts +62 -0
- package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
- package/dist/os/spec/live-spec-shared.js +315 -0
- package/dist/os/spec/live-spec-shared.js.map +1 -0
- package/dist/os/spec/live-spec-types.d.ts +17 -1
- package/dist/os/spec/live-spec-types.d.ts.map +1 -1
- package/dist/os/spec/live-spec-types.js +3 -0
- package/dist/os/spec/live-spec-types.js.map +1 -1
- package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
- package/dist/os/spec/live-spec-validate.js +105 -2
- package/dist/os/spec/live-spec-validate.js.map +1 -1
- package/dist/os/spec/spec-materialize.d.ts +2 -0
- package/dist/os/spec/spec-materialize.d.ts.map +1 -1
- package/dist/os/spec/spec-materialize.js +45 -5
- package/dist/os/spec/spec-materialize.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +19 -0
- package/dist/os/spec/spec-schema.d.ts.map +1 -1
- package/dist/os/spec/spec-schema.js +30 -0
- package/dist/os/spec/spec-schema.js.map +1 -1
- package/dist/os/spec/spec-status.d.ts +4 -0
- package/dist/os/spec/spec-status.d.ts.map +1 -1
- package/dist/os/spec/spec-status.js +19 -1
- package/dist/os/spec/spec-status.js.map +1 -1
- package/dist/os/spec/spec-validate.d.ts.map +1 -1
- package/dist/os/spec/spec-validate.js +55 -8
- package/dist/os/spec/spec-validate.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +84 -8
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +13 -4
- package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
- package/dist/os/workflow/workflow.module.d.ts.map +1 -1
- package/dist/os/workflow/workflow.module.js +13 -5
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/cli-flags.d.ts.map +1 -1
- package/dist/src/cli-flags.js +25 -5
- package/dist/src/cli-flags.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +3 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +2 -3
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +8 -3
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +14 -1
- package/dist/src/pipeline-status.d.ts.map +1 -1
- package/dist/src/pipeline-status.js +28 -8
- package/dist/src/pipeline-status.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/generated-marker.d.ts +8 -0
- package/dist/src/utils/generated-marker.d.ts.map +1 -1
- package/dist/src/utils/generated-marker.js +25 -0
- package/dist/src/utils/generated-marker.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +2 -4
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/os/adr/adr.module.ts +2 -0
- package/os/audit/audit.module.ts +2 -0
- package/os/compass/compass.module.ts +49 -15
- package/os/compass/handlers/compass-audit-handler.ts +51 -10
- package/os/compass/handlers/compass-docs-validate.ts +361 -0
- package/os/compass/handlers/summary-record.ts +106 -15
- package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
- package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
- package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
- package/os/compass/handlers/tests/summary-record.test.ts +127 -0
- package/os/core/core.module.ts +147 -75
- package/os/core/docs-archive.test.ts +136 -0
- package/os/core/handlers/file-size-lint.ts +2 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/naming/naming-convention.ts +11 -1
- package/os/naming/naming.module.ts +9 -2
- package/os/notes/notes.module.ts +17 -6
- package/os/plan/plan-0000-template.md +150 -0
- package/os/plan/plan.module.ts +2 -0
- package/os/plugin/plugin.module.ts +9 -2
- package/os/program/program.module.ts +19 -4
- package/os/queue/decision-ledger.test.ts +163 -0
- package/os/queue/handlers/queue-validate.ts +190 -8
- package/os/queue/manifest.ts +201 -7
- package/os/queue/queue-validate.test.ts +303 -0
- package/os/queue/queue.module.ts +32 -4
- package/os/queue/types.ts +56 -1
- package/os/rfc/handlers/implement-stamp.ts +37 -8
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
- package/os/rfc/rfc.module.ts +27 -6
- package/os/rfc/types.ts +18 -3
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +149 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +74 -38
- package/os/session/handlers/save.ts +37 -5
- package/os/session/session.module.ts +5 -0
- package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
- package/os/spec/live-spec-list-show-validate.test.ts +10 -6
- package/os/spec/live-spec-merge.test.ts +187 -7
- package/os/spec/live-spec-merge.ts +71 -155
- package/os/spec/live-spec-rebuild.test.ts +317 -0
- package/os/spec/live-spec-rebuild.ts +356 -0
- package/os/spec/live-spec-shared.ts +378 -0
- package/os/spec/live-spec-types.ts +21 -1
- package/os/spec/live-spec-validate.test.ts +317 -0
- package/os/spec/live-spec-validate.ts +106 -2
- package/os/spec/spec-materialize.test.ts +192 -0
- package/os/spec/spec-materialize.ts +48 -4
- package/os/spec/spec-schema.ts +49 -0
- package/os/spec/spec-status.ts +24 -0
- package/os/spec/spec-validate.test.ts +66 -0
- package/os/spec/spec-validate.ts +62 -8
- package/os/spec/spec.module.ts +85 -8
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
- package/os/werkstatt/werkstatt.module.ts +13 -4
- package/os/workflow/workflow.module.ts +13 -5
- package/package.json +1 -1
- package/skills/_shared/fo-pipeline-conventions.md +126 -1
- package/skills/fo/fo-doc-audit/SKILL.md +4 -4
- package/skills/fo/fo-explore/SKILL.md +2 -1
- package/skills/fo/fo-fix/SKILL.md +11 -3
- package/skills/fo/fo-handoff/SKILL.md +4 -6
- package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
- package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
- package/skills/fo/fo-idea-implement/SKILL.md +53 -10
- package/skills/fo/fo-idea-plan/SKILL.md +28 -6
- package/skills/fo/fo-review/SKILL.md +69 -8
- package/skills/fo/fo-session-retro/SKILL.md +3 -2
- package/skills/fo/fo-step-commit/SKILL.md +2 -0
- package/skills/shared/grilling/SKILL.md +12 -0
- package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
- package/skills/shared/writing-great-skills/SKILL.md +2 -1
- package/src/cli-flags.ts +23 -5
- package/src/config/forge-config.ts +4 -0
- package/src/onboarding/doctor.ts +2 -3
- package/src/onboarding/init.ts +8 -3
- package/src/pipeline-status.ts +32 -8
- package/src/tests/cli-flags.test.ts +20 -0
- package/src/tests/cli-output.test.ts +13 -7
- package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
- package/src/tests/implement-stamp.test.ts +38 -0
- package/src/tests/session-handlers.test.ts +33 -0
- package/src/types.ts +13 -1
- package/src/utils/generated-marker.ts +28 -0
- package/src/validators/skill-validate.ts +2 -4
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
package/os/rfc/types.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* </non-goals>
|
|
13
13
|
* </MODULE_CONTRACT>
|
|
14
14
|
* * <CHANGE_SUMMARY>
|
|
15
|
-
<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>
|
|
16
15
|
<item>RFC-0998: added TestProbe and JsonSchemaProbe to AcceptanceProbe union for test and json-schema acceptance probe kinds.</item>
|
|
17
16
|
<item>RFC-0999: added lastRefreshedAt to VerificationEvidence; added RfcVerificationRefreshResult interface.</item>
|
|
18
17
|
<item>RFC-1097: step 6 — compass.migrate codemod run
|
|
@@ -21,7 +20,8 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
|
|
|
21
20
|
<item>RFC-1097: sweep — packages/forge + services clean
|
|
22
21
|
|
|
23
22
|
Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
|
|
24
|
-
<
|
|
23
|
+
<item>RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt</item>
|
|
24
|
+
<history>RFC-0465, RFC-0478, RFC-0480, RFC-0795</history>
|
|
25
25
|
</CHANGE_SUMMARY>
|
|
26
26
|
***************************************************************/
|
|
27
27
|
|
|
@@ -338,12 +338,25 @@ export interface VerificationEvidence {
|
|
|
338
338
|
filesModified?: string[];
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
export interface VerificationDelta {
|
|
342
|
+
/** Envelopes that flipped fail → pass since the committed version. */
|
|
343
|
+
recovered: string[];
|
|
344
|
+
/** Envelopes that flipped pass → fail since the committed version. */
|
|
345
|
+
regressed: string[];
|
|
346
|
+
}
|
|
347
|
+
|
|
341
348
|
export interface RfcVerificationEmitResult {
|
|
342
349
|
command: "rfc.verification.emit";
|
|
343
350
|
status: "pass" | "fail";
|
|
344
|
-
emitted: Array<{
|
|
351
|
+
emitted: Array<{
|
|
352
|
+
rfcId: string;
|
|
353
|
+
file: string;
|
|
354
|
+
overall: "pass" | "fail";
|
|
355
|
+
previousOverall?: "pass" | "fail";
|
|
356
|
+
}>;
|
|
345
357
|
skipped: Array<{ rfcId: string; reason: "no-probes" }>;
|
|
346
358
|
diagnostics: Diagnostic[];
|
|
359
|
+
delta?: VerificationDelta;
|
|
347
360
|
}
|
|
348
361
|
|
|
349
362
|
export interface RfcVerificationRefreshResult {
|
|
@@ -353,11 +366,13 @@ export interface RfcVerificationRefreshResult {
|
|
|
353
366
|
rfcId: string;
|
|
354
367
|
file: string;
|
|
355
368
|
overall: "pass" | "fail";
|
|
369
|
+
previousOverall?: "pass" | "fail";
|
|
356
370
|
probesTotal: number;
|
|
357
371
|
probesFailed: number;
|
|
358
372
|
}>;
|
|
359
373
|
skipped: Array<{ rfcId: string; reason: "no evidence envelope" | "not implemented" }>;
|
|
360
374
|
diagnostics: Diagnostic[];
|
|
375
|
+
delta?: VerificationDelta;
|
|
361
376
|
summary: { total: number; passed: number; failed: number; skipped: number };
|
|
362
377
|
} // ─── Validation ──────────────────────────────────────────────────────────────
|
|
363
378
|
export interface RfcValidationViolation {
|
|
@@ -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,12 @@ 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>RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt</item>
|
|
17
23
|
</CHANGE_SUMMARY>
|
|
18
24
|
*/
|
|
19
25
|
|
|
@@ -26,9 +32,8 @@ import { byteHash } from "../../src/utils/hash.ts";
|
|
|
26
32
|
|
|
27
33
|
import { runProbe } from "./acceptance.ts";
|
|
28
34
|
import { listRfcFiles, readAndParseRfc } from "./frontmatter-io.ts";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { stringify as yamlStringify } from "yaml";
|
|
35
|
+
import { generatedHeaderFields } from "../../src/utils/generated-marker.ts";
|
|
36
|
+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
32
37
|
import { RFC_DIR } from "./types.ts";
|
|
33
38
|
|
|
34
39
|
// Ambient default for helper fns without a context param — handlers override
|
|
@@ -90,6 +95,71 @@ export function byteHashHex(content: string): string {
|
|
|
90
95
|
return byteHash(content).slice(HASH_PREFIX.length);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Bounded-parallel map preserving input order — one in-flight worker per slot,
|
|
100
|
+
* results land in the same positions as their inputs. Used by the emit/refresh
|
|
101
|
+
* sweeps so `--concurrency N` overlaps probe subprocesses across RFCs.
|
|
102
|
+
*/
|
|
103
|
+
export async function mapWithConcurrency<T, R>(
|
|
104
|
+
items: readonly T[],
|
|
105
|
+
concurrency: number,
|
|
106
|
+
worker: (item: T, index: number) => Promise<R>,
|
|
107
|
+
): Promise<R[]> {
|
|
108
|
+
const results = new Array<R>(items.length);
|
|
109
|
+
let cursor = 0;
|
|
110
|
+
const lanes = Array.from({ length: Math.min(Math.max(concurrency, 1), items.length) }, () =>
|
|
111
|
+
(async () => {
|
|
112
|
+
while (cursor < items.length) {
|
|
113
|
+
const index = cursor;
|
|
114
|
+
cursor += 1;
|
|
115
|
+
results[index] = await worker(items[index]!, index);
|
|
116
|
+
}
|
|
117
|
+
})(),
|
|
118
|
+
);
|
|
119
|
+
await Promise.all(lanes);
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve the `--concurrency` flag (string flag; the kernel schema has no
|
|
125
|
+
* number kind). Garbage/NaN/non-positive degrades to 1 with a warning;
|
|
126
|
+
* clamped to 16 — probe workers spawn vitest/site-kernel children and wider
|
|
127
|
+
* fan-out starves CPU rather than speeding the sweep.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveConcurrency(flag: unknown, warn?: (message: string) => void): number {
|
|
130
|
+
if (flag === undefined) return 1;
|
|
131
|
+
const raw = Array.isArray(flag) ? flag[0] : flag;
|
|
132
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
133
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
134
|
+
warn?.(`--concurrency "${String(raw)}" is not a positive integer — falling back to 1.`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
if (parsed > 16) {
|
|
138
|
+
warn?.(
|
|
139
|
+
`--concurrency ${parsed} exceeds the 16 cap — clamped (probe workers spawn subprocesses).`,
|
|
140
|
+
);
|
|
141
|
+
return 16;
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* RFC-1237: the emit-canonical envelope serialization — structured marker keys
|
|
148
|
+
* (generatedMarker/doNotEdit/ownerCommand/editInstead/regenerateCommand) as
|
|
149
|
+
* leading YAML fields, then the envelope body. emit and refresh MUST share
|
|
150
|
+
* this serializer: refresh owns content freshness, never envelope identity
|
|
151
|
+
* (OWN-DUP-01), so refresh output is identical-by-construction.
|
|
152
|
+
*/
|
|
153
|
+
export function serializeEvidenceEnvelope(envelope: VerificationEvidence): string {
|
|
154
|
+
return `${yamlStringify({
|
|
155
|
+
...generatedHeaderFields({
|
|
156
|
+
ownerCommand: "rfc.verification.emit",
|
|
157
|
+
commandPrefix: "pnpm exec werkstatt run",
|
|
158
|
+
}),
|
|
159
|
+
...envelope,
|
|
160
|
+
})}\n`;
|
|
161
|
+
}
|
|
162
|
+
|
|
93
163
|
function normalizeProbes(probes: AcceptanceProbe[]): string {
|
|
94
164
|
return JSON.stringify(probes, Object.keys(probes[0] ?? {}).sort());
|
|
95
165
|
}
|
|
@@ -130,6 +200,10 @@ export async function runRfcVerificationEmit(
|
|
|
130
200
|
const rfcDirPath = join(workspaceRoot, RFC_DIR);
|
|
131
201
|
const targetId = input.flags["id"] as string | undefined;
|
|
132
202
|
const targetStatus = input.flags["status"] as string | undefined;
|
|
203
|
+
// RFC-1237: consumeCommonFlags strips --dry-run into context.dryRun before
|
|
204
|
+
// dispatch — the input.flags read stays for direct/programmatic callers.
|
|
205
|
+
const dryRun = context.dryRun === true || input.flags["dry-run"] === true;
|
|
206
|
+
const concurrency = resolveConcurrency(input.flags["concurrency"], (m) => logger.warn(m));
|
|
133
207
|
|
|
134
208
|
if (!targetId && !targetStatus) {
|
|
135
209
|
return {
|
|
@@ -161,22 +235,27 @@ export async function runRfcVerificationEmit(
|
|
|
161
235
|
}
|
|
162
236
|
const kernelVersion = await getKernelVersion(workspaceRoot);
|
|
163
237
|
const verificationDirAbs = join(workspaceRoot, VERIFICATION_DIR);
|
|
164
|
-
await io.mkdir(verificationDirAbs);
|
|
238
|
+
if (!dryRun) await io.mkdir(verificationDirAbs);
|
|
165
239
|
|
|
166
|
-
|
|
240
|
+
const processFile = async (
|
|
241
|
+
fileName: string,
|
|
242
|
+
): Promise<{
|
|
243
|
+
emittedEntry?: NonNullable<RfcVerificationEmitResult["emitted"]>[number];
|
|
244
|
+
skippedEntry?: NonNullable<RfcVerificationEmitResult["skipped"]>[number];
|
|
245
|
+
diagnostic?: Diagnostic;
|
|
246
|
+
}> => {
|
|
167
247
|
const parsedFile = await readAndParseRfc(rfcDirPath, fileName);
|
|
168
|
-
if (!parsedFile)
|
|
169
|
-
if ("error" in parsedFile)
|
|
248
|
+
if (!parsedFile) return {};
|
|
249
|
+
if ("error" in parsedFile) return {};
|
|
170
250
|
const fm = parsedFile.parsed.frontmatter;
|
|
171
251
|
const rfcId = String(fm["id"] ?? "");
|
|
172
252
|
|
|
173
|
-
if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase())
|
|
174
|
-
if (targetStatus && String(fm["status"] ?? "") !== targetStatus)
|
|
253
|
+
if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) return {};
|
|
254
|
+
if (targetStatus && String(fm["status"] ?? "") !== targetStatus) return {};
|
|
175
255
|
|
|
176
256
|
const acceptance = fm["acceptance"];
|
|
177
257
|
if (!Array.isArray(acceptance) || acceptance.length === 0) {
|
|
178
|
-
|
|
179
|
-
continue;
|
|
258
|
+
return { skippedEntry: { rfcId, reason: "no-probes" } };
|
|
180
259
|
}
|
|
181
260
|
|
|
182
261
|
const probes = acceptance as AcceptanceProbe[];
|
|
@@ -213,29 +292,74 @@ export async function runRfcVerificationEmit(
|
|
|
213
292
|
const evidenceFileName = `${slug}.generated.yaml`;
|
|
214
293
|
const evidenceRelPath = join(VERIFICATION_DIR, evidenceFileName);
|
|
215
294
|
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
295
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
296
|
+
// Committed-envelope baseline for the sweep delta — lets a fail-heavy run
|
|
297
|
+
// separate still-failing drift from fresh regressions without git diff.
|
|
298
|
+
let previousOverall: "pass" | "fail" | undefined;
|
|
299
|
+
try {
|
|
300
|
+
const existing = yamlParse(await io.readFile(evidenceAbsPath)) as VerificationEvidence | null;
|
|
301
|
+
if (existing && (existing.overall === "pass" || existing.overall === "fail")) {
|
|
302
|
+
previousOverall = existing.overall;
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// No committed envelope — first emission for this RFC.
|
|
228
306
|
}
|
|
229
307
|
|
|
308
|
+
// RFC-1237: port write — the recording IO adapter intercepts under kernel
|
|
309
|
+
// --dry-run even if the explicit guard regresses.
|
|
310
|
+
if (!dryRun) await io.writeFile(evidenceAbsPath, serializeEvidenceEnvelope(envelope));
|
|
311
|
+
|
|
230
312
|
if (outputFormat === "pretty") {
|
|
313
|
+
const drift =
|
|
314
|
+
previousOverall !== undefined && previousOverall !== envelope.overall
|
|
315
|
+
? `, was ${previousOverall}`
|
|
316
|
+
: "";
|
|
231
317
|
logger.info(
|
|
232
|
-
`[evidence] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes)`,
|
|
318
|
+
`[evidence] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes${drift}${dryRun ? ", dry-run" : ""})`,
|
|
233
319
|
);
|
|
234
320
|
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
emittedEntry: {
|
|
324
|
+
rfcId,
|
|
325
|
+
file: evidenceRelPath,
|
|
326
|
+
overall: envelope.overall,
|
|
327
|
+
...(previousOverall !== undefined ? { previousOverall } : {}),
|
|
328
|
+
},
|
|
329
|
+
...(envelope.overall === "fail"
|
|
330
|
+
? {
|
|
331
|
+
diagnostic: {
|
|
332
|
+
ruleId: "RFC-EVID-02",
|
|
333
|
+
severity: "error",
|
|
334
|
+
file: evidenceRelPath,
|
|
335
|
+
message: `${rfcId}: evidence overall is "fail" — ${probeRecords.filter((r) => !r.ok).length} probe(s) failed${previousOverall === "pass" ? " (regressed: was pass)" : ""}.`,
|
|
336
|
+
} satisfies Diagnostic,
|
|
337
|
+
}
|
|
338
|
+
: {}),
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const results = await mapWithConcurrency(allFiles, concurrency, processFile);
|
|
343
|
+
for (const r of results) {
|
|
344
|
+
if (r.emittedEntry) emitted.push(r.emittedEntry);
|
|
345
|
+
if (r.skippedEntry) skipped.push(r.skippedEntry);
|
|
346
|
+
if (r.diagnostic) diagnostics.push(r.diagnostic);
|
|
235
347
|
}
|
|
236
348
|
|
|
237
349
|
const hasFailures = emitted.some((e) => e.overall === "fail");
|
|
238
350
|
const status: RfcVerificationEmitResult["status"] = hasFailures ? "fail" : "pass";
|
|
351
|
+
const delta = {
|
|
352
|
+
recovered: emitted
|
|
353
|
+
.filter((e) => e.previousOverall === "fail" && e.overall === "pass")
|
|
354
|
+
.map((e) => e.rfcId),
|
|
355
|
+
regressed: emitted
|
|
356
|
+
.filter((e) => e.previousOverall === "pass" && e.overall === "fail")
|
|
357
|
+
.map((e) => e.rfcId),
|
|
358
|
+
};
|
|
359
|
+
const deltaSummary =
|
|
360
|
+
delta.recovered.length + delta.regressed.length > 0
|
|
361
|
+
? `, ${delta.regressed.length} regressed, ${delta.recovered.length} recovered`
|
|
362
|
+
: "";
|
|
239
363
|
|
|
240
364
|
return {
|
|
241
365
|
data: {
|
|
@@ -244,8 +368,9 @@ export async function runRfcVerificationEmit(
|
|
|
244
368
|
emitted,
|
|
245
369
|
skipped,
|
|
246
370
|
diagnostics,
|
|
371
|
+
delta,
|
|
247
372
|
},
|
|
248
373
|
exitCode: hasFailures ? 1 : 0,
|
|
249
|
-
summary: `rfc.verification.emit: ${emitted.length} emitted, ${skipped.length} skipped`,
|
|
374
|
+
summary: `rfc.verification.emit: ${emitted.length} emitted, ${skipped.length} skipped${deltaSummary}${dryRun ? ", dry-run" : ""}`,
|
|
250
375
|
};
|
|
251
376
|
}
|