@peterxiaoyang/superspec 0.1.13 → 0.1.15-alpha

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 (126) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +302 -0
  3. package/dist/format.d.ts +29 -0
  4. package/dist/format.js +141 -0
  5. package/dist/next.d.ts +3 -0
  6. package/dist/next.js +161 -0
  7. package/dist/openspec.d.ts +9 -0
  8. package/dist/openspec.js +41 -0
  9. package/dist/record.d.ts +20 -0
  10. package/dist/record.js +211 -0
  11. package/dist/skill_loop.d.ts +23 -0
  12. package/dist/skill_loop.js +58 -0
  13. package/dist/store.d.ts +31 -0
  14. package/dist/store.js +213 -0
  15. package/dist/sync.d.ts +3 -0
  16. package/dist/sync.js +135 -0
  17. package/dist/task.d.ts +7 -0
  18. package/dist/task.js +44 -0
  19. package/dist/transition.d.ts +34 -0
  20. package/dist/transition.js +400 -0
  21. package/dist/types.d.ts +138 -0
  22. package/dist/types.js +3 -0
  23. package/package.json +8 -53
  24. package/templates/workflow/skills/superspec-apply/SKILL.md +46 -118
  25. package/templates/workflow/skills/superspec-archive/SKILL.md +22 -47
  26. package/templates/workflow/skills/superspec-explore/SKILL.md +38 -69
  27. package/templates/workflow/skills/superspec-propose/SKILL.md +58 -57
  28. package/templates/workflow/skills/superspec-review/SKILL.md +36 -81
  29. package/adapters/codex/agents/architect.toml +0 -13
  30. package/adapters/codex/agents/code-reviewer.toml +0 -13
  31. package/adapters/codex/agents/critic.toml +0 -13
  32. package/adapters/codex/agents/executor.toml +0 -13
  33. package/adapters/codex/agents/explore.toml +0 -13
  34. package/adapters/codex/agents/test-engineer.toml +0 -13
  35. package/adapters/codex/agents/test-runner.toml +0 -13
  36. package/adapters/codex/agents/verifier.toml +0 -13
  37. package/adapters/codex/install-map.json +0 -116
  38. package/bin/launch.js +0 -37
  39. package/bin/superspec-check.js +0 -4
  40. package/bin/superspec-hook.js +0 -4
  41. package/bin/superspec-init.js +0 -4
  42. package/bin/superspec.js +0 -4
  43. package/dist/src/apply_worker_chain.d.ts +0 -60
  44. package/dist/src/apply_worker_chain.js +0 -1283
  45. package/dist/src/apply_worker_chain_lifecycle.d.ts +0 -21
  46. package/dist/src/apply_worker_chain_lifecycle.js +0 -409
  47. package/dist/src/archive.d.ts +0 -23
  48. package/dist/src/archive.js +0 -430
  49. package/dist/src/cli.d.ts +0 -1
  50. package/dist/src/cli.js +0 -33
  51. package/dist/src/cli_args.d.ts +0 -32
  52. package/dist/src/cli_args.js +0 -454
  53. package/dist/src/core.d.ts +0 -20
  54. package/dist/src/core.js +0 -394
  55. package/dist/src/disclosure.d.ts +0 -35
  56. package/dist/src/disclosure.js +0 -671
  57. package/dist/src/doctor.d.ts +0 -44
  58. package/dist/src/doctor.js +0 -230
  59. package/dist/src/evidence.d.ts +0 -29
  60. package/dist/src/evidence.js +0 -1107
  61. package/dist/src/gates.d.ts +0 -19
  62. package/dist/src/gates.js +0 -1631
  63. package/dist/src/git.d.ts +0 -8
  64. package/dist/src/git.js +0 -112
  65. package/dist/src/hooks/adapter.d.ts +0 -5
  66. package/dist/src/hooks/adapter.js +0 -347
  67. package/dist/src/hooks/guard_api.d.ts +0 -12
  68. package/dist/src/hooks/guard_api.js +0 -797
  69. package/dist/src/hooks/health.d.ts +0 -4
  70. package/dist/src/hooks/health.js +0 -86
  71. package/dist/src/hooks/policy_event.d.ts +0 -41
  72. package/dist/src/hooks/policy_event.js +0 -2634
  73. package/dist/src/hooks/types.d.ts +0 -72
  74. package/dist/src/hooks/types.js +0 -1
  75. package/dist/src/hooks/validation.d.ts +0 -3
  76. package/dist/src/hooks/validation.js +0 -70
  77. package/dist/src/i18n.d.ts +0 -21
  78. package/dist/src/i18n.js +0 -720
  79. package/dist/src/init_cli.d.ts +0 -28
  80. package/dist/src/init_cli.js +0 -399
  81. package/dist/src/install_engine.d.ts +0 -71
  82. package/dist/src/install_engine.js +0 -538
  83. package/dist/src/invariants.d.ts +0 -16
  84. package/dist/src/invariants.js +0 -363
  85. package/dist/src/openspec.d.ts +0 -37
  86. package/dist/src/openspec.js +0 -274
  87. package/dist/src/packet_render.d.ts +0 -5
  88. package/dist/src/packet_render.js +0 -1759
  89. package/dist/src/packet_schema.d.ts +0 -58
  90. package/dist/src/packet_schema.js +0 -1
  91. package/dist/src/paths.d.ts +0 -22
  92. package/dist/src/paths.js +0 -203
  93. package/dist/src/project_init.d.ts +0 -31
  94. package/dist/src/project_init.js +0 -167
  95. package/dist/src/self_update.d.ts +0 -14
  96. package/dist/src/self_update.js +0 -56
  97. package/dist/src/state.d.ts +0 -37
  98. package/dist/src/state.js +0 -464
  99. package/dist/src/tasks.d.ts +0 -38
  100. package/dist/src/tasks.js +0 -373
  101. package/dist/src/util.d.ts +0 -163
  102. package/dist/src/util.js +0 -792
  103. package/dist/superspec.d.ts +0 -6
  104. package/dist/superspec.js +0 -97
  105. package/dist/superspec_guard.d.ts +0 -4
  106. package/dist/superspec_guard.js +0 -19
  107. package/dist/superspec_hook.d.ts +0 -4
  108. package/dist/superspec_hook.js +0 -35
  109. package/dist/superspec_init.d.ts +0 -2
  110. package/dist/superspec_init.js +0 -17
  111. package/schemas/hook-event.schema.json +0 -27
  112. package/schemas/install-manifest.schema.json +0 -97
  113. package/templates/hooks/codex-hooks.json +0 -35
  114. package/templates/sidecar/archive-preservation.json +0 -11
  115. package/templates/sidecar/business-invariants.md +0 -38
  116. package/templates/sidecar/config.yaml +0 -13
  117. package/templates/sidecar/discovery.md +0 -33
  118. package/templates/sidecar/test-contract.md +0 -27
  119. package/templates/workflow/prompts/architect.md +0 -27
  120. package/templates/workflow/prompts/code-reviewer.md +0 -34
  121. package/templates/workflow/prompts/critic.md +0 -30
  122. package/templates/workflow/prompts/executor.md +0 -32
  123. package/templates/workflow/prompts/explore.md +0 -27
  124. package/templates/workflow/prompts/test-engineer.md +0 -28
  125. package/templates/workflow/prompts/test-runner.md +0 -35
  126. package/templates/workflow/prompts/verifier.md +0 -35
