@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.
Files changed (68) hide show
  1. package/README.md +47 -0
  2. package/adapters/codex/agents/architect.toml +157 -0
  3. package/adapters/codex/agents/code-reviewer.toml +175 -0
  4. package/adapters/codex/agents/critic.toml +114 -0
  5. package/adapters/codex/agents/test-engineer.toml +163 -0
  6. package/adapters/codex/agents/verifier.toml +119 -0
  7. package/adapters/codex/install-map.json +81 -0
  8. package/bin/launch.js +37 -0
  9. package/bin/superspec-guard.js +4 -0
  10. package/bin/superspec-init.js +4 -0
  11. package/bin/superspec.js +4 -0
  12. package/dist/src/archive.d.ts +23 -0
  13. package/dist/src/archive.js +428 -0
  14. package/dist/src/cli.d.ts +1 -0
  15. package/dist/src/cli.js +20 -0
  16. package/dist/src/cli_args.d.ts +12 -0
  17. package/dist/src/cli_args.js +146 -0
  18. package/dist/src/core.d.ts +19 -0
  19. package/dist/src/core.js +357 -0
  20. package/dist/src/disclosure.d.ts +35 -0
  21. package/dist/src/disclosure.js +671 -0
  22. package/dist/src/evidence.d.ts +28 -0
  23. package/dist/src/evidence.js +849 -0
  24. package/dist/src/gates.d.ts +16 -0
  25. package/dist/src/gates.js +1470 -0
  26. package/dist/src/git.d.ts +8 -0
  27. package/dist/src/git.js +112 -0
  28. package/dist/src/init_cli.d.ts +2 -0
  29. package/dist/src/init_cli.js +145 -0
  30. package/dist/src/install_engine.d.ts +54 -0
  31. package/dist/src/install_engine.js +351 -0
  32. package/dist/src/invariants.d.ts +16 -0
  33. package/dist/src/invariants.js +363 -0
  34. package/dist/src/openspec.d.ts +18 -0
  35. package/dist/src/openspec.js +157 -0
  36. package/dist/src/paths.d.ts +22 -0
  37. package/dist/src/paths.js +203 -0
  38. package/dist/src/project_init.d.ts +4 -0
  39. package/dist/src/project_init.js +161 -0
  40. package/dist/src/state.d.ts +37 -0
  41. package/dist/src/state.js +464 -0
  42. package/dist/src/tasks.d.ts +23 -0
  43. package/dist/src/tasks.js +225 -0
  44. package/dist/src/util.d.ts +120 -0
  45. package/dist/src/util.js +442 -0
  46. package/dist/superspec.d.ts +4 -0
  47. package/dist/superspec.js +57 -0
  48. package/dist/superspec_guard.d.ts +4 -0
  49. package/dist/superspec_guard.js +19 -0
  50. package/dist/superspec_init.d.ts +2 -0
  51. package/dist/superspec_init.js +17 -0
  52. package/package.json +63 -0
  53. package/schemas/install-manifest.schema.json +80 -0
  54. package/templates/sidecar/archive-preservation.json +11 -0
  55. package/templates/sidecar/business-invariants.md +38 -0
  56. package/templates/sidecar/config.yaml +13 -0
  57. package/templates/sidecar/discovery.md +24 -0
  58. package/templates/sidecar/test-contract.md +26 -0
  59. package/templates/workflow/prompts/architect.md +113 -0
  60. package/templates/workflow/prompts/code-reviewer.md +141 -0
  61. package/templates/workflow/prompts/critic.md +80 -0
  62. package/templates/workflow/prompts/test-engineer.md +130 -0
  63. package/templates/workflow/prompts/verifier.md +85 -0
  64. package/templates/workflow/skills/superspec-apply/SKILL.md +72 -0
  65. package/templates/workflow/skills/superspec-archive/SKILL.md +41 -0
  66. package/templates/workflow/skills/superspec-explore/SKILL.md +70 -0
  67. package/templates/workflow/skills/superspec-propose/SKILL.md +79 -0
  68. package/templates/workflow/skills/superspec-review/SKILL.md +237 -0
