@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Property-based test for the spec.live.merge idempotency law (RFC-1230,
|
|
4
|
+
AC-10 / DNA-101): merge(merge(spec, rfc), rfc) ≡ merge(spec, rfc) — a second
|
|
5
|
+
merge of the same RFC is already-merged and byte-identical — and
|
|
6
|
+
force∘force ≡ force — a forced re-merge stabilizes after one application.</purpose>
|
|
7
|
+
<non-goals>
|
|
8
|
+
<item>Do not test rebuild or validate — covered by their own test files.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>RFC-1230: initial PBT for the merge idempotency law.</item>
|
|
13
|
+
<item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, test, expect } from "vitest";
|
|
18
|
+
import fs from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import fc from "fast-check";
|
|
22
|
+
import { runSpecLiveMerge } from "./live-spec-merge.ts";
|
|
23
|
+
import { escapeRegex } from "./live-spec-shared.ts";
|
|
24
|
+
import type { ForgeRuntimeContext } from "../../src/types.ts";
|
|
25
|
+
|
|
26
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
27
|
+
return {
|
|
28
|
+
workspaceRoot,
|
|
29
|
+
logger: {
|
|
30
|
+
section: () => {},
|
|
31
|
+
info: () => {},
|
|
32
|
+
warn: () => {},
|
|
33
|
+
error: () => {},
|
|
34
|
+
success: () => {},
|
|
35
|
+
},
|
|
36
|
+
dryRun: false,
|
|
37
|
+
outputFormat: "json",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Heading text: single-line, non-empty after trim, never carrying an (RFC-XXXX)
|
|
42
|
+
// marker — namespaceHeadings leaves already-marked headings alone, which would
|
|
43
|
+
// make sections belong to a different RFC id.
|
|
44
|
+
const headingTextArb = fc
|
|
45
|
+
.string({
|
|
46
|
+
unit: fc.constantFrom(
|
|
47
|
+
..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_".split(""),
|
|
48
|
+
),
|
|
49
|
+
minLength: 1,
|
|
50
|
+
maxLength: 40,
|
|
51
|
+
})
|
|
52
|
+
.filter((s) => s.trim().length > 0 && !/\(RFC-\d{4}\)/.test(s));
|
|
53
|
+
|
|
54
|
+
const bodyLineArb = fc
|
|
55
|
+
.string({
|
|
56
|
+
unit: fc.constantFrom(
|
|
57
|
+
..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_.,;:!?()[]".split(""),
|
|
58
|
+
),
|
|
59
|
+
maxLength: 80,
|
|
60
|
+
})
|
|
61
|
+
.filter((s) => !s.trimStart().startsWith("#"));
|
|
62
|
+
|
|
63
|
+
const sectionArb = fc.record({
|
|
64
|
+
heading: headingTextArb,
|
|
65
|
+
body: fc.array(bodyLineArb, { maxLength: 6 }).map((lines) => lines.join("\n")),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function rfcSource(id: string, design: string): string {
|
|
69
|
+
return `---
|
|
70
|
+
id: ${id}
|
|
71
|
+
title: "PBT RFC ${id}"
|
|
72
|
+
status: implemented
|
|
73
|
+
kind: architecture
|
|
74
|
+
scope: workspace
|
|
75
|
+
owners:
|
|
76
|
+
- architecture
|
|
77
|
+
reviewers:
|
|
78
|
+
- human:test
|
|
79
|
+
createdAt: 2026-08-06
|
|
80
|
+
updatedAt: 2026-08-06
|
|
81
|
+
implementedAt: 2026-08-06
|
|
82
|
+
versionBump: patch
|
|
83
|
+
liveSpec: true
|
|
84
|
+
packagesImpacted:
|
|
85
|
+
- packages/forge
|
|
86
|
+
commands:
|
|
87
|
+
proposed: []
|
|
88
|
+
added: []
|
|
89
|
+
changed: []
|
|
90
|
+
removed: []
|
|
91
|
+
appsImpacted: []
|
|
92
|
+
successSignals: []
|
|
93
|
+
nonGoals: []
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# ${id}
|
|
97
|
+
|
|
98
|
+
## Design
|
|
99
|
+
|
|
100
|
+
${design}
|
|
101
|
+
|
|
102
|
+
## Rollout
|
|
103
|
+
|
|
104
|
+
Done.
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("spec.live.merge idempotency law (AC-10)", () => {
|
|
109
|
+
test("merge(merge(spec, rfc), rfc) ≡ merge(spec, rfc)", async () => {
|
|
110
|
+
await fc.assert(
|
|
111
|
+
fc.asyncProperty(
|
|
112
|
+
fc.array(sectionArb, { minLength: 1, maxLength: 6 }),
|
|
113
|
+
fc.boolean(), // seed a spec via a different RFC first → exercise the existing-spec path
|
|
114
|
+
async (sections, seedWithOther) => {
|
|
115
|
+
const design = sections
|
|
116
|
+
.map((s) => `### ${s.heading}\n\n${s.body}`)
|
|
117
|
+
.join("\n\n");
|
|
118
|
+
|
|
119
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-pbt-"));
|
|
120
|
+
try {
|
|
121
|
+
const rfcDir = path.join(tmpDir, "docs/rfcs");
|
|
122
|
+
await fs.mkdir(rfcDir, { recursive: true });
|
|
123
|
+
await fs.writeFile(
|
|
124
|
+
path.join(rfcDir, "rfc-9001-pbt.md"),
|
|
125
|
+
rfcSource("RFC-9001", design),
|
|
126
|
+
);
|
|
127
|
+
if (seedWithOther) {
|
|
128
|
+
await fs.writeFile(
|
|
129
|
+
path.join(rfcDir, "rfc-9002-seed.md"),
|
|
130
|
+
rfcSource("RFC-9002", "### Seed\n\nseed body."),
|
|
131
|
+
);
|
|
132
|
+
await runSpecLiveMerge(
|
|
133
|
+
{ argv: [], flags: { id: "RFC-9002" } },
|
|
134
|
+
makeContext(tmpDir),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const mergeInput = { argv: [], flags: { id: "RFC-9001" } };
|
|
139
|
+
const first = await runSpecLiveMerge(mergeInput, makeContext(tmpDir));
|
|
140
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
141
|
+
const afterFirst = await fs.readFile(specFile, "utf-8");
|
|
142
|
+
|
|
143
|
+
const second = await runSpecLiveMerge(mergeInput, makeContext(tmpDir));
|
|
144
|
+
const afterSecond = await fs.readFile(specFile, "utf-8");
|
|
145
|
+
|
|
146
|
+
expect(first.exitCode).toBe(0);
|
|
147
|
+
expect(second.exitCode).toBe(0);
|
|
148
|
+
expect(second.data?.operation).toBe("already-merged");
|
|
149
|
+
expect(second.data?.deltas).toEqual([]);
|
|
150
|
+
expect(afterSecond).toBe(afterFirst);
|
|
151
|
+
} finally {
|
|
152
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
{ numRuns: 30 },
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("force(force(spec)) ≡ force(spec) — forced re-merge stabilizes", async () => {
|
|
161
|
+
await fc.assert(
|
|
162
|
+
fc.asyncProperty(
|
|
163
|
+
fc.array(sectionArb, { minLength: 1, maxLength: 6 }),
|
|
164
|
+
async (sections) => {
|
|
165
|
+
const design = sections
|
|
166
|
+
.map((s) => `### ${s.heading}\n\n${s.body}`)
|
|
167
|
+
.join("\n\n");
|
|
168
|
+
|
|
169
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-pbt-"));
|
|
170
|
+
try {
|
|
171
|
+
const rfcDir = path.join(tmpDir, "docs/rfcs");
|
|
172
|
+
await fs.mkdir(rfcDir, { recursive: true });
|
|
173
|
+
await fs.writeFile(
|
|
174
|
+
path.join(rfcDir, "rfc-9001-pbt.md"),
|
|
175
|
+
rfcSource("RFC-9001", design),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await runSpecLiveMerge(
|
|
179
|
+
{ argv: [], flags: { id: "RFC-9001" } },
|
|
180
|
+
makeContext(tmpDir),
|
|
181
|
+
);
|
|
182
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
183
|
+
|
|
184
|
+
const forceInput = {
|
|
185
|
+
argv: [],
|
|
186
|
+
flags: { id: "RFC-9001", force: true },
|
|
187
|
+
};
|
|
188
|
+
const force1 = await runSpecLiveMerge(forceInput, makeContext(tmpDir));
|
|
189
|
+
const afterForce1 = await fs.readFile(specFile, "utf-8");
|
|
190
|
+
|
|
191
|
+
const force2 = await runSpecLiveMerge(forceInput, makeContext(tmpDir));
|
|
192
|
+
const afterForce2 = await fs.readFile(specFile, "utf-8");
|
|
193
|
+
|
|
194
|
+
expect(force1.exitCode).toBe(0);
|
|
195
|
+
expect(force2.exitCode).toBe(0);
|
|
196
|
+
expect(force1.data?.operation).toBe("modified");
|
|
197
|
+
expect(force2.data?.operation).toBe("modified");
|
|
198
|
+
// Exactly one namespaced section set per heading and one history entry.
|
|
199
|
+
expect(afterForce2).toBe(afterForce1);
|
|
200
|
+
// Each namespaced heading occurs exactly as often as the RFC's
|
|
201
|
+
// design repeats that heading text (headings are trimmed on parse).
|
|
202
|
+
// Count heading lines — substring matching would let "a (RFC-9001)"
|
|
203
|
+
// match inside "aa (RFC-9001)".
|
|
204
|
+
for (const s of sections) {
|
|
205
|
+
const namespaced = `${s.heading.trim()} (RFC-9001)`;
|
|
206
|
+
const occurrences = (afterForce2.match(
|
|
207
|
+
new RegExp(`^### ${escapeRegex(namespaced)}$`, "gm"),
|
|
208
|
+
) ?? []).length;
|
|
209
|
+
const expected = sections.filter(
|
|
210
|
+
(x) => x.heading.trim() === s.heading.trim(),
|
|
211
|
+
).length;
|
|
212
|
+
expect(occurrences).toBe(expected);
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
),
|
|
219
|
+
{ numRuns: 30 },
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
</MODULE_CONTRACT>
|
|
8
8
|
<CHANGE_SUMMARY>
|
|
9
9
|
<item>RFC-0711: initial unit tests for spec.live.list and spec.live.validate.</item>
|
|
10
|
+
<item>RFC-1234: zero-violations fixture now produced by runSpecLiveMerge so the spec is byte-canonical under the V-LS-09 replay check; RFC-9001 fixture gained liveSpec: true.</item>
|
|
10
11
|
</CHANGE_SUMMARY>
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -17,6 +18,7 @@ import os from "node:os";
|
|
|
17
18
|
import { runSpecLiveList } from "./live-spec-list.ts";
|
|
18
19
|
import { runSpecLiveShow } from "./live-spec-show.ts";
|
|
19
20
|
import { runSpecLiveValidate } from "./live-spec-validate.ts";
|
|
21
|
+
import { runSpecLiveMerge } from "./live-spec-merge.ts";
|
|
20
22
|
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../src/types.ts";
|
|
21
23
|
|
|
22
24
|
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
@@ -157,7 +159,9 @@ createdAt: 2026-08-06
|
|
|
157
159
|
updatedAt: 2026-08-06
|
|
158
160
|
implementedAt: 2026-08-06
|
|
159
161
|
versionBump: patch
|
|
160
|
-
|
|
162
|
+
liveSpec: true
|
|
163
|
+
packagesImpacted:
|
|
164
|
+
- packages/forge
|
|
161
165
|
commands:
|
|
162
166
|
proposed: []
|
|
163
167
|
added: []
|
|
@@ -184,13 +188,13 @@ Content.
|
|
|
184
188
|
});
|
|
185
189
|
|
|
186
190
|
it("passes with zero violations on a valid living spec", async () => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
+
// V-LS-09 (RFC-1234) byte-compares against the canonical replay — the
|
|
192
|
+
// fixture spec is produced by the real merge writer, not hand-written.
|
|
193
|
+
const ctx = makeContext(tmpDir);
|
|
194
|
+
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, ctx);
|
|
191
195
|
|
|
192
196
|
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
193
|
-
const result = await runSpecLiveValidate(input,
|
|
197
|
+
const result = await runSpecLiveValidate(input, ctx);
|
|
194
198
|
|
|
195
199
|
expect(result.exitCode).toBe(0);
|
|
196
200
|
expect(result.data?.status).toBe("pass");
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
3
|
<purpose>Unit tests for spec.live.merge handler — covers creation, modification,
|
|
4
|
-
conflict detection, dry-run,
|
|
4
|
+
idempotent already-merged skip, --force re-merge, conflict detection, dry-run,
|
|
5
|
+
no-op cases (RFC-0711, RFC-1230).</purpose>
|
|
5
6
|
<non-goals>
|
|
6
7
|
<item>Do not test docs.archive integration — covered separately.</item>
|
|
8
|
+
<item>Do not test spec.live.rebuild — covered by live-spec-rebuild.test.ts.</item>
|
|
7
9
|
</non-goals>
|
|
8
10
|
</MODULE_CONTRACT>
|
|
9
11
|
<CHANGE_SUMMARY>
|
|
10
12
|
<item>RFC-0711: initial unit tests for spec.live.merge.</item>
|
|
13
|
+
<item>RFC-1230: updated re-merge expectations to already-merged; added --force surgical re-merge and byte-identity cases.</item>
|
|
14
|
+
<item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
|
|
11
15
|
</CHANGE_SUMMARY>
|
|
12
16
|
*/
|
|
13
17
|
|
|
@@ -201,6 +205,57 @@ Different types from RFC-9004.
|
|
|
201
205
|
Some rollout.
|
|
202
206
|
`;
|
|
203
207
|
|
|
208
|
+
const SAMPLE_RFC_FENCED = `---
|
|
209
|
+
id: RFC-9005
|
|
210
|
+
title: "RFC with fenced heading-like lines"
|
|
211
|
+
status: implemented
|
|
212
|
+
kind: architecture
|
|
213
|
+
scope: workspace
|
|
214
|
+
owners:
|
|
215
|
+
- architecture
|
|
216
|
+
reviewers:
|
|
217
|
+
- human:test
|
|
218
|
+
createdAt: 2026-08-06
|
|
219
|
+
updatedAt: 2026-08-06
|
|
220
|
+
implementedAt: 2026-08-06
|
|
221
|
+
versionBump: patch
|
|
222
|
+
liveSpec: true
|
|
223
|
+
packagesImpacted:
|
|
224
|
+
- packages/forge
|
|
225
|
+
commands:
|
|
226
|
+
proposed: []
|
|
227
|
+
added: []
|
|
228
|
+
changed: []
|
|
229
|
+
removed: []
|
|
230
|
+
appsImpacted: []
|
|
231
|
+
successSignals: []
|
|
232
|
+
nonGoals: []
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
# RFC-9005: RFC with fenced heading-like lines
|
|
236
|
+
|
|
237
|
+
## Design
|
|
238
|
+
|
|
239
|
+
### CLI surface
|
|
240
|
+
|
|
241
|
+
\`\`\`sh
|
|
242
|
+
### this is a shell comment, not a heading
|
|
243
|
+
werkstatt run spec.live.merge --id RFC-9005
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
### TypeScript contracts
|
|
247
|
+
|
|
248
|
+
~~~text
|
|
249
|
+
### markdown-looking line inside tilde fence
|
|
250
|
+
~~~
|
|
251
|
+
|
|
252
|
+
Real text.
|
|
253
|
+
|
|
254
|
+
## Rollout
|
|
255
|
+
|
|
256
|
+
Done.
|
|
257
|
+
`;
|
|
258
|
+
|
|
204
259
|
async function setupWorkspace(): Promise<string> {
|
|
205
260
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-test-"));
|
|
206
261
|
const rfcDir = path.join(tmpDir, "docs/rfcs");
|
|
@@ -209,6 +264,7 @@ async function setupWorkspace(): Promise<string> {
|
|
|
209
264
|
await fs.writeFile(path.join(rfcDir, "rfc-9002-test-rfc-without-livespec.md"), SAMPLE_RFC_NO_LIVE_SPEC);
|
|
210
265
|
await fs.writeFile(path.join(rfcDir, "rfc-9003-rejected-rfc-with-livespec.md"), SAMPLE_RFC_REJECTED);
|
|
211
266
|
await fs.writeFile(path.join(rfcDir, "rfc-9004-second-rfc-with-same-headings.md"), SAMPLE_RFC_9004);
|
|
267
|
+
await fs.writeFile(path.join(rfcDir, "rfc-9005-rfc-with-fenced-heading-like-lines.md"), SAMPLE_RFC_FENCED);
|
|
212
268
|
return tmpDir;
|
|
213
269
|
}
|
|
214
270
|
|
|
@@ -243,18 +299,61 @@ describe("spec.live.merge", () => {
|
|
|
243
299
|
expect(content).toContain("lastMergedRfc: RFC-9001");
|
|
244
300
|
});
|
|
245
301
|
|
|
246
|
-
it("
|
|
302
|
+
it("skips an already-merged RFC and leaves the spec byte-identical (AC-1)", async () => {
|
|
247
303
|
// First merge creates the spec
|
|
248
304
|
const input1: ForgeCommandInput = { argv: [], flags: { id: "RFC-9001" } };
|
|
249
305
|
await runSpecLiveMerge(input1, makeContext(tmpDir));
|
|
250
306
|
|
|
251
|
-
|
|
307
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
308
|
+
const before = await fs.readFile(specFile, "utf-8");
|
|
309
|
+
|
|
310
|
+
// Second merge of the same RFC is a no-op
|
|
252
311
|
const input2: ForgeCommandInput = { argv: [], flags: { id: "RFC-9001" } };
|
|
253
312
|
const result = await runSpecLiveMerge(input2, makeContext(tmpDir));
|
|
254
313
|
|
|
255
314
|
expect(result.exitCode).toBe(0);
|
|
256
|
-
expect(result.data?.operation).toBe("
|
|
315
|
+
expect(result.data?.operation).toBe("already-merged");
|
|
257
316
|
expect(result.data?.domain).toBe("forge");
|
|
317
|
+
expect(result.data?.deltas).toEqual([]);
|
|
318
|
+
|
|
319
|
+
const after = await fs.readFile(specFile, "utf-8");
|
|
320
|
+
expect(after).toBe(before);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("re-merges an already-merged RFC surgically with --force (AC-2)", async () => {
|
|
324
|
+
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
|
|
325
|
+
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9004" } }, makeContext(tmpDir));
|
|
326
|
+
|
|
327
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
328
|
+
|
|
329
|
+
// Simulate prior corruption: a duplicated RFC-9001 section + history entry.
|
|
330
|
+
const corrupted = (await fs.readFile(specFile, "utf-8"))
|
|
331
|
+
.replace(
|
|
332
|
+
"### TypeScript contracts (RFC-9001)\n\nSome types.",
|
|
333
|
+
"### TypeScript contracts (RFC-9001)\n\nSome types.\n\n### CLI surface (RFC-9001)\n\nDuplicated stale section.",
|
|
334
|
+
)
|
|
335
|
+
.replace(
|
|
336
|
+
" - rfc: RFC-9004",
|
|
337
|
+
" - rfc: RFC-9001\n mergedAt: 2026-08-07\n operation: modified\n - rfc: RFC-9004",
|
|
338
|
+
);
|
|
339
|
+
await fs.writeFile(specFile, corrupted);
|
|
340
|
+
|
|
341
|
+
const result = await runSpecLiveMerge(
|
|
342
|
+
{ argv: [], flags: { id: "RFC-9001", force: true } },
|
|
343
|
+
makeContext(tmpDir),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(result.exitCode).toBe(0);
|
|
347
|
+
expect(result.data?.operation).toBe("modified");
|
|
348
|
+
|
|
349
|
+
const after = await fs.readFile(specFile, "utf-8");
|
|
350
|
+
// Exactly one section set for RFC-9001 remains; stale duplicate is gone.
|
|
351
|
+
expect(after.match(/\(RFC-9001\)/g)?.length).toBe(3);
|
|
352
|
+
expect(after).not.toContain("Duplicated stale section.");
|
|
353
|
+
// Exactly one history entry for RFC-9001.
|
|
354
|
+
expect(after.match(/rfc: RFC-9001/g)?.length).toBe(1);
|
|
355
|
+
// RFC-9004 sections untouched.
|
|
356
|
+
expect(after).toContain("CLI surface (RFC-9004)");
|
|
258
357
|
});
|
|
259
358
|
|
|
260
359
|
it("skips RFCs without liveSpec field (no-op)", async () => {
|
|
@@ -323,15 +422,96 @@ describe("spec.live.merge", () => {
|
|
|
323
422
|
expect(content).toContain("CLI surface (RFC-9004)");
|
|
324
423
|
});
|
|
325
424
|
|
|
326
|
-
it("
|
|
425
|
+
it("reports already-merged on re-run instead of duplicating sections (RFC-1230)", async () => {
|
|
327
426
|
// Initial merges
|
|
328
427
|
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
|
|
329
428
|
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9004" } }, makeContext(tmpDir));
|
|
330
429
|
|
|
331
|
-
// Re-merge RFC-9001 (simulates
|
|
430
|
+
// Re-merge RFC-9001 (simulates a repeated caller) — idempotent no-op
|
|
332
431
|
const result = await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
|
|
333
432
|
|
|
334
433
|
expect(result.exitCode).toBe(0);
|
|
335
|
-
expect(result.data?.
|
|
434
|
+
expect(result.data?.operation).toBe("already-merged");
|
|
435
|
+
expect(result.data?.deltas).toEqual([]);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("writes nothing under --dry-run on an existing spec (AC-9)", async () => {
|
|
439
|
+
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
|
|
440
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
441
|
+
const before = await fs.readFile(specFile, "utf-8");
|
|
442
|
+
|
|
443
|
+
const ctx = makeContext(tmpDir);
|
|
444
|
+
ctx.dryRun = true;
|
|
445
|
+
const result = await runSpecLiveMerge(
|
|
446
|
+
{ argv: [], flags: { id: "RFC-9001", force: true, "dry-run": true } },
|
|
447
|
+
ctx,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
expect(result.data?.dryRun).toBe(true);
|
|
451
|
+
const after = await fs.readFile(specFile, "utf-8");
|
|
452
|
+
expect(after).toBe(before);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("does not namespace-rewrite ### lines inside ``` and ~~~ fences (RFC-1232 AC-1/AC-5)", async () => {
|
|
456
|
+
const result = await runSpecLiveMerge(
|
|
457
|
+
{ argv: [], flags: { id: "RFC-9005" } },
|
|
458
|
+
makeContext(tmpDir),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(result.exitCode).toBe(0);
|
|
462
|
+
|
|
463
|
+
const content = await fs.readFile(path.join(tmpDir, "docs/specs/live/forge.md"), "utf-8");
|
|
464
|
+
// Fence-interior ### lines pass through verbatim — no namespacing suffix.
|
|
465
|
+
expect(content).toContain("### this is a shell comment, not a heading\n");
|
|
466
|
+
expect(content).toContain("### markdown-looking line inside tilde fence\n");
|
|
467
|
+
expect(content).not.toContain("shell comment, not a heading (RFC-9005)");
|
|
468
|
+
expect(content).not.toContain("inside tilde fence (RFC-9005)");
|
|
469
|
+
// Real headings still get namespaced.
|
|
470
|
+
expect(content).toContain("### CLI surface (RFC-9005)");
|
|
471
|
+
expect(content).toContain("### TypeScript contracts (RFC-9005)");
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("--force re-merge preserves fence-interior ### (RFC-NNNN) lines outside the RFC's sections", async () => {
|
|
475
|
+
await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9005" } }, makeContext(tmpDir));
|
|
476
|
+
|
|
477
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
478
|
+
const before = await fs.readFile(specFile, "utf-8");
|
|
479
|
+
// A fenced block carrying a phantom namespaced heading inside Overview —
|
|
480
|
+
// the pre-RFC-1232 scanner would slice from this line to the next real
|
|
481
|
+
// heading, deleting Overview tail content and leaving a stray fence.
|
|
482
|
+
await fs.writeFile(
|
|
483
|
+
specFile,
|
|
484
|
+
before.replace(
|
|
485
|
+
"## Overview\n",
|
|
486
|
+
"## Overview\n\n```md\n### Phantom (RFC-9005)\n```\n",
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const result = await runSpecLiveMerge(
|
|
491
|
+
{ argv: [], flags: { id: "RFC-9005", force: true } },
|
|
492
|
+
makeContext(tmpDir),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(result.exitCode).toBe(0);
|
|
496
|
+
const after = await fs.readFile(specFile, "utf-8");
|
|
497
|
+
expect(after).toContain("### Phantom (RFC-9005)");
|
|
498
|
+
expect(after.match(/### CLI surface \(RFC-9005\)/g)?.length).toBe(1);
|
|
499
|
+
expect(after.match(/### TypeScript contracts \(RFC-9005\)/g)?.length).toBe(1);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("fails instead of overwriting a spec with unparseable frontmatter", async () => {
|
|
503
|
+
const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
|
|
504
|
+
await fs.mkdir(path.dirname(specFile), { recursive: true });
|
|
505
|
+
const corrupt = "# Living Spec: forge\n\nno frontmatter — orphaned content\n";
|
|
506
|
+
await fs.writeFile(specFile, corrupt);
|
|
507
|
+
|
|
508
|
+
const result = await runSpecLiveMerge(
|
|
509
|
+
{ argv: [], flags: { id: "RFC-9001" } },
|
|
510
|
+
makeContext(tmpDir),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(result.exitCode).toBe(1);
|
|
514
|
+
expect(result.summary).toContain("valid frontmatter");
|
|
515
|
+
expect(await fs.readFile(specFile, "utf-8")).toBe(corrupt);
|
|
336
516
|
});
|
|
337
517
|
});
|