toolflow 3.1.4
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/.github/workflows/ci.yml +31 -0
- package/README.md +106 -0
- package/README_zh.md +109 -0
- package/docs/reports/ADVANCED_EVOLUTION_REPORT.md +44 -0
- package/docs/reports/AUDIT_AND_OPTIMIZATION_REPORT.md +645 -0
- package/docs/reports/COLD_START_REVIEW_EVOLUTION.md +51 -0
- package/docs/reports/DEEP_ECOSYSTEM_EVOLUTION.md +43 -0
- package/docs/reports/MEMORY.md +18 -0
- package/docs/reports/MICHAEL_DISPATCH_RESULT.md +36 -0
- package/docs/reports/OPENSOURCE_INTEGRATION_REPORT.md +43 -0
- package/docs/reports/PHASE_1_OPTIMIZATION_REPORT.md +87 -0
- package/docs/reports/PHASE_2_OPTIMIZATION_REPORT.md +50 -0
- package/docs/reports/PHASE_3_OPTIMIZATION_REPORT.md +24 -0
- package/docs/reports/PHASE_4_OPTIMIZATION_REPORT.md +28 -0
- package/docs/reports/REPORT_TO_MICHAEL.md +101 -0
- package/docs/reports/SIGNOFF_AND_RELEASE_REPORT.md +85 -0
- package/docs/reports/STAFF_ASSIGNMENTS.md +26 -0
- package/docs/reports/TASK_ASSIGNMENTS.md +59 -0
- package/docs/reports/V1_6_0_EVOLUTION_REPORT.md +48 -0
- package/docs/reports/V1_9_0_HOTFIX_REPORT.md +30 -0
- package/docs/reports/V2_0_0_RELEASE_REPORT.md +18 -0
- package/docs/reports/V2_2_0_ZERO_SPECIALIZATION_REPORT.md +24 -0
- package/docs/reports/V2_3_0_EVOLUTION_REPORT.md +12 -0
- package/ecosystem_taxonomy.json +798 -0
- package/package.json +46 -0
- package/src/blast_radius.ts +302 -0
- package/src/deep_ecosystem.ts +523 -0
- package/src/degradation_matrix.ts +180 -0
- package/src/dehydrator.ts +532 -0
- package/src/ecosystem_taxonomy.json +803 -0
- package/src/engine.ts +1510 -0
- package/src/i18n.ts +89 -0
- package/src/index.ts +983 -0
- package/src/json_extractor.ts +57 -0
- package/src/memory.ts +151 -0
- package/src/prompts_manager.ts +262 -0
- package/src/review_isolation.ts +188 -0
- package/src/state.ts +810 -0
- package/src/taxonomy.ts +580 -0
- package/src/types.ts +341 -0
- package/src/ui.ts +1036 -0
- package/src/worker_orchestrator.ts +60 -0
- package/tests/challenger_stress_harness.ts +265 -0
- package/tests/monorepo_multilang_stress.ts +404 -0
- package/tests/sandbox_e2e.ts +167 -0
- package/tests/test_json_extractor.ts +44 -0
- package/tests/test_modules_1_to_4.ts +106 -0
- package/tests/test_suite.ts +1689 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BlueprintStage, DAGPlanResult } from "./types.js";
|
|
2
|
+
import { planDAGWaves } from "./engine.js";
|
|
3
|
+
|
|
4
|
+
export interface ParallelTaskUnit {
|
|
5
|
+
laneId: string;
|
|
6
|
+
stageId: string;
|
|
7
|
+
stageTitle: string;
|
|
8
|
+
targetArtifacts: string[];
|
|
9
|
+
executionPrompt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WaveExecutionBundle {
|
|
13
|
+
waveIndex: number;
|
|
14
|
+
isParallel: boolean;
|
|
15
|
+
tasks: ParallelTaskUnit[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MultiAgentWorkerOrchestrator {
|
|
19
|
+
public static compileWaveBundles(stages: BlueprintStage[], maxConcurrency: number = 4): WaveExecutionBundle[] {
|
|
20
|
+
const dagResult: DAGPlanResult = planDAGWaves(stages);
|
|
21
|
+
const bundles: WaveExecutionBundle[] = [];
|
|
22
|
+
let bundleIdx = 1;
|
|
23
|
+
const limit = Math.max(1, maxConcurrency);
|
|
24
|
+
|
|
25
|
+
for (const wave of dagResult.waves) {
|
|
26
|
+
const stageChunks: BlueprintStage[][] = [];
|
|
27
|
+
for (let i = 0; i < wave.stages.length; i += limit) {
|
|
28
|
+
stageChunks.push(wave.stages.slice(i, i + limit));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const chunk of stageChunks) {
|
|
32
|
+
const isParallel = chunk.length > 1;
|
|
33
|
+
const tasks: ParallelTaskUnit[] = chunk.map((s, subIdx) => {
|
|
34
|
+
const artifacts =
|
|
35
|
+
s.expectedArtifacts && s.expectedArtifacts.length > 0
|
|
36
|
+
? s.expectedArtifacts
|
|
37
|
+
: s.expectedArtifact
|
|
38
|
+
? [s.expectedArtifact]
|
|
39
|
+
: [];
|
|
40
|
+
const desc = s.coreObjective || s.title;
|
|
41
|
+
return {
|
|
42
|
+
laneId: "wave_" + bundleIdx + "_lane_" + (subIdx + 1),
|
|
43
|
+
stageId: s.stageId,
|
|
44
|
+
stageTitle: s.title,
|
|
45
|
+
targetArtifacts: artifacts,
|
|
46
|
+
executionPrompt: "[Stage: " + s.title + "] " + desc + " -> 交付产物: " + artifacts.join(", ")
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
bundles.push({
|
|
51
|
+
waveIndex: bundleIdx++,
|
|
52
|
+
isParallel,
|
|
53
|
+
tasks
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return bundles;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import {
|
|
6
|
+
saveSessionStateToFile,
|
|
7
|
+
loadPersistedSessionState,
|
|
8
|
+
clearMemoryState,
|
|
9
|
+
verifyStageArtifacts,
|
|
10
|
+
startBlueprintExecution,
|
|
11
|
+
advanceStage,
|
|
12
|
+
atomicWriteFileSync
|
|
13
|
+
} from "../src/state.js";
|
|
14
|
+
import { ContextDehydrator } from "../src/dehydrator.js";
|
|
15
|
+
import { padToVisibleWidth, renderValueReceipt, renderUnicodeDAG } from "../src/ui.js";
|
|
16
|
+
import type { Blueprint, BlueprintStage } from "../src/types.js";
|
|
17
|
+
|
|
18
|
+
let passedAsserts = 0;
|
|
19
|
+
let failedAsserts = 0;
|
|
20
|
+
|
|
21
|
+
function assert(condition: boolean, msg: string) {
|
|
22
|
+
if (condition) {
|
|
23
|
+
passedAsserts++;
|
|
24
|
+
console.log(` [PASS] ${msg}`);
|
|
25
|
+
} else {
|
|
26
|
+
failedAsserts++;
|
|
27
|
+
console.error(` [FAIL] ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runAdversarialChallenges() {
|
|
32
|
+
console.log("================================================================================");
|
|
33
|
+
console.log("TOOLFLOW ADVERSARIAL CHALLENGE HARNESS (Empirical Verification & Stress Suite)");
|
|
34
|
+
console.log("================================================================================\n");
|
|
35
|
+
const tempBase = fs.mkdtempSync(path.join(os.tmpdir(), "toolflow_challenger_test_"));
|
|
36
|
+
try {
|
|
37
|
+
console.log(">>> [CHALLENGE 1] State Persistence across turns & Verification Failures...");
|
|
38
|
+
const ch1Dir = path.join(tempBase, "ch1_state");
|
|
39
|
+
fs.mkdirSync(ch1Dir, { recursive: true });
|
|
40
|
+
const dummyBlueprint: Blueprint = {
|
|
41
|
+
blueprintId: "bp_test_persist_01",
|
|
42
|
+
task: "Test State Persistence Across Turns",
|
|
43
|
+
projectFingerprint: {
|
|
44
|
+
projectType: "node",
|
|
45
|
+
packageManager: "npm",
|
|
46
|
+
hasGit: false,
|
|
47
|
+
isClean: true,
|
|
48
|
+
topLevelDirs: [],
|
|
49
|
+
coreDependencies: []
|
|
50
|
+
},
|
|
51
|
+
stages: [
|
|
52
|
+
{
|
|
53
|
+
stageId: "stage_1",
|
|
54
|
+
title: "Stage 1 Missing File",
|
|
55
|
+
roleProfile: "Developer",
|
|
56
|
+
coreObjective: "Implement missing component",
|
|
57
|
+
expectedArtifact: "src/missing_component.ts",
|
|
58
|
+
artifactContract: "Export missing component",
|
|
59
|
+
allowedTools: ["write_to_file"],
|
|
60
|
+
tokenCostNotice: "$0",
|
|
61
|
+
boundCapabilities: { extensions: [], skills: [], prompts: [] },
|
|
62
|
+
verificationCommands: []
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
stageId: "stage_2",
|
|
66
|
+
title: "Stage 2 Second Step",
|
|
67
|
+
roleProfile: "QA",
|
|
68
|
+
coreObjective: "Verify output JSON",
|
|
69
|
+
expectedArtifact: "tests/output.json",
|
|
70
|
+
artifactContract: "Valid test output",
|
|
71
|
+
allowedTools: ["run_command"],
|
|
72
|
+
tokenCostNotice: "$0",
|
|
73
|
+
boundCapabilities: { extensions: [], skills: [], prompts: [] },
|
|
74
|
+
verificationCommands: []
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
userChoices: {},
|
|
79
|
+
activatedCapabilities: { extensions: [], skills: [], prompts: [] },
|
|
80
|
+
tokenEfficiencySummary: "Test efficiency summary"
|
|
81
|
+
};
|
|
82
|
+
startBlueprintExecution(dummyBlueprint, ch1Dir);
|
|
83
|
+
assert(fs.existsSync(path.join(ch1Dir, ".pi", "blueprint_state.json")), "1.1 Initial blueprint_state.json persisted to disk");
|
|
84
|
+
const vResult1 = verifyStageArtifacts(dummyBlueprint.stages[0], ch1Dir, false);
|
|
85
|
+
assert(vResult1.valid === false, "1.2 vResult1.valid === false");
|
|
86
|
+
assert(vResult1.retryCount === 1, "1.3 vResult1.retryCount === 1");
|
|
87
|
+
const diskContent1 = JSON.parse(fs.readFileSync(path.join(ch1Dir, ".pi", "blueprint_state.json"), "utf-8"));
|
|
88
|
+
assert(diskContent1.retryCount === 1, "1.4 Disk state immediately persisted retryCount: 1");
|
|
89
|
+
assert(diskContent1.status === "in_progress", "1.5 Disk status is in_progress");
|
|
90
|
+
clearMemoryState();
|
|
91
|
+
const restored1 = loadPersistedSessionState(ch1Dir);
|
|
92
|
+
assert(restored1 !== null, "1.6 Persisted state successfully loaded from disk");
|
|
93
|
+
assert(restored1?.retryCount === 1, "1.7 Restored session state retains retryCount === 1 (NOT reset to 0)");
|
|
94
|
+
assert(restored1?.currentStageIndex === 0, "1.8 Restored session state retains currentStageIndex === 0");
|
|
95
|
+
const vResult2 = verifyStageArtifacts(dummyBlueprint.stages[0], ch1Dir, false);
|
|
96
|
+
assert(vResult2.valid === false, "1.9 vResult2.valid === false");
|
|
97
|
+
assert(vResult2.retryCount === 2, "1.10 vResult2.retryCount === 2");
|
|
98
|
+
const diskContent2 = JSON.parse(fs.readFileSync(path.join(ch1Dir, ".pi", "blueprint_state.json"), "utf-8"));
|
|
99
|
+
assert(diskContent2.retryCount === 2, "1.11 Disk state immediately persisted retryCount: 2");
|
|
100
|
+
clearMemoryState();
|
|
101
|
+
const restored2 = loadPersistedSessionState(ch1Dir);
|
|
102
|
+
assert(restored2?.retryCount === 2, "1.12 Restored session retains retryCount === 2");
|
|
103
|
+
const vResult3 = verifyStageArtifacts(dummyBlueprint.stages[0], ch1Dir, false);
|
|
104
|
+
assert(vResult3.valid === false, "1.13 vResult3.valid === false on 3rd failure");
|
|
105
|
+
assert(vResult3.retryCount === 3, "1.14 vResult3.retryCount === 3");
|
|
106
|
+
assert(vResult3.isCircuitBroken === true, "1.15 vResult3.isCircuitBroken === true");
|
|
107
|
+
const diskContent3 = JSON.parse(fs.readFileSync(path.join(ch1Dir, ".pi", "blueprint_state.json"), "utf-8"));
|
|
108
|
+
assert(diskContent3.retryCount === 3, "1.16 Disk state persisted retryCount: 3");
|
|
109
|
+
assert(diskContent3.status === "healing_failed_circuit_break", "1.17 Disk state persisted status: healing_failed_circuit_break");
|
|
110
|
+
clearMemoryState();
|
|
111
|
+
const restored3 = loadPersistedSessionState(ch1Dir);
|
|
112
|
+
assert(restored3?.retryCount === 3, "1.18 Restored session retains retryCount === 3 across turn boundary");
|
|
113
|
+
assert(restored3?.status === "healing_failed_circuit_break", "1.19 Restored session retains status === healing_failed_circuit_break");
|
|
114
|
+
const roResult = verifyStageArtifacts(dummyBlueprint.stages[0], ch1Dir, true);
|
|
115
|
+
assert(roResult.retryCount === 3, "1.20 Read-only query returns current retryCount 3");
|
|
116
|
+
const diskContentRO = JSON.parse(fs.readFileSync(path.join(ch1Dir, ".pi", "blueprint_state.json"), "utf-8"));
|
|
117
|
+
assert(diskContentRO.retryCount === 3, "1.21 Read-only query did NOT increment retryCount on disk");
|
|
118
|
+
const jsonPath = path.join(ch1Dir, ".pi", "blueprint_state.json");
|
|
119
|
+
const bakPath = path.join(ch1Dir, ".pi", "blueprint_state.json.bak");
|
|
120
|
+
assert(fs.existsSync(bakPath), "1.22 .bak file was created by atomicWriteFileSync");
|
|
121
|
+
fs.writeFileSync(jsonPath, "{ corrupted invalid json ...", "utf-8");
|
|
122
|
+
clearMemoryState();
|
|
123
|
+
const restoredBak = loadPersistedSessionState(ch1Dir);
|
|
124
|
+
assert(restoredBak !== null && restoredBak.retryCount === 3, "1.23 Fallback to .bak file successfully recovered corrupted primary state");
|
|
125
|
+
console.log("[OK] Challenge 1: State Persistence & Resilience Verified 100%\n");
|
|
126
|
+
console.log(">>> [CHALLENGE 2] Dehydrator LRU Quota Eviction & Byte Limits...");
|
|
127
|
+
const ch2Dir = path.join(tempBase, "ch2_dehydrator");
|
|
128
|
+
const runsBase = path.join(ch2Dir, ".pi", "toolflow", "runs");
|
|
129
|
+
fs.mkdirSync(runsBase, { recursive: true });
|
|
130
|
+
function createMockRun(runName: string, ageMs: number, sizeMB: number) {
|
|
131
|
+
const runDir = path.join(runsBase, runName);
|
|
132
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
133
|
+
const dummyFile = path.join(runDir, "data.log");
|
|
134
|
+
const buf = Buffer.alloc(sizeMB * 1024 * 1024, 0x41);
|
|
135
|
+
fs.writeFileSync(dummyFile, buf);
|
|
136
|
+
const targetTime = new Date(Date.now() - ageMs);
|
|
137
|
+
fs.utimesSync(runDir, targetTime, targetTime);
|
|
138
|
+
fs.utimesSync(dummyFile, targetTime, targetTime);
|
|
139
|
+
return runDir;
|
|
140
|
+
}
|
|
141
|
+
createMockRun("run_1_oldest", 500000, 60);
|
|
142
|
+
createMockRun("run_2_older", 400000, 60);
|
|
143
|
+
createMockRun("run_3_medium", 300000, 60);
|
|
144
|
+
createMockRun("run_4_newer", 200000, 60);
|
|
145
|
+
const activeRunDir = createMockRun("run_active_current", 1000, 50);
|
|
146
|
+
const dehydrator = new ContextDehydrator(ch2Dir, "run_active_current");
|
|
147
|
+
const evicted = dehydrator.pruneOldRuns(10, 7 * 24 * 3600 * 1000, 200 * 1024 * 1024);
|
|
148
|
+
assert(evicted.length === 1, `2.1 Evicted exactly 1 run (got ${evicted.length}: ${evicted.join(", ")})`);
|
|
149
|
+
assert(evicted[0] === "run_1_oldest", `2.2 Oldest run run_1_oldest was evicted first (got ${evicted[0]})`);
|
|
150
|
+
assert(!fs.existsSync(path.join(runsBase, "run_1_oldest")), "2.3 run_1_oldest folder was deleted from disk");
|
|
151
|
+
assert(fs.existsSync(path.join(runsBase, "run_2_older")), "2.4 run_2_older is preserved");
|
|
152
|
+
assert(fs.existsSync(path.join(runsBase, "run_3_medium")), "2.5 run_3_medium is preserved");
|
|
153
|
+
assert(fs.existsSync(path.join(runsBase, "run_4_newer")), "2.6 run_4_newer is preserved");
|
|
154
|
+
assert(fs.existsSync(activeRunDir), "2.7 Active run directory is NEVER evicted");
|
|
155
|
+
const evictedCount = dehydrator.pruneOldRuns(2, 7 * 24 * 3600 * 1000, 500 * 1024 * 1024);
|
|
156
|
+
assert(evictedCount.includes("run_2_older"), "2.8 MaxRuns limit evicted oldest remaining run run_2_older");
|
|
157
|
+
assert(fs.existsSync(path.join(runsBase, "run_3_medium")), "2.9 run_3_medium preserved");
|
|
158
|
+
assert(fs.existsSync(path.join(runsBase, "run_4_newer")), "2.10 run_4_newer preserved");
|
|
159
|
+
const hugeLog = "ToolFlow Log " + "x".repeat(11 * 1024 * 1024);
|
|
160
|
+
const handoff = dehydrator.dehydrateStageLog("stage_huge", "Huge Log Stage", hugeLog, [], "Summary of huge log");
|
|
161
|
+
assert(fs.existsSync(handoff.rawLogFilePath), "2.11 Huge log written to disk");
|
|
162
|
+
const writtenLog = fs.readFileSync(handoff.rawLogFilePath, "utf-8");
|
|
163
|
+
assert(writtenLog.includes("TOOLFLOW LOG TRUNCATED"), "2.12 Exceeded 10MB log was safely truncated with notice");
|
|
164
|
+
assert(Buffer.byteLength(writtenLog, "utf-8") < 3 * 1024 * 1024, "2.13 Truncated log file size is well under 3MB");
|
|
165
|
+
const binFile = path.join(ch2Dir, "image.png");
|
|
166
|
+
fs.writeFileSync(binFile, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
|
167
|
+
const binHandoff = dehydrator.dehydrateStageLog(
|
|
168
|
+
"stage_bin",
|
|
169
|
+
"Binary Asset Stage",
|
|
170
|
+
"Short log",
|
|
171
|
+
[{ path: binFile, sizeBytes: 8, sha256: "test", verifiedAt: Date.now() }],
|
|
172
|
+
"Image summary"
|
|
173
|
+
);
|
|
174
|
+
assert(binHandoff.topologyHints === undefined, "2.14 Binary files (.png) skipped for AST regex parsing without error");
|
|
175
|
+
console.log("[OK] Challenge 2: Dehydrator LRU Quota & Byte Limits Verified 100%\n");
|
|
176
|
+
console.log(">>> [CHALLENGE 3] TUI Monospace Alignment & 4-Corner Box Border Symmetry...");
|
|
177
|
+
const testCases = [
|
|
178
|
+
{ str: "Hello World", targetW: 20, desc: "Pure ASCII padding" },
|
|
179
|
+
{ str: "方案设计与意图契约", targetW: 24, desc: "Pure CJK (9 chars = 18 width -> pad to 24)" },
|
|
180
|
+
{ str: "[OK] 架构设计 (Design)", targetW: 35, desc: "Mixed CJK + ASCII + Symbols" },
|
|
181
|
+
{ str: "\x1b[32m[OK]\x1b[39m \x1b[1mStage 1\x1b[22m", targetW: 25, desc: "ANSI color codes embedded" },
|
|
182
|
+
{ str: "极长文本需要截断处理:这是一个非常非常长的中文字符串测试", targetW: 20, desc: "CJK truncation (longer than targetWidth)" }
|
|
183
|
+
];
|
|
184
|
+
testCases.forEach((tc, idx) => {
|
|
185
|
+
const padded = padToVisibleWidth(tc.str, tc.targetW);
|
|
186
|
+
const visW = visibleWidth(padded);
|
|
187
|
+
assert(visW === tc.targetW, `3.1.${idx + 1} padToVisibleWidth (${tc.desc}) visW === ${tc.targetW} (got ${visW})`);
|
|
188
|
+
});
|
|
189
|
+
const halfW = 40;
|
|
190
|
+
const tableRows = [
|
|
191
|
+
{ left: " 通用基础工具: fs, path", right: " 在线查阅: web_search, doc" },
|
|
192
|
+
{ left: " 技能库: 智能诊断与方案推导", right: " 扩展插件: @pi-btw, @pi-guard" },
|
|
193
|
+
{ left: " 流程编排 (Kahn DAG Waves)", right: " 质量门禁 (3次就地自愈)" },
|
|
194
|
+
{ left: " Fast Agile Mode (Plan A)", right: " Enterprise 5-Stage (Plan B)" }
|
|
195
|
+
];
|
|
196
|
+
const rendered2ColLines = tableRows.map(r => {
|
|
197
|
+
const col1 = padToVisibleWidth(r.left, halfW);
|
|
198
|
+
const col2 = padToVisibleWidth(r.right, halfW);
|
|
199
|
+
return `${col1} │ ${col2}`;
|
|
200
|
+
});
|
|
201
|
+
rendered2ColLines.forEach((line, idx) => {
|
|
202
|
+
const sepIndex = line.indexOf("│");
|
|
203
|
+
const leftPart = line.slice(0, sepIndex - 1);
|
|
204
|
+
const leftVisW = visibleWidth(leftPart);
|
|
205
|
+
const rightPart = line.slice(sepIndex + 2);
|
|
206
|
+
const rightVisW = visibleWidth(rightPart);
|
|
207
|
+
const totalVisW = visibleWidth(line);
|
|
208
|
+
assert(leftVisW === halfW, `3.2.${idx + 1}a Row ${idx + 1} Left column visW === ${halfW} (got ${leftVisW})`);
|
|
209
|
+
assert(rightVisW === halfW, `3.2.${idx + 1}b Row ${idx + 1} Right column visW === ${halfW} (got ${rightVisW})`);
|
|
210
|
+
assert(totalVisW === halfW + 3 + halfW, `3.2.${idx + 1}c Row ${idx + 1} Total line visW === ${halfW + 3 + halfW} (got ${totalVisW})`);
|
|
211
|
+
});
|
|
212
|
+
const testBoxWidths = [60, 80, 100, 120];
|
|
213
|
+
testBoxWidths.forEach((termWidth, wIdx) => {
|
|
214
|
+
const BOX_BORDER_LEFT = "│ ";
|
|
215
|
+
const BOX_BORDER_RIGHT = " │";
|
|
216
|
+
const BOX_BORDER_OVERHEAD = 4;
|
|
217
|
+
const innerWidth = Math.max(10, termWidth - BOX_BORDER_OVERHEAD);
|
|
218
|
+
const title = " ToolFlow ";
|
|
219
|
+
const titleVisW = visibleWidth(title);
|
|
220
|
+
const topInnerFill = Math.max(0, innerWidth + 1 - titleVisW);
|
|
221
|
+
const topBorder = truncateToWidth("╭─" + title + "─".repeat(topInnerFill) + "╮", termWidth, "", true);
|
|
222
|
+
const botBorder = truncateToWidth(`╰${"─".repeat(Math.max(0, innerWidth + 2))}╯`, termWidth, "", true);
|
|
223
|
+
const sampleBodyContent = " • 技能库: 智能诊断与方案推导与物理自愈机制 (CJK Test)";
|
|
224
|
+
const padded = truncateToWidth(sampleBodyContent, innerWidth, "", true);
|
|
225
|
+
const remaining = Math.max(0, innerWidth - visibleWidth(padded));
|
|
226
|
+
const content = padded + " ".repeat(remaining);
|
|
227
|
+
const bodyLine = truncateToWidth(BOX_BORDER_LEFT + content + BOX_BORDER_RIGHT, termWidth, "", true);
|
|
228
|
+
const topVisW = visibleWidth(topBorder);
|
|
229
|
+
const botVisW = visibleWidth(botBorder);
|
|
230
|
+
const bodyVisW = visibleWidth(bodyLine);
|
|
231
|
+
assert(topVisW === botVisW, `3.3.${wIdx + 1}a [Width ${termWidth}] Top border visW (${topVisW}) === Bot border visW (${botVisW})`);
|
|
232
|
+
assert(topVisW === bodyVisW, `3.3.${wIdx + 1}b [Width ${termWidth}] Top border visW (${topVisW}) === Body line visW (${bodyVisW})`);
|
|
233
|
+
assert(topVisW === termWidth || topVisW === innerWidth + 4, `3.3.${wIdx + 1}c [Width ${termWidth}] Border width matches box width (${topVisW})`);
|
|
234
|
+
});
|
|
235
|
+
const receipt = renderValueReceipt({
|
|
236
|
+
task: "开发微信客服服务 (Mixed English / CJK 测试用例)",
|
|
237
|
+
blueprintId: "bp_receipt_test_01",
|
|
238
|
+
stageCount: 5,
|
|
239
|
+
verifiedFiles: ["src/wechat.ts", "docs/design.md", "tests/wechat.test.ts"],
|
|
240
|
+
totalDurationSec: 42,
|
|
241
|
+
tokenSavingsRatio: ">96.5%"
|
|
242
|
+
});
|
|
243
|
+
const receiptWidths = receipt.map(r => visibleWidth(r));
|
|
244
|
+
const firstReceiptW = receiptWidths[0];
|
|
245
|
+
const allSameW = receiptWidths.every(w => w === firstReceiptW);
|
|
246
|
+
assert(allSameW, `3.4.1 Value Delivery Receipt all rows have identical visible width (${firstReceiptW})`);
|
|
247
|
+
assert(receipt[0].startsWith("+") && receipt[0].endsWith("+"), "3.4.2 Receipt top row starts and ends with +");
|
|
248
|
+
assert(receipt[receipt.length - 1].startsWith("+") && receipt[receipt.length - 1].endsWith("+"), "3.4.3 Receipt bottom row starts and ends with +");
|
|
249
|
+
console.log("[OK] Challenge 3: TUI Monospace & Box Symmetry Verified 100%\n");
|
|
250
|
+
} finally {
|
|
251
|
+
try {
|
|
252
|
+
fs.rmSync(tempBase, { recursive: true, force: true });
|
|
253
|
+
} catch (_) {}
|
|
254
|
+
}
|
|
255
|
+
console.log("================================================================================");
|
|
256
|
+
console.log(`CHALLENGE RESULTS: ${passedAsserts} PASSED, ${failedAsserts} FAILED`);
|
|
257
|
+
console.log("================================================================================");
|
|
258
|
+
if (failedAsserts > 0) {
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
runAdversarialChallenges().catch(err => {
|
|
263
|
+
console.error("Unhandled error in adversarial challenge:", err);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
});
|