@@ -0,0 +1,41 @@
1
+ // SuperSpec 流程引擎 — OpenSpec 探测
2
+ import { execSync, execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ /** B2 修复:校验 change 字符集,防 shell 注入 */
7
+ function validateChange(change) {
8
+ if (!/^[A-Za-z0-9._-]+$/.test(change)) {
9
+ throw new Error(`change 名称含非法字符:${change}(只允许字母数字._-)`);
10
+ }
11
+ }
12
+ export function probeOpenSpec(projectRoot, change) {
13
+ try {
14
+ const version = execSync("openspec --version 2>/dev/null", { encoding: "utf8" }).trim();
15
+ let changeExists = false;
16
+ if (change) {
17
+ validateChange(change);
18
+ changeExists = existsSync(join(projectRoot, "openspec", "changes", change));
19
+ }
20
+ return { available: true, version, changeExists };
21
+ }
22
+ catch {
23
+ return { available: false, version: null, changeExists: false, error: "openspec CLI 不可用" };
24
+ }
25
+ }
26
+ export function openspecStatus(projectRoot, change) {
27
+ validateChange(change);
28
+ try {
29
+ // B2 修复:用 execFileSync 数组传参,彻底避免 shell 解析
30
+ const result = execFileSync("openspec", ["status", "--change", change, "--json"], {
31
+ encoding: "utf8", cwd: projectRoot, stdio: ["pipe", "pipe", "ignore"],
32
+ });
33
+ return "sha256:" + createHash("sha256").update(result).digest("hex");
34
+ }
35
+ catch {
36
+ return "sha256:unknown";
37
+ }
38
+ }
39
+ export function changeRoot(projectRoot, change) {
40
+ return join(projectRoot, "openspec", "changes", change);
41
+ }
@@ -0,0 +1,20 @@
1
+ import type { RecordResult, Job } from "./types.ts";
2
+ /** record job-submit:登记工作项结果 */
3
+ export declare function recordJobSubmit(projectRoot: string, change: string, changeRoot: string, jobId: string, reportFile: string): RecordResult;
4
+ /** record user-decision:登记用户决策 */
5
+ export declare function recordUserDecision(projectRoot: string, change: string, inputFile: string): RecordResult;
6
+ /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
7
+ export declare function jobsList(projectRoot: string, change: string): {
8
+ open: Job[];
9
+ accepted: Job[];
10
+ rejected: {
11
+ job_id: string;
12
+ role: string;
13
+ }[];
14
+ };
15
+ /** jobs packet:返回工作项执行说明 */
16
+ export declare function jobsPacket(projectRoot: string, change: string, jobId: string): {
17
+ found: boolean;
18
+ packet?: Record<string, unknown>;
19
+ message: string;
20
+ };
package/dist/record.js ADDED
@@ -0,0 +1,211 @@
1
+ // SuperSpec 流程引擎 — record:工作项结果登记
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, withLock, } from "./store.js";
5
+ /** 从 events 中查找 job(H4 修复:job 只在 transition_commit 的 new_jobs payload 里) */
6
+ function findJob(events, jobId) {
7
+ for (const ev of events) {
8
+ if (ev.event_type === "transition_commit") {
9
+ const newJobs = ev.payload.new_jobs ?? [];
10
+ const job = newJobs.find(j => j.job_id === jobId);
11
+ if (job)
12
+ return job;
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+ /** 检查 job 是否已终态 */
18
+ function jobTerminalState(events, jobId) {
19
+ for (const ev of events) {
20
+ if (ev.event_type === "job_accepted" && ev.payload.job_id === jobId)
21
+ return "accepted";
22
+ if (ev.event_type === "job_rejected" && ev.payload.job_id === jobId)
23
+ return "rejected";
24
+ }
25
+ return null;
26
+ }
27
+ /** record job-submit:登记工作项结果 */
28
+ export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFile) {
29
+ return withLock(projectRoot, change, () => {
30
+ ensureChangeLayout(projectRoot, change);
31
+ const events = readEvents(projectRoot, change);
32
+ // 查找 job
33
+ const job = findJob(events, jobId);
34
+ if (!job) {
35
+ return { event_type: "job_rejected", accepted: false, message: `工作项 ${jobId} 不存在` };
36
+ }
37
+ // 检查终态
38
+ const terminal = jobTerminalState(events, jobId);
39
+ if (terminal) {
40
+ // 幂等检查:同 report_digest → 返回旧结果
41
+ const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
42
+ const existing = events.find(e => (e.event_type === "job_accepted" || e.event_type === "job_rejected")
43
+ && e.payload.job_id === jobId
44
+ && e.payload.report_digest === reportDigest);
45
+ if (existing) {
46
+ return {
47
+ event_type: existing.event_type,
48
+ accepted: existing.event_type === "job_accepted",
49
+ message: "幂等返回:同 report 已提交",
50
+ job_state: existing.event_type === "job_accepted" ? "accepted" : "rejected",
51
+ };
52
+ }
53
+ // 终态 job + 不同 report → 拒绝
54
+ return {
55
+ event_type: "job_rejected",
56
+ accepted: false,
57
+ message: `工作项 ${jobId} 已终态(${terminal}),不接受新报告。需要新工作项请重跑 transition。`,
58
+ };
59
+ }
60
+ // 读报告
61
+ if (!existsSync(reportFile)) {
62
+ return { event_type: "job_rejected", accepted: false, message: `报告文件不存在:${reportFile}` };
63
+ }
64
+ const reportContent = readFileSync(reportFile, "utf8");
65
+ const reportDigest = sha256File(reportFile) ?? "sha256:unknown";
66
+ // acceptance checks
67
+ const checks = [];
68
+ // 0. 报告角色匹配(HIGH 5 修复)
69
+ try {
70
+ const report = JSON.parse(reportContent);
71
+ if (report.role && report.role !== job.role) {
72
+ checks.push(`报告角色 ${report.role} 与工作项角色 ${job.role} 不匹配`);
73
+ }
74
+ }
75
+ catch {
76
+ // 非 JSON 报告,Phase 1 允许(Phase 2 加正式 schema)
77
+ }
78
+ // 1. boundFiles 仍匹配当前文档(missing 也算不匹配)
79
+ for (const bf of job.boundFiles) {
80
+ const currentSha = sha256File(join(changeRoot, bf.path)) ?? "sha256:missing";
81
+ if (currentSha !== bf.sha) {
82
+ checks.push(`绑定文件 ${bf.path} 已变化(${bf.sha} → ${currentSha})`);
83
+ }
84
+ }
85
+ // 2. 报告格式基本校验(非空 JSON 或文本)
86
+ if (!reportContent.trim()) {
87
+ checks.push("报告内容为空");
88
+ }
89
+ if (checks.length > 0) {
90
+ // 拒绝
91
+ const rejectEvent = makeEvent(change, "job_rejected", {
92
+ job_id: jobId,
93
+ role: job.role,
94
+ report_digest: reportDigest,
95
+ reason: checks.join("; "),
96
+ });
97
+ appendEvent(projectRoot, change, rejectEvent);
98
+ return {
99
+ event_type: "job_rejected",
100
+ accepted: false,
101
+ message: `工作项 ${jobId} 被拒绝:${checks.join("; ")}`,
102
+ job_state: "rejected",
103
+ };
104
+ }
105
+ // 接受
106
+ const acceptEvent = makeEvent(change, "job_accepted", {
107
+ job_id: jobId,
108
+ role: job.role,
109
+ report_digest: reportDigest,
110
+ accepted_at: new Date().toISOString(),
111
+ });
112
+ appendEvent(projectRoot, change, acceptEvent);
113
+ return {
114
+ event_type: "job_accepted",
115
+ accepted: true,
116
+ message: `工作项 ${jobId}(${job.role})已接受`,
117
+ job_state: "accepted",
118
+ };
119
+ });
120
+ }
121
+ /** record user-decision:登记用户决策 */
122
+ export function recordUserDecision(projectRoot, change, inputFile) {
123
+ return withLock(projectRoot, change, () => {
124
+ ensureChangeLayout(projectRoot, change);
125
+ if (!existsSync(inputFile)) {
126
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "file_not_found", path: inputFile }));
127
+ return { event_type: "user_decision_recorded", accepted: false, message: `决策文件不存在:${inputFile}` };
128
+ }
129
+ const content = readFileSync(inputFile, "utf8");
130
+ let decision;
131
+ try {
132
+ decision = JSON.parse(content);
133
+ }
134
+ catch {
135
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "invalid_json" }));
136
+ return { event_type: "user_decision_recorded", accepted: false, message: "决策文件不是有效 JSON" };
137
+ }
138
+ if (!decision.scope || !decision.answer) {
139
+ appendEvent(projectRoot, change, makeEvent(change, "user_decision_recorded", { accepted: false, reason: "missing_scope_or_answer" }));
140
+ return { event_type: "user_decision_recorded", accepted: false, message: "决策文件缺少 scope 或 answer" };
141
+ }
142
+ const event = makeEvent(change, "user_decision_recorded", {
143
+ scope: decision.scope,
144
+ question: decision.question ?? "",
145
+ answer: decision.answer,
146
+ input_digest: sha256File(inputFile) ?? "sha256:unknown",
147
+ });
148
+ appendEvent(projectRoot, change, event);
149
+ return {
150
+ event_type: "user_decision_recorded",
151
+ accepted: true,
152
+ message: `用户决策已登记:scope=${decision.scope}`,
153
+ };
154
+ });
155
+ }
156
+ /** jobs list(HIGH-1 修复:从 transition_commit.new_jobs 提取,不再依赖已删除的 job_requested 事件) */
157
+ export function jobsList(projectRoot, change) {
158
+ const events = readEvents(projectRoot, change);
159
+ const open = [];
160
+ const accepted = [];
161
+ const rejected = [];
162
+ for (const ev of events) {
163
+ if (ev.event_type === "transition_commit") {
164
+ const newJobs = ev.payload.new_jobs ?? [];
165
+ for (const job of newJobs) {
166
+ if (!open.some(j => j.job_id === job.job_id) && !accepted.some(j => j.job_id === job.job_id) && !rejected.some(r => r.job_id === job.job_id)) {
167
+ open.push(job);
168
+ }
169
+ }
170
+ }
171
+ else if (ev.event_type === "job_accepted") {
172
+ const { job_id } = ev.payload;
173
+ const idx = open.findIndex(j => j.job_id === job_id);
174
+ if (idx >= 0) {
175
+ const job = open.splice(idx, 1)[0];
176
+ job.state = "accepted";
177
+ accepted.push(job);
178
+ }
179
+ }
180
+ else if (ev.event_type === "job_rejected") {
181
+ const { job_id, role } = ev.payload;
182
+ const idx = open.findIndex(j => j.job_id === job_id);
183
+ if (idx >= 0)
184
+ open.splice(idx, 1)[0];
185
+ rejected.push({ job_id, role });
186
+ }
187
+ }
188
+ return { open, accepted, rejected };
189
+ }
190
+ /** jobs packet:返回工作项执行说明 */
191
+ export function jobsPacket(projectRoot, change, jobId) {
192
+ const events = readEvents(projectRoot, change);
193
+ const job = findJob(events, jobId);
194
+ if (!job) {
195
+ return { found: false, message: `工作项 ${jobId} 不存在` };
196
+ }
197
+ return {
198
+ found: true,
199
+ packet: {
200
+ job_id: job.job_id,
201
+ role: job.role,
202
+ boundFiles: job.boundFiles,
203
+ packet_digest: job.packet_digest,
204
+ required_output_kind: "report",
205
+ output_instructions: `请审查 ${job.boundFiles.map(f => f.path).join(", ")},产出审查报告`,
206
+ stop_conditions: ["审查完成后提交报告,不要修改文档"],
207
+ created_from_transition: job.created_from_transition,
208
+ },
209
+ message: `工作项 ${jobId} 的执行说明`,
210
+ };
211
+ }
@@ -0,0 +1,23 @@
1
+ import type { NextOutput } from "./types.ts";
2
+ export interface LoopStep {
3
+ step: number;
4
+ state: string;
5
+ path: string;
6
+ action: string;
7
+ detail?: string;
8
+ }
9
+ export interface LoopResult {
10
+ completed: boolean;
11
+ steps: LoopStep[];
12
+ finalState: string;
13
+ message: string;
14
+ }
15
+ /**
16
+ * 执行 agent 循环逻辑(不实际执行命令,只模拟状态推进)。
17
+ * 用于测试 next 返回值链是否自洽。
18
+ *
19
+ * @param nextFn next 函数(可 mock)
20
+ * @param executeFn 命令执行函数(可 mock,返回是否成功)
21
+ * @param maxSteps 防无限循环
22
+ */
23
+ export declare function simulateLoop(nextFn: () => NextOutput, executeFn: (output: NextOutput) => boolean, maxSteps?: number): LoopResult;
@@ -0,0 +1,58 @@
1
+ // SuperSpec 流程引擎 — skill-loop:薄适配循环驱动
2
+ /**
3
+ * skillLoop 是 agent 适配层的核心函数。
4
+ * 它不是 CLI 命令(实际 agent 循环由 LLM 驱动),
5
+ * 而是一个可测试的逻辑函数,验证 next→execute→record 循环的正确性。
6
+ */
7
+ import { next } from "./next.js";
8
+ /**
9
+ * 执行 agent 循环逻辑(不实际执行命令,只模拟状态推进)。
10
+ * 用于测试 next 返回值链是否自洽。
11
+ *
12
+ * @param nextFn next 函数(可 mock)
13
+ * @param executeFn 命令执行函数(可 mock,返回是否成功)
14
+ * @param maxSteps 防无限循环
15
+ */
16
+ export function simulateLoop(nextFn, executeFn, maxSteps = 20) {
17
+ const steps = [];
18
+ let completed = false;
19
+ for (let i = 0; i < maxSteps; i++) {
20
+ const output = nextFn();
21
+ const step = {
22
+ step: i + 1,
23
+ state: output.state,
24
+ path: output.path,
25
+ action: "",
26
+ };
27
+ switch (output.path) {
28
+ case "done":
29
+ step.action = "stop";
30
+ step.detail = output.reason;
31
+ steps.push(step);
32
+ completed = true;
33
+ return { completed, steps, finalState: output.state, message: output.reason };
34
+ case "next_command":
35
+ step.action = "execute";
36
+ step.detail = output.next_command;
37
+ steps.push(step);
38
+ if (!executeFn(output)) {
39
+ return { completed: false, steps, finalState: output.state, message: `执行失败:${output.next_command}` };
40
+ }
41
+ break;
42
+ case "required_job":
43
+ step.action = "job";
44
+ step.detail = output.required_jobs.map(j => `${j.role}(${j.job_id})`).join(", ");
45
+ steps.push(step);
46
+ if (!executeFn(output)) {
47
+ return { completed: false, steps, finalState: output.state, message: `工作项执行失败` };
48
+ }
49
+ break;
50
+ case "ask_user":
51
+ step.action = "ask";
52
+ step.detail = output.ask_user.question;
53
+ steps.push(step);
54
+ return { completed: false, steps, finalState: output.state, message: `需要用户确认:${output.ask_user.question}` };
55
+ }
56
+ }
57
+ return { completed: false, steps, finalState: steps[steps.length - 1]?.state ?? "unknown", message: "达到最大步数" };
58
+ }
@@ -0,0 +1,31 @@
1
+ import type { Event, Snapshot, Ref } from "./types.ts";
2
+ export declare function engineRoot(projectRoot: string): string;
3
+ export declare function changeDir(projectRoot: string, change: string): string;
4
+ export declare function eventsFile(projectRoot: string, change: string): string;
5
+ export declare function snapshotFile(projectRoot: string, change: string): string;
6
+ export declare function lockFile(projectRoot: string, change: string): string;
7
+ export declare function stagingDir(projectRoot: string, change: string, transitionId: string): string;
8
+ export declare function jobsDir(projectRoot: string, change: string): string;
9
+ export declare function ensureChangeLayout(projectRoot: string, change: string): void;
10
+ export declare function sha256Text(text: string): string;
11
+ export declare function sha256File(filePath: string): string | null;
12
+ export declare function computeDocumentDigests(changeRoot: string, docPaths: string[]): Record<string, string>;
13
+ export declare function digestOf(obj: unknown): string;
14
+ export declare function appendEvent(projectRoot: string, change: string, event: Event): void;
15
+ export declare function readEvents(projectRoot: string, change: string): Event[];
16
+ export declare function makeEvent(change: string, eventType: Event["event_type"], payload: Record<string, unknown>, opts?: {
17
+ transitionId?: string | null;
18
+ idempotencyKey?: string | null;
19
+ prevSnapshotDigest?: string | null;
20
+ inputRefs?: Ref[];
21
+ outputRefs?: Ref[];
22
+ }): Event;
23
+ export declare function eventsDigest(events: Event[]): string;
24
+ export declare function writeSnapshot(projectRoot: string, change: string, snapshot: Snapshot): void;
25
+ export declare function readSnapshot(projectRoot: string, change: string): Snapshot | null;
26
+ export declare function snapshotDigest(snapshot: Snapshot): string;
27
+ export declare function acquireLock(projectRoot: string, change: string): void;
28
+ export declare function releaseLock(projectRoot: string, change: string): void;
29
+ export declare function withLock<T>(projectRoot: string, change: string, fn: () => T): T;
30
+ export declare function idempotencyKey(transitionName: string, inputs: Record<string, unknown>, prevSnapshotDigest: string | null): string;
31
+ export declare function ref(path: string, projectRoot: string): Ref;
package/dist/store.js ADDED
@@ -0,0 +1,213 @@
1
+ // SuperSpec 流程引擎 — 存储层:路径、指纹、事件日志、快照、锁
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, openSync, closeSync, unlinkSync, statSync, renameSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { hostname } from "node:os";
6
+ // ===== 路径 =====
7
+ export function engineRoot(projectRoot) {
8
+ return join(projectRoot, ".superspec");
9
+ }
10
+ export function changeDir(projectRoot, change) {
11
+ return join(engineRoot(projectRoot), "changes", change);
12
+ }
13
+ export function eventsFile(projectRoot, change) {
14
+ return join(changeDir(projectRoot, change), "events.jsonl");
15
+ }
16
+ export function snapshotFile(projectRoot, change) {
17
+ return join(changeDir(projectRoot, change), "snapshot.json");
18
+ }
19
+ export function lockFile(projectRoot, change) {
20
+ return join(changeDir(projectRoot, change), "lock");
21
+ }
22
+ export function stagingDir(projectRoot, change, transitionId) {
23
+ return join(changeDir(projectRoot, change), "staging", transitionId);
24
+ }
25
+ export function jobsDir(projectRoot, change) {
26
+ return join(changeDir(projectRoot, change), "jobs");
27
+ }
28
+ export function ensureChangeLayout(projectRoot, change) {
29
+ const dir = changeDir(projectRoot, change);
30
+ mkdirSync(dir, { recursive: true });
31
+ mkdirSync(join(dir, "jobs"), { recursive: true });
32
+ mkdirSync(join(dir, "staging"), { recursive: true });
33
+ mkdirSync(join(dir, "raw"), { recursive: true });
34
+ const ef = eventsFile(projectRoot, change);
35
+ if (!existsSync(ef))
36
+ writeFileSync(ef, "", "utf8");
37
+ }
38
+ // ===== 指纹 =====
39
+ export function sha256Text(text) {
40
+ return "sha256:" + createHash("sha256").update(text, "utf8").digest("hex");
41
+ }
42
+ export function sha256File(filePath) {
43
+ if (!existsSync(filePath) || !statSync(filePath).isFile())
44
+ return null;
45
+ return "sha256:" + createHash("sha256").update(readFileSync(filePath)).digest("hex");
46
+ }
47
+ export function computeDocumentDigests(changeRoot, docPaths) {
48
+ const digests = {};
49
+ for (const p of docPaths) {
50
+ const full = join(changeRoot, p);
51
+ digests[p] = sha256File(full) ?? "sha256:missing";
52
+ }
53
+ return digests;
54
+ }
55
+ export function digestOf(obj) {
56
+ const keys = Object.keys(obj).sort();
57
+ return sha256Text(JSON.stringify(obj, keys));
58
+ }
59
+ // ===== 事件日志 =====
60
+ let eventSeq = 0;
61
+ export function appendEvent(projectRoot, change, event) {
62
+ const line = JSON.stringify(event) + "\n";
63
+ const ef = eventsFile(projectRoot, change);
64
+ // append
65
+ const fd = openSync(ef, "a");
66
+ try {
67
+ writeFileSync(fd, line, "utf8");
68
+ }
69
+ finally {
70
+ closeSync(fd);
71
+ }
72
+ }
73
+ export function readEvents(projectRoot, change) {
74
+ const ef = eventsFile(projectRoot, change);
75
+ if (!existsSync(ef))
76
+ return [];
77
+ const content = readFileSync(ef, "utf8");
78
+ // H3 修复:逐行解析,跳过损坏行(截断 JSON),不整体崩溃
79
+ const events = [];
80
+ for (const line of content.split("\n")) {
81
+ if (!line.trim())
82
+ continue;
83
+ try {
84
+ events.push(JSON.parse(line));
85
+ }
86
+ catch {
87
+ // 截断/损坏行跳过(崩溃恢复:最后一条可能是半写入的)
88
+ }
89
+ }
90
+ return events;
91
+ }
92
+ export function makeEvent(change, eventType, payload, opts = {}) {
93
+ const base = {
94
+ change_id: change,
95
+ event_type: eventType,
96
+ transition_id: opts.transitionId ?? null,
97
+ idempotency_key: opts.idempotencyKey ?? null,
98
+ created_at: new Date().toISOString(),
99
+ actor: process.env.SUPERSPEC_ACTOR ?? "cli",
100
+ prev_snapshot_digest: opts.prevSnapshotDigest ?? null,
101
+ input_refs: opts.inputRefs ?? [],
102
+ output_refs: opts.outputRefs ?? [],
103
+ payload,
104
+ };
105
+ const event_digest = digestOf(base);
106
+ const event_id = `EVT-${Date.now()}-${++eventSeq}`;
107
+ return { event_id, ...base, event_digest };
108
+ }
109
+ export function eventsDigest(events) {
110
+ return sha256Text(events.map(e => e.event_digest).join("\n"));
111
+ }
112
+ // ===== 快照 =====
113
+ export function writeSnapshot(projectRoot, change, snapshot) {
114
+ writeFileSync(snapshotFile(projectRoot, change), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
115
+ }
116
+ export function readSnapshot(projectRoot, change) {
117
+ const sf = snapshotFile(projectRoot, change);
118
+ if (!existsSync(sf))
119
+ return null;
120
+ try {
121
+ return JSON.parse(readFileSync(sf, "utf8"));
122
+ }
123
+ catch {
124
+ // H3 修复:损坏的 snapshot.json 返回 null(让调用方走 sync 重建)
125
+ return null;
126
+ }
127
+ }
128
+ export function snapshotDigest(snapshot) {
129
+ return sha256Text(JSON.stringify({
130
+ state: snapshot.state,
131
+ events_digest: snapshot.events_digest,
132
+ document_digests: snapshot.document_digests,
133
+ tasks_structure_digest: snapshot.tasks_structure_digest,
134
+ open_jobs: snapshot.open_jobs.map(j => j.job_id),
135
+ accepted_jobs: snapshot.accepted_jobs.map(j => j.job_id),
136
+ // H5 修复:纳入 task 状态(否则幂等键不反映任务世界状态)
137
+ active_task_attempts: (snapshot.active_task_attempts ?? []).map(a => a.attempt_id),
138
+ task_statuses: snapshot.task_statuses,
139
+ }));
140
+ }
141
+ // ===== 锁(per-change 文件锁,PID + staleness 回收)=====
142
+ const LOCK_STALE_MS = 5 * 60 * 1000;
143
+ export function acquireLock(projectRoot, change) {
144
+ ensureChangeLayout(projectRoot, change); // 确保 change 目录存在(含 events.jsonl)
145
+ const lf = lockFile(projectRoot, change);
146
+ const info = {
147
+ pid: process.pid,
148
+ hostname: hostname(),
149
+ created_at: new Date().toISOString(),
150
+ };
151
+ for (let attempt = 0; attempt < 2; attempt++) {
152
+ try {
153
+ const fd = openSync(lf, "wx");
154
+ try {
155
+ writeFileSync(fd, JSON.stringify(info) + "\n", "utf8");
156
+ }
157
+ finally {
158
+ closeSync(fd);
159
+ }
160
+ return;
161
+ }
162
+ catch (err) {
163
+ if (err.code !== "EEXIST")
164
+ throw err;
165
+ // 尝试回收 stale lock
166
+ const existing = readLockInfo(lf);
167
+ if (existing && isLockStale(existing)) {
168
+ unlinkSync(lf);
169
+ continue; // retry
170
+ }
171
+ throw new Error(`Lock contention: ${lf} held by pid=${existing?.pid} on ${existing?.hostname}. ` +
172
+ `If no engine process is active, remove the lock file manually.`);
173
+ }
174
+ }
175
+ throw new Error(`Failed to acquire lock after stale recovery: ${lf}`);
176
+ }
177
+ export function releaseLock(projectRoot, change) {
178
+ try {
179
+ unlinkSync(lockFile(projectRoot, change));
180
+ }
181
+ catch { /* best effort */ }
182
+ }
183
+ function readLockInfo(lf) {
184
+ try {
185
+ return JSON.parse(readFileSync(lf, "utf8"));
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ function isLockStale(info) {
192
+ const age = Date.now() - new Date(info.created_at).getTime();
193
+ return age > LOCK_STALE_MS;
194
+ }
195
+ export function withLock(projectRoot, change, fn) {
196
+ acquireLock(projectRoot, change);
197
+ try {
198
+ return fn();
199
+ }
200
+ finally {
201
+ releaseLock(projectRoot, change);
202
+ }
203
+ }
204
+ // ===== 幂等键 =====
205
+ export function idempotencyKey(transitionName, inputs, prevSnapshotDigest) {
206
+ // strip 时间戳/审计字段;保留安全前提 digest
207
+ const stripped = { transition: transitionName, ...inputs, _world: prevSnapshotDigest };
208
+ return sha256Text(JSON.stringify(stripped, Object.keys(stripped).sort()));
209
+ }
210
+ // ===== 工具 =====
211
+ export function ref(path, projectRoot) {
212
+ return { path, sha: sha256File(join(projectRoot, path)) ?? "sha256:missing" };
213
+ }
package/dist/sync.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Snapshot } from "./types.ts";
2
+ /** 重建 snapshot(sync 的核心) */
3
+ export declare function rebuildSnapshot(projectRoot: string, change: string, changeRoot: string, openspecStatusDigest?: string): Snapshot;