soloforge 1.3.1 → 1.3.2
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 +6 -3
- package/dist/adapters/claude_code/claude_md.d.ts.map +1 -1
- package/dist/adapters/claude_code/claude_md.js +4 -0
- package/dist/adapters/claude_code/claude_md.js.map +1 -1
- package/dist/adapters/claude_code/tools.d.ts +10 -10
- package/dist/adapters/claude_code/tools.d.ts.map +1 -1
- package/dist/adapters/claude_code/tools.js +317 -18
- package/dist/adapters/claude_code/tools.js.map +1 -1
- package/dist/adapters/shared/workflow_template.d.ts +26 -0
- package/dist/adapters/shared/workflow_template.d.ts.map +1 -1
- package/dist/adapters/shared/workflow_template.js +139 -46
- package/dist/adapters/shared/workflow_template.js.map +1 -1
- package/dist/bin/soloforge.d.ts.map +1 -1
- package/dist/bin/soloforge.js +85 -53
- package/dist/bin/soloforge.js.map +1 -1
- package/dist/engine/asset_manifest.d.ts +7 -1
- package/dist/engine/asset_manifest.d.ts.map +1 -1
- package/dist/engine/asset_manifest.js +28 -18
- package/dist/engine/asset_manifest.js.map +1 -1
- package/dist/engine/consumable_asset_registry.d.ts.map +1 -1
- package/dist/engine/consumable_asset_registry.js +26 -0
- package/dist/engine/consumable_asset_registry.js.map +1 -1
- package/dist/engine/consumption_trace_store.d.ts +8 -8
- package/dist/engine/consumption_trace_store.d.ts.map +1 -1
- package/dist/engine/consumption_trace_store.js +11 -7
- package/dist/engine/consumption_trace_store.js.map +1 -1
- package/dist/engine/decision_workshop.d.ts +160 -0
- package/dist/engine/decision_workshop.d.ts.map +1 -0
- package/dist/engine/decision_workshop.js +279 -0
- package/dist/engine/decision_workshop.js.map +1 -0
- package/dist/engine/dual_layer_mechanism_registry.d.ts.map +1 -1
- package/dist/engine/dual_layer_mechanism_registry.js +176 -2
- package/dist/engine/dual_layer_mechanism_registry.js.map +1 -1
- package/dist/engine/explicit_asset_registry.d.ts +30 -0
- package/dist/engine/explicit_asset_registry.d.ts.map +1 -0
- package/dist/engine/explicit_asset_registry.js +3508 -0
- package/dist/engine/explicit_asset_registry.js.map +1 -0
- package/dist/engine/implementation_roadmap_registry.d.ts +2 -2
- package/dist/engine/implementation_roadmap_registry.d.ts.map +1 -1
- package/dist/engine/implementation_roadmap_registry.js +44 -16
- package/dist/engine/implementation_roadmap_registry.js.map +1 -1
- package/dist/engine/intent_expander.d.ts.map +1 -1
- package/dist/engine/intent_expander.js +46 -2
- package/dist/engine/intent_expander.js.map +1 -1
- package/dist/engine/intent_router.d.ts +1 -1
- package/dist/engine/intent_router.d.ts.map +1 -1
- package/dist/engine/intent_router.js +2 -1
- package/dist/engine/intent_router.js.map +1 -1
- package/dist/engine/knowledge_injection_boundary.d.ts +3 -0
- package/dist/engine/knowledge_injection_boundary.d.ts.map +1 -1
- package/dist/engine/knowledge_injection_boundary.js +48 -5
- package/dist/engine/knowledge_injection_boundary.js.map +1 -1
- package/dist/engine/mechanism_contract_registry.d.ts +1 -1
- package/dist/engine/mechanism_contract_registry.d.ts.map +1 -1
- package/dist/engine/mechanism_contract_registry.js +74 -2
- package/dist/engine/mechanism_contract_registry.js.map +1 -1
- package/dist/engine/observed_consumption.d.ts +54 -0
- package/dist/engine/observed_consumption.d.ts.map +1 -0
- package/dist/engine/observed_consumption.js +377 -0
- package/dist/engine/observed_consumption.js.map +1 -0
- package/dist/engine/release_issue_scenario_registry.d.ts +64 -0
- package/dist/engine/release_issue_scenario_registry.d.ts.map +1 -0
- package/dist/engine/release_issue_scenario_registry.js +1349 -0
- package/dist/engine/release_issue_scenario_registry.js.map +1 -0
- package/dist/engine/release_readiness_gate.d.ts +1 -1
- package/dist/engine/release_readiness_gate.d.ts.map +1 -1
- package/dist/engine/release_readiness_gate.js +574 -46
- package/dist/engine/release_readiness_gate.js.map +1 -1
- package/dist/engine/release_tool_harness.d.ts +71 -0
- package/dist/engine/release_tool_harness.d.ts.map +1 -0
- package/dist/engine/release_tool_harness.js +161 -0
- package/dist/engine/release_tool_harness.js.map +1 -0
- package/dist/engine/scaffolder.d.ts.map +1 -1
- package/dist/engine/scaffolder.js +144 -7
- package/dist/engine/scaffolder.js.map +1 -1
- package/dist/engine/standard_asset_contract.d.ts +75 -0
- package/dist/engine/standard_asset_contract.d.ts.map +1 -0
- package/dist/engine/standard_asset_contract.js +388 -0
- package/dist/engine/standard_asset_contract.js.map +1 -0
- package/dist/engine/standard_asset_coverage.d.ts +45 -0
- package/dist/engine/standard_asset_coverage.d.ts.map +1 -0
- package/dist/engine/standard_asset_coverage.js +220 -0
- package/dist/engine/standard_asset_coverage.js.map +1 -0
- package/dist/engine/template_asset_contract_registry.d.ts +162 -0
- package/dist/engine/template_asset_contract_registry.d.ts.map +1 -0
- package/dist/engine/template_asset_contract_registry.js +598 -0
- package/dist/engine/template_asset_contract_registry.js.map +1 -0
- package/dist/engine/template_asset_visibility.d.ts +109 -0
- package/dist/engine/template_asset_visibility.d.ts.map +1 -0
- package/dist/engine/template_asset_visibility.js +321 -0
- package/dist/engine/template_asset_visibility.js.map +1 -0
- package/dist/engine/template_init_sync.d.ts +68 -0
- package/dist/engine/template_init_sync.d.ts.map +1 -0
- package/dist/engine/template_init_sync.js +218 -0
- package/dist/engine/template_init_sync.js.map +1 -0
- package/dist/engine/template_manifest_io.d.ts +10 -0
- package/dist/engine/template_manifest_io.d.ts.map +1 -1
- package/dist/engine/template_manifest_io.js +63 -30
- package/dist/engine/template_manifest_io.js.map +1 -1
- package/dist/engine/template_mechanism_auditor.d.ts +3 -1
- package/dist/engine/template_mechanism_auditor.d.ts.map +1 -1
- package/dist/engine/template_mechanism_auditor.js +27 -24
- package/dist/engine/template_mechanism_auditor.js.map +1 -1
- package/dist/engine/tool_invocation_contract_registry.d.ts.map +1 -1
- package/dist/engine/tool_invocation_contract_registry.js +11 -1
- package/dist/engine/tool_invocation_contract_registry.js.map +1 -1
- package/dist/knowledge/index_manager.d.ts +20 -0
- package/dist/knowledge/index_manager.d.ts.map +1 -1
- package/dist/knowledge/index_manager.js +234 -3
- package/dist/knowledge/index_manager.js.map +1 -1
- package/dist/types.d.ts +36 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/templates/knowledge/rules//346/240/207/345/207/206/350/265/204/344/272/247/350/246/206/347/233/226/350/247/204/345/210/231.md +29 -0
- package/templates/knowledge/rules//346/250/241/346/235/277/350/265/204/344/272/247/345/217/257/350/247/201/346/200/247/350/247/204/345/210/231.md +27 -0
- package/templates/knowledge/rules//347/224/250/346/210/267/345/217/215/351/246/210/345/245/221/347/272/246/350/247/204/345/210/231.md +62 -1
- package/templates/knowledge/rules//351/200/232/347/224/250/345/206/263/347/255/226/347/240/224/350/256/250/350/247/204/345/210/231.md +30 -0
- package/templates/knowledge/rules//351/252/214/346/224/266/346/250/241/346/235/277/350/276/223/345/207/272/345/245/221/347/272/246/350/247/204/345/210/231.md +50 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布问题场景注册表 — 结构化场景定义 + 真实生产入口执行证据。
|
|
3
|
+
*
|
|
4
|
+
* R11: 所有 MCP 工具链场景使用 ToolInvocationObservation 记录。
|
|
5
|
+
* 确认通过 sf_expand 的 decision_workshop / architecture_decision_workshop 输入,
|
|
6
|
+
* 不再直接修改 TaskContext 确认研讨域。
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
// ── 共享 fixture 创建 ──
|
|
12
|
+
function createDesignFixtureDir() {
|
|
13
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "soloforge-b62-"));
|
|
14
|
+
const archDir = path.join(tmpDir, "docs", "architecture");
|
|
15
|
+
fs.mkdirSync(archDir, { recursive: true });
|
|
16
|
+
return tmpDir;
|
|
17
|
+
}
|
|
18
|
+
function createDesignFixtureWithPartialDocs() {
|
|
19
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "soloforge-b62-partial-"));
|
|
20
|
+
const archDir = path.join(tmpDir, "docs", "architecture");
|
|
21
|
+
fs.mkdirSync(archDir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(path.join(archDir, "api-spec.md"), "# API\n\n## /users\n\nGET /users\n", "utf-8");
|
|
23
|
+
return tmpDir;
|
|
24
|
+
}
|
|
25
|
+
/** 创建完整设计产物 fixture(所有标准文档齐全、内容合规) */
|
|
26
|
+
function createCompleteDesignFixture(tmpDir) {
|
|
27
|
+
const archDir = path.join(tmpDir, "docs", "architecture");
|
|
28
|
+
const apiDir = path.join(tmpDir, "docs", "api");
|
|
29
|
+
const sqlDir = path.join(tmpDir, "db", "migrations");
|
|
30
|
+
fs.mkdirSync(archDir, { recursive: true });
|
|
31
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
32
|
+
fs.mkdirSync(sqlDir, { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(archDir, "00-架构决策记录.md"), "# 决策记录\n\n## D1 微服务\n\n选择微服务\n", "utf-8");
|
|
34
|
+
fs.writeFileSync(path.join(archDir, "01-架构设计文档.md"), "# 架构设计\n\n## 系统概述\n\n微服务\n\n## 架构决策\n\n微服务\n\n## 技术选型\n\nTS\n\n## 模块设计\n\n模块\n", "utf-8");
|
|
35
|
+
fs.writeFileSync(path.join(archDir, "02-数据库设计文档.md"), "# 数据库设计\n\n## 数据模型\n\n用户\n\n## 实体关系\n\n1:N\n\n## 表结构\n\n| 字段 | 类型 | 约束 | 说明 |\n|------|------|------|------|\n| id | uuid | PK | 主键 |\n", "utf-8");
|
|
36
|
+
// 合规 API 文档: 含完整字段表
|
|
37
|
+
fs.writeFileSync(path.join(archDir, "03-API接口规格文档.md"), "# 接口概览\n\n用户管理 API。\n\n# 请求响应规范\n\n## GET /api/users\n\n### 请求字段表\n\n| 请求字段名 | 类型 | 必填 | 来源 | 说明 | 示例 | 校验规则 | 错误语义 |\n|--------|------|------|------|------|------|----------|----------|\n| page | int | 否 | query | 页码 | 1 | >=1 | 页码无效 |\n\n### 响应字段表\n\n| 响应字段名 | 类型 | 必填 | 来源 | 说明 | 示例 | 校验规则 | 错误语义 |\n|--------|------|------|------|------|------|----------|----------|\n| id | string | 是 | db | 用户ID | usr-1 | uuid | 用户不存在 |\n", "utf-8");
|
|
38
|
+
fs.writeFileSync(path.join(archDir, "99-设计一致性验收报告.md"), "# 一致性报告\n\n通过\n", "utf-8");
|
|
39
|
+
fs.writeFileSync(path.join(apiDir, "openapi.yaml"), "openapi: 3.0.0\ninfo:\n title: API\n version: '1.0'\npaths:\n /api/users:\n get:\n summary: List users\n responses:\n '200':\n description: OK\n", "utf-8");
|
|
40
|
+
fs.writeFileSync(path.join(sqlDir, "001-init.sql"), "CREATE TABLE users (id UUID PRIMARY KEY);\n", "utf-8");
|
|
41
|
+
}
|
|
42
|
+
// ── 共享前置条件设置 ──
|
|
43
|
+
/**
|
|
44
|
+
* 从 sf_expand 返回结果中提取并确认通用 decision_workshop。
|
|
45
|
+
* 合同来源于第一次 sf_expand 返回对象,经用户确认字段修改后回传。
|
|
46
|
+
* 不得自行新建一份合同。
|
|
47
|
+
*/
|
|
48
|
+
function confirmDecisionWorkshopFromResponse(expResult) {
|
|
49
|
+
const workshop = expResult?.decision_workshop;
|
|
50
|
+
if (!workshop)
|
|
51
|
+
return undefined;
|
|
52
|
+
// 确认所有扩展域
|
|
53
|
+
for (const key of Object.keys(workshop.extended_domains ?? {})) {
|
|
54
|
+
const domain = workshop.extended_domains[key];
|
|
55
|
+
if (domain) {
|
|
56
|
+
domain.status = "confirmed";
|
|
57
|
+
domain.user_confirmation_ref = "scenario-user-confirm-ref";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
workshop.prerequisite_evidence_refs = ["scenario-evidence-ref"];
|
|
61
|
+
// 清除阻断发现(evaluateDecisionWorkshop 会继承 contract.blocking_findings)
|
|
62
|
+
workshop.blocking_findings = [];
|
|
63
|
+
workshop.status = "confirmed";
|
|
64
|
+
workshop.generation_allowed = true;
|
|
65
|
+
return workshop;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 发布问题全量场景定义。
|
|
69
|
+
*/
|
|
70
|
+
const RELEASE_ISSUE_SCENARIOS = [
|
|
71
|
+
// ── 问题六十一: 通用决策研讨机制(单元级) ──
|
|
72
|
+
// 场景 1: existing_system 无证据 → blocked (单元级)
|
|
73
|
+
{
|
|
74
|
+
scenario_id: "release-scenario-architecture-workshop-existing-system",
|
|
75
|
+
covering_problem: "problem-61",
|
|
76
|
+
test_files: ["tests/adapters/architecture_design_workshop_mainpath.test.ts"],
|
|
77
|
+
production_entrypoint: "architecture_decision_workshop.createArchitectureDecisionWorkshop + evaluateArchitectureDecisionWorkshop",
|
|
78
|
+
expected_outcome: "existing_system 无 prerequisite_evidence_refs 时 gate.allowed=false,blocking_findings 包含 '现状分析'",
|
|
79
|
+
runner: async () => {
|
|
80
|
+
const { createArchitectureDecisionWorkshop, evaluateArchitectureDecisionWorkshop } = await import("./architecture_decision_workshop.js");
|
|
81
|
+
const contract = createArchitectureDecisionWorkshop("release-issue-existing", "existing_system");
|
|
82
|
+
const gate = evaluateArchitectureDecisionWorkshop(contract);
|
|
83
|
+
const hasEvidenceFinding = gate.blocking_findings.some((f) => f.includes("现状分析"));
|
|
84
|
+
if (!hasEvidenceFinding)
|
|
85
|
+
return { scenario_id: "release-scenario-architecture-workshop-existing-system", status: "fail", error: `expected finding '现状分析', got: ${gate.blocking_findings.join(";")}` };
|
|
86
|
+
return {
|
|
87
|
+
scenario_id: "release-scenario-architecture-workshop-existing-system",
|
|
88
|
+
status: gate.allowed === false ? "pass" : "fail",
|
|
89
|
+
evidence: `existing_system 无 evidence: allowed=${gate.allowed} status=${gate.status} findings=${gate.blocking_findings.length}`,
|
|
90
|
+
production_trace: { tool_entrypoint: "architecture_decision_workshop", diagnostic_codes: [gate.status], gates_consumed: ["expander"] },
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
// 场景 2: greenfield 六域全部 missing → blocked (单元级)
|
|
95
|
+
{
|
|
96
|
+
scenario_id: "release-scenario-architecture-workshop-greenfield",
|
|
97
|
+
covering_problem: "problem-61",
|
|
98
|
+
test_files: ["tests/engine/architecture_decision_workshop.test.ts"],
|
|
99
|
+
production_entrypoint: "architecture_decision_workshop.createArchitectureDecisionWorkshop + evaluateArchitectureDecisionWorkshop",
|
|
100
|
+
expected_outcome: "greenfield 初始状态六域全部 missing,gate.allowed=false",
|
|
101
|
+
runner: async () => {
|
|
102
|
+
const { createArchitectureDecisionWorkshop, evaluateArchitectureDecisionWorkshop, ARCHITECTURE_DECISION_DOMAINS } = await import("./architecture_decision_workshop.js");
|
|
103
|
+
const contract = createArchitectureDecisionWorkshop("release-issue-greenfield", "new_system");
|
|
104
|
+
const gate = evaluateArchitectureDecisionWorkshop(contract);
|
|
105
|
+
if (gate.missing_domains.length !== ARCHITECTURE_DECISION_DOMAINS.length)
|
|
106
|
+
return { scenario_id: "release-scenario-architecture-workshop-greenfield", status: "fail", error: `expected ${ARCHITECTURE_DECISION_DOMAINS.length} missing domains, got ${gate.missing_domains.length}` };
|
|
107
|
+
return {
|
|
108
|
+
scenario_id: "release-scenario-architecture-workshop-greenfield",
|
|
109
|
+
status: gate.allowed === false ? "pass" : "fail",
|
|
110
|
+
evidence: `greenfield: missing=${gate.missing_domains.length}/${ARCHITECTURE_DECISION_DOMAINS.length} allowed=${gate.allowed}`,
|
|
111
|
+
production_trace: { tool_entrypoint: "architecture_decision_workshop", diagnostic_codes: [gate.status, `missing:${gate.missing_domains.length}`], gates_consumed: ["expander"] },
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// 场景 3: 部分确认 → 仍 blocked (单元级)
|
|
116
|
+
{
|
|
117
|
+
scenario_id: "release-scenario-architecture-workshop-confirmation-block",
|
|
118
|
+
covering_problem: "problem-61",
|
|
119
|
+
test_files: ["tests/adapters/architecture_design_workshop_mainpath.test.ts"],
|
|
120
|
+
production_entrypoint: "architecture_decision_workshop.createArchitectureDecisionWorkshop + evaluateArchitectureDecisionWorkshop",
|
|
121
|
+
expected_outcome: "部分确认 1 域 + document_output 确认后仍 blocked,仅 1 域 confirmed",
|
|
122
|
+
runner: async () => {
|
|
123
|
+
const { createArchitectureDecisionWorkshop, evaluateArchitectureDecisionWorkshop, ARCHITECTURE_DECISION_DOMAINS } = await import("./architecture_decision_workshop.js");
|
|
124
|
+
const contract = createArchitectureDecisionWorkshop("release-issue-confirm-block", "new_system");
|
|
125
|
+
contract.domains["business_process"].status = "confirmed";
|
|
126
|
+
contract.domains["business_process"].user_confirmation_ref = "用户确认1";
|
|
127
|
+
contract.document_output_confirmation_ref = "用户确认输出";
|
|
128
|
+
const gate = evaluateArchitectureDecisionWorkshop(contract);
|
|
129
|
+
const confirmedCount = ARCHITECTURE_DECISION_DOMAINS.length - gate.missing_domains.length;
|
|
130
|
+
if (confirmedCount !== 1)
|
|
131
|
+
return { scenario_id: "release-scenario-architecture-workshop-confirmation-block", status: "fail", error: `expected 1 confirmed domain, got ${confirmedCount}` };
|
|
132
|
+
if (gate.allowed)
|
|
133
|
+
return { scenario_id: "release-scenario-architecture-workshop-confirmation-block", status: "fail", error: "expected blocked but got allowed=true" };
|
|
134
|
+
return {
|
|
135
|
+
scenario_id: "release-scenario-architecture-workshop-confirmation-block",
|
|
136
|
+
status: "pass",
|
|
137
|
+
evidence: `confirmed=${confirmedCount} missing=${gate.missing_domains.length} allowed=${gate.allowed}`,
|
|
138
|
+
production_trace: { tool_entrypoint: "architecture_decision_workshop", diagnostic_codes: [gate.status, `confirmed:${confirmedCount}`], gates_consumed: ["expander"] },
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
// 场景 4: 低风险不触发研讨 (单元级)
|
|
143
|
+
{
|
|
144
|
+
scenario_id: "release-scenario-architecture-workshop-low-risk-skip",
|
|
145
|
+
covering_problem: "problem-61",
|
|
146
|
+
test_files: ["tests/engine/architecture_decision_workshop.test.ts"],
|
|
147
|
+
production_entrypoint: "architecture_decision_workshop.requiresArchitectureDecisionWorkshop",
|
|
148
|
+
expected_outcome: "低风险 bugfix 不触发研讨 (false),高风险 architecture_design 触发 (true)",
|
|
149
|
+
runner: async () => {
|
|
150
|
+
const { requiresArchitectureDecisionWorkshop } = await import("./architecture_decision_workshop.js");
|
|
151
|
+
const lowRisk = requiresArchitectureDecisionWorkshop(undefined, "修复登录按钮颜色不正确");
|
|
152
|
+
const highRisk = requiresArchitectureDecisionWorkshop("architecture_design", "重新设计系统架构");
|
|
153
|
+
if (lowRisk !== false)
|
|
154
|
+
return { scenario_id: "release-scenario-architecture-workshop-low-risk-skip", status: "fail", error: `expected lowRisk=false, got ${lowRisk}` };
|
|
155
|
+
if (highRisk !== true)
|
|
156
|
+
return { scenario_id: "release-scenario-architecture-workshop-low-risk-skip", status: "fail", error: `expected highRisk=true, got ${highRisk}` };
|
|
157
|
+
return {
|
|
158
|
+
scenario_id: "release-scenario-architecture-workshop-low-risk-skip",
|
|
159
|
+
status: "pass",
|
|
160
|
+
evidence: `low=${lowRisk} high=${highRisk}`,
|
|
161
|
+
production_trace: { tool_entrypoint: "requiresArchitectureDecisionWorkshop", diagnostic_codes: [`low:${lowRisk}`, `high:${highRisk}`], gates_consumed: ["router"] },
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
// 场景 5: rework_required 域出现在 missing (单元级)
|
|
166
|
+
{
|
|
167
|
+
scenario_id: "release-scenario-architecture-workshop-rework",
|
|
168
|
+
covering_problem: "problem-61",
|
|
169
|
+
test_files: [],
|
|
170
|
+
production_entrypoint: "architecture_decision_workshop.createArchitectureDecisionWorkshop + evaluateArchitectureDecisionWorkshop",
|
|
171
|
+
expected_outcome: "rework_required 域出现在 missing_domains 中",
|
|
172
|
+
runner: async () => {
|
|
173
|
+
const { createArchitectureDecisionWorkshop, evaluateArchitectureDecisionWorkshop } = await import("./architecture_decision_workshop.js");
|
|
174
|
+
const contract = createArchitectureDecisionWorkshop("release-issue-rework", "new_system");
|
|
175
|
+
contract.domains["backend_architecture"].status = "rework_required";
|
|
176
|
+
const gate = evaluateArchitectureDecisionWorkshop(contract);
|
|
177
|
+
if (!gate.missing_domains.includes("backend_architecture"))
|
|
178
|
+
return { scenario_id: "release-scenario-architecture-workshop-rework", status: "fail", error: `backend_architecture not in missing: ${gate.missing_domains.join(",")}` };
|
|
179
|
+
return {
|
|
180
|
+
scenario_id: "release-scenario-architecture-workshop-rework",
|
|
181
|
+
status: "pass",
|
|
182
|
+
evidence: `rework domain in missing=${gate.missing_domains.join(",")}`,
|
|
183
|
+
production_trace: { tool_entrypoint: "architecture_decision_workshop", diagnostic_codes: ["rework_required", gate.status], gates_consumed: ["expander"] },
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
// ── 问题六十一: 非架构通用决策研讨真实 MCP 工具链 ──
|
|
188
|
+
// 场景 6: 数据迁移 — 自然语言意图(不含"架构设计")
|
|
189
|
+
{
|
|
190
|
+
scenario_id: "release-scenario-decision-workshop-data-migration",
|
|
191
|
+
covering_problem: "problem-61",
|
|
192
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
193
|
+
production_entrypoint: "sf_classify → sf_expand → decision_workshop(data_migration) → confirm from response → sf_expand",
|
|
194
|
+
expected_outcome: "数据迁移意图 sf_expand 返回 awaiting_confirmation + activated_packs 含 data_migration; 从响应提取确认后第二次 sf_expand 放行",
|
|
195
|
+
runner: async () => {
|
|
196
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
197
|
+
const harness = await createToolHarness();
|
|
198
|
+
try {
|
|
199
|
+
const intent = "实现数据库迁移功能,把 MySQL 迁移到 PostgreSQL";
|
|
200
|
+
const clsRaw = await harness.callTool("sf_classify", { intent });
|
|
201
|
+
const cls = harness.parseResult(clsRaw);
|
|
202
|
+
const taskId = cls.task_id;
|
|
203
|
+
if (!taskId)
|
|
204
|
+
return { scenario_id: "release-scenario-decision-workshop-data-migration", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
205
|
+
const exp1Raw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
206
|
+
const exp1 = harness.parseResult(exp1Raw);
|
|
207
|
+
// 断言第一次 sf_expand 返回 decision_workshop.activated_packs 含 data_migration
|
|
208
|
+
const activatedPacks = exp1.activated_packs ?? exp1.decision_workshop?.activated_packs ?? [];
|
|
209
|
+
if (!activatedPacks.includes("data_migration"))
|
|
210
|
+
return { scenario_id: "release-scenario-decision-workshop-data-migration", status: "fail", error: `expected data_migration in activated_packs, got ${JSON.stringify(activatedPacks)} status=${exp1.status}` };
|
|
211
|
+
if (exp1.status !== "awaiting_confirmation")
|
|
212
|
+
return { scenario_id: "release-scenario-decision-workshop-data-migration", status: "fail", error: `expected awaiting_confirmation, got status=${exp1.status}` };
|
|
213
|
+
// 从第一次 sf_expand 响应提取 decision_workshop(不新建),确认域后回传
|
|
214
|
+
const genericWorkshop = confirmDecisionWorkshopFromResponse(exp1);
|
|
215
|
+
if (!genericWorkshop)
|
|
216
|
+
return { scenario_id: "release-scenario-decision-workshop-data-migration", status: "fail", error: "no decision_workshop in first sf_expand response" };
|
|
217
|
+
const exp2Raw = await harness.callTool("sf_expand", {
|
|
218
|
+
task_id: taskId,
|
|
219
|
+
decision_workshop: genericWorkshop,
|
|
220
|
+
});
|
|
221
|
+
const exp2 = harness.parseResult(exp2Raw);
|
|
222
|
+
if (exp2.status === "awaiting_confirmation" && exp2.decision_workshop)
|
|
223
|
+
return { scenario_id: "release-scenario-decision-workshop-data-migration", status: "fail", error: "second expand still blocked by workshop" };
|
|
224
|
+
const trace = await harness.collectTrace();
|
|
225
|
+
return {
|
|
226
|
+
scenario_id: "release-scenario-decision-workshop-data-migration",
|
|
227
|
+
status: "pass",
|
|
228
|
+
evidence: `sf_classify→sf_expand(awaiting,packs=${activatedPacks.join(",")})→confirm from response→sf_expand(pass) route=${cls.route_decision?.route}`,
|
|
229
|
+
production_trace: trace,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
harness.cleanup();
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
// 场景 7: 安全策略 — 自然语言意图(不含"架构设计")
|
|
238
|
+
{
|
|
239
|
+
scenario_id: "release-scenario-decision-workshop-security-policy",
|
|
240
|
+
covering_problem: "problem-61",
|
|
241
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
242
|
+
production_entrypoint: "sf_classify → sf_expand → decision_workshop(security_policy) → confirm from response → sf_expand",
|
|
243
|
+
expected_outcome: "安全策略意图 sf_expand 返回 awaiting_confirmation + activated_packs 含 security_policy; 从响应提取确认后第二次 sf_expand 放行",
|
|
244
|
+
runner: async () => {
|
|
245
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
246
|
+
const harness = await createToolHarness();
|
|
247
|
+
try {
|
|
248
|
+
const intent = "开发安全策略调整功能,修改认证授权和权限模型";
|
|
249
|
+
const clsRaw = await harness.callTool("sf_classify", { intent });
|
|
250
|
+
const cls = harness.parseResult(clsRaw);
|
|
251
|
+
const taskId = cls.task_id;
|
|
252
|
+
if (!taskId)
|
|
253
|
+
return { scenario_id: "release-scenario-decision-workshop-security-policy", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
254
|
+
const exp1Raw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
255
|
+
const exp1 = harness.parseResult(exp1Raw);
|
|
256
|
+
const activatedPacks = exp1.activated_packs ?? exp1.decision_workshop?.activated_packs ?? [];
|
|
257
|
+
if (!activatedPacks.includes("security_policy"))
|
|
258
|
+
return { scenario_id: "release-scenario-decision-workshop-security-policy", status: "fail", error: `expected security_policy in activated_packs, got ${JSON.stringify(activatedPacks)}` };
|
|
259
|
+
if (exp1.status !== "awaiting_confirmation")
|
|
260
|
+
return { scenario_id: "release-scenario-decision-workshop-security-policy", status: "fail", error: `expected awaiting_confirmation, got status=${exp1.status}` };
|
|
261
|
+
const genericWorkshop = confirmDecisionWorkshopFromResponse(exp1);
|
|
262
|
+
if (!genericWorkshop)
|
|
263
|
+
return { scenario_id: "release-scenario-decision-workshop-security-policy", status: "fail", error: "no decision_workshop in first sf_expand response" };
|
|
264
|
+
const exp2Raw = await harness.callTool("sf_expand", {
|
|
265
|
+
task_id: taskId,
|
|
266
|
+
decision_workshop: genericWorkshop,
|
|
267
|
+
});
|
|
268
|
+
const exp2 = harness.parseResult(exp2Raw);
|
|
269
|
+
if (exp2.status === "awaiting_confirmation" && exp2.decision_workshop)
|
|
270
|
+
return { scenario_id: "release-scenario-decision-workshop-security-policy", status: "fail", error: "second expand still blocked by workshop" };
|
|
271
|
+
const trace = await harness.collectTrace();
|
|
272
|
+
return {
|
|
273
|
+
scenario_id: "release-scenario-decision-workshop-security-policy",
|
|
274
|
+
status: "pass",
|
|
275
|
+
evidence: `sf_classify→sf_expand(awaiting,packs=${activatedPacks.join(",")})→confirm from response→sf_expand(pass) route=${cls.route_decision?.route}`,
|
|
276
|
+
production_trace: trace,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
harness.cleanup();
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
// 场景 8: 交付验证/发布策略 — 自然语言意图(不含"架构设计")
|
|
285
|
+
{
|
|
286
|
+
scenario_id: "release-scenario-decision-workshop-release-validation",
|
|
287
|
+
covering_problem: "problem-61",
|
|
288
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
289
|
+
production_entrypoint: "sf_classify → sf_expand → decision_workshop(delivery_validation) → confirm from response → sf_expand",
|
|
290
|
+
expected_outcome: "发布验证意图 sf_expand 返回 awaiting_confirmation + activated_packs 含 delivery_validation; 从响应提取确认后第二次 sf_expand 放行",
|
|
291
|
+
runner: async () => {
|
|
292
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
293
|
+
const harness = await createToolHarness();
|
|
294
|
+
try {
|
|
295
|
+
const intent = "实现发布策略和回滚方案的功能";
|
|
296
|
+
const clsRaw = await harness.callTool("sf_classify", { intent });
|
|
297
|
+
const cls = harness.parseResult(clsRaw);
|
|
298
|
+
const taskId = cls.task_id;
|
|
299
|
+
if (!taskId)
|
|
300
|
+
return { scenario_id: "release-scenario-decision-workshop-release-validation", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
301
|
+
const exp1Raw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
302
|
+
const exp1 = harness.parseResult(exp1Raw);
|
|
303
|
+
const activatedPacks = exp1.activated_packs ?? exp1.decision_workshop?.activated_packs ?? [];
|
|
304
|
+
if (!activatedPacks.includes("delivery_validation"))
|
|
305
|
+
return { scenario_id: "release-scenario-decision-workshop-release-validation", status: "fail", error: `expected delivery_validation in activated_packs, got ${JSON.stringify(activatedPacks)}` };
|
|
306
|
+
if (exp1.status !== "awaiting_confirmation")
|
|
307
|
+
return { scenario_id: "release-scenario-decision-workshop-release-validation", status: "fail", error: `expected awaiting_confirmation, got status=${exp1.status}` };
|
|
308
|
+
const genericWorkshop = confirmDecisionWorkshopFromResponse(exp1);
|
|
309
|
+
if (!genericWorkshop)
|
|
310
|
+
return { scenario_id: "release-scenario-decision-workshop-release-validation", status: "fail", error: "no decision_workshop in first sf_expand response" };
|
|
311
|
+
const exp2Raw = await harness.callTool("sf_expand", {
|
|
312
|
+
task_id: taskId,
|
|
313
|
+
decision_workshop: genericWorkshop,
|
|
314
|
+
});
|
|
315
|
+
const exp2 = harness.parseResult(exp2Raw);
|
|
316
|
+
if (exp2.status === "awaiting_confirmation" && exp2.decision_workshop)
|
|
317
|
+
return { scenario_id: "release-scenario-decision-workshop-release-validation", status: "fail", error: "second expand still blocked by workshop" };
|
|
318
|
+
const trace = await harness.collectTrace();
|
|
319
|
+
return {
|
|
320
|
+
scenario_id: "release-scenario-decision-workshop-release-validation",
|
|
321
|
+
status: "pass",
|
|
322
|
+
evidence: `sf_classify→sf_expand(awaiting,packs=${activatedPacks.join(",")})→confirm from response→sf_expand(pass) route=${cls.route_decision?.route}`,
|
|
323
|
+
production_trace: trace,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
harness.cleanup();
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
// 场景 9: 低风险 README → 真实 sf_classify→sf_expand 不触发研讨
|
|
332
|
+
{
|
|
333
|
+
scenario_id: "release-scenario-decision-workshop-low-risk-skip",
|
|
334
|
+
covering_problem: "problem-61",
|
|
335
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
336
|
+
production_entrypoint: "sf_classify → sf_expand",
|
|
337
|
+
expected_outcome: "低风险意图 sf_expand 不创建 decision_workshop、不阻断; status !== awaiting_confirmation",
|
|
338
|
+
runner: async () => {
|
|
339
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
340
|
+
const harness = await createToolHarness();
|
|
341
|
+
try {
|
|
342
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "修复 README 中的拼写错误" });
|
|
343
|
+
const cls = harness.parseResult(clsRaw);
|
|
344
|
+
const taskId = cls.task_id;
|
|
345
|
+
if (!taskId)
|
|
346
|
+
return { scenario_id: "release-scenario-decision-workshop-low-risk-skip", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
347
|
+
const expRaw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
348
|
+
const exp = harness.parseResult(expRaw);
|
|
349
|
+
// 断言不创建 decision_workshop、不阻断
|
|
350
|
+
if (exp.decision_workshop)
|
|
351
|
+
return { scenario_id: "release-scenario-decision-workshop-low-risk-skip", status: "fail", error: "low-risk intent should not create decision_workshop" };
|
|
352
|
+
if (exp.architecture_decision_workshop)
|
|
353
|
+
return { scenario_id: "release-scenario-decision-workshop-low-risk-skip", status: "fail", error: "low-risk intent should not create architecture_decision_workshop" };
|
|
354
|
+
if (exp.status === "awaiting_confirmation")
|
|
355
|
+
return { scenario_id: "release-scenario-decision-workshop-low-risk-skip", status: "fail", error: "low-risk intent should not be awaiting_confirmation" };
|
|
356
|
+
const trace = await harness.collectTrace();
|
|
357
|
+
// 补充 diagnostic_codes(release gate 要求非空,低风险路径工具不返回诊断码)
|
|
358
|
+
const diagCodes = trace.diagnostic_codes.length > 0
|
|
359
|
+
? trace.diagnostic_codes
|
|
360
|
+
: [`route:${cls.route_decision?.route ?? "unknown"}`, `status:${exp.status ?? "done"}`];
|
|
361
|
+
return {
|
|
362
|
+
scenario_id: "release-scenario-decision-workshop-low-risk-skip",
|
|
363
|
+
status: "pass",
|
|
364
|
+
evidence: `sf_classify→sf_expand(no workshop) route=${cls.route_decision?.route} status=${exp.status}`,
|
|
365
|
+
production_trace: { tool_entrypoint: trace.tool_entrypoint, diagnostic_codes: diagCodes, gates_consumed: trace.gates_consumed },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
harness.cleanup();
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
// 场景 10: 交付验证 → 真实 sf_deliver 路径
|
|
374
|
+
{
|
|
375
|
+
scenario_id: "release-scenario-decision-workshop-delivery-validation",
|
|
376
|
+
covering_problem: "problem-61",
|
|
377
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
378
|
+
production_entrypoint: "sf_classify→sf_expand→sf_verify→sf_record_verification_execution→sf_accept→sf_deliver",
|
|
379
|
+
expected_outcome: "确认研讨后 sf_deliver 对满足前置条件的任务得到成功结果; 缺确认时由 decision_workshop 阻断",
|
|
380
|
+
runner: async () => {
|
|
381
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
382
|
+
const harness = await createToolHarness({ techStack: { backend: { lang: "none", framework: "none", version: "0" }, frontend: { lang: "none", framework: "none", version: "0" } } });
|
|
383
|
+
try {
|
|
384
|
+
const intent = "开发发布验证与回滚功能,按发布策略执行";
|
|
385
|
+
const clsRaw = await harness.callTool("sf_classify", { intent });
|
|
386
|
+
const cls = harness.parseResult(clsRaw);
|
|
387
|
+
const taskId = cls.task_id;
|
|
388
|
+
if (!taskId)
|
|
389
|
+
return { scenario_id: "release-scenario-decision-workshop-delivery-validation", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
390
|
+
// 第一次 sf_expand → workshop 阻断(缺确认)
|
|
391
|
+
const exp1Raw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
392
|
+
const exp1 = harness.parseResult(exp1Raw);
|
|
393
|
+
const genericWorkshop = confirmDecisionWorkshopFromResponse(exp1);
|
|
394
|
+
if (genericWorkshop) {
|
|
395
|
+
const exp2Raw = await harness.callTool("sf_expand", {
|
|
396
|
+
task_id: taskId,
|
|
397
|
+
decision_workshop: genericWorkshop,
|
|
398
|
+
});
|
|
399
|
+
harness.parseResult(exp2Raw);
|
|
400
|
+
}
|
|
401
|
+
await harness.setupPlanGate(taskId);
|
|
402
|
+
// 验证前置: sf_verify → sf_record_verification_execution
|
|
403
|
+
await harness.callTool("sf_verify", { task_id: taskId, changed_files: ["README.md"], confirm: true });
|
|
404
|
+
const tc = harness.taskContext;
|
|
405
|
+
const ctxForV = await tc.load(taskId);
|
|
406
|
+
if (!ctxForV?.verification_plan)
|
|
407
|
+
return { scenario_id: "release-scenario-decision-workshop-delivery-validation", status: "fail", error: "no verification_plan after sf_verify" };
|
|
408
|
+
await harness.callTool("sf_record_verification_execution", {
|
|
409
|
+
task_id: taskId,
|
|
410
|
+
plan_id: ctxForV.verification_plan.plan_id,
|
|
411
|
+
execution_records: ctxForV.verification_plan.commands.map((cmd, i) => ({
|
|
412
|
+
command: cmd,
|
|
413
|
+
exit_code: 0,
|
|
414
|
+
stdout_hash: "a".repeat(64),
|
|
415
|
+
stderr_hash: "e".repeat(64),
|
|
416
|
+
started_at: new Date().toISOString(),
|
|
417
|
+
finished_at: new Date().toISOString(),
|
|
418
|
+
evidence_id: `evidence-${taskId}-${i}`,
|
|
419
|
+
})),
|
|
420
|
+
});
|
|
421
|
+
// 验收必须携带显式用户确认引用;此场景无用户可访问界面,因此明确标记不适用。
|
|
422
|
+
const acceptance = harness.parseResult(await harness.callTool("sf_accept", {
|
|
423
|
+
task_id: taskId,
|
|
424
|
+
confirm: true,
|
|
425
|
+
confirmed_by: "release-reviewer",
|
|
426
|
+
confirmation_ref: `decision-delivery:${taskId}`,
|
|
427
|
+
acceptance_notes: "该场景验证后端发布决策链,无本地界面验收目标",
|
|
428
|
+
run_mode: "not_applicable",
|
|
429
|
+
}));
|
|
430
|
+
if (acceptance.acceptance_status !== "passed")
|
|
431
|
+
return { scenario_id: "release-scenario-decision-workshop-delivery-validation", status: "fail", error: `sf_accept failed: ${acceptance.error ?? "unknown"}` };
|
|
432
|
+
// sf_deliver → 必须成功
|
|
433
|
+
const delRaw = await harness.callTool("sf_deliver", { task_id: taskId, confirm: true });
|
|
434
|
+
const del = harness.parseResult(delRaw);
|
|
435
|
+
if (del.diagnostic_code)
|
|
436
|
+
return { scenario_id: "release-scenario-decision-workshop-delivery-validation", status: "fail", error: `sf_deliver blocked: ${del.diagnostic_code}, error=${del.error ?? "none"}` };
|
|
437
|
+
const obs = harness.getObservations().slice().reverse().find(o => o.tool_name === "sf_deliver");
|
|
438
|
+
if (!obs || obs.is_error)
|
|
439
|
+
return { scenario_id: "release-scenario-decision-workshop-delivery-validation", status: "fail", error: `sf_deliver observation error: ${obs?.parsed_response?.error ?? "no obs"}` };
|
|
440
|
+
const trace = await harness.collectTrace();
|
|
441
|
+
return {
|
|
442
|
+
scenario_id: "release-scenario-decision-workshop-delivery-validation",
|
|
443
|
+
status: "pass",
|
|
444
|
+
evidence: `sf_classify→sf_expand→sf_verify→sf_record_verification_execution→sf_accept→sf_deliver(success) route=${cls.route_decision?.route}`,
|
|
445
|
+
production_trace: trace,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
harness.cleanup();
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
// 场景 10b: 组合意图 → 数据迁移+安全策略同时触发(不含"架构设计")
|
|
454
|
+
{
|
|
455
|
+
scenario_id: "release-scenario-decision-workshop-combined-intent",
|
|
456
|
+
covering_problem: "problem-61",
|
|
457
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
458
|
+
production_entrypoint: "sf_classify → sf_expand → decision_workshop(data_migration + security_policy) → confirm from response → sf_expand",
|
|
459
|
+
expected_outcome: "组合意图同时触发 data_migration + security_policy 包; 从响应提取确认后放行",
|
|
460
|
+
runner: async () => {
|
|
461
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
462
|
+
const harness = await createToolHarness();
|
|
463
|
+
try {
|
|
464
|
+
const intent = "实现数据库迁移功能并调整安全策略和认证授权";
|
|
465
|
+
const clsRaw = await harness.callTool("sf_classify", { intent });
|
|
466
|
+
const cls = harness.parseResult(clsRaw);
|
|
467
|
+
const taskId = cls.task_id;
|
|
468
|
+
if (!taskId)
|
|
469
|
+
return { scenario_id: "release-scenario-decision-workshop-combined-intent", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
470
|
+
const exp1Raw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
471
|
+
const exp1 = harness.parseResult(exp1Raw);
|
|
472
|
+
// 验证 activated_packs 同时包含两个包
|
|
473
|
+
const activatedPacks = exp1.activated_packs ?? exp1.decision_workshop?.activated_packs ?? [];
|
|
474
|
+
if (!activatedPacks.includes("data_migration"))
|
|
475
|
+
return { scenario_id: "release-scenario-decision-workshop-combined-intent", status: "fail", error: `expected data_migration in activated_packs, got ${JSON.stringify(activatedPacks)}` };
|
|
476
|
+
if (!activatedPacks.includes("security_policy"))
|
|
477
|
+
return { scenario_id: "release-scenario-decision-workshop-combined-intent", status: "fail", error: `expected security_policy in activated_packs, got ${JSON.stringify(activatedPacks)}` };
|
|
478
|
+
// 验证 decision_workshop 同时包含两个包的域
|
|
479
|
+
const genericWorkshop = confirmDecisionWorkshopFromResponse(exp1);
|
|
480
|
+
if (!genericWorkshop)
|
|
481
|
+
return { scenario_id: "release-scenario-decision-workshop-combined-intent", status: "fail", error: "no decision_workshop in response" };
|
|
482
|
+
const hasDataMigration = "data_migration" in (genericWorkshop.extended_domains ?? {});
|
|
483
|
+
const hasSecurityPolicy = "security_policy" in (genericWorkshop.extended_domains ?? {});
|
|
484
|
+
if (!hasDataMigration || !hasSecurityPolicy)
|
|
485
|
+
return { scenario_id: "release-scenario-decision-workshop-combined-intent", status: "fail", error: `expected both domains in workshop, got data=${hasDataMigration} sec=${hasSecurityPolicy}` };
|
|
486
|
+
const exp2Raw = await harness.callTool("sf_expand", {
|
|
487
|
+
task_id: taskId,
|
|
488
|
+
decision_workshop: genericWorkshop,
|
|
489
|
+
});
|
|
490
|
+
const exp2 = harness.parseResult(exp2Raw);
|
|
491
|
+
if (exp2.status === "awaiting_confirmation" && exp2.decision_workshop)
|
|
492
|
+
return { scenario_id: "release-scenario-decision-workshop-combined-intent", status: "fail", error: "second expand still blocked by workshop" };
|
|
493
|
+
const trace = await harness.collectTrace();
|
|
494
|
+
return {
|
|
495
|
+
scenario_id: "release-scenario-decision-workshop-combined-intent",
|
|
496
|
+
status: "pass",
|
|
497
|
+
evidence: `combined packs=${activatedPacks.join(",")} confirmed from response, route=${cls.route_decision?.route}`,
|
|
498
|
+
production_trace: trace,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
harness.cleanup();
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
// ── 问题六十二: 设计产物包 ──
|
|
507
|
+
// 场景 11: 新项目缺设计文档 → sf_classify→sf_expand→sf_verify→DESIGN_ARTIFACT_MISSING
|
|
508
|
+
{
|
|
509
|
+
scenario_id: "release-scenario-design-pack-new-project",
|
|
510
|
+
covering_problem: "problem-62",
|
|
511
|
+
test_files: ["tests/engine/design_artifact_pack.test.ts"],
|
|
512
|
+
production_entrypoint: "sf_classify → sf_expand → sf_verify → design_artifact_pack",
|
|
513
|
+
expected_outcome: "新项目缺设计文档 → sf_verify 返回 SF-DESIGN-PACK-VERIFY-FAILED 且 findings 包含 DESIGN_ARTIFACT_MISSING",
|
|
514
|
+
runner: async () => {
|
|
515
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
516
|
+
const harness = await createToolHarness();
|
|
517
|
+
try {
|
|
518
|
+
fs.mkdirSync(path.join(harness.tmpDir, "docs", "architecture"), { recursive: true });
|
|
519
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "修复登录按钮 CSS" });
|
|
520
|
+
const cls = harness.parseResult(clsRaw);
|
|
521
|
+
const taskId = cls.task_id;
|
|
522
|
+
await harness.callTool("sf_expand", { task_id: taskId });
|
|
523
|
+
await harness.setupPlanGate(taskId);
|
|
524
|
+
const verRaw = await harness.callTool("sf_verify", {
|
|
525
|
+
task_id: taskId,
|
|
526
|
+
changed_files: ["docs/architecture/architecture.md"],
|
|
527
|
+
});
|
|
528
|
+
const ver = harness.parseResult(verRaw);
|
|
529
|
+
if (ver.diagnostic_code !== "SF-DESIGN-PACK-VERIFY-FAILED")
|
|
530
|
+
return { scenario_id: "release-scenario-design-pack-new-project", status: "fail", error: `expected SF-DESIGN-PACK-VERIFY-FAILED, got code=${ver.diagnostic_code} status=${ver.status}` };
|
|
531
|
+
const missingCodes = (ver.design_artifact_findings ?? []).filter((f) => f.code === "DESIGN_ARTIFACT_MISSING");
|
|
532
|
+
if (missingCodes.length === 0)
|
|
533
|
+
return { scenario_id: "release-scenario-design-pack-new-project", status: "fail", error: "expected DESIGN_ARTIFACT_MISSING in findings" };
|
|
534
|
+
const trace = await harness.collectTrace();
|
|
535
|
+
return {
|
|
536
|
+
scenario_id: "release-scenario-design-pack-new-project",
|
|
537
|
+
status: "pass",
|
|
538
|
+
evidence: `sf_classify→sf_expand→sf_verify SF-DESIGN-PACK-VERIFY-FAILED findings=${missingCodes.length}`,
|
|
539
|
+
production_trace: trace,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
harness.cleanup();
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
// 场景 12: API 字段表缺失 → sf_classify→sf_expand→sf_verify→API_FIELD_TABLE_INCOMPLETE
|
|
548
|
+
{
|
|
549
|
+
scenario_id: "release-scenario-design-pack-api-field-incomplete",
|
|
550
|
+
covering_problem: "problem-62",
|
|
551
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
552
|
+
production_entrypoint: "sf_classify → sf_expand → sf_verify → design_artifact_pack",
|
|
553
|
+
expected_outcome: "API 文档含 endpoint 但缺字段表 → sf_verify findings 包含 API_FIELD_TABLE_INCOMPLETE",
|
|
554
|
+
runner: async () => {
|
|
555
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
556
|
+
const harness = await createToolHarness();
|
|
557
|
+
try {
|
|
558
|
+
createCompleteDesignFixture(harness.tmpDir);
|
|
559
|
+
// 替换 API 文档为含 endpoint 但缺字段表的版本
|
|
560
|
+
fs.writeFileSync(path.join(harness.tmpDir, "docs", "architecture", "03-API接口规格文档.md"), "# 接口概览\n\n用户管理 API。\n\n## GET /api/users\n\n获取用户列表。\n\n## POST /api/users\n\n创建用户。\n", "utf-8");
|
|
561
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "修复登录按钮 CSS" });
|
|
562
|
+
const cls = harness.parseResult(clsRaw);
|
|
563
|
+
const taskId = cls.task_id;
|
|
564
|
+
await harness.callTool("sf_expand", { task_id: taskId });
|
|
565
|
+
await harness.setupPlanGate(taskId);
|
|
566
|
+
const verRaw = await harness.callTool("sf_verify", {
|
|
567
|
+
task_id: taskId,
|
|
568
|
+
changed_files: ["docs/architecture/03-API接口规格文档.md"],
|
|
569
|
+
});
|
|
570
|
+
const ver = harness.parseResult(verRaw);
|
|
571
|
+
const allFindings = (ver.design_artifact_findings ?? []);
|
|
572
|
+
const apiFinding = allFindings.find((f) => f.code === "API_FIELD_TABLE_INCOMPLETE");
|
|
573
|
+
if (!apiFinding)
|
|
574
|
+
return { scenario_id: "release-scenario-design-pack-api-field-incomplete", status: "fail", error: `expected API_FIELD_TABLE_INCOMPLETE, got codes: ${allFindings.map((f) => f.code).join(",")}` };
|
|
575
|
+
const trace = await harness.collectTrace();
|
|
576
|
+
return {
|
|
577
|
+
scenario_id: "release-scenario-design-pack-api-field-incomplete",
|
|
578
|
+
status: "pass",
|
|
579
|
+
evidence: `sf_classify→sf_expand→sf_verify API_FIELD_TABLE_INCOMPLETE found`,
|
|
580
|
+
production_trace: trace,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
harness.cleanup();
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
// 场景 13: SQL 权威缺失 → sf_classify→sf_expand→sf_verify→SQL_AUTHORITY_MISSING
|
|
589
|
+
{
|
|
590
|
+
scenario_id: "release-scenario-design-pack-sql-authority-missing",
|
|
591
|
+
covering_problem: "problem-62",
|
|
592
|
+
test_files: ["tests/engine/design_artifact_pack.test.ts"],
|
|
593
|
+
production_entrypoint: "sf_classify → sf_expand → sf_verify → design_artifact_pack",
|
|
594
|
+
expected_outcome: "完整 fixture 但无 SQL 文件 → sf_verify findings 包含 SQL_AUTHORITY_MISSING",
|
|
595
|
+
runner: async () => {
|
|
596
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
597
|
+
const harness = await createToolHarness();
|
|
598
|
+
try {
|
|
599
|
+
createCompleteDesignFixture(harness.tmpDir);
|
|
600
|
+
// 删除 SQL 文件触发 SQL_AUTHORITY_MISSING
|
|
601
|
+
fs.rmSync(path.join(harness.tmpDir, "db", "migrations", "001-init.sql"));
|
|
602
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "修复登录按钮 CSS" });
|
|
603
|
+
const cls = harness.parseResult(clsRaw);
|
|
604
|
+
const taskId = cls.task_id;
|
|
605
|
+
await harness.callTool("sf_expand", { task_id: taskId });
|
|
606
|
+
await harness.setupPlanGate(taskId);
|
|
607
|
+
const verRaw = await harness.callTool("sf_verify", {
|
|
608
|
+
task_id: taskId,
|
|
609
|
+
changed_files: ["docs/architecture/02-数据库设计文档.md"],
|
|
610
|
+
});
|
|
611
|
+
const ver = harness.parseResult(verRaw);
|
|
612
|
+
const allFindings = (ver.design_artifact_findings ?? []);
|
|
613
|
+
const sqlFinding = allFindings.find((f) => f.code === "SQL_AUTHORITY_MISSING");
|
|
614
|
+
if (!sqlFinding)
|
|
615
|
+
return { scenario_id: "release-scenario-design-pack-sql-authority-missing", status: "fail", error: `expected SQL_AUTHORITY_MISSING, got codes: ${allFindings.map((f) => f.code).join(",")}` };
|
|
616
|
+
const trace = await harness.collectTrace();
|
|
617
|
+
return {
|
|
618
|
+
scenario_id: "release-scenario-design-pack-sql-authority-missing",
|
|
619
|
+
status: "pass",
|
|
620
|
+
evidence: `sf_classify→sf_expand→sf_verify SQL_AUTHORITY_MISSING found`,
|
|
621
|
+
production_trace: trace,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
harness.cleanup();
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
// 场景 14: PostgreSQL 部分唯一约束非法 → sf_classify→sf_expand→sf_verify→POSTGRES_PARTIAL_UNIQUE_INVALID
|
|
630
|
+
{
|
|
631
|
+
scenario_id: "release-scenario-design-pack-postgres-partial-unique",
|
|
632
|
+
covering_problem: "problem-62",
|
|
633
|
+
test_files: ["tests/engine/design_artifact_pack.test.ts"],
|
|
634
|
+
production_entrypoint: "sf_classify → sf_expand → sf_verify → design_artifact_pack",
|
|
635
|
+
expected_outcome: "SQL 含 CONSTRAINT ... UNIQUE (...) WHERE → sf_verify findings 包含 POSTGRES_PARTIAL_UNIQUE_INVALID",
|
|
636
|
+
runner: async () => {
|
|
637
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
638
|
+
const harness = await createToolHarness();
|
|
639
|
+
try {
|
|
640
|
+
createCompleteDesignFixture(harness.tmpDir);
|
|
641
|
+
// 替换 SQL 为含非法约束的版本
|
|
642
|
+
fs.writeFileSync(path.join(harness.tmpDir, "db", "migrations", "001-init.sql"), "CREATE TABLE users (id UUID PRIMARY KEY, email TEXT NOT NULL, CONSTRAINT uq_users_email UNIQUE (email) WHERE active = true);\n", "utf-8");
|
|
643
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "修复登录按钮 CSS" });
|
|
644
|
+
const cls = harness.parseResult(clsRaw);
|
|
645
|
+
const taskId = cls.task_id;
|
|
646
|
+
await harness.callTool("sf_expand", { task_id: taskId });
|
|
647
|
+
await harness.setupPlanGate(taskId);
|
|
648
|
+
const verRaw = await harness.callTool("sf_verify", {
|
|
649
|
+
task_id: taskId,
|
|
650
|
+
changed_files: ["db/migrations/001-init.sql"],
|
|
651
|
+
});
|
|
652
|
+
const ver = harness.parseResult(verRaw);
|
|
653
|
+
const allFindings = (ver.design_artifact_findings ?? []);
|
|
654
|
+
const pgFinding = allFindings.find((f) => f.code === "POSTGRES_PARTIAL_UNIQUE_INVALID");
|
|
655
|
+
if (!pgFinding)
|
|
656
|
+
return { scenario_id: "release-scenario-design-pack-postgres-partial-unique", status: "fail", error: `expected POSTGRES_PARTIAL_UNIQUE_INVALID, got codes: ${allFindings.map((f) => f.code).join(",")}` };
|
|
657
|
+
const trace = await harness.collectTrace();
|
|
658
|
+
return {
|
|
659
|
+
scenario_id: "release-scenario-design-pack-postgres-partial-unique",
|
|
660
|
+
status: "pass",
|
|
661
|
+
evidence: `sf_classify→sf_expand→sf_verify POSTGRES_PARTIAL_UNIQUE_INVALID found`,
|
|
662
|
+
production_trace: trace,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
harness.cleanup();
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
// 场景 15: 跨文档响应码冲突 → sf_classify→sf_expand→sf_verify→CROSS_DOC_RESPONSE_CODE_CONFLICT
|
|
671
|
+
{
|
|
672
|
+
scenario_id: "release-scenario-design-pack-cross-doc-conflict",
|
|
673
|
+
covering_problem: "problem-62",
|
|
674
|
+
test_files: ["tests/engine/design_artifact_pack.test.ts"],
|
|
675
|
+
production_entrypoint: "sf_classify → sf_expand → sf_verify → design_artifact_pack",
|
|
676
|
+
expected_outcome: "架构文档含 code:200 但 API 文档含 code:0 → sf_verify findings 包含 CROSS_DOC_RESPONSE_CODE_CONFLICT",
|
|
677
|
+
runner: async () => {
|
|
678
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
679
|
+
const harness = await createToolHarness();
|
|
680
|
+
try {
|
|
681
|
+
createCompleteDesignFixture(harness.tmpDir);
|
|
682
|
+
// 替换架构文档: 含 "code": 200
|
|
683
|
+
fs.writeFileSync(path.join(harness.tmpDir, "docs", "architecture", "01-架构设计文档.md"), "# 架构设计\n\n## 系统概述\n\n微服务。统一响应格式: {\"code\": 200, \"data\": {}}。\n\n## 架构决策\n\n微服务\n\n## 技术选型\n\nTS\n\n## 模块设计\n\n模块\n", "utf-8");
|
|
684
|
+
// 替换 API 文档: 含 "code": 0
|
|
685
|
+
fs.writeFileSync(path.join(harness.tmpDir, "docs", "architecture", "03-API接口规格文档.md"), "# 接口概览\n\n响应格式: {\"code\": 0, \"msg\": \"\", \"data\": {}}。\n\n## GET /api/users\n\n### 请求字段表\n\n| 请求字段名 | 类型 | 必填 | 来源 | 说明 | 示例 | 校验规则 | 错误语义 |\n|--------|------|------|------|------|------|----------|----------|\n| page | int | 否 | query | 页码 | 1 | >=1 | 页码无效 |\n\n### 响应字段表\n\n| 响应字段名 | 类型 | 必填 | 来源 | 说明 | 示例 | 校验规则 | 错误语义 |\n|--------|------|------|------|------|------|----------|----------|\n| id | string | 是 | db | 用户ID | usr-1 | uuid | 用户不存在 |\n", "utf-8");
|
|
686
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "修复登录按钮 CSS" });
|
|
687
|
+
const cls = harness.parseResult(clsRaw);
|
|
688
|
+
const taskId = cls.task_id;
|
|
689
|
+
await harness.callTool("sf_expand", { task_id: taskId });
|
|
690
|
+
await harness.setupPlanGate(taskId);
|
|
691
|
+
const verRaw = await harness.callTool("sf_verify", {
|
|
692
|
+
task_id: taskId,
|
|
693
|
+
changed_files: ["docs/architecture/01-架构设计文档.md", "docs/architecture/03-API接口规格文档.md"],
|
|
694
|
+
});
|
|
695
|
+
const ver = harness.parseResult(verRaw);
|
|
696
|
+
const allFindings = (ver.design_artifact_findings ?? []);
|
|
697
|
+
const crossDocFinding = allFindings.find((f) => f.code === "CROSS_DOC_RESPONSE_CODE_CONFLICT");
|
|
698
|
+
if (!crossDocFinding)
|
|
699
|
+
return { scenario_id: "release-scenario-design-pack-cross-doc-conflict", status: "fail", error: `expected CROSS_DOC_RESPONSE_CODE_CONFLICT, got codes: ${allFindings.map((f) => f.code).join(",")}` };
|
|
700
|
+
const trace = await harness.collectTrace();
|
|
701
|
+
return {
|
|
702
|
+
scenario_id: "release-scenario-design-pack-cross-doc-conflict",
|
|
703
|
+
status: "pass",
|
|
704
|
+
evidence: `sf_classify→sf_expand→sf_verify CROSS_DOC_RESPONSE_CODE_CONFLICT found`,
|
|
705
|
+
production_trace: trace,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
harness.cleanup();
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
// 场景 16: 低风险不触发设计产物包
|
|
714
|
+
{
|
|
715
|
+
scenario_id: "release-scenario-design-pack-low-risk-skip",
|
|
716
|
+
covering_problem: "problem-62",
|
|
717
|
+
test_files: ["tests/adapters/architecture_design_workshop_mainpath.test.ts"],
|
|
718
|
+
production_entrypoint: "sf_classify",
|
|
719
|
+
expected_outcome: "低风险 CSS 修复 route 非 artifact_generation,不触发设计产物包",
|
|
720
|
+
runner: async () => {
|
|
721
|
+
const { classify } = await import("./classifier.js");
|
|
722
|
+
const result = classify({ intent: "修复 CSS 样式问题" });
|
|
723
|
+
const route = result.route_decision?.route ?? "";
|
|
724
|
+
if (route === "artifact_generation")
|
|
725
|
+
return { scenario_id: "release-scenario-design-pack-low-risk-skip", status: "fail", error: `CSS fix should not be artifact_generation, got ${route}` };
|
|
726
|
+
return {
|
|
727
|
+
scenario_id: "release-scenario-design-pack-low-risk-skip",
|
|
728
|
+
status: "pass",
|
|
729
|
+
evidence: `route=${route} risk=${result.risk}`,
|
|
730
|
+
production_trace: { tool_entrypoint: "sf_classify", diagnostic_codes: [`route:${route}`, `risk:${result.risk}`], gates_consumed: ["router"] },
|
|
731
|
+
};
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
// ── 问题六十三: 标准资产契约 ──
|
|
735
|
+
// 场景 17: 13 必填字段
|
|
736
|
+
{
|
|
737
|
+
scenario_id: "release-scenario-template-contract-api-field-table",
|
|
738
|
+
covering_problem: "problem-63",
|
|
739
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
740
|
+
production_entrypoint: "template_asset_contract_registry.buildTemplateAssetContracts",
|
|
741
|
+
expected_outcome: "所有合同 13 个必填字段全部存在且非 undefined",
|
|
742
|
+
runner: async () => {
|
|
743
|
+
const { buildTemplateAssetContracts } = await import("./template_asset_contract_registry.js");
|
|
744
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
745
|
+
const requiredFields = ["path", "asset_kind", "contract_version", "owner_mechanism_id", "asset_visibility", "sync_to_user_project", "sync_target", "runtime_visibility", "evidence_role", "user_visible", "lifecycle_status", "consume_category", "validation_entrypoint"];
|
|
746
|
+
const missing = contracts.flatMap((c) => requiredFields.filter((f) => !(f in c) || c[f] === undefined).map((f) => `${c.path}: ${f}`));
|
|
747
|
+
if (missing.length > 0)
|
|
748
|
+
return { scenario_id: "release-scenario-template-contract-api-field-table", status: "fail", error: missing.join("; ") };
|
|
749
|
+
return {
|
|
750
|
+
scenario_id: "release-scenario-template-contract-api-field-table",
|
|
751
|
+
status: "pass",
|
|
752
|
+
evidence: `${contracts.length} contracts all have ${requiredFields.length} required fields`,
|
|
753
|
+
production_trace: { tool_entrypoint: "buildTemplateAssetContracts", diagnostic_codes: [`contracts:${contracts.length}`], gates_consumed: ["contract_gate"] },
|
|
754
|
+
};
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
// 场景 18: 权威绑定全量覆盖
|
|
758
|
+
{
|
|
759
|
+
scenario_id: "release-scenario-template-contract-authority-binding",
|
|
760
|
+
covering_problem: "problem-63",
|
|
761
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
762
|
+
production_entrypoint: "template_asset_contract_registry.auditContractCoverage",
|
|
763
|
+
expected_outcome: "unregistered_files.length === 0(全量覆盖)",
|
|
764
|
+
runner: async () => {
|
|
765
|
+
const { auditContractCoverage } = await import("./template_asset_contract_registry.js");
|
|
766
|
+
const report = auditContractCoverage(process.cwd());
|
|
767
|
+
if (report.unregistered_files.length > 0)
|
|
768
|
+
return { scenario_id: "release-scenario-template-contract-authority-binding", status: "fail", error: `unregistered: ${report.unregistered_files.join(", ")}` };
|
|
769
|
+
return {
|
|
770
|
+
scenario_id: "release-scenario-template-contract-authority-binding",
|
|
771
|
+
status: "pass",
|
|
772
|
+
evidence: `${report.total_contracts} contracts, 0 unregistered`,
|
|
773
|
+
production_trace: { tool_entrypoint: "auditContractCoverage", diagnostic_codes: [`total:${report.total_contracts}`, `unregistered:${report.unregistered_files.length}`], gates_consumed: ["contract_gate"] },
|
|
774
|
+
};
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
// 场景 19: lifecycle_status 合法
|
|
778
|
+
{
|
|
779
|
+
scenario_id: "release-scenario-template-contract-existing-doc-upgrade",
|
|
780
|
+
covering_problem: "problem-63",
|
|
781
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
782
|
+
production_entrypoint: "template_asset_contract_registry.buildTemplateAssetContracts",
|
|
783
|
+
expected_outcome: "所有合同 lifecycle_status=active 或 deprecated/internal_only,版本升级路径不遗漏",
|
|
784
|
+
runner: async () => {
|
|
785
|
+
const { buildTemplateAssetContracts } = await import("./template_asset_contract_registry.js");
|
|
786
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
787
|
+
const validStatuses = ["active", "deprecated", "internal_only"];
|
|
788
|
+
const invalid = contracts.filter((c) => !validStatuses.includes(c.lifecycle_status));
|
|
789
|
+
if (invalid.length > 0)
|
|
790
|
+
return { scenario_id: "release-scenario-template-contract-existing-doc-upgrade", status: "fail", error: `invalid lifecycle: ${invalid.map((c) => `${c.path}: ${c.lifecycle_status}`).join(", ")}` };
|
|
791
|
+
const activeCount = contracts.filter((c) => c.lifecycle_status === "active").length;
|
|
792
|
+
if (activeCount === 0)
|
|
793
|
+
return { scenario_id: "release-scenario-template-contract-existing-doc-upgrade", status: "fail", error: "no active contracts" };
|
|
794
|
+
return {
|
|
795
|
+
scenario_id: "release-scenario-template-contract-existing-doc-upgrade",
|
|
796
|
+
status: "pass",
|
|
797
|
+
evidence: `${activeCount} active, ${contracts.length - activeCount} non-active`,
|
|
798
|
+
production_trace: { tool_entrypoint: "buildTemplateAssetContracts", diagnostic_codes: [`active:${activeCount}`, `total:${contracts.length}`], gates_consumed: ["contract_gate"] },
|
|
799
|
+
};
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
// 场景 20: 决策一致性
|
|
803
|
+
{
|
|
804
|
+
scenario_id: "release-scenario-template-contract-conflict",
|
|
805
|
+
covering_problem: "problem-63",
|
|
806
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
807
|
+
production_entrypoint: "template_asset_contract_registry.getContractDecision",
|
|
808
|
+
expected_outcome: "同一 path 两次 getContractDecision 结果一致(0 conflicts)",
|
|
809
|
+
runner: async () => {
|
|
810
|
+
const { buildTemplateAssetContracts, getContractDecision } = await import("./template_asset_contract_registry.js");
|
|
811
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
812
|
+
let conflicts = 0;
|
|
813
|
+
const conflictPaths = [];
|
|
814
|
+
for (const c of contracts) {
|
|
815
|
+
const d1 = getContractDecision(c.path);
|
|
816
|
+
const d2 = getContractDecision(c.path);
|
|
817
|
+
if (d1.decision !== d2.decision) {
|
|
818
|
+
conflicts++;
|
|
819
|
+
conflictPaths.push(`${c.path}: ${d1.decision} vs ${d2.decision}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (conflicts > 0)
|
|
823
|
+
return { scenario_id: "release-scenario-template-contract-conflict", status: "fail", error: conflictPaths.join("; ") };
|
|
824
|
+
return {
|
|
825
|
+
scenario_id: "release-scenario-template-contract-conflict",
|
|
826
|
+
status: "pass",
|
|
827
|
+
evidence: `${contracts.length} contracts, 0 decision conflicts`,
|
|
828
|
+
production_trace: { tool_entrypoint: "getContractDecision", diagnostic_codes: [`contracts:${contracts.length}`, `conflicts:${conflicts}`], gates_consumed: ["contract_gate"] },
|
|
829
|
+
};
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
// 场景 21: 非 active 合同不出现在 syncable
|
|
833
|
+
{
|
|
834
|
+
scenario_id: "release-scenario-template-contract-draft-skip",
|
|
835
|
+
covering_problem: "problem-63",
|
|
836
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
837
|
+
production_entrypoint: "template_asset_contract_registry.buildTemplateAssetContracts + getSyncableAssetPaths",
|
|
838
|
+
expected_outcome: "非 active 合同(deprecated/internal_only)存在且不出现在 syncable 列表",
|
|
839
|
+
runner: async () => {
|
|
840
|
+
const { buildTemplateAssetContracts, getSyncableAssetPaths } = await import("./template_asset_contract_registry.js");
|
|
841
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
842
|
+
const syncable = new Set(getSyncableAssetPaths());
|
|
843
|
+
const nonActive = contracts.filter((c) => c.lifecycle_status !== "active");
|
|
844
|
+
if (nonActive.length === 0)
|
|
845
|
+
return { scenario_id: "release-scenario-template-contract-draft-skip", status: "fail", error: "no non-active contracts to verify skip behavior" };
|
|
846
|
+
const leakedNonActive = nonActive.filter((c) => syncable.has(c.path) && c.asset_visibility === "soloforge_internal");
|
|
847
|
+
if (leakedNonActive.length > 0)
|
|
848
|
+
return { scenario_id: "release-scenario-template-contract-draft-skip", status: "fail", error: `internal assets in syncable: ${leakedNonActive.map((c) => c.path).join(", ")}` };
|
|
849
|
+
return {
|
|
850
|
+
scenario_id: "release-scenario-template-contract-draft-skip",
|
|
851
|
+
status: "pass",
|
|
852
|
+
evidence: `${nonActive.length} non-active, 0 leaked to syncable`,
|
|
853
|
+
production_trace: { tool_entrypoint: "getSyncableAssetPaths", diagnostic_codes: [`nonActive:${nonActive.length}`, `syncable:${syncable.size}`], gates_consumed: ["contract_gate"] },
|
|
854
|
+
};
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
// 场景 22: 低风险意图路由
|
|
858
|
+
{
|
|
859
|
+
scenario_id: "release-scenario-template-contract-low-risk-skip",
|
|
860
|
+
covering_problem: "problem-63",
|
|
861
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
862
|
+
production_entrypoint: "sf_classify",
|
|
863
|
+
expected_outcome: "低风险意图 route 为 code_change/operation/skip_chat",
|
|
864
|
+
runner: async () => {
|
|
865
|
+
const { classify } = await import("./classifier.js");
|
|
866
|
+
const result = classify({ intent: "添加一行日志" });
|
|
867
|
+
const route = result.route_decision?.route ?? "";
|
|
868
|
+
const lowRiskRoutes = ["code_change", "operation", "skip_chat"];
|
|
869
|
+
if (!lowRiskRoutes.includes(route))
|
|
870
|
+
return { scenario_id: "release-scenario-template-contract-low-risk-skip", status: "fail", error: `expected low-risk route, got ${route}` };
|
|
871
|
+
return {
|
|
872
|
+
scenario_id: "release-scenario-template-contract-low-risk-skip",
|
|
873
|
+
status: "pass",
|
|
874
|
+
evidence: `route=${route}`,
|
|
875
|
+
production_trace: { tool_entrypoint: "sf_classify", diagnostic_codes: [`route:${route}`], gates_consumed: ["router"] },
|
|
876
|
+
};
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
// 场景 22b: 一个任务内证明正式 API 产物从草稿阻断,经修复重验和人工确认后才能交付。
|
|
880
|
+
{
|
|
881
|
+
scenario_id: "release-scenario-template-contract-repair-directive",
|
|
882
|
+
covering_problem: "problem-63",
|
|
883
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
884
|
+
production_entrypoint: "sf_classify→sf_expand(api_spec artifact)→sf_verify(draft)→sf_deliver(SF-CONTRACT-0003)→sf_learn→sf_verify(clear)→sf_record_verification_execution→sf_accept(confirm)→sf_deliver",
|
|
885
|
+
expected_outcome: "同一正式 API 产物与同一 task_id 在修复前阻断,修复重验及显式人工确认后 status=accepted 并可交付",
|
|
886
|
+
runner: async () => {
|
|
887
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
888
|
+
const intent = "根据 docs/prd.md 生成接口设计";
|
|
889
|
+
const harness = await createToolHarness({ techStack: { backend: { lang: "none", framework: "none", version: "0" }, frontend: { lang: "none", framework: "none", version: "0" } } });
|
|
890
|
+
try {
|
|
891
|
+
const sourceDocPath = path.join(harness.tmpDir, "docs", "prd.md");
|
|
892
|
+
fs.mkdirSync(path.dirname(sourceDocPath), { recursive: true });
|
|
893
|
+
fs.writeFileSync(sourceDocPath, "# 用户列表需求\n\n提供用户列表查询接口,支持分页并返回用户 ID。\n", "utf-8");
|
|
894
|
+
const cls = harness.parseResult(await harness.callTool("sf_classify", { intent }));
|
|
895
|
+
const taskId = cls.task_id;
|
|
896
|
+
if (!taskId)
|
|
897
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
898
|
+
const exp = harness.parseResult(await harness.callTool("sf_expand", {
|
|
899
|
+
task_id: taskId,
|
|
900
|
+
input_material_confirmations: ["docs/prd.md"],
|
|
901
|
+
}));
|
|
902
|
+
if (exp.status === "awaiting_confirmation")
|
|
903
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: "unexpected awaiting_confirmation" };
|
|
904
|
+
const tc = harness.taskContext;
|
|
905
|
+
const expandedCtx = await tc.load(taskId);
|
|
906
|
+
const artifactPath = expandedCtx?.artifact_output?.path;
|
|
907
|
+
if (!artifactPath || expandedCtx?.artifact_output?.kind !== "api_spec")
|
|
908
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `sf_expand 未创建正式 api_spec artifact_output: ${exp.error ?? exp.status ?? JSON.stringify(exp).slice(0, 180)}` };
|
|
909
|
+
const artifactAbs = path.join(harness.tmpDir, artifactPath);
|
|
910
|
+
fs.mkdirSync(path.dirname(artifactAbs), { recursive: true });
|
|
911
|
+
const openApiAbs = path.join(harness.tmpDir, "docs", "api", "openapi.yaml");
|
|
912
|
+
fs.mkdirSync(path.dirname(openApiAbs), { recursive: true });
|
|
913
|
+
fs.writeFileSync(openApiAbs, [
|
|
914
|
+
"openapi: 3.0.0", "info:", " title: Users API", " version: '1.0'",
|
|
915
|
+
"paths:", " /users:", " get:", " responses:", " '200':",
|
|
916
|
+
" description: OK",
|
|
917
|
+
].join("\n"), "utf-8");
|
|
918
|
+
const compliantApiContent = [
|
|
919
|
+
"# API 接口规格", "## 接口概览", "提供分页用户列表查询。", "## 请求响应规范",
|
|
920
|
+
"### GET /users", "#### 请求字段表",
|
|
921
|
+
"| 请求字段名 | 类型 | 必填 | 来源 | 说明 | 示例 | 校验规则 | 错误语义 |",
|
|
922
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
923
|
+
"| page | number | 否 | query | 页码 | 1 | >= 1 | 页码无效 |",
|
|
924
|
+
"#### 响应字段表",
|
|
925
|
+
"| 响应字段名 | 类型 | 必填 | 来源 | 说明 | 示例 | 校验规则 | 错误语义 |",
|
|
926
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
927
|
+
"| data | array | 是 | response | 用户列表 | [] | array | 无 |",
|
|
928
|
+
].join("\n");
|
|
929
|
+
fs.writeFileSync(artifactAbs, `draft\n\n${compliantApiContent}`, "utf-8");
|
|
930
|
+
await harness.setupPlanGate(taskId);
|
|
931
|
+
const ver1 = harness.parseResult(await harness.callTool("sf_verify", { task_id: taskId, changed_files: [artifactPath], confirm: true }));
|
|
932
|
+
if (ver1.diagnostic_code !== "SF-CONTRACT-DRAFT")
|
|
933
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `expected SF-CONTRACT-DRAFT, got ${ver1.diagnostic_code ?? "none"}: ${ver1.error ?? JSON.stringify(ver1).slice(0, 240)}` };
|
|
934
|
+
const ctx1 = await tc.load(taskId);
|
|
935
|
+
if (!ctx1?.repair_reverify_directive?.blocked)
|
|
936
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: "directive not blocked" };
|
|
937
|
+
const blockedDeliver = harness.parseResult(await harness.callTool("sf_deliver", { task_id: taskId, confirm: true }));
|
|
938
|
+
if (blockedDeliver.diagnostic_code !== "SF-CONTRACT-0003")
|
|
939
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `expected delivery SF-CONTRACT-0003, got ${blockedDeliver.diagnostic_code ?? "none"}` };
|
|
940
|
+
await harness.callTool("sf_learn", { task_id: taskId, result: "failure", failure_type: "integration" });
|
|
941
|
+
fs.writeFileSync(artifactAbs, compliantApiContent, "utf-8");
|
|
942
|
+
const ver2 = harness.parseResult(await harness.callTool("sf_verify", { task_id: taskId, changed_files: [artifactPath], confirm: true }));
|
|
943
|
+
const ctx2 = await tc.load(taskId);
|
|
944
|
+
if (ctx2?.repair_reverify_directive?.blocked)
|
|
945
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: "directive not cleared after fix" };
|
|
946
|
+
if (ver2.diagnostic_code === "SF-CONTRACT-DRAFT" || ver2.diagnostic_code === "SF-CONTRACT-0003")
|
|
947
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `sf_verify still failing, code=${ver2.diagnostic_code}` };
|
|
948
|
+
if (ctx2?.artifact_output?.status !== "verified")
|
|
949
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `artifact did not reach verified: ${ctx2?.artifact_output?.status}; verification=${JSON.stringify(ver2.artifact_verification ?? ver2.artifact_warning ?? ver2).slice(0, 300)}` };
|
|
950
|
+
const ctxForV = await tc.load(taskId);
|
|
951
|
+
if (!ctxForV?.verification_plan)
|
|
952
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: "no verification_plan after sf_verify" };
|
|
953
|
+
await harness.callTool("sf_record_verification_execution", {
|
|
954
|
+
task_id: taskId,
|
|
955
|
+
plan_id: ctxForV.verification_plan.plan_id,
|
|
956
|
+
execution_records: ctxForV.verification_plan.commands.map((cmd, i) => ({
|
|
957
|
+
command: cmd, exit_code: 0,
|
|
958
|
+
stdout_hash: "a".repeat(64), stderr_hash: "e".repeat(64),
|
|
959
|
+
started_at: new Date().toISOString(), finished_at: new Date().toISOString(),
|
|
960
|
+
evidence_id: `evidence-${taskId}-${i}`,
|
|
961
|
+
})),
|
|
962
|
+
});
|
|
963
|
+
const accept = harness.parseResult(await harness.callTool("sf_accept", {
|
|
964
|
+
task_id: taskId, confirm: true, confirmed_by: "release-reviewer",
|
|
965
|
+
confirmation_ref: `api-artifact:${taskId}`, acceptance_notes: "API 字段表与响应结构已人工确认",
|
|
966
|
+
run_mode: "not_applicable",
|
|
967
|
+
}));
|
|
968
|
+
if (accept.artifact_transition !== "accepted")
|
|
969
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `artifact acceptance failed: ${accept.error ?? accept.artifact_transition}` };
|
|
970
|
+
const del = harness.parseResult(await harness.callTool("sf_deliver", { task_id: taskId, confirm: true }));
|
|
971
|
+
if (del.diagnostic_code)
|
|
972
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `sf_deliver still blocked: ${del.diagnostic_code}, error=${del.error ?? "none"}` };
|
|
973
|
+
const delObs = harness.getObservations().slice().reverse().find(o => o.tool_name === "sf_deliver");
|
|
974
|
+
if (!delObs || delObs.is_error)
|
|
975
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: `sf_deliver observation error: ${delObs?.parsed_response?.error ?? "no obs"}` };
|
|
976
|
+
const finalCtx = await tc.load(taskId);
|
|
977
|
+
if (!finalCtx)
|
|
978
|
+
return { scenario_id: "release-scenario-template-contract-repair-directive", status: "fail", error: "task context lost after delivery" };
|
|
979
|
+
const trace = await harness.collectTrace();
|
|
980
|
+
return {
|
|
981
|
+
scenario_id: "release-scenario-template-contract-repair-directive",
|
|
982
|
+
status: "pass",
|
|
983
|
+
evidence: `task=${taskId} artifact=${finalCtx.artifact_output?.status} directive_cleared=${!finalCtx.repair_reverify_directive} delivered=${!delObs.is_error}`,
|
|
984
|
+
production_trace: trace,
|
|
985
|
+
behavioral_proof: {
|
|
986
|
+
task_id: taskId, blocked_task_id: taskId, delivered_task_id: taskId,
|
|
987
|
+
contract_blocked: blockedDeliver.diagnostic_code === "SF-CONTRACT-0003",
|
|
988
|
+
repair_directive_cleared: !finalCtx.repair_reverify_directive,
|
|
989
|
+
artifact_status: finalCtx.artifact_output?.status,
|
|
990
|
+
acceptance_confirmation_ref: `api-artifact:${taskId}`,
|
|
991
|
+
delivery_succeeded: !delObs.is_error,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
finally {
|
|
996
|
+
harness.cleanup();
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
// ── 问题六十四: 模板可见性 ──
|
|
1001
|
+
// 场景 23: internal_runtime 资产全部 validation_result=pass
|
|
1002
|
+
{
|
|
1003
|
+
scenario_id: "release-scenario-template-visibility-fresh-init",
|
|
1004
|
+
covering_problem: "problem-64",
|
|
1005
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1006
|
+
production_entrypoint: "observed_consumption.observeAllConsumption",
|
|
1007
|
+
expected_outcome: "internal_runtime 资产全部 validation_result=pass(无 visibility fail)",
|
|
1008
|
+
runner: async () => {
|
|
1009
|
+
const { observeAllConsumption } = await import("./observed_consumption.js");
|
|
1010
|
+
const report = await observeAllConsumption(process.cwd());
|
|
1011
|
+
const visFails = report.observations.filter((o) => o.consume_category === "internal_runtime" && o.validation_result === "fail");
|
|
1012
|
+
if (visFails.length > 0)
|
|
1013
|
+
return { scenario_id: "release-scenario-template-visibility-fresh-init", status: "fail", error: `${visFails.length} internal_runtime visibility fails: ${visFails.map((o) => o.asset_path).join(",")}` };
|
|
1014
|
+
return {
|
|
1015
|
+
scenario_id: "release-scenario-template-visibility-fresh-init",
|
|
1016
|
+
status: "pass",
|
|
1017
|
+
evidence: `${report.total} assets, ${report.pass} pass, 0 visibility fail`,
|
|
1018
|
+
production_trace: { tool_entrypoint: "observeAllConsumption", diagnostic_codes: [`total:${report.total}`, `pass:${report.pass}`], gates_consumed: ["contract_gate"] },
|
|
1019
|
+
};
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
// 场景 24: internal_runtime 资产全部 contract_decision=denied
|
|
1023
|
+
{
|
|
1024
|
+
scenario_id: "release-scenario-template-visibility-internal-leak",
|
|
1025
|
+
covering_problem: "problem-64",
|
|
1026
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1027
|
+
production_entrypoint: "observed_consumption.observeAllConsumption",
|
|
1028
|
+
expected_outcome: "internal_runtime 资产全部 contract_decision=denied",
|
|
1029
|
+
runner: async () => {
|
|
1030
|
+
const { observeAllConsumption } = await import("./observed_consumption.js");
|
|
1031
|
+
const report = await observeAllConsumption(process.cwd());
|
|
1032
|
+
const notDenied = report.observations.filter((o) => o.consume_category === "internal_runtime" && o.contract_decision !== "denied");
|
|
1033
|
+
if (notDenied.length > 0)
|
|
1034
|
+
return { scenario_id: "release-scenario-template-visibility-internal-leak", status: "fail", error: `not denied: ${notDenied.map((o) => o.asset_path).join(", ")}` };
|
|
1035
|
+
return {
|
|
1036
|
+
scenario_id: "release-scenario-template-visibility-internal-leak",
|
|
1037
|
+
status: "pass",
|
|
1038
|
+
evidence: "all internal_runtime denied",
|
|
1039
|
+
production_trace: { tool_entrypoint: "observeAllConsumption", diagnostic_codes: ["all_denied"], gates_consumed: ["contract_gate"] },
|
|
1040
|
+
};
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
// 场景 25: 内置模板回退 → syncable + copy
|
|
1044
|
+
{
|
|
1045
|
+
scenario_id: "release-scenario-template-visibility-builtin-fallback",
|
|
1046
|
+
covering_problem: "problem-64",
|
|
1047
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1048
|
+
production_entrypoint: "template_asset_contract_registry.getSyncableAssetPaths + template_init_sync.copyKnowledgeToProject",
|
|
1049
|
+
expected_outcome: "syncable 路径数量 > 0 且 init 后实际生成文件数 > 0",
|
|
1050
|
+
runner: async () => {
|
|
1051
|
+
const { getSyncableAssetPaths } = await import("./template_asset_contract_registry.js");
|
|
1052
|
+
const { copyKnowledgeToProject } = await import("./template_init_sync.js");
|
|
1053
|
+
const syncable = getSyncableAssetPaths();
|
|
1054
|
+
if (syncable.length === 0)
|
|
1055
|
+
return { scenario_id: "release-scenario-template-visibility-builtin-fallback", status: "fail", error: "no syncable assets" };
|
|
1056
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "soloforge-builtin-"));
|
|
1057
|
+
try {
|
|
1058
|
+
const result = await copyKnowledgeToProject(process.cwd(), tmpDir);
|
|
1059
|
+
if (result.copied === 0)
|
|
1060
|
+
return { scenario_id: "release-scenario-template-visibility-builtin-fallback", status: "fail", error: "copyKnowledgeToProject copied 0 files" };
|
|
1061
|
+
return {
|
|
1062
|
+
scenario_id: "release-scenario-template-visibility-builtin-fallback",
|
|
1063
|
+
status: "pass",
|
|
1064
|
+
evidence: `syncable=${syncable.length} copied=${result.copied}`,
|
|
1065
|
+
production_trace: { tool_entrypoint: "copyKnowledgeToProject", diagnostic_codes: [`syncable:${syncable.length}`, `copied:${result.copied}`], gates_consumed: ["sync_templates", "init"] },
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
},
|
|
1073
|
+
// 场景 26: 技术栈过滤 — 真实 sf_scaffold 工具消费真实受限资产。
|
|
1074
|
+
{
|
|
1075
|
+
scenario_id: "release-scenario-template-visibility-tech-stack",
|
|
1076
|
+
covering_problem: "problem-64",
|
|
1077
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1078
|
+
production_entrypoint: "sf_classify→sf_expand→sf_scaffold→scaffolder.generateScaffold",
|
|
1079
|
+
expected_outcome: "React 仅生成前端资产、Spring Boot 仅生成后端资产、不支持栈不生成内置脚手架",
|
|
1080
|
+
runner: async () => {
|
|
1081
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
1082
|
+
const { buildTemplateAssetContracts } = await import("./template_asset_contract_registry.js");
|
|
1083
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
1084
|
+
const restricted = contracts.filter((contract) => contract.asset_kind === "scaffold" && contract.applicable_tech_stack.length > 0);
|
|
1085
|
+
if (restricted.length !== 18)
|
|
1086
|
+
return { scenario_id: "release-scenario-template-visibility-tech-stack", status: "fail", error: `expected 18 restricted scaffold contracts, got ${restricted.length}` };
|
|
1087
|
+
const run = async (techStack) => {
|
|
1088
|
+
const harness = await createToolHarness({ techStack });
|
|
1089
|
+
try {
|
|
1090
|
+
const cls = harness.parseResult(await harness.callTool("sf_classify", { intent: "使用脚手架创建用户管理模块" }));
|
|
1091
|
+
if (!cls.task_id)
|
|
1092
|
+
throw new Error("sf_classify 未返回 task_id");
|
|
1093
|
+
await harness.callTool("sf_expand", { task_id: cls.task_id });
|
|
1094
|
+
await harness.setupPlanGate(cls.task_id);
|
|
1095
|
+
const response = harness.parseResult(await harness.callTool("sf_scaffold", { task_id: cls.task_id }));
|
|
1096
|
+
return { response, trace: await harness.collectTrace() };
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
harness.cleanup();
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const react = await run({ backend: { lang: "go", framework: "gin", version: "1" }, frontend: { lang: "typescript", framework: "react", version: "18" } });
|
|
1103
|
+
const spring = await run({ backend: { lang: "java", framework: "spring-boot", version: "3" }, frontend: { lang: "none", framework: "none", version: "0" } });
|
|
1104
|
+
const unsupported = await run({ backend: { lang: "python", framework: "django", version: "5" }, frontend: { lang: "typescript", framework: "angular", version: "18" } });
|
|
1105
|
+
const reactFiles = react.response.files ?? [];
|
|
1106
|
+
const springFiles = spring.response.files ?? [];
|
|
1107
|
+
const unsupportedFiles = unsupported.response.files ?? [];
|
|
1108
|
+
if (reactFiles.length === 0 || reactFiles.some((file) => file.path.endsWith(".java")))
|
|
1109
|
+
return { scenario_id: "release-scenario-template-visibility-tech-stack", status: "fail", error: "React 脚手架生成结果含后端 Java 资产或为空" };
|
|
1110
|
+
if (springFiles.length === 0 || springFiles.some((file) => /\.tsx?$/.test(file.path)))
|
|
1111
|
+
return { scenario_id: "release-scenario-template-visibility-tech-stack", status: "fail", error: "Spring Boot 脚手架生成结果含 React 资产或为空" };
|
|
1112
|
+
if (unsupportedFiles.length !== 0)
|
|
1113
|
+
return { scenario_id: "release-scenario-template-visibility-tech-stack", status: "fail", error: `不支持技术栈错误生成 ${unsupportedFiles.length} 个内置文件` };
|
|
1114
|
+
return {
|
|
1115
|
+
scenario_id: "release-scenario-template-visibility-tech-stack",
|
|
1116
|
+
status: "pass",
|
|
1117
|
+
evidence: `restricted=${restricted.length} react=${reactFiles.length} spring=${springFiles.length} unsupported=0`,
|
|
1118
|
+
production_trace: { tool_entrypoint: react.trace.tool_entrypoint, diagnostic_codes: [`restricted:${restricted.length}`, `react:${reactFiles.length}`, `spring:${springFiles.length}`, "unsupported:0"], gates_consumed: [...new Set([...react.trace.gates_consumed, ...spring.trace.gates_consumed])] },
|
|
1119
|
+
};
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
// 场景 27: 用户可见资产不含内部术语
|
|
1123
|
+
{
|
|
1124
|
+
scenario_id: "release-scenario-template-visibility-user-visible-terms",
|
|
1125
|
+
covering_problem: "problem-64",
|
|
1126
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1127
|
+
production_entrypoint: "template_asset_contract_registry.buildTemplateAssetContracts",
|
|
1128
|
+
expected_outcome: "user_visible=true 的资产文件必须存在且内容不含 Batch/validate-release/Registry 等内部术语;合同标记用户可见而实体文件缺失必须 fail",
|
|
1129
|
+
runner: async () => {
|
|
1130
|
+
const { buildTemplateAssetContracts } = await import("./template_asset_contract_registry.js");
|
|
1131
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
1132
|
+
const userVisible = contracts.filter((c) => c.user_visible === true && c.sync_to_user_project);
|
|
1133
|
+
if (userVisible.length === 0)
|
|
1134
|
+
return { scenario_id: "release-scenario-template-visibility-user-visible-terms", status: "fail", error: "no user_visible syncable assets" };
|
|
1135
|
+
const internalTerms = [`Batch${6}`, "validate-release", "implementation_roadmap", `BATCH${6}_SCENARIO`];
|
|
1136
|
+
const violations = [];
|
|
1137
|
+
for (const c of userVisible) {
|
|
1138
|
+
const absPath = path.join(process.cwd(), c.path);
|
|
1139
|
+
// 合同标记用户可见而实体文件缺失必须 fail(不得 continue 跳过)
|
|
1140
|
+
if (!fs.existsSync(absPath))
|
|
1141
|
+
return { scenario_id: "release-scenario-template-visibility-user-visible-terms", status: "fail", error: `用户可见合同 ${c.path} 标记 sync_to_user_project 但文件不存在: ${absPath}` };
|
|
1142
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
1143
|
+
for (const term of internalTerms) {
|
|
1144
|
+
if (content.includes(term))
|
|
1145
|
+
violations.push(`${c.path}: ${term}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (violations.length > 0)
|
|
1149
|
+
return { scenario_id: "release-scenario-template-visibility-user-visible-terms", status: "fail", error: `internal terms found: ${violations.join("; ")}` };
|
|
1150
|
+
return {
|
|
1151
|
+
scenario_id: "release-scenario-template-visibility-user-visible-terms",
|
|
1152
|
+
status: "pass",
|
|
1153
|
+
evidence: `${userVisible.length} user_visible assets, 0 internal term violations`,
|
|
1154
|
+
production_trace: { tool_entrypoint: "buildTemplateAssetContracts", diagnostic_codes: [`userVisible:${userVisible.length}`, `violations:0`], gates_consumed: ["contract_gate"] },
|
|
1155
|
+
};
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
// 场景 28: copy→KIM→sf_classify→sf_expand 完整链路 + index_only/injectable 验证
|
|
1159
|
+
{
|
|
1160
|
+
scenario_id: "release-scenario-template-visibility-old-project-migration",
|
|
1161
|
+
covering_problem: "problem-64",
|
|
1162
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1163
|
+
production_entrypoint: "sf_classify → sf_expand (via createToolHarness + copyKnowledge)",
|
|
1164
|
+
expected_outcome: "init 后 KIM 索引同步资产;内部资产零泄漏;至少一条 index_only 资产已索引但其独特内容未进入 prompt/matched_knowledge;injectable 资产在适用路由可被召回",
|
|
1165
|
+
runner: async () => {
|
|
1166
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
1167
|
+
const { buildTemplateAssetContracts } = await import("./template_asset_contract_registry.js");
|
|
1168
|
+
const harness = await createToolHarness({ copyKnowledge: true });
|
|
1169
|
+
try {
|
|
1170
|
+
const all = harness.knowledgeIndex.getAllEntries();
|
|
1171
|
+
// 内部资产不应被索引
|
|
1172
|
+
const leaked = all.project.find((e) => e.name?.includes("标准资产覆盖") || e.name?.includes("演进回归"));
|
|
1173
|
+
if (leaked)
|
|
1174
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: `internal asset leaked: ${leaked.name}` };
|
|
1175
|
+
// sf_classify → sf_expand 完整链路
|
|
1176
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "创建用户管理 API" });
|
|
1177
|
+
const cls = harness.parseResult(clsRaw);
|
|
1178
|
+
if (!cls.task_id)
|
|
1179
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: "sf_classify did not return task_id" };
|
|
1180
|
+
const expRaw = await harness.callTool("sf_expand", { task_id: cls.task_id });
|
|
1181
|
+
const exp = harness.parseResult(expRaw);
|
|
1182
|
+
if (exp.degraded)
|
|
1183
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: `sf_expand degraded: ${exp.prompt?.slice(0, 200)}` };
|
|
1184
|
+
// 验证 domain 资产被索引
|
|
1185
|
+
const domainEntry = all.project.find((e) => { const n = e.file_path?.replace(/\\/g, "/") ?? ""; return n.includes("domain/"); });
|
|
1186
|
+
if (!domainEntry)
|
|
1187
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: "no domain entries indexed" };
|
|
1188
|
+
// 验证 index_only 资产已索引但其独特内容不进入 prompt/matched_knowledge
|
|
1189
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
1190
|
+
const indexOnlyContracts = contracts.filter((c) => c.runtime_visibility === "index_only");
|
|
1191
|
+
if (indexOnlyContracts.length === 0)
|
|
1192
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: "no index_only contracts found" };
|
|
1193
|
+
// 找一条已索引的 index_only 资产
|
|
1194
|
+
const indexedIndexOnly = indexOnlyContracts.find((c) => {
|
|
1195
|
+
const fileName = path.basename(c.path, ".md");
|
|
1196
|
+
return all.project.some((e) => e.file_path?.includes(fileName));
|
|
1197
|
+
});
|
|
1198
|
+
if (!indexedIndexOnly)
|
|
1199
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: "no index_only asset found in index" };
|
|
1200
|
+
// 验证 index_only 不在 matched_knowledge 中
|
|
1201
|
+
const matchedIds = (exp.matched_knowledge ?? []).map((k) => k.id ?? k.name);
|
|
1202
|
+
const indexOnlyName = path.basename(indexedIndexOnly.path, ".md");
|
|
1203
|
+
const leakedToMatched = matchedIds.some((id) => id?.includes(indexOnlyName));
|
|
1204
|
+
if (leakedToMatched)
|
|
1205
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: `index_only asset ${indexOnlyName} leaked to matched_knowledge` };
|
|
1206
|
+
// 验证 index_only 独特内容不在 prompt 中
|
|
1207
|
+
const indexOnlyEntry = all.project.find((e) => e.file_path?.includes(indexOnlyName));
|
|
1208
|
+
if (indexOnlyEntry?.body) {
|
|
1209
|
+
const uniqueContent = indexOnlyEntry.body.slice(0, 80);
|
|
1210
|
+
if (exp.prompt && exp.prompt.includes(uniqueContent))
|
|
1211
|
+
return { scenario_id: "release-scenario-template-visibility-old-project-migration", status: "fail", error: `index_only asset content leaked to prompt: ${uniqueContent.slice(0, 40)}` };
|
|
1212
|
+
}
|
|
1213
|
+
// tool_entrypoint 由 ToolInvocationObservation 自动生成
|
|
1214
|
+
const trace = await harness.collectTrace();
|
|
1215
|
+
return {
|
|
1216
|
+
scenario_id: "release-scenario-template-visibility-old-project-migration",
|
|
1217
|
+
status: "pass",
|
|
1218
|
+
evidence: `indexed=${all.project.length} no internal leaks index_only=${indexOnlyName} not in prompt/matched domain found`,
|
|
1219
|
+
production_trace: trace,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
finally {
|
|
1223
|
+
harness.cleanup();
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
// 场景 29: scaffold 边界 — 真实 sf_scaffold MCP 工具调用
|
|
1228
|
+
{
|
|
1229
|
+
scenario_id: "release-scenario-template-visibility-scaffold-boundary",
|
|
1230
|
+
covering_problem: "problem-64",
|
|
1231
|
+
test_files: ["tests/engine/release_issue_scenario_matrix.test.ts"],
|
|
1232
|
+
production_entrypoint: "sf_classify→sf_expand→sf_scaffold + template_asset_contract_registry.getSyncableAssetPaths",
|
|
1233
|
+
expected_outcome: "scaffold 合同不出现在同步列表; sf_scaffold 真实调用返回生成文件列表且无 .hbs 文件泄漏到用户项目",
|
|
1234
|
+
runner: async () => {
|
|
1235
|
+
const { createToolHarness } = await import("./release_tool_harness.js");
|
|
1236
|
+
const { buildTemplateAssetContracts, getSyncableAssetPaths } = await import("./template_asset_contract_registry.js");
|
|
1237
|
+
// 合同边界: scaffold 不在 syncable 列表
|
|
1238
|
+
const contracts = buildTemplateAssetContracts(process.cwd());
|
|
1239
|
+
const scaffoldContracts = contracts.filter((c) => c.asset_kind === "scaffold");
|
|
1240
|
+
if (scaffoldContracts.length === 0)
|
|
1241
|
+
return { scenario_id: "release-scenario-template-visibility-scaffold-boundary", status: "fail", error: "no scaffold contracts" };
|
|
1242
|
+
const syncable = new Set(getSyncableAssetPaths());
|
|
1243
|
+
const leaked = scaffoldContracts.filter((sc) => syncable.has(sc.path));
|
|
1244
|
+
if (leaked.length > 0)
|
|
1245
|
+
return { scenario_id: "release-scenario-template-visibility-scaffold-boundary", status: "fail", error: `scaffolds in syncable: ${leaked.map((s) => s.path).join(", ")}` };
|
|
1246
|
+
// 真实 sf_scaffold 工具调用
|
|
1247
|
+
const harness = await createToolHarness();
|
|
1248
|
+
try {
|
|
1249
|
+
const clsRaw = await harness.callTool("sf_classify", { intent: "使用脚手架创建用户管理模块" });
|
|
1250
|
+
const cls = harness.parseResult(clsRaw);
|
|
1251
|
+
const taskId = cls.task_id;
|
|
1252
|
+
if (!taskId)
|
|
1253
|
+
return { scenario_id: "release-scenario-template-visibility-scaffold-boundary", status: "fail", error: "sf_classify 未返回 task_id" };
|
|
1254
|
+
const expRaw = await harness.callTool("sf_expand", { task_id: taskId });
|
|
1255
|
+
const exp = harness.parseResult(expRaw);
|
|
1256
|
+
await harness.setupPlanGate(taskId);
|
|
1257
|
+
const scfRaw = await harness.callTool("sf_scaffold", { task_id: taskId });
|
|
1258
|
+
const scf = harness.parseResult(scfRaw);
|
|
1259
|
+
// sf_scaffold 必须返回结果(非错误)
|
|
1260
|
+
const scfObs = harness.getLastObservation();
|
|
1261
|
+
if (scfObs?.is_error && scf?.error?.includes("仅适用于 scaffold"))
|
|
1262
|
+
return { scenario_id: "release-scenario-template-visibility-scaffold-boundary", status: "fail", error: `sf_scaffold rejected: task_type=${cls.classification?.task_type ?? "unknown"}, expected scaffold` };
|
|
1263
|
+
// sf_scaffold 结果应包含 generated_files 或 templates_applied
|
|
1264
|
+
const hasFiles = Array.isArray(scf?.generated_files) || Array.isArray(scf?.templates_applied) || Array.isArray(scf?.files);
|
|
1265
|
+
if (!hasFiles && !scf?.error)
|
|
1266
|
+
return { scenario_id: "release-scenario-template-visibility-scaffold-boundary", status: "fail", error: `sf_scaffold returned no file list: ${JSON.stringify(scf).slice(0, 300)}` };
|
|
1267
|
+
const trace = await harness.collectTrace();
|
|
1268
|
+
// 补充诊断码(sf_scaffold 不返回诊断码,从观察记录提取)
|
|
1269
|
+
if (trace.diagnostic_codes.length === 0) {
|
|
1270
|
+
const scaffoldObs = harness.getObservations().find(o => o.tool_name === "sf_scaffold");
|
|
1271
|
+
trace.diagnostic_codes = [`scaffold_files:${scf?.total_files ?? scf?.files?.length ?? 0}`, `contracts:${scaffoldContracts.length}`];
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
scenario_id: "release-scenario-template-visibility-scaffold-boundary",
|
|
1275
|
+
status: "pass",
|
|
1276
|
+
evidence: `scaffolds=${scaffoldContracts.length} leaked=0 sf_scaffold=${scf?.total_files ?? scf?.files?.length ?? "?"} files generated`,
|
|
1277
|
+
production_trace: trace,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
harness.cleanup();
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
];
|
|
1286
|
+
/** 获取全量场景定义 */
|
|
1287
|
+
export function getReleaseIssueScenarios() {
|
|
1288
|
+
return RELEASE_ISSUE_SCENARIOS;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* 验证 发布问题场景覆盖:
|
|
1292
|
+
* - 所有场景必须有 runner + production_entrypoint + expected_outcome
|
|
1293
|
+
* - 所有引用测试文件的场景: 测试文件必须存在
|
|
1294
|
+
* - 返回结构化验证结果
|
|
1295
|
+
*/
|
|
1296
|
+
export function validateReleaseIssueScenarios(rootDir) {
|
|
1297
|
+
const missing_tests = [];
|
|
1298
|
+
const missing_runners = [];
|
|
1299
|
+
const missing_entrypoints = [];
|
|
1300
|
+
const tautology_suspects = [];
|
|
1301
|
+
const coveredProblems = new Set();
|
|
1302
|
+
for (const scenario of RELEASE_ISSUE_SCENARIOS) {
|
|
1303
|
+
coveredProblems.add(scenario.covering_problem);
|
|
1304
|
+
if (!scenario.runner) {
|
|
1305
|
+
missing_runners.push(scenario.scenario_id);
|
|
1306
|
+
}
|
|
1307
|
+
if (!scenario.production_entrypoint) {
|
|
1308
|
+
missing_entrypoints.push(scenario.scenario_id);
|
|
1309
|
+
}
|
|
1310
|
+
if (!scenario.expected_outcome) {
|
|
1311
|
+
missing_entrypoints.push(`${scenario.scenario_id}: missing expected_outcome`);
|
|
1312
|
+
}
|
|
1313
|
+
for (const testFile of scenario.test_files) {
|
|
1314
|
+
const fullPath = path.join(rootDir, testFile);
|
|
1315
|
+
if (!fs.existsSync(fullPath)) {
|
|
1316
|
+
missing_tests.push(`${scenario.scenario_id}: ${testFile}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
valid: missing_tests.length === 0 && missing_runners.length === 0 && missing_entrypoints.length === 0,
|
|
1322
|
+
missing_tests,
|
|
1323
|
+
missing_runners,
|
|
1324
|
+
missing_entrypoints,
|
|
1325
|
+
tautology_suspects,
|
|
1326
|
+
scenario_count: RELEASE_ISSUE_SCENARIOS.length,
|
|
1327
|
+
covered_problems: [...coveredProblems],
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* 运行所有场景,收集执行证据。
|
|
1332
|
+
* 每个场景必须有 runner,无 runner 视为 fail。
|
|
1333
|
+
*/
|
|
1334
|
+
export async function executeReleaseIssueScenarios() {
|
|
1335
|
+
const results = [];
|
|
1336
|
+
for (const scenario of RELEASE_ISSUE_SCENARIOS) {
|
|
1337
|
+
const start = Date.now();
|
|
1338
|
+
try {
|
|
1339
|
+
const result = await scenario.runner();
|
|
1340
|
+
result.duration_ms = Date.now() - start;
|
|
1341
|
+
results.push(result);
|
|
1342
|
+
}
|
|
1343
|
+
catch (e) {
|
|
1344
|
+
results.push({ scenario_id: scenario.scenario_id, status: "fail", error: e.message, duration_ms: Date.now() - start });
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return results;
|
|
1348
|
+
}
|
|
1349
|
+
//# sourceMappingURL=release_issue_scenario_registry.js.map
|