deveco_hmigbot 0.21.5
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/LICENSE +26 -0
- package/README.md +50 -0
- package/agents/hmigbot-worker.md +61 -0
- package/agents/hmigbot.md +22 -0
- package/agents/workflow-subagent.md +55 -0
- package/commands/hmigbot.md +17 -0
- package/dist/index.js +1 -0
- package/manifest.json +19 -0
- package/package.json +29 -0
- package/skills/migrate-core/FILES.md +26 -0
- package/skills/migrate-core/SKILL.md +484 -0
- package/skills/migrate-core/references/README.md +64 -0
- package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
- package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
- package/skills/migrate-core/references/flow/conventions-template.md +244 -0
- package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
- package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
- package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
- package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
- package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
- package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
- package/skills/migrate-core/references/host-capabilities.md +24 -0
- package/skills/migrate-core/references/topics/app-identity.md +214 -0
- package/skills/migrate-core/references/topics/env-doctor.md +245 -0
- package/skills/migrate-core/references/topics/i18n/README.md +458 -0
- package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
- package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
- package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
- package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
- package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
- package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
- package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
- package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
- package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
- package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
- package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
- package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
- package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
- package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
- package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
- package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
- package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
- package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
- package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
- package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
- package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
- package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
- package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
- package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
- package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
- package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
- package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
- package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
- package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
- package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
- package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
- package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
- package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
- package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
- package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
- package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
- package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
- package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
- package/skills/migrate-core/scripts/closure/idioms.json +105 -0
- package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
- package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
- package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
- package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
- package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
- package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
- package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
- package/skills/migrate-core/scripts/front.mjs +107 -0
- package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
- package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
- package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
- package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
- package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
- package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
- package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
- package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
- package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
- package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
- package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
- package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
- package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
- package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
- package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
- package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
- package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
- package/skills/migrate-core/templates/acceptance.js +365 -0
- package/skills/migrate-core/templates/explore.js +86 -0
- package/skills/migrate-core/templates/implement.js +211 -0
- package/skills/migrate-core/templates/mig_slices.js +491 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: 'mig_slices',
|
|
3
|
+
description: '轻编排实现流水:模型自主实现每片 → 确定性静态门(增量构建 + 算法自测 + 切片级闭合,均可重跑不翻转)→ 落 checked。判断性验收(verify/对抗)不在此,由末端 acceptance 一遍完成——杜绝逐片重验的 churn。依赖分层调度,收口跑集成级闭合。',
|
|
4
|
+
phases: [
|
|
5
|
+
{ title: 'Implement', detail: '切片实现(填空式:骨架已在,行为接线)' },
|
|
6
|
+
{ title: 'Checkpoint', detail: '确定性静态门:增量构建 + selfTest + 切片级闭合(退出码裁决,失败一次定向修)' },
|
|
7
|
+
{ title: 'Integration', detail: '全切片实现后的集成级闭合' },
|
|
8
|
+
],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* 轻编排设计(2026-09-05 重构,见 docs/design-2026-09-05-light-orchestration.md):
|
|
13
|
+
* - 只做"确定性验证":构建/selfTest/闭合,退出码裁决、重跑同答案、不靠真机、不 churn。
|
|
14
|
+
* - 不做"判断性验证":verify agent、对抗否证一律不在此——它们会翻转,逐片连续跑=churn 病根,
|
|
15
|
+
* 统一挪到末端 acceptance 模板跑一遍。故本模板只把切片推进到 ledger 的 checked,不过账 featureIds。
|
|
16
|
+
* - 失败即定向:确定性门红了派一次修(构建错/闭合悬空都是可行动的确定失败),仍红则记 failed 并继续,
|
|
17
|
+
* 绝不整流水 resume 重验已完成片。
|
|
18
|
+
* - 无栅栏:同层切片各自流过 实现→检查点,快的不等慢的;真依赖按拓扑分层,仅层间有序。
|
|
19
|
+
*
|
|
20
|
+
* args = {
|
|
21
|
+
* goal, dst,
|
|
22
|
+
* build: "$ cd <dst> && hvigorw assembleHap --no-daemon --incremental"
|
|
23
|
+
* closureSlice: "$ node <home>/scripts/closure/closure-suite.mjs --project <dst> --stage slice"
|
|
24
|
+
* closureIntegration: "$ node <home>/scripts/closure/closure-suite.mjs --project <dst> --stage integration"
|
|
25
|
+
* ledgerCmd?: "node <home>/scripts/ledger/ledger.mjs --dir <ledgerDir>"
|
|
26
|
+
* instructionsFile, conventionsFile, contextFiles?, skills?,
|
|
27
|
+
* slices: [{ id, title, targetPath (串或数组), expectation, section?, deps?: [], skills?, route?,
|
|
28
|
+
* selfTest?: "$ node spec/run-vectors.mjs" }], // featureIds 由末端 acceptance 过账,此处可不填
|
|
29
|
+
* unownedRoutes?: [{ route, reason }], // 确不迁移的注册路由(路由归属校验豁免;同时应从 route_map 删注册)
|
|
30
|
+
* smoke?: "$ node <home>/scripts/smoke/smoke.mjs --project <dst> --out <spec>/smoke",
|
|
31
|
+
* only?: [], models?: { implement?, fix? }
|
|
32
|
+
* }
|
|
33
|
+
* 机制内置:路由归属按文件校验(route_map 每个注册页文件须落在某切片 targetPath 内;带 route 的片须拥有其页文件);
|
|
34
|
+
* 越权申报 blocked → 台账 debts + summary.blocked;波级冒烟环境阻塞记账不派修;切片过大只给信号不设配额。
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const A = args || {};
|
|
38
|
+
const fail = (m) => { throw new Error(`mig_slices args: ${m}`); };
|
|
39
|
+
for (const k of ['build', 'closureSlice', 'closureIntegration']) {
|
|
40
|
+
if (!A[k] || !String(A[k]).trim().startsWith('$ ')) fail(`${k} 必须是 "$ <shell 命令>" 形态`);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(A.slices) || !A.slices.length) fail('slices 为空');
|
|
43
|
+
const ids = A.slices.map(s => s.id);
|
|
44
|
+
if (new Set(ids).size !== ids.length) fail('切片 id 重复');
|
|
45
|
+
const only0 = Array.isArray(A.only) && A.only.length ? A.only : null;
|
|
46
|
+
for (const s of A.slices) {
|
|
47
|
+
if (!s.id || !s.targetPath) fail(`切片缺 id/targetPath`);
|
|
48
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(s.id)) fail(`切片 id "${s.id}" 含非法字符(台账/shell 拼接安全集:字母数字 _ . : -)`);
|
|
49
|
+
// expectation 长度只校验本次执行集:闭包里"地基波已完成、本轮不执行"的占位片不该拦整流(实测短标注触发整流重跑)
|
|
50
|
+
const inExec = !only0 || only0.includes(s.id);
|
|
51
|
+
if (inExec && (!s.expectation || String(s.expectation).trim().length < 20)) fail(`切片 ${s.id} 的 expectation 过短(不足 20 字):写成可逐条核对的完成标准(原子可断言/穷举/带 文件:行 锚点)`);
|
|
52
|
+
if (s.selfTest && !String(s.selfTest).trim().startsWith('$ ')) fail(`切片 ${s.id} 的 selfTest 必须是 "$ <shell 命令>" 形态`);
|
|
53
|
+
// deps 指向不在 slices 的 id:视为外部已满足(与 extSat 同义)并告警,不整流拒跑——依赖顺序只在执行集内有意义
|
|
54
|
+
//(实测闭包漏列一条已完成地基片 id 就整流重跑)
|
|
55
|
+
s.deps = (s.deps || []).filter(d => {
|
|
56
|
+
if (ids.includes(d)) return true;
|
|
57
|
+
log(`⚠ 切片 ${s.id} 依赖 ${d} 不在 slices(视为外部已满足;若它其实未完成,补进闭包再跑)`);
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// smoke 会被模板尾接 --routes/--json:bash -c '…' 包裹会把尾接参数变成 bash 位置参数(冒烟退化成仅冷启)且引号错配
|
|
62
|
+
// (实证:/bin/sh unexpected EOF,冒烟从未执行、误派修复一轮)。要切目录写 "cd <dir> && node …"。
|
|
63
|
+
if (A.smoke && /\b(?:bash|sh|zsh)\s+-l?c\b/.test(String(A.smoke))) fail('smoke 直接写命令(模板会尾接 --routes/--json),不要 bash -c 包裹;需切目录写 "$ cd <dir> && node <smoke.mjs> …"');
|
|
64
|
+
// 环检测(对全依赖图,保持校验能力)
|
|
65
|
+
(function detectCycles() {
|
|
66
|
+
const state = new Map();
|
|
67
|
+
const walk = (s, path) => {
|
|
68
|
+
if (state.get(s.id) === 2) return;
|
|
69
|
+
if (state.get(s.id) === 1) fail(`依赖成环:${[...path, s.id].join('→')}`);
|
|
70
|
+
state.set(s.id, 1);
|
|
71
|
+
for (const d of s.deps || []) walk(A.slices.find(x => x.id === d), [...path, s.id]);
|
|
72
|
+
state.set(s.id, 2);
|
|
73
|
+
};
|
|
74
|
+
for (const s of A.slices) walk(s, []);
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
// 依赖方向提示:业务核心不该宿主进页面片(tier 前缀数据化,未知前缀不误判)
|
|
78
|
+
const CORE_TIERS = /^(backend|platform|core|fnd|service|domain|data)-/;
|
|
79
|
+
for (const s of A.slices) {
|
|
80
|
+
if (CORE_TIERS.test(s.id)) {
|
|
81
|
+
const bad = (s.deps || []).filter(d => /^(page|view|ui)-/.test(d));
|
|
82
|
+
if (bad.length) log(`⚠ 依赖方向提示:核心片 ${s.id} 依赖页面片 ${bad.join(',')}——建议把共享状态机/服务提为独立核心片`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const only = Array.isArray(A.only) && A.only.length ? A.only : null;
|
|
87
|
+
if (only) for (const o of only) if (!ids.includes(o)) fail(`only 中的 ${o} 不在 slices`);
|
|
88
|
+
const slices = only ? A.slices.filter(s => only.includes(s.id)) : A.slices;
|
|
89
|
+
|
|
90
|
+
// 拓扑分层:只有"本次也要执行"的依赖才推高层级(已完成/外部依赖视为已满足,不制造空转层)。
|
|
91
|
+
const execSet = new Set(slices.map(s => s.id));
|
|
92
|
+
const levelOf = new Map();
|
|
93
|
+
function level(s) {
|
|
94
|
+
if (levelOf.has(s.id)) return levelOf.get(s.id);
|
|
95
|
+
const activeDeps = (s.deps || []).filter(d => execSet.has(d));
|
|
96
|
+
levelOf.set(s.id, 0);
|
|
97
|
+
const l = activeDeps.length ? Math.max(...activeDeps.map(d => level(A.slices.find(x => x.id === d)))) + 1 : 0;
|
|
98
|
+
levelOf.set(s.id, l);
|
|
99
|
+
return l;
|
|
100
|
+
}
|
|
101
|
+
for (const s of slices) level(s);
|
|
102
|
+
const extSat = [...new Set(slices.flatMap(s => (s.deps || []).filter(d => !execSet.has(d))))];
|
|
103
|
+
if (extSat.length) log(`外部依赖视为已满足(不在本次执行集):${extSat.join(', ')}`);
|
|
104
|
+
const maxLevel = Math.max(...slices.map(s => levelOf.get(s.id)));
|
|
105
|
+
const M = A.models || {};
|
|
106
|
+
const opt = (model, extra) => model ? { model, ...extra } : { ...extra };
|
|
107
|
+
const cmd = (c) => String(c).trim().replace(/^\$\s*/, '');
|
|
108
|
+
|
|
109
|
+
// ---- 路由归属按文件校验(契约靠机制不靠自觉)----
|
|
110
|
+
// 注册页文件就是交付文件:route_map 每个 pageSourceFile 必须落在某切片 targetPath 内(确不迁移的路由用
|
|
111
|
+
// unownedRoutes:[{route,reason}] 豁免并删注册);带 route 的片,其注册页文件必须在自己的 targetPath 内。
|
|
112
|
+
// 否则实现落在壳外目录、壳永远挂不上——静态门/冒烟/验收三道都看不见。dst 未给或无 route_map 则跳过。
|
|
113
|
+
if (A.dst) {
|
|
114
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
115
|
+
const dstN = norm(A.dst);
|
|
116
|
+
// 扫各模块的 route_map(模块目录不写死),输出 [{module,name,pageSourceFile}]
|
|
117
|
+
const rr = await check(`node -e "const fs=require('fs'),p=require('path'),d=process.argv[1],o=[];try{for(const m of fs.readdirSync(d)){const f=p.join(d,m,'src','main','resources','base','profile','route_map.json');if(!fs.existsSync(f))continue;try{for(const e of (JSON.parse(fs.readFileSync(f,'utf8')).routerMap||[]))o.push({module:m,name:e.name,pageSourceFile:e.pageSourceFile})}catch{}}}catch{};process.stdout.write(JSON.stringify(o))" "${dstN}"`);
|
|
118
|
+
let entries = [];
|
|
119
|
+
try { entries = rr.ok ? JSON.parse(String(rr.output || '[]')) : []; } catch { entries = []; }
|
|
120
|
+
if (!Array.isArray(entries)) entries = [];
|
|
121
|
+
if (entries.length) {
|
|
122
|
+
const owners = (file) => A.slices.filter(s => [].concat(s.targetPath).map(norm).some(tp => file === tp || file.startsWith(tp + '/')));
|
|
123
|
+
const unowned = new Map((A.unownedRoutes || []).map(u => [u.route || u.name, String(u.reason || '')]));
|
|
124
|
+
const pageFile = (e) => `${dstN}/${e.module}/${norm(e.pageSourceFile || '')}`;
|
|
125
|
+
const problems = [];
|
|
126
|
+
for (const e of entries) {
|
|
127
|
+
const file = pageFile(e);
|
|
128
|
+
if (unowned.has(e.name)) { if (unowned.get(e.name).trim().length < 8) problems.push(`unownedRoutes ${e.name} 须给实质理由`); continue; }
|
|
129
|
+
if (!owners(file).length) problems.push(`路由 ${e.name} 的注册页 ${file} 不在任何切片 targetPath 内`);
|
|
130
|
+
}
|
|
131
|
+
for (const s of A.slices) {
|
|
132
|
+
if (!s.route) continue;
|
|
133
|
+
const e = entries.find(x => x.name === s.route);
|
|
134
|
+
if (!e) { problems.push(`切片 ${s.id} 的 route ${s.route} 未在 route_map 注册`); continue; }
|
|
135
|
+
const file = pageFile(e);
|
|
136
|
+
if (!owners(file).some(o => o.id === s.id)) problems.push(`切片 ${s.id} 声明 route ${s.route},但其注册页 ${file} 不在本片 targetPath(${[].concat(s.targetPath).join(', ')})内——实现会落在别处、壳永远挂不上`);
|
|
137
|
+
}
|
|
138
|
+
if (problems.length) fail(`路由归属校验未过(${problems.length} 条):\n- ${problems.join('\n- ')}\n(怎么改:归属按 slices[] 全集校验、与 only 无关——分波执行时把全部页面片(含后续波的)都列进 slices[],每片 targetPath 含其注册页文件,再用 only 限定本波执行集;确不迁移的路由才用 unownedRoutes:[{route,reason}] 豁免并从 route_map 删注册)`);
|
|
139
|
+
log(`路由归属校验通过:${entries.length} 个注册页均有切片拥有${unowned.size ? `(豁免 ${unowned.size})` : ''}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const IMPL_SCHEMA = {
|
|
144
|
+
type: 'object', required: ['ok', 'summary', 'files'],
|
|
145
|
+
properties: { ok: { type: 'boolean' }, summary: { type: 'string' }, files: { type: 'array', items: { type: 'string' } }, deviations: { type: 'array', items: { type: 'object', required: ['kind', 'text'], properties: { kind: { type: 'string', enum: ['no_equiv', 'approx', 'policy', 'source_stub'] }, text: { type: 'string' }, symbols: { type: 'array', items: { type: 'string' } }, probe: { type: 'string' } } } }, unresolved: { type: 'array', items: { type: 'string' } }, blocked: { type: 'string' } },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 检查点互斥锁(构建/闭合共享资源,串行化)
|
|
149
|
+
let lock = Promise.resolve();
|
|
150
|
+
function withLock(fn) {
|
|
151
|
+
const run = lock.then(fn, fn);
|
|
152
|
+
lock = run.then(() => undefined, () => undefined);
|
|
153
|
+
return run;
|
|
154
|
+
}
|
|
155
|
+
const failReasons = new Map();
|
|
156
|
+
let buildPoisoned = null;
|
|
157
|
+
function failSlice(id, reason) { failReasons.set(id, reason); log(`✗ ${id}:${reason}`); throw new Error(reason); }
|
|
158
|
+
// ---- 构建红归责(无栅栏流水的代价:检查点编译的是整个工程,同层他片可能正写到一半)----
|
|
159
|
+
// 从构建输出抽 .ets/.ts 文件路径(绝对路径按 cwd 转相对),按切片 targetPath 判归属:
|
|
160
|
+
// own=本片文件;others=他片文件(键=片 id);unknown=无片拥有(壳/共享件)。
|
|
161
|
+
// 全在"同层尚未落定的他片"文件里 → 不是本片之责:不派修、不判毒化,延后到本层收口复检一次。
|
|
162
|
+
const checkedIds = new Set();
|
|
163
|
+
const fixRounds = new Set(); // `${片}:${pass|final}`——每片每阶段最多一轮构建修复
|
|
164
|
+
let levelUnstable = false; // 本层出现不可归责他片的持续红:后续检查点不各自派修,统一延后收口复检(每层重置)
|
|
165
|
+
const normPath = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
166
|
+
const ownerOf = (file) => A.slices.find(x => [].concat(x.targetPath).map(normPath).some(tp => file === tp || file.startsWith(tp + '/')));
|
|
167
|
+
function blameBuild(output, s) {
|
|
168
|
+
const own = new Set(), others = new Map(), unknown = new Set();
|
|
169
|
+
const base = (typeof cwd === 'string' && cwd) ? normPath(cwd) + '/' : '';
|
|
170
|
+
// Windows 盘符大小写不定(hvigor 打印 c:\… 与 C:\… 都有),前缀比较不区分大小写
|
|
171
|
+
const stripBase = (f) => { const nf = normPath(f); return base && nf.slice(0, base.length).toLowerCase() === base.toLowerCase() ? nf.slice(base.length) : nf; };
|
|
172
|
+
for (const m of String(output || '').matchAll(/(?:[A-Za-z]:)?[\/\\]?[\w.\-]+(?:[\/\\][\w.\-]+)*\.(?:ets|ts)(?=[:\s'")\]]|$)/g)) {
|
|
173
|
+
let f = normPath(m[0]);
|
|
174
|
+
f = stripBase(f);
|
|
175
|
+
const o = ownerOf(f);
|
|
176
|
+
if (!o) { unknown.add(f); continue; }
|
|
177
|
+
if (o.id === s.id) own.add(f);
|
|
178
|
+
else { if (!others.has(o.id)) others.set(o.id, new Set()); others.get(o.id).add(f); }
|
|
179
|
+
}
|
|
180
|
+
return { own: [...own], others, unknown: [...unknown] };
|
|
181
|
+
}
|
|
182
|
+
const ledgerWarns = [];
|
|
183
|
+
async function ledger(op) {
|
|
184
|
+
if (!A.ledgerCmd) return;
|
|
185
|
+
let r = await check(`${A.ledgerCmd} ${op}`);
|
|
186
|
+
// 宿主重解压内置技能时脚本文件会短暂消失(另一版本 deveco 在同机启动即触发,实证 71 秒窗口):等 5 s 重试两次,不把丢账当结果
|
|
187
|
+
for (let i = 0; i < 2 && !r.ok && /Cannot find module/.test(r.output || ''); i++) { await check(`node -e "setTimeout(()=>{},5000)"`); r = await check(`${A.ledgerCmd} ${op}`); }
|
|
188
|
+
if (!r.ok) { const w = `台账写入失败:${op.slice(0, 140)} → ${(r.output || '').trim().slice(0, 200)}`; ledgerWarns.push(w); log(`⚠ ${w}`); }
|
|
189
|
+
}
|
|
190
|
+
// 批量条目不经 shell 传 JSON(cmd.exe 不认单引号、命令行 8K 上限):base64 分段由 node 写成临时文件,再 --file 读入。
|
|
191
|
+
// 分段长度取 4 的倍数使每段可独立解码;文件落 .deveco/workflows/tmp/(引擎会把 .deveco 改写为实际点目录)
|
|
192
|
+
let tmpSeq = 0; // 引擎沙箱禁 Date.now()/Math.random()(脚本须确定性,journal 才能按命令串回放):临时文件名用自增序号
|
|
193
|
+
// 沙箱是干净 V8 上下文:没有 Buffer/btoa/TextEncoder,只有 JS 内建——base64 用内建函数手写(UTF-8 经 encodeURIComponent+unescape 转字节串)
|
|
194
|
+
const toB64 = (str) => {
|
|
195
|
+
const bytes = unescape(encodeURIComponent(str)); const T = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; let out = '';
|
|
196
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
197
|
+
const a = bytes.charCodeAt(i), b = i + 1 < bytes.length ? bytes.charCodeAt(i + 1) : NaN, c = i + 2 < bytes.length ? bytes.charCodeAt(i + 2) : NaN;
|
|
198
|
+
out += T[a >> 2] + T[((a & 3) << 4) | (isNaN(b) ? 0 : b >> 4)] + (isNaN(b) ? '=' : T[((b & 15) << 2) | (isNaN(c) ? 0 : c >> 6)]) + (isNaN(c) ? '=' : T[c & 63]);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
};
|
|
202
|
+
async function ledgerAddFile(type, items, tag) {
|
|
203
|
+
if (!A.ledgerCmd) return;
|
|
204
|
+
const b64 = toB64(JSON.stringify(items));
|
|
205
|
+
const file = `.deveco/workflows/tmp/${String(tag).replace(/[^\w.-]/g, '_')}-${String(++tmpSeq)}.json`;
|
|
206
|
+
const b64file = `${file}.b64`;
|
|
207
|
+
const chunks = b64.match(/.{1,1600}/g) || [''];
|
|
208
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
209
|
+
const w = await check(`node -e "const fs=require('fs');const p=process.argv[1];fs.mkdirSync(require('path').dirname(p),{recursive:true});fs[${i === 0 ? "'writeFileSync'" : "'appendFileSync'"}](p,process.argv[2])" "${b64file}" ${chunks[i]}`);
|
|
210
|
+
if (!w.ok) { const m = `台账写入失败:临时文件 ${b64file} 写不出(${(w.output || '').trim().slice(0, 120)})`; ledgerWarns.push(m); log(`⚠ ${m}`); return; }
|
|
211
|
+
}
|
|
212
|
+
const d = await check(`node -e "const fs=require('fs');const p=process.argv[1];fs.writeFileSync(p,Buffer.from(fs.readFileSync(p+'.b64','utf8'),'base64'));fs.unlinkSync(p+'.b64')" "${file}"`);
|
|
213
|
+
if (!d.ok) { const m = `台账写入失败:临时文件 ${file} 解码失败`; ledgerWarns.push(m); log(`⚠ ${m}`); return; }
|
|
214
|
+
await ledger(`add --type ${type} --file "${file}"`);
|
|
215
|
+
await check(`node -e "try{require('fs').unlinkSync(process.argv[1])}catch{}" "${file}"`); // 交接文件用完即删
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const results = [];
|
|
219
|
+
// 越权申报(实现者按纪律不动壳/共享件、写进 blocked 的项):以前只打日志无人消费(实测 120 条含"需壳单元挂载 X"),
|
|
220
|
+
// 现在进台账 debts(blk-<片>)+ 汇总 summary.blocked,收口前必须处置。
|
|
221
|
+
const blockedReports = [];
|
|
222
|
+
const smokeWaves = [];
|
|
223
|
+
|
|
224
|
+
// ---- 机械切卡(引擎机制,不靠装配纪律)----
|
|
225
|
+
// 模板启动读一次工单与规范,按 `## SECTION: <id>` 机械切分注入各片 prompt。
|
|
226
|
+
// 治的不是那几十 K 文件本身,是"每 agent 开局 read 一轮 = 全上下文重传一轮"的固定税
|
|
227
|
+
// (实测 conventions/instructions 被 31–43 个会话各整读一遍)。
|
|
228
|
+
// 读法:逐片 readSection(工单, 'SECTION: <id>')——引擎按节抽取(前言+该节,50K 上限),不经 check() 的 16K 输出截断
|
|
229
|
+
// (实证一轮 176K/31 节的工单经 check 读入只切出 5 节,26 片静默回退自读文件)。规范全文 readSection 整读,超限即不附。
|
|
230
|
+
let convText = '';
|
|
231
|
+
if (A.conventionsFile) {
|
|
232
|
+
try {
|
|
233
|
+
// 标题含"不注入"的节(待验条目层)整节剔除——未经真机验证的规则不进实现 prompt
|
|
234
|
+
convText = String(await readSection(A.conventionsFile)).replace(/^##[^\n]*不注入[\s\S]*?(?=^## |\s*$(?![\s\S]))/m, '').trim();
|
|
235
|
+
} catch (e) { log(`⚠ 规范整读失败(${String(e && e.message || e).slice(0, 120)})——卡片不附规范,改指路必读文件`); convText = ''; }
|
|
236
|
+
}
|
|
237
|
+
const sectionMisses = [];
|
|
238
|
+
async function cardOf(s) {
|
|
239
|
+
if (s.card) return s.card; // 装配显式给卡则优先
|
|
240
|
+
if (!A.instructionsFile) return null;
|
|
241
|
+
let sec = null;
|
|
242
|
+
const want = s.section || s.id;
|
|
243
|
+
try { sec = String(await readSection(A.instructionsFile, `SECTION: ${want}`)); } // 前言 + 本节
|
|
244
|
+
catch (e) { sectionMisses.push(s.id); log(`⚠ ${s.id} 工单无对应 SECTION(${String(e && e.message || e).slice(0, 100)})——回退指路,agent 自读工单`); return null; }
|
|
245
|
+
// 抽到的必须正是本节:引擎按行包含匹配,`page-a` 可能抽到 `page-ab`(本版引擎已优先精确行,这里再守一道)
|
|
246
|
+
const head = sec.match(/^\W*SECTION:\s*(\S+)\s*$/m);
|
|
247
|
+
if (!head || head[1] !== want) { sectionMisses.push(s.id); log(`⚠ ${s.id} 抽到的节标题是 "${head ? head[1] : '(无)'}" 不是 "${want}"——回退指路`); return null; }
|
|
248
|
+
const bodyLen = sec.length - (head.index || 0); // 本节自身长度(不含前言),过大信号按它算
|
|
249
|
+
// 24K 上限:先砍规范(工单节永不砍——那是本片的完成标准),规范砍空仍超则如实带超长卡片并告警
|
|
250
|
+
const conv = convText ? `【写法规范(全文已附,勿再读文件)】\n${convText}` : '';
|
|
251
|
+
let card = [sec, conv].filter(Boolean).join('\n\n');
|
|
252
|
+
if (card.length > 24000 && conv) { const room = Math.max(0, 24000 - sec.length - 2); log(`⚠ ${s.id} 卡片超 24K:规范截到 ${room} 字(工单节 ${sec.length} 字保留完整)`); card = room > 400 ? `${sec}\n\n${conv.slice(0, room)}\n…[规范截断,完整规范见 ${A.conventionsFile}]` : sec; }
|
|
253
|
+
if (card.length > 24000) log(`⚠ ${s.id} 工单节本身 ${bodyLen} 字超 24K——切片过大,按松耦合边界拆`);
|
|
254
|
+
// 切片过大信号(不是配额):单节工单 >8K 或断言 >40 条的片单轮难写完(实测 87 条消息均值、最长 205 条报废)
|
|
255
|
+
const secLen = bodyLen, exLines = String(s.expectation || '').split('\n').filter(l => l.trim()).length;
|
|
256
|
+
if (secLen > 8000 || exLines > 40) log(`⚠ 切片过大信号 ${s.id}:SECTION ${secLen} 字 · 断言 ${exLines} 行——找松耦合边界拆片,找不到就接受更长单轮`);
|
|
257
|
+
return card;
|
|
258
|
+
}
|
|
259
|
+
// 技能不是引擎选项:与 implement.js 同法写进 prompt,由子代理用 skill 工具加载(此前作为 agent 选项传入被引擎静默丢弃,实证一轮无一次加载)
|
|
260
|
+
const skillLine = (...vs) => { const sk = []; for (const v of vs) for (const x of [].concat(v || [])) if (typeof x === 'string' && x && !sk.includes(x)) sk.push(x); return sk.length ? `开工前先用 skill 工具加载并遵循这些技能:${sk.join('、')}。\n` : ''; };
|
|
261
|
+
// 结构化结果失败时引擎回退原文字符串:对本流程等同死亡(既无 ok 也无 files),统一按 null 走增量续跑
|
|
262
|
+
const asObj = (r, label) => { if (r !== null && r !== undefined && typeof r !== 'object') { log(`⚠ ${label} 返回非结构化文本(schema 修复失败),按死亡处理`); return null; } return r; };
|
|
263
|
+
|
|
264
|
+
async function implementSlice(s) {
|
|
265
|
+
const card = await cardOf(s);
|
|
266
|
+
const briefing = card
|
|
267
|
+
? `【工单(自足,禁再读工单/规范原文件)】\n${card}\n`
|
|
268
|
+
: `工单:${A.instructionsFile || '(见 expectation)'}${s.section ? ` 的 SECTION: ${s.section}` : ''}\n` +
|
|
269
|
+
`必读规范:${A.conventionsFile}\n`;
|
|
270
|
+
const basePrompt =
|
|
271
|
+
`实现迁移切片 ${s.id}(${s.title || ''})。骨架与路由已就位,你负责行为接线与语义还原。\n` +
|
|
272
|
+
skillLine(s.skills, A.skills) +
|
|
273
|
+
briefing +
|
|
274
|
+
`参考:${(A.contextFiles || []).join(', ')}\n` +
|
|
275
|
+
`【只准创建/修改 ${[].concat(s.targetPath).join(' 、 ')} 及其下文件】(注册页文件在列即由你实现,页面内容写进它本身,不另起壳外目录);需要动共享/壳文件的,不动手,写进返回值 blocked。\n` +
|
|
276
|
+
`完成标准(后续末端验收将逐条断言,你现在就照它实现到位):\n${s.expectation}\n` +
|
|
277
|
+
(s.selfTest ? `【自测前置】实现后立即自跑:\`${String(s.selfTest).replace(/^\$\s*/, '')}\`,修到全过再返回——不许把红着的自测留给检查点。\n` : '') +
|
|
278
|
+
`完成判定(诚实即效率):ok:true 仅当"完成标准"逐条落实、或确属平台无等价的项已逐条写入 deviations;` +
|
|
279
|
+
`deviations 每项写成对象 {kind, text, symbols, probe}:kind=no_equiv(鸿蒙无等价——symbols 列出你在 SDK d.ts 里逐个查过、确认不存在的 API 名,如 ["WaterFlowSections","onDidScroll"],probe 一句话说查法与结果;脚本会拿 symbols 去 d.ts 核,任一存在即退回)| approx(视觉/动画近似,行为契约不变)| policy(平台策略/权限限制)| source_stub(源码本就是桩);` +
|
|
280
|
+
`登记的降级先记"待裁决",收口时核实后才算数,写不出 probe 的"无等价"不会被豁免。` +
|
|
281
|
+
`可实现而未写完 = 如实 ok:false 并在 unresolved 列清(会安排续跑;虚报 ok:true 会在末端验收暴露并回炉,代价远高于续跑)。\n` +
|
|
282
|
+
`纪律:源码是绝对真源;不降级不占位;禁跑全工程构建(检查点统一做);` +
|
|
283
|
+
`传入了 skills 的,在 summary 末尾用一句话报告其是否提供了决定性证据。`;
|
|
284
|
+
// retries:0:超时/stall 死亡不许引擎原 prompt 从零重跑(实测单片报废可达 1–2 小时)。
|
|
285
|
+
// 死亡回到本层做增量续跑:先扫 targetPath 已落盘文件注入 prompt,只补缺口。
|
|
286
|
+
let r = asObj(await agent(basePrompt,
|
|
287
|
+
opt(M.implement, { label: `impl:${s.id}`, phase: 'Implement', schema: IMPL_SCHEMA, retries: 0 })), `impl:${s.id}`);
|
|
288
|
+
if (r === null) {
|
|
289
|
+
// node 单行代替 find:Windows 无同义 find(宿主 shell 差异),node 恒在(引擎前置依赖);targetPath 可为数组/文件
|
|
290
|
+
const done = await check(`node -e "const fs=require('fs');const w=(d,o=[])=>{try{if(fs.statSync(d).isFile()){o.push(d);return o}for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=d+'/'+e.name;e.isDirectory()?w(p,o):/\\.(ets|ts|json5?)$/.test(e.name)&&o.push(p)}}catch{};return o};console.log(process.argv.slice(1).flatMap(p=>w(p)).slice(0,40).join('\\n'))" ${[].concat(s.targetPath).map(p => `"${p}"`).join(' ')}`);
|
|
291
|
+
const doneList = (done.output || '').trim();
|
|
292
|
+
r = asObj(await agent(
|
|
293
|
+
`切片 ${s.id} 增量续跑:上一轮实现中途死亡(超时/断连),部分文件可能已落盘。\n` +
|
|
294
|
+
(doneList ? `已存在的交付文件(逐个查看现状,完好的不重写,只补缺口与未完成部分):\n${doneList}\n` : `目标目录尚无交付文件,从头实现。\n`) +
|
|
295
|
+
basePrompt,
|
|
296
|
+
opt(M.implement, { label: `impl:${s.id} 补`, phase: 'Implement', schema: IMPL_SCHEMA, retries: 0, cache: false })), `impl:${s.id} 补`);
|
|
297
|
+
}
|
|
298
|
+
// ok:false = 代理诚实报未完成(多为单元过大一轮没写完交付文件)。据此重派续跑,别放它进 checkpoint 蒙混。
|
|
299
|
+
let att = 0;
|
|
300
|
+
// 续跑者是隔离的新会话,没有首轮卡片:完整工单+纪律必须重附(此前只给 summary/unresolved 还禁读工单文件,续跑者无完成标准可依)
|
|
301
|
+
while (r !== null && r.ok === false && att < 2) {
|
|
302
|
+
att++;
|
|
303
|
+
r = asObj(await agent(`切片 ${s.id} 续跑 ${att}:上一轮 ok:false 未完成。已做:${String(r.summary || '').slice(0, 500)}\n仍需落地(把交付文件真正写到 ${s.targetPath} 下、写完再返回 ok:true;一轮写不完就先落地能完成的最小部分并如实 ok:false):\n- ${(r.unresolved || []).slice(0, 10).join('\n- ')}\n\n` + basePrompt,
|
|
304
|
+
opt(M.implement, { label: `impl:${s.id} 续${att}`, phase: 'Implement', schema: IMPL_SCHEMA, cache: false })), `impl:${s.id} 续${att}`);
|
|
305
|
+
}
|
|
306
|
+
if (r && r.blocked) { log(`⚠ ${s.id} 越权申报:${String(r.blocked).slice(0, 200)}`); blockedReports.push({ id: s.id, blocked: String(r.blocked).slice(0, 600) }); }
|
|
307
|
+
if (r === null) failSlice(s.id, `实现 agent 两轮死亡(含增量续跑)——resumeFrom + only:["${s.id}"] 续跑`);
|
|
308
|
+
// 诚实未完成 = 失败:此前 ok:false 耗尽续跑仍溜进检查点蒙混——构建/闭合测不出"功能没写",
|
|
309
|
+
// 欠账滚到末端验收变成最贵的验→修→复验循环(后台通知片实证)。
|
|
310
|
+
if (r.ok === false) failSlice(s.id, `切片 ${s.id} 自报未完成(续跑 ${att} 次后仍 ok:false)——未完成项:${(r.unresolved || []).slice(0, 5).join(';') || '(未列)'}。拆片或 resumeFrom + only:["${s.id}"] 续跑`);
|
|
311
|
+
return r;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 确定性静态门:构建 + selfTest + 闭合,各失败派一次定向修,仍红记 failed。均为退出码裁决、重跑同答案。
|
|
315
|
+
// finalPass=本层收口复检(他片已落定),不再延后。
|
|
316
|
+
async function checkpoint(s, impl, finalPass = false) {
|
|
317
|
+
return withLock(async () => {
|
|
318
|
+
if (buildPoisoned) failSlice(s.id, `构建已被切片 ${buildPoisoned} 毒化,本片快速失败——先修 ${buildPoisoned} 再 only 续跑`);
|
|
319
|
+
// 实现期登记的平台偏差当场过账(放串行段内写,避免并行写台账竞态)。
|
|
320
|
+
// 末端验收判据"已登记降级不计失败"依赖这一步——防止同一批偏差被逐轮重复裁决。
|
|
321
|
+
if (impl && Array.isArray(impl.deviations) && impl.deviations.length && A.ledgerCmd) {
|
|
322
|
+
// id 按内容散列:同一偏差重跑再报=同 id 同 note(ledger add 幂等跳过),不同偏差不撞车(此前按序号编 id,片重跑整批写失败)
|
|
323
|
+
const h = (t) => { let x = 5381; for (const c of String(t)) x = ((x * 33) ^ c.charCodeAt(0)) >>> 0; return x.toString(36).slice(0, 6); };
|
|
324
|
+
if (impl.deviations.length > 24) log(`⚠ ${s.id} 偏差 ${impl.deviations.length} 条只登记前 24 条——偏差这么多说明片划错或在自我豁免,复核工单`);
|
|
325
|
+
// 结构化:{kind,text,probe};字符串(旧形态/未分类)记 kind 缺失——同样 pending,收口裁决时补分类
|
|
326
|
+
const clean = (t, n) => String(t || '').replace(/["'\n\r]/g, ' ').slice(0, n);
|
|
327
|
+
const items = impl.deviations.slice(0, 24).map((d) => {
|
|
328
|
+
const o = (d && typeof d === 'object') ? d : { text: String(d) };
|
|
329
|
+
const note = clean(o.text, 160);
|
|
330
|
+
if (!note) return null;
|
|
331
|
+
const symbols = Array.isArray(o.symbols) ? o.symbols.map(x => clean(x, 80)).filter(Boolean).slice(0, 12) : [];
|
|
332
|
+
return { id: `deg-${s.id}-${h(note)}`, note, ...(o.kind ? { kind: String(o.kind) } : {}), ...(symbols.length ? { symbols } : {}), ...(o.probe ? { probe: clean(o.probe, 200) } : {}), status: 'pending', disposition: `实现期登记(切片 ${s.id})·待裁决` };
|
|
333
|
+
}).filter(Boolean);
|
|
334
|
+
if (items.length) await ledgerAddFile('degradations', items, `deg-${s.id}`);
|
|
335
|
+
}
|
|
336
|
+
if (impl && impl.blocked && A.ledgerCmd) {
|
|
337
|
+
const item = [{ id: `blk-${s.id}`, note: String(impl.blocked).replace(/["'\n\r]/g, ' ').slice(0, 300), disposition: `切片 ${s.id} 越权申报,待壳/共享件 owner 处置(3b 收口清账)` }];
|
|
338
|
+
await ledgerAddFile('debts', item, `blk-${s.id}`);
|
|
339
|
+
}
|
|
340
|
+
let b = await check(cmd(A.build));
|
|
341
|
+
const othersInFlight = (out) => { const bl = blameBuild(out, s); const inf = [...bl.others.keys()].filter(id => execSet.has(id) && !checkedIds.has(id) && !failReasons.has(id)); return (!bl.own.length && !bl.unknown.length && inf.length) ? inf : null; };
|
|
342
|
+
const defer = (why) => { log(`检查点[${s.id}]:${why}——本层收口复检`); return { id: s.id, status: 'deferred', s, impl: null }; };
|
|
343
|
+
if (!b.ok && !finalPass) {
|
|
344
|
+
const inf = othersInFlight(b.output);
|
|
345
|
+
if (inf) return defer(`构建红全在他片文件(${inf.join(', ')} 尚未落定),不判本片`);
|
|
346
|
+
// 本层已有不可归责的持续红:不再各自派修(同一个红会让每片都派一轮),统一延后到收口
|
|
347
|
+
if (levelUnstable) return defer('本层构建持续红且不可归责他片,不重复派修');
|
|
348
|
+
}
|
|
349
|
+
if (!b.ok) {
|
|
350
|
+
// 每片每阶段最多一轮构建修复:首过一轮、收口复检可再一轮(他片落定后同一处红可能已能修)
|
|
351
|
+
const key = `${s.id}:${finalPass ? 'final' : 'pass'}`;
|
|
352
|
+
if (!fixRounds.has(key)) {
|
|
353
|
+
fixRounds.add(key);
|
|
354
|
+
log(`检查点[${s.id}]:构建红,派修复`);
|
|
355
|
+
await agent(skillLine(A.skills || ['arkts-error-fixes', 'arkts-runtime-fix']) + `切片 ${s.id} 后构建失败。只改必要文件恢复绿灯,禁降级止血。构建输出:\n${(b.output || '').slice(-6000)}`,
|
|
356
|
+
opt(M.fix, { label: `fix-build:${s.id}`, phase: 'Checkpoint', cache: false }));
|
|
357
|
+
b = await check(cmd(A.build));
|
|
358
|
+
}
|
|
359
|
+
if (!b.ok) {
|
|
360
|
+
if (!finalPass) {
|
|
361
|
+
// 修后仍红:并行流水里多半是他片半成品(实证:整流续跑后同一份代码全绿)——延后,不在此判毒化连锁
|
|
362
|
+
const inf = othersInFlight(b.output);
|
|
363
|
+
if (inf) return defer(`修后剩余红全在他片文件(${inf.join(', ')})`);
|
|
364
|
+
levelUnstable = true;
|
|
365
|
+
return defer('修后仍红且不可归责他片——本层构建不稳,延后统一复检(不连锁判失败)');
|
|
366
|
+
}
|
|
367
|
+
// 收口复检(他片已落定)仍红:真毒化,本片失败,其余待复检片快速失败
|
|
368
|
+
buildPoisoned = s.id;
|
|
369
|
+
failSlice(s.id, `切片 ${s.id} 后构建无法恢复绿灯(resumeFrom + only:["${s.id}"] 续跑)`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (s.selfTest) {
|
|
373
|
+
const st = await check(cmd(s.selfTest));
|
|
374
|
+
if (!st.ok) {
|
|
375
|
+
log(`检查点[${s.id}]:自测门红,派修复`);
|
|
376
|
+
await agent(skillLine(A.skills || ['arkts-error-fixes', 'arkts-runtime-fix']) + `切片 ${s.id} 自测未过。按失败清单修复(只改 ${s.targetPath} 下文件):\n${(st.output || '').slice(-4000)}`,
|
|
377
|
+
opt(M.fix, { label: `fix-selftest:${s.id}`, phase: 'Checkpoint', cache: false }));
|
|
378
|
+
const st2 = await check(cmd(s.selfTest));
|
|
379
|
+
if (!st2.ok) failSlice(s.id, `切片 ${s.id} 自测门未清(resumeFrom + only:["${s.id}"])`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const c = await check(cmd(A.closureSlice));
|
|
383
|
+
if (!c.ok) {
|
|
384
|
+
log(`检查点[${s.id}]:切片级闭合有硬违规,派修复`);
|
|
385
|
+
await agent(`切片 ${s.id} 触发闭合硬违规(跳转未注册名/资源悬空类)。按输出逐条修复:\n${(c.output || '').slice(-4000)}`,
|
|
386
|
+
opt(M.fix, { label: `fix-closure:${s.id}`, phase: 'Checkpoint', cache: false }));
|
|
387
|
+
const c2 = await check(cmd(A.closureSlice));
|
|
388
|
+
if (!c2.ok) failSlice(s.id, `切片 ${s.id} 闭合硬违规未清`);
|
|
389
|
+
}
|
|
390
|
+
// 只到 checked(确定性证据);passed 由末端 acceptance 判断性验收过账
|
|
391
|
+
checkedIds.add(s.id);
|
|
392
|
+
await ledger(`status --type slices --id ${s.id} --to checked --evidence "build+selfTest+closure(slice) exit0"`);
|
|
393
|
+
return { id: s.id, status: 'checked' };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------- 分层无栅栏流水:实现 → 确定性检查点(无判断性验收) ----------
|
|
398
|
+
for (let l = 0; l <= maxLevel; l++) {
|
|
399
|
+
const batch = slices.filter(s => levelOf.get(s.id) === l);
|
|
400
|
+
if (!batch.length) continue;
|
|
401
|
+
log(`第 ${l} 层:${batch.length} 个切片进入流水(${batch.map(s => s.id).join(', ')})`);
|
|
402
|
+
levelUnstable = false;
|
|
403
|
+
const statusNow = new Map(results.filter(Boolean).map(r => [r.id, r.status]));
|
|
404
|
+
const out = await pipeline(
|
|
405
|
+
batch,
|
|
406
|
+
(s) => {
|
|
407
|
+
const badDeps = (s.deps || []).filter(d => execSet.has(d) && statusNow.get(d) !== 'checked');
|
|
408
|
+
if (badDeps.length) failSlice(s.id, `依赖切片 ${badDeps.join(',')} 未过检查点,本片不实现(依赖修好后 only:["${s.id}"] 续跑)`);
|
|
409
|
+
return implementSlice(s).then(r => ({ s, impl: r }));
|
|
410
|
+
},
|
|
411
|
+
(x) => checkpoint(x.s, x.impl),
|
|
412
|
+
);
|
|
413
|
+
// 延后复检:检查点时构建红全在他片在飞文件的片,本层其余片落定后再过一次检查点(不再延后;实现期登记已写过账,impl 传空)
|
|
414
|
+
for (let i = 0; i < out.length; i++) {
|
|
415
|
+
const r = out[i];
|
|
416
|
+
if (!r || r.status !== 'deferred') continue;
|
|
417
|
+
log(`本层收口复检 ${r.id}(此前构建红归他片文件)`);
|
|
418
|
+
try { out[i] = await checkpoint(r.s, null, true); } catch { out[i] = null; }
|
|
419
|
+
}
|
|
420
|
+
out.forEach((r, i) => results.push(r || { id: batch[i].id, status: 'failed', evidence: failReasons.get(batch[i].id) || '流水中途死亡——resumeFrom 续跑' }));
|
|
421
|
+
|
|
422
|
+
// ---- 波级装机冒烟(A.smoke 配置且本层有带 route 的页面片才跑)----
|
|
423
|
+
// 判定阶梯见 smoke.mjs:环境阻塞(3)=记账不派修(装机失败/无设备/hap 过期,fix agent 修不了);
|
|
424
|
+
// 崩溃(10)=修复一轮,仍崩即停轮(带病铺片无意义);立案(2)分两类:接线类(misland/nohook/not-foreground/bounced/scaffold)修一轮、
|
|
425
|
+
// 仍在则末端裁决;软立案(forwarded/hollow/expect-miss/nodump)只记案不派修。
|
|
426
|
+
if (A.smoke) {
|
|
427
|
+
const okIds = new Set(results.filter(r => r && r.status === 'checked').map(r => r.id));
|
|
428
|
+
const routes = batch.filter(s => s.route && okIds.has(s.id)).map(s => s.route);
|
|
429
|
+
if (routes.length) {
|
|
430
|
+
const smokeOut = (cmd(A.smoke).match(/--out\s+["']?([^\s"']+)/) || [, 'spec/smoke'])[1]; // 不吞引号(引号包裹的路径)
|
|
431
|
+
const smokeCmd = `${cmd(A.smoke)} --routes ${routes.join(',')} --json "${smokeOut}/wave-${l}.json"`;
|
|
432
|
+
let sm = await check(smokeCmd);
|
|
433
|
+
const out0 = sm.output || '';
|
|
434
|
+
if (sm.ok) {
|
|
435
|
+
log(`✓ 波 ${l} 冒烟绿(${routes.length} 页:装机·前台·着陆·页面子树均核)`);
|
|
436
|
+
smokeWaves.push({ wave: l, status: 'ok', routes });
|
|
437
|
+
} else if (/环境阻塞|exit 3|跳过/.test(out0)) {
|
|
438
|
+
const why = out0.trim().split('\n').filter(Boolean).pop() || '';
|
|
439
|
+
log(`▲ 波 ${l} 冒烟环境阻塞(不派修,收口报数):${why.slice(0, 200)}`);
|
|
440
|
+
smokeWaves.push({ wave: l, status: 'blocked', routes, detail: why.slice(0, 300) });
|
|
441
|
+
} else {
|
|
442
|
+
const crashed = /崩溃|jscrash|cppcrash/.test(out0);
|
|
443
|
+
// 只对崩溃与接线类派修(misland/nohook/not-foreground/bounced/scaffold);hollow(空态或未实现,回模型裁决)、
|
|
444
|
+
// forwarded(应用自身转发链,4b 沿链走查)、expect-miss、nodump 只记案不派修——实证 w0 把登录分派转发判成接线问题派修 9 分钟,修复代理只做了豁免。
|
|
445
|
+
const tally = (out0.match(/页未过(([^)]*))/) || [])[1] || '';
|
|
446
|
+
const hard = crashed || /\b(misland|nohook|not-foreground|bounced|scaffold)\b/.test(tally);
|
|
447
|
+
if (!hard) {
|
|
448
|
+
log(`▲ 波 ${l} 冒烟软立案(${tally || '见报告'})——非崩溃/接线类,不派修,末端验收裁决`);
|
|
449
|
+
smokeWaves.push({ wave: l, status: 'cases', routes, detail: out0.trim().split('\n').filter(Boolean).pop() || '' });
|
|
450
|
+
} else {
|
|
451
|
+
log(`▲ 波 ${l} 冒烟未过(${crashed ? '崩溃' : tally}),派修复`);
|
|
452
|
+
await agent(skillLine(A.skills) + `波 ${l} 装机冒烟${crashed ? '崩溃' : '未过'}。报告语义:misland/nohook=接线问题(EntryAbility smokeRoute 透传、Index 钩子、route_map 注册与页面 Builder 导出);bounced=推入即弹回(参数判型/pop 防御,参数经序列化后 instanceof 恒 false);scaffold=注册页仍是骨架占位(片未实现);not-foreground=应用未起(崩溃/白屏)。forwarded/hollow/expect-miss 不在本次修复范围(末端裁决),不要为它们改代码。按输出定位并只修涉事页面(faultlog 栈/控件树/截图见 ${smokeOut}/):\n${out0.slice(-5000)}`,
|
|
453
|
+
opt(M.fix, { label: `fix-smoke-w${l}`, phase: 'Implement', cache: false }));
|
|
454
|
+
sm = await check(smokeCmd);
|
|
455
|
+
const out1 = sm.output || '';
|
|
456
|
+
if (!sm.ok && /崩溃|jscrash|cppcrash/.test(out1)) throw new Error(`波 ${l} 冒烟崩溃修复未收敛——先修崩溃再 resumeFrom 续跑(报告 ${smokeOut}/wave-${l}.json)`);
|
|
457
|
+
if (!sm.ok) { log(`▲ 波 ${l} 冒烟仍有立案(不阻塞,末端验收裁决)`); smokeWaves.push({ wave: l, status: 'cases', routes, detail: out1.trim().split('\n').filter(Boolean).pop() || '' }); }
|
|
458
|
+
else { log(`✓ 波 ${l} 修后冒烟绿`); smokeWaves.push({ wave: l, status: 'ok-after-fix', routes }); }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------- 集成级闭合(确定性全局判定;有失败片先收账不跑闭合) ----------
|
|
466
|
+
phase('Integration');
|
|
467
|
+
const bad = results.filter(r => !r || r.status !== 'checked');
|
|
468
|
+
if (bad.length) {
|
|
469
|
+
log(`跳过集成级闭合:${bad.length} 个切片未过检查点——先收账(见收口 throw 逐片原因),修好续跑再闭合`);
|
|
470
|
+
} else {
|
|
471
|
+
const integ = await check(cmd(A.closureIntegration));
|
|
472
|
+
if (!integ.ok) {
|
|
473
|
+
log('集成级闭合有硬违规,派一次修复后复检');
|
|
474
|
+
await agent(`集成级闭合硬违规,逐条修复:\n${(integ.output || '').slice(-6000)}`, opt(M.fix, { label: 'fix-integration', phase: 'Integration', cache: false }));
|
|
475
|
+
const integ2 = await check(cmd(A.closureIntegration));
|
|
476
|
+
if (!integ2.ok) throw new Error('mig_slices FAILED: 集成级闭合硬违规未清');
|
|
477
|
+
}
|
|
478
|
+
log('集成级闭合完成(确定性全局判定通过;判断性验收由末端 acceptance 一遍完成)');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const checked = results.filter(r => r && r.status === 'checked');
|
|
482
|
+
log(`实现流水收口:${checked.length}/${slices.length} 到 checked(passed 待末端 acceptance 过账)${bad.length ? `;未过 ${bad.map(b => b.id).join(',')}` : ''}`);
|
|
483
|
+
if (blockedReports.length) log(`⚠ 越权申报 ${blockedReports.length} 条(台账 debts blk-*,见 summary.blocked)——壳/共享件改动由主会话处置后再进末端验收:${blockedReports.map(b => b.id).join(', ')}`);
|
|
484
|
+
if (smokeWaves.some(w => w.status === 'blocked')) log(`⚠ 波级冒烟环境阻塞 ${smokeWaves.filter(w => w.status === 'blocked').length} 波(装机/设备/hap 问题,无运行时证据)——见 summary.smoke,报告须如实标"未冒烟"`);
|
|
485
|
+
if (ledgerWarns.length) log(`⚠ 台账写入失败 ${ledgerWarns.length} 次——报告前须核对补账`);
|
|
486
|
+
const summary = { goal: A.goal || '', slicesTotal: slices.length, checked: checked.map(r => r.id), failed: bad.map(r => ({ id: r.id, evidence: String(r.evidence).slice(0, 300) })),
|
|
487
|
+
...(blockedReports.length ? { blocked: blockedReports } : {}), ...(smokeWaves.length ? { smoke: smokeWaves } : {}), ...(ledgerWarns.length ? { ledgerWarns } : {}) };
|
|
488
|
+
if (bad.length) {
|
|
489
|
+
throw new Error(`mig_slices FAILED: ${bad.length} 个切片未到 checked(${bad.map(b => b.id).join(', ')})。修复后 only:[${bad.map(b => `"${b.id}"`).join(',')}] + resumeFrom 续跑。`);
|
|
490
|
+
}
|
|
491
|
+
return summary;
|