@peterxiaoyang/superspec 0.1.37 → 0.1.39
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/dist/cli.js +45 -6
- package/dist/code_review.d.ts +18 -2
- package/dist/code_review.js +473 -81
- package/dist/format.d.ts +46 -4
- package/dist/format.js +311 -26
- package/dist/git_state.d.ts +36 -0
- package/dist/git_state.js +174 -0
- package/dist/job_validity.d.ts +16 -0
- package/dist/job_validity.js +37 -0
- package/dist/next.js +45 -355
- package/dist/phase_plan.d.ts +97 -0
- package/dist/phase_plan.js +582 -0
- package/dist/record.d.ts +2 -2
- package/dist/record.js +67 -31
- package/dist/review.d.ts +3 -2
- package/dist/review.js +66 -33
- package/dist/review_job_gates.d.ts +20 -0
- package/dist/review_job_gates.js +85 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +53 -3
- package/dist/sync.js +7 -9
- package/dist/task.js +87 -9
- package/dist/task_evidence.d.ts +10 -0
- package/dist/task_evidence.js +126 -0
- package/dist/transition.d.ts +1 -1
- package/dist/transition.js +449 -337
- package/dist/types.d.ts +81 -1
- package/dist/workflow_profile.d.ts +11 -0
- package/dist/workflow_profile.js +39 -0
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +17 -27
- package/templates/workflow/prompts/code-reviewer.md +13 -2
- package/templates/workflow/prompts/critic.md +62 -61
- package/templates/workflow/prompts/executor.md +1 -1
- package/templates/workflow/prompts/explore.md +38 -26
- package/templates/workflow/prompts/test-engineer.md +18 -32
- package/templates/workflow/prompts/verifier.md +5 -3
- package/templates/workflow/skills/superspec-apply/SKILL.md +34 -11
- package/templates/workflow/skills/superspec-explore/SKILL.md +65 -64
- package/templates/workflow/skills/superspec-propose/SKILL.md +64 -43
- package/templates/workflow/skills/superspec-review/SKILL.md +1 -1
package/dist/task.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — task:测试运行记录 + 任务结构指纹工具
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord } from "./store.js";
|
|
4
|
+
import { sha256Text, ensureChangeLayout, appendEvent, makeEvent, withLock, appendRawRecord, readEvents } from "./store.js";
|
|
5
5
|
import { tasksStructureDigest as formatDigest } from "./format.js";
|
|
6
6
|
/** tasks.md 结构指纹(委托给 format.ts 统一实现) */
|
|
7
7
|
export function tasksStructureDigestOf(changeRoot) {
|
|
@@ -19,30 +19,44 @@ function recordTestRunLoaded(projectRoot, change, content) {
|
|
|
19
19
|
catch {
|
|
20
20
|
return { accepted: false, message: "无效 JSON" };
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const events = readEvents(projectRoot, change);
|
|
23
|
+
const attemptRecord = typeof tr.attempt_id === "string" ? attemptById(events, tr.attempt_id) : null;
|
|
24
|
+
const activeAttempt = attemptRecord?.state === "active" ? attemptRecord.attempt : null;
|
|
25
|
+
if (attemptRecord?.attempt.contract_mode === true && attemptRecord.state !== "active") {
|
|
26
|
+
return { accepted: false, message: "契约测试证据只能登记到当前活跃任务尝试(attempt_id)" };
|
|
27
|
+
}
|
|
28
|
+
if (!activeAttempt && activeContractAttemptExists(events)) {
|
|
29
|
+
return { accepted: false, message: "契约测试证据必须带当前活跃任务尝试 ID(attempt_id)" };
|
|
30
|
+
}
|
|
31
|
+
if (activeAttempt?.contract_mode === true) {
|
|
32
|
+
const contractCheck = validateContractTestRunInput(tr, activeAttempt);
|
|
33
|
+
if (!contractCheck.ok)
|
|
34
|
+
return { accepted: false, message: contractCheck.message };
|
|
35
|
+
}
|
|
36
|
+
else if (!tr.test_id || !tr.task_structure_digest) {
|
|
37
|
+
return { accepted: false, message: "缺少测试 ID(test_id)或历史任务结构指纹(task_structure_digest)" };
|
|
24
38
|
}
|
|
25
39
|
let coversTaskIds;
|
|
26
40
|
if (tr.covers_task_ids !== undefined) {
|
|
27
41
|
if (!Array.isArray(tr.covers_task_ids)) {
|
|
28
|
-
return { accepted: false, message: "covers_task_ids
|
|
42
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)必须是字符串数组" };
|
|
29
43
|
}
|
|
30
44
|
if (tr.covers_task_ids.length === 0) {
|
|
31
|
-
return { accepted: false, message: "covers_task_ids
|
|
45
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)不能是空数组" };
|
|
32
46
|
}
|
|
33
47
|
for (const raw of tr.covers_task_ids) {
|
|
34
48
|
if (typeof raw !== "string")
|
|
35
|
-
return { accepted: false, message: "covers_task_ids
|
|
49
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)必须是字符串数组" };
|
|
36
50
|
const value = raw.trim();
|
|
37
51
|
if (!value)
|
|
38
|
-
return { accepted: false, message: "covers_task_ids
|
|
52
|
+
return { accepted: false, message: "回归覆盖任务列表(covers_task_ids)不能包含空字符串" };
|
|
39
53
|
(coversTaskIds ??= []).push(value);
|
|
40
54
|
}
|
|
41
55
|
coversTaskIds = [...new Set(coversTaskIds)].sort();
|
|
42
56
|
}
|
|
43
57
|
const normalizedTestRun = {
|
|
44
58
|
test_id: tr.test_id,
|
|
45
|
-
task_structure_digest: tr.task_structure_digest,
|
|
59
|
+
task_structure_digest: tr.task_structure_digest ?? activeAttempt?.task_structure_digest ?? "",
|
|
46
60
|
attempt_id: tr.attempt_id ?? null,
|
|
47
61
|
...(coversTaskIds ? { covers_task_ids: coversTaskIds } : {}),
|
|
48
62
|
command: tr.command ?? "",
|
|
@@ -58,7 +72,71 @@ function recordTestRunLoaded(projectRoot, change, content) {
|
|
|
58
72
|
...rawRef,
|
|
59
73
|
});
|
|
60
74
|
appendEvent(projectRoot, change, event);
|
|
61
|
-
return { accepted: true, message:
|
|
75
|
+
return { accepted: true, message: `测试运行已登记:测试 ID(test_id)=${tr.test_id}` };
|
|
76
|
+
}
|
|
77
|
+
function attemptById(events, attemptId) {
|
|
78
|
+
const attempts = new Map();
|
|
79
|
+
for (const ev of events) {
|
|
80
|
+
if (ev.event_type === "task_started") {
|
|
81
|
+
const attempt = ev.payload;
|
|
82
|
+
if (typeof attempt.attempt_id === "string")
|
|
83
|
+
attempts.set(attempt.attempt_id, { attempt, state: "active" });
|
|
84
|
+
}
|
|
85
|
+
else if (ev.event_type === "task_completed") {
|
|
86
|
+
const payload = ev.payload;
|
|
87
|
+
const existing = typeof payload.attempt_id === "string" ? attempts.get(payload.attempt_id) : null;
|
|
88
|
+
if (existing)
|
|
89
|
+
existing.state = "completed";
|
|
90
|
+
}
|
|
91
|
+
else if (ev.event_type === "task_abandoned") {
|
|
92
|
+
const payload = ev.payload;
|
|
93
|
+
const existing = typeof payload.attempt_id === "string" ? attempts.get(payload.attempt_id) : null;
|
|
94
|
+
if (existing)
|
|
95
|
+
existing.state = "abandoned";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return attempts.get(attemptId) ?? null;
|
|
99
|
+
}
|
|
100
|
+
function activeContractAttemptExists(events) {
|
|
101
|
+
const attempts = new Map();
|
|
102
|
+
for (const ev of events) {
|
|
103
|
+
if (ev.event_type === "task_started") {
|
|
104
|
+
const attempt = ev.payload;
|
|
105
|
+
if (typeof attempt.attempt_id === "string")
|
|
106
|
+
attempts.set(attempt.attempt_id, attempt);
|
|
107
|
+
}
|
|
108
|
+
else if (ev.event_type === "task_completed" || ev.event_type === "task_abandoned") {
|
|
109
|
+
const payload = ev.payload;
|
|
110
|
+
if (typeof payload.attempt_id === "string")
|
|
111
|
+
attempts.delete(payload.attempt_id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...attempts.values()].some(attempt => attempt.contract_mode === true);
|
|
115
|
+
}
|
|
116
|
+
function validateContractTestRunInput(tr, attempt) {
|
|
117
|
+
if (!tr.test_id || !tr.command || !tr.cwd || typeof tr.exit_code !== "number" || !tr.semantic_status) {
|
|
118
|
+
return { ok: false, message: "契约测试证据缺少测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)或语义状态(semantic_status)" };
|
|
119
|
+
}
|
|
120
|
+
if (typeof tr.attempt_id !== "string" || tr.attempt_id !== attempt.attempt_id) {
|
|
121
|
+
return { ok: false, message: "契约测试证据必须带当前活跃任务尝试 ID(attempt_id)" };
|
|
122
|
+
}
|
|
123
|
+
if (!["expected_failure", "expected_success", "characterization_pass"].includes(tr.semantic_status)) {
|
|
124
|
+
return { ok: false, message: "语义状态(semantic_status)必须是 expected_failure、expected_success 或 characterization_pass" };
|
|
125
|
+
}
|
|
126
|
+
if (tr.semantic_status === "expected_failure" && tr.exit_code === 0) {
|
|
127
|
+
return { ok: false, message: "RED 预期失败(expected_failure)要求退出码(exit_code)非 0" };
|
|
128
|
+
}
|
|
129
|
+
if ((tr.semantic_status === "expected_success" || tr.semantic_status === "characterization_pass") && tr.exit_code !== 0) {
|
|
130
|
+
return { ok: false, message: `语义状态(semantic_status=${tr.semantic_status})要求退出码(exit_code)为 0` };
|
|
131
|
+
}
|
|
132
|
+
if (tr.semantic_status === "characterization_pass" && !(attempt.tdd_required === false && attempt.no_tdd_reason === "characterization")) {
|
|
133
|
+
return { ok: false, message: "特征化通过(characterization_pass)只适用于无需 TDD 的特征化任务(tdd_required:false,no_tdd_reason:characterization)" };
|
|
134
|
+
}
|
|
135
|
+
const declaredTests = attempt.contract?.tests ?? [];
|
|
136
|
+
if (declaredTests.length > 0 && !declaredTests.includes(tr.test_id)) {
|
|
137
|
+
return { ok: false, message: `测试 ID(test_id=${tr.test_id})不属于当前任务契约声明的测试列表` };
|
|
138
|
+
}
|
|
139
|
+
return { ok: true };
|
|
62
140
|
}
|
|
63
141
|
/** record test-run:登记测试运行记录 */
|
|
64
142
|
export function recordTestRun(projectRoot, change, inputFile) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TaskAttempt } from "./types.ts";
|
|
2
|
+
export type TaskEvidenceReadiness = {
|
|
3
|
+
ready: true;
|
|
4
|
+
missing: [];
|
|
5
|
+
} | {
|
|
6
|
+
ready: false;
|
|
7
|
+
missing: string[];
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function taskEvidenceReadiness(projectRoot: string, change: string, changeRoot: string, attempt: TaskAttempt): TaskEvidenceReadiness;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseTasksMd } from "./format.js";
|
|
4
|
+
import { readEvents, sha256Text } from "./store.js";
|
|
5
|
+
export function taskEvidenceReadiness(projectRoot, change, changeRoot, attempt) {
|
|
6
|
+
if (attempt.contract_mode === true) {
|
|
7
|
+
return contractTaskEvidenceReadiness(projectRoot, change, attempt);
|
|
8
|
+
}
|
|
9
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
10
|
+
const tasks = parseTasksMd(tasksContent);
|
|
11
|
+
const taskInfo = tasks.find(task => task.taskId === attempt.task_id);
|
|
12
|
+
const missing = [];
|
|
13
|
+
if (!taskInfo) {
|
|
14
|
+
missing.push(`任务 ${attempt.task_id} 不存在`);
|
|
15
|
+
return { ready: false, missing, reason: missing.join("、") };
|
|
16
|
+
}
|
|
17
|
+
const currentDigest = sha256Text(tasksContent.replace(/- \[[xX]\]/g, "- [ ]"));
|
|
18
|
+
if (currentDigest !== attempt.task_structure_digest)
|
|
19
|
+
missing.push("任务结构指纹");
|
|
20
|
+
if (!taskInfo.tddRequired) {
|
|
21
|
+
if (!taskInfo.noTddReason)
|
|
22
|
+
missing.push("no_tdd_reason");
|
|
23
|
+
return missing.length === 0
|
|
24
|
+
? { ready: true, missing: [] }
|
|
25
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
26
|
+
}
|
|
27
|
+
let hasRed = false;
|
|
28
|
+
let hasGreen = false;
|
|
29
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
30
|
+
if (ev.event_type !== "test_run_recorded")
|
|
31
|
+
continue;
|
|
32
|
+
const tr = ev.payload;
|
|
33
|
+
const matches = tr.attempt_id === attempt.attempt_id ||
|
|
34
|
+
(!tr.attempt_id && tr.task_structure_digest === attempt.task_structure_digest);
|
|
35
|
+
if (!matches)
|
|
36
|
+
continue;
|
|
37
|
+
if (tr.semantic_status === "expected_failure" || tr.semantic_status === "characterization_pass")
|
|
38
|
+
hasRed = true;
|
|
39
|
+
if (tr.semantic_status === "expected_success")
|
|
40
|
+
hasGreen = true;
|
|
41
|
+
}
|
|
42
|
+
if (!hasRed)
|
|
43
|
+
missing.push("RED 证据");
|
|
44
|
+
if (!hasGreen)
|
|
45
|
+
missing.push("GREEN 证据");
|
|
46
|
+
return missing.length === 0
|
|
47
|
+
? { ready: true, missing: [] }
|
|
48
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
49
|
+
}
|
|
50
|
+
function contractTaskEvidenceReadiness(projectRoot, change, attempt) {
|
|
51
|
+
const missing = [];
|
|
52
|
+
const tddRequired = attempt.tdd_required !== false;
|
|
53
|
+
const characterization = attempt.tdd_required === false && attempt.no_tdd_reason === "characterization";
|
|
54
|
+
const declaredTests = attempt.contract?.tests ?? [];
|
|
55
|
+
if (!tddRequired && !attempt.no_tdd_reason) {
|
|
56
|
+
missing.push("no_tdd_reason");
|
|
57
|
+
}
|
|
58
|
+
if (declaredTests.length === 0) {
|
|
59
|
+
if (tddRequired) {
|
|
60
|
+
const hasAttemptEvidence = attemptLevelRedGreenEvidence(projectRoot, change, attempt.attempt_id);
|
|
61
|
+
if (!hasAttemptEvidence.hasRed)
|
|
62
|
+
missing.push("RED 证据");
|
|
63
|
+
if (!hasAttemptEvidence.hasGreen)
|
|
64
|
+
missing.push("GREEN 证据");
|
|
65
|
+
}
|
|
66
|
+
return missing.length === 0
|
|
67
|
+
? { ready: true, missing: [] }
|
|
68
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
69
|
+
}
|
|
70
|
+
const perTest = new Map();
|
|
71
|
+
for (const testId of declaredTests) {
|
|
72
|
+
perTest.set(testId, { sawRed: false, sawGreen: false, paired: false });
|
|
73
|
+
}
|
|
74
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
75
|
+
if (ev.event_type !== "test_run_recorded")
|
|
76
|
+
continue;
|
|
77
|
+
const tr = ev.payload;
|
|
78
|
+
if (tr.attempt_id !== attempt.attempt_id)
|
|
79
|
+
continue;
|
|
80
|
+
if (typeof tr.test_id !== "string")
|
|
81
|
+
continue;
|
|
82
|
+
const state = perTest.get(tr.test_id);
|
|
83
|
+
if (!state)
|
|
84
|
+
continue;
|
|
85
|
+
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
|
|
86
|
+
state.sawRed = true;
|
|
87
|
+
}
|
|
88
|
+
const isExpectedSuccess = tr.semantic_status === "expected_success" && tr.exit_code === 0;
|
|
89
|
+
const isCharacterizationPass = characterization && tr.semantic_status === "characterization_pass" && tr.exit_code === 0;
|
|
90
|
+
if (isExpectedSuccess || isCharacterizationPass) {
|
|
91
|
+
if (state.sawRed)
|
|
92
|
+
state.paired = true;
|
|
93
|
+
state.sawGreen = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [testId, state] of perTest) {
|
|
97
|
+
if (!state.sawGreen)
|
|
98
|
+
missing.push(`${testId} GREEN 证据`);
|
|
99
|
+
}
|
|
100
|
+
if (tddRequired) {
|
|
101
|
+
const hasPairedTest = [...perTest.values()].some(state => state.paired);
|
|
102
|
+
if (!hasPairedTest)
|
|
103
|
+
missing.push("同一 TEST 的 RED/GREEN 配对");
|
|
104
|
+
}
|
|
105
|
+
return missing.length === 0
|
|
106
|
+
? { ready: true, missing: [] }
|
|
107
|
+
: { ready: false, missing, reason: missing.join("、") };
|
|
108
|
+
}
|
|
109
|
+
function attemptLevelRedGreenEvidence(projectRoot, change, attemptId) {
|
|
110
|
+
let hasRed = false;
|
|
111
|
+
let hasGreen = false;
|
|
112
|
+
for (const ev of readEvents(projectRoot, change)) {
|
|
113
|
+
if (ev.event_type !== "test_run_recorded")
|
|
114
|
+
continue;
|
|
115
|
+
const tr = ev.payload;
|
|
116
|
+
if (tr.attempt_id !== attemptId)
|
|
117
|
+
continue;
|
|
118
|
+
if (tr.semantic_status === "expected_failure" && typeof tr.exit_code === "number" && tr.exit_code !== 0) {
|
|
119
|
+
hasRed = true;
|
|
120
|
+
}
|
|
121
|
+
if (tr.semantic_status === "expected_success" && tr.exit_code === 0) {
|
|
122
|
+
hasGreen = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { hasRed, hasGreen };
|
|
126
|
+
}
|
package/dist/transition.d.ts
CHANGED
|
@@ -43,5 +43,5 @@ export declare function reopen(projectRoot: string, change: string, changeRoot:
|
|
|
43
43
|
export declare function reviewReady(projectRoot: string, change: string, changeRoot: string, risk?: "minimal" | "normal" | "strict"): TransitionResult;
|
|
44
44
|
export declare function accept(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
45
45
|
export declare function archive(projectRoot: string, change: string, changeRoot: string): TransitionResult;
|
|
46
|
-
export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string): TransitionResult;
|
|
46
|
+
export declare function taskComplete(projectRoot: string, change: string, changeRoot: string, taskId: string, inputContent?: string | null): TransitionResult;
|
|
47
47
|
export {};
|