svharness 0.15.11 → 0.15.13
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 +106 -2
- package/auto/auto-harness-detail-design.md +136 -4
- package/auto/skills/svharness/SKILL.md +44 -1
- package/auto/templates/auto-pack-req.config.example.yaml +29 -0
- package/auto/templates/auto.config.example.yaml +1 -1
- package/auto/templates/phase-prompts/S40-adversarial-review.md +31 -0
- package/auto/templates/phase-prompts/S40-review.md +22 -0
- package/auto/templates/phase-prompts/S40-sample-trace.md +26 -0
- package/auto/templates/requirements-review-rubric.yaml +79 -0
- package/dist/commands/auto-pack-req.js +70 -0
- package/dist/commands/auto.js +16 -1
- package/dist/commands/convert.js +2 -0
- package/dist/commands/shell-integration.js +29 -56
- package/dist/config/merge-options.js +2 -0
- package/dist/config/normalize.js +7 -0
- package/dist/constants/convert.js +5 -0
- package/dist/core/markitdown-client.js +7 -2
- package/dist/index.js +77 -5
- package/dist/lib/acp-agent-cli-resolver.js +193 -0
- package/dist/lib/acp-client.js +4 -2
- package/dist/lib/agent-launcher.js +9 -4
- package/dist/lib/auto-assets.js +1 -0
- package/dist/lib/auto-optimize.js +23 -3
- package/dist/lib/auto-orchestrator.js +53 -20
- package/dist/lib/auto-pack-req-context.js +88 -0
- package/dist/lib/auto-pack-req-finalize.js +28 -0
- package/dist/lib/auto-req-pack-review.js +161 -0
- package/dist/lib/auto-sample-trace.js +144 -0
- package/dist/lib/auto-state.js +124 -15
- package/dist/lib/gate-checker.js +20 -10
- package/dist/lib/phase-runner.js +13 -2
- package/dist/lib/ps-codechat-alias.js +29 -16
- package/dist/lib/review-parser.js +14 -0
- package/dist/lib/score-aggregator.js +21 -0
- package/dist/lib/win-registry.js +1 -1
- package/dist/utils/validate-args.js +2 -1
- package/package.json +3 -2
- package/templates/codechat/Start-CodeChat.ps1 +176 -135
- package/templates/codechat/bash.exe.stackdump +29 -0
- package/templates/codechat/docs/project-structure.md +59 -0
- package/templates/svharness.config.example.yaml +2 -0
- package/dist/lib/launch-script-path.js +0 -24
- package/templates/codechat/.claude/.env +0 -59
package/dist/lib/auto-assets.js
CHANGED
|
@@ -14,6 +14,7 @@ function resolveAutoAssetPaths(promptDirOverride) {
|
|
|
14
14
|
autoRoot,
|
|
15
15
|
promptDir,
|
|
16
16
|
reviewRubric: node_path_1.default.join(autoRoot, 'templates', 'review-rubric.yaml'),
|
|
17
|
+
requirementsReviewRubric: node_path_1.default.join(autoRoot, 'templates', 'requirements-review-rubric.yaml'),
|
|
17
18
|
skillDir: node_path_1.default.join(autoRoot, 'skills', 'svharness'),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
@@ -11,6 +11,18 @@ const logger_1 = require("../utils/logger");
|
|
|
11
11
|
const phase_prompt_loader_1 = require("./phase-prompt-loader");
|
|
12
12
|
const phase_deps_1 = require("./phase-deps");
|
|
13
13
|
const auto_state_1 = require("./auto-state");
|
|
14
|
+
const SCOPE_ALLOWED_PHASES = {
|
|
15
|
+
full: new Set([
|
|
16
|
+
'S10_wiki',
|
|
17
|
+
'S40_extract_requirements',
|
|
18
|
+
'S50_generate_specs',
|
|
19
|
+
'S60_process_references',
|
|
20
|
+
'S65_customize_agent_env',
|
|
21
|
+
'S70_runtime_assets',
|
|
22
|
+
'S80_seed_memory',
|
|
23
|
+
]),
|
|
24
|
+
requirements: new Set(['S40_extract_requirements']),
|
|
25
|
+
};
|
|
14
26
|
async function runOptimizePlan(ctx, acp, cost, round, review) {
|
|
15
27
|
if (!ctx.harnessRoot)
|
|
16
28
|
throw new Error('缺少 harnessRoot,无法生成优化计划');
|
|
@@ -62,22 +74,27 @@ async function runOptimizeImplement(ctx, acp, cost, round, review) {
|
|
|
62
74
|
logger_1.logger.warn(`优化实施 ACP 调用失败:${result.error ?? 'unknown error'},继续状态重置`);
|
|
63
75
|
}
|
|
64
76
|
const state = await (0, auto_state_1.readBuildState)(ctx.harnessRoot);
|
|
65
|
-
const
|
|
77
|
+
const allowed = SCOPE_ALLOWED_PHASES[ctx.scope];
|
|
78
|
+
const phases = (await collectPlanPhases(ctx.harnessRoot, round, review)).filter((phase) => allowed.has(phase));
|
|
66
79
|
for (const phase of phases) {
|
|
67
80
|
if (!state.phases[phase])
|
|
68
81
|
continue;
|
|
69
82
|
state.phases[phase].status = 'PENDING';
|
|
70
83
|
delete state.phases[phase].outdated_reason;
|
|
71
|
-
(
|
|
84
|
+
if (ctx.scope === 'full') {
|
|
85
|
+
(0, phase_deps_1.invalidateDownstream)(phase, state);
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
88
|
for (const phase of (0, phase_deps_1.collectOutdatedKeys)(state)) {
|
|
89
|
+
if (!allowed.has(phase))
|
|
90
|
+
continue;
|
|
74
91
|
state.phases[phase].status = 'PENDING';
|
|
75
92
|
delete state.phases[phase].outdated_reason;
|
|
76
93
|
}
|
|
77
94
|
await (0, auto_state_1.writeBuildState)(ctx.harnessRoot, state);
|
|
78
95
|
}
|
|
79
96
|
async function collectPlanPhases(harnessRoot, round, review) {
|
|
80
|
-
const phases = new Set(review.gaps.map((gap) => (0, phase_deps_1.normalizeGapPhase)(gap.phase)));
|
|
97
|
+
const phases = new Set(review.gaps.map((gap) => (0, phase_deps_1.normalizeGapPhase)(gap.phase)).filter((p) => p.includes('_')));
|
|
81
98
|
const planPath = node_path_1.default.join(harnessRoot, 'auto-review', `第${round}轮修改-plan.md`);
|
|
82
99
|
if (await fs_extra_1.default.pathExists(planPath)) {
|
|
83
100
|
const body = await fs_extra_1.default.readFile(planPath, 'utf8');
|
|
@@ -85,5 +102,8 @@ async function collectPlanPhases(harnessRoot, round, review) {
|
|
|
85
102
|
phases.add((0, phase_deps_1.normalizeGapPhase)(match[0]));
|
|
86
103
|
}
|
|
87
104
|
}
|
|
105
|
+
if (phases.size === 0) {
|
|
106
|
+
phases.add('S40_extract_requirements');
|
|
107
|
+
}
|
|
88
108
|
return [...phases];
|
|
89
109
|
}
|
|
@@ -52,9 +52,9 @@ class AutoOrchestrator {
|
|
|
52
52
|
phaseRunner;
|
|
53
53
|
constructor(ctx) {
|
|
54
54
|
this.ctx = ctx;
|
|
55
|
-
|
|
55
|
+
const agentCommand = ctx.acpAgentCli?.command ?? 'claude';
|
|
56
|
+
this.acp = new acp_client_1.AcpClient(ctx.acpMode, agentCommand);
|
|
56
57
|
this.cost = new cost_tracker_1.CostTracker(ctx.maxTotalCalls, ctx.budgetUsd, ctx.costAlertThreshold);
|
|
57
|
-
// PhaseRunner 在 run() 中 ctx 就绪后延迟创建,避免初始 ctx.harnessRoot=null 时 PhaseRunner 持有陈旧引用
|
|
58
58
|
}
|
|
59
59
|
async run() {
|
|
60
60
|
if (!this.ctx.harnessExists) {
|
|
@@ -63,7 +63,6 @@ class AutoOrchestrator {
|
|
|
63
63
|
if (!this.ctx.harnessRoot) {
|
|
64
64
|
throw new Error('未解析到 harnessRoot,无法继续 auto');
|
|
65
65
|
}
|
|
66
|
-
// PhaseRunner 延迟创建:此时 ctx.harnessRoot 已就绪(可能在 runMaterialsPrepAndBuild 后才赋值)
|
|
67
66
|
this.phaseRunner = new phase_runner_1.PhaseRunner(this.ctx, this.acp, this.cost);
|
|
68
67
|
event_bus_1.dashboardEventBus.emitBuildStarted(phase_runner_1.PHASE_DEFS.length);
|
|
69
68
|
await this.ensureReviewRubric();
|
|
@@ -76,36 +75,53 @@ class AutoOrchestrator {
|
|
|
76
75
|
event_bus_1.dashboardEventBus.emitBuildFailed(undefined);
|
|
77
76
|
throw new Error(`构建阶段失败:${msg};auto 构建终止,请在修复后重新执行`);
|
|
78
77
|
}
|
|
79
|
-
if (this.
|
|
80
|
-
logger_1.logger.info(
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
if (this.shouldSkipReviewLoop()) {
|
|
79
|
+
logger_1.logger.info(this.ctx.scope === 'requirements'
|
|
80
|
+
? '[auto_pack_req] 构建阶段完成,进入三维度评审'
|
|
81
|
+
: `已到达 --max-phase ${this.ctx.maxPhase},跳过评审优化`);
|
|
82
|
+
if (this.ctx.scope !== 'requirements') {
|
|
83
|
+
event_bus_1.dashboardEventBus.emitBuildCompleted(undefined);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
83
86
|
}
|
|
87
|
+
await this.runReviewOptimizeLoop();
|
|
88
|
+
}
|
|
89
|
+
shouldSkipReviewLoop() {
|
|
90
|
+
if (this.ctx.scope === 'requirements')
|
|
91
|
+
return false;
|
|
92
|
+
return !!(this.ctx.maxPhase && phaseNumber(this.ctx.maxPhase) < 85);
|
|
93
|
+
}
|
|
94
|
+
async runReviewOptimizeLoop() {
|
|
84
95
|
let previousScore = 0;
|
|
85
96
|
let finalReview;
|
|
86
97
|
for (let round = 1; round <= this.ctx.maxOptimizeRounds; round++) {
|
|
87
98
|
event_bus_1.dashboardEventBus.emitReviewStarted(round);
|
|
88
99
|
let review;
|
|
89
100
|
try {
|
|
90
|
-
review = await this.
|
|
101
|
+
review = await this.runReview(round);
|
|
91
102
|
}
|
|
92
103
|
catch (err) {
|
|
93
104
|
const msg = err.message;
|
|
94
105
|
if (msg.includes('预算') || msg.includes('调用次数') || msg.includes('ACP 调用')) {
|
|
95
106
|
logger_1.logger.error(`[auto] 第 ${round} 轮评审因资源限制终止:${msg}`);
|
|
96
107
|
event_bus_1.dashboardEventBus.emitBuildFailed(undefined);
|
|
97
|
-
throw err;
|
|
108
|
+
throw err;
|
|
98
109
|
}
|
|
99
110
|
logger_1.logger.warn(`[auto] 第 ${round} 轮评审异常:${msg},继续下一轮`);
|
|
100
111
|
continue;
|
|
101
112
|
}
|
|
102
113
|
event_bus_1.dashboardEventBus.emitReviewCompleted(round, review.composite_score, review.critical_count);
|
|
103
114
|
event_bus_1.dashboardEventBus.emitScoreUpdated(review.composite_score, round);
|
|
104
|
-
|
|
105
|
-
review.
|
|
106
|
-
|
|
115
|
+
const passed = this.ctx.scope === 'requirements'
|
|
116
|
+
? review.composite_score > this.ctx.targetScore &&
|
|
117
|
+
review.gate_pass &&
|
|
118
|
+
review.critical_count === 0
|
|
119
|
+
: review.composite_score >= this.ctx.targetScore &&
|
|
120
|
+
review.gate_pass &&
|
|
121
|
+
review.critical_count === 0;
|
|
122
|
+
if (passed) {
|
|
107
123
|
finalReview = review;
|
|
108
|
-
await this.
|
|
124
|
+
await this.runCompletion(review);
|
|
109
125
|
break;
|
|
110
126
|
}
|
|
111
127
|
if (round >= this.ctx.maxOptimizeRounds) {
|
|
@@ -127,7 +143,7 @@ class AutoOrchestrator {
|
|
|
127
143
|
const msg = err.message;
|
|
128
144
|
if (msg.includes('预算') || msg.includes('次数') || msg.includes('ACP 调用')) {
|
|
129
145
|
logger_1.logger.warn(`[auto] 第 ${round} 轮优化因资源限制终止:${msg}`);
|
|
130
|
-
break;
|
|
146
|
+
break;
|
|
131
147
|
}
|
|
132
148
|
logger_1.logger.warn(`第 ${round} 轮优化执行失败:${msg},状态重置已完成,下一轮可继续`);
|
|
133
149
|
}
|
|
@@ -144,14 +160,26 @@ class AutoOrchestrator {
|
|
|
144
160
|
if (!this.ctx.harnessRoot)
|
|
145
161
|
return;
|
|
146
162
|
const assets = (0, auto_assets_1.resolveAutoAssetPaths)();
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
const autoDir = node_path_1.default.join(this.ctx.harnessRoot, 'auto');
|
|
164
|
+
await fs_extra_1.default.ensureDir(autoDir);
|
|
165
|
+
const rubricDest = node_path_1.default.join(autoDir, 'review-rubric.yaml');
|
|
166
|
+
if (!(await fs_extra_1.default.pathExists(rubricDest))) {
|
|
167
|
+
await fs_extra_1.default.copyFile(assets.reviewRubric, rubricDest);
|
|
151
168
|
logger_1.logger.success('已注入 auto/review-rubric.yaml');
|
|
152
169
|
}
|
|
170
|
+
if (this.ctx.scope === 'requirements') {
|
|
171
|
+
const reqRubricDest = node_path_1.default.join(autoDir, 'requirements-review-rubric.yaml');
|
|
172
|
+
if (!(await fs_extra_1.default.pathExists(reqRubricDest))) {
|
|
173
|
+
await fs_extra_1.default.copyFile(assets.requirementsReviewRubric, reqRubricDest);
|
|
174
|
+
logger_1.logger.success('已注入 auto/requirements-review-rubric.yaml');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
153
177
|
}
|
|
154
|
-
async
|
|
178
|
+
async runReview(round) {
|
|
179
|
+
if (this.ctx.scope === 'requirements') {
|
|
180
|
+
const { runReqPackReview } = await Promise.resolve().then(() => __importStar(require('./auto-req-pack-review')));
|
|
181
|
+
return runReqPackReview(this.ctx, this.acp, this.cost, round);
|
|
182
|
+
}
|
|
155
183
|
const { runHarnessReview } = await Promise.resolve().then(() => __importStar(require('./auto-review')));
|
|
156
184
|
return runHarnessReview(this.ctx, this.acp, this.cost, round);
|
|
157
185
|
}
|
|
@@ -164,7 +192,12 @@ class AutoOrchestrator {
|
|
|
164
192
|
await runOptimizeImplement(this.ctx, this.acp, this.cost, round, review);
|
|
165
193
|
await this.phaseRunner.runBuildPhases({ onlyPendingOrOutdated: true });
|
|
166
194
|
}
|
|
167
|
-
async
|
|
195
|
+
async runCompletion(review) {
|
|
196
|
+
if (this.ctx.scope === 'requirements') {
|
|
197
|
+
const { finalizeReqPack } = await Promise.resolve().then(() => __importStar(require('./auto-pack-req-finalize')));
|
|
198
|
+
await finalizeReqPack(this.ctx, review);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
168
201
|
const { finalizeAutoHarness } = await Promise.resolve().then(() => __importStar(require('./auto-finalize')));
|
|
169
202
|
await finalizeAutoHarness(this.ctx, review);
|
|
170
203
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.looksLikeMaterialsPackRoot = looksLikeMaterialsPackRoot;
|
|
7
|
+
exports.looksLikeRequirementsFolder = looksLikeRequirementsFolder;
|
|
8
|
+
exports.resolveAutoPackReqWorkContext = resolveAutoPackReqWorkContext;
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const harness_resolver_1 = require("./harness-resolver");
|
|
12
|
+
const DOC_FILE_RE = /\.(md|docx|doc|pdf|xlsx|xls|pptx|ppt|txt|csv)$/i;
|
|
13
|
+
const MATERIALS_PACK_SUBDIR_RE = /^(requirements?|baseline|references?|extra[-_]?skills?|需求|接口|协议|skill)/i;
|
|
14
|
+
/** 当前目录是否像资料包根(含 requirements/、baseline/ 等子目录) */
|
|
15
|
+
async function looksLikeMaterialsPackRoot(dir) {
|
|
16
|
+
if (!(await fs_extra_1.default.pathExists(dir)))
|
|
17
|
+
return false;
|
|
18
|
+
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (!entry.isDirectory())
|
|
21
|
+
continue;
|
|
22
|
+
if (MATERIALS_PACK_SUBDIR_RE.test(entry.name))
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** 当前目录是否像 requirements 资料夹(含文档文件,且不像资料包根) */
|
|
28
|
+
async function looksLikeRequirementsFolder(dir) {
|
|
29
|
+
if (!(await fs_extra_1.default.pathExists(dir)))
|
|
30
|
+
return false;
|
|
31
|
+
if (await (0, harness_resolver_1.isHarnessRoot)(dir))
|
|
32
|
+
return false;
|
|
33
|
+
if (await looksLikeMaterialsPackRoot(dir))
|
|
34
|
+
return false;
|
|
35
|
+
const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
|
|
36
|
+
const topFiles = entries.filter((e) => e.isFile());
|
|
37
|
+
if (topFiles.some((f) => DOC_FILE_RE.test(f.name)))
|
|
38
|
+
return true;
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
41
|
+
continue;
|
|
42
|
+
const sub = node_path_1.default.join(dir, entry.name);
|
|
43
|
+
const subEntries = await fs_extra_1.default.readdir(sub, { withFileTypes: true });
|
|
44
|
+
if (subEntries.some((f) => f.isFile() && DOC_FILE_RE.test(f.name)))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 解析 auto_pack_req 工作上下文。
|
|
51
|
+
* 无显式 workdir 时:若当前目录本身是 requirements 资料夹,则以其父目录为 workDir 并固定 requirements 输入。
|
|
52
|
+
*/
|
|
53
|
+
async function resolveAutoPackReqWorkContext(positionalWorkdir, configWorkDir, requirementsOverride) {
|
|
54
|
+
const explicit = positionalWorkdir ?? configWorkDir;
|
|
55
|
+
if (explicit) {
|
|
56
|
+
const workDir = node_path_1.default.resolve(explicit);
|
|
57
|
+
const harnessRoot = await (0, harness_resolver_1.resolveHarnessRoot)(workDir);
|
|
58
|
+
return {
|
|
59
|
+
workDir,
|
|
60
|
+
harnessRoot,
|
|
61
|
+
requirementsPath: requirementsOverride
|
|
62
|
+
? node_path_1.default.isAbsolute(requirementsOverride)
|
|
63
|
+
? requirementsOverride
|
|
64
|
+
: node_path_1.default.resolve(workDir, requirementsOverride)
|
|
65
|
+
: undefined,
|
|
66
|
+
mode: harnessRoot ? 'harness-rerun' : 'materials-pack',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const harnessFromCwd = await (0, harness_resolver_1.resolveHarnessRoot)(cwd);
|
|
71
|
+
if (harnessFromCwd) {
|
|
72
|
+
return { workDir: cwd, harnessRoot: harnessFromCwd, mode: 'harness-rerun' };
|
|
73
|
+
}
|
|
74
|
+
if (await looksLikeMaterialsPackRoot(cwd)) {
|
|
75
|
+
return { workDir: cwd, harnessRoot: null, mode: 'materials-pack' };
|
|
76
|
+
}
|
|
77
|
+
if (await looksLikeRequirementsFolder(cwd)) {
|
|
78
|
+
const parent = node_path_1.default.dirname(cwd);
|
|
79
|
+
const harnessFromParent = await (0, harness_resolver_1.resolveHarnessRoot)(parent);
|
|
80
|
+
return {
|
|
81
|
+
workDir: parent,
|
|
82
|
+
harnessRoot: harnessFromParent,
|
|
83
|
+
requirementsPath: node_path_1.default.resolve(cwd),
|
|
84
|
+
mode: harnessFromParent ? 'harness-rerun' : 'requirements-cwd',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { workDir: cwd, harnessRoot: null, mode: 'materials-pack' };
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.finalizeReqPack = finalizeReqPack;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
async function finalizeReqPack(ctx, review) {
|
|
11
|
+
if (!ctx.harnessRoot)
|
|
12
|
+
throw new Error('缺少 harnessRoot,无法完成需求包流程');
|
|
13
|
+
const outPath = node_path_1.default.join(ctx.harnessRoot, 'auto-review', 'pack-req-complete.json');
|
|
14
|
+
await fs_extra_1.default.ensureDir(node_path_1.default.dirname(outPath));
|
|
15
|
+
await fs_extra_1.default.writeFile(outPath, JSON.stringify({
|
|
16
|
+
completed_at: new Date().toISOString(),
|
|
17
|
+
scope: 'requirements',
|
|
18
|
+
composite_score: review.composite_score,
|
|
19
|
+
target_score: ctx.targetScore,
|
|
20
|
+
gate_pass: review.gate_pass,
|
|
21
|
+
critical_count: review.critical_count,
|
|
22
|
+
mechanical_score: review.mechanical_score,
|
|
23
|
+
adversarial_score: review.review_score,
|
|
24
|
+
report_path: node_path_1.default.relative(ctx.harnessRoot, review.report_path).replace(/\\/g, '/'),
|
|
25
|
+
note: 'auto_pack_req 需求条目化闭环完成(未执行 S90 封存)',
|
|
26
|
+
}, null, 2), 'utf8');
|
|
27
|
+
logger_1.logger.success(`auto_pack_req 完成:综合分 ${review.composite_score.toFixed(1)} > ${ctx.targetScore},产物见 auto-review/`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runReqPackReview = runReqPackReview;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const phase_prompt_loader_1 = require("./phase-prompt-loader");
|
|
11
|
+
const gate_checker_1 = require("./gate-checker");
|
|
12
|
+
const score_aggregator_1 = require("./score-aggregator");
|
|
13
|
+
const auto_sample_trace_1 = require("./auto-sample-trace");
|
|
14
|
+
const review_parser_1 = require("./review-parser");
|
|
15
|
+
async function runReqPackReview(ctx, acp, cost, round) {
|
|
16
|
+
if (!ctx.harnessRoot)
|
|
17
|
+
throw new Error('缺少 harnessRoot,无法评审');
|
|
18
|
+
await fs_extra_1.default.ensureDir(node_path_1.default.join(ctx.harnessRoot, 'auto-review'));
|
|
19
|
+
const gates = await (0, gate_checker_1.runMechanicalGates)(ctx.harnessRoot, {
|
|
20
|
+
scope: 'requirements',
|
|
21
|
+
verbose: ctx.verbose,
|
|
22
|
+
});
|
|
23
|
+
await (0, auto_sample_trace_1.writeSampleFragmentsFile)(ctx.harnessRoot, ctx.sampleCount);
|
|
24
|
+
const selfReviewPath = node_path_1.default.join(ctx.harnessRoot, 'auto-review', `第${round}轮需求自评报告.md`);
|
|
25
|
+
const adversarialPath = node_path_1.default.join(ctx.harnessRoot, 'auto-review', `第${round}轮对抗评审.md`);
|
|
26
|
+
const samplePath = node_path_1.default.join(ctx.harnessRoot, 'auto-review', `第${round}轮抽样验证报告.md`);
|
|
27
|
+
if (ctx.dryRun) {
|
|
28
|
+
for (const template of ['S40-review.md', 'S40-adversarial-review.md', 'S40-sample-trace.md']) {
|
|
29
|
+
const prompt = await (0, phase_prompt_loader_1.loadPhasePrompt)(template, buildPromptVars(ctx, round, gates), ctx.promptDir);
|
|
30
|
+
logger_1.logger.plain(`\n--- ${template} ---\n${prompt}\n`);
|
|
31
|
+
}
|
|
32
|
+
return dryRunReqPackReview(round, gates);
|
|
33
|
+
}
|
|
34
|
+
await runAcpReviewPhase(ctx, acp, cost, round, 'S40-review.md', 'req_self_review', '<AUTO_SIGNAL>REQ_REVIEW_DONE</AUTO_SIGNAL>');
|
|
35
|
+
await runAcpReviewPhase(ctx, acp, cost, round, 'S40-adversarial-review.md', 'req_adversarial_review', '<AUTO_SIGNAL>ADV_REVIEW_DONE</AUTO_SIGNAL>');
|
|
36
|
+
await runAcpReviewPhase(ctx, acp, cost, round, 'S40-sample-trace.md', 'req_sample_trace', '<AUTO_SIGNAL>SAMPLE_TRACE_DONE</AUTO_SIGNAL>');
|
|
37
|
+
const selfRaw = await (0, review_parser_1.parseReportFrontmatter)(selfReviewPath);
|
|
38
|
+
const advRaw = await (0, review_parser_1.parseReportFrontmatter)(adversarialPath);
|
|
39
|
+
const sampleRaw = await (0, review_parser_1.parseReportFrontmatter)(samplePath);
|
|
40
|
+
if (!advRaw || !sampleRaw) {
|
|
41
|
+
throw new Error(`第 ${round} 轮需求评审报告缺失或 frontmatter 格式错误`);
|
|
42
|
+
}
|
|
43
|
+
const adversarial = parseAdversarialScore(advRaw);
|
|
44
|
+
const sampleTrace = parseSampleTraceScore(sampleRaw);
|
|
45
|
+
const selfContent = await fs_extra_1.default.readFile(selfReviewPath, 'utf8').catch(() => '');
|
|
46
|
+
const gaps = (0, review_parser_1.parseGapsFromMarkdown)(selfContent);
|
|
47
|
+
const criticalCount = countCriticalIssues(adversarial, gaps);
|
|
48
|
+
const scores = (0, score_aggregator_1.computeReqPackScore)(gates, adversarial.average, sampleTrace.average, criticalCount);
|
|
49
|
+
const review = {
|
|
50
|
+
round,
|
|
51
|
+
report_path: adversarialPath,
|
|
52
|
+
dimension_scores: {
|
|
53
|
+
requirements: Number(asRecord(asRecord(selfRaw?.score).dimensions).requirements ?? 0),
|
|
54
|
+
},
|
|
55
|
+
mechanical_score: scores.mechanical,
|
|
56
|
+
review_score: scores.adversarial,
|
|
57
|
+
critical_count: criticalCount,
|
|
58
|
+
gate_pass: scores.gate_pass,
|
|
59
|
+
gate_details: gates,
|
|
60
|
+
modifications_required: !(scores.composite > ctx.targetScore &&
|
|
61
|
+
scores.gate_pass &&
|
|
62
|
+
criticalCount === 0),
|
|
63
|
+
gaps,
|
|
64
|
+
composite_score: scores.composite,
|
|
65
|
+
};
|
|
66
|
+
await writeReqPackScoreSnapshot(ctx, review, adversarial, sampleTrace, scores);
|
|
67
|
+
logger_1.logger.info(`第 ${round} 轮需求包评分:综合 ${scores.composite.toFixed(1)} / 10 ` +
|
|
68
|
+
`(机械 ${scores.mechanical.toFixed(1)} + 对抗 ${scores.adversarial.toFixed(1)} + 抽样 ${scores.sample_trace.toFixed(1)})`);
|
|
69
|
+
return review;
|
|
70
|
+
}
|
|
71
|
+
async function runAcpReviewPhase(ctx, acp, cost, round, template, phaseKey, signal) {
|
|
72
|
+
const gates = await (0, gate_checker_1.runMechanicalGates)(ctx.harnessRoot, { scope: 'requirements', verbose: ctx.verbose });
|
|
73
|
+
const prompt = await (0, phase_prompt_loader_1.loadPhasePrompt)(template, buildPromptVars(ctx, round, gates), ctx.promptDir);
|
|
74
|
+
cost.recordCall(`${phaseKey}_round_${round}`);
|
|
75
|
+
const result = await acp.call({
|
|
76
|
+
workDir: ctx.harnessRoot,
|
|
77
|
+
phase: phaseKey,
|
|
78
|
+
prompt: (0, phase_prompt_loader_1.wrapAcpPrompt)(prompt, signal),
|
|
79
|
+
timeoutMs: ctx.acpTimeoutMs,
|
|
80
|
+
});
|
|
81
|
+
await fs_extra_1.default.writeFile(node_path_1.default.join(ctx.harnessRoot, 'auto-review', `${phaseKey}-round-${round}.log`), [result.output, result.error ? `\n[stderr]\n${result.error}` : ''].join(''), 'utf8');
|
|
82
|
+
if (!result.success) {
|
|
83
|
+
throw new Error(`${template} Agent 调用失败:${result.error ?? 'unknown error'}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildPromptVars(ctx, round, gates) {
|
|
87
|
+
return {
|
|
88
|
+
HARNESS_ROOT: ctx.harnessRoot,
|
|
89
|
+
AUTO_GOAL: ctx.goal ?? '未提供构建目的,请根据 harness 内容判断',
|
|
90
|
+
ROUND: round,
|
|
91
|
+
DOCTOR_EXIT: gates.doctor_exit,
|
|
92
|
+
VERIFY_EXIT: gates.verify_exit,
|
|
93
|
+
FILE_COMPLETENESS_PCT: gates.file_completeness_pct.toFixed(1),
|
|
94
|
+
SAMPLE_FRAGMENTS_PATH: node_path_1.default.join(ctx.harnessRoot, 'auto-review', 'sample-fragments.json'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseAdversarialScore(raw) {
|
|
98
|
+
const score = asRecord(raw.score);
|
|
99
|
+
const adv = asRecord(score.adversarial);
|
|
100
|
+
const coverage = Number(adv.coverage ?? 0);
|
|
101
|
+
const fidelity = Number(adv.fidelity ?? 0);
|
|
102
|
+
const completeness = Number(adv.completeness ?? 0);
|
|
103
|
+
const average = Number(adv.average ?? (coverage + fidelity + completeness) / 3);
|
|
104
|
+
const issues = Array.isArray(raw.issues)
|
|
105
|
+
? raw.issues
|
|
106
|
+
: [];
|
|
107
|
+
return { coverage, fidelity, completeness, average, issues };
|
|
108
|
+
}
|
|
109
|
+
function parseSampleTraceScore(raw) {
|
|
110
|
+
const score = asRecord(raw.score);
|
|
111
|
+
const st = asRecord(score.sample_trace);
|
|
112
|
+
const traceability = Number(st.traceability ?? 0);
|
|
113
|
+
const fidelity = Number(st.fidelity ?? 0);
|
|
114
|
+
const usability = Number(st.usability ?? 0);
|
|
115
|
+
const average = Number(st.average ?? (traceability + fidelity + usability) / 3);
|
|
116
|
+
const items = Array.isArray(st.items)
|
|
117
|
+
? st.items
|
|
118
|
+
: [];
|
|
119
|
+
return { traceability, fidelity, usability, average, items };
|
|
120
|
+
}
|
|
121
|
+
function countCriticalIssues(adversarial, gaps) {
|
|
122
|
+
const fromAdv = adversarial.issues.filter((i) => i.severity === 'critical').length;
|
|
123
|
+
const fromGaps = gaps.filter((g) => g.severity === 'critical').length;
|
|
124
|
+
return fromAdv + fromGaps;
|
|
125
|
+
}
|
|
126
|
+
async function writeReqPackScoreSnapshot(ctx, review, adversarial, sampleTrace, scores) {
|
|
127
|
+
const snapshot = {
|
|
128
|
+
round: review.round,
|
|
129
|
+
evaluated_at: new Date().toISOString(),
|
|
130
|
+
scope: 'requirements',
|
|
131
|
+
mechanical_score: scores.mechanical,
|
|
132
|
+
adversarial_score: scores.adversarial,
|
|
133
|
+
sample_trace_score: scores.sample_trace,
|
|
134
|
+
composite_score: scores.composite,
|
|
135
|
+
target_score: ctx.targetScore,
|
|
136
|
+
gate_pass: scores.gate_pass,
|
|
137
|
+
critical_count: review.critical_count,
|
|
138
|
+
adversarial_details: adversarial,
|
|
139
|
+
sample_trace_details: sampleTrace,
|
|
140
|
+
gate_details: review.gate_details,
|
|
141
|
+
};
|
|
142
|
+
await fs_extra_1.default.writeFile(node_path_1.default.join(ctx.harnessRoot, 'auto-review', 'auto-score.json'), JSON.stringify(snapshot, null, 2), 'utf8');
|
|
143
|
+
}
|
|
144
|
+
function dryRunReqPackReview(round, gates) {
|
|
145
|
+
return {
|
|
146
|
+
round,
|
|
147
|
+
report_path: 'dry-run',
|
|
148
|
+
review_score: 0,
|
|
149
|
+
dimension_scores: {},
|
|
150
|
+
critical_count: 0,
|
|
151
|
+
gaps: [],
|
|
152
|
+
gate_pass: false,
|
|
153
|
+
gate_details: gates,
|
|
154
|
+
mechanical_score: 0,
|
|
155
|
+
composite_score: 0,
|
|
156
|
+
modifications_required: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function asRecord(value) {
|
|
160
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
161
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateSampleFragments = generateSampleFragments;
|
|
7
|
+
exports.writeSampleFragmentsFile = writeSampleFragmentsFile;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const MIN_EXCERPT_CHARS = 50;
|
|
11
|
+
const MAX_EXCERPT_CHARS = 200;
|
|
12
|
+
const NUMERIC_PATTERN = /\d+(\.\d+)?(%|ms|s|Hz|MHz|GB|MB|KB)?/;
|
|
13
|
+
const ENUM_PATTERN = /\b(ON|OFF|TRUE|FALSE|ENABLE|DISABLE|是|否|打开|关闭)\b/i;
|
|
14
|
+
const TIMING_PATTERN = /\b(延迟|超时|timeout|delay|ms|秒|分钟|时序|周期)\b/i;
|
|
15
|
+
const SIGNAL_PRIORITY = {
|
|
16
|
+
numeric: 5,
|
|
17
|
+
timing: 4,
|
|
18
|
+
enum: 4,
|
|
19
|
+
table_row: 3,
|
|
20
|
+
list_item: 2,
|
|
21
|
+
paragraph: 1,
|
|
22
|
+
};
|
|
23
|
+
function classifyLine(text) {
|
|
24
|
+
const trimmed = text.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith('```') || trimmed === '---') {
|
|
26
|
+
return { signal_type: 'paragraph', priority: 0 };
|
|
27
|
+
}
|
|
28
|
+
if (trimmed.startsWith('|') && trimmed.split('|').filter(Boolean).length >= 2) {
|
|
29
|
+
return { signal_type: 'table_row', priority: SIGNAL_PRIORITY.table_row };
|
|
30
|
+
}
|
|
31
|
+
if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
|
|
32
|
+
return { signal_type: 'list_item', priority: SIGNAL_PRIORITY.list_item };
|
|
33
|
+
}
|
|
34
|
+
if (NUMERIC_PATTERN.test(trimmed)) {
|
|
35
|
+
return { signal_type: 'numeric', priority: SIGNAL_PRIORITY.numeric };
|
|
36
|
+
}
|
|
37
|
+
if (TIMING_PATTERN.test(trimmed)) {
|
|
38
|
+
return { signal_type: 'timing', priority: SIGNAL_PRIORITY.timing };
|
|
39
|
+
}
|
|
40
|
+
if (ENUM_PATTERN.test(trimmed)) {
|
|
41
|
+
return { signal_type: 'enum', priority: SIGNAL_PRIORITY.enum };
|
|
42
|
+
}
|
|
43
|
+
if (/^#{1,6}\s/.test(trimmed)) {
|
|
44
|
+
return { signal_type: 'list_item', priority: SIGNAL_PRIORITY.list_item + 1 };
|
|
45
|
+
}
|
|
46
|
+
return { signal_type: 'paragraph', priority: SIGNAL_PRIORITY.paragraph };
|
|
47
|
+
}
|
|
48
|
+
function buildExcerpt(lines, startIdx) {
|
|
49
|
+
let excerpt = '';
|
|
50
|
+
let endIdx = startIdx;
|
|
51
|
+
for (let i = startIdx; i < lines.length && excerpt.length < MAX_EXCERPT_CHARS; i++) {
|
|
52
|
+
const line = lines[i]?.trim();
|
|
53
|
+
if (!line)
|
|
54
|
+
continue;
|
|
55
|
+
const next = excerpt ? `${excerpt}\n${line}` : line;
|
|
56
|
+
if (next.length > MAX_EXCERPT_CHARS && excerpt.length >= MIN_EXCERPT_CHARS)
|
|
57
|
+
break;
|
|
58
|
+
excerpt = next;
|
|
59
|
+
endIdx = i;
|
|
60
|
+
if (excerpt.length >= MIN_EXCERPT_CHARS && excerpt.length >= MAX_EXCERPT_CHARS * 0.6)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (excerpt.length < MIN_EXCERPT_CHARS)
|
|
64
|
+
return null;
|
|
65
|
+
return { excerpt, endIdx };
|
|
66
|
+
}
|
|
67
|
+
async function collectCandidates(mdDir) {
|
|
68
|
+
const candidates = [];
|
|
69
|
+
if (!(await fs_extra_1.default.pathExists(mdDir)))
|
|
70
|
+
return candidates;
|
|
71
|
+
const files = (await fs_extra_1.default.readdir(mdDir)).filter((f) => f.endsWith('.md'));
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
const abs = node_path_1.default.join(mdDir, file);
|
|
74
|
+
const content = await fs_extra_1.default.readFile(abs, 'utf8');
|
|
75
|
+
const lines = content.split(/\r?\n/);
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const { signal_type, priority } = classifyLine(lines[i] ?? '');
|
|
78
|
+
if (priority === 0)
|
|
79
|
+
continue;
|
|
80
|
+
candidates.push({
|
|
81
|
+
file: node_path_1.default.join('requirements/md', file).replace(/\\/g, '/'),
|
|
82
|
+
lineNo: i + 1,
|
|
83
|
+
text: lines[i] ?? '',
|
|
84
|
+
signal_type,
|
|
85
|
+
priority,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
}
|
|
91
|
+
function shuffle(items) {
|
|
92
|
+
const copy = [...items];
|
|
93
|
+
for (let i = copy.length - 1; i > 0; i--) {
|
|
94
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
95
|
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
96
|
+
}
|
|
97
|
+
return copy;
|
|
98
|
+
}
|
|
99
|
+
async function generateSampleFragments(harnessRoot, count = 7) {
|
|
100
|
+
const mdDir = node_path_1.default.join(harnessRoot, 'requirements', 'md');
|
|
101
|
+
const candidates = await collectCandidates(mdDir);
|
|
102
|
+
if (candidates.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
const sorted = [...candidates].sort((a, b) => b.priority - a.priority);
|
|
105
|
+
const highPriority = sorted.filter((c) => c.priority >= 3);
|
|
106
|
+
const pool = shuffle(highPriority.length >= count ? highPriority : sorted);
|
|
107
|
+
const selected = pool.slice(0, Math.min(count, pool.length));
|
|
108
|
+
const fragments = [];
|
|
109
|
+
const usedKeys = new Set();
|
|
110
|
+
for (const cand of selected) {
|
|
111
|
+
const abs = node_path_1.default.join(harnessRoot, cand.file);
|
|
112
|
+
if (!(await fs_extra_1.default.pathExists(abs)))
|
|
113
|
+
continue;
|
|
114
|
+
const lines = (await fs_extra_1.default.readFile(abs, 'utf8')).split(/\r?\n/);
|
|
115
|
+
const startIdx = cand.lineNo - 1;
|
|
116
|
+
const built = buildExcerpt(lines, startIdx);
|
|
117
|
+
if (!built)
|
|
118
|
+
continue;
|
|
119
|
+
const key = `${cand.file}:${cand.lineNo}`;
|
|
120
|
+
if (usedKeys.has(key))
|
|
121
|
+
continue;
|
|
122
|
+
usedKeys.add(key);
|
|
123
|
+
fragments.push({
|
|
124
|
+
id: `SF-${String(fragments.length + 1).padStart(3, '0')}`,
|
|
125
|
+
file: cand.file,
|
|
126
|
+
start_line: cand.lineNo,
|
|
127
|
+
end_line: built.endIdx + 1,
|
|
128
|
+
excerpt: built.excerpt,
|
|
129
|
+
signal_type: cand.signal_type,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return fragments;
|
|
133
|
+
}
|
|
134
|
+
async function writeSampleFragmentsFile(harnessRoot, count = 7) {
|
|
135
|
+
const fragments = await generateSampleFragments(harnessRoot, count);
|
|
136
|
+
const outDir = node_path_1.default.join(harnessRoot, 'auto-review');
|
|
137
|
+
await fs_extra_1.default.ensureDir(outDir);
|
|
138
|
+
await fs_extra_1.default.writeJson(node_path_1.default.join(outDir, 'sample-fragments.json'), {
|
|
139
|
+
generated_at: new Date().toISOString(),
|
|
140
|
+
count: fragments.length,
|
|
141
|
+
fragments,
|
|
142
|
+
}, { spaces: 2 });
|
|
143
|
+
return fragments;
|
|
144
|
+
}
|