@peterxiaoyang/superspec 0.1.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/README.md +47 -0
- package/adapters/codex/agents/architect.toml +157 -0
- package/adapters/codex/agents/code-reviewer.toml +175 -0
- package/adapters/codex/agents/critic.toml +114 -0
- package/adapters/codex/agents/test-engineer.toml +163 -0
- package/adapters/codex/agents/verifier.toml +119 -0
- package/adapters/codex/install-map.json +81 -0
- package/bin/launch.js +37 -0
- package/bin/superspec-guard.js +4 -0
- package/bin/superspec-init.js +4 -0
- package/bin/superspec.js +4 -0
- package/dist/src/archive.d.ts +23 -0
- package/dist/src/archive.js +428 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +20 -0
- package/dist/src/cli_args.d.ts +12 -0
- package/dist/src/cli_args.js +146 -0
- package/dist/src/core.d.ts +19 -0
- package/dist/src/core.js +357 -0
- package/dist/src/disclosure.d.ts +35 -0
- package/dist/src/disclosure.js +671 -0
- package/dist/src/evidence.d.ts +28 -0
- package/dist/src/evidence.js +849 -0
- package/dist/src/gates.d.ts +16 -0
- package/dist/src/gates.js +1470 -0
- package/dist/src/git.d.ts +8 -0
- package/dist/src/git.js +112 -0
- package/dist/src/init_cli.d.ts +2 -0
- package/dist/src/init_cli.js +145 -0
- package/dist/src/install_engine.d.ts +54 -0
- package/dist/src/install_engine.js +351 -0
- package/dist/src/invariants.d.ts +16 -0
- package/dist/src/invariants.js +363 -0
- package/dist/src/openspec.d.ts +18 -0
- package/dist/src/openspec.js +157 -0
- package/dist/src/paths.d.ts +22 -0
- package/dist/src/paths.js +203 -0
- package/dist/src/project_init.d.ts +4 -0
- package/dist/src/project_init.js +161 -0
- package/dist/src/state.d.ts +37 -0
- package/dist/src/state.js +464 -0
- package/dist/src/tasks.d.ts +23 -0
- package/dist/src/tasks.js +225 -0
- package/dist/src/util.d.ts +120 -0
- package/dist/src/util.js +442 -0
- package/dist/superspec.d.ts +4 -0
- package/dist/superspec.js +57 -0
- package/dist/superspec_guard.d.ts +4 -0
- package/dist/superspec_guard.js +19 -0
- package/dist/superspec_init.d.ts +2 -0
- package/dist/superspec_init.js +17 -0
- package/package.json +63 -0
- package/schemas/install-manifest.schema.json +80 -0
- package/templates/sidecar/archive-preservation.json +11 -0
- package/templates/sidecar/business-invariants.md +38 -0
- package/templates/sidecar/config.yaml +13 -0
- package/templates/sidecar/discovery.md +24 -0
- package/templates/sidecar/test-contract.md +26 -0
- package/templates/workflow/prompts/architect.md +113 -0
- package/templates/workflow/prompts/code-reviewer.md +141 -0
- package/templates/workflow/prompts/critic.md +80 -0
- package/templates/workflow/prompts/test-engineer.md +130 -0
- package/templates/workflow/prompts/verifier.md +85 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +72 -0
- package/templates/workflow/skills/superspec-archive/SKILL.md +41 -0
- package/templates/workflow/skills/superspec-explore/SKILL.md +70 -0
- package/templates/workflow/skills/superspec-propose/SKILL.md +79 -0
- package/templates/workflow/skills/superspec-review/SKILL.md +237 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { reason, renderList, safe_within } from "./util.js";
|
|
3
|
+
import { sidecar_business_invariants_path, sidecar_test_contract_path } from "./paths.js";
|
|
4
|
+
import { live_pass } from "./evidence.js";
|
|
5
|
+
import { parse_test_contract_records, splitList } from "./tasks.js";
|
|
6
|
+
const INV_ID_RE = /\bINV-[A-Za-z0-9_-]+\b/g;
|
|
7
|
+
const REQUIRED_INVARIANT_FIELDS = [
|
|
8
|
+
"statement",
|
|
9
|
+
"scope",
|
|
10
|
+
"source_anchors",
|
|
11
|
+
"acceptance_refs",
|
|
12
|
+
"risk_refs",
|
|
13
|
+
"confidence",
|
|
14
|
+
"enforcement_level",
|
|
15
|
+
"test_refs_or_review_only_reason",
|
|
16
|
+
"verification",
|
|
17
|
+
];
|
|
18
|
+
const CONFIDENCE_VALUES = new Set(["confirmed", "source-backed", "inferred", "uncertain"]);
|
|
19
|
+
const ENFORCEMENT_VALUES = new Set(["automated-test", "review-checklist", "human-confirmation", "advisory"]);
|
|
20
|
+
function readTextIfFile(filePath) {
|
|
21
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
22
|
+
return "";
|
|
23
|
+
return readFileSync(filePath, "utf8");
|
|
24
|
+
}
|
|
25
|
+
function parseMarkdownRow(line) {
|
|
26
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
27
|
+
}
|
|
28
|
+
function isSeparatorRow(cells) {
|
|
29
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
30
|
+
}
|
|
31
|
+
function normalizeHeader(header) {
|
|
32
|
+
return header.trim().toLowerCase().replace(/\s+/g, "_");
|
|
33
|
+
}
|
|
34
|
+
function tableLinesAfterHeading(text, heading) {
|
|
35
|
+
const lines = text.split(/\r?\n/);
|
|
36
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, "i");
|
|
37
|
+
const start = lines.findIndex((line) => headingRe.test(line.trim()));
|
|
38
|
+
if (start < 0)
|
|
39
|
+
return [];
|
|
40
|
+
const table = [];
|
|
41
|
+
let started = false;
|
|
42
|
+
for (const line of lines.slice(start + 1)) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed && !started)
|
|
45
|
+
continue;
|
|
46
|
+
if (!trimmed.startsWith("|")) {
|
|
47
|
+
if (started)
|
|
48
|
+
break;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
started = true;
|
|
52
|
+
table.push(line);
|
|
53
|
+
}
|
|
54
|
+
return table;
|
|
55
|
+
}
|
|
56
|
+
export function business_invariants_text(changeRoot) {
|
|
57
|
+
return readTextIfFile(sidecar_business_invariants_path(changeRoot));
|
|
58
|
+
}
|
|
59
|
+
export function parse_business_invariant_records(changeRoot) {
|
|
60
|
+
const text = business_invariants_text(changeRoot);
|
|
61
|
+
const table = tableLinesAfterHeading(text, "Invariants");
|
|
62
|
+
if (table.length < 3)
|
|
63
|
+
return [];
|
|
64
|
+
const headers = parseMarkdownRow(table[0]).map(normalizeHeader);
|
|
65
|
+
const records = [];
|
|
66
|
+
for (const line of table.slice(1)) {
|
|
67
|
+
const cells = parseMarkdownRow(line);
|
|
68
|
+
if (isSeparatorRow(cells))
|
|
69
|
+
continue;
|
|
70
|
+
const record = {};
|
|
71
|
+
headers.forEach((header, idx) => {
|
|
72
|
+
record[header] = cells[idx] ?? "";
|
|
73
|
+
});
|
|
74
|
+
records.push(record);
|
|
75
|
+
}
|
|
76
|
+
return records;
|
|
77
|
+
}
|
|
78
|
+
function invariantId(record) {
|
|
79
|
+
return String(record["inv-id"] ?? record.inv_id ?? record.id ?? "").trim();
|
|
80
|
+
}
|
|
81
|
+
function isCreatedAfterImplementation(record) {
|
|
82
|
+
return ["true", "yes", "1"].includes(String(record.created_after_implementation ?? "").trim().toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
function isHardInvariant(record) {
|
|
85
|
+
const confidence = String(record.confidence ?? "").trim();
|
|
86
|
+
const enforcement = String(record.enforcement_level ?? "").trim();
|
|
87
|
+
return (confidence === "confirmed" || confidence === "source-backed") && enforcement !== "advisory";
|
|
88
|
+
}
|
|
89
|
+
function enforcementLevel(record) {
|
|
90
|
+
return String(record.enforcement_level ?? "").trim();
|
|
91
|
+
}
|
|
92
|
+
export function business_invariant_ids(changeRoot) {
|
|
93
|
+
const records = parse_business_invariant_records(changeRoot);
|
|
94
|
+
if (records.length > 0)
|
|
95
|
+
return new Set(records.map(invariantId).filter(Boolean));
|
|
96
|
+
return new Set([...business_invariants_text(changeRoot).matchAll(INV_ID_RE)].map((match) => match[0]));
|
|
97
|
+
}
|
|
98
|
+
export function hard_business_invariant_ids(changeRoot) {
|
|
99
|
+
const hard = new Set();
|
|
100
|
+
for (const record of parse_business_invariant_records(changeRoot)) {
|
|
101
|
+
const id = invariantId(record);
|
|
102
|
+
if (!id)
|
|
103
|
+
continue;
|
|
104
|
+
if (isHardInvariant(record) && !isCreatedAfterImplementation(record))
|
|
105
|
+
hard.add(id);
|
|
106
|
+
}
|
|
107
|
+
return hard;
|
|
108
|
+
}
|
|
109
|
+
export function automated_hard_business_invariant_ids(changeRoot) {
|
|
110
|
+
const hard = new Set();
|
|
111
|
+
for (const record of parse_business_invariant_records(changeRoot)) {
|
|
112
|
+
const id = invariantId(record);
|
|
113
|
+
if (!id)
|
|
114
|
+
continue;
|
|
115
|
+
if (isHardInvariant(record) && !isCreatedAfterImplementation(record) && enforcementLevel(record) === "automated-test")
|
|
116
|
+
hard.add(id);
|
|
117
|
+
}
|
|
118
|
+
return hard;
|
|
119
|
+
}
|
|
120
|
+
export function post_implementation_business_invariant_ids(changeRoot) {
|
|
121
|
+
const ids = new Set();
|
|
122
|
+
for (const record of parse_business_invariant_records(changeRoot)) {
|
|
123
|
+
const id = invariantId(record);
|
|
124
|
+
if (id && isCreatedAfterImplementation(record))
|
|
125
|
+
ids.add(id);
|
|
126
|
+
}
|
|
127
|
+
return ids;
|
|
128
|
+
}
|
|
129
|
+
export function human_confirmation_business_invariant_ids(changeRoot) {
|
|
130
|
+
const ids = new Set();
|
|
131
|
+
for (const record of parse_business_invariant_records(changeRoot)) {
|
|
132
|
+
const id = invariantId(record);
|
|
133
|
+
if (!id)
|
|
134
|
+
continue;
|
|
135
|
+
const enforcement = String(record.enforcement_level ?? "").trim();
|
|
136
|
+
if (enforcement === "human-confirmation")
|
|
137
|
+
ids.add(id);
|
|
138
|
+
}
|
|
139
|
+
return ids;
|
|
140
|
+
}
|
|
141
|
+
export function business_invariant_validation_reasons(changeRoot) {
|
|
142
|
+
const filePath = sidecar_business_invariants_path(changeRoot);
|
|
143
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
144
|
+
return [reason("missing_business_invariants", "sidecar .superspec/artifacts/business-invariants.md missing")];
|
|
145
|
+
}
|
|
146
|
+
const text = business_invariants_text(changeRoot);
|
|
147
|
+
if (!text.trim())
|
|
148
|
+
return [reason("missing_business_invariants", "sidecar .superspec/artifacts/business-invariants.md empty")];
|
|
149
|
+
const records = parse_business_invariant_records(changeRoot);
|
|
150
|
+
const problems = [];
|
|
151
|
+
if (records.length === 0) {
|
|
152
|
+
problems.push(reason("invalid_business_invariants", "business-invariants.md has no parseable Invariants table"));
|
|
153
|
+
return problems;
|
|
154
|
+
}
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
records.forEach((record, idx) => {
|
|
157
|
+
const row = idx + 1;
|
|
158
|
+
const id = invariantId(record);
|
|
159
|
+
if (!/^INV-[A-Za-z0-9_-]+$/.test(id)) {
|
|
160
|
+
problems.push(reason("invalid_invariant_ref", `business-invariants row ${row} has invalid INV-ID ${id || "<empty>"}`));
|
|
161
|
+
}
|
|
162
|
+
else if (seen.has(id)) {
|
|
163
|
+
problems.push(reason("invalid_invariant_ref", `duplicate business invariant id ${id}`));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
seen.add(id);
|
|
167
|
+
}
|
|
168
|
+
for (const field of REQUIRED_INVARIANT_FIELDS) {
|
|
169
|
+
if (!String(record[field] ?? "").trim()) {
|
|
170
|
+
problems.push(reason("invalid_business_invariants", `${id || `row ${row}`}: missing ${field}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const confidence = String(record.confidence ?? "").trim();
|
|
174
|
+
if (confidence && !CONFIDENCE_VALUES.has(confidence)) {
|
|
175
|
+
problems.push(reason("invalid_business_invariants", `${id || `row ${row}`}: unsupported confidence ${confidence}`));
|
|
176
|
+
}
|
|
177
|
+
const enforcement = String(record.enforcement_level ?? "").trim();
|
|
178
|
+
if (enforcement && !ENFORCEMENT_VALUES.has(enforcement)) {
|
|
179
|
+
problems.push(reason("invalid_business_invariants", `${id || `row ${row}`}: unsupported enforcement_level ${enforcement}`));
|
|
180
|
+
}
|
|
181
|
+
if (isCreatedAfterImplementation(record) && isHardInvariant(record)) {
|
|
182
|
+
problems.push(reason("post_implementation_invariant_backfill", `${id || `row ${row}`}: created_after_implementation hard invariant cannot satisfy current RED/GREEN contract`));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return problems;
|
|
186
|
+
}
|
|
187
|
+
export function test_contract_invariant_ids(changeRoot) {
|
|
188
|
+
const refs = new Set();
|
|
189
|
+
for (const record of parse_test_contract_records(changeRoot)) {
|
|
190
|
+
for (const ref of record.invariant_refs)
|
|
191
|
+
refs.add(ref);
|
|
192
|
+
}
|
|
193
|
+
return refs;
|
|
194
|
+
}
|
|
195
|
+
export function task_invariant_refs(tasks) {
|
|
196
|
+
const refs = new Set();
|
|
197
|
+
for (const task of Object.values(tasks)) {
|
|
198
|
+
for (const ref of splitList(task.attrs.invariant_refs ?? ""))
|
|
199
|
+
refs.add(ref);
|
|
200
|
+
}
|
|
201
|
+
return refs;
|
|
202
|
+
}
|
|
203
|
+
export function evidence_invariant_refs(ev) {
|
|
204
|
+
const value = ev.invariant_refs;
|
|
205
|
+
if (Array.isArray(value))
|
|
206
|
+
return new Set(value.map((item) => String(item)).filter(Boolean));
|
|
207
|
+
if (typeof value === "string")
|
|
208
|
+
return new Set(splitList(value));
|
|
209
|
+
return new Set();
|
|
210
|
+
}
|
|
211
|
+
export function red_green_invariant_ids(evidences) {
|
|
212
|
+
const ids = new Set();
|
|
213
|
+
for (const ev of live_pass(evidences, { kind: "test_run" })) {
|
|
214
|
+
for (const id of evidence_invariant_refs(ev))
|
|
215
|
+
ids.add(id);
|
|
216
|
+
}
|
|
217
|
+
return ids;
|
|
218
|
+
}
|
|
219
|
+
export function evidence_invariant_ref_reasons(evidences, taskId, declared, validIds, semanticStatus) {
|
|
220
|
+
const problems = [];
|
|
221
|
+
for (const ev of evidences) {
|
|
222
|
+
const refs = evidence_invariant_refs(ev);
|
|
223
|
+
if (declared.size > 0 && refs.size === 0) {
|
|
224
|
+
problems.push(reason("missing_invariant_ref", `task ${taskId} ${semanticStatus} evidence requires invariant_refs`));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const inv of refs) {
|
|
228
|
+
if (declared.size > 0 && !declared.has(inv)) {
|
|
229
|
+
problems.push(reason("invariant_not_honored", `task ${taskId} ${semanticStatus} invariant_ref ${inv} not in declared invariant_refs ${renderList([...declared].sort())}`));
|
|
230
|
+
}
|
|
231
|
+
if (validIds.size > 0 && !validIds.has(inv)) {
|
|
232
|
+
problems.push(reason("invalid_invariant_ref", `task ${taskId} ${semanticStatus} references unknown invariant ${inv}`));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return problems;
|
|
237
|
+
}
|
|
238
|
+
export function evidence_test_contract_invariant_reasons(evidences, taskId, byTest, semanticStatus) {
|
|
239
|
+
const problems = [];
|
|
240
|
+
for (const ev of evidences) {
|
|
241
|
+
const testId = typeof ev.test_id === "string" ? ev.test_id : "";
|
|
242
|
+
const expected = byTest.get(testId) ?? new Set();
|
|
243
|
+
if (expected.size === 0)
|
|
244
|
+
continue;
|
|
245
|
+
const actual = evidence_invariant_refs(ev);
|
|
246
|
+
if (actual.size === 0) {
|
|
247
|
+
problems.push(reason("missing_invariant_ref", `task ${taskId} ${semanticStatus} evidence for ${testId} requires invariant_refs ${renderList([...expected].sort())}`));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const missing = [...expected].filter((inv) => !actual.has(inv)).sort();
|
|
251
|
+
if (missing.length > 0) {
|
|
252
|
+
problems.push(reason("invariant_not_honored", `task ${taskId} ${semanticStatus} evidence for ${testId} missing invariant_refs ${renderList(missing)}`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return problems;
|
|
256
|
+
}
|
|
257
|
+
function firstMarkdownTableLines(text) {
|
|
258
|
+
const lines = text.split(/\r?\n/);
|
|
259
|
+
const table = [];
|
|
260
|
+
let started = false;
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
if (!trimmed.startsWith("|")) {
|
|
264
|
+
if (started)
|
|
265
|
+
break;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
started = true;
|
|
269
|
+
table.push(line);
|
|
270
|
+
}
|
|
271
|
+
return table;
|
|
272
|
+
}
|
|
273
|
+
function parse_invariant_matrix_records(text) {
|
|
274
|
+
const table = firstMarkdownTableLines(text);
|
|
275
|
+
if (table.length < 3)
|
|
276
|
+
return { records: [], missing_columns: ["table"] };
|
|
277
|
+
const headers = parseMarkdownRow(table[0]).map(normalizeHeader);
|
|
278
|
+
const invIdx = headers.findIndex((header) => header === "inv-id" || header === "inv_id" || header.includes("invariant") || header.includes("inv"));
|
|
279
|
+
const statusIdx = headers.findIndex((header) => header === "status" || header.includes("状态"));
|
|
280
|
+
const evidenceIdx = headers.findIndex((header) => header.includes("evidence") || header.includes("proof") || header.includes("证据"));
|
|
281
|
+
const missingColumns = [];
|
|
282
|
+
if (invIdx < 0)
|
|
283
|
+
missingColumns.push("INV-ID");
|
|
284
|
+
if (statusIdx < 0)
|
|
285
|
+
missingColumns.push("status");
|
|
286
|
+
if (evidenceIdx < 0)
|
|
287
|
+
missingColumns.push("evidence");
|
|
288
|
+
if (missingColumns.length > 0)
|
|
289
|
+
return { records: [], missing_columns: missingColumns };
|
|
290
|
+
const records = [];
|
|
291
|
+
for (const line of table.slice(1)) {
|
|
292
|
+
const cells = parseMarkdownRow(line);
|
|
293
|
+
if (isSeparatorRow(cells))
|
|
294
|
+
continue;
|
|
295
|
+
const invId = (cells[invIdx] ?? "").match(INV_ID_RE)?.[0] ?? "";
|
|
296
|
+
if (!invId)
|
|
297
|
+
continue;
|
|
298
|
+
records.push({
|
|
299
|
+
inv_id: invId,
|
|
300
|
+
status: cells[statusIdx] ?? "",
|
|
301
|
+
evidence: cells[evidenceIdx] ?? "",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return { records, missing_columns: [] };
|
|
305
|
+
}
|
|
306
|
+
function escapeRegExp(text) {
|
|
307
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
308
|
+
}
|
|
309
|
+
function cellReferencesEvidenceId(cell, evidenceId) {
|
|
310
|
+
if (!evidenceId)
|
|
311
|
+
return false;
|
|
312
|
+
const tokenCandidates = cell
|
|
313
|
+
.split(/[\s,;|()[\]{}<>`'"]+/)
|
|
314
|
+
.map((item) => item.trim().replace(/^[.:]+|[.:]+$/g, ""))
|
|
315
|
+
.filter(Boolean);
|
|
316
|
+
if (tokenCandidates.includes(evidenceId))
|
|
317
|
+
return true;
|
|
318
|
+
const pattern = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(evidenceId)}([^A-Za-z0-9_-]|$)`);
|
|
319
|
+
return pattern.test(cell);
|
|
320
|
+
}
|
|
321
|
+
function referencedLiveEvidenceIds(cell, liveEvidenceIds) {
|
|
322
|
+
return [...liveEvidenceIds].filter((evidenceId) => cellReferencesEvidenceId(cell, evidenceId)).sort();
|
|
323
|
+
}
|
|
324
|
+
export function invariant_matrix_coverage_reasons(changeRoot, ev, evidences) {
|
|
325
|
+
const matrixRef = ev.invariant_matrix_ref;
|
|
326
|
+
if (typeof matrixRef !== "string" || !matrixRef)
|
|
327
|
+
return [];
|
|
328
|
+
const filePath = safe_within(changeRoot, matrixRef);
|
|
329
|
+
if (filePath === null || !existsSync(filePath) || !statSync(filePath).isFile())
|
|
330
|
+
return [];
|
|
331
|
+
const required = hard_business_invariant_ids(changeRoot);
|
|
332
|
+
if (required.size === 0)
|
|
333
|
+
return [];
|
|
334
|
+
const { records, missing_columns: missingColumns } = parse_invariant_matrix_records(readFileSync(filePath, "utf8"));
|
|
335
|
+
const label = String(ev._path ?? ev.evidence_id ?? "verification_review");
|
|
336
|
+
const reasons = [];
|
|
337
|
+
if (missingColumns.length > 0) {
|
|
338
|
+
reasons.push(reason("invariant_matrix_incomplete", `${label}: invariant_matrix_ref ${matrixRef} missing parseable columns/table: ${renderList(missingColumns)}`, missingColumns));
|
|
339
|
+
return reasons;
|
|
340
|
+
}
|
|
341
|
+
const byId = new Map(records.map((record) => [record.inv_id, record]));
|
|
342
|
+
const missing = [...required].filter((id) => !byId.has(id)).sort();
|
|
343
|
+
if (missing.length > 0) {
|
|
344
|
+
reasons.push(reason("invariant_matrix_incomplete", `${label}: invariant matrix missing hard business invariants: ${renderList(missing)}`, missing));
|
|
345
|
+
}
|
|
346
|
+
const liveEvidenceIds = new Set(live_pass(evidences)
|
|
347
|
+
.map((item) => String(item.evidence_id ?? ""))
|
|
348
|
+
.filter(Boolean));
|
|
349
|
+
for (const id of [...required].sort()) {
|
|
350
|
+
const record = byId.get(id);
|
|
351
|
+
if (!record)
|
|
352
|
+
continue;
|
|
353
|
+
const status = record.status.trim().toLowerCase();
|
|
354
|
+
if (status !== "pass" && status !== "accepted") {
|
|
355
|
+
reasons.push(reason("invariant_matrix_incomplete", `${label}: invariant ${id} matrix status must be pass or accepted, got ${record.status || "<empty>"}`, [id]));
|
|
356
|
+
}
|
|
357
|
+
const evidenceIds = referencedLiveEvidenceIds(record.evidence, liveEvidenceIds);
|
|
358
|
+
if (evidenceIds.length === 0) {
|
|
359
|
+
reasons.push(reason("invariant_matrix_incomplete", `${label}: invariant ${id} matrix evidence must reference at least one live/pass evidence_id`, [id]));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return reasons;
|
|
363
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JsonMap, Reason } from "./util.ts";
|
|
2
|
+
export declare function openspec_status(change: string): JsonMap;
|
|
3
|
+
export declare function openspec_validate(change: string): [boolean, string];
|
|
4
|
+
export declare function openspec_version(): string;
|
|
5
|
+
export declare function openspec_status_shape_reasons(status: JsonMap): Reason[];
|
|
6
|
+
export declare function artifact_status_map(status: JsonMap): Record<string, string>;
|
|
7
|
+
export declare function is_ready_or_done(status: JsonMap, artifact: string): boolean;
|
|
8
|
+
export declare function is_done(status: JsonMap, artifact: string): boolean;
|
|
9
|
+
export declare function all_done(status: JsonMap): boolean;
|
|
10
|
+
export declare function openspec_floor_route(status: JsonMap): string;
|
|
11
|
+
export declare function normalize_route_phase(route: string): string;
|
|
12
|
+
export declare function normalize_gate(gate: string): string;
|
|
13
|
+
export declare function gate_route_phase(gate: string): string;
|
|
14
|
+
export declare function effective_route_phase(status: JsonMap, requestedRoute: string, decision: JsonMap): string;
|
|
15
|
+
export declare function status_fingerprint(status: JsonMap): string;
|
|
16
|
+
export declare function get_change_root(status: JsonMap): string;
|
|
17
|
+
export declare function get_repo_root(status: JsonMap): string;
|
|
18
|
+
export declare function repo_root_from_cwd(start?: string): string;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { GATE_ALIASES, GATE_ROUTE, GuardError, ROUTE_ALIASES, ROUTE_ORDER, fingerprint_obj, isObject, reason, repr, runCommand, } from "./util.js";
|
|
4
|
+
export function openspec_status(change) {
|
|
5
|
+
const proc = runCommand("openspec", ["status", "--change", change, "--json"], { timeout: 30_000 });
|
|
6
|
+
if (proc.error) {
|
|
7
|
+
throw new GuardError(`openspec_unavailable: status failed: ${proc.error.message}`);
|
|
8
|
+
}
|
|
9
|
+
if (proc.status !== 0) {
|
|
10
|
+
throw new GuardError(`openspec_unavailable: status exit ${proc.status}: ${proc.stderr.trim()}`);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(proc.stdout);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new GuardError(`openspec_unavailable: status json parse failed`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function openspec_validate(change) {
|
|
20
|
+
const proc = runCommand("openspec", ["validate", change], { timeout: 60_000 });
|
|
21
|
+
if (proc.error) {
|
|
22
|
+
throw new GuardError(`openspec_unavailable: validate failed: ${proc.error.message}`);
|
|
23
|
+
}
|
|
24
|
+
return [proc.status === 0, `${proc.stdout}${proc.stderr}`.trim()];
|
|
25
|
+
}
|
|
26
|
+
export function openspec_version() {
|
|
27
|
+
const proc = runCommand("openspec", ["--version"], { timeout: 15_000 });
|
|
28
|
+
if (proc.error || proc.status !== 0)
|
|
29
|
+
return "unknown";
|
|
30
|
+
const raw = (proc.stdout || proc.stderr).trim();
|
|
31
|
+
const match = raw.match(/\d+\.\d+\.\d+/);
|
|
32
|
+
return match ? match[0] : raw || "unknown";
|
|
33
|
+
}
|
|
34
|
+
export function openspec_status_shape_reasons(status) {
|
|
35
|
+
const problems = [];
|
|
36
|
+
if (typeof status.changeRoot !== "string" || !status.changeRoot) {
|
|
37
|
+
problems.push(reason("openspec_status_incompatible", "status.changeRoot missing or not a string"));
|
|
38
|
+
}
|
|
39
|
+
const planningHome = status.planningHome;
|
|
40
|
+
if (!isObject(planningHome) || typeof planningHome.root !== "string") {
|
|
41
|
+
problems.push(reason("openspec_status_incompatible", "status.planningHome.root missing or not a string"));
|
|
42
|
+
}
|
|
43
|
+
const artifacts = status.artifacts;
|
|
44
|
+
if (!Array.isArray(artifacts)) {
|
|
45
|
+
problems.push(reason("openspec_status_incompatible", "status.artifacts missing or not a list"));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
artifacts.forEach((artifact, idx) => {
|
|
49
|
+
if (!isObject(artifact)) {
|
|
50
|
+
problems.push(reason("openspec_status_incompatible", `status.artifacts[${idx}] is not an object`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof artifact.id !== "string") {
|
|
54
|
+
problems.push(reason("openspec_status_incompatible", `status.artifacts[${idx}].id missing`));
|
|
55
|
+
}
|
|
56
|
+
if (!["done", "ready", "blocked"].includes(String(artifact.status))) {
|
|
57
|
+
problems.push(reason("openspec_status_incompatible", `status.artifacts[${idx}].status unsupported: ${repr(artifact.status)}`));
|
|
58
|
+
}
|
|
59
|
+
if ("missingDeps" in artifact && !Array.isArray(artifact.missingDeps)) {
|
|
60
|
+
problems.push(reason("openspec_status_incompatible", `status.artifacts[${idx}].missingDeps must be a list`));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(status.applyRequires)) {
|
|
65
|
+
problems.push(reason("openspec_status_incompatible", "status.applyRequires missing or not a list"));
|
|
66
|
+
}
|
|
67
|
+
return problems;
|
|
68
|
+
}
|
|
69
|
+
export function artifact_status_map(status) {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const artifact of Array.isArray(status.artifacts) ? status.artifacts : []) {
|
|
72
|
+
if (isObject(artifact) && typeof artifact.id === "string") {
|
|
73
|
+
out[artifact.id] = String(artifact.status ?? "unknown");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
export function is_ready_or_done(status, artifact) {
|
|
79
|
+
return ["ready", "done"].includes(artifact_status_map(status)[artifact]);
|
|
80
|
+
}
|
|
81
|
+
export function is_done(status, artifact) {
|
|
82
|
+
return artifact_status_map(status)[artifact] === "done";
|
|
83
|
+
}
|
|
84
|
+
export function all_done(status) {
|
|
85
|
+
const values = Object.values(artifact_status_map(status));
|
|
86
|
+
return values.length > 0 && values.every((item) => item === "done");
|
|
87
|
+
}
|
|
88
|
+
export function openspec_floor_route(status) {
|
|
89
|
+
if (is_done(status, "tasks"))
|
|
90
|
+
return "apply";
|
|
91
|
+
if (is_done(status, "design"))
|
|
92
|
+
return "propose";
|
|
93
|
+
if (is_done(status, "proposal") && is_done(status, "specs"))
|
|
94
|
+
return "propose";
|
|
95
|
+
if (is_ready_or_done(status, "proposal") && is_ready_or_done(status, "specs"))
|
|
96
|
+
return "propose";
|
|
97
|
+
return "init";
|
|
98
|
+
}
|
|
99
|
+
export function normalize_route_phase(route) {
|
|
100
|
+
return ROUTE_ALIASES[route] ?? route;
|
|
101
|
+
}
|
|
102
|
+
export function normalize_gate(gate) {
|
|
103
|
+
return GATE_ALIASES[gate] ?? gate;
|
|
104
|
+
}
|
|
105
|
+
export function gate_route_phase(gate) {
|
|
106
|
+
return GATE_ROUTE[normalize_gate(gate)] ?? "propose";
|
|
107
|
+
}
|
|
108
|
+
export function effective_route_phase(status, requestedRoute, decision) {
|
|
109
|
+
const requested = normalize_route_phase(requestedRoute);
|
|
110
|
+
if (decision.allowed)
|
|
111
|
+
return requested;
|
|
112
|
+
const floor = openspec_floor_route(status);
|
|
113
|
+
const requestedOrder = ROUTE_ORDER[requested];
|
|
114
|
+
if (requestedOrder === undefined)
|
|
115
|
+
return floor;
|
|
116
|
+
return requestedOrder <= ROUTE_ORDER[floor] ? requested : floor;
|
|
117
|
+
}
|
|
118
|
+
export function status_fingerprint(status) {
|
|
119
|
+
const artifacts = (Array.isArray(status.artifacts) ? status.artifacts : [])
|
|
120
|
+
.map((artifact) => ({
|
|
121
|
+
id: artifact.id,
|
|
122
|
+
status: artifact.status,
|
|
123
|
+
missingDeps: [...(Array.isArray(artifact.missingDeps) ? artifact.missingDeps : [])].sort(),
|
|
124
|
+
}))
|
|
125
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
126
|
+
const core = {
|
|
127
|
+
artifacts,
|
|
128
|
+
applyRequires: [...(Array.isArray(status.applyRequires) ? status.applyRequires : [])].sort(),
|
|
129
|
+
};
|
|
130
|
+
return fingerprint_obj(core);
|
|
131
|
+
}
|
|
132
|
+
export function get_change_root(status) {
|
|
133
|
+
if (!status.changeRoot)
|
|
134
|
+
throw new GuardError("openspec_unavailable: status missing changeRoot");
|
|
135
|
+
return resolve(String(status.changeRoot));
|
|
136
|
+
}
|
|
137
|
+
export function get_repo_root(status) {
|
|
138
|
+
const root = isObject(status.planningHome) ? status.planningHome.root : undefined;
|
|
139
|
+
if (root)
|
|
140
|
+
return resolve(String(root));
|
|
141
|
+
const changeRoot = get_change_root(status);
|
|
142
|
+
const parts = changeRoot.split(sep);
|
|
143
|
+
if (parts.length < 3)
|
|
144
|
+
throw new GuardError("openspec_unavailable: cannot derive repo root");
|
|
145
|
+
return resolve(changeRoot, "..", "..", "..");
|
|
146
|
+
}
|
|
147
|
+
export function repo_root_from_cwd(start = process.cwd()) {
|
|
148
|
+
let dir = resolve(start);
|
|
149
|
+
while (true) {
|
|
150
|
+
if (existsSync(join(dir, "openspec", "changes")))
|
|
151
|
+
return dir;
|
|
152
|
+
const parent = dirname(dir);
|
|
153
|
+
if (parent === dir)
|
|
154
|
+
return resolve(start);
|
|
155
|
+
dir = parent;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { JsonMap, Reason } from "./util.ts";
|
|
2
|
+
export declare function superspec_dir(changeRoot: string): string;
|
|
3
|
+
export declare function project_superspec_dir(repoRoot: string): string;
|
|
4
|
+
export declare function sidecar_artifacts_dir(changeRoot: string): string;
|
|
5
|
+
export declare function sidecar_test_contract_path(changeRoot: string): string;
|
|
6
|
+
export declare function sidecar_business_invariants_path(changeRoot: string): string;
|
|
7
|
+
export declare function sidecar_discovery_path(changeRoot: string): string;
|
|
8
|
+
export declare function sidecar_output_path(changeRoot: string, relPath: string): string;
|
|
9
|
+
export declare function write_sidecar_text(changeRoot: string, relPath: string, text: string): string;
|
|
10
|
+
export declare function write_sidecar_json(changeRoot: string, relPath: string, data: JsonMap): string;
|
|
11
|
+
export declare function config_file(changeRoot: string): string;
|
|
12
|
+
export declare function project_config_file(repoRoot: string): string;
|
|
13
|
+
export declare function required_sidecar_paths(changeRoot: string): string[];
|
|
14
|
+
export declare function ensure_sidecar_layout(changeRoot: string): void;
|
|
15
|
+
export declare function ensure_state_layout(changeRoot: string): void;
|
|
16
|
+
export declare function find_forbidden_aliases(repoRoot: string, changeRoot: string): string[];
|
|
17
|
+
export declare function read_skill_frontmatter_name(skillPath: string): string | null;
|
|
18
|
+
export declare function read_agent_toml_name(agentPath: string): string | null;
|
|
19
|
+
export declare function parse_simple_yaml(filePath: string): JsonMap;
|
|
20
|
+
export declare function merge_config(base: JsonMap, override: JsonMap): JsonMap;
|
|
21
|
+
export declare function validate_config_keys(config: JsonMap, source: string): Reason[];
|
|
22
|
+
export declare function load_config(repoRoot: string, changeRoot: string): [JsonMap, Reason[]];
|