@peterxiaoyang/superspec 0.1.47 → 0.1.48
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 +52 -8
- package/dist/explore_round.d.ts +3 -1
- package/dist/explore_round.js +48 -3
- package/dist/format.d.ts +28 -1
- package/dist/format.js +121 -4
- package/dist/next.d.ts +1 -1
- package/dist/next.js +97 -12
- package/dist/phase_confirmation.js +3 -1
- package/dist/phase_plan.d.ts +21 -1
- package/dist/phase_plan.js +151 -35
- package/dist/propose_round.d.ts +15 -0
- package/dist/propose_round.js +137 -0
- package/dist/record.js +133 -11
- package/dist/review_job_gates.d.ts +1 -1
- package/dist/review_job_gates.js +12 -4
- package/dist/skill_loop.js +20 -0
- package/dist/transition.js +31 -4
- package/dist/types.d.ts +49 -1
- package/dist/workflow_profile.js +1 -1
- package/package.json +7 -1
- package/templates/workflow/AGENTS.md +2 -0
- package/templates/workflow/prompts/architect.md +2 -0
- package/templates/workflow/prompts/critic.md +11 -2
- package/templates/workflow/prompts/test-engineer.md +1 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +7 -3
- package/templates/workflow/skills/superspec-explore/SKILL.md +7 -3
- package/templates/workflow/skills/superspec-propose/SKILL.md +16 -4
package/dist/cli.js
CHANGED
|
@@ -66,7 +66,7 @@ function transitionExitCode(result) {
|
|
|
66
66
|
function proposeReadyExitCode(result) {
|
|
67
67
|
if (result.outcome === "blocked")
|
|
68
68
|
return 0;
|
|
69
|
-
return result.events_written === 0 && result.
|
|
69
|
+
return result.events_written === 0 && result.outcome === "advanced" && result.from_state === result.to_state ? 1 : 0;
|
|
70
70
|
}
|
|
71
71
|
function ensureWorkflowMode(projectRoot, _opts) {
|
|
72
72
|
try {
|
|
@@ -493,10 +493,8 @@ async function askToUpdateSelfIfNeeded(projectRoot, rerunArgs) {
|
|
|
493
493
|
// ===== 初始化 transition init =====
|
|
494
494
|
// ===== init/explore 现在在 transition.ts 中(走统一锁内路径)=====
|
|
495
495
|
// ===== 主分发 =====
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
499
|
-
console.log(`SuperSpec 流程引擎 ${SUPERSPEC_VERSION}
|
|
496
|
+
function topLevelHelp() {
|
|
497
|
+
return `SuperSpec 流程引擎 ${SUPERSPEC_VERSION}
|
|
500
498
|
|
|
501
499
|
用法:superspec <命令> [选项]
|
|
502
500
|
|
|
@@ -507,8 +505,8 @@ async function main(argv) {
|
|
|
507
505
|
jobs <子命令> --change <C> 工作项管理(见下)
|
|
508
506
|
install 安装项目工作流入口
|
|
509
507
|
init --scope project install 的兼容别名
|
|
510
|
-
update
|
|
511
|
-
version
|
|
508
|
+
update 升级 CLI 到 npm latest 并同步项目工作流模板
|
|
509
|
+
version 版本号
|
|
512
510
|
|
|
513
511
|
transition 子命令:
|
|
514
512
|
init / explore / sync / next / propose-ready / start-apply
|
|
@@ -525,7 +523,49 @@ record 子命令:
|
|
|
525
523
|
|
|
526
524
|
jobs 子命令:
|
|
527
525
|
list / packet --job <J>
|
|
528
|
-
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
function commandHelp(command, subcommand) {
|
|
529
|
+
if (command === "record" && subcommand === "user-decision") {
|
|
530
|
+
return `用法:superspec record user-decision --change <C> --input <F|->
|
|
531
|
+
|
|
532
|
+
从 JSON 文件读取用户决定;--input - 表示从 stdin 读取。
|
|
533
|
+
输入至少包含 scope 和 answer,工作流给出的 question 可一并保留。
|
|
534
|
+
|
|
535
|
+
示例:
|
|
536
|
+
printf '%s' '{"scope":"<next 返回的 scope>","question":"<原问题>","answer":"<用户答复>"}' | superspec record user-decision --change <C> --input -
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
if (command === "record" && subcommand === "job-submit") {
|
|
540
|
+
return `用法:superspec record job-submit --change <C> --job <J> --report <F|->
|
|
541
|
+
|
|
542
|
+
提交 reviewer JSON 报告;--report - 表示从 stdin 读取。报告契约以 jobs packet 返回的 report_schema 为准。
|
|
543
|
+
|
|
544
|
+
示例:
|
|
545
|
+
superspec jobs packet --change <C> --job <J>
|
|
546
|
+
superspec record job-submit --change <C> --job <J> --report report.json
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
if (command === "record" && subcommand === "test-run") {
|
|
550
|
+
return `用法:superspec record test-run --change <C> --input <F|->
|
|
551
|
+
|
|
552
|
+
登记测试证据;--input - 表示从 stdin 读取。输入需包含 test_id、task_structure_digest、command、cwd、exit_code 和 semantic_status。
|
|
553
|
+
|
|
554
|
+
示例:
|
|
555
|
+
printf '%s' '{"test_id":"TEST-001","task_structure_digest":"sha256:<digest>","command":"npm test","cwd":"<project>","exit_code":0,"semantic_status":"expected_success"}' | superspec record test-run --change <C> --input -
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
if (command === "transition" && subcommand === "propose-ready") {
|
|
559
|
+
return `用法:superspec transition propose-ready --change <C>
|
|
560
|
+
|
|
561
|
+
校验当前计划材料和审查门禁;满足条件时推进到 propose_ready。工作流模式来自项目配置,不接受 --risk。
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
return topLevelHelp();
|
|
565
|
+
}
|
|
566
|
+
async function main(argv) {
|
|
567
|
+
if (argv.length === 0) {
|
|
568
|
+
console.log(topLevelHelp());
|
|
529
569
|
return 0;
|
|
530
570
|
}
|
|
531
571
|
// version
|
|
@@ -534,6 +574,10 @@ jobs 子命令:
|
|
|
534
574
|
return 0;
|
|
535
575
|
}
|
|
536
576
|
const { command, subcommand, opts } = parseArgs(argv);
|
|
577
|
+
if (argv.includes("--help") || argv.includes("-h") || command === "--help" || command === "-h") {
|
|
578
|
+
console.log(commandHelp(command, subcommand));
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
537
581
|
const projectRoot = process.cwd();
|
|
538
582
|
// install / init / update 不需要 --change
|
|
539
583
|
if (command === "install" || command === "init") {
|
package/dist/explore_round.d.ts
CHANGED
|
@@ -15,9 +15,11 @@ export declare function exploreAnswerRegistrationPayload(discoveryContent: strin
|
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
17
|
/** 当前确认事项已通过本轮主流程登记过答复。 */
|
|
18
|
-
export declare function exploreAnswerWasRecorded(events: readonly Event[], roundId: string, question: Pick<DiscoveryQuestion, "id" | "ordinal">, discoveryContent: string): boolean;
|
|
18
|
+
export declare function exploreAnswerWasRecorded(events: readonly Event[], roundId: string, question: Pick<DiscoveryQuestion, "id" | "ordinal" | "text">, discoveryContent: string): boolean;
|
|
19
19
|
/**
|
|
20
20
|
* 新协议轮次中,被回写为已确认的事项必须有对应答复记录。
|
|
21
21
|
* 进入本轮前已经关闭的事项作为基线保留,历史 change 因缺少协议标记整体兼容。
|
|
22
22
|
*/
|
|
23
23
|
export declare function unregisteredClosedExploreQuestions(events: readonly Event[], discoveryContent: string): DiscoveryQuestion[];
|
|
24
|
+
/** 已正式展示但尚未登记答复的 Explore 问题不能通过删除问题行绕过。 */
|
|
25
|
+
export declare function unresolvedPresentedExploreQuestionScopes(events: readonly Event[]): string[];
|
package/dist/explore_round.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { discoveryQuestionContextFingerprint, discoveryQuestionKey, parseDiscoveryQuestions, } from "./format.js";
|
|
1
|
+
import { discoveryQuestionContextFingerprint, discoveryQuestionDecisionBasisDigest, discoveryQuestionKey, parseDiscoveryQuestions, } from "./format.js";
|
|
2
2
|
const EXPLORE_ANSWER_REGISTRATION_VERSION = 1;
|
|
3
3
|
function isExploreRoundEntry(event) {
|
|
4
4
|
if (event.event_type !== "transition_commit")
|
|
@@ -68,16 +68,20 @@ export function exploreAnswerWasRecorded(events, roundId, question, discoveryCon
|
|
|
68
68
|
const currentContextFingerprint = discoveryQuestionContextFingerprint(discoveryContent, question);
|
|
69
69
|
if (!currentContextFingerprint)
|
|
70
70
|
return false;
|
|
71
|
+
const currentBasisDigest = discoveryQuestionDecisionBasisDigest(question);
|
|
71
72
|
return events.some(event => {
|
|
72
73
|
if (event.event_type !== "user_decision_recorded")
|
|
73
74
|
return false;
|
|
74
75
|
const payload = event.payload;
|
|
75
76
|
const recorded = payload.explore_open_question;
|
|
77
|
+
const revisionMatches = typeof recorded?.decision_basis_digest === "string"
|
|
78
|
+
? recorded.decision_basis_digest === currentBasisDigest
|
|
79
|
+
: recorded?.context_fingerprint === currentContextFingerprint;
|
|
76
80
|
return payload.accepted === true &&
|
|
77
81
|
recorded?.round_id === roundId &&
|
|
78
82
|
recorded.question_id === question.id &&
|
|
79
|
-
recorded.question_ordinal === question.ordinal &&
|
|
80
|
-
|
|
83
|
+
(!question.id.startsWith("item-") || recorded.question_ordinal === question.ordinal) &&
|
|
84
|
+
revisionMatches;
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
@@ -92,3 +96,44 @@ export function unregisteredClosedExploreQuestions(events, discoveryContent) {
|
|
|
92
96
|
!registration.baselineClosedQuestionKeys.has(discoveryQuestionKey(question)) &&
|
|
93
97
|
!exploreAnswerWasRecorded(events, registration.roundId, question, discoveryContent));
|
|
94
98
|
}
|
|
99
|
+
/** 已正式展示但尚未登记答复的 Explore 问题不能通过删除问题行绕过。 */
|
|
100
|
+
export function unresolvedPresentedExploreQuestionScopes(events) {
|
|
101
|
+
const roundId = currentExploreRoundId(events);
|
|
102
|
+
const acceptedDecisions = events.flatMap(event => {
|
|
103
|
+
if (event.event_type !== "user_decision_recorded")
|
|
104
|
+
return [];
|
|
105
|
+
const payload = event.payload;
|
|
106
|
+
return payload.accepted === true ? [payload] : [];
|
|
107
|
+
});
|
|
108
|
+
const latestByQuestion = new Map();
|
|
109
|
+
for (const event of events) {
|
|
110
|
+
if (event.event_type !== "user_question_presented")
|
|
111
|
+
continue;
|
|
112
|
+
const payload = event.payload;
|
|
113
|
+
if (payload.phase !== "explore" || payload.round_id !== roundId || typeof payload.scope !== "string" || typeof payload.question_id !== "string")
|
|
114
|
+
continue;
|
|
115
|
+
const identity = payload.question_id.startsWith("item-")
|
|
116
|
+
? `${payload.question_id}:${String(payload.question_ordinal)}`
|
|
117
|
+
: payload.question_id;
|
|
118
|
+
latestByQuestion.set(identity, {
|
|
119
|
+
scope: payload.scope,
|
|
120
|
+
legacyScope: typeof payload.legacy_scope === "string" ? payload.legacy_scope : null,
|
|
121
|
+
questionId: payload.question_id,
|
|
122
|
+
questionOrdinal: payload.question_ordinal,
|
|
123
|
+
revisionDigest: payload.decision_basis_digest,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return [...latestByQuestion.values()].flatMap(presented => {
|
|
127
|
+
const answered = acceptedDecisions.some(decision => {
|
|
128
|
+
if (decision.scope === presented.scope || decision.scope === presented.legacyScope)
|
|
129
|
+
return true;
|
|
130
|
+
const recorded = decision.explore_open_question;
|
|
131
|
+
return recorded?.round_id === roundId &&
|
|
132
|
+
recorded.question_id === presented.questionId &&
|
|
133
|
+
(!presented.questionId.startsWith("item-") || recorded.question_ordinal === presented.questionOrdinal) &&
|
|
134
|
+
typeof presented.revisionDigest === "string" &&
|
|
135
|
+
recorded.decision_basis_digest === presented.revisionDigest;
|
|
136
|
+
});
|
|
137
|
+
return answered ? [] : [presented.scope];
|
|
138
|
+
});
|
|
139
|
+
}
|
package/dist/format.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export interface DiscoveryQuestion extends DiscoveryOpenQuestion {
|
|
|
26
26
|
* 已经确认的旧事项误认为新答复。
|
|
27
27
|
*/
|
|
28
28
|
export declare function discoveryQuestionKey(question: Pick<DiscoveryQuestion, "id" | "ordinal" | "text">): string;
|
|
29
|
+
/**
|
|
30
|
+
* 当前决定所依据的规范化问题内容。会改变选择、推荐或影响的事实必须写入该问题项;
|
|
31
|
+
* 状态机只绑定这份明确依据,不猜测文档其它段落与决定是否相关。
|
|
32
|
+
*/
|
|
33
|
+
export declare function discoveryQuestionDecisionBasisDigest(question: Pick<DiscoveryQuestion, "id" | "ordinal" | "text">): string;
|
|
29
34
|
/**
|
|
30
35
|
* 计算某一确认事项之外的 Discovery 决策上下文。回写时该事项本身会从问题改为
|
|
31
36
|
* 结论,因此只归一化这一行;其余事实、证据和其它待确认项的改动都会使指纹失效。
|
|
@@ -35,7 +40,9 @@ export declare function discoveryQuestionContextFingerprint(content: string, que
|
|
|
35
40
|
export declare function parseDiscoveryQuestions(content: string): DiscoveryQuestion[];
|
|
36
41
|
/** 按文档顺序提取 discovery.md 中尚未确认的问题。 */
|
|
37
42
|
export declare function parseDiscoveryOpenQuestions(content: string): DiscoveryOpenQuestion[];
|
|
38
|
-
export declare function discoveryOpenQuestionScope(question: Pick<DiscoveryOpenQuestion, "id" | "
|
|
43
|
+
export declare function discoveryOpenQuestionScope(question: Pick<DiscoveryOpenQuestion, "id" | "ordinal" | "text">, exploreRoundId: string): string;
|
|
44
|
+
/** 兼容升级前已经展示给用户、但尚未登记的 scope。 */
|
|
45
|
+
export declare function legacyDiscoveryOpenQuestionScope(question: Pick<DiscoveryOpenQuestion, "id" | "documentFingerprint">, exploreRoundId: string): string;
|
|
39
46
|
/** 面向用户展示时隐藏 Q-xxx 这一内部编号;历史无编号问题保持原文。 */
|
|
40
47
|
export declare function discoveryOpenQuestionDisplayText(question: Pick<DiscoveryOpenQuestion, "id" | "text">): string;
|
|
41
48
|
/** 从 discovery.md 提取“待确认问题”段内的未确认项数量。 */
|
|
@@ -61,6 +68,26 @@ export interface ProposeOpenQuestionFile {
|
|
|
61
68
|
path: string;
|
|
62
69
|
openCount: number;
|
|
63
70
|
}
|
|
71
|
+
export declare const PROPOSE_OPEN_QUESTION_SCOPE_PREFIX = "propose_open_question:";
|
|
72
|
+
export interface ProposeQuestion {
|
|
73
|
+
path: string;
|
|
74
|
+
id: string;
|
|
75
|
+
ordinal: number;
|
|
76
|
+
text: string;
|
|
77
|
+
raw: string;
|
|
78
|
+
documentFingerprint: string;
|
|
79
|
+
status: "open" | "closed";
|
|
80
|
+
}
|
|
81
|
+
export declare function parseProposeQuestions(content: string, path: string): ProposeQuestion[];
|
|
82
|
+
export declare function proposeQuestionKey(question: Pick<ProposeQuestion, "path" | "id" | "ordinal" | "text">): string;
|
|
83
|
+
/** Propose 决定的明确依据;其它计划材料变化不会自动使已展示决定失效。 */
|
|
84
|
+
export declare function proposeQuestionDecisionBasisDigest(question: Pick<ProposeQuestion, "path" | "id" | "ordinal" | "text">): string;
|
|
85
|
+
export declare function proposeQuestionContextFingerprint(content: string, question: Pick<ProposeQuestion, "id" | "ordinal">): string | null;
|
|
86
|
+
export declare function proposeOpenQuestionScope(question: ProposeQuestion, proposeRoundId: string): string;
|
|
87
|
+
/** 兼容升级前已经展示给用户、但尚未登记的 scope。 */
|
|
88
|
+
export declare function legacyProposeOpenQuestionScope(question: ProposeQuestion, proposeRoundId: string): string;
|
|
89
|
+
export declare function proposeOpenQuestionDisplayText(question: Pick<ProposeQuestion, "id" | "text">): string;
|
|
90
|
+
export declare function collectProposeQuestions(changeRoot: string): ProposeQuestion[];
|
|
64
91
|
export declare function countProposeOpenQuestionsInContent(content: string): number;
|
|
65
92
|
export declare function collectProposeOpenQuestions(changeRoot: string): {
|
|
66
93
|
openCount: number;
|
package/dist/format.js
CHANGED
|
@@ -52,6 +52,17 @@ export const EXPLORE_OPEN_QUESTION_SCOPE_PREFIX = "explore_open_question:";
|
|
|
52
52
|
export function discoveryQuestionKey(question) {
|
|
53
53
|
return sha256Text(`${question.id}\n${question.ordinal}\n${question.text}`);
|
|
54
54
|
}
|
|
55
|
+
function normalizedDecisionQuestionText(text) {
|
|
56
|
+
return text.replace(/\s+/g, " ").trim();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 当前决定所依据的规范化问题内容。会改变选择、推荐或影响的事实必须写入该问题项;
|
|
60
|
+
* 状态机只绑定这份明确依据,不猜测文档其它段落与决定是否相关。
|
|
61
|
+
*/
|
|
62
|
+
export function discoveryQuestionDecisionBasisDigest(question) {
|
|
63
|
+
const legacyOrdinal = question.id.startsWith("item-") ? String(question.ordinal) : "explicit-id";
|
|
64
|
+
return sha256Text(`decision-basis:v1\nexplore\n${question.id}\n${legacyOrdinal}\n${normalizedDecisionQuestionText(question.text)}`);
|
|
65
|
+
}
|
|
55
66
|
/**
|
|
56
67
|
* 计算某一确认事项之外的 Discovery 决策上下文。回写时该事项本身会从问题改为
|
|
57
68
|
* 结论,因此只归一化这一行;其余事实、证据和其它待确认项的改动都会使指纹失效。
|
|
@@ -105,6 +116,11 @@ export function parseDiscoveryOpenQuestions(content) {
|
|
|
105
116
|
.map(({ status: _status, ...question }) => question);
|
|
106
117
|
}
|
|
107
118
|
export function discoveryOpenQuestionScope(question, exploreRoundId) {
|
|
119
|
+
const fingerprint = sha256Text(`${exploreRoundId}\n${discoveryQuestionDecisionBasisDigest(question)}`);
|
|
120
|
+
return `${EXPLORE_OPEN_QUESTION_SCOPE_PREFIX}${fingerprint}:${question.id}`;
|
|
121
|
+
}
|
|
122
|
+
/** 兼容升级前已经展示给用户、但尚未登记的 scope。 */
|
|
123
|
+
export function legacyDiscoveryOpenQuestionScope(question, exploreRoundId) {
|
|
108
124
|
const fingerprint = sha256Text(`${exploreRoundId}\n${question.documentFingerprint}`);
|
|
109
125
|
return `${EXPLORE_OPEN_QUESTION_SCOPE_PREFIX}${fingerprint}:${question.id}`;
|
|
110
126
|
}
|
|
@@ -168,21 +184,24 @@ export function validateDiscoveryChainCoverage(content) {
|
|
|
168
184
|
}
|
|
169
185
|
const statusIdx = header.indexOf("状态");
|
|
170
186
|
const requiredColumnIndexes = DISCOVERY_CHAIN_REQUIRED_COLUMNS.map(col => ({ col, idx: header.indexOf(col) }));
|
|
187
|
+
const errors = [];
|
|
171
188
|
for (const [idx, cells] of rows.entries()) {
|
|
172
189
|
const rowNum = idx + 1;
|
|
173
190
|
for (const { col, idx: colIdx } of requiredColumnIndexes) {
|
|
174
191
|
if (!(cells[colIdx]?.trim())) {
|
|
175
|
-
|
|
192
|
+
errors.push(`链路五要素第 ${rowNum} 行缺少${col}`);
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
const status = cells[statusIdx]?.trim() ?? "";
|
|
179
196
|
if (!DISCOVERY_CHAIN_STATUSES.has(status)) {
|
|
180
|
-
|
|
197
|
+
errors.push(`链路五要素第 ${rowNum} 行状态必须是 已确认、未知阻塞 或 未知非阻塞`);
|
|
181
198
|
}
|
|
182
199
|
if (status.includes("未知阻塞") && countDiscoveryOpenQuestions(content) === 0) {
|
|
183
|
-
|
|
200
|
+
errors.push("链路五要素存在未知阻塞,但待确认问题中没有未解决项");
|
|
184
201
|
}
|
|
185
202
|
}
|
|
203
|
+
if (errors.length > 0)
|
|
204
|
+
return { ok: false, message: [...new Set(errors)].join(";"), present: true };
|
|
186
205
|
return { ok: true, message: "链路五要素就绪", present: true };
|
|
187
206
|
}
|
|
188
207
|
/**
|
|
@@ -206,12 +225,84 @@ export function validateDiscovery(changeRoot) {
|
|
|
206
225
|
openCount,
|
|
207
226
|
};
|
|
208
227
|
}
|
|
228
|
+
export const PROPOSE_OPEN_QUESTION_SCOPE_PREFIX = "propose_open_question:";
|
|
209
229
|
const PROPOSE_CONFIRMATION_DOCS = [
|
|
210
230
|
"proposal.md",
|
|
211
231
|
"design.md",
|
|
212
232
|
".superspec/artifacts/test-contract.md",
|
|
213
233
|
];
|
|
214
234
|
const PROPOSE_CONFIRMATION_HEADINGS = ["待用户确认", "待确认问题", "Open Questions", "Pending Questions"];
|
|
235
|
+
export function parseProposeQuestions(content, path) {
|
|
236
|
+
const sectionBody = sectionBodyByHeadings(content, PROPOSE_CONFIRMATION_HEADINGS);
|
|
237
|
+
if (sectionBody == null)
|
|
238
|
+
return [];
|
|
239
|
+
const documentFingerprint = sha256Text(content);
|
|
240
|
+
const questions = [];
|
|
241
|
+
const checklist = /^\s*-\s+\[([ xX])\]\s+(.*?)\s*$/gm;
|
|
242
|
+
let ordinal = 0;
|
|
243
|
+
for (const match of sectionBody.matchAll(checklist)) {
|
|
244
|
+
ordinal += 1;
|
|
245
|
+
const text = match[2];
|
|
246
|
+
const idMatch = /^\s*(DEC-[A-Za-z0-9][A-Za-z0-9_-]*)\b/.exec(text);
|
|
247
|
+
questions.push({
|
|
248
|
+
path,
|
|
249
|
+
id: idMatch?.[1] ?? `item-${ordinal}`,
|
|
250
|
+
ordinal,
|
|
251
|
+
text,
|
|
252
|
+
raw: match[0],
|
|
253
|
+
documentFingerprint,
|
|
254
|
+
status: match[1] === " " ? "open" : "closed",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return questions;
|
|
258
|
+
}
|
|
259
|
+
export function proposeQuestionKey(question) {
|
|
260
|
+
return sha256Text(`${question.path}\n${question.id}\n${question.ordinal}\n${question.text}`);
|
|
261
|
+
}
|
|
262
|
+
/** Propose 决定的明确依据;其它计划材料变化不会自动使已展示决定失效。 */
|
|
263
|
+
export function proposeQuestionDecisionBasisDigest(question) {
|
|
264
|
+
const legacyOrdinal = question.id.startsWith("item-") ? String(question.ordinal) : "explicit-id";
|
|
265
|
+
return sha256Text(`decision-basis:v1\npropose\n${question.path}\n${question.id}\n${legacyOrdinal}\n${normalizedDecisionQuestionText(question.text)}`);
|
|
266
|
+
}
|
|
267
|
+
export function proposeQuestionContextFingerprint(content, question) {
|
|
268
|
+
const range = sectionRangeByHeadings(content, PROPOSE_CONFIRMATION_HEADINGS);
|
|
269
|
+
if (!range)
|
|
270
|
+
return null;
|
|
271
|
+
const sectionBody = content.slice(range.start, range.end);
|
|
272
|
+
const checklist = /^\s*-\s+\[([ xX])\]\s+(.*?)\s*$/gm;
|
|
273
|
+
let ordinal = 0;
|
|
274
|
+
for (const match of sectionBody.matchAll(checklist)) {
|
|
275
|
+
ordinal += 1;
|
|
276
|
+
if (ordinal !== question.ordinal)
|
|
277
|
+
continue;
|
|
278
|
+
const lineStart = range.start + match.index;
|
|
279
|
+
const lineEnd = lineStart + match[0].length;
|
|
280
|
+
const placeholder = `- [ ] <propose-question:${question.id}:${question.ordinal}>`;
|
|
281
|
+
return sha256Text(`${content.slice(0, lineStart)}${placeholder}${content.slice(lineEnd)}`);
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
export function proposeOpenQuestionScope(question, proposeRoundId) {
|
|
286
|
+
const fingerprint = sha256Text(`${proposeRoundId}\n${proposeQuestionDecisionBasisDigest(question)}`);
|
|
287
|
+
return `${PROPOSE_OPEN_QUESTION_SCOPE_PREFIX}${fingerprint}:${question.id}`;
|
|
288
|
+
}
|
|
289
|
+
/** 兼容升级前已经展示给用户、但尚未登记的 scope。 */
|
|
290
|
+
export function legacyProposeOpenQuestionScope(question, proposeRoundId) {
|
|
291
|
+
const fingerprint = sha256Text(`${proposeRoundId}\n${question.path}\n${question.documentFingerprint}`);
|
|
292
|
+
return `${PROPOSE_OPEN_QUESTION_SCOPE_PREFIX}${fingerprint}:${question.id}`;
|
|
293
|
+
}
|
|
294
|
+
export function proposeOpenQuestionDisplayText(question) {
|
|
295
|
+
if (question.id.startsWith("DEC-") && question.text.startsWith(question.id)) {
|
|
296
|
+
return question.text.slice(question.id.length).replace(/^[\s::—–-]+/, "").trim();
|
|
297
|
+
}
|
|
298
|
+
return question.text.trim();
|
|
299
|
+
}
|
|
300
|
+
export function collectProposeQuestions(changeRoot) {
|
|
301
|
+
return PROPOSE_CONFIRMATION_DOCS.flatMap(path => {
|
|
302
|
+
const fullPath = join(changeRoot, path);
|
|
303
|
+
return existsSync(fullPath) ? parseProposeQuestions(readFileSync(fullPath, "utf8"), path) : [];
|
|
304
|
+
});
|
|
305
|
+
}
|
|
215
306
|
export function countProposeOpenQuestionsInContent(content) {
|
|
216
307
|
return countOpenChecklistItemsInSection(content, PROPOSE_CONFIRMATION_HEADINGS);
|
|
217
308
|
}
|
|
@@ -543,6 +634,31 @@ function documentAnchorParts(anchor) {
|
|
|
543
634
|
? parts
|
|
544
635
|
: [anchor];
|
|
545
636
|
}
|
|
637
|
+
function documentAnchorCandidates(content, path, requested) {
|
|
638
|
+
const candidates = [];
|
|
639
|
+
for (const match of content.matchAll(/^#{1,6}[\t ]+(.+?)(?:[\t ]+#+)?[\t ]*$/gm)) {
|
|
640
|
+
const anchor = match[1]?.trim();
|
|
641
|
+
if (anchor)
|
|
642
|
+
candidates.push({ anchor, index: match.index ?? candidates.length });
|
|
643
|
+
}
|
|
644
|
+
for (const match of content.matchAll(/(?:^|[^A-Za-z0-9_-])((?:TEST|CHAIN|IDC)-[A-Za-z0-9_-]+)(?![A-Za-z0-9_-])/gm)) {
|
|
645
|
+
candidates.push({ anchor: match[1], index: match.index ?? candidates.length });
|
|
646
|
+
}
|
|
647
|
+
const normalizedRequested = requested.toLocaleLowerCase();
|
|
648
|
+
const score = (anchor) => {
|
|
649
|
+
const normalized = anchor.toLocaleLowerCase();
|
|
650
|
+
if (normalized === normalizedRequested)
|
|
651
|
+
return 100;
|
|
652
|
+
if (normalized.includes(normalizedRequested) || normalizedRequested.includes(normalized))
|
|
653
|
+
return 50;
|
|
654
|
+
const requestedTokens = new Set(normalizedRequested.split(/[^\p{L}\p{N}_-]+/u).filter(Boolean));
|
|
655
|
+
return normalized.split(/[^\p{L}\p{N}_-]+/u).filter(Boolean).filter(token => requestedTokens.has(token)).length * 10;
|
|
656
|
+
};
|
|
657
|
+
return [...new Map(candidates.map(candidate => [candidate.anchor, candidate])).values()]
|
|
658
|
+
.sort((left, right) => score(right.anchor) - score(left.anchor) || left.index - right.index)
|
|
659
|
+
.slice(0, 8)
|
|
660
|
+
.map(candidate => `${path}#${candidate.anchor}`);
|
|
661
|
+
}
|
|
546
662
|
function canonicalDocumentRefPath(path) {
|
|
547
663
|
if (path === "discovery.md" || path === "test-contract.md") {
|
|
548
664
|
return join(".superspec", "artifacts", path);
|
|
@@ -589,7 +705,8 @@ export function validateExecutionRequirementDocumentReferences(changeRoot, contr
|
|
|
589
705
|
const targetContent = readFileSync(target, "utf8");
|
|
590
706
|
for (const anchor of documentAnchorParts(parsed.anchor)) {
|
|
591
707
|
if (!documentContainsAnchor(targetContent, anchor)) {
|
|
592
|
-
|
|
708
|
+
const candidates = documentAnchorCandidates(targetContent, parsed.path, anchor);
|
|
709
|
+
errors.push(`${contract.taskId} 的引用锚点不存在:${parsed.path}#${anchor}${candidates.length > 0 ? `;可用锚点:${candidates.join("、")}` : ""}`);
|
|
593
710
|
}
|
|
594
711
|
}
|
|
595
712
|
}
|
package/dist/next.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { NextOutput } from "./types.ts";
|
|
2
|
-
/** next
|
|
2
|
+
/** next 命令:读取当前状态,返回唯一可执行路径,并登记正式展示的用户问题。 */
|
|
3
3
|
export declare function next(projectRoot: string, change: string, changeRoot: string, defaultRisk?: import("./review.ts").ReviewRisk): NextOutput;
|
package/dist/next.js
CHANGED
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
// SuperSpec 流程引擎 — next:返回可执行路径
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { rebuildSnapshot } from "./sync.js";
|
|
3
|
-
import { readEvents } from "./store.js";
|
|
5
|
+
import { appendEvent, makeEvent, readEvents, withLock } from "./store.js";
|
|
4
6
|
import { requiredJobActions } from "./job_action.js";
|
|
5
7
|
import { planNextStep } from "./phase_plan.js";
|
|
6
8
|
import { workflowRiskForProject } from "./workflow_config.js";
|
|
9
|
+
import { currentExploreRoundId } from "./explore_round.js";
|
|
10
|
+
import { currentProposeRoundId } from "./propose_round.js";
|
|
11
|
+
import { discoveryQuestionDecisionBasisDigest, legacyDiscoveryOpenQuestionScope, parseDiscoveryOpenQuestions, collectProposeQuestions, proposeQuestionDecisionBasisDigest, legacyProposeOpenQuestionScope, } from "./format.js";
|
|
12
|
+
function recordPresentedQuestion(projectRoot, change, changeRoot, output) {
|
|
13
|
+
if (output.path !== "ask_user")
|
|
14
|
+
return;
|
|
15
|
+
const isExplore = output.ask_user.scope.startsWith("explore_open_question:");
|
|
16
|
+
const isPropose = output.ask_user.scope.startsWith("propose_open_question:");
|
|
17
|
+
if (!isExplore && !isPropose)
|
|
18
|
+
return;
|
|
19
|
+
const events = readEvents(projectRoot, change);
|
|
20
|
+
if (isExplore) {
|
|
21
|
+
const current = parseDiscoveryOpenQuestions(readFileSync(join(changeRoot, ".superspec", "artifacts", "discovery.md"), "utf8"))[0];
|
|
22
|
+
if (!current)
|
|
23
|
+
return;
|
|
24
|
+
const roundId = currentExploreRoundId(events);
|
|
25
|
+
const latest = [...events].reverse().find(event => {
|
|
26
|
+
if (event.event_type !== "user_question_presented")
|
|
27
|
+
return false;
|
|
28
|
+
const payload = event.payload;
|
|
29
|
+
return payload.phase === "explore" && payload.round_id === roundId && payload.question_id === current.id &&
|
|
30
|
+
(!current.id.startsWith("item-") || payload.question_ordinal === current.ordinal);
|
|
31
|
+
});
|
|
32
|
+
if (latest?.payload?.scope === output.ask_user.scope)
|
|
33
|
+
return;
|
|
34
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_question_presented", {
|
|
35
|
+
phase: "explore",
|
|
36
|
+
round_id: roundId,
|
|
37
|
+
scope: output.ask_user.scope,
|
|
38
|
+
legacy_scope: legacyDiscoveryOpenQuestionScope(current, roundId),
|
|
39
|
+
question: output.ask_user.question,
|
|
40
|
+
question_id: current.id,
|
|
41
|
+
question_ordinal: current.ordinal,
|
|
42
|
+
decision_basis_digest: discoveryQuestionDecisionBasisDigest(current),
|
|
43
|
+
}));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const current = collectProposeQuestions(changeRoot).find(question => question.status === "open");
|
|
47
|
+
if (!current)
|
|
48
|
+
return;
|
|
49
|
+
const roundId = currentProposeRoundId(events);
|
|
50
|
+
const latest = [...events].reverse().find(event => {
|
|
51
|
+
if (event.event_type !== "user_question_presented")
|
|
52
|
+
return false;
|
|
53
|
+
const payload = event.payload;
|
|
54
|
+
return payload.phase === "propose" && payload.round_id === roundId && payload.path === current.path && payload.question_id === current.id &&
|
|
55
|
+
(!current.id.startsWith("item-") || payload.question_ordinal === current.ordinal);
|
|
56
|
+
});
|
|
57
|
+
if (latest?.payload?.scope === output.ask_user.scope)
|
|
58
|
+
return;
|
|
59
|
+
appendEvent(projectRoot, change, makeEvent(change, "user_question_presented", {
|
|
60
|
+
phase: "propose",
|
|
61
|
+
round_id: roundId,
|
|
62
|
+
scope: output.ask_user.scope,
|
|
63
|
+
legacy_scope: legacyProposeOpenQuestionScope(current, roundId),
|
|
64
|
+
question: output.ask_user.question,
|
|
65
|
+
path: current.path,
|
|
66
|
+
question_id: current.id,
|
|
67
|
+
question_ordinal: current.ordinal,
|
|
68
|
+
decision_basis_digest: proposeQuestionDecisionBasisDigest(current),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
7
71
|
function requiredJobsOutput(state, change, jobs, reason) {
|
|
8
72
|
return {
|
|
9
73
|
state,
|
|
@@ -33,8 +97,24 @@ function toNextOutput(change, plan) {
|
|
|
33
97
|
switch (plan.kind) {
|
|
34
98
|
case "required_jobs":
|
|
35
99
|
return requiredJobsOutput(plan.state, change, plan.jobs, plan.reason);
|
|
100
|
+
case "artifact_required":
|
|
101
|
+
return {
|
|
102
|
+
state: plan.state,
|
|
103
|
+
path: "artifact_required",
|
|
104
|
+
artifact: plan.artifact,
|
|
105
|
+
resume: plan.resume,
|
|
106
|
+
reason: plan.reason,
|
|
107
|
+
};
|
|
36
108
|
case "ask_user":
|
|
37
109
|
return { state: plan.state, path: "ask_user", ask_user: plan.ask, reason: plan.reason };
|
|
110
|
+
case "material_update_required":
|
|
111
|
+
return {
|
|
112
|
+
state: plan.state,
|
|
113
|
+
path: "material_update_required",
|
|
114
|
+
errors: plan.errors,
|
|
115
|
+
resume: { argv: ["superspec", "transition", "next", "--change", change] },
|
|
116
|
+
reason: plan.reason,
|
|
117
|
+
};
|
|
38
118
|
case "run_transition":
|
|
39
119
|
return {
|
|
40
120
|
state: plan.state,
|
|
@@ -52,16 +132,21 @@ function toNextOutput(change, plan) {
|
|
|
52
132
|
};
|
|
53
133
|
}
|
|
54
134
|
}
|
|
55
|
-
/** next
|
|
135
|
+
/** next 命令:读取当前状态,返回唯一可执行路径,并登记正式展示的用户问题。 */
|
|
56
136
|
export function next(projectRoot, change, changeRoot, defaultRisk = workflowRiskForProject(projectRoot)) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
return withLock(projectRoot, change, () => {
|
|
138
|
+
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
139
|
+
const events = readEvents(projectRoot, change);
|
|
140
|
+
const plannedNextStep = planNextStep({ projectRoot, change, changeRoot, events, snapshot, mode: { kind: "risk", risk: defaultRisk } });
|
|
141
|
+
if (plannedNextStep) {
|
|
142
|
+
const output = toNextOutput(change, plannedNextStep);
|
|
143
|
+
recordPresentedQuestion(projectRoot, change, changeRoot, output);
|
|
144
|
+
return output;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
state: snapshot.state,
|
|
148
|
+
path: "done",
|
|
149
|
+
reason: `状态 ${snapshot.state} 没有可执行下一步`,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
67
152
|
}
|
|
@@ -123,6 +123,7 @@ const SPECS = {
|
|
|
123
123
|
epoch: events => latestTransition(events, payload => payload.from_state === "apply" && payload.to_state === "apply_done"),
|
|
124
124
|
},
|
|
125
125
|
};
|
|
126
|
+
const CURRENT_USER_DECISION_NOTICE = "请向用户展示本次选择并等待当前明确答复;不得用启动工作流、要求推进、一般授权、历史偏好或模型推断代替本次回答。";
|
|
126
127
|
function boundaryForState(state) {
|
|
127
128
|
switch (state) {
|
|
128
129
|
case "explore": return "explore_to_propose";
|
|
@@ -255,7 +256,8 @@ export function phaseConfirmationForBoundary(projectRoot, events, snapshot, boun
|
|
|
255
256
|
const summary = boundary === "propose_to_apply"
|
|
256
257
|
? proposeTaskDeliverySummary(openspecChangeRoot(projectRoot, snapshot.change_id))
|
|
257
258
|
: null;
|
|
258
|
-
const
|
|
259
|
+
const boundaryQuestion = `${spec.question}\n\n${CURRENT_USER_DECISION_NOTICE}`;
|
|
260
|
+
const question = summary ? `${summary}\n\n${boundaryQuestion}` : boundaryQuestion;
|
|
259
261
|
const actions = buildActions(snapshot.change_id, boundary, scope, question, spec.actions, risk);
|
|
260
262
|
return {
|
|
261
263
|
boundary,
|
package/dist/phase_plan.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReviewGateRule } from "./review_job_gates.ts";
|
|
2
2
|
import { exploreAnswerRegistrationPayload } from "./explore_round.ts";
|
|
3
|
-
import
|
|
3
|
+
import { proposeAnswerRegistrationPayload } from "./propose_round.ts";
|
|
4
|
+
import type { AcceptedMaterialFollowupContinuation, AskUser, Event, ExecutionPolicy, Job, JobRole, PlanningValidationProfile, WorkflowArtifactKind, State } from "./types.ts";
|
|
4
5
|
import type { Snapshot } from "./types.ts";
|
|
5
6
|
import type { ReviewRisk } from "./review.ts";
|
|
6
7
|
export type TransitionName = "explore" | "propose-ready" | "start-apply" | "task-start" | "task-complete" | "review-ready" | "reopen" | "accept";
|
|
@@ -39,11 +40,28 @@ export type NextStepPlan = {
|
|
|
39
40
|
state: State;
|
|
40
41
|
jobs: Job[];
|
|
41
42
|
reason: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "artifact_required";
|
|
45
|
+
state: State;
|
|
46
|
+
artifact: {
|
|
47
|
+
kind: WorkflowArtifactKind;
|
|
48
|
+
path: string;
|
|
49
|
+
operation: "create_or_update";
|
|
50
|
+
};
|
|
51
|
+
resume: {
|
|
52
|
+
argv: string[];
|
|
53
|
+
};
|
|
54
|
+
reason: string;
|
|
42
55
|
} | {
|
|
43
56
|
kind: "ask_user";
|
|
44
57
|
state: State;
|
|
45
58
|
ask: AskUser;
|
|
46
59
|
reason: string;
|
|
60
|
+
} | {
|
|
61
|
+
kind: "material_update_required";
|
|
62
|
+
state: State;
|
|
63
|
+
errors: string[];
|
|
64
|
+
reason: string;
|
|
47
65
|
} | {
|
|
48
66
|
kind: "run_transition";
|
|
49
67
|
state: State;
|
|
@@ -70,6 +88,7 @@ export type TransitionDecisionPlan = {
|
|
|
70
88
|
kind: "create_gate_jobs";
|
|
71
89
|
gate: ReviewGateRule;
|
|
72
90
|
roles: JobRole[];
|
|
91
|
+
requiredRoles: JobRole[];
|
|
73
92
|
reason: string;
|
|
74
93
|
} | {
|
|
75
94
|
kind: "advance";
|
|
@@ -90,6 +109,7 @@ export declare function proposalDocsBaseline(changeRoot: string): Record<string,
|
|
|
90
109
|
export declare function discoveryDocsBaseline(changeRoot: string): Record<string, string>;
|
|
91
110
|
/** 新 Explore 轮次冻结已有已确认事项,避免把历史答复当作本轮遗漏。 */
|
|
92
111
|
export declare function exploreAnswerRegistrationPayloadForChange(changeRoot: string): ReturnType<typeof exploreAnswerRegistrationPayload>;
|
|
112
|
+
export declare function proposeAnswerRegistrationPayloadForChange(changeRoot: string): ReturnType<typeof proposeAnswerRegistrationPayload>;
|
|
93
113
|
export declare function latestAcceptedProposalBaseline(events: Event[]): Record<string, string> | null;
|
|
94
114
|
export declare function latestReopenProposeBaseline(events: Event[]): Record<string, string> | null;
|
|
95
115
|
export declare function latestReopenExploreBaseline(events: Event[]): Record<string, string> | null;
|