@peterxiaoyang/superspec 0.1.56 → 0.1.58
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 +28 -32
- package/dist/approved_ref.d.ts +1 -1
- package/dist/approved_ref.js +9 -8
- package/dist/cli.js +5 -1
- package/dist/code_review.d.ts +6 -0
- package/dist/code_review.js +56 -1
- package/dist/format.d.ts +25 -1
- package/dist/format.js +235 -1
- package/dist/phase_confirmation.js +7 -2
- package/dist/phase_plan.d.ts +4 -0
- package/dist/phase_plan.js +160 -44
- package/dist/propose_round.d.ts +7 -1
- package/dist/propose_round.js +35 -0
- package/dist/record.d.ts +2 -0
- package/dist/record.js +23 -5
- package/dist/transition.js +3 -3
- package/dist/types.d.ts +19 -1
- package/dist/workflow_config.d.ts +15 -0
- package/dist/workflow_config.js +47 -0
- package/package.json +1 -1
- package/templates/workflow/prompts/architect.md +1 -0
- package/templates/workflow/prompts/code-reviewer.md +2 -1
- package/templates/workflow/prompts/critic.md +2 -1
- package/templates/workflow/prompts/executor.md +1 -0
- package/templates/workflow/prompts/explore.md +1 -1
- package/templates/workflow/skills/superspec-explore/SKILL.md +16 -12
- package/templates/workflow/skills/superspec-propose/SKILL.md +28 -5
package/dist/phase_plan.js
CHANGED
|
@@ -2,16 +2,19 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { EXPLORE_DISCOVERY_REVIEW_GATE, PROPOSE_FINAL_REVIEW_GATE } from "./review_job_gates.js";
|
|
4
4
|
import { currentExploreRoundId, exploreAnswerRegistrationPayload, unregisteredClosedExploreQuestions, unresolvedPresentedExploreQuestionScopes, } from "./explore_round.js";
|
|
5
|
-
import { currentProposeOpenQuestion, currentProposeRoundId, proposeAnswerRegistrationPayload, unregisteredClosedProposeQuestions, unresolvedPresentedProposeQuestionScopes, } from "./propose_round.js";
|
|
6
|
-
import { discoveryOpenQuestionDisplayText, discoveryOpenQuestionScope, proposeOpenQuestionDisplayText, proposeOpenQuestionScope, parseExecutionRequirements, parseDiscoveryOpenQuestions, parseTasksMd, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, } from "./format.js";
|
|
5
|
+
import { currentProposeOpenQuestion, currentProposeRoundId, isPlanningValidationProfile, proposeAnswerRegistrationPayload, unregisteredClosedProposeQuestions, unresolvedPresentedProposeQuestionScopes, } from "./propose_round.js";
|
|
6
|
+
import { discoveryOpenQuestionDisplayText, discoveryOpenQuestionScope, proposeOpenQuestionDisplayText, proposeOpenQuestionScope, parseExecutionRequirements, parseDiscoveryOpenQuestions, parseTasksMd, parseTestContractEntries, isFixTaskId, pendingTasksInContent, validateDiscovery, validateExecutionRequirements, validateExecutionRequirementDocumentReferences, validateProposalImpact, validateTasksDocument, parseStructureChangeLedger, validateStructureChangeLedger, } from "./format.js";
|
|
7
7
|
import { currentGitHead } from "./git_state.js";
|
|
8
8
|
import { validateOpenSpecChange } from "./openspec.js";
|
|
9
|
-
import { docRef, sha256File } from "./store.js";
|
|
9
|
+
import { docRef, sha256File, sha256Text, findLatestEvent } from "./store.js";
|
|
10
10
|
import { isReviewReadyVerifier, isFreshReviewVerifier, historicalProposeReadyRoles, readReviewPolicyFromEvents, reviewGateRoleResolution, reviewRejectionOverrideScope, reviewEvidenceDigest, } from "./review.js";
|
|
11
|
-
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewJobStaleReason, collectCodeReviewGateFacts, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
11
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_REPAIR_SCOPE_PREFIX, codeReviewDecisionScope, codeReviewFindingNeedsUserDecision, codeReviewJobStaleReason, collectCodeReviewGateFacts, countReviewFixReopensSinceStartApply, currentCodeReviewWorkingPaths, isReviewFixCapReached, latestCodeReviewFailedStatus, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, } from "./code_review.js";
|
|
12
12
|
import { isPhaseAdvanceAuthorized, latestAcceptedPhaseDecision, phaseConfirmationCommitPayload, phaseConfirmationForBoundary, phaseConfirmationMissingMessage, } from "./phase_confirmation.js";
|
|
13
13
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
14
|
-
import { workflowRiskForProposeRound, workflowRiskForState } from "./workflow_config.js";
|
|
14
|
+
import { workflowRiskForProposeRound, workflowRiskForState, workflowBudgetForRisk } from "./workflow_config.js";
|
|
15
|
+
export const PLAN_SIZE_BUDGET_SCOPE_PREFIX = "plan_size_budget:";
|
|
16
|
+
export const PLAN_SIZE_BUDGET_CONFIRM_ANSWER = "确认规模合理,继续审查";
|
|
17
|
+
export const PLAN_SIZE_BUDGET_SHRINK_ANSWER = "回去收缩计划";
|
|
15
18
|
/** 从失败 finding 提取定位上下文:只回传 evidence(位置事实),不回传 description——那是审查建议叙事,不进执行上下文。 */
|
|
16
19
|
function reviewFindingContext(finding) {
|
|
17
20
|
const evidence = typeof finding?.evidence === "string" ? finding.evidence.trim() : "";
|
|
@@ -137,32 +140,47 @@ const REQUIRED_DESIGN_HEADINGS = [
|
|
|
137
140
|
"## 实现方案",
|
|
138
141
|
];
|
|
139
142
|
function validateDesignPlan(changeRoot, profile) {
|
|
140
|
-
if (profile?.openspec.mode !== "strict" || profile.design?.schema_version !== 1)
|
|
141
|
-
return null;
|
|
142
|
-
const designPath = join(changeRoot, "design.md");
|
|
143
|
-
if (!existsSync(designPath))
|
|
144
|
-
return null;
|
|
145
|
-
const lines = readFileSync(designPath, "utf8").split(/\r?\n/).map(line => line.trimEnd());
|
|
146
143
|
const errors = [];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
144
|
+
// 稳定标题检查自 design.schema_version 引入起生效;更早的 strict round 没有 design 字段,保持不检查。
|
|
145
|
+
if (profile?.openspec.mode === "strict" && profile.design != null) {
|
|
146
|
+
const designPath = join(changeRoot, "design.md");
|
|
147
|
+
if (existsSync(designPath)) {
|
|
148
|
+
const lines = readFileSync(designPath, "utf8").split(/\r?\n/).map(line => line.trimEnd());
|
|
149
|
+
const headingIndexes = new Map();
|
|
150
|
+
for (const heading of REQUIRED_DESIGN_HEADINGS) {
|
|
151
|
+
const indexes = lines.flatMap((line, index) => line === heading ? [index] : []);
|
|
152
|
+
headingIndexes.set(heading, indexes);
|
|
153
|
+
if (indexes.length === 0)
|
|
154
|
+
errors.push(`design.md 缺少稳定结构标题:${heading}`);
|
|
155
|
+
if (indexes.length > 1)
|
|
156
|
+
errors.push(`design.md 稳定结构标题重复:${heading}`);
|
|
157
|
+
}
|
|
158
|
+
if (errors.length === 0) {
|
|
159
|
+
let previousIndex = -1;
|
|
160
|
+
for (const heading of REQUIRED_DESIGN_HEADINGS) {
|
|
161
|
+
const indexes = headingIndexes.get(heading);
|
|
162
|
+
if (indexes[0] <= previousIndex) {
|
|
163
|
+
errors.push(`design.md 稳定结构标题顺序错误:${heading}`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
previousIndex = indexes[0];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
155
170
|
}
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
if (profile?.design?.schema_version === 2) {
|
|
172
|
+
const designPath = join(changeRoot, "design.md");
|
|
173
|
+
if (!existsSync(designPath)) {
|
|
174
|
+
errors.push('design.md 缺少 ## 结构变更清单;没有结构变更时在该标题下写"无"');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const ledger = parseStructureChangeLedger(readFileSync(designPath, "utf8"));
|
|
178
|
+
const validation = validateStructureChangeLedger(changeRoot, ledger);
|
|
179
|
+
if (!validation.ok)
|
|
180
|
+
errors.push(...validation.errors);
|
|
181
|
+
}
|
|
164
182
|
}
|
|
165
|
-
return null;
|
|
183
|
+
return errors.length > 0 ? [...new Set(errors)].join(";") : null;
|
|
166
184
|
}
|
|
167
185
|
export function executionPolicyForRisk(risk) {
|
|
168
186
|
return risk === "strict" ? "tdd" : "green_only";
|
|
@@ -353,6 +371,102 @@ function latestStartApplyIndex(events) {
|
|
|
353
371
|
}
|
|
354
372
|
return -1;
|
|
355
373
|
}
|
|
374
|
+
function planSizeCountDigest(taskCount, testCount) {
|
|
375
|
+
return sha256Text(`${taskCount},${testCount}`).replace(/^sha256:/, "");
|
|
376
|
+
}
|
|
377
|
+
export function planSizeBudgetScope(proposeRoundId, taskCount, testCount) {
|
|
378
|
+
return `${PLAN_SIZE_BUDGET_SCOPE_PREFIX}${proposeRoundId}:${planSizeCountDigest(taskCount, testCount)}`;
|
|
379
|
+
}
|
|
380
|
+
function countNonFixPlanTasks(changeRoot) {
|
|
381
|
+
const tasksContent = readFileSync(join(changeRoot, "tasks.md"), "utf8");
|
|
382
|
+
return parseTasksMd(tasksContent).filter(task => !isFixTaskId(task.taskId)).length;
|
|
383
|
+
}
|
|
384
|
+
function countPlanTestEntries(changeRoot) {
|
|
385
|
+
const testContractPath = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
|
|
386
|
+
if (!existsSync(testContractPath))
|
|
387
|
+
return 0;
|
|
388
|
+
const parsed = parseTestContractEntries(readFileSync(testContractPath, "utf8"));
|
|
389
|
+
return parsed.ok ? parsed.entries.length : 0;
|
|
390
|
+
}
|
|
391
|
+
function isOverPlanSizeBudget(budget, taskCount, testCount) {
|
|
392
|
+
const overTasks = budget.tasks !== null && taskCount > budget.tasks;
|
|
393
|
+
const overTests = budget.tests !== null && testCount > budget.tests;
|
|
394
|
+
return overTasks || overTests;
|
|
395
|
+
}
|
|
396
|
+
function latestPlanSizeBudgetAnswer(events, scope) {
|
|
397
|
+
const event = findLatestEvent(events, "user_decision_recorded", ev => {
|
|
398
|
+
const payload = ev.payload;
|
|
399
|
+
if (payload.scope !== scope)
|
|
400
|
+
return false;
|
|
401
|
+
if (payload.accepted === false)
|
|
402
|
+
return false;
|
|
403
|
+
return typeof payload.answer === "string" && payload.answer.trim() !== "";
|
|
404
|
+
});
|
|
405
|
+
if (!event)
|
|
406
|
+
return null;
|
|
407
|
+
const answer = event.payload.answer;
|
|
408
|
+
return typeof answer === "string" ? answer.trim() : null;
|
|
409
|
+
}
|
|
410
|
+
function planSizeBudgetOverageMessage(budget, taskCount, testCount) {
|
|
411
|
+
const parts = [];
|
|
412
|
+
if (budget.tasks !== null && taskCount > budget.tasks) {
|
|
413
|
+
parts.push(`任务 ${taskCount} 个(预算 ${budget.tasks})`);
|
|
414
|
+
}
|
|
415
|
+
if (budget.tests !== null && testCount > budget.tests) {
|
|
416
|
+
parts.push(`TEST ${testCount} 个(预算 ${budget.tests})`);
|
|
417
|
+
}
|
|
418
|
+
return parts.join(";");
|
|
419
|
+
}
|
|
420
|
+
function planSizeBudgetSkipReason(projectRoot, changeRoot, events, risk) {
|
|
421
|
+
const budget = workflowBudgetForRisk(projectRoot, risk);
|
|
422
|
+
if (!budget)
|
|
423
|
+
return null;
|
|
424
|
+
const taskCount = countNonFixPlanTasks(changeRoot);
|
|
425
|
+
const testCount = countPlanTestEntries(changeRoot);
|
|
426
|
+
if (!isOverPlanSizeBudget(budget, taskCount, testCount))
|
|
427
|
+
return null;
|
|
428
|
+
const scope = planSizeBudgetScope(currentProposeRoundId(events), taskCount, testCount);
|
|
429
|
+
const answer = latestPlanSizeBudgetAnswer(events, scope);
|
|
430
|
+
if (answer === PLAN_SIZE_BUDGET_CONFIRM_ANSWER)
|
|
431
|
+
return null;
|
|
432
|
+
if (answer === PLAN_SIZE_BUDGET_SHRINK_ANSWER) {
|
|
433
|
+
return `计划规模仍超过预算(${planSizeBudgetOverageMessage(budget, taskCount, testCount)}),请收缩 tasks 或 test-contract 后重试`;
|
|
434
|
+
}
|
|
435
|
+
return `计划规模超过预算(${planSizeBudgetOverageMessage(budget, taskCount, testCount)}),需要先确认规模或收缩计划`;
|
|
436
|
+
}
|
|
437
|
+
function planSizeBudgetNextStep(context) {
|
|
438
|
+
const { change, changeRoot, events, mode } = context;
|
|
439
|
+
const budget = workflowBudgetForRisk(context.projectRoot, mode.risk);
|
|
440
|
+
if (!budget)
|
|
441
|
+
return null;
|
|
442
|
+
const taskCount = countNonFixPlanTasks(changeRoot);
|
|
443
|
+
const testCount = countPlanTestEntries(changeRoot);
|
|
444
|
+
if (!isOverPlanSizeBudget(budget, taskCount, testCount))
|
|
445
|
+
return null;
|
|
446
|
+
const scope = planSizeBudgetScope(currentProposeRoundId(events), taskCount, testCount);
|
|
447
|
+
const answer = latestPlanSizeBudgetAnswer(events, scope);
|
|
448
|
+
if (answer === PLAN_SIZE_BUDGET_CONFIRM_ANSWER)
|
|
449
|
+
return null;
|
|
450
|
+
if (answer === PLAN_SIZE_BUDGET_SHRINK_ANSWER) {
|
|
451
|
+
return {
|
|
452
|
+
kind: "material_update_required",
|
|
453
|
+
state: "propose",
|
|
454
|
+
errors: [`计划规模仍超过预算:${planSizeBudgetOverageMessage(budget, taskCount, testCount)}`],
|
|
455
|
+
reason: "已选择收缩计划但规模未变化",
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const overage = planSizeBudgetOverageMessage(budget, taskCount, testCount);
|
|
459
|
+
const question = `当前计划规模为 ${overage}。请确认是否按此规模继续进入审查,或先回去收缩 tasks / test-contract。`;
|
|
460
|
+
const ask = {
|
|
461
|
+
question,
|
|
462
|
+
allowed_answers: [PLAN_SIZE_BUDGET_CONFIRM_ANSWER, PLAN_SIZE_BUDGET_SHRINK_ANSWER],
|
|
463
|
+
scope,
|
|
464
|
+
record_argv: ["superspec", "record", "user-decision", "--change", change, "--input", "-"],
|
|
465
|
+
record_input: { scope, question, answer: null },
|
|
466
|
+
required_fields: ["answer"],
|
|
467
|
+
};
|
|
468
|
+
return { kind: "ask_user", state: "propose", ask, reason: "计划规模超过预算" };
|
|
469
|
+
}
|
|
356
470
|
function executionRequirementVersionFromPayload(payload) {
|
|
357
471
|
return payload.execution_requirement_version === 2 ? 2 : 1;
|
|
358
472
|
}
|
|
@@ -374,22 +488,12 @@ function executionRequirementVersionForProposeRound(events) {
|
|
|
374
488
|
}
|
|
375
489
|
return 1;
|
|
376
490
|
}
|
|
377
|
-
function isPlanningValidationProfile(value) {
|
|
378
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
379
|
-
return false;
|
|
380
|
-
const profile = value;
|
|
381
|
-
if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
|
|
382
|
-
return false;
|
|
383
|
-
const designValid = profile.design == null || profile.design.schema_version === 1;
|
|
384
|
-
return designValid && (profile.openspec.mode === "disabled" ||
|
|
385
|
-
profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string");
|
|
386
|
-
}
|
|
387
491
|
/** 新 planning round 在进入 propose 时冻结当前 OpenSpec 校验契约。 */
|
|
388
492
|
export function planningValidationProfileForNewRound(projectRoot) {
|
|
389
493
|
const configDigest = sha256File(join(projectRoot, "openspec", "config.yaml"));
|
|
390
494
|
return configDigest == null
|
|
391
|
-
? { version: 2, openspec: { mode: "disabled" }, design: { schema_version:
|
|
392
|
-
: { version: 2, openspec: { mode: "strict", config_digest: configDigest }, design: { schema_version:
|
|
495
|
+
? { version: 2, openspec: { mode: "disabled" }, design: { schema_version: 2 } }
|
|
496
|
+
: { version: 2, openspec: { mode: "strict", config_digest: configDigest }, design: { schema_version: 2 } };
|
|
393
497
|
}
|
|
394
498
|
/** propose 状态尚未 ready 时,从进入本 planning round 的事件读取冻结 profile。 */
|
|
395
499
|
function planningValidationProfileForPendingProposeRound(events) {
|
|
@@ -664,6 +768,9 @@ export function planNextStep(context) {
|
|
|
664
768
|
reason: preflight.error,
|
|
665
769
|
};
|
|
666
770
|
}
|
|
771
|
+
const budgetStep = planSizeBudgetNextStep(context);
|
|
772
|
+
if (budgetStep)
|
|
773
|
+
return budgetStep;
|
|
667
774
|
const proposalReviewJobs = PROPOSE_FINAL_REVIEW_GATE.openJobsForGate(snapshot);
|
|
668
775
|
if (proposalReviewJobs.length > 0) {
|
|
669
776
|
return requiredJobs("propose", proposalReviewJobs, `有 ${proposalReviewJobs.length} 个待完成 proposal 审查工作项`);
|
|
@@ -836,7 +943,8 @@ function planApplyDoneNext(context) {
|
|
|
836
943
|
const findingId = pendingFinding?.id ?? "";
|
|
837
944
|
const type = pendingFinding?.type;
|
|
838
945
|
const decision = pendingFinding?.decision;
|
|
839
|
-
|
|
946
|
+
const reviewFixCapReached = isReviewFixCapReached(context.projectRoot, events);
|
|
947
|
+
if (findingId && type === "implementation" && !reviewFixCapReached) {
|
|
840
948
|
return {
|
|
841
949
|
kind: "run_transition",
|
|
842
950
|
state: "apply_done",
|
|
@@ -852,7 +960,7 @@ function planApplyDoneNext(context) {
|
|
|
852
960
|
reason: `代码审查发现纯代码实现问题 ${findingId},回到实现阶段修复`,
|
|
853
961
|
};
|
|
854
962
|
}
|
|
855
|
-
if (findingId && (type
|
|
963
|
+
if (findingId && codeReviewFindingNeedsUserDecision(type, reviewFixCapReached)) {
|
|
856
964
|
if (decision?.answer === "reopen_propose") {
|
|
857
965
|
return {
|
|
858
966
|
kind: "run_transition",
|
|
@@ -880,9 +988,14 @@ function planApplyDoneNext(context) {
|
|
|
880
988
|
}
|
|
881
989
|
const problemKind = type === "spec"
|
|
882
990
|
? "方案或需求文档可能需要调整"
|
|
883
|
-
: "
|
|
991
|
+
: type === "mixed"
|
|
992
|
+
? "代码实现和方案文档都可能有关"
|
|
993
|
+
: "纯代码实现问题";
|
|
994
|
+
const capNote = reviewFixCapReached && type === "implementation"
|
|
995
|
+
? `本轮 Apply 已自动修复 ${countReviewFixReopensSinceStartApply(events)} 次,`
|
|
996
|
+
: "";
|
|
884
997
|
const ask = {
|
|
885
|
-
question:
|
|
998
|
+
question: `${capNote}代码审查发现问题 ${findingId}:${problemKind}。请选择回到计划阶段修改文档、确认现有文档方向不变并回到实现阶段修代码,或驳回该问题;无论选择哪一项都必须写明原因。`,
|
|
886
999
|
allowed_answers: [
|
|
887
1000
|
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_propose,
|
|
888
1001
|
CODE_REVIEW_DECISION_ANSWER_LABELS.reopen_apply,
|
|
@@ -1055,6 +1168,9 @@ function planProposeReadyTransition(context) {
|
|
|
1055
1168
|
if (unresolvedPresentedProposeQuestionScopes(context.events).length > 0) {
|
|
1056
1169
|
return { kind: "skip", message: "此前展示的设计问题缺少答复登记且已从计划材料消失,请恢复原问题并完成登记" };
|
|
1057
1170
|
}
|
|
1171
|
+
const budgetSkip = planSizeBudgetSkipReason(context.projectRoot, changeRoot, context.events, risk);
|
|
1172
|
+
if (budgetSkip)
|
|
1173
|
+
return { kind: "skip", message: budgetSkip };
|
|
1058
1174
|
const requiredRoles = PROPOSE_FINAL_REVIEW_GATE.requiredRolesForRisk(risk);
|
|
1059
1175
|
const gatePlan = reviewGatePlan(snapshot, context.events, changeRoot, PROPOSE_FINAL_REVIEW_GATE, requiredRoles);
|
|
1060
1176
|
if (gatePlan)
|
package/dist/propose_round.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type ProposeQuestion } from "./format.ts";
|
|
2
|
-
import type { Event } from "./types.ts";
|
|
2
|
+
import type { Event, PlanningValidationProfile } from "./types.ts";
|
|
3
|
+
export declare function isPlanningValidationProfile(value: unknown): value is PlanningValidationProfile;
|
|
4
|
+
/**
|
|
5
|
+
* 当前 planning round 的冻结 profile:优先取最近一次 propose-ready 写入的快照,
|
|
6
|
+
* 否则取进入 propose 的边界事件。两者都没有时是升级前的 v1 change。
|
|
7
|
+
*/
|
|
8
|
+
export declare function planningValidationProfileForCurrentRound(events: readonly Event[]): PlanningValidationProfile | null;
|
|
3
9
|
export declare function currentProposeRoundId(events: readonly Event[]): string;
|
|
4
10
|
export declare function proposeAnswerRegistrationPayload(changeRoot: string): {
|
|
5
11
|
propose_answer_registration: {
|
package/dist/propose_round.js
CHANGED
|
@@ -2,6 +2,41 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { collectProposeQuestions, parseProposeQuestions, proposeQuestionContextFingerprint, proposeQuestionDecisionBasisDigest, proposeQuestionKey, } from "./format.js";
|
|
4
4
|
const PROPOSE_ANSWER_REGISTRATION_VERSION = 1;
|
|
5
|
+
// ===== planning validation profile =====
|
|
6
|
+
//
|
|
7
|
+
// profile 在进入 propose 的边界事件上冻结,propose-ready 时复制到 propose_ready
|
|
8
|
+
// 事件。所有读取方共用这里的判定,避免各处复制一份而在升级时漏改。
|
|
9
|
+
export function isPlanningValidationProfile(value) {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
11
|
+
return false;
|
|
12
|
+
const profile = value;
|
|
13
|
+
if (profile.version !== 2 || !profile.openspec || typeof profile.openspec !== "object")
|
|
14
|
+
return false;
|
|
15
|
+
const designValid = profile.design == null
|
|
16
|
+
|| profile.design.schema_version === 1
|
|
17
|
+
|| profile.design.schema_version === 2;
|
|
18
|
+
return designValid && (profile.openspec.mode === "disabled" ||
|
|
19
|
+
profile.openspec.mode === "strict" && typeof profile.openspec.config_digest === "string");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 当前 planning round 的冻结 profile:优先取最近一次 propose-ready 写入的快照,
|
|
23
|
+
* 否则取进入 propose 的边界事件。两者都没有时是升级前的 v1 change。
|
|
24
|
+
*/
|
|
25
|
+
export function planningValidationProfileForCurrentRound(events) {
|
|
26
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
27
|
+
const event = events[index];
|
|
28
|
+
if (event.event_type !== "transition_commit")
|
|
29
|
+
continue;
|
|
30
|
+
const payload = event.payload;
|
|
31
|
+
const isReady = payload.transition === "propose-ready" && payload.to_state === "propose_ready";
|
|
32
|
+
if (!isReady && !isProposeRoundEntry(event))
|
|
33
|
+
continue;
|
|
34
|
+
return isPlanningValidationProfile(payload.planning_validation_profile)
|
|
35
|
+
? payload.planning_validation_profile
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
5
40
|
function isProposeRoundEntry(event) {
|
|
6
41
|
if (event.event_type !== "transition_commit")
|
|
7
42
|
return false;
|
package/dist/record.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { RecordResult, Job, JobPacket } from "./types.ts";
|
|
2
|
+
/** 报告文件的推荐落盘位置:工作流记录目录,不进入计划材料。 */
|
|
3
|
+
export declare function jobReportFilePath(change: string, jobId: string): string;
|
|
2
4
|
/** record job-submit:登记工作项结果 */
|
|
3
5
|
export declare function recordJobSubmit(projectRoot: string, change: string, changeRoot: string, jobId: string, reportFile: string): RecordResult;
|
|
4
6
|
/** record job-submit:从 JSON 内容登记工作项结果 */
|
package/dist/record.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ensureChangeLayout, readEvents, appendEvent, makeEvent, sha256File, sha
|
|
|
5
5
|
import { rebuildSnapshot } from "./sync.js";
|
|
6
6
|
import { changeRoot as openspecChangeRoot } from "./openspec.js";
|
|
7
7
|
import { isPhaseConfirmationScope, phaseActionForAnswer, phaseConfirmationForCurrentState, workflowRiskForPhaseConfirmation, } from "./phase_confirmation.js";
|
|
8
|
-
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
|
|
8
|
+
import { CODE_REVIEW_DECISION_ANSWER_LABELS, CODE_REVIEW_DECISION_SCOPE_PREFIX, codeReviewFindingNeedsUserDecision, codeReviewJobStaleReason, codeReviewDecisionAnswerLabel, currentCodeReviewWorkingPaths, isReviewFixCapReached, latestCodeReviewFailedStatus, normalizeCodeReviewDecisionAnswer, parseCodeReviewDecisionScope, } from "./code_review.js";
|
|
9
9
|
import { invalidReasonForSubmittedReport } from "./job_validity.js";
|
|
10
10
|
import { jobSubmitArgv } from "./job_action.js";
|
|
11
11
|
import { hasBehaviorAnchor, isCodeReviewClaimKind, resolveApprovedRefs, } from "./approved_ref.js";
|
|
@@ -679,10 +679,23 @@ function recordJobSubmitLoaded(projectRoot, change, changeRoot, jobId, job, even
|
|
|
679
679
|
return {
|
|
680
680
|
event_type: "job_accepted",
|
|
681
681
|
accepted: true,
|
|
682
|
-
message: `工作项 ${jobId}(${job.role}
|
|
682
|
+
message: `工作项 ${jobId}(${job.role})已接受${misplacedReportFileNote(change, reportPath)}`,
|
|
683
683
|
job_state: "accepted",
|
|
684
684
|
};
|
|
685
685
|
}
|
|
686
|
+
/** 报告文件的推荐落盘位置:工作流记录目录,不进入计划材料。 */
|
|
687
|
+
export function jobReportFilePath(change, jobId) {
|
|
688
|
+
return `.superspec/changes/${change}/jobs/${jobId}.report.json`;
|
|
689
|
+
}
|
|
690
|
+
/** 报告内容已存入 raw 记录;文件若写在 openspec change 目录里会跟计划材料一起进版本库,提示清理。 */
|
|
691
|
+
function misplacedReportFileNote(change, reportPath) {
|
|
692
|
+
if (!reportPath)
|
|
693
|
+
return "";
|
|
694
|
+
const normalized = reportPath.replace(/\\/g, "/");
|
|
695
|
+
if (!normalized.startsWith(`openspec/changes/${change}/`))
|
|
696
|
+
return "";
|
|
697
|
+
return `;报告内容已存入工作流记录,${normalized} 位于计划材料目录,请删除该文件(落盘位置见 packet 的 report_file_path)`;
|
|
698
|
+
}
|
|
686
699
|
/** record job-submit:登记工作项结果 */
|
|
687
700
|
export function recordJobSubmit(projectRoot, change, changeRoot, jobId, reportFile) {
|
|
688
701
|
return withLock(projectRoot, change, () => {
|
|
@@ -1003,8 +1016,9 @@ function recordUserDecisionLoaded(projectRoot, change, events, content, inputDig
|
|
|
1003
1016
|
const changeRoot = openspecChangeRoot(projectRoot, change);
|
|
1004
1017
|
const snapshot = rebuildSnapshot(projectRoot, change, changeRoot);
|
|
1005
1018
|
const status = latestCodeReviewFailedStatus(events);
|
|
1019
|
+
const reviewFixCapReached = isReviewFixCapReached(projectRoot, events);
|
|
1006
1020
|
const finding = ref && status?.terminal.job.job_id === ref.jobId
|
|
1007
|
-
? status.findings.find(item => item.id === ref.findingId && (item.type
|
|
1021
|
+
? status.findings.find(item => item.id === ref.findingId && codeReviewFindingNeedsUserDecision(item.type, reviewFixCapReached))
|
|
1008
1022
|
: null;
|
|
1009
1023
|
const staleReason = status && ref && status.terminal.job.job_id === ref.jobId
|
|
1010
1024
|
? codeReviewJobStaleReason(projectRoot, status.terminal.job, currentCodeReviewWorkingPaths(projectRoot, events), events)
|
|
@@ -1179,10 +1193,12 @@ function packetFieldDescriptions() {
|
|
|
1179
1193
|
changed_paths_partial_reason: "该任务(task)的提交段 diff 失败原因;存在时 changed_paths 只包含工作区对比结果,归属可能不完整。",
|
|
1180
1194
|
unattributed_paths: "代码审查范围中暂时无法归属到某个任务(task)的文件。",
|
|
1181
1195
|
added_code_paths: "相对本次代码审查基点新建的代码文件,供判断是否服务已批准行为。",
|
|
1196
|
+
structure_ledger: "design.md 中已批准的结构变更清单;清单是已批准结构的边界,清单外结构按 unjustified_addition 处理。",
|
|
1182
1197
|
claim_kind: "阻塞问题相对已批准计划的关系:漏做、破坏已有行为、或计划或验收没有要求的改动。",
|
|
1183
1198
|
approved_refs: "指向当前 change 已批准材料的引用;引擎只检查能否解析,apply 漏做还需要 TEST 或 spec Requirement。",
|
|
1184
1199
|
unknown_attribution_tasks: "因为缺少边界快照或提交段 diff 失败而无法完整计算改动归属的任务(task)。",
|
|
1185
1200
|
coverage_exemption_refs: "测试覆盖豁免引用:说明某个 TEST 为什么没有绑定到任务(task)。",
|
|
1201
|
+
report_file_path: "报告需要落盘时的文件位置(项目相对路径),位于工作流记录目录;报告内容登记后由引擎存入 raw 记录,不属于计划材料。",
|
|
1186
1202
|
code_review_gate: "最终验证读取的代码审查门禁事实:passed 指向已接受的代码审查工作项,skipped 表示本轮没有代码类改动。",
|
|
1187
1203
|
code_state_check: "代码状态检查:最终验证时用于判断代码审查后代码是否又发生变化。",
|
|
1188
1204
|
event_id: "事件 ID,用于追溯证据来源。",
|
|
@@ -1227,6 +1243,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
1227
1243
|
...(packetContext?.unattributed_paths ? { unattributed_paths: packetContext.unattributed_paths } : {}),
|
|
1228
1244
|
...(packetContext?.unknown_attribution_tasks ? { unknown_attribution_tasks: packetContext.unknown_attribution_tasks } : {}),
|
|
1229
1245
|
...(packetContext?.added_code_paths ? { added_code_paths: packetContext.added_code_paths } : {}),
|
|
1246
|
+
...(packetContext?.structure_ledger ? { structure_ledger: packetContext.structure_ledger } : {}),
|
|
1230
1247
|
...(packetContext?.code_state_check ? { code_state_check: packetContext.code_state_check } : {}),
|
|
1231
1248
|
packet_digest: job.packet_digest,
|
|
1232
1249
|
required_output_kind: "job_report_json",
|
|
@@ -1234,6 +1251,7 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
1234
1251
|
submission_command: `superspec record job-submit --change "${change}" --job "${job.job_id}" --report -`,
|
|
1235
1252
|
submission_argv: jobSubmitArgv(change, job.job_id),
|
|
1236
1253
|
file_fallback: true,
|
|
1254
|
+
report_file_path: jobReportFilePath(change, job.job_id),
|
|
1237
1255
|
output_contract_fields: isCodeReviewer
|
|
1238
1256
|
? [...REVIEW_REPORT_REQUIRED_FIELDS, "reviewer", "review_scope"]
|
|
1239
1257
|
: [
|
|
@@ -1249,12 +1267,12 @@ export function jobsPacket(projectRoot, change, jobId) {
|
|
|
1249
1267
|
(job.review_evidence_digest ? `本工作项对应的执行证据版本为 ${job.review_evidence_digest},` : "") +
|
|
1250
1268
|
(isReviewer ? genericReviewCoverageInstruction(job) + proposalIncrementalReviewInstruction(job) + previousRejectionInstruction(job) : "") +
|
|
1251
1269
|
(requiresReviewer(job.role) ? `必须由独立 ${recommendedAgentForRole(job.role)} 审查角色执行,并在审查者来源字段(reviewer.kind/id)中记录来源,` : "") +
|
|
1252
|
-
`产出 JSON 报告内容并优先通过 --report - 从 stdin
|
|
1270
|
+
`产出 JSON 报告内容并优先通过 --report - 从 stdin 登记;需要落盘时写到 report_file_path,不要写进 openspec/changes 或 .superspec/artifacts 等计划材料目录。${recordInputInstruction(job)}协议字段含义见 packet 顶层“字段说明”,普通对话不要原样复述 JSON。` +
|
|
1253
1271
|
(isCodeReviewer
|
|
1254
1272
|
? `格式骨架:{"role":"code-reviewer","verdict":"pass","review_scope":{"job_id":"${job.job_id}","packet_digest":"${job.packet_digest}","checked_paths":[],"checked_docs":[],"unchecked":[]},"findings":[],"reviewer":{"kind":"subagent","id":"<thread-or-agent-id>"}}。提交前按真实审查结果填写数组;不得从 boundFiles 自动复制 checked_paths。verdict 只能为 pass 或 fail;审查覆盖范围(review_scope)用来说明本次审查覆盖了哪些文件和文档,已检查路径(checked_paths)与未检查项(unchecked)必须合起来覆盖全部绑定文件(boundFiles),unchecked 条目格式为 {"path":"<path>","reason":"<reason>"};pass 不允许仍有未检查的绑定文件。`
|
|
1255
1273
|
+ `报告结论为 fail 时,问题列表(findings)至少包含一个可处理、可追溯的阻塞问题,字段为 {"id":"<stable-id>","blocking":true,"type":"implementation|spec|mixed","claim_kind":"missing_approved|breaks_existing|unjustified_addition","approved_refs":["TEST-001"],"description":"<what>","evidence":"<why>","source_refs":["<path:line>"],"impact":"<impact>","suggested_action":"apply|propose"}。问题类型(type)中 implementation 表示纯代码实现问题,spec 表示方案/需求文档问题,mixed 表示需要使用者判断的混合问题;claim_kind 与 approved_refs 见字段说明。`
|
|
1256
1274
|
+ (packetContext?.task_execution_index
|
|
1257
|
-
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;scope_note 既可能解释必要的范围扩大,也可能说明代码审查修复为何保留原实现,均需结合 Diff、调用链和验证证据独立判断;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动和 added_code_paths
|
|
1275
|
+
? `本工作项带任务执行索引(task_execution_index):按 task 对照其执行依据快照(contract)审查——实现路线对照 design 引用原文、累计 diff 对照 guard 边界、测试断言对照 tests 声明的 scenario;每项的 required_evidence 是 task-start 冻结的证据口径,red_required/green_required 分别说明是否需要 RED/GREEN;fix 非空表示状态机创建的实现修复,source、parent_task_id 和 reason 说明其归属,code_review 来源还需核对 review_finding;scope_note 既可能解释必要的范围扩大,也可能说明代码审查修复为何保留原实现,均需结合 Diff、调用链和验证证据独立判断;changed_paths 是归属线索不是结论(null 表示未知);unattributed_paths 中的无主改动和 added_code_paths 中的新建代码文件,均需判断是否服务已批准行为:放行其中任何计划外文件,都必须写明它服务于哪条已批准锚点、为何无法避免,说不出依据的按 unjustified_addition 收缩;coverage_exemption_refs 解释未绑定 task 的 TEST 豁免。当前 packet 的 boundFiles 是本轮冻结的审查范围;若它来自前一轮审查后的增量,只复核本轮变化及其直接影响链路,不要求重复审查未变化文件,但仍要判断批准行为是否完整闭合。`
|
|
1258
1276
|
: "")
|
|
1259
1277
|
: job.role === "verifier"
|
|
1260
1278
|
? `最小格式:{"role":"verifier","verdict":"pass","findings":[]${hasReviewScope ? `,"review_scope":{"checked_paths":${JSON.stringify(job.boundFiles.map(file => file.path))}}` : ""}}。verdict 只能为 pass 或 fail;核对代码审查记录(code_review_gate):passed 必须能追溯到已接受的代码审查工作项,skipped 必须能证明本次没有代码类改动。核对修复闭环:task_execution_index.fix.source=code_review 时必须核对 review_finding 对应问题是否关闭;source=self_test 时必须核对 parent_task_id、记录的自测原因、本次 attempt 验证和最新代码审查是否共同闭环。方案/混合问题必须有用户决策或后续修复证据。按 task_execution_index 的 required_evidence 核对测试证据:red_required 时需要同一 TEST 的 RED(expected_failure)后 GREEN;green_required 时每个声明 TEST 都需要允许的 GREEN 语义状态;测试运行证据应包含测试 ID(test_id)、命令(command)、工作目录(cwd)、退出码(exit_code)、语义状态(semantic_status)。修复 task 的回归测试运行可用回归覆盖任务列表(covers_task_ids)说明覆盖了哪些已完成任务;缺少任务尝试 ID(attempt_id)的旧证据只能弱引用。` +
|
package/dist/transition.js
CHANGED
|
@@ -6,7 +6,7 @@ import { rebuildSnapshot } from "./sync.js";
|
|
|
6
6
|
import { requiredJobActions } from "./job_action.js";
|
|
7
7
|
import { assertCommitPayloadExtension, isFreshReviewVerifier, isReviewReadyVerifier, latestReviewHistoryForGateRole, readReviewPolicyFromEvents, reviewBoundFiles, reviewEvidenceDigest, reviewPolicyForRisk, REVIEW_DOC_PATHS, } from "./review.js";
|
|
8
8
|
import { REVIEW_CODE_REVIEW_GATE_ID, REVIEW_FINAL_VERIFIER_GATE, REVIEW_FINAL_VERIFIER_GATE_ID, reviewScopeForGateRole, } from "./review_job_gates.js";
|
|
9
|
-
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, } from "./code_review.js";
|
|
9
|
+
import { codeReviewBoundFiles, codeReviewDecisionScope, codeReviewJobStaleReason, codeReviewPacketContext, codeReviewPacketDigest, collectCodeReviewGateFacts, computeCodeStateCheck, currentCodeReviewWorkingPaths, dismissedCodeReviewSummary, effectiveCoverageExemptionRefsFromEvents, latestCodeReviewGateEvidence, latestCodeReviewDecision, latestCodeReviewFailedStatus, missingCoverageExemptionTestIds, requiresFinalVerifierForCurrentReview, scanCodeChangesForReview, taskExecutionIndexForReview, codeReviewFindingNeedsUserDecision, isReviewFixCapReached, } from "./code_review.js";
|
|
10
10
|
import { taskEvidenceReadiness } from "./task_evidence.js";
|
|
11
11
|
import { adoptedContractForTask, findTaskInLines, isFixTaskId, parseTasksMd, parseTestContractEntries, } from "./format.js";
|
|
12
12
|
import { isCodeReviewClaimKind, reviewFixReason } from "./approved_ref.js";
|
|
@@ -1136,8 +1136,8 @@ export function reopen(projectRoot, change, changeRoot, to, reason, opts = {}) {
|
|
|
1136
1136
|
const found = findReviewFailedFinding(events, ref);
|
|
1137
1137
|
if (!found)
|
|
1138
1138
|
return { skip: true, message: `找不到有效的代码审查问题 ${opts.reviewFix}` };
|
|
1139
|
-
const type = found.finding.type;
|
|
1140
|
-
if (type
|
|
1139
|
+
const type = typeof found.finding.type === "string" ? found.finding.type : undefined;
|
|
1140
|
+
if (codeReviewFindingNeedsUserDecision(type, isReviewFixCapReached(projectRoot, events))) {
|
|
1141
1141
|
const scope = codeReviewDecisionScope(ref.jobId, ref.findingId);
|
|
1142
1142
|
const decision = latestCodeReviewDecision(events, scope);
|
|
1143
1143
|
if (decision?.answer !== "reopen_apply")
|
package/dist/types.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export interface JobPacketContext {
|
|
|
133
133
|
unattributed_paths?: string[];
|
|
134
134
|
unknown_attribution_tasks?: string[];
|
|
135
135
|
added_code_paths?: string[];
|
|
136
|
+
structure_ledger?: StructureChangeLedger;
|
|
136
137
|
code_state_check?: CodeStateCheck;
|
|
137
138
|
}
|
|
138
139
|
export interface JobPacket {
|
|
@@ -153,6 +154,7 @@ export interface JobPacket {
|
|
|
153
154
|
unattributed_paths?: string[];
|
|
154
155
|
unknown_attribution_tasks?: string[];
|
|
155
156
|
added_code_paths?: string[];
|
|
157
|
+
structure_ledger?: StructureChangeLedger;
|
|
156
158
|
code_state_check?: CodeStateCheck;
|
|
157
159
|
packet_digest: string;
|
|
158
160
|
required_output_kind: string;
|
|
@@ -160,6 +162,8 @@ export interface JobPacket {
|
|
|
160
162
|
submission_command?: string;
|
|
161
163
|
submission_argv?: string[];
|
|
162
164
|
file_fallback?: boolean;
|
|
165
|
+
/** 需要落盘时的报告文件位置(项目相对路径),位于工作流记录目录而非计划材料目录。 */
|
|
166
|
+
report_file_path?: string;
|
|
163
167
|
output_contract_fields?: string[];
|
|
164
168
|
output_contract_optional_fields?: string[];
|
|
165
169
|
字段说明?: Record<string, string>;
|
|
@@ -198,9 +202,23 @@ export interface PlanningValidationProfile {
|
|
|
198
202
|
version: 2;
|
|
199
203
|
openspec: OpenSpecValidationProfile;
|
|
200
204
|
design?: {
|
|
201
|
-
schema_version: 1;
|
|
205
|
+
schema_version: 1 | 2;
|
|
202
206
|
};
|
|
203
207
|
}
|
|
208
|
+
export interface StructureChangeEntry {
|
|
209
|
+
id: string;
|
|
210
|
+
category: string;
|
|
211
|
+
change: string;
|
|
212
|
+
basis: string;
|
|
213
|
+
decision: string;
|
|
214
|
+
}
|
|
215
|
+
export interface StructureChangeLedger {
|
|
216
|
+
present: boolean;
|
|
217
|
+
none: boolean;
|
|
218
|
+
entries: StructureChangeEntry[];
|
|
219
|
+
/** 标题存在但表格无法解析时的原因;有值时 entries 为空。 */
|
|
220
|
+
format_error?: string;
|
|
221
|
+
}
|
|
204
222
|
export interface TransitionCommitPayload {
|
|
205
223
|
transition: string;
|
|
206
224
|
from_state: State;
|
|
@@ -3,6 +3,16 @@ import type { Event, State } from "./types.ts";
|
|
|
3
3
|
export declare const WORKFLOW_CONFIG_PATH = ".superspec/config.json";
|
|
4
4
|
/** 项目未声明 workflow.mode 时采用的默认档位。 */
|
|
5
5
|
export declare const DEFAULT_WORKFLOW_RISK: ReviewRisk;
|
|
6
|
+
export declare const DEFAULT_WORKFLOW_BUDGET: {
|
|
7
|
+
readonly tasks: 10;
|
|
8
|
+
readonly tests: 20;
|
|
9
|
+
readonly review_fix_rounds: 2;
|
|
10
|
+
};
|
|
11
|
+
export type WorkflowBudget = {
|
|
12
|
+
tasks: number | null;
|
|
13
|
+
tests: number | null;
|
|
14
|
+
review_fix_rounds: number | null;
|
|
15
|
+
};
|
|
6
16
|
export declare const WORKFLOW_HOSTS: readonly ["codex", "omp"];
|
|
7
17
|
export type WorkflowHost = (typeof WORKFLOW_HOSTS)[number];
|
|
8
18
|
/** 未声明 hosts 的旧项目按 Codex 入口处理。 */
|
|
@@ -10,6 +20,11 @@ export declare const DEFAULT_WORKFLOW_HOSTS: WorkflowHost[];
|
|
|
10
20
|
export declare class WorkflowConfigError extends Error {
|
|
11
21
|
constructor(message: string);
|
|
12
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 读取计划规模与 review-fix 上限;minimal 档整体忽略,budget 为 null 整体关闭,单项 null 关闭该项检查。
|
|
25
|
+
* 注意 0 不等于关闭:tasks/tests 为 0 表示任何任务/TEST 都超预算,review_fix_rounds 为 0 表示不允许自动修复。
|
|
26
|
+
*/
|
|
27
|
+
export declare function workflowBudgetForRisk(projectRoot: string, risk: ReviewRisk): WorkflowBudget | null;
|
|
13
28
|
export declare function normalizeWorkflowHosts(values: readonly string[]): WorkflowHost[];
|
|
14
29
|
export declare function parseWorkflowHostsFlag(raw: string): WorkflowHost[];
|
|
15
30
|
/** 读取项目已选宿主。缺少配置或缺少 workflow.hosts 时默认 Codex。 */
|
package/dist/workflow_config.js
CHANGED
|
@@ -5,6 +5,11 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
export const WORKFLOW_CONFIG_PATH = ".superspec/config.json";
|
|
6
6
|
/** 项目未声明 workflow.mode 时采用的默认档位。 */
|
|
7
7
|
export const DEFAULT_WORKFLOW_RISK = "normal";
|
|
8
|
+
export const DEFAULT_WORKFLOW_BUDGET = {
|
|
9
|
+
tasks: 10,
|
|
10
|
+
tests: 20,
|
|
11
|
+
review_fix_rounds: 2,
|
|
12
|
+
};
|
|
8
13
|
export const WORKFLOW_HOSTS = ["codex", "omp"];
|
|
9
14
|
/** 未声明 hosts 的旧项目按 Codex 入口处理。 */
|
|
10
15
|
export const DEFAULT_WORKFLOW_HOSTS = ["codex"];
|
|
@@ -17,6 +22,48 @@ export class WorkflowConfigError extends Error {
|
|
|
17
22
|
function isReviewRisk(value) {
|
|
18
23
|
return value === "minimal" || value === "normal" || value === "strict";
|
|
19
24
|
}
|
|
25
|
+
function parseWorkflowBudgetValue(value, field) {
|
|
26
|
+
if (value === null)
|
|
27
|
+
return null;
|
|
28
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
29
|
+
throw new WorkflowConfigError(`${WORKFLOW_CONFIG_PATH} 的 workflow.budget.${field} 必须是非负整数或 null`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function workflowBudgetFromObject(budget) {
|
|
34
|
+
if (!budget)
|
|
35
|
+
return { ...DEFAULT_WORKFLOW_BUDGET };
|
|
36
|
+
const tasks = budget.tasks === undefined
|
|
37
|
+
? DEFAULT_WORKFLOW_BUDGET.tasks
|
|
38
|
+
: parseWorkflowBudgetValue(budget.tasks, "tasks");
|
|
39
|
+
const tests = budget.tests === undefined
|
|
40
|
+
? DEFAULT_WORKFLOW_BUDGET.tests
|
|
41
|
+
: parseWorkflowBudgetValue(budget.tests, "tests");
|
|
42
|
+
const review_fix_rounds = budget.review_fix_rounds === undefined
|
|
43
|
+
? DEFAULT_WORKFLOW_BUDGET.review_fix_rounds
|
|
44
|
+
: parseWorkflowBudgetValue(budget.review_fix_rounds, "review_fix_rounds");
|
|
45
|
+
return { tasks, tests, review_fix_rounds };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 读取计划规模与 review-fix 上限;minimal 档整体忽略,budget 为 null 整体关闭,单项 null 关闭该项检查。
|
|
49
|
+
* 注意 0 不等于关闭:tasks/tests 为 0 表示任何任务/TEST 都超预算,review_fix_rounds 为 0 表示不允许自动修复。
|
|
50
|
+
*/
|
|
51
|
+
export function workflowBudgetForRisk(projectRoot, risk) {
|
|
52
|
+
if (risk === "minimal")
|
|
53
|
+
return null;
|
|
54
|
+
const workflow = workflowObject(readWorkflowConfigObject(projectRoot));
|
|
55
|
+
if (!workflow)
|
|
56
|
+
return { ...DEFAULT_WORKFLOW_BUDGET };
|
|
57
|
+
if (workflow.budget === undefined)
|
|
58
|
+
return { ...DEFAULT_WORKFLOW_BUDGET };
|
|
59
|
+
// budget: null 表示整体关闭预算与修复上限。
|
|
60
|
+
if (workflow.budget === null)
|
|
61
|
+
return { tasks: null, tests: null, review_fix_rounds: null };
|
|
62
|
+
if (typeof workflow.budget !== "object" || Array.isArray(workflow.budget)) {
|
|
63
|
+
throw new WorkflowConfigError(`${WORKFLOW_CONFIG_PATH} 的 workflow.budget 必须是 object 或 null`);
|
|
64
|
+
}
|
|
65
|
+
return workflowBudgetFromObject(workflow.budget);
|
|
66
|
+
}
|
|
20
67
|
function isWorkflowHost(value) {
|
|
21
68
|
return value === "codex" || value === "omp";
|
|
22
69
|
}
|
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ argument-hint: "本次架构审查说明"
|
|
|
24
24
|
- 复用既有机制时,核对接入点、本次差异和保持语义;只审查本次接入是否破坏现有契约,不要求重建或重新证明底层基础设施。
|
|
25
25
|
- 参考实现证明的是可复用能力和候选机制,不自动决定本次的接口、资源、数据模型或模块形态。新增 Controller、API、实体、表、公共类型、独立模块或跨仓库改动时,检查其是否承担不可由现有边界表达的独立责任;不要规定数量,但应挑战无必要依据的平行结构和机械复制。
|
|
26
26
|
- 只有本次确实改变的兼容、并发、恢复、数据语义或发布顺序才需要明确设计;未改变的既有风险和理论故障不是 blocker。
|
|
27
|
+
- 结构变更清单是本次已批准结构的边界。方案正文出现而清单未列的表、接口、开关、迁移或签名变化,或清单条目缺少 Requirement / TEST 依据,是 blocker;处理方向是删除或补依据。
|
|
27
28
|
- 设计取舍受用户决定和明确非目标约束。报告无法满足的结果或事实冲突,不把未经采纳的架构方案写成 required fix。
|
|
28
29
|
|
|
29
30
|
### 方案可行性
|