@peterxiaoyang/superspec 0.1.14 → 0.1.16-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.
- package/README.md +37 -36
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +301 -0
- package/dist/format.d.ts +29 -0
- package/dist/format.js +141 -0
- package/dist/install.d.ts +18 -0
- package/dist/install.js +161 -0
- package/dist/next.d.ts +3 -0
- package/dist/next.js +161 -0
- package/dist/openspec.d.ts +9 -0
- package/dist/openspec.js +41 -0
- package/dist/record.d.ts +20 -0
- package/dist/record.js +266 -0
- package/dist/skill_loop.d.ts +23 -0
- package/dist/skill_loop.js +58 -0
- package/dist/store.d.ts +31 -0
- package/dist/store.js +213 -0
- package/dist/sync.d.ts +3 -0
- package/dist/sync.js +135 -0
- package/dist/task.d.ts +7 -0
- package/dist/task.js +44 -0
- package/dist/transition.d.ts +34 -0
- package/dist/transition.js +400 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.js +3 -0
- package/package.json +8 -53
- package/templates/workflow/agents/architect.toml +13 -0
- package/{adapters/codex → templates/workflow}/agents/code-reviewer.toml +2 -2
- package/templates/workflow/agents/critic.toml +13 -0
- package/{adapters/codex → templates/workflow}/agents/executor.toml +2 -2
- package/{adapters/codex → templates/workflow}/agents/explore.toml +3 -3
- package/templates/workflow/agents/final-audit.toml +13 -0
- package/templates/workflow/agents/proposal-auditor.toml +13 -0
- package/{adapters/codex → templates/workflow}/agents/test-engineer.toml +4 -4
- package/{adapters/codex → templates/workflow}/agents/test-runner.toml +2 -2
- package/{adapters/codex → templates/workflow}/agents/verifier.toml +2 -2
- package/templates/workflow/prompts/architect.md +16 -0
- package/templates/workflow/prompts/critic.md +16 -0
- package/templates/workflow/prompts/explore.md +1 -1
- package/templates/workflow/prompts/final-audit.md +32 -0
- package/templates/workflow/prompts/proposal-auditor.md +34 -0
- package/templates/workflow/prompts/test-engineer.md +16 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +39 -121
- package/templates/workflow/skills/superspec-archive/SKILL.md +17 -49
- package/templates/workflow/skills/superspec-explore/SKILL.md +34 -70
- package/templates/workflow/skills/superspec-propose/SKILL.md +57 -60
- package/templates/workflow/skills/superspec-review/SKILL.md +18 -85
- package/adapters/codex/agents/architect.toml +0 -13
- package/adapters/codex/agents/critic.toml +0 -13
- package/adapters/codex/install-map.json +0 -116
- package/bin/launch.js +0 -37
- package/bin/superspec-check.js +0 -4
- package/bin/superspec-hook.js +0 -4
- package/bin/superspec-init.js +0 -4
- package/bin/superspec.js +0 -4
- package/dist/src/apply_worker_chain.d.ts +0 -60
- package/dist/src/apply_worker_chain.js +0 -1283
- package/dist/src/apply_worker_chain_lifecycle.d.ts +0 -21
- package/dist/src/apply_worker_chain_lifecycle.js +0 -409
- package/dist/src/archive.d.ts +0 -23
- package/dist/src/archive.js +0 -430
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -33
- package/dist/src/cli_args.d.ts +0 -32
- package/dist/src/cli_args.js +0 -454
- package/dist/src/core.d.ts +0 -20
- package/dist/src/core.js +0 -394
- package/dist/src/disclosure.d.ts +0 -35
- package/dist/src/disclosure.js +0 -671
- package/dist/src/doctor.d.ts +0 -44
- package/dist/src/doctor.js +0 -230
- package/dist/src/evidence.d.ts +0 -29
- package/dist/src/evidence.js +0 -1107
- package/dist/src/gates.d.ts +0 -19
- package/dist/src/gates.js +0 -1631
- package/dist/src/git.d.ts +0 -8
- package/dist/src/git.js +0 -112
- package/dist/src/hooks/adapter.d.ts +0 -5
- package/dist/src/hooks/adapter.js +0 -347
- package/dist/src/hooks/guard_api.d.ts +0 -12
- package/dist/src/hooks/guard_api.js +0 -797
- package/dist/src/hooks/health.d.ts +0 -4
- package/dist/src/hooks/health.js +0 -86
- package/dist/src/hooks/policy_event.d.ts +0 -41
- package/dist/src/hooks/policy_event.js +0 -2634
- package/dist/src/hooks/types.d.ts +0 -72
- package/dist/src/hooks/types.js +0 -1
- package/dist/src/hooks/validation.d.ts +0 -3
- package/dist/src/hooks/validation.js +0 -70
- package/dist/src/i18n.d.ts +0 -21
- package/dist/src/i18n.js +0 -720
- package/dist/src/init_cli.d.ts +0 -28
- package/dist/src/init_cli.js +0 -404
- package/dist/src/install_engine.d.ts +0 -71
- package/dist/src/install_engine.js +0 -538
- package/dist/src/invariants.d.ts +0 -16
- package/dist/src/invariants.js +0 -363
- package/dist/src/openspec.d.ts +0 -37
- package/dist/src/openspec.js +0 -274
- package/dist/src/packet_render.d.ts +0 -5
- package/dist/src/packet_render.js +0 -1767
- package/dist/src/packet_schema.d.ts +0 -59
- package/dist/src/packet_schema.js +0 -1
- package/dist/src/paths.d.ts +0 -22
- package/dist/src/paths.js +0 -203
- package/dist/src/project_init.d.ts +0 -33
- package/dist/src/project_init.js +0 -191
- package/dist/src/self_update.d.ts +0 -14
- package/dist/src/self_update.js +0 -56
- package/dist/src/state.d.ts +0 -37
- package/dist/src/state.js +0 -464
- package/dist/src/tasks.d.ts +0 -38
- package/dist/src/tasks.js +0 -373
- package/dist/src/util.d.ts +0 -163
- package/dist/src/util.js +0 -792
- package/dist/superspec.d.ts +0 -6
- package/dist/superspec.js +0 -97
- package/dist/superspec_guard.d.ts +0 -4
- package/dist/superspec_guard.js +0 -19
- package/dist/superspec_hook.d.ts +0 -4
- package/dist/superspec_hook.js +0 -35
- package/dist/superspec_init.d.ts +0 -2
- package/dist/superspec_init.js +0 -17
- package/schemas/hook-event.schema.json +0 -27
- package/schemas/install-manifest.schema.json +0 -97
- package/templates/hooks/codex-hooks.json +0 -35
- package/templates/sidecar/archive-preservation.json +0 -11
- package/templates/sidecar/business-invariants.md +0 -38
- package/templates/sidecar/config.yaml +0 -13
- package/templates/sidecar/discovery.md +0 -33
- package/templates/sidecar/test-contract.md +0 -27
|
@@ -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
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -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
package/dist/sync.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SuperSpec 流程引擎 — sync:从当前文档 + events.jsonl 重建 snapshot
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readEvents, eventsDigest, computeDocumentDigests, sha256File, sha256Text, ensureChangeLayout, } from "./store.js";
|
|
5
|
+
const TRACKED_DOCS = [
|
|
6
|
+
"proposal.md", "design.md", "tasks.md",
|
|
7
|
+
".superspec/artifacts/discovery.md",
|
|
8
|
+
".superspec/artifacts/business-invariants.md",
|
|
9
|
+
".superspec/artifacts/test-contract.md",
|
|
10
|
+
];
|
|
11
|
+
/** 从 events.jsonl 推导:当前状态、jobs、attempts、pending decisions */
|
|
12
|
+
function replayEvents(events) {
|
|
13
|
+
let state = "init";
|
|
14
|
+
const openJobs = [];
|
|
15
|
+
const acceptedJobs = [];
|
|
16
|
+
const activeAttempts = [];
|
|
17
|
+
const taskStatuses = {};
|
|
18
|
+
let lastTransition = null;
|
|
19
|
+
for (const ev of events) {
|
|
20
|
+
switch (ev.event_type) {
|
|
21
|
+
case "transition_commit": {
|
|
22
|
+
const p = ev.payload;
|
|
23
|
+
state = p.to_state;
|
|
24
|
+
if (ev.transition_id)
|
|
25
|
+
lastTransition = ev.transition_id;
|
|
26
|
+
// HIGH 6 修复:从 commit payload replay job
|
|
27
|
+
const created = ev.payload.created_job_ids ?? [];
|
|
28
|
+
const payloadJobs = ev.payload.new_jobs ?? [];
|
|
29
|
+
for (const jid of created) {
|
|
30
|
+
const job = payloadJobs.find(j => j.job_id === jid);
|
|
31
|
+
if (job && !openJobs.some(j => j.job_id === jid))
|
|
32
|
+
openJobs.push(job);
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
// H4 修复:删除 job_requested / job_invalidated 死分支——job 只通过 transition_commit 的 new_jobs payload 创建,staleness 只在 sync 内存计算(不写事件)
|
|
37
|
+
case "job_accepted": {
|
|
38
|
+
const { job_id } = ev.payload;
|
|
39
|
+
const idx = openJobs.findIndex(j => j.job_id === job_id);
|
|
40
|
+
if (idx >= 0) {
|
|
41
|
+
const job = openJobs.splice(idx, 1)[0];
|
|
42
|
+
job.state = "accepted";
|
|
43
|
+
acceptedJobs.push(job);
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case "job_rejected": {
|
|
48
|
+
const { job_id } = ev.payload;
|
|
49
|
+
const idx = openJobs.findIndex(j => j.job_id === job_id);
|
|
50
|
+
if (idx >= 0) {
|
|
51
|
+
openJobs.splice(idx, 1)[0]; // remove from open
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
// H4:job_invalidated 事件从不发射(删除死分支)
|
|
56
|
+
case "task_started": {
|
|
57
|
+
const attempt = ev.payload;
|
|
58
|
+
activeAttempts.push(attempt);
|
|
59
|
+
taskStatuses[attempt.task_id] = "doing";
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "task_completed": {
|
|
63
|
+
const { task_id, attempt_id } = ev.payload;
|
|
64
|
+
const idx = activeAttempts.findIndex(a => a.attempt_id === attempt_id);
|
|
65
|
+
if (idx >= 0)
|
|
66
|
+
activeAttempts[idx].state = "closed";
|
|
67
|
+
taskStatuses[task_id] = "done";
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "task_abandoned": {
|
|
71
|
+
const { attempt_id } = ev.payload;
|
|
72
|
+
const idx = activeAttempts.findIndex(a => a.attempt_id === attempt_id);
|
|
73
|
+
if (idx >= 0)
|
|
74
|
+
activeAttempts.splice(idx, 1);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition };
|
|
80
|
+
}
|
|
81
|
+
/** 粗粒度失效:检查 accepted job 的 boundFiles 是否仍匹配当前文档 */
|
|
82
|
+
function checkStaleJobs(acceptedJobs, documentDigests) {
|
|
83
|
+
const stale = [];
|
|
84
|
+
for (const job of acceptedJobs) {
|
|
85
|
+
for (const bf of job.boundFiles) {
|
|
86
|
+
const current = documentDigests[bf.path];
|
|
87
|
+
if (current && current !== bf.sha) {
|
|
88
|
+
stale.push({
|
|
89
|
+
job_id: job.job_id,
|
|
90
|
+
reason: `绑定文件 ${bf.path} 已变化(${bf.sha} → ${current})`,
|
|
91
|
+
});
|
|
92
|
+
break; // 一个文件变就够了
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return stale;
|
|
97
|
+
}
|
|
98
|
+
/** 解析 tasks.md 结构指纹(复选框归一化) */
|
|
99
|
+
function tasksStructureDigest(changeRoot) {
|
|
100
|
+
const tasksPath = join(changeRoot, "tasks.md");
|
|
101
|
+
if (!existsSync(tasksPath))
|
|
102
|
+
return null;
|
|
103
|
+
const content = readFileSync(tasksPath, "utf8");
|
|
104
|
+
// 复选框归一化:- [x] → - [ ]
|
|
105
|
+
const normalized = content.replace(/- \[[xX]\]/g, "- [ ]");
|
|
106
|
+
return sha256Text(normalized);
|
|
107
|
+
}
|
|
108
|
+
/** 重建 snapshot(sync 的核心) */
|
|
109
|
+
export function rebuildSnapshot(projectRoot, change, changeRoot, openspecStatusDigest = "sha256:unknown") {
|
|
110
|
+
ensureChangeLayout(projectRoot, change);
|
|
111
|
+
const events = readEvents(projectRoot, change);
|
|
112
|
+
const evDigest = eventsDigest(events);
|
|
113
|
+
const documentDigests = computeDocumentDigests(changeRoot, TRACKED_DOCS);
|
|
114
|
+
const tsDigest = tasksStructureDigest(changeRoot);
|
|
115
|
+
const { state, openJobs, acceptedJobs, activeAttempts, taskStatuses, lastTransition } = replayEvents(events);
|
|
116
|
+
// 粗粒度失效检查(只读,不写事件)
|
|
117
|
+
const staleInfo = checkStaleJobs(acceptedJobs, documentDigests);
|
|
118
|
+
// stale 的 job 从 accepted 移除(sync 只反映当前事实)
|
|
119
|
+
const freshAccepted = acceptedJobs.filter(j => !staleInfo.some(s => s.job_id === j.job_id));
|
|
120
|
+
return {
|
|
121
|
+
change_id: change,
|
|
122
|
+
state,
|
|
123
|
+
openspec_status_digest: openspecStatusDigest,
|
|
124
|
+
events_digest: evDigest,
|
|
125
|
+
document_digests: documentDigests,
|
|
126
|
+
tasks_structure_digest: tsDigest,
|
|
127
|
+
task_statuses: taskStatuses,
|
|
128
|
+
open_jobs: openJobs,
|
|
129
|
+
accepted_jobs: freshAccepted,
|
|
130
|
+
active_task_attempts: activeAttempts,
|
|
131
|
+
pending_user_decisions: [],
|
|
132
|
+
last_transition: lastTransition,
|
|
133
|
+
computed_at: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/task.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
2
|
+
export declare function tasksStructureDigestOf(changeRoot: string): string | null;
|
|
3
|
+
/** record test-run:登记测试运行记录 */
|
|
4
|
+
export declare function recordTestRun(projectRoot: string, change: string, inputFile: string): {
|
|
5
|
+
accepted: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
package/dist/task.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SuperSpec 流程引擎 — task:测试运行记录 + 任务结构指纹工具
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock } from "./store.js";
|
|
5
|
+
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
|
+
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
7
|
+
export function tasksStructureDigestOf(changeRoot) {
|
|
8
|
+
const p = join(changeRoot, "tasks.md");
|
|
9
|
+
if (!existsSync(p))
|
|
10
|
+
return null;
|
|
11
|
+
const content = readFileSync(p, "utf8");
|
|
12
|
+
return formatDigest(content, sha256Text);
|
|
13
|
+
}
|
|
14
|
+
/** record test-run:登记测试运行记录 */
|
|
15
|
+
export function recordTestRun(projectRoot, change, inputFile) {
|
|
16
|
+
return withLock(projectRoot, change, () => {
|
|
17
|
+
ensureChangeLayout(projectRoot, change);
|
|
18
|
+
if (!existsSync(inputFile))
|
|
19
|
+
return { accepted: false, message: `文件不存在:${inputFile}` };
|
|
20
|
+
let tr;
|
|
21
|
+
try {
|
|
22
|
+
tr = JSON.parse(readFileSync(inputFile, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { accepted: false, message: "无效 JSON" };
|
|
26
|
+
}
|
|
27
|
+
if (!tr.test_id || !tr.task_structure_digest) {
|
|
28
|
+
return { accepted: false, message: "缺少 test_id 或 task_structure_digest" };
|
|
29
|
+
}
|
|
30
|
+
const event = makeEvent(change, "test_run_recorded", {
|
|
31
|
+
test_id: tr.test_id,
|
|
32
|
+
task_structure_digest: tr.task_structure_digest,
|
|
33
|
+
attempt_id: tr.attempt_id ?? null,
|
|
34
|
+
command: tr.command ?? "",
|
|
35
|
+
cwd: tr.cwd ?? "",
|
|
36
|
+
exit_code: tr.exit_code ?? -1,
|
|
37
|
+
semantic_status: tr.semantic_status ?? "unknown",
|
|
38
|
+
target_fingerprint: tr.target_fingerprint ?? null,
|
|
39
|
+
raw_log_ref: tr.raw_log_ref ?? null,
|
|
40
|
+
});
|
|
41
|
+
appendEvent(projectRoot, change, event);
|
|
42
|
+
return { accepted: true, message: `测试运行已登记:test_id=${tr.test_id}` };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Snapshot, State, Job, TransitionResult } from "./types.ts";
|
|
2
|
+
interface Decision {
|
|
3
|
+
fromState: State;
|
|
4
|
+
toState: State;
|
|
5
|
+
outcome: "advanced" | "job_created";
|
|
6
|
+
newJobs?: Job[];
|
|
7
|
+
reason: string;
|
|
8
|
+
extraEvents?: {
|
|
9
|
+
type: string;
|
|
10
|
+
payload: Record<string, unknown>;
|
|
11
|
+
}[];
|
|
12
|
+
postCommit?: (projectRoot: string, change: string, changeRoot: string) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 统一 transition 提交协议——所有校验在锁内。
|
|
16
|
+
*/
|
|
17
|
+
export declare function commitTransition(projectRoot: string, change: string, changeRoot: string, opts: {
|
|
18
|
+
name: string;
|
|
19
|
+
decide: (snapshot: Snapshot) => Decision | {
|
|
20
|
+
skip: true;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
idempotencyInputs?: Record<string, unknown>;
|
|
24
|
+
}): TransitionResult;
|
|
25
|
+
export declare function proposeReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
26
|
+
export declare function transitionInit(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
27
|
+
export declare function transitionExplore(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
28
|
+
export declare function startApply(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
29
|
+
export declare function taskStart(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
30
|
+
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
31
|
+
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
32
|
+
export declare function archive(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
33
|
+
export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
34
|
+
export {};
|