@@ -0,0 +1,225 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { reason, renderList, walkFiles } from "./util.js";
5
+ import { sidecar_test_contract_path } from "./paths.js";
6
+ import { live_pass } from "./evidence.js";
7
+ const TASK_LINE = /^- \[( |x|X)\]\s+(\S+)\s*(.*)$/;
8
+ const ATTR_LINE = /^\s+- (\w+):\s*(.*)$/;
9
+ const TEST_ID_RE = /\bTEST-\d+[A-Za-z0-9_-]*\b/g;
10
+ const SCENARIO_RE = /^####\s+Scenario:?\s*(.+?)\s*$/;
11
+ export function parse_tasks(changeRoot) {
12
+ const tasksMd = join(changeRoot, "tasks.md");
13
+ if (!existsSync(tasksMd) || !statSync(tasksMd).isFile())
14
+ return {};
15
+ const tasks = {};
16
+ let current = null;
17
+ for (const raw of readFileSync(tasksMd, "utf8").split(/\r?\n/)) {
18
+ const match = raw.match(TASK_LINE);
19
+ if (match) {
20
+ const checked = match[1].toLowerCase() === "x";
21
+ const taskId = match[2].trim();
22
+ current = { task_id: taskId, checked, desc: match[3].trim(), attrs: {} };
23
+ tasks[taskId] = current;
24
+ continue;
25
+ }
26
+ const attr = raw.match(ATTR_LINE);
27
+ if (attr && current) {
28
+ current.attrs[attr[1]] = attr[2].trim();
29
+ }
30
+ else if (raw && !raw.startsWith(" ") && !raw.startsWith("\t")) {
31
+ current = null;
32
+ }
33
+ }
34
+ return tasks;
35
+ }
36
+ // FIX-8 (audit A-5): checkbox-state-insensitive fingerprint of tasks.md. Checking a box is
37
+ // normal apply progress; any other edit (new task, attrs, descriptions) changes the hash and
38
+ // therefore invalidates the user's apply-scope approval.
39
+ export function tasks_structure_hash(changeRoot) {
40
+ const tasksMd = join(changeRoot, "tasks.md");
41
+ if (!existsSync(tasksMd) || !statSync(tasksMd).isFile())
42
+ return null;
43
+ const normalized = readFileSync(tasksMd, "utf8").replace(/^(\s*[-*]\s*)\[[xX]\]/gm, "$1[ ]");
44
+ return `sha256:${createHash("sha256").update(normalized, "utf8").digest("hex")}`;
45
+ }
46
+ export function splitList(value) {
47
+ const stripped = value.trim().replace(/^\[/, "").replace(/\]$/, "");
48
+ if (!stripped)
49
+ return [];
50
+ return stripped.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
51
+ }
52
+ function parseMarkdownRow(line) {
53
+ return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
54
+ }
55
+ function isSeparatorRow(cells) {
56
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
57
+ }
58
+ function normalizeHeader(header) {
59
+ return header.trim().toLowerCase().replace(/\s+/g, "_");
60
+ }
61
+ function tableLinesAfterHeading(text, headings) {
62
+ const lines = text.split(/\r?\n/);
63
+ const start = lines.findIndex((line) => headings.some((heading) => heading.test(line.trim())));
64
+ if (start < 0)
65
+ return [];
66
+ const table = [];
67
+ let started = false;
68
+ for (const line of lines.slice(start + 1)) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed && !started)
71
+ continue;
72
+ if (!trimmed.startsWith("|")) {
73
+ if (started)
74
+ break;
75
+ continue;
76
+ }
77
+ started = true;
78
+ table.push(line);
79
+ }
80
+ return table;
81
+ }
82
+ function columnIndex(headers, predicate) {
83
+ return headers.findIndex(predicate);
84
+ }
85
+ export function parse_test_contract_records(changeRoot) {
86
+ const text = test_contract_text(changeRoot);
87
+ const table = tableLinesAfterHeading(text, [/^##\s+测试覆盖矩阵\s*$/i, /^##\s+test coverage matrix\s*$/i]);
88
+ if (table.length < 3)
89
+ return [];
90
+ const headers = parseMarkdownRow(table[0]).map(normalizeHeader);
91
+ const testIdx = columnIndex(headers, (header) => header === "test-id" || header === "test_id" || header === "testid");
92
+ const scenarioIdx = columnIndex(headers, (header) => header.includes("scenario") || header.includes("req") || header.includes("需求") || header.includes("关联"));
93
+ const invIdx = columnIndex(headers, (header) => header.includes("inv"));
94
+ if (testIdx < 0)
95
+ return [];
96
+ const records = [];
97
+ for (const line of table.slice(1)) {
98
+ const cells = parseMarkdownRow(line);
99
+ if (isSeparatorRow(cells))
100
+ continue;
101
+ const testId = cells[testIdx] ?? "";
102
+ if (!TEST_ID_RE.test(testId))
103
+ continue;
104
+ TEST_ID_RE.lastIndex = 0;
105
+ records.push({
106
+ row: records.length + 1,
107
+ test_id: testId.match(TEST_ID_RE)?.[0] ?? "",
108
+ scenario_ref: scenarioIdx >= 0 ? cells[scenarioIdx] ?? "" : "",
109
+ invariant_refs: invIdx >= 0 ? splitList(cells[invIdx] ?? "").filter((item) => item.startsWith("INV-")) : [],
110
+ });
111
+ }
112
+ return records;
113
+ }
114
+ export function parse_test_contract_ids(changeRoot) {
115
+ return new Set(parse_test_contract_records(changeRoot).map((record) => record.test_id).filter(Boolean));
116
+ }
117
+ export function test_contract_text(changeRoot) {
118
+ const filePath = sidecar_test_contract_path(changeRoot);
119
+ if (!existsSync(filePath) || !statSync(filePath).isFile())
120
+ return "";
121
+ return readFileSync(filePath, "utf8");
122
+ }
123
+ export function parse_spec_scenarios(changeRoot) {
124
+ const specs = join(changeRoot, "specs");
125
+ const scenarios = [];
126
+ if (!existsSync(specs) || !statSync(specs).isDirectory())
127
+ return scenarios;
128
+ for (const filePath of walkFiles(specs).sort()) {
129
+ if (!filePath.endsWith(".md"))
130
+ continue;
131
+ for (const line of readFileSync(filePath, "utf8").split(/\r?\n/)) {
132
+ const match = line.match(SCENARIO_RE);
133
+ if (match)
134
+ scenarios.push(match[1].trim());
135
+ }
136
+ }
137
+ return scenarios;
138
+ }
139
+ export function test_contract_covers_scenario(changeRoot, scenario) {
140
+ return parse_test_contract_records(changeRoot).some((record) => record.scenario_ref.includes(scenario));
141
+ }
142
+ export function test_contract_invariant_refs_by_test(changeRoot) {
143
+ const out = new Map();
144
+ for (const record of parse_test_contract_records(changeRoot)) {
145
+ const refs = out.get(record.test_id) ?? new Set();
146
+ for (const ref of record.invariant_refs)
147
+ refs.add(ref);
148
+ out.set(record.test_id, refs);
149
+ }
150
+ return out;
151
+ }
152
+ export function task_test_refs(tasks) {
153
+ const refs = new Set();
154
+ for (const task of Object.values(tasks)) {
155
+ for (const ref of splitList(task.attrs.test_refs ?? ""))
156
+ refs.add(ref);
157
+ }
158
+ return refs;
159
+ }
160
+ export function write_scope_conflict_reasons(tasks) {
161
+ const byGroup = {};
162
+ for (const [taskId, task] of Object.entries(tasks)) {
163
+ const group = task.attrs.parallel_group;
164
+ if (!group)
165
+ continue;
166
+ for (const scope of splitList(task.attrs.write_scope ?? "")) {
167
+ byGroup[group] ??= {};
168
+ byGroup[group][scope] ??= [];
169
+ byGroup[group][scope].push(taskId);
170
+ }
171
+ }
172
+ const problems = [];
173
+ for (const [group, scopes] of Object.entries(byGroup)) {
174
+ for (const [scope, taskIds] of Object.entries(scopes)) {
175
+ if (taskIds.length > 1)
176
+ problems.push(reason("write_scope_conflict", `parallel_group ${group} has overlapping write_scope ${scope}: ${renderList(taskIds)}`));
177
+ }
178
+ }
179
+ return problems;
180
+ }
181
+ export function red_green_test_ids(evidences) {
182
+ const ids = new Set();
183
+ for (const ev of live_pass(evidences, { kind: "test_run" })) {
184
+ if (typeof ev.test_id === "string" && ev.test_id)
185
+ ids.add(ev.test_id);
186
+ }
187
+ return ids;
188
+ }
189
+ export function task_test_evidence(evidences, taskId, semanticStatus, gate = null) {
190
+ return live_pass(evidences, { gate, kind: "test_run", task_id: taskId }).filter((ev) => ev.semantic_status === semanticStatus);
191
+ }
192
+ export function task_alternative_verification(evidences, taskId) {
193
+ return [
194
+ ...live_pass(evidences, { kind: "alternative_verification", task_id: taskId }),
195
+ ...live_pass(evidences, { kind: "manual_verification", task_id: taskId }),
196
+ ];
197
+ }
198
+ export function evidence_test_id_reasons(evidences, taskId, declared, contractIds, semanticStatus) {
199
+ const problems = [];
200
+ for (const ev of evidences) {
201
+ const tid = ev.test_id;
202
+ if (typeof tid !== "string" || !tid) {
203
+ problems.push(reason("missing_test_id", `task ${taskId} ${semanticStatus} evidence requires test_id`));
204
+ continue;
205
+ }
206
+ if (declared.size > 0 && !declared.has(tid)) {
207
+ problems.push(reason("test_contract_not_honored", `task ${taskId} ${semanticStatus} test_id ${tid} not in declared test_refs ${renderList([...declared].sort())}`));
208
+ }
209
+ if (contractIds.size > 0 && !contractIds.has(tid)) {
210
+ problems.push(reason("test_contract_not_honored", `task ${taskId} ${semanticStatus} test_id ${tid} not in test-contract ids ${renderList([...contractIds].sort())}`));
211
+ }
212
+ }
213
+ return problems;
214
+ }
215
+ export function declared_test_evidence_reasons(evidences, taskId, declared, semanticStatus) {
216
+ if (declared.size === 0)
217
+ return [];
218
+ const covered = new Set(evidences
219
+ .map((ev) => typeof ev.test_id === "string" ? ev.test_id : "")
220
+ .filter((testId) => declared.has(testId)));
221
+ const missing = [...declared].filter((testId) => !covered.has(testId)).sort();
222
+ if (missing.length === 0)
223
+ return [];
224
+ return [reason("missing_declared_test_evidence", `task ${taskId} ${semanticStatus} evidence missing declared test_refs: ${renderList(missing)}`, missing)];
225
+ }
@@ -0,0 +1,120 @@
1
+ export declare const SCHEMA_VERSION = 1;
2
+ export declare const GUARD_VERSION = "superspec-guard@1";
3
+ export declare const CONFIG_FILENAME = "config.yaml";
4
+ export declare const STATE_FILENAME = "superspec-state.json";
5
+ export declare const STATE_LOCK_FILENAME = "superspec-state.lock";
6
+ export declare const PROJECT_CONFIG_ALIASES: readonly [".superspec.yaml", ".superspec.yml", "superspec.yaml", "superspec.yml", "superspec.json", ".superspecrc", ".superspec/config.json"];
7
+ export declare const CHANGE_CONFIG_ALIASES: readonly [".superspec.yaml", ".superspec.yml", "superspec.yaml", "superspec.yml", "superspec.json", ".superspecrc", ".superspec/config.json"];
8
+ export declare const STATE_ALIASES: readonly [".superspec/state.json", ".superspec/state.lock", ".superspec/superspec_state.json", ".superspec/superspec-state.yaml"];
9
+ export declare const REQUIRED_SIDECAR_DIRS: readonly ["artifacts", "evidence/discovery", "evidence/design", "evidence/invariants", "evidence/test-contract", "evidence/tasks", "evidence/red", "evidence/green", "evidence/reviews", "evidence/verification", "evidence/archive", "handoffs", "reports", "raw"];
10
+ export type JsonMap = Record<string, any>;
11
+ export type Reason = {
12
+ code: string;
13
+ message: string;
14
+ refs: string[];
15
+ };
16
+ export type Decision = {
17
+ allowed: boolean;
18
+ decision: "allow" | "block" | "status";
19
+ change_id: string;
20
+ gate: string;
21
+ task_id: string | null;
22
+ openspec_status_summary: Record<string, string>;
23
+ superspec_gate_summary: Record<string, any>;
24
+ block_reasons: Reason[];
25
+ next_allowed_actions: string[];
26
+ trust_warnings: string[];
27
+ };
28
+ export type TaskInfo = {
29
+ task_id: string;
30
+ checked: boolean;
31
+ desc: string;
32
+ attrs: Record<string, string>;
33
+ };
34
+ export declare const BUILTIN_CONFIG: JsonMap;
35
+ export declare const ALLOWED_CONFIG_KEYS: Set<string>;
36
+ export declare const EVIDENCE_STATUSES: Set<string>;
37
+ export declare const EVIDENCE_KINDS: Set<string>;
38
+ export declare const HUMAN_CONFIRMATION_GATES: Set<string>;
39
+ export declare const NO_TDD_REASONS: Set<string>;
40
+ export declare const TDD_MODES: Set<string>;
41
+ export declare const ROLE_EVIDENCE_FIELDS: readonly ["agent_role", "agent_id", "prompt_ref", "output_ref", "source_anchors", "target_refs"];
42
+ export declare const SELF_REVIEW_MARKERS: Set<string>;
43
+ export declare const REVIEW_EVIDENCE_REQUIRED_FIELDS: readonly ["base_ref", "head_ref", "reviewed_files"];
44
+ export declare const VERIFY_EVIDENCE_REQUIRED_FIELDS: readonly ["openspec_validate_ref", "task_matrix_ref", "invariant_matrix_ref", "scope_drift_ref", "test_evidence_refs"];
45
+ export declare const REVIEW_GUIDANCE_ROLES: readonly ["code-reviewer", "architect", "critic"];
46
+ export declare const FINAL_VERIFICATION_ROLES: readonly ["verifier", "critic"];
47
+ export declare const SOURCE_GUIDANCE_REQUIRED_FIELDS: readonly ["source_refs", "required_load_refs", "required_claim_ids"];
48
+ export declare const MAIN_ADJUDICATION_REQUIRED_FIELDS: readonly ["output_ref", "source_evidence_refs", "verification_evidence_refs", "loaded_refs", "claim_adjudications", "finding_adjudications"];
49
+ export declare const MAIN_ADJUDICATION_DECISIONS: readonly ["allow", "request_changes"];
50
+ export declare const REQUEST_CHANGES_ROUTES: readonly ["reopen_tasks", "change_update"];
51
+ export declare const CLAIM_ADJUDICATION_DECISIONS: readonly ["accept", "reject", "needs_fix"];
52
+ export declare const FINDING_ADJUDICATION_DECISIONS: readonly ["dismissed", "accepted_fixed", "accepted_deviation", "needs_fix"];
53
+ export declare const FINAL_TEST_REQUIRED_FIELDS: readonly ["test_command", "output_ref"];
54
+ export declare const TASK_REOPEN_REQUIRED_FIELDS: readonly ["task_id", "reopen_id", "source_adjudication_evidence_id", "source_guidance_evidence_id", "violated_test_ids", "violated_requirement_refs", "invalidated_completion_evidence_ids", "required_supersede_evidence_ids", "completion_invalidity_class", "scope_expansion", "why_completion_invalid", "required_fix", "before_tasks_sha256", "after_tasks_sha256"];
55
+ export declare const TASK_REOPEN_RESOLVED_REQUIRED_FIELDS: readonly ["reopen_evidence_id", "reopen_id", "task_id", "successor_completion_evidence_ids", "after_tasks_sha256"];
56
+ export declare const TASK_REOPEN_INVALIDITY_CLASSES: readonly ["insufficient_completion_evidence"];
57
+ export declare const FORBIDDEN_FIELDS: Set<string>;
58
+ export declare const OPENSPEC_ARTIFACTS: Set<string>;
59
+ export declare const REQUIRED_OPENSPEC_CODEX_SKILLS: readonly ["openspec-explore", "openspec-propose", "openspec-apply-change", "openspec-archive-change"];
60
+ export declare const REQUIRED_SUPERSPEC_WORKFLOW_SKILLS: readonly ["superspec-explore", "superspec-propose", "superspec-apply", "superspec-review", "superspec-archive"];
61
+ export declare const REQUIRED_SUPERSPEC_AGENT_ROLES: readonly ["architect", "critic", "test-engineer", "code-reviewer", "verifier"];
62
+ export declare const REQUIRED_OPENSPEC_CLI_SURFACES: readonly [readonly ["instructions", "--help"], readonly ["archive", "--help"], readonly ["validate", "--help"], readonly ["status", "--help"]];
63
+ export declare const ARTIFACT_ENTER_GATE: Record<string, string>;
64
+ export declare const ROUTE_ORDER: Record<string, number>;
65
+ export declare const ROUTE_ALIASES: Record<string, string>;
66
+ export declare const GATE_ALIASES: Record<string, string>;
67
+ export declare const GATE_ROUTE: Record<string, string>;
68
+ export declare class GuardError extends Error {
69
+ }
70
+ export declare const runtime: JsonMap;
71
+ export declare function reason(code: string, message: string, refs?: string[] | null): Reason;
72
+ export declare function pinned_ref_key(item: JsonMap): string;
73
+ export declare function trustWarnings(): string[];
74
+ export declare function allow(change: string, gate: string, opts?: {
75
+ task_id?: string | null;
76
+ openspec_summary?: Record<string, string>;
77
+ gate_summary?: Record<string, any>;
78
+ }): Decision;
79
+ export declare function block(change: string, gate: string, reasons: Reason[], opts?: {
80
+ task_id?: string | null;
81
+ openspec_summary?: Record<string, string>;
82
+ gate_summary?: Record<string, any>;
83
+ next_actions?: string[];
84
+ }): Decision;
85
+ export declare function printDecision(decision: JsonMap): void;
86
+ export declare function runCommand(cmd: string, args: string[], opts?: {
87
+ cwd?: string;
88
+ timeout?: number;
89
+ platform?: NodeJS.Platform;
90
+ }): {
91
+ status: number | null;
92
+ stdout: string;
93
+ stderr: string;
94
+ error?: Error;
95
+ };
96
+ export declare function sha256_text(text: string): string;
97
+ export declare function sha256_file(filePath: string): string | null;
98
+ export declare function fingerprint_obj(obj: any): string;
99
+ export declare function safe_within(base: string, candidate: string): string | null;
100
+ export declare function now(): string;
101
+ export declare function isObject(value: any): value is JsonMap;
102
+ export declare function repr(value: any): string;
103
+ export declare function renderList(items: any[]): string;
104
+ export declare function walkFiles(root: string): string[];
105
+ export declare function toPosix(pathValue: string): string;
106
+ export declare function commandLookupInvocation(cmd: string, platform?: NodeJS.Platform): {
107
+ cmd: string;
108
+ args: string[];
109
+ shell: boolean;
110
+ };
111
+ export declare function windowsShellEscapeArg(arg: string): string;
112
+ export declare function windowsCmdShimInvocation(cmdPath: string, args: string[], comspec?: string): {
113
+ cmd: string;
114
+ args: string[];
115
+ };
116
+ export declare function commandExists(cmd: string, opts?: {
117
+ cwd?: string;
118
+ platform?: NodeJS.Platform;
119
+ }): boolean;
120
+ export declare function deepEqual(a: any, b: any): boolean;