@warpgogol/forge 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
validateSingleRfc,
|
|
4
|
+
collectMarkers,
|
|
5
|
+
checkFrontmatterYamlParse,
|
|
6
|
+
type AddViolationFn,
|
|
7
|
+
} from "./validate-rules.ts";
|
|
8
|
+
import type { ParsedRfc } from "../frontmatter-io.ts";
|
|
9
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
|
|
14
|
+
const testWorkspace = join(tmpdir(), "test-workspace");
|
|
15
|
+
|
|
16
|
+
function makeParsed(
|
|
17
|
+
status: string,
|
|
18
|
+
body: string,
|
|
19
|
+
extraFm: Record<string, unknown> = {},
|
|
20
|
+
): ParsedRfc {
|
|
21
|
+
return {
|
|
22
|
+
frontmatter: {
|
|
23
|
+
id: "RFC-9999",
|
|
24
|
+
title: "Test RFC",
|
|
25
|
+
status,
|
|
26
|
+
kind: "policy",
|
|
27
|
+
scope: "workspace",
|
|
28
|
+
owners: ["architecture"],
|
|
29
|
+
createdAt: "2026-01-01",
|
|
30
|
+
updatedAt: "2026-01-01",
|
|
31
|
+
...extraFm,
|
|
32
|
+
},
|
|
33
|
+
body,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const BASE_BODY = `
|
|
38
|
+
# RFC-9999: Test RFC
|
|
39
|
+
|
|
40
|
+
## Context
|
|
41
|
+
|
|
42
|
+
Test context.
|
|
43
|
+
|
|
44
|
+
## Problem
|
|
45
|
+
|
|
46
|
+
Test problem.
|
|
47
|
+
|
|
48
|
+
## Decision
|
|
49
|
+
|
|
50
|
+
Test decision.
|
|
51
|
+
|
|
52
|
+
## Architectural fit
|
|
53
|
+
|
|
54
|
+
Test fit.
|
|
55
|
+
|
|
56
|
+
## Design
|
|
57
|
+
|
|
58
|
+
### CLI surface
|
|
59
|
+
|
|
60
|
+
Test CLI.
|
|
61
|
+
|
|
62
|
+
### TypeScript contracts
|
|
63
|
+
|
|
64
|
+
Test types.
|
|
65
|
+
|
|
66
|
+
### File system responsibilities
|
|
67
|
+
|
|
68
|
+
| Path | Role |
|
|
69
|
+
|---|---|
|
|
70
|
+
| \`test.ts\` | test |
|
|
71
|
+
|
|
72
|
+
### Output format
|
|
73
|
+
|
|
74
|
+
Test output.
|
|
75
|
+
|
|
76
|
+
### Failure modes
|
|
77
|
+
|
|
78
|
+
Test failures.
|
|
79
|
+
|
|
80
|
+
## Rollout
|
|
81
|
+
|
|
82
|
+
Test rollout.
|
|
83
|
+
|
|
84
|
+
## Alternatives considered
|
|
85
|
+
|
|
86
|
+
Test alternatives.
|
|
87
|
+
|
|
88
|
+
## Risks
|
|
89
|
+
|
|
90
|
+
Test risks.
|
|
91
|
+
|
|
92
|
+
## Acceptance criteria
|
|
93
|
+
|
|
94
|
+
ACCEPTANCE_HERE
|
|
95
|
+
|
|
96
|
+
## Implementation notes for agents
|
|
97
|
+
|
|
98
|
+
Test notes.
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
function makeViolationsCollector(): {
|
|
102
|
+
add: AddViolationFn;
|
|
103
|
+
violations: { rfcId: string; rule: string; message: string; severity: string }[];
|
|
104
|
+
} {
|
|
105
|
+
const violations: { rfcId: string; rule: string; message: string; severity: string }[] = [];
|
|
106
|
+
const add: AddViolationFn = (rfcId, _file, rule, message, severity = "error") => {
|
|
107
|
+
violations.push({ rfcId, rule, message, severity });
|
|
108
|
+
};
|
|
109
|
+
return { add, violations };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function runValidate(
|
|
113
|
+
parsed: ParsedRfc,
|
|
114
|
+
): Promise<{ rfcId: string; rule: string; message: string; severity: string }[]> {
|
|
115
|
+
const { add, violations } = makeViolationsCollector();
|
|
116
|
+
await validateSingleRfc(
|
|
117
|
+
"rfc-9999-test.md",
|
|
118
|
+
parsed,
|
|
119
|
+
new Map(),
|
|
120
|
+
new Map(),
|
|
121
|
+
new Set(),
|
|
122
|
+
new Set(),
|
|
123
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
124
|
+
testWorkspace,
|
|
125
|
+
add,
|
|
126
|
+
);
|
|
127
|
+
return violations;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function filterRule(
|
|
131
|
+
violations: { rfcId: string; rule: string; message: string; severity: string }[],
|
|
132
|
+
rule: string,
|
|
133
|
+
): { rfcId: string; rule: string; message: string; severity: string }[] {
|
|
134
|
+
return violations.filter((v) => v.rule === rule);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("V-26: implemented RFCs must have all acceptance criteria checked", () => {
|
|
138
|
+
test("error when implemented with unchecked criteria", async () => {
|
|
139
|
+
const body = BASE_BODY.replace(
|
|
140
|
+
"ACCEPTANCE_HERE",
|
|
141
|
+
"- [x] done criterion (evidence: test.ts:1)\n- [ ] undone criterion\n- [x] another done (evidence: test.ts:2)",
|
|
142
|
+
);
|
|
143
|
+
const parsed = makeParsed("implemented", body);
|
|
144
|
+
const violations = await runValidate(parsed);
|
|
145
|
+
const v26 = filterRule(violations, "V-26");
|
|
146
|
+
expect(v26).toHaveLength(1);
|
|
147
|
+
expect(v26[0]!.message).toContain("1 acceptance criteria are unchecked");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("no V-26 error when implemented with all checked", async () => {
|
|
151
|
+
const body = BASE_BODY.replace(
|
|
152
|
+
"ACCEPTANCE_HERE",
|
|
153
|
+
"- [x] done (evidence: test.ts:1)\n- [x] another (evidence: test.ts:2)\n- [x] third (evidence: test.ts:3)",
|
|
154
|
+
);
|
|
155
|
+
const parsed = makeParsed("implemented", body);
|
|
156
|
+
const violations = await runValidate(parsed);
|
|
157
|
+
const v26 = filterRule(violations, "V-26");
|
|
158
|
+
expect(v26).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("no V-26 error when accepted with unchecked criteria", async () => {
|
|
162
|
+
const body = BASE_BODY.replace(
|
|
163
|
+
"ACCEPTANCE_HERE",
|
|
164
|
+
"- [x] done (evidence: test.ts:1)\n- [ ] undone\n- [x] another (evidence: test.ts:2)",
|
|
165
|
+
);
|
|
166
|
+
const parsed = makeParsed("accepted", body);
|
|
167
|
+
const violations = await runValidate(parsed);
|
|
168
|
+
const v26 = filterRule(violations, "V-26");
|
|
169
|
+
expect(v26).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("no V-26 error when draft with unchecked criteria", async () => {
|
|
173
|
+
const body = BASE_BODY.replace(
|
|
174
|
+
"ACCEPTANCE_HERE",
|
|
175
|
+
"- [x] done (evidence: test.ts:1)\n- [ ] undone\n- [x] another (evidence: test.ts:2)",
|
|
176
|
+
);
|
|
177
|
+
const parsed = makeParsed("draft", body);
|
|
178
|
+
const violations = await runValidate(parsed);
|
|
179
|
+
const v26 = filterRule(violations, "V-26");
|
|
180
|
+
expect(v26).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("V-27: checked criteria must carry inline evidence", () => {
|
|
185
|
+
test("no error when [x] has evidence", async () => {
|
|
186
|
+
const body = BASE_BODY.replace(
|
|
187
|
+
"ACCEPTANCE_HERE",
|
|
188
|
+
"- [x] done (evidence: test.ts:1)\n- [x] another (evidence: test.ts:2)\n- [x] third (evidence: test.ts:3)",
|
|
189
|
+
);
|
|
190
|
+
const parsed = makeParsed("accepted", body);
|
|
191
|
+
const violations = await runValidate(parsed);
|
|
192
|
+
const v27 = filterRule(violations, "V-27");
|
|
193
|
+
expect(v27).toHaveLength(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("error when [x] lacks evidence", async () => {
|
|
197
|
+
const body = BASE_BODY.replace(
|
|
198
|
+
"ACCEPTANCE_HERE",
|
|
199
|
+
"- [x] done without evidence\n- [x] another (evidence: test.ts:2)\n- [x] third (evidence: test.ts:3)",
|
|
200
|
+
);
|
|
201
|
+
const parsed = makeParsed("accepted", body);
|
|
202
|
+
const violations = await runValidate(parsed);
|
|
203
|
+
const v27 = filterRule(violations, "V-27");
|
|
204
|
+
expect(v27).toHaveLength(1);
|
|
205
|
+
expect(v27[0]!.message).toContain("done without evidence");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("no V-27 error for unchecked [ ] without evidence", async () => {
|
|
209
|
+
const body = BASE_BODY.replace(
|
|
210
|
+
"ACCEPTANCE_HERE",
|
|
211
|
+
"- [ ] unchecked without evidence\n- [x] done (evidence: test.ts:2)\n- [x] third (evidence: test.ts:3)",
|
|
212
|
+
);
|
|
213
|
+
const parsed = makeParsed("accepted", body);
|
|
214
|
+
const violations = await runValidate(parsed);
|
|
215
|
+
const v27 = filterRule(violations, "V-27");
|
|
216
|
+
expect(v27).toHaveLength(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("no V-27 error for indented sub-items without evidence", async () => {
|
|
220
|
+
const body = BASE_BODY.replace(
|
|
221
|
+
"ACCEPTANCE_HERE",
|
|
222
|
+
"- [x] parent (evidence: test.ts:1)\n - [x] sub-item without evidence\n- [x] another (evidence: test.ts:3)",
|
|
223
|
+
);
|
|
224
|
+
const parsed = makeParsed("accepted", body);
|
|
225
|
+
const violations = await runValidate(parsed);
|
|
226
|
+
const v27 = filterRule(violations, "V-27");
|
|
227
|
+
expect(v27).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("V-31: filename-number uniqueness and filename/id consistency", () => {
|
|
232
|
+
test("no V-31 error when filename number matches frontmatter id", async () => {
|
|
233
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
234
|
+
const { add, violations } = makeViolationsCollector();
|
|
235
|
+
const seenFilenameNumbers = new Map<number, string>();
|
|
236
|
+
await validateSingleRfc(
|
|
237
|
+
"rfc-9999-test.md",
|
|
238
|
+
parsed,
|
|
239
|
+
new Map(),
|
|
240
|
+
new Map(),
|
|
241
|
+
new Set(),
|
|
242
|
+
new Set(),
|
|
243
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
244
|
+
testWorkspace,
|
|
245
|
+
add,
|
|
246
|
+
seenFilenameNumbers,
|
|
247
|
+
);
|
|
248
|
+
const v31 = filterRule(violations, "V-31");
|
|
249
|
+
expect(v31).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("V-31 error when filename number does not match frontmatter id", async () => {
|
|
253
|
+
const parsed = makeParsed("accepted", BASE_BODY, { id: "RFC-0488" });
|
|
254
|
+
const { add, violations } = makeViolationsCollector();
|
|
255
|
+
const seenFilenameNumbers = new Map<number, string>();
|
|
256
|
+
await validateSingleRfc(
|
|
257
|
+
"rfc-0490-foo.md",
|
|
258
|
+
parsed,
|
|
259
|
+
new Map(),
|
|
260
|
+
new Map(),
|
|
261
|
+
new Set(),
|
|
262
|
+
new Set(),
|
|
263
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
264
|
+
testWorkspace,
|
|
265
|
+
add,
|
|
266
|
+
seenFilenameNumbers,
|
|
267
|
+
);
|
|
268
|
+
const v31 = filterRule(violations, "V-31");
|
|
269
|
+
expect(v31).toHaveLength(1);
|
|
270
|
+
expect(v31[0]!.message).toContain(
|
|
271
|
+
"Filename number 0490 does not match frontmatter id RFC-0488",
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("V-31 error when duplicate filename number found", async () => {
|
|
276
|
+
const parsed = makeParsed("accepted", BASE_BODY, { id: "RFC-0490" });
|
|
277
|
+
const { add, violations } = makeViolationsCollector();
|
|
278
|
+
const seenFilenameNumbers = new Map<number, string>([[490, "docs/rfcs/rfc-0490-foo.md"]]);
|
|
279
|
+
await validateSingleRfc(
|
|
280
|
+
"rfc-0490-bar.md",
|
|
281
|
+
parsed,
|
|
282
|
+
new Map(),
|
|
283
|
+
new Map(),
|
|
284
|
+
new Set(),
|
|
285
|
+
new Set(),
|
|
286
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
287
|
+
testWorkspace,
|
|
288
|
+
add,
|
|
289
|
+
seenFilenameNumbers,
|
|
290
|
+
);
|
|
291
|
+
const v31 = filterRule(violations, "V-31");
|
|
292
|
+
expect(v31).toHaveLength(1);
|
|
293
|
+
expect(v31[0]!.message).toContain("Duplicate filename number 0490");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("no V-31 error when seenFilenameNumbers is not passed", async () => {
|
|
297
|
+
const parsed = makeParsed("accepted", BASE_BODY, { id: "RFC-0488" });
|
|
298
|
+
const { add, violations } = makeViolationsCollector();
|
|
299
|
+
await validateSingleRfc(
|
|
300
|
+
"rfc-0490-foo.md",
|
|
301
|
+
parsed,
|
|
302
|
+
new Map(),
|
|
303
|
+
new Map(),
|
|
304
|
+
new Set(),
|
|
305
|
+
new Set(),
|
|
306
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
307
|
+
testWorkspace,
|
|
308
|
+
add,
|
|
309
|
+
);
|
|
310
|
+
const v31 = filterRule(violations, "V-31");
|
|
311
|
+
expect(v31).toHaveLength(0);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("V-32: implementation commit drift detection", () => {
|
|
316
|
+
function createGitRepoWithCommits(commits: { message: string; date: string }[]): string {
|
|
317
|
+
const dir = mkdtempSync(join(tmpdir(), "v32-test-"));
|
|
318
|
+
execSync("git init", { cwd: dir, timeout: 5000 });
|
|
319
|
+
execSync("git config user.email test@test.com", { cwd: dir, timeout: 5000 });
|
|
320
|
+
execSync("git config user.name Test", { cwd: dir, timeout: 5000 });
|
|
321
|
+
for (const c of commits) {
|
|
322
|
+
execFileSync("git", ["commit", "--allow-empty", "-m", c.message], {
|
|
323
|
+
cwd: dir,
|
|
324
|
+
timeout: 5000,
|
|
325
|
+
env: { ...process.env, GIT_AUTHOR_DATE: c.date, GIT_COMMITTER_DATE: c.date },
|
|
326
|
+
stdio: "pipe",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return dir;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function runValidateInDir(
|
|
333
|
+
parsed: ParsedRfc,
|
|
334
|
+
workspaceRoot: string,
|
|
335
|
+
): Promise<{ rfcId: string; rule: string; message: string; severity: string }[]> {
|
|
336
|
+
const { add, violations } = makeViolationsCollector();
|
|
337
|
+
await validateSingleRfc(
|
|
338
|
+
"rfc-9999-test.md",
|
|
339
|
+
parsed,
|
|
340
|
+
new Map(),
|
|
341
|
+
new Map(),
|
|
342
|
+
new Set(),
|
|
343
|
+
new Set(),
|
|
344
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
345
|
+
workspaceRoot,
|
|
346
|
+
add,
|
|
347
|
+
);
|
|
348
|
+
return violations;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
test("V-32 warning when accepted RFC has implement: commits", async () => {
|
|
352
|
+
const dir = createGitRepoWithCommits([
|
|
353
|
+
{ message: "implement: RFC-9999 — step 1", date: "2026-01-02T10:00:00" },
|
|
354
|
+
]);
|
|
355
|
+
try {
|
|
356
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
357
|
+
const violations = await runValidateInDir(parsed, dir);
|
|
358
|
+
const v32 = filterRule(violations, "V-32");
|
|
359
|
+
expect(v32).toHaveLength(1);
|
|
360
|
+
expect(v32[0]!.severity).toBe("warning");
|
|
361
|
+
expect(v32[0]!.message).toContain("RFC-9999");
|
|
362
|
+
expect(v32[0]!.message).toContain("accepted");
|
|
363
|
+
} finally {
|
|
364
|
+
rmSync(dir, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("no V-32 when status is implemented", async () => {
|
|
369
|
+
const dir = createGitRepoWithCommits([
|
|
370
|
+
{ message: "implement: RFC-9999 — step 1", date: "2026-01-02T10:00:00" },
|
|
371
|
+
]);
|
|
372
|
+
try {
|
|
373
|
+
const body = BASE_BODY.replace(
|
|
374
|
+
"ACCEPTANCE_HERE",
|
|
375
|
+
"- [x] done (evidence: test.ts:1)\n- [x] another (evidence: test.ts:2)\n- [x] third (evidence: test.ts:3)",
|
|
376
|
+
);
|
|
377
|
+
const parsed = makeParsed("implemented", body, {
|
|
378
|
+
implementedAt: "2026-01-03",
|
|
379
|
+
});
|
|
380
|
+
const violations = await runValidateInDir(parsed, dir);
|
|
381
|
+
const v32 = filterRule(violations, "V-32");
|
|
382
|
+
expect(v32).toHaveLength(0);
|
|
383
|
+
} finally {
|
|
384
|
+
rmSync(dir, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("no V-32 when no implement: commits exist", async () => {
|
|
389
|
+
const dir = createGitRepoWithCommits([
|
|
390
|
+
{ message: "feat: add some feature", date: "2026-01-02T10:00:00" },
|
|
391
|
+
]);
|
|
392
|
+
try {
|
|
393
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
394
|
+
const violations = await runValidateInDir(parsed, dir);
|
|
395
|
+
const v32 = filterRule(violations, "V-32");
|
|
396
|
+
expect(v32).toHaveLength(0);
|
|
397
|
+
} finally {
|
|
398
|
+
rmSync(dir, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("no V-32 in non-git directory", async () => {
|
|
403
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
404
|
+
const violations = await runValidateInDir(parsed, join(tmpdir(), "nonexistent-xyz"));
|
|
405
|
+
const v32 = filterRule(violations, "V-32");
|
|
406
|
+
expect(v32).toHaveLength(0);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("V-16: status <-> implementedAt coupling (error, not warning)", () => {
|
|
411
|
+
test("error when status is accepted but implementedAt is set", async () => {
|
|
412
|
+
const parsed = makeParsed("accepted", BASE_BODY, {
|
|
413
|
+
implementedAt: "2026-01-15",
|
|
414
|
+
});
|
|
415
|
+
const violations = await runValidate(parsed);
|
|
416
|
+
const v16 = filterRule(violations, "V-16");
|
|
417
|
+
expect(v16).toHaveLength(1);
|
|
418
|
+
expect(v16[0]!.severity).toBe("error");
|
|
419
|
+
expect(v16[0]!.message).toContain("accepted");
|
|
420
|
+
expect(v16[0]!.message).toContain("implementedAt");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("error when status is draft but implementedAt is set", async () => {
|
|
424
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
425
|
+
implementedAt: "2026-01-15",
|
|
426
|
+
});
|
|
427
|
+
const violations = await runValidate(parsed);
|
|
428
|
+
const v16 = filterRule(violations, "V-16");
|
|
429
|
+
expect(v16).toHaveLength(1);
|
|
430
|
+
expect(v16[0]!.severity).toBe("error");
|
|
431
|
+
expect(v16[0]!.message).toContain("draft");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("error when status is implemented but implementedAt is empty", async () => {
|
|
435
|
+
const parsed = makeParsed("implemented", BASE_BODY);
|
|
436
|
+
const violations = await runValidate(parsed);
|
|
437
|
+
const v16 = filterRule(violations, "V-16");
|
|
438
|
+
expect(v16).toHaveLength(1);
|
|
439
|
+
expect(v16[0]!.severity).toBe("error");
|
|
440
|
+
expect(v16[0]!.message).toContain("implemented");
|
|
441
|
+
expect(v16[0]!.message).toContain("empty");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("no V-16 when status is implemented and implementedAt is set", async () => {
|
|
445
|
+
const parsed = makeParsed("implemented", BASE_BODY, {
|
|
446
|
+
implementedAt: "2026-01-15",
|
|
447
|
+
});
|
|
448
|
+
const violations = await runValidate(parsed);
|
|
449
|
+
const v16 = filterRule(violations, "V-16");
|
|
450
|
+
expect(v16).toHaveLength(0);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("no V-16 when status is accepted and implementedAt is empty", async () => {
|
|
454
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
455
|
+
const violations = await runValidate(parsed);
|
|
456
|
+
const v16 = filterRule(violations, "V-16");
|
|
457
|
+
// V-16 for implementedAt should not fire; closedAt warnings may exist but are separate
|
|
458
|
+
const implV16 = v16.filter((v) => v.message.includes("implementedAt"));
|
|
459
|
+
expect(implV16).toHaveLength(0);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe("V-NC-01: NEEDS CLARIFICATION marker detection (RFC-0709)", () => {
|
|
464
|
+
test("draft RFC with marker produces warning violation", async () => {
|
|
465
|
+
const body = BASE_BODY.replace(
|
|
466
|
+
"Test context.",
|
|
467
|
+
"> NEEDS CLARIFICATION: what is the exact scope?\n\nTest context.",
|
|
468
|
+
);
|
|
469
|
+
const parsed = makeParsed("draft", body, { createdAt: "2026-08-06" });
|
|
470
|
+
const violations = await runValidate(parsed);
|
|
471
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
472
|
+
expect(nc).toHaveLength(1);
|
|
473
|
+
expect(nc[0]!.severity).toBe("warning");
|
|
474
|
+
expect(nc[0]!.message).toContain("NEEDS CLARIFICATION");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("accepted RFC with marker produces error violation", async () => {
|
|
478
|
+
const body = BASE_BODY.replace(
|
|
479
|
+
"Test context.",
|
|
480
|
+
"> NEEDS CLARIFICATION: what is the exact scope?\n\nTest context.",
|
|
481
|
+
);
|
|
482
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-06" });
|
|
483
|
+
const violations = await runValidate(parsed);
|
|
484
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
485
|
+
expect(nc).toHaveLength(1);
|
|
486
|
+
expect(nc[0]!.severity).toBe("error");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("RFC with marker inside code block produces no V-NC-01 violation", async () => {
|
|
490
|
+
const body = BASE_BODY.replace(
|
|
491
|
+
"Test context.",
|
|
492
|
+
"```\n> NEEDS CLARIFICATION: inside code block\n```\n\nTest context.",
|
|
493
|
+
);
|
|
494
|
+
const parsed = makeParsed("draft", body, { createdAt: "2026-08-06" });
|
|
495
|
+
const violations = await runValidate(parsed);
|
|
496
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
497
|
+
expect(nc).toHaveLength(0);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("RFC with createdAt before cutoff is exempt", async () => {
|
|
501
|
+
const body = BASE_BODY.replace(
|
|
502
|
+
"Test context.",
|
|
503
|
+
"> NEEDS CLARIFICATION: old marker\n\nTest context.",
|
|
504
|
+
);
|
|
505
|
+
const parsed = makeParsed("draft", body, { createdAt: "2026-08-05" });
|
|
506
|
+
const violations = await runValidate(parsed);
|
|
507
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
508
|
+
expect(nc).toHaveLength(0);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("RFC with no markers produces no V-NC-01 violations", async () => {
|
|
512
|
+
const parsed = makeParsed("draft", BASE_BODY, { createdAt: "2026-08-06" });
|
|
513
|
+
const violations = await runValidate(parsed);
|
|
514
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
515
|
+
expect(nc).toHaveLength(0);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("multiple markers produce multiple violations with correct line numbers", async () => {
|
|
519
|
+
const body = BASE_BODY.replace(
|
|
520
|
+
"Test context.",
|
|
521
|
+
"> NEEDS CLARIFICATION: first question\n\nSome text.\n\n> NEEDS CLARIFICATION: second question\n\nTest context.",
|
|
522
|
+
);
|
|
523
|
+
const parsed = makeParsed("draft", body, { createdAt: "2026-08-06" });
|
|
524
|
+
const violations = await runValidate(parsed);
|
|
525
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
526
|
+
expect(nc).toHaveLength(2);
|
|
527
|
+
expect(nc[0]!.message).toContain("first question");
|
|
528
|
+
expect(nc[1]!.message).toContain("second question");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("lowercase 'needs clarification' is not matched (case-sensitive)", async () => {
|
|
532
|
+
const body = BASE_BODY.replace(
|
|
533
|
+
"Test context.",
|
|
534
|
+
"> needs clarification: lowercase marker\n\nTest context.",
|
|
535
|
+
);
|
|
536
|
+
const parsed = makeParsed("draft", body, { createdAt: "2026-08-06" });
|
|
537
|
+
const violations = await runValidate(parsed);
|
|
538
|
+
const nc = filterRule(violations, "V-NC-01");
|
|
539
|
+
expect(nc).toHaveLength(0);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("collectMarkers pure function returns correct markers", () => {
|
|
543
|
+
const body = "Line 1\n> NEEDS CLARIFICATION: test marker\nLine 3";
|
|
544
|
+
const markers = collectMarkers(body, "draft", "2026-08-06");
|
|
545
|
+
expect(markers).toHaveLength(1);
|
|
546
|
+
expect(markers[0]!.line).toBe(2);
|
|
547
|
+
expect(markers[0]!.text).toBe("test marker");
|
|
548
|
+
expect(markers[0]!.severity).toBe("warn");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("collectMarkers returns error severity for accepted status", () => {
|
|
552
|
+
const body = "> NEEDS CLARIFICATION: test marker";
|
|
553
|
+
const markers = collectMarkers(body, "accepted", "2026-08-06");
|
|
554
|
+
expect(markers).toHaveLength(1);
|
|
555
|
+
expect(markers[0]!.severity).toBe("error");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("collectMarkers returns empty for pre-cutoff createdAt", () => {
|
|
559
|
+
const body = "> NEEDS CLARIFICATION: test marker";
|
|
560
|
+
const markers = collectMarkers(body, "draft", "2026-08-05");
|
|
561
|
+
expect(markers).toHaveLength(0);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe("V-29: versionBump required for post-cutoff accepted/implemented RFCs (RFC-0478, ADR-0029)", () => {
|
|
566
|
+
test("error when post-cutoff accepted RFC lacks versionBump", async () => {
|
|
567
|
+
const parsed = makeParsed("accepted", BASE_BODY, { createdAt: "2026-07-21" });
|
|
568
|
+
const violations = await runValidate(parsed);
|
|
569
|
+
const v29 = filterRule(violations, "V-29");
|
|
570
|
+
expect(v29).toHaveLength(1);
|
|
571
|
+
expect(v29[0]!.severity).toBe("error");
|
|
572
|
+
expect(v29[0]!.message).toContain("accepted");
|
|
573
|
+
expect(v29[0]!.message).toContain("versionBump");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("error when post-cutoff implemented RFC lacks versionBump", async () => {
|
|
577
|
+
const body = BASE_BODY.replace(
|
|
578
|
+
"ACCEPTANCE_HERE",
|
|
579
|
+
"- [x] done (evidence: test.ts:1)\n- [x] another (evidence: test.ts:2)",
|
|
580
|
+
);
|
|
581
|
+
const parsed = makeParsed("implemented", body, {
|
|
582
|
+
createdAt: "2026-07-21",
|
|
583
|
+
implementedAt: "2026-07-22",
|
|
584
|
+
});
|
|
585
|
+
const violations = await runValidate(parsed);
|
|
586
|
+
const v29 = filterRule(violations, "V-29");
|
|
587
|
+
expect(v29).toHaveLength(1);
|
|
588
|
+
expect(v29[0]!.severity).toBe("error");
|
|
589
|
+
expect(v29[0]!.message).toContain("implemented");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("no error when post-cutoff accepted RFC has versionBump", async () => {
|
|
593
|
+
const parsed = makeParsed("accepted", BASE_BODY, {
|
|
594
|
+
createdAt: "2026-07-21",
|
|
595
|
+
versionBump: "patch",
|
|
596
|
+
});
|
|
597
|
+
const violations = await runValidate(parsed);
|
|
598
|
+
const v29 = filterRule(violations, "V-29");
|
|
599
|
+
expect(v29).toHaveLength(0);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("no error when post-cutoff implemented RFC has versionBump", async () => {
|
|
603
|
+
const body = BASE_BODY.replace(
|
|
604
|
+
"ACCEPTANCE_HERE",
|
|
605
|
+
"- [x] done (evidence: test.ts:1)\n- [x] another (evidence: test.ts:2)",
|
|
606
|
+
);
|
|
607
|
+
const parsed = makeParsed("implemented", body, {
|
|
608
|
+
createdAt: "2026-07-21",
|
|
609
|
+
implementedAt: "2026-07-22",
|
|
610
|
+
versionBump: "minor",
|
|
611
|
+
});
|
|
612
|
+
const violations = await runValidate(parsed);
|
|
613
|
+
const v29 = filterRule(violations, "V-29");
|
|
614
|
+
expect(v29).toHaveLength(0);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("no error when pre-cutoff accepted RFC lacks versionBump", async () => {
|
|
618
|
+
const parsed = makeParsed("accepted", BASE_BODY, { createdAt: "2026-07-20" });
|
|
619
|
+
const violations = await runValidate(parsed);
|
|
620
|
+
const v29 = filterRule(violations, "V-29");
|
|
621
|
+
expect(v29).toHaveLength(0);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("no error when draft RFC lacks versionBump (regardless of cutoff)", async () => {
|
|
625
|
+
const parsed = makeParsed("draft", BASE_BODY, { createdAt: "2026-07-21" });
|
|
626
|
+
const violations = await runValidate(parsed);
|
|
627
|
+
const v29 = filterRule(violations, "V-29");
|
|
628
|
+
expect(v29).toHaveLength(0);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("warning when versionBump is none but commands.added is non-empty", async () => {
|
|
632
|
+
const parsed = makeParsed("accepted", BASE_BODY, {
|
|
633
|
+
createdAt: "2026-07-21",
|
|
634
|
+
versionBump: "none",
|
|
635
|
+
commands: { added: ["some.command"], changed: [], removed: [] },
|
|
636
|
+
});
|
|
637
|
+
const violations = await runValidate(parsed);
|
|
638
|
+
const v29 = filterRule(violations, "V-29");
|
|
639
|
+
expect(v29).toHaveLength(1);
|
|
640
|
+
expect(v29[0]!.severity).toBe("warning");
|
|
641
|
+
expect(v29[0]!.message).toContain("none");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("no warning when versionBump is none and no commands added/changed", async () => {
|
|
645
|
+
const parsed = makeParsed("accepted", BASE_BODY, {
|
|
646
|
+
createdAt: "2026-07-21",
|
|
647
|
+
versionBump: "none",
|
|
648
|
+
commands: { added: [], changed: [], removed: [] },
|
|
649
|
+
});
|
|
650
|
+
const violations = await runValidate(parsed);
|
|
651
|
+
const v29 = filterRule(violations, "V-29");
|
|
652
|
+
expect(v29).toHaveLength(0);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
describe("RFC-DIR-01: directory structure convention (RFC-0722)", () => {
|
|
657
|
+
test("warning when RFC file is in an unsanctioned subdirectory", async () => {
|
|
658
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
659
|
+
const { add, violations } = makeViolationsCollector();
|
|
660
|
+
await validateSingleRfc(
|
|
661
|
+
"draft/rfc-9999-test.md",
|
|
662
|
+
parsed,
|
|
663
|
+
new Map(),
|
|
664
|
+
new Map(),
|
|
665
|
+
new Set(),
|
|
666
|
+
new Set(),
|
|
667
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
668
|
+
testWorkspace,
|
|
669
|
+
add,
|
|
670
|
+
);
|
|
671
|
+
const dir01 = filterRule(violations, "RFC-DIR-01");
|
|
672
|
+
expect(dir01).toHaveLength(1);
|
|
673
|
+
expect(dir01[0]!.severity).toBe("warning");
|
|
674
|
+
expect(dir01[0]!.message).toContain("unsanctioned subdirectory");
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("no warning when RFC file is at root", async () => {
|
|
678
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
679
|
+
const { add, violations } = makeViolationsCollector();
|
|
680
|
+
await validateSingleRfc(
|
|
681
|
+
"rfc-9999-test.md",
|
|
682
|
+
parsed,
|
|
683
|
+
new Map(),
|
|
684
|
+
new Map(),
|
|
685
|
+
new Set(),
|
|
686
|
+
new Set(),
|
|
687
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
688
|
+
testWorkspace,
|
|
689
|
+
add,
|
|
690
|
+
);
|
|
691
|
+
const dir01 = filterRule(violations, "RFC-DIR-01");
|
|
692
|
+
expect(dir01).toHaveLength(0);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("no warning when RFC file is in archive/ subdirectory", async () => {
|
|
696
|
+
const parsed = makeParsed("implemented", BASE_BODY);
|
|
697
|
+
const { add, violations } = makeViolationsCollector();
|
|
698
|
+
await validateSingleRfc(
|
|
699
|
+
"archive/implemented/rfc-9999-test.md",
|
|
700
|
+
parsed,
|
|
701
|
+
new Map(),
|
|
702
|
+
new Map(),
|
|
703
|
+
new Set(),
|
|
704
|
+
new Set(),
|
|
705
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
706
|
+
testWorkspace,
|
|
707
|
+
add,
|
|
708
|
+
);
|
|
709
|
+
const dir01 = filterRule(violations, "RFC-DIR-01");
|
|
710
|
+
expect(dir01).toHaveLength(0);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("no warning when RFC file is in verification/ subdirectory", async () => {
|
|
714
|
+
const parsed = makeParsed("accepted", BASE_BODY);
|
|
715
|
+
const { add, violations } = makeViolationsCollector();
|
|
716
|
+
await validateSingleRfc(
|
|
717
|
+
"verification/rfc-9999-test.md",
|
|
718
|
+
parsed,
|
|
719
|
+
new Map(),
|
|
720
|
+
new Map(),
|
|
721
|
+
new Set(),
|
|
722
|
+
new Set(),
|
|
723
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
724
|
+
testWorkspace,
|
|
725
|
+
add,
|
|
726
|
+
);
|
|
727
|
+
const dir01 = filterRule(violations, "RFC-DIR-01");
|
|
728
|
+
expect(dir01).toHaveLength(0);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
describe("V-RFC-33: frontmatter YAML parseability (RFC-0755)", () => {
|
|
733
|
+
test("produces violation when result has error variant", () => {
|
|
734
|
+
const { add, violations } = makeViolationsCollector();
|
|
735
|
+
const errorResult = {
|
|
736
|
+
fileName: "rfc-0100-bad.md",
|
|
737
|
+
error: "YAML parse error at line 3, column 5: bad indentation",
|
|
738
|
+
};
|
|
739
|
+
checkFrontmatterYamlParse("rfc-0100-bad.md", errorResult, add);
|
|
740
|
+
const v33 = filterRule(violations, "V-RFC-33");
|
|
741
|
+
expect(v33).toHaveLength(1);
|
|
742
|
+
expect(v33[0]!.message).toContain("rfc-0100-bad.md");
|
|
743
|
+
expect(v33[0]!.message).toContain("YAML parse error");
|
|
744
|
+
expect(v33[0]!.message).toContain("bad indentation");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("produces no violation when result has parsed variant", () => {
|
|
748
|
+
const { add, violations } = makeViolationsCollector();
|
|
749
|
+
const parsedResult = {
|
|
750
|
+
fileName: "rfc-0100-ok.md",
|
|
751
|
+
parsed: { frontmatter: { id: "RFC-0100" }, body: "# RFC-0100\n" },
|
|
752
|
+
};
|
|
753
|
+
checkFrontmatterYamlParse("rfc-0100-ok.md", parsedResult, add);
|
|
754
|
+
const v33 = filterRule(violations, "V-RFC-33");
|
|
755
|
+
expect(v33).toHaveLength(0);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("produces no violation when result is undefined", () => {
|
|
759
|
+
const { add, violations } = makeViolationsCollector();
|
|
760
|
+
checkFrontmatterYamlParse("rfc-0100-missing.md", undefined, add);
|
|
761
|
+
const v33 = filterRule(violations, "V-RFC-33");
|
|
762
|
+
expect(v33).toHaveLength(0);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
describe("V-33: dependsOn referential integrity (RFC-0795)", () => {
|
|
767
|
+
async function runValidateWithDeps(
|
|
768
|
+
parsed: ParsedRfc,
|
|
769
|
+
allParsed: Map<string, { fileName: string; parsed: ParsedRfc }>,
|
|
770
|
+
): Promise<{ rfcId: string; rule: string; message: string; severity: string }[]> {
|
|
771
|
+
const { add, violations } = makeViolationsCollector();
|
|
772
|
+
await validateSingleRfc(
|
|
773
|
+
"rfc-9999-test.md",
|
|
774
|
+
parsed,
|
|
775
|
+
allParsed,
|
|
776
|
+
new Map(),
|
|
777
|
+
new Set(),
|
|
778
|
+
new Set(),
|
|
779
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
780
|
+
testWorkspace,
|
|
781
|
+
add,
|
|
782
|
+
);
|
|
783
|
+
return violations;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function makeAllParsed(
|
|
787
|
+
entries: [string, ParsedRfc][],
|
|
788
|
+
): Map<string, { fileName: string; parsed: ParsedRfc }> {
|
|
789
|
+
const map = new Map<string, { fileName: string; parsed: ParsedRfc }>();
|
|
790
|
+
for (const [id, parsed] of entries) {
|
|
791
|
+
map.set(id, { fileName: `rfc-${id.toLowerCase()}-test.md`, parsed });
|
|
792
|
+
}
|
|
793
|
+
return map;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
test("warning when dependsOn references non-existent RFC", async () => {
|
|
797
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
798
|
+
dependsOn: ["RFC-8888"],
|
|
799
|
+
});
|
|
800
|
+
const violations = await runValidateWithDeps(parsed, new Map());
|
|
801
|
+
const v33 = filterRule(violations, "V-33");
|
|
802
|
+
expect(v33).toHaveLength(1);
|
|
803
|
+
expect(v33[0]!.message).toContain("does not match any existing RFC");
|
|
804
|
+
expect(v33[0]!.severity).toBe("warning");
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("warning when dependsOn includes itself (self-dependency)", async () => {
|
|
808
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
809
|
+
dependsOn: ["RFC-9999"],
|
|
810
|
+
});
|
|
811
|
+
const allParsed = makeAllParsed([["RFC-9999", parsed]]);
|
|
812
|
+
const violations = await runValidateWithDeps(parsed, allParsed);
|
|
813
|
+
const v33 = filterRule(violations, "V-33");
|
|
814
|
+
expect(v33).toHaveLength(1);
|
|
815
|
+
expect(v33[0]!.message).toContain("cannot depend on itself");
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("warning when dependsOn references a rejected RFC", async () => {
|
|
819
|
+
const depParsed = makeParsed("rejected", BASE_BODY, { id: "RFC-8888" });
|
|
820
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
821
|
+
dependsOn: ["RFC-8888"],
|
|
822
|
+
});
|
|
823
|
+
const allParsed = makeAllParsed([
|
|
824
|
+
["RFC-8888", depParsed],
|
|
825
|
+
["RFC-9999", parsed],
|
|
826
|
+
]);
|
|
827
|
+
const violations = await runValidateWithDeps(parsed, allParsed);
|
|
828
|
+
const v33 = filterRule(violations, "V-33");
|
|
829
|
+
expect(v33).toHaveLength(1);
|
|
830
|
+
expect(v33[0]!.message).toContain('"rejected"');
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("no violation when dependsOn references an existing non-rejected RFC", async () => {
|
|
834
|
+
const depParsed = makeParsed("implemented", BASE_BODY, { id: "RFC-8888" });
|
|
835
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
836
|
+
dependsOn: ["RFC-8888"],
|
|
837
|
+
});
|
|
838
|
+
const allParsed = makeAllParsed([
|
|
839
|
+
["RFC-8888", depParsed],
|
|
840
|
+
["RFC-9999", parsed],
|
|
841
|
+
]);
|
|
842
|
+
const violations = await runValidateWithDeps(parsed, allParsed);
|
|
843
|
+
const v33 = filterRule(violations, "V-33");
|
|
844
|
+
expect(v33).toHaveLength(0);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("no violation when dependsOn is absent", async () => {
|
|
848
|
+
const parsed = makeParsed("draft", BASE_BODY);
|
|
849
|
+
const violations = await runValidateWithDeps(parsed, new Map());
|
|
850
|
+
const v33 = filterRule(violations, "V-33");
|
|
851
|
+
expect(v33).toHaveLength(0);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe("V-34: batch slug format (RFC-0795)", () => {
|
|
856
|
+
test("warning when batch slug has uppercase and spaces", async () => {
|
|
857
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
858
|
+
batch: "Engine Consolidation",
|
|
859
|
+
});
|
|
860
|
+
const violations = await runValidate(parsed);
|
|
861
|
+
const v34 = filterRule(violations, "V-34");
|
|
862
|
+
expect(v34).toHaveLength(1);
|
|
863
|
+
expect(v34[0]!.message).toContain("kebab-case");
|
|
864
|
+
expect(v34[0]!.severity).toBe("warning");
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("no violation when batch slug is valid kebab-case", async () => {
|
|
868
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
869
|
+
batch: "engine-consolidation",
|
|
870
|
+
});
|
|
871
|
+
const violations = await runValidate(parsed);
|
|
872
|
+
const v34 = filterRule(violations, "V-34");
|
|
873
|
+
expect(v34).toHaveLength(0);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("no violation when batch is absent", async () => {
|
|
877
|
+
const parsed = makeParsed("draft", BASE_BODY);
|
|
878
|
+
const violations = await runValidate(parsed);
|
|
879
|
+
const v34 = filterRule(violations, "V-34");
|
|
880
|
+
expect(v34).toHaveLength(0);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test("warning when batch slug has underscores", async () => {
|
|
884
|
+
const parsed = makeParsed("draft", BASE_BODY, {
|
|
885
|
+
batch: "engine_consolidation",
|
|
886
|
+
});
|
|
887
|
+
const violations = await runValidate(parsed);
|
|
888
|
+
const v34 = filterRule(violations, "V-34");
|
|
889
|
+
expect(v34).toHaveLength(1);
|
|
890
|
+
});
|
|
891
|
+
});
|