@harmonyos-arkts/droid2hmos 0.0.7 → 0.0.8
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 +12 -14
- package/dist/cli.js +1805 -738
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,19 +12,19 @@ function quoteWinArg(arg) {
|
|
|
12
12
|
if (!/[\s"&|<>()^%]/.test(arg)) return arg;
|
|
13
13
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
14
14
|
}
|
|
15
|
-
function windowsCommandLine(
|
|
16
|
-
return [
|
|
15
|
+
function windowsCommandLine(command2, args) {
|
|
16
|
+
return [command2, ...args].map((value) => quoteWinArg(String(value))).join(" ");
|
|
17
17
|
}
|
|
18
|
-
function resolveSpawnCommand(
|
|
19
|
-
if (platform !== "win32") return { command:
|
|
20
|
-
return { command: windowsCommandLine(
|
|
18
|
+
function resolveSpawnCommand(command2, args, platform = process.platform) {
|
|
19
|
+
if (platform !== "win32") return { command: command2, args: [...args], shell: false };
|
|
20
|
+
return { command: windowsCommandLine(command2, args), args: [], shell: true };
|
|
21
21
|
}
|
|
22
|
-
function spawnCommandSync(
|
|
23
|
-
const invocation = resolveSpawnCommand(
|
|
22
|
+
function spawnCommandSync(command2, args, options = {}) {
|
|
23
|
+
const invocation = resolveSpawnCommand(command2, args);
|
|
24
24
|
return nodeSpawnSync(invocation.command, invocation.args, { ...options, shell: invocation.shell });
|
|
25
25
|
}
|
|
26
|
-
function spawnCommand(
|
|
27
|
-
const invocation = resolveSpawnCommand(
|
|
26
|
+
function spawnCommand(command2, args, options = {}) {
|
|
27
|
+
const invocation = resolveSpawnCommand(command2, args);
|
|
28
28
|
return nodeSpawn(invocation.command, invocation.args, { ...options, shell: invocation.shell });
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -110,7 +110,7 @@ function checkAndroidBuild(argv) {
|
|
|
110
110
|
present,
|
|
111
111
|
kind: "android-apk-inventory",
|
|
112
112
|
status,
|
|
113
|
-
summary: present ? `\u53D1\u73B0 ${artifacts.length} \u4E2A APK \u6784\u5EFA\u4EA7\u7269\
|
|
113
|
+
summary: present ? `\u53D1\u73B0 ${artifacts.length} \u4E2A APK \u6784\u5EFA\u4EA7\u7269\uFF08\u4EC5\u4F5C\u53C2\u8003\u4FE1\u606F\uFF09` : "\u672A\u53D1\u73B0 APK \u6784\u5EFA\u4EA7\u7269\uFF08\u8FC1\u79FB\u4E0D\u4F9D\u8D56 Android \u7F16\u8BD1\uFF09",
|
|
114
114
|
artifacts: artifacts.length,
|
|
115
115
|
report: reportPath
|
|
116
116
|
};
|
|
@@ -138,8 +138,8 @@ function sourceRoots(projectRootArgument) {
|
|
|
138
138
|
const projectRoot = path3.resolve(projectRootArgument);
|
|
139
139
|
const config = readJson(path3.join(migrationDirectory(projectRoot), "config.json"));
|
|
140
140
|
const androidRoot = path3.resolve(projectRoot, config?.androidProject ?? projectRoot);
|
|
141
|
-
const
|
|
142
|
-
return { projectRoot, androidRoot, harmonyRoot };
|
|
141
|
+
const harmonyRoot2 = path3.resolve(projectRoot, config?.harmonyProject ?? "ohos");
|
|
142
|
+
return { projectRoot, androidRoot, harmonyRoot: harmonyRoot2 };
|
|
143
143
|
}
|
|
144
144
|
function inside(root, absolute) {
|
|
145
145
|
const relative2 = path3.relative(root, absolute);
|
|
@@ -147,10 +147,10 @@ function inside(root, absolute) {
|
|
|
147
147
|
}
|
|
148
148
|
function sourceFile(projectRoot, value) {
|
|
149
149
|
if (typeof value !== "string" || !value.trim()) throw new Error("Android source path must be a non-empty string");
|
|
150
|
-
const { androidRoot, harmonyRoot } = sourceRoots(projectRoot);
|
|
150
|
+
const { androidRoot, harmonyRoot: harmonyRoot2 } = sourceRoots(projectRoot);
|
|
151
151
|
const absolute = path3.resolve(androidRoot, value);
|
|
152
152
|
if (!inside(androidRoot, absolute)) throw new Error(`Android source must stay inside the managed Android project: ${value}`);
|
|
153
|
-
if (inside(
|
|
153
|
+
if (inside(harmonyRoot2, absolute)) throw new Error(`Android source cannot point into the HarmonyOS project: ${value}`);
|
|
154
154
|
if (!fs3.existsSync(absolute) || !fs3.statSync(absolute).isFile()) throw new Error(`Android source file not found: ${value}`);
|
|
155
155
|
const realRoot = fs3.realpathSync(androidRoot);
|
|
156
156
|
const realFile = fs3.realpathSync(absolute);
|
|
@@ -161,12 +161,12 @@ function sourceFile(projectRoot, value) {
|
|
|
161
161
|
}
|
|
162
162
|
function managedHarmonyFile(projectRootArgument, value) {
|
|
163
163
|
if (typeof value !== "string" || !value.trim()) throw new Error("HarmonyOS path must be a non-empty string");
|
|
164
|
-
const { projectRoot, harmonyRoot } = sourceRoots(projectRootArgument);
|
|
164
|
+
const { projectRoot, harmonyRoot: harmonyRoot2 } = sourceRoots(projectRootArgument);
|
|
165
165
|
const absolute = path3.resolve(projectRoot, value);
|
|
166
|
-
if (!inside(
|
|
166
|
+
if (!inside(harmonyRoot2, absolute)) throw new Error(`HarmonyOS file must stay inside the configured HarmonyOS project: ${value}`);
|
|
167
167
|
if (!fs3.existsSync(absolute) || !fs3.statSync(absolute).isFile()) throw new Error(`HarmonyOS file not found: ${value}`);
|
|
168
|
-
if (!fs3.existsSync(
|
|
169
|
-
const realRoot = fs3.realpathSync(
|
|
168
|
+
if (!fs3.existsSync(harmonyRoot2) || !fs3.statSync(harmonyRoot2).isDirectory()) throw new Error(`Configured HarmonyOS project not found: ${harmonyRoot2}`);
|
|
169
|
+
const realRoot = fs3.realpathSync(harmonyRoot2);
|
|
170
170
|
const realFile = fs3.realpathSync(absolute);
|
|
171
171
|
if (!inside(realRoot, realFile)) throw new Error(`HarmonyOS file resolves outside the configured HarmonyOS project: ${value}`);
|
|
172
172
|
return { absolute, relative: path3.relative(projectRoot, absolute).replaceAll("\\", "/") };
|
|
@@ -183,14 +183,14 @@ function provenanceForFiles(projectRoot, values) {
|
|
|
183
183
|
return [...result.values()].sort((left, right) => left.androidFile.localeCompare(right.androidFile));
|
|
184
184
|
}
|
|
185
185
|
function managedSourceManifest(projectRootArgument) {
|
|
186
|
-
const { projectRoot, androidRoot, harmonyRoot } = sourceRoots(projectRootArgument);
|
|
186
|
+
const { projectRoot, androidRoot, harmonyRoot: harmonyRoot2 } = sourceRoots(projectRootArgument);
|
|
187
187
|
if (!fs3.existsSync(androidRoot) || !fs3.statSync(androidRoot).isDirectory()) throw new Error(`Managed Android project not found: ${androidRoot}`);
|
|
188
188
|
const members = [];
|
|
189
189
|
const visit = (directory) => {
|
|
190
190
|
for (const entry of fs3.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
191
191
|
if (ignoredSourceParts.has(entry.name)) continue;
|
|
192
192
|
const absolute = path3.join(directory, entry.name);
|
|
193
|
-
if (inside(
|
|
193
|
+
if (inside(harmonyRoot2, absolute)) continue;
|
|
194
194
|
if (entry.isSymbolicLink()) continue;
|
|
195
195
|
if (entry.isDirectory()) visit(absolute);
|
|
196
196
|
else if (entry.isFile()) {
|
|
@@ -387,7 +387,9 @@ function auditSourceOrder(projectRootArgument, items, options = {}) {
|
|
|
387
387
|
}
|
|
388
388
|
function auditSourceClosure(projectRootArgument, items) {
|
|
389
389
|
const projectRoot = path3.resolve(projectRootArgument);
|
|
390
|
-
const
|
|
390
|
+
const tasksRoot = path3.join(migrationDirectory(projectRoot), "tasks");
|
|
391
|
+
const executionGraph = path3.join(tasksRoot, "execution-graph.json");
|
|
392
|
+
const graph = readJson(fs3.existsSync(executionGraph) ? executionGraph : path3.join(tasksRoot, "graph.json"));
|
|
391
393
|
const auditable = items.filter((item) => ["implemented", "verified"].includes(item?.status) && item?.kind !== "feature");
|
|
392
394
|
if (!graph || !Array.isArray(graph.tasks)) return { auditVersion: "1.0.0", status: "unavailable", auditedRefs: [], findings: ["Task Plan source closure is unavailable; sourceProvenance hashes remain enforceable"] };
|
|
393
395
|
const findings = [];
|
|
@@ -496,7 +498,7 @@ function initMigrationReport(argv) {
|
|
|
496
498
|
const fail = (message) => {
|
|
497
499
|
throw new Error(message);
|
|
498
500
|
};
|
|
499
|
-
const
|
|
501
|
+
const readJson9 = (file) => JSON.parse(fs4.readFileSync(file, "utf8"));
|
|
500
502
|
const writeJson2 = (file, value) => {
|
|
501
503
|
fs4.mkdirSync(path4.dirname(file), { recursive: true });
|
|
502
504
|
fs4.writeFileSync(file, `${JSON.stringify(value, null, 2)}
|
|
@@ -510,7 +512,7 @@ function initMigrationReport(argv) {
|
|
|
510
512
|
const relReport = (file) => path4.relative(reportRoot, file).replaceAll("\\", "/");
|
|
511
513
|
const relProject = (file) => path4.relative(workspaceRoot, file).replaceAll("\\", "/");
|
|
512
514
|
const reportItemValues = (document) => [document.page, ...document.controls ?? [], ...document.entries ?? [], ...document.interactions ?? [], ...document.items ?? []].filter(Boolean);
|
|
513
|
-
const
|
|
515
|
+
const confinedReportFile2 = (relative2) => {
|
|
514
516
|
if (typeof relative2 !== "string" || !relative2.trim() || path4.isAbsolute(relative2)) fail(`Legacy report shard path must be report-relative: ${String(relative2)}`);
|
|
515
517
|
const absolute = path4.resolve(reportRoot, relative2);
|
|
516
518
|
const relation = path4.relative(reportRoot, absolute);
|
|
@@ -523,18 +525,18 @@ function initMigrationReport(argv) {
|
|
|
523
525
|
return realFile;
|
|
524
526
|
};
|
|
525
527
|
if (!fs4.existsSync(indexPath)) fail(`Deterministic SPEC index not found: ${indexPath}`);
|
|
526
|
-
const specIndex =
|
|
528
|
+
const specIndex = readJson9(indexPath);
|
|
527
529
|
if (specIndex.shardFormatVersion !== "2.0.0" || !specIndex.files?.facts) fail("report init requires deterministic SPEC facts in shardFormatVersion 2.0.0");
|
|
528
530
|
if (!specIndex.factsSha256) fail("SPEC index does not contain factsSha256");
|
|
529
531
|
const specFile = (file) => path4.join(specRoot, file);
|
|
530
532
|
const files = specIndex.files.facts;
|
|
531
|
-
const appFacts =
|
|
532
|
-
const pageIndex =
|
|
533
|
-
const interactionFacts =
|
|
534
|
-
const unresolvedFacts =
|
|
533
|
+
const appFacts = readJson9(specFile(files.app));
|
|
534
|
+
const pageIndex = readJson9(specFile(files.pages));
|
|
535
|
+
const interactionFacts = readJson9(specFile(files.interactions));
|
|
536
|
+
const unresolvedFacts = readJson9(specFile(files.unresolved));
|
|
535
537
|
const reportIndexPath = path4.join(reportRoot, "index.json");
|
|
536
538
|
if (fs4.existsSync(reportIndexPath)) {
|
|
537
|
-
const previous =
|
|
539
|
+
const previous = readJson9(reportIndexPath);
|
|
538
540
|
if (previous.baseline?.factsSha256 !== specIndex.factsSha256) fail(`Existing report targets facts ${previous.baseline?.factsSha256 ?? "<unknown>"}; current facts are ${specIndex.factsSha256}. Remove .migration/report or preserve it under another run.`);
|
|
539
541
|
if (previous.reportVersion === "1.1.0") {
|
|
540
542
|
const shardFiles = [
|
|
@@ -543,10 +545,10 @@ function initMigrationReport(argv) {
|
|
|
543
545
|
previous.interactionsFile,
|
|
544
546
|
previous.discoveriesFile
|
|
545
547
|
].filter(Boolean);
|
|
546
|
-
const migrationTargets = shardFiles.map((relative2) =>
|
|
548
|
+
const migrationTargets = shardFiles.map((relative2) => confinedReportFile2(relative2));
|
|
547
549
|
let migratedFiles = 0;
|
|
548
550
|
for (const file of migrationTargets) {
|
|
549
|
-
const document =
|
|
551
|
+
const document = readJson9(file);
|
|
550
552
|
for (const value of reportItemValues(document)) if (!Array.isArray(value.sourceProvenance)) value.sourceProvenance = [];
|
|
551
553
|
if (document.reportShardVersion === "1.1.0") document.reportShardVersion = REPORT_VERSION;
|
|
552
554
|
writeJson2(file, document);
|
|
@@ -579,7 +581,7 @@ function initMigrationReport(argv) {
|
|
|
579
581
|
const pages = [];
|
|
580
582
|
for (const descriptor of pageIndex.pages ?? []) {
|
|
581
583
|
const factPath = specFile(descriptor.file);
|
|
582
|
-
const facts =
|
|
584
|
+
const facts = readJson9(factPath);
|
|
583
585
|
if (!facts.surface?.surfaceRef) fail(`Invalid surface facts shard: ${factPath}`);
|
|
584
586
|
const output = path4.join(reportRoot, "pages", path4.basename(descriptor.file));
|
|
585
587
|
const controlRefByCarrier = new Map((facts.controls ?? []).map((value) => [value.sourceCarrierRef, value.controlRef]));
|
|
@@ -605,7 +607,7 @@ function initMigrationReport(argv) {
|
|
|
605
607
|
let unassignedEntriesFile = null;
|
|
606
608
|
if (pageIndex.unassignedEntriesFile) {
|
|
607
609
|
const factPath = specFile(pageIndex.unassignedEntriesFile);
|
|
608
|
-
const facts =
|
|
610
|
+
const facts = readJson9(factPath);
|
|
609
611
|
const output = path4.join(reportRoot, "pages", "unassigned.json");
|
|
610
612
|
const shard = { reportShardVersion: REPORT_VERSION, kind: "unassigned-entry-report", factsFile: relProject(factPath), entries: (facts.entries ?? []).map((value) => item(value.entryRef, "entry", value.displayLabel || value.event || value.entryRef, { controlRef: null })) };
|
|
611
613
|
if (writeIfMissing(output, shard)) createdFiles++;
|
|
@@ -2147,9 +2149,12 @@ function initHmosProject(options) {
|
|
|
2147
2149
|
// src/commands/run-appgraph.ts
|
|
2148
2150
|
import { rmSync } from "fs";
|
|
2149
2151
|
import { join, resolve as resolve2 } from "path";
|
|
2150
|
-
function runAppGraph(argv) {
|
|
2152
|
+
function runAppGraph(argv, runCommand = spawnCommandSync) {
|
|
2153
|
+
if (argv.some((arg) => arg === "--resolve-dependencies" || arg.startsWith("--resolve-dependencies="))) {
|
|
2154
|
+
throw new Error("Android Gradle \u4F9D\u8D56\u5206\u6790\u5165\u53E3\u6682\u65F6\u5173\u95ED\uFF0C\u8BF7\u79FB\u9664 --resolve-dependencies \u540E\u6267\u884C\u6E90\u7801\u9759\u6001\u5206\u6790\u3002");
|
|
2155
|
+
}
|
|
2151
2156
|
const runManaged = (args) => {
|
|
2152
|
-
const result =
|
|
2157
|
+
const result = runCommand("appgraph", args, { stdio: "inherit" });
|
|
2153
2158
|
if (result.error) {
|
|
2154
2159
|
throw new Error(`\u65E0\u6CD5\u6267\u884C appgraph \u547D\u4EE4\uFF1A${result.error.message}\u3002\u8BF7\u5148\u5B89\u88C5 AppGraph \u5E76\u786E\u4FDD appgraph \u5728 PATH \u4E2D\u3002`);
|
|
2155
2160
|
}
|
|
@@ -2164,10 +2169,10 @@ function runAppGraph(argv) {
|
|
|
2164
2169
|
const projectRoot = resolve2(projectPath);
|
|
2165
2170
|
ensureMigrationWorkspace(projectRoot);
|
|
2166
2171
|
const outputPath = join(migrationDirectory(projectRoot), "spec");
|
|
2167
|
-
const
|
|
2172
|
+
const command2 = ["spec-analysis", projectRoot, "--out", outputPath];
|
|
2168
2173
|
const graphIndex = argv.indexOf("--graph");
|
|
2169
|
-
if (graphIndex >= 0 && argv[graphIndex + 1])
|
|
2170
|
-
const analysis = runManaged(
|
|
2174
|
+
if (graphIndex >= 0 && argv[graphIndex + 1]) command2.push("--graph", argv[graphIndex + 1]);
|
|
2175
|
+
const analysis = runManaged(command2);
|
|
2171
2176
|
if ((analysis.status ?? 1) !== 0) return analysis.status ?? 1;
|
|
2172
2177
|
console.log(JSON.stringify(renderSpecHtml([join(outputPath, "index.json"), join(outputPath, "spec.html")]), null, 2));
|
|
2173
2178
|
const legacyDirectory = join(projectRoot, ".migration", "appgraph");
|
|
@@ -2177,18 +2182,19 @@ function runAppGraph(argv) {
|
|
|
2177
2182
|
return 0;
|
|
2178
2183
|
}
|
|
2179
2184
|
|
|
2185
|
+
// src/commands/harmony-toolchain.ts
|
|
2186
|
+
import { execFileSync } from "child_process";
|
|
2187
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2188
|
+
import path15 from "path";
|
|
2189
|
+
import JSON52 from "json5";
|
|
2190
|
+
|
|
2180
2191
|
// src/commands/run-config-command.ts
|
|
2181
2192
|
import { closeSync as closeSync2, createWriteStream, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
|
|
2182
2193
|
import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve3 } from "path";
|
|
2183
2194
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
2184
|
-
function androidProjectRoot(absoluteConfig, androidProject) {
|
|
2185
|
-
const workspaceParent = resolve3(dirname2(absoluteConfig), "..", "..");
|
|
2186
|
-
return typeof androidProject === "string" && androidProject ? resolve3(workspaceParent, androidProject) : workspaceParent;
|
|
2187
|
-
}
|
|
2188
2195
|
var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
const value = String(command3 ?? "").trim();
|
|
2196
|
+
function validateFiniteCommand(command2) {
|
|
2197
|
+
const value = String(command2 ?? "").trim();
|
|
2192
2198
|
const errors = [];
|
|
2193
2199
|
if (!value) return ["\u547D\u4EE4\u4E3A\u7A7A"];
|
|
2194
2200
|
if (value.includes("|")) errors.push("\u7981\u6B62\u5728\u53D7\u7BA1\u547D\u4EE4\u4E2D\u4F7F\u7528\u8F93\u51FA\u7BA1\u9053\uFF1B\u65E5\u5FD7\u7531\u8FD0\u884C\u5668\u76F4\u63A5\u5199\u6587\u4EF6");
|
|
@@ -2205,47 +2211,9 @@ function validateFiniteCommand(command3) {
|
|
|
2205
2211
|
}
|
|
2206
2212
|
return errors;
|
|
2207
2213
|
}
|
|
2208
|
-
function
|
|
2209
|
-
if (!configPath || !key) throw new Error("\u7F3A\u5C11 configPath \u6216 command key");
|
|
2210
|
-
if (!COMMAND_KEYS.has(key)) throw new Error(`\u4E0D\u652F\u6301\u7684 command key: ${key}`);
|
|
2211
|
-
const absoluteConfig = resolve3(configPath);
|
|
2212
|
-
const config = JSON.parse(readFileSync2(absoluteConfig, "utf8"));
|
|
2213
|
-
const configuredCommand = config.commands?.[key];
|
|
2214
|
-
if (configuredCommand === null) return { status: "not_applicable", key, summary: `${key} \u672A\u914D\u7F6E` };
|
|
2215
|
-
for (const token of requiredTokens) {
|
|
2216
|
-
if (!String(configuredCommand).includes(token)) {
|
|
2217
|
-
return { status: "rejected", key, errors: [`${key} \u7F3A\u5C11\u5FC5\u8981\u6A21\u677F ${token}`], summary: `${key} \u4E0D\u652F\u6301\u672C\u6B21\u589E\u91CF\u9009\u62E9\u5668` };
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
let command3 = String(configuredCommand);
|
|
2221
|
-
for (const [token, value] of Object.entries(replacements)) command3 = command3.replaceAll(token, String(value));
|
|
2222
|
-
const projectRoot = androidProjectRoot(absoluteConfig, config.androidProject);
|
|
2223
|
-
const configuredCwd = key.startsWith("android") ? config.androidProject : config.harmonyProject;
|
|
2224
|
-
const cwd = isAbsolute(configuredCwd) ? configuredCwd : resolve3(projectRoot, configuredCwd);
|
|
2225
|
-
return runFiniteCommand({ command: command3, key, cwd, projectRoot, timeoutMs, runner });
|
|
2226
|
-
}
|
|
2227
|
-
function runConfigCommandAsync({ configPath, key, timeoutMs = DEFAULT_TIMEOUT_MS, replacements = {}, requiredTokens = [] }) {
|
|
2228
|
-
if (!configPath || !key) throw new Error("\u7F3A\u5C11 configPath \u6216 command key");
|
|
2229
|
-
if (!COMMAND_KEYS.has(key)) throw new Error(`\u4E0D\u652F\u6301\u7684 command key: ${key}`);
|
|
2230
|
-
const absoluteConfig = resolve3(configPath);
|
|
2231
|
-
const config = JSON.parse(readFileSync2(absoluteConfig, "utf8"));
|
|
2232
|
-
const configuredCommand = config.commands?.[key];
|
|
2233
|
-
if (configuredCommand === null) return Promise.resolve({ status: "not_applicable", key, summary: `${key} \u672A\u914D\u7F6E` });
|
|
2234
|
-
for (const token of requiredTokens) {
|
|
2235
|
-
if (!String(configuredCommand).includes(token)) {
|
|
2236
|
-
return Promise.resolve({ status: "rejected", key, errors: [`${key} \u7F3A\u5C11\u5FC5\u8981\u6A21\u677F ${token}`], summary: `${key} \u4E0D\u652F\u6301\u672C\u6B21\u589E\u91CF\u9009\u62E9\u5668` });
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
let command3 = String(configuredCommand);
|
|
2240
|
-
for (const [token, value] of Object.entries(replacements)) command3 = command3.replaceAll(token, String(value));
|
|
2241
|
-
const projectRoot = androidProjectRoot(absoluteConfig, config.androidProject);
|
|
2242
|
-
const configuredCwd = key.startsWith("android") ? config.androidProject : config.harmonyProject;
|
|
2243
|
-
const cwd = isAbsolute(configuredCwd) ? configuredCwd : resolve3(projectRoot, configuredCwd);
|
|
2244
|
-
return runFiniteCommandAsync({ command: command3, key, cwd, projectRoot, timeoutMs });
|
|
2245
|
-
}
|
|
2246
|
-
function runFiniteCommand({ command: command3, key, cwd, projectRoot, timeoutMs = DEFAULT_TIMEOUT_MS, runner = spawnSync2 }) {
|
|
2214
|
+
function runFiniteCommand({ command: command2, key, cwd, projectRoot, timeoutMs = DEFAULT_TIMEOUT_MS, runner = spawnSync2 }) {
|
|
2247
2215
|
if (!key || !cwd || !projectRoot) throw new Error("\u7F3A\u5C11 key\u3001cwd \u6216 projectRoot");
|
|
2248
|
-
const errors = validateFiniteCommand(
|
|
2216
|
+
const errors = validateFiniteCommand(command2);
|
|
2249
2217
|
if (errors.length) return { status: "rejected", key, errors, summary: `${key} \u547D\u4EE4\u8FDD\u53CD\u6D41\u7A0B\u5B58\u6D3B\u95E8\u7981` };
|
|
2250
2218
|
if (!existsSync2(cwd)) throw new Error(`${key} \u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728: ${cwd}`);
|
|
2251
2219
|
const logDir = join2(migrationDirectory(projectRoot), "preflight-logs");
|
|
@@ -2257,7 +2225,7 @@ function runFiniteCommand({ command: command3, key, cwd, projectRoot, timeoutMs
|
|
|
2257
2225
|
let result;
|
|
2258
2226
|
try {
|
|
2259
2227
|
const executable = process.platform === "win32" ? "powershell.exe" : "/bin/sh";
|
|
2260
|
-
const args = process.platform === "win32" ? ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command",
|
|
2228
|
+
const args = process.platform === "win32" ? ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command2] : ["-lc", command2];
|
|
2261
2229
|
result = runner(executable, args, {
|
|
2262
2230
|
cwd,
|
|
2263
2231
|
windowsHide: true,
|
|
@@ -2329,69 +2297,6 @@ function runFiniteExecutable({ executable, args, key, cwd, projectRoot, timeoutM
|
|
|
2329
2297
|
})()
|
|
2330
2298
|
};
|
|
2331
2299
|
}
|
|
2332
|
-
async function runFiniteCommandAsync({ command: command3, key, cwd, projectRoot, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
2333
|
-
if (!key || !cwd || !projectRoot) throw new Error("\u7F3A\u5C11 key\u3001cwd \u6216 projectRoot");
|
|
2334
|
-
const errors = validateFiniteCommand(command3);
|
|
2335
|
-
if (errors.length) return { status: "rejected", key, errors, summary: `${key} \u547D\u4EE4\u8FDD\u53CD\u6D41\u7A0B\u5B58\u6D3B\u95E8\u7981` };
|
|
2336
|
-
if (!existsSync2(cwd)) throw new Error(`${key} \u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728: ${cwd}`);
|
|
2337
|
-
const logDir = join2(migrationDirectory(projectRoot), "preflight-logs");
|
|
2338
|
-
mkdirSync2(logDir, { recursive: true });
|
|
2339
|
-
const stdoutPath = join2(logDir, `${key}.stdout.log`);
|
|
2340
|
-
const stderrPath = join2(logDir, `${key}.stderr.log`);
|
|
2341
|
-
const stdout = createWriteStream(stdoutPath, { flags: "w" });
|
|
2342
|
-
const stderr = createWriteStream(stderrPath, { flags: "w" });
|
|
2343
|
-
const executable = process.platform === "win32" ? "powershell.exe" : "/bin/sh";
|
|
2344
|
-
const args = process.platform === "win32" ? ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command3] : ["-lc", command3];
|
|
2345
|
-
const child = spawn2(executable, args, {
|
|
2346
|
-
cwd,
|
|
2347
|
-
windowsHide: true,
|
|
2348
|
-
shell: false,
|
|
2349
|
-
detached: process.platform !== "win32",
|
|
2350
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2351
|
-
});
|
|
2352
|
-
child.stdout.pipe(stdout);
|
|
2353
|
-
child.stderr.pipe(stderr);
|
|
2354
|
-
let timedOut = false;
|
|
2355
|
-
const timer = setTimeout(() => {
|
|
2356
|
-
timedOut = true;
|
|
2357
|
-
if (process.platform !== "win32" && child.pid) {
|
|
2358
|
-
try {
|
|
2359
|
-
process.kill(-child.pid, "SIGKILL");
|
|
2360
|
-
} catch {
|
|
2361
|
-
child.kill("SIGKILL");
|
|
2362
|
-
}
|
|
2363
|
-
} else child.kill("SIGKILL");
|
|
2364
|
-
}, Number(timeoutMs));
|
|
2365
|
-
const exitCode = await new Promise((resolveExit) => {
|
|
2366
|
-
child.once("error", () => resolveExit(null));
|
|
2367
|
-
child.once("close", (code) => resolveExit(code));
|
|
2368
|
-
});
|
|
2369
|
-
clearTimeout(timer);
|
|
2370
|
-
await Promise.all([
|
|
2371
|
-
new Promise((done) => stdout.end(done)),
|
|
2372
|
-
new Promise((done) => stderr.end(done))
|
|
2373
|
-
]);
|
|
2374
|
-
return {
|
|
2375
|
-
status: !timedOut && exitCode === 0 ? "passed" : "failed",
|
|
2376
|
-
key,
|
|
2377
|
-
exitCode,
|
|
2378
|
-
timedOut,
|
|
2379
|
-
timeoutMs: Number(timeoutMs),
|
|
2380
|
-
cwd,
|
|
2381
|
-
stdout: stdoutPath,
|
|
2382
|
-
stderr: stderrPath,
|
|
2383
|
-
summary: timedOut ? `${key} \u8D85\u8FC7 ${timeoutMs}ms\uFF0C\u5DF2\u7EC8\u6B62\u53D7\u7BA1\u547D\u4EE4` : (() => {
|
|
2384
|
-
if (exitCode === 0) return `${key} \u6267\u884C\u6210\u529F`;
|
|
2385
|
-
return `${key} \u6267\u884C\u5931\u8D25\uFF0C\u9000\u51FA\u7801 ${exitCode ?? "unknown"}`;
|
|
2386
|
-
})()
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
// src/commands/harmony-toolchain.ts
|
|
2391
|
-
import { execFileSync } from "child_process";
|
|
2392
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2393
|
-
import path15 from "path";
|
|
2394
|
-
import JSON52 from "json5";
|
|
2395
2300
|
|
|
2396
2301
|
// src/commands/check-hmos-device.ts
|
|
2397
2302
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -2498,8 +2403,8 @@ function devEcoCliSpawn(executable, args) {
|
|
|
2498
2403
|
return scriptWrapper ? [process.execPath, executable, ...args] : [executable, ...args];
|
|
2499
2404
|
}
|
|
2500
2405
|
function runDevEcoCli(args, { executable = executableName(), runner = spawnCommandSync, timeoutMs = 15e3 } = {}, cwd) {
|
|
2501
|
-
const [
|
|
2502
|
-
const result = runner(
|
|
2406
|
+
const [command2, ...commandArgs] = devEcoCliSpawn(executable, args);
|
|
2407
|
+
const result = runner(command2, commandArgs, {
|
|
2503
2408
|
cwd,
|
|
2504
2409
|
encoding: "utf8",
|
|
2505
2410
|
timeout: timeoutMs,
|
|
@@ -2517,8 +2422,8 @@ function runDevEcoCli(args, { executable = executableName(), runner = spawnComma
|
|
|
2517
2422
|
};
|
|
2518
2423
|
}
|
|
2519
2424
|
async function runDevEcoCliAsync(args, { executable = executableName(), timeoutMs = 15e3 } = {}, cwd) {
|
|
2520
|
-
const [
|
|
2521
|
-
const child = spawnCommand(
|
|
2425
|
+
const [command2, ...commandArgs] = devEcoCliSpawn(executable, args);
|
|
2426
|
+
const child = spawnCommand(command2, commandArgs, {
|
|
2522
2427
|
cwd,
|
|
2523
2428
|
windowsHide: true,
|
|
2524
2429
|
detached: process.platform !== "win32",
|
|
@@ -2773,8 +2678,8 @@ function isRealDeviceTarget(target) {
|
|
|
2773
2678
|
}
|
|
2774
2679
|
return true;
|
|
2775
2680
|
}
|
|
2776
|
-
function readSigningMaterialFingerprints(
|
|
2777
|
-
const profile = path14.join(
|
|
2681
|
+
function readSigningMaterialFingerprints(harmonyRoot2) {
|
|
2682
|
+
const profile = path14.join(harmonyRoot2, "build-profile.json5");
|
|
2778
2683
|
if (!fs13.existsSync(profile)) return [];
|
|
2779
2684
|
let parsed;
|
|
2780
2685
|
try {
|
|
@@ -2783,7 +2688,7 @@ function readSigningMaterialFingerprints(harmonyRoot) {
|
|
|
2783
2688
|
return [];
|
|
2784
2689
|
}
|
|
2785
2690
|
const signingConfigs = parsed?.app?.signingConfigs ?? [];
|
|
2786
|
-
const files = [...new Set(signingConfigs.flatMap((config) => [config?.material?.certpath, config?.material?.profile, config?.material?.storeFile]).filter((file) => typeof file === "string" && file.length > 0).map((file) => path14.isAbsolute(file) ? file : path14.resolve(
|
|
2691
|
+
const files = [...new Set(signingConfigs.flatMap((config) => [config?.material?.certpath, config?.material?.profile, config?.material?.storeFile]).filter((file) => typeof file === "string" && file.length > 0).map((file) => path14.isAbsolute(file) ? file : path14.resolve(harmonyRoot2, file)))];
|
|
2787
2692
|
return [...new Set(files)].flatMap((file) => {
|
|
2788
2693
|
if (!fs13.existsSync(file)) return [];
|
|
2789
2694
|
const stat = fs13.statSync(file);
|
|
@@ -2900,10 +2805,10 @@ function detectDeviceSdkVersion(target) {
|
|
|
2900
2805
|
diverged: systemAbc !== null && systemAbc.major < claimedApiLevel
|
|
2901
2806
|
};
|
|
2902
2807
|
}
|
|
2903
|
-
function ensureDeviceInstallable(
|
|
2808
|
+
function ensureDeviceInstallable(harmonyRoot2, target) {
|
|
2904
2809
|
const device = detectDeviceSdkVersion(target);
|
|
2905
2810
|
if (!device) return { status: "skipped", changed: false, summary: "\u65E0\u6CD5\u8BFB\u53D6\u771F\u673A\u7CFB\u7EDF\u7248\u672C\uFF0C\u8DF3\u8FC7\u517C\u5BB9\u7248\u672C\u68C0\u67E5" };
|
|
2906
|
-
const capped = capCompatibleSdkVersionTo(
|
|
2811
|
+
const capped = capCompatibleSdkVersionTo(harmonyRoot2, device.sdkVersion);
|
|
2907
2812
|
const divergence = device.diverged ? `\uFF1B\u771F\u673A param \u58F0\u79F0 API ${device.claimedApiLevel}\uFF0C\u4F46\u7CFB\u7EDF ABC \u5B57\u8282\u7801\u4E0A\u9650\u4E3A ${device.systemAbcVersion}\uFF08log \u7248\u955C\u50CF\u5E38\u89C1\u865A\u62A5\uFF09\uFF0C\u5DF2\u6309\u8FD0\u884C\u65F6\u5B9E\u9645\u80FD\u529B\u53D6 API ${device.apiLevel}` : "";
|
|
2908
2813
|
return {
|
|
2909
2814
|
...capped,
|
|
@@ -2994,10 +2899,10 @@ function readJson5(file) {
|
|
|
2994
2899
|
function firstExisting(values) {
|
|
2995
2900
|
return values.find((value) => Boolean(value && existsSync4(value)));
|
|
2996
2901
|
}
|
|
2997
|
-
function pathLookup(
|
|
2902
|
+
function pathLookup(command2) {
|
|
2998
2903
|
try {
|
|
2999
2904
|
const lookup = process.platform === "win32" ? "where.exe" : "which";
|
|
3000
|
-
return execFileSync(lookup, [
|
|
2905
|
+
return execFileSync(lookup, [command2], { encoding: "utf8", windowsHide: true }).split(/\r?\n/).map((value) => value.trim()).find(Boolean);
|
|
3001
2906
|
} catch {
|
|
3002
2907
|
return null;
|
|
3003
2908
|
}
|
|
@@ -3158,8 +3063,8 @@ function runHarmonyTest({ projectRoot, harmonyProject, timeoutMs, testClass = nu
|
|
|
3158
3063
|
if (appBuild.status !== "passed") return { status: "failed", key: "harmonyTest", stage: "app-build", appBuild };
|
|
3159
3064
|
const ohpmInstall = ensureOhpmDependencies({ projectRoot, harmonyProject, timeoutMs });
|
|
3160
3065
|
if (ohpmInstall.status !== "passed") return { status: "failed", key: "harmonyTest", stage: "ohpm-install", appBuild, ohpmInstall };
|
|
3161
|
-
const { info, command:
|
|
3162
|
-
const testBuild = runFiniteCommand({ command:
|
|
3066
|
+
const { info, command: command2 } = buildHarmonyTestCommand({ projectRoot, harmonyProject });
|
|
3067
|
+
const testBuild = runFiniteCommand({ command: command2, key: "harmonyTest-build", cwd: info.root, projectRoot, timeoutMs });
|
|
3163
3068
|
if (testBuild.status !== "passed") return { status: "failed", key: "harmonyTest", stage: "test-build", appBuild, testBuild };
|
|
3164
3069
|
const testArtifact = newestHap(info.moduleRoot, (name) => name.toLowerCase().includes(info.testTarget.toLowerCase()));
|
|
3165
3070
|
if (!testArtifact) return { status: "failed", key: "harmonyTest", stage: "test-artifact", summary: "Hvigor \u8FD4\u56DE\u6210\u529F\uFF0C\u4F46\u6CA1\u6709\u53EF\u5B89\u88C5\u7684\u6D4B\u8BD5 HAP", appBuild, testBuild };
|
|
@@ -3345,8 +3250,8 @@ function detectHarmonySdk({ project, sdkPath, writeConfig = false, env = process
|
|
|
3345
3250
|
push(sdkPath, "explicit");
|
|
3346
3251
|
push(config?.harmonySdk?.path, "existing-config");
|
|
3347
3252
|
const configuredHarmony = config?.harmonyProject ?? defaultHarmonyProjectPath(projectRoot);
|
|
3348
|
-
const
|
|
3349
|
-
const localProperties = path17.join(
|
|
3253
|
+
const harmonyRoot2 = path17.isAbsolute(configuredHarmony) ? configuredHarmony : path17.resolve(projectRoot, configuredHarmony);
|
|
3254
|
+
const localProperties = path17.join(harmonyRoot2, "local.properties");
|
|
3350
3255
|
if (fs14.existsSync(localProperties)) {
|
|
3351
3256
|
const properties = parseProperties(fs14.readFileSync(localProperties, "utf8"));
|
|
3352
3257
|
push(properties["sdk.dir"] ?? properties["harmony.sdk.dir"] ?? properties["ohos.sdk.dir"], "local-properties");
|
|
@@ -3485,22 +3390,19 @@ var atomicJson = (file, value) => {
|
|
|
3485
3390
|
`, "utf8");
|
|
3486
3391
|
fs15.renameSync(temporary, file);
|
|
3487
3392
|
};
|
|
3488
|
-
function
|
|
3489
|
-
return
|
|
3490
|
-
}
|
|
3491
|
-
function normalizeAndroidBuildCommand(command3, platform = process.platform) {
|
|
3492
|
-
if (typeof command3 !== "string") return command3;
|
|
3393
|
+
function normalizeAndroidBuildCommand(command2, platform = process.platform) {
|
|
3394
|
+
if (typeof command2 !== "string") return command2;
|
|
3493
3395
|
if (platform === "win32") {
|
|
3494
|
-
return
|
|
3396
|
+
return command2.replace(/^(\s*)(?:&\s*)?["']?\.\/gradlew["']?(?=\s|$)/i, "$1.\\gradlew.bat");
|
|
3495
3397
|
}
|
|
3496
|
-
return
|
|
3398
|
+
return command2.replace(/^(\s*)(?:&\s*)?["']?(?:\.\\gradlew(?:\.bat)?|gradlew\.bat)["']?(?=\s|$)/i, "$1./gradlew");
|
|
3497
3399
|
}
|
|
3498
|
-
function validateAndroidBuildPlatform(
|
|
3499
|
-
if (typeof
|
|
3500
|
-
if (platform === "win32" && /(?:^|\s)\.\/gradlew(?:\s|$)/i.test(
|
|
3400
|
+
function validateAndroidBuildPlatform(command2, platform = process.platform) {
|
|
3401
|
+
if (typeof command2 !== "string") return null;
|
|
3402
|
+
if (platform === "win32" && /(?:^|\s)\.\/gradlew(?:\s|$)/i.test(command2)) {
|
|
3501
3403
|
return "commands.androidBuild uses a Unix Gradle wrapper on Windows; use .\\gradlew.bat";
|
|
3502
3404
|
}
|
|
3503
|
-
if (platform !== "win32" && /(?:gradlew\.bat|\.\\gradlew)(?:\s|$)/i.test(
|
|
3405
|
+
if (platform !== "win32" && /(?:gradlew\.bat|\.\\gradlew)(?:\s|$)/i.test(command2)) {
|
|
3504
3406
|
return `commands.androidBuild uses a Windows Gradle wrapper on ${platform}; use ./gradlew`;
|
|
3505
3407
|
}
|
|
3506
3408
|
return null;
|
|
@@ -3522,12 +3424,12 @@ function validateMigrationConfigValue(config, projectRoot) {
|
|
|
3522
3424
|
if (path19.isAbsolute(config.androidProject)) return config.androidProject;
|
|
3523
3425
|
return path19.resolve(projectRoot, config.androidProject);
|
|
3524
3426
|
})() : null;
|
|
3525
|
-
const
|
|
3427
|
+
const harmonyRoot2 = config?.harmonyProject ? (() => {
|
|
3526
3428
|
if (path19.isAbsolute(config.harmonyProject)) return config.harmonyProject;
|
|
3527
3429
|
return path19.resolve(projectRoot, config.harmonyProject);
|
|
3528
3430
|
})() : null;
|
|
3529
3431
|
if (androidRoot && (!fs15.existsSync(androidRoot) || !fs15.statSync(androidRoot).isDirectory())) errors.push(`androidProject directory not found: ${androidRoot}`);
|
|
3530
|
-
return { valid: errors.length === 0, errors, projectRoot, androidRoot, harmonyRoot };
|
|
3432
|
+
return { valid: errors.length === 0, errors, projectRoot, androidRoot, harmonyRoot: harmonyRoot2 };
|
|
3531
3433
|
}
|
|
3532
3434
|
function initMigrationConfig(options) {
|
|
3533
3435
|
const projectRoot = path19.resolve(options.project);
|
|
@@ -3536,7 +3438,6 @@ function initMigrationConfig(options) {
|
|
|
3536
3438
|
const configPath = path19.join(migrationDirectory(projectRoot), "config.json");
|
|
3537
3439
|
const created = !fs15.existsSync(configPath);
|
|
3538
3440
|
const existing = created ? {} : JSON.parse(fs15.readFileSync(configPath, "utf8"));
|
|
3539
|
-
const configuredAndroidBuild = options.androidBuild ?? existing.commands?.androidBuild ?? defaultAndroidBuild(projectRoot);
|
|
3540
3441
|
const config = {
|
|
3541
3442
|
...existing,
|
|
3542
3443
|
configVersion: existing.configVersion ?? "1.0.0",
|
|
@@ -3545,8 +3446,8 @@ function initMigrationConfig(options) {
|
|
|
3545
3446
|
...existing.harmonySdk ? { harmonySdk: existing.harmonySdk } : {},
|
|
3546
3447
|
commands: {
|
|
3547
3448
|
...existing.commands ?? {},
|
|
3548
|
-
androidBuild: normalizeAndroidBuildCommand(
|
|
3549
|
-
androidTest:
|
|
3449
|
+
androidBuild: normalizeAndroidBuildCommand(existing.commands?.androidBuild ?? null),
|
|
3450
|
+
androidTest: existing.commands?.androidTest ?? null,
|
|
3550
3451
|
harmonyStatic: existing.commands?.harmonyStatic ?? null
|
|
3551
3452
|
}
|
|
3552
3453
|
};
|
|
@@ -3570,7 +3471,7 @@ function validateMigrationConfig(project) {
|
|
|
3570
3471
|
|
|
3571
3472
|
// src/commands/preflight.ts
|
|
3572
3473
|
import fs20 from "fs";
|
|
3573
|
-
import
|
|
3474
|
+
import path23 from "path";
|
|
3574
3475
|
|
|
3575
3476
|
// src/commands/detect-android-ui-stack.ts
|
|
3576
3477
|
import fs16 from "fs";
|
|
@@ -3641,69 +3542,13 @@ function detectAndroidUiStack(projectRoot) {
|
|
|
3641
3542
|
};
|
|
3642
3543
|
}
|
|
3643
3544
|
|
|
3644
|
-
// src/commands/check-android-emulator.ts
|
|
3645
|
-
import { spawnSync as spawnSync5 } from "child_process";
|
|
3646
|
-
var READY_MARKER2 = "A2H_ANDROID_EMULATOR_READY";
|
|
3647
|
-
function command2(executable, args, runner) {
|
|
3648
|
-
const result = runner(executable, args, { encoding: "utf8", shell: false, timeout: 15e3 });
|
|
3649
|
-
return {
|
|
3650
|
-
exitCode: Number.isInteger(result.status) ? result.status : 1,
|
|
3651
|
-
stdout: result.stdout ?? "",
|
|
3652
|
-
stderr: result.stderr ?? result.error?.message ?? ""
|
|
3653
|
-
};
|
|
3654
|
-
}
|
|
3655
|
-
function parseAdbDevices(output) {
|
|
3656
|
-
const devices = [];
|
|
3657
|
-
for (const rawLine of String(output).split(/\r?\n/)) {
|
|
3658
|
-
const line = rawLine.trim();
|
|
3659
|
-
if (!line || line.startsWith("List of devices attached")) continue;
|
|
3660
|
-
const [serial, state] = line.split(/\s+/, 2);
|
|
3661
|
-
if (serial && state) devices.push({ serial, state });
|
|
3662
|
-
}
|
|
3663
|
-
return devices;
|
|
3664
|
-
}
|
|
3665
|
-
function checkAndroidEmulator({ adb = process.env.ADB_BINARY || "adb", runner = spawnSync5 } = {}) {
|
|
3666
|
-
const list = command2(adb, ["devices"], runner);
|
|
3667
|
-
if (list.exitCode !== 0) {
|
|
3668
|
-
return optionalResult(adb, [], [], "ADB \u4E0D\u53EF\u7528\uFF0C\u672A\u68C0\u6D4B Android \u6A21\u62DF\u5668\uFF1B\u5EFA\u8BAE\u5B89\u88C5 Android SDK Platform-Tools \u548C Android \u6A21\u62DF\u5668\uFF0C\u540E\u7EED\u53EF\u8FDB\u884C Android/HarmonyOS \u5BF9\u7167\u9A8C\u8BC1", [list.stderr || list.stdout]);
|
|
3669
|
-
}
|
|
3670
|
-
const devices = parseAdbDevices(list.stdout).filter((device) => device.state === "device");
|
|
3671
|
-
const probes = devices.map((device) => {
|
|
3672
|
-
const qemu = command2(adb, ["-s", device.serial, "shell", "getprop", "ro.kernel.qemu"], runner);
|
|
3673
|
-
return { ...device, qemu, isEmulator: qemu.exitCode === 0 && qemu.stdout.trim() === "1" };
|
|
3674
|
-
});
|
|
3675
|
-
const emulators = probes.filter((probe2) => probe2.isEmulator).map((probe2) => probe2.serial);
|
|
3676
|
-
if (!emulators.length) {
|
|
3677
|
-
return optionalResult(adb, devices.map((device) => device.serial), [], "\u672A\u68C0\u6D4B\u5230 Android \u6A21\u62DF\u5668\uFF08\u53EF\u9009\uFF09\uFF1B\u5EFA\u8BAE\u5B89\u88C5\u5E76\u542F\u52A8\u6A21\u62DF\u5668\uFF0C\u4EE5\u4FBF\u8FC1\u79FB\u65F6\u5BF9\u7167 Android \u4E0E HarmonyOS \u9875\u9762", [list.stdout || "adb devices returned no emulator"]);
|
|
3678
|
-
}
|
|
3679
|
-
const shellProbes = emulators.map((serial) => ({
|
|
3680
|
-
serial,
|
|
3681
|
-
result: command2(adb, ["-s", serial, "shell", "echo", READY_MARKER2], runner)
|
|
3682
|
-
}));
|
|
3683
|
-
const responsiveEmulators = shellProbes.filter(({ result }) => result.exitCode === 0 && result.stdout.includes(READY_MARKER2)).map(({ serial }) => serial);
|
|
3684
|
-
if (!responsiveEmulators.length) {
|
|
3685
|
-
return optionalResult(adb, emulators, [], "\u68C0\u6D4B\u5230 Android \u6A21\u62DF\u5668\u4F46 Shell \u65E0\u54CD\u5E94\uFF08\u53EF\u9009\uFF09\uFF1B\u5EFA\u8BAE\u4FEE\u590D\u6216\u91CD\u542F\u6A21\u62DF\u5668\uFF0C\u4EE5\u4FBF\u8FDB\u884C\u53CC\u7AEF\u9875\u9762\u5BF9\u7167", shellProbes.map(({ serial, result }) => `${serial}: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`));
|
|
3686
|
-
}
|
|
3687
|
-
return {
|
|
3688
|
-
status: "passed",
|
|
3689
|
-
adb,
|
|
3690
|
-
emulators,
|
|
3691
|
-
responsiveEmulators,
|
|
3692
|
-
summary: `\u53D1\u73B0 ${responsiveEmulators.length} \u4E2A\u53EF\u54CD\u5E94\u7684 Android \u6A21\u62DF\u5668\uFF0C\u53EF\u7528\u4E8E Android/HarmonyOS \u5BF9\u7167\u9A8C\u8BC1`,
|
|
3693
|
-
evidence: responsiveEmulators
|
|
3694
|
-
};
|
|
3695
|
-
}
|
|
3696
|
-
function optionalResult(adb, emulators, responsiveEmulators, summary, evidence2) {
|
|
3697
|
-
return { status: "not_applicable", adb, emulators, responsiveEmulators, summary, evidence: evidence2 };
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
3545
|
// src/commands/git-checkpoint.ts
|
|
3701
3546
|
import path21 from "path";
|
|
3702
3547
|
import fs17 from "fs";
|
|
3703
3548
|
import { createHash as createHash5 } from "crypto";
|
|
3704
|
-
import { spawnSync as
|
|
3549
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
3705
3550
|
function git(projectRoot, args) {
|
|
3706
|
-
const result =
|
|
3551
|
+
const result = spawnSync5("git", ["-C", projectRoot, ...args], { encoding: "utf8", shell: false, timeout: 3e4, windowsHide: true });
|
|
3707
3552
|
return {
|
|
3708
3553
|
ok: !result.error && result.status === 0,
|
|
3709
3554
|
exitCode: Number.isInteger(result.status) ? result.status : null,
|
|
@@ -3733,17 +3578,17 @@ function unstage(root, files) {
|
|
|
3733
3578
|
if (head.ok) git(root, ["restore", "--staged", "--", ...files]);
|
|
3734
3579
|
else git(root, ["rm", "--cached", "-r", "--ignore-unmatch", "--", ...files]);
|
|
3735
3580
|
}
|
|
3736
|
-
function initializeHarmonyRepository(androidRoot,
|
|
3737
|
-
const existing = repositoryRoot(
|
|
3581
|
+
function initializeHarmonyRepository(androidRoot, harmonyRoot2) {
|
|
3582
|
+
const existing = repositoryRoot(harmonyRoot2);
|
|
3738
3583
|
if (existing) return { root: existing, initialized: false };
|
|
3739
3584
|
const androidBranch = git(androidRoot, ["branch", "--show-current"]).stdout || "main";
|
|
3740
|
-
const initialized = git(
|
|
3741
|
-
if (!initialized.ok) throw new Error(initialized.stderr || `Cannot initialize HarmonyOS Git repository: ${
|
|
3585
|
+
const initialized = git(harmonyRoot2, ["init", "-b", androidBranch]);
|
|
3586
|
+
if (!initialized.ok) throw new Error(initialized.stderr || `Cannot initialize HarmonyOS Git repository: ${harmonyRoot2}`);
|
|
3742
3587
|
for (const key of ["user.name", "user.email"]) {
|
|
3743
3588
|
const value = git(androidRoot, ["config", "--get", key]);
|
|
3744
|
-
if (value.ok && value.stdout) git(
|
|
3589
|
+
if (value.ok && value.stdout) git(harmonyRoot2, ["config", key, value.stdout]);
|
|
3745
3590
|
}
|
|
3746
|
-
return { root: path21.resolve(
|
|
3591
|
+
return { root: path21.resolve(harmonyRoot2), initialized: true };
|
|
3747
3592
|
}
|
|
3748
3593
|
function relevantStatus(status, ignoreUntrackedRoots = []) {
|
|
3749
3594
|
const ignored = ignoreUntrackedRoots.map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
|
|
@@ -3756,8 +3601,8 @@ function relevantStatus(status, ignoreUntrackedRoots = []) {
|
|
|
3756
3601
|
}
|
|
3757
3602
|
function gitWorkspaceStatus(project, options = {}) {
|
|
3758
3603
|
const projectRoot = path21.resolve(project);
|
|
3759
|
-
const
|
|
3760
|
-
if (!
|
|
3604
|
+
const inside5 = git(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
|
|
3605
|
+
if (!inside5.ok || inside5.stdout !== "true") return { status: "failed", repository: false, projectRoot, errors: [inside5.stderr || "not a Git worktree"] };
|
|
3761
3606
|
const head = git(projectRoot, ["rev-parse", "HEAD"]);
|
|
3762
3607
|
const branch = git(projectRoot, ["branch", "--show-current"]);
|
|
3763
3608
|
const porcelain = git(projectRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
@@ -3777,13 +3622,13 @@ function createGitCheckpoint(options) {
|
|
|
3777
3622
|
const projectRoot = fs17.realpathSync(path21.resolve(options.project));
|
|
3778
3623
|
const requestedHarmonyRoot = options.harmonyProject ? path21.resolve(options.harmonyProject) : null;
|
|
3779
3624
|
if (requestedHarmonyRoot && (!fs17.existsSync(requestedHarmonyRoot) || !fs17.statSync(requestedHarmonyRoot).isDirectory())) throw new Error(`HarmonyOS project not found: ${requestedHarmonyRoot}`);
|
|
3780
|
-
const
|
|
3625
|
+
const harmonyRoot2 = requestedHarmonyRoot ? fs17.realpathSync(requestedHarmonyRoot) : null;
|
|
3781
3626
|
if (!options.message?.trim()) throw new Error("checkpoint create requires --message");
|
|
3782
3627
|
if (!options.all && options.include.length === 0) throw new Error("checkpoint create requires at least one --include or explicit --all");
|
|
3783
3628
|
const baseline = gitWorkspaceStatus(projectRoot);
|
|
3784
3629
|
if (!baseline.repository) throw new Error(baseline.errors?.[0] ?? "not a Git repository");
|
|
3785
3630
|
const androidRepository = repositoryRoot(projectRoot);
|
|
3786
|
-
const harmonyRepository =
|
|
3631
|
+
const harmonyRepository = harmonyRoot2 ? repositoryRoot(harmonyRoot2) ?? initializeHarmonyRepository(androidRepository, harmonyRoot2).root : androidRepository;
|
|
3787
3632
|
const repositories = [.../* @__PURE__ */ new Set([androidRepository, harmonyRepository])];
|
|
3788
3633
|
for (const root of repositories) {
|
|
3789
3634
|
const before = stagedFiles(root);
|
|
@@ -3796,18 +3641,18 @@ function createGitCheckpoint(options) {
|
|
|
3796
3641
|
if (options.all) {
|
|
3797
3642
|
const androidAdd = git(projectRoot, ["add", "-A", "--", ".", ":!.migration"]);
|
|
3798
3643
|
if (!androidAdd.ok) throw new Error(androidAdd.stderr || "git add failed for Android project");
|
|
3799
|
-
if (
|
|
3800
|
-
const harmonyAdd = git(
|
|
3644
|
+
if (harmonyRoot2 && harmonyRepository !== androidRepository) {
|
|
3645
|
+
const harmonyAdd = git(harmonyRoot2, ["add", "-A", "--", ".", ":!.migration"]);
|
|
3801
3646
|
if (!harmonyAdd.ok) throw new Error(harmonyAdd.stderr || "git add failed for HarmonyOS project");
|
|
3802
|
-
} else if (
|
|
3803
|
-
const harmonyPath = normalized(path21.relative(androidRepository,
|
|
3647
|
+
} else if (harmonyRoot2 && !inside2(projectRoot, harmonyRoot2)) {
|
|
3648
|
+
const harmonyPath = normalized(path21.relative(androidRepository, harmonyRoot2));
|
|
3804
3649
|
const harmonyAdd = git(androidRepository, ["add", "-A", "--", harmonyPath]);
|
|
3805
3650
|
if (!harmonyAdd.ok) throw new Error(harmonyAdd.stderr || "git add failed for HarmonyOS project");
|
|
3806
3651
|
}
|
|
3807
3652
|
} else {
|
|
3808
3653
|
for (const value of options.include) {
|
|
3809
3654
|
const absolute = path21.resolve(projectRoot, value);
|
|
3810
|
-
const root =
|
|
3655
|
+
const root = harmonyRoot2 && inside2(harmonyRoot2, absolute) ? harmonyRepository : (() => {
|
|
3811
3656
|
if (inside2(projectRoot, absolute)) return androidRepository;
|
|
3812
3657
|
return null;
|
|
3813
3658
|
})();
|
|
@@ -3829,7 +3674,7 @@ function createGitCheckpoint(options) {
|
|
|
3829
3674
|
try {
|
|
3830
3675
|
committedFiles = [...new Map((options.provenanceFiles ?? []).map((item) => [`${item.role}:${item.file}`, item])).values()].sort((left, right) => left.file.localeCompare(right.file) || left.role.localeCompare(right.role)).map((item) => {
|
|
3831
3676
|
const absolute = path21.resolve(projectRoot, item.file);
|
|
3832
|
-
const expectedRoot = item.role === "harmony-implementation" ?
|
|
3677
|
+
const expectedRoot = item.role === "harmony-implementation" ? harmonyRoot2 : projectRoot;
|
|
3833
3678
|
const root = item.role === "harmony-implementation" ? harmonyRepository : androidRepository;
|
|
3834
3679
|
const relative2 = normalized(path21.relative(projectRoot, absolute));
|
|
3835
3680
|
if (!expectedRoot || !inside2(expectedRoot, absolute) || !fs17.existsSync(absolute) || !fs17.statSync(absolute).isFile()) {
|
|
@@ -3859,12 +3704,12 @@ function createGitCheckpoint(options) {
|
|
|
3859
3704
|
committedFiles,
|
|
3860
3705
|
repositories: {
|
|
3861
3706
|
android: { root: ".", gitRoot: normalized(path21.relative(projectRoot, androidRepository)) || "." },
|
|
3862
|
-
harmony:
|
|
3707
|
+
harmony: harmonyRoot2 ? { root: normalized(path21.relative(projectRoot, harmonyRoot2)), gitRoot: normalized(path21.relative(projectRoot, harmonyRepository)) } : null
|
|
3863
3708
|
}
|
|
3864
3709
|
};
|
|
3865
3710
|
}
|
|
3866
3711
|
const commitResults = [];
|
|
3867
|
-
const separateHarmony =
|
|
3712
|
+
const separateHarmony = harmonyRoot2 && harmonyRepository !== androidRepository;
|
|
3868
3713
|
if (separateHarmony) {
|
|
3869
3714
|
const files2 = stagedFiles(harmonyRepository);
|
|
3870
3715
|
if (files2.length) {
|
|
@@ -3886,9 +3731,9 @@ function createGitCheckpoint(options) {
|
|
|
3886
3731
|
commitResults.push({ kind: "android", root: androidRepository, commit: git(androidRepository, ["rev-parse", "HEAD"]).stdout, files: androidFiles, summary: androidCommit.stdout });
|
|
3887
3732
|
}
|
|
3888
3733
|
const androidCheckpoint = commitResults.find((item) => item.kind === "android");
|
|
3889
|
-
if (
|
|
3890
|
-
commitResults.push({ kind: "harmony", root: harmonyRepository, commit: androidCheckpoint.commit, files: androidFiles.filter((file) => inside2(
|
|
3891
|
-
} else if (
|
|
3734
|
+
if (harmonyRoot2 && harmonyRepository === androidRepository) {
|
|
3735
|
+
commitResults.push({ kind: "harmony", root: harmonyRepository, commit: androidCheckpoint.commit, files: androidFiles.filter((file) => inside2(harmonyRoot2, path21.resolve(harmonyRepository, file))), summary: androidCheckpoint.summary });
|
|
3736
|
+
} else if (harmonyRoot2 && !commitResults.some((item) => item.kind === "harmony")) {
|
|
3892
3737
|
const harmonyHead = git(harmonyRepository, ["rev-parse", "HEAD"]).stdout;
|
|
3893
3738
|
if (harmonyHead) commitResults.push({ kind: "harmony", root: harmonyRepository, commit: harmonyHead, files: [], summary: "HarmonyOS repository already clean" });
|
|
3894
3739
|
}
|
|
@@ -3921,12 +3766,10 @@ function createGitCheckpoint(options) {
|
|
|
3921
3766
|
|
|
3922
3767
|
// src/commands/system-checks.ts
|
|
3923
3768
|
import fs18 from "fs";
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
function probe(command3, args, timeout = 15e3) {
|
|
3927
|
-
const result = spawnCommandSync(command3, args, { encoding: "utf8", timeout, windowsHide: true });
|
|
3769
|
+
function probe(command2, args, timeout = 15e3) {
|
|
3770
|
+
const result = spawnCommandSync(command2, args, { encoding: "utf8", timeout, windowsHide: true });
|
|
3928
3771
|
return {
|
|
3929
|
-
command:
|
|
3772
|
+
command: command2,
|
|
3930
3773
|
available: !result.error && result.status === 0,
|
|
3931
3774
|
exitCode: Number.isInteger(result.status) ? result.status : null,
|
|
3932
3775
|
output: String(result.stdout || result.stderr || result.error?.message || "").trim().split(/\r?\n/)[0] ?? ""
|
|
@@ -3935,8 +3778,7 @@ function probe(command3, args, timeout = 15e3) {
|
|
|
3935
3778
|
function firstDirectory(values) {
|
|
3936
3779
|
return values.find((value) => Boolean(value && fs18.existsSync(value) && fs18.statSync(value).isDirectory())) ?? null;
|
|
3937
3780
|
}
|
|
3938
|
-
function checkToolchain(
|
|
3939
|
-
const projectRoot = path22.resolve(project);
|
|
3781
|
+
function checkToolchain(_project, env = process.env) {
|
|
3940
3782
|
const pythonCandidates = [
|
|
3941
3783
|
env.PYTHON_BINARY,
|
|
3942
3784
|
process.platform === "win32" ? "python" : "python3",
|
|
@@ -3956,23 +3798,14 @@ function checkToolchain(project, env = process.env) {
|
|
|
3956
3798
|
java: probe("java", ["-version"]),
|
|
3957
3799
|
appgraph: probe("appgraph", ["--version"])
|
|
3958
3800
|
};
|
|
3959
|
-
const androidSdk = firstDirectory([
|
|
3960
|
-
env.ANDROID_SDK_ROOT,
|
|
3961
|
-
env.ANDROID_HOME,
|
|
3962
|
-
process.platform === "darwin" ? path22.join(os3.homedir(), "Library", "Android", "sdk") : null,
|
|
3963
|
-
process.platform === "win32" && env.LOCALAPPDATA ? path22.join(env.LOCALAPPDATA, "Android", "Sdk") : null
|
|
3964
|
-
]);
|
|
3965
3801
|
const devecoHome = firstDirectory([
|
|
3966
3802
|
env.DEVECO_STUDIO_HOME,
|
|
3967
3803
|
env.DEVECO_HOME,
|
|
3968
3804
|
process.platform === "darwin" ? "/Applications/DevEco-Studio.app/Contents" : null
|
|
3969
3805
|
]);
|
|
3970
|
-
const androidBuildEntry = ["gradlew", "gradlew.bat", "settings.gradle", "settings.gradle.kts"].map((name) => path22.join(projectRoot, name)).find((file) => fs18.existsSync(file)) ?? null;
|
|
3971
3806
|
const checks = [
|
|
3972
3807
|
...Object.entries(commands).map(([id, value]) => ({ id, status: value.available ? "passed" : "failed", summary: value.available ? value.output || `${id} available` : `${id} unavailable`, evidence: [value.command] })),
|
|
3973
|
-
{ id: "
|
|
3974
|
-
{ id: "deveco", status: devecoHome ? "passed" : "failed", summary: devecoHome ?? "DevEco Studio not found", evidence: devecoHome ? [devecoHome] : [] },
|
|
3975
|
-
{ id: "android-build-entry", status: androidBuildEntry ? "passed" : "failed", summary: androidBuildEntry ?? "Android build entry not found", evidence: androidBuildEntry ? [androidBuildEntry] : [] }
|
|
3808
|
+
{ id: "deveco", status: devecoHome ? "passed" : "failed", summary: devecoHome ?? "DevEco Studio not found", evidence: devecoHome ? [devecoHome] : [] }
|
|
3976
3809
|
];
|
|
3977
3810
|
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
3978
3811
|
const pythonMatch = commands.python.output.match(/(\d+)\.(\d+)/);
|
|
@@ -3981,7 +3814,7 @@ function checkToolchain(project, env = process.env) {
|
|
|
3981
3814
|
if (nodeCheck && (nodeMajor < 20 || nodeMajor > 24)) Object.assign(nodeCheck, { status: "failed", summary: `Node.js ${process.versions.node} is outside supported range 20-24` });
|
|
3982
3815
|
const pythonCheck = checks.find((item) => item.id === "python");
|
|
3983
3816
|
if (pythonCheck && !pythonSupported) Object.assign(pythonCheck, { status: "failed", summary: `${commands.python.output || "Python unavailable"}; Python 3.10+ is required` });
|
|
3984
|
-
return { status: checks.some((item) => item.status === "failed") ? "failed" : "passed", checks, commands,
|
|
3817
|
+
return { status: checks.some((item) => item.status === "failed") ? "failed" : "passed", checks, commands, devecoHome };
|
|
3985
3818
|
}
|
|
3986
3819
|
function checkAppGraphHealth() {
|
|
3987
3820
|
const version = probe("appgraph", ["--version"]);
|
|
@@ -3993,7 +3826,7 @@ function checkAppGraphHealth() {
|
|
|
3993
3826
|
|
|
3994
3827
|
// src/commands/preflight-checklist.ts
|
|
3995
3828
|
import fs19 from "fs";
|
|
3996
|
-
import
|
|
3829
|
+
import path22 from "path";
|
|
3997
3830
|
var PREFLIGHT_CHECKS = [
|
|
3998
3831
|
{ id: "android-ui-stack", name: "Android UI \u6280\u672F\u6808", required: true },
|
|
3999
3832
|
{ id: "git", name: "Git \u4ED3\u5E93\u548C\u5DE5\u4F5C\u533A", required: true },
|
|
@@ -4027,7 +3860,7 @@ function validateChecklist(value) {
|
|
|
4027
3860
|
return value;
|
|
4028
3861
|
}
|
|
4029
3862
|
function loadPreflightChecklist(file) {
|
|
4030
|
-
const checklistPath =
|
|
3863
|
+
const checklistPath = path22.resolve(file);
|
|
4031
3864
|
if (!fs19.existsSync(checklistPath)) throw new Error(`Preflight checklist not found: ${checklistPath}`);
|
|
4032
3865
|
let checklist;
|
|
4033
3866
|
try {
|
|
@@ -4045,7 +3878,7 @@ function loadPreflightChecklist(file) {
|
|
|
4045
3878
|
|
|
4046
3879
|
// src/commands/preflight.ts
|
|
4047
3880
|
var atomicJson2 = (file, value) => {
|
|
4048
|
-
fs20.mkdirSync(
|
|
3881
|
+
fs20.mkdirSync(path23.dirname(file), { recursive: true });
|
|
4049
3882
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
4050
3883
|
fs20.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
4051
3884
|
`, "utf8");
|
|
@@ -4059,7 +3892,7 @@ function check(id, result, required = true) {
|
|
|
4059
3892
|
return entry;
|
|
4060
3893
|
}
|
|
4061
3894
|
async function runPreflight(options) {
|
|
4062
|
-
const projectRoot =
|
|
3895
|
+
const projectRoot = path23.resolve(options.project);
|
|
4063
3896
|
if (!fs20.existsSync(projectRoot) || !fs20.statSync(projectRoot).isDirectory()) throw new Error(`Android project directory not found: ${projectRoot}`);
|
|
4064
3897
|
const timeoutMs = options.timeoutMs ?? 30 * 60 * 1e3;
|
|
4065
3898
|
const checks = [];
|
|
@@ -4071,7 +3904,7 @@ async function runPreflight(options) {
|
|
|
4071
3904
|
const enabled = (id) => checklist.isEnabled(id);
|
|
4072
3905
|
const skip = (id) => checks.push(check(id, { status: "skipped", summary: "Skipped by preflight checklist", evidence: [checklist.checklistPath] }, required(id)));
|
|
4073
3906
|
const environmentRemediation2 = {
|
|
4074
|
-
toolchain: "\u4E3B\u673A\u5DE5\u5177\u94FE\u4E0D\u5B8C\u6574\u3002\u8BF7\u5B89\u88C5/\u914D\u7F6E\u7F3A\u5931\u7EC4\u4EF6\uFF08git\u3001Node.js 20-24\u3001npm\u3001Python 3.10+\u3001JDK\
|
|
3907
|
+
toolchain: "\u4E3B\u673A\u5DE5\u5177\u94FE\u4E0D\u5B8C\u6574\u3002\u8BF7\u5B89\u88C5/\u914D\u7F6E\u7F3A\u5931\u7EC4\u4EF6\uFF08git\u3001Node.js 20-24\u3001npm\u3001Python 3.10+\u3001JDK\u3001DevEco \u5DE5\u5177\u94FE\uFF09\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C droid2hmos preflight\u3002",
|
|
4075
3908
|
appgraph: "appgraph CLI \u4E0D\u53EF\u7528\u6216\u4E0D\u517C\u5BB9\u3002\u8BF7\u5148\u6267\u884C npm install -g @harmonyos-arkts/appgraph@latest --force\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C droid2hmos preflight\u3002",
|
|
4076
3909
|
"harmony-sdk": "\u672A\u53D1\u73B0\u53EF\u7528\u7684 HarmonyOS SDK\u3002\u8BF7\u5728 DevEco Studio \u4E2D\u5B89\u88C5 SDK\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C droid2hmos preflight\u3002",
|
|
4077
3910
|
"harmony-device": "\u672A\u53D1\u73B0\u53EF\u54CD\u5E94\u7684 HarmonyOS \u8BBE\u5907/\u6A21\u62DF\u5668\u3002\u8BF7\u8FDE\u63A5\u5E76\u6388\u6743\u771F\u673A\u6216\u542F\u52A8\u6A21\u62DF\u5668\uFF0C\u786E\u4FDD DevEco CLI \u53EF\u53D1\u73B0\u4E14 shell \u53EF\u54CD\u5E94\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C droid2hmos preflight\u3002"
|
|
@@ -4104,12 +3937,12 @@ async function runPreflight(options) {
|
|
|
4104
3937
|
let harmonyProject = defaultHarmonyProjectPath(projectRoot);
|
|
4105
3938
|
let existingConfig = null;
|
|
4106
3939
|
try {
|
|
4107
|
-
existingConfig = JSON.parse(fs20.readFileSync(
|
|
3940
|
+
existingConfig = JSON.parse(fs20.readFileSync(path23.join(migrationDirectory(projectRoot), "config.json"), "utf8"));
|
|
4108
3941
|
const configuredHarmony = existingConfig.harmonyProject ?? defaultHarmonyProjectPath(projectRoot);
|
|
4109
|
-
harmonyProject =
|
|
3942
|
+
harmonyProject = path23.isAbsolute(configuredHarmony) ? configuredHarmony : path23.resolve(projectRoot, configuredHarmony);
|
|
4110
3943
|
} catch {
|
|
4111
3944
|
}
|
|
4112
|
-
const relativeHarmony =
|
|
3945
|
+
const relativeHarmony = path23.relative(projectRoot, harmonyProject).replaceAll("\\", "/");
|
|
4113
3946
|
const ignoreUntrackedRoots = existingConfig && relativeHarmony && !relativeHarmony.startsWith("../") ? [relativeHarmony] : [];
|
|
4114
3947
|
git2 = gitWorkspaceStatus(projectRoot, { ignoreUntrackedRoots });
|
|
4115
3948
|
const hasRequiredFailures = () => checks.some((item) => item.required && item.status !== "passed" && item.status !== "skipped");
|
|
@@ -4126,7 +3959,7 @@ async function runPreflight(options) {
|
|
|
4126
3959
|
result,
|
|
4127
3960
|
checks: checks.map(({ id, status, summary, evidence: evidence2, retry }) => retry ? { id, status, summary, evidence: evidence2, retry } : { id, status, summary, evidence: evidence2 })
|
|
4128
3961
|
};
|
|
4129
|
-
const reportPath =
|
|
3962
|
+
const reportPath = path23.join(migrationDirectory(projectRoot), "preflight-report.json");
|
|
4130
3963
|
atomicJson2(reportPath, report);
|
|
4131
3964
|
if (showProgress) progressOutput.write(`[preflight] \u5B8C\u6210: ${result}\uFF08\u5171 ${checks.length} \u9879\u68C0\u67E5\uFF0C\u5FC5\u9700\u5931\u8D25 ${requiredFailures.length} \u9879\uFF09\uFF0C\u62A5\u544A: ${reportPath}
|
|
4132
3965
|
`);
|
|
@@ -4174,7 +4007,7 @@ async function runPreflight(options) {
|
|
|
4174
4007
|
let configInit;
|
|
4175
4008
|
if (enabled("config")) displayGate("config");
|
|
4176
4009
|
try {
|
|
4177
|
-
const existingConfigPath =
|
|
4010
|
+
const existingConfigPath = path23.join(migrationDirectory(projectRoot), "config.json");
|
|
4178
4011
|
configInit = !enabled("config") && fs20.existsSync(existingConfigPath) ? { configPath: existingConfigPath, config: JSON.parse(fs20.readFileSync(existingConfigPath, "utf8")) } : initMigrationConfig({ project: projectRoot });
|
|
4179
4012
|
} catch (error) {
|
|
4180
4013
|
checks.push(check("config", { status: "failed", summary: error instanceof Error ? error.message : String(error) }));
|
|
@@ -4182,26 +4015,27 @@ async function runPreflight(options) {
|
|
|
4182
4015
|
}
|
|
4183
4016
|
const configPath = configInit.configPath;
|
|
4184
4017
|
const config = configInit.config;
|
|
4185
|
-
const configuredHarmonyProject = typeof config.harmonyProject === "string" ? config.harmonyProject :
|
|
4186
|
-
harmonyProject =
|
|
4018
|
+
const configuredHarmonyProject = typeof config.harmonyProject === "string" ? config.harmonyProject : path23.relative(projectRoot, defaultHarmonyProjectPath(projectRoot));
|
|
4019
|
+
harmonyProject = path23.isAbsolute(configuredHarmonyProject) ? configuredHarmonyProject : path23.resolve(projectRoot, configuredHarmonyProject);
|
|
4187
4020
|
if (enabled("config")) checks.push(check("config", { ...validateMigrationConfig(projectRoot), summary: "Migration config initialized and validated", evidence: [configPath] }));
|
|
4188
4021
|
else skip("config");
|
|
4189
4022
|
if (hasRequiredFailures()) return finish();
|
|
4190
4023
|
if (!enabled("android-build")) skip("android-build");
|
|
4191
|
-
else
|
|
4192
|
-
const androidBuild = await displayBuildProgress("android-build", () => runConfigCommandAsync({ configPath, key: "androidBuild", timeoutMs }));
|
|
4193
|
-
checks.push(check("android-build", androidBuild, required("android-build")));
|
|
4194
|
-
} else {
|
|
4024
|
+
else {
|
|
4195
4025
|
displayGate("android-build");
|
|
4196
4026
|
checks.push(check("android-build", {
|
|
4197
4027
|
status: "not_applicable",
|
|
4198
|
-
summary: "Android \u6784\u5EFA\
|
|
4028
|
+
summary: "Android \u6784\u5EFA\u80FD\u529B\u5DF2\u79FB\u9664\uFF08\u8FC1\u79FB\u6D41\u7A0B\u4E0D\u4F9D\u8D56 Android \u7F16\u8BD1\u73AF\u5883\uFF09\uFF1B\u53CC\u7AEF\u5BF9\u7167\u4EE5\u6E90\u7801\u4E0E\u9759\u6001 facts \u4E3A\u57FA\u51C6"
|
|
4199
4029
|
}, required("android-build")));
|
|
4200
4030
|
}
|
|
4201
|
-
if (enabled("android-emulator"))
|
|
4031
|
+
if (!enabled("android-emulator")) skip("android-emulator");
|
|
4032
|
+
else {
|
|
4202
4033
|
displayGate("android-emulator");
|
|
4203
|
-
checks.push(check("android-emulator",
|
|
4204
|
-
|
|
4034
|
+
checks.push(check("android-emulator", {
|
|
4035
|
+
status: "not_applicable",
|
|
4036
|
+
summary: "Android \u6A21\u62DF\u5668\u68C0\u6D4B\u5DF2\u79FB\u9664\uFF08\u8FC1\u79FB\u6D41\u7A0B\u4E0D\u4F9D\u8D56 Android \u8FD0\u884C\u73AF\u5883\uFF09"
|
|
4037
|
+
}, required("android-emulator")));
|
|
4038
|
+
}
|
|
4205
4039
|
if (hasRequiredFailures()) return finish();
|
|
4206
4040
|
if (enabled("harmony-project")) {
|
|
4207
4041
|
displayGate("harmony-project");
|
|
@@ -4240,9 +4074,9 @@ async function runPreflight(options) {
|
|
|
4240
4074
|
}
|
|
4241
4075
|
|
|
4242
4076
|
// src/commands/deveco-auth-login.ts
|
|
4243
|
-
import { spawn as spawn3, spawnSync as
|
|
4077
|
+
import { spawn as spawn3, spawnSync as spawnSync6 } from "child_process";
|
|
4244
4078
|
import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4245
|
-
import
|
|
4079
|
+
import path24 from "path";
|
|
4246
4080
|
import { setTimeout as delay2 } from "timers/promises";
|
|
4247
4081
|
var MANAGED_LOGIN_SERVICE_NAME = "devecocli-auth";
|
|
4248
4082
|
var LOGIN_URL_PATTERN = /https?:\/\/[^\s"'<>]+/i;
|
|
@@ -4251,13 +4085,13 @@ function isDevEcoCliLoginCommand(commandLine) {
|
|
|
4251
4085
|
return DEVECO_LOGIN_COMMAND_PATTERN.test(commandLine);
|
|
4252
4086
|
}
|
|
4253
4087
|
function managedLoginStatePaths(project) {
|
|
4254
|
-
const directory =
|
|
4255
|
-
const pidFile =
|
|
4088
|
+
const directory = path24.join(migrationDirectory(project), "services");
|
|
4089
|
+
const pidFile = path24.join(directory, `${MANAGED_LOGIN_SERVICE_NAME}.pid`);
|
|
4256
4090
|
return {
|
|
4257
4091
|
directory,
|
|
4258
4092
|
pidFile,
|
|
4259
|
-
stdoutLog:
|
|
4260
|
-
stderrLog:
|
|
4093
|
+
stdoutLog: path24.join(directory, `${MANAGED_LOGIN_SERVICE_NAME}.stdout.log`),
|
|
4094
|
+
stderrLog: path24.join(directory, `${MANAGED_LOGIN_SERVICE_NAME}.stderr.log`),
|
|
4261
4095
|
exitFile: `${pidFile}.exit`,
|
|
4262
4096
|
errorFile: `${pidFile}.error`
|
|
4263
4097
|
};
|
|
@@ -4288,7 +4122,7 @@ function tailFile(file, maxChars) {
|
|
|
4288
4122
|
}
|
|
4289
4123
|
}
|
|
4290
4124
|
function listPosixProcesses() {
|
|
4291
|
-
const result =
|
|
4125
|
+
const result = spawnSync6("ps", ["-eo", "pid=,command="], { encoding: "utf8", timeout: 15e3 });
|
|
4292
4126
|
if (result.error || result.status !== 0) return [];
|
|
4293
4127
|
return result.stdout.split("\n").flatMap((line) => {
|
|
4294
4128
|
const trimmed = line.trim();
|
|
@@ -4296,15 +4130,15 @@ function listPosixProcesses() {
|
|
|
4296
4130
|
const separator = trimmed.indexOf(" ");
|
|
4297
4131
|
if (separator <= 0) return [];
|
|
4298
4132
|
const pid = Number.parseInt(trimmed.slice(0, separator), 10);
|
|
4299
|
-
const
|
|
4300
|
-
return Number.isInteger(pid) && pid > 0 ? [{ pid, command:
|
|
4133
|
+
const command2 = trimmed.slice(separator + 1);
|
|
4134
|
+
return Number.isInteger(pid) && pid > 0 ? [{ pid, command: command2 }] : [];
|
|
4301
4135
|
});
|
|
4302
4136
|
}
|
|
4303
4137
|
function listWindowsProcesses() {
|
|
4304
4138
|
const source = 'Get-CimInstance Win32_Process -Filter "CommandLine IS NOT NULL" | ForEach-Object { "$($_.ProcessId)`t$($_.CommandLine)" }';
|
|
4305
4139
|
const encoded = Buffer.from(source, "utf16le").toString("base64");
|
|
4306
|
-
const powershell = process.env.SystemRoot ?
|
|
4307
|
-
const result =
|
|
4140
|
+
const powershell = process.env.SystemRoot ? path24.resolve(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
|
|
4141
|
+
const result = spawnSync6(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
|
|
4308
4142
|
encoding: "utf8",
|
|
4309
4143
|
timeout: 3e4,
|
|
4310
4144
|
windowsHide: true
|
|
@@ -4314,8 +4148,8 @@ function listWindowsProcesses() {
|
|
|
4314
4148
|
const separator = line.indexOf(" ");
|
|
4315
4149
|
if (separator <= 0) return [];
|
|
4316
4150
|
const pid = Number.parseInt(line.slice(0, separator), 10);
|
|
4317
|
-
const
|
|
4318
|
-
return Number.isInteger(pid) && pid > 0 ? [{ pid, command:
|
|
4151
|
+
const command2 = line.slice(separator + 1);
|
|
4152
|
+
return Number.isInteger(pid) && pid > 0 ? [{ pid, command: command2 }] : [];
|
|
4319
4153
|
});
|
|
4320
4154
|
}
|
|
4321
4155
|
function listDevEcoCliLoginProcesses(options = {}) {
|
|
@@ -4327,7 +4161,7 @@ function listDevEcoCliLoginProcesses(options = {}) {
|
|
|
4327
4161
|
}
|
|
4328
4162
|
async function terminateProcessTree(pid) {
|
|
4329
4163
|
if (process.platform === "win32") {
|
|
4330
|
-
|
|
4164
|
+
spawnSync6("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", timeout: 15e3, windowsHide: true });
|
|
4331
4165
|
return;
|
|
4332
4166
|
}
|
|
4333
4167
|
try {
|
|
@@ -4345,7 +4179,7 @@ async function terminateProcessTree(pid) {
|
|
|
4345
4179
|
}
|
|
4346
4180
|
}
|
|
4347
4181
|
async function killOrphanDevEcoCliLogins(options) {
|
|
4348
|
-
const state = managedLoginStatePaths(
|
|
4182
|
+
const state = managedLoginStatePaths(path24.resolve(options.project));
|
|
4349
4183
|
const terminate = options.terminator ?? terminateProcessTree;
|
|
4350
4184
|
const targets = /* @__PURE__ */ new Set();
|
|
4351
4185
|
const managedPid = readPidFile(state.pidFile);
|
|
@@ -4373,8 +4207,8 @@ async function killOrphanDevEcoCliLogins(options) {
|
|
|
4373
4207
|
}
|
|
4374
4208
|
function runDetachedDevEcoAuthHost(requestPath) {
|
|
4375
4209
|
const request = JSON.parse(readFileSync5(requestPath, "utf8"));
|
|
4376
|
-
mkdirSync3(
|
|
4377
|
-
mkdirSync3(
|
|
4210
|
+
mkdirSync3(path24.dirname(request.stdout), { recursive: true });
|
|
4211
|
+
mkdirSync3(path24.dirname(request.stderr), { recursive: true });
|
|
4378
4212
|
const stdout = openSync3(request.stdout, "a");
|
|
4379
4213
|
const stderr = openSync3(request.stderr, "a");
|
|
4380
4214
|
try {
|
|
@@ -4437,7 +4271,7 @@ async function managedDevEcoCliLogin(options) {
|
|
|
4437
4271
|
const statusTimeoutMs = options.statusTimeoutMs ?? 15e3;
|
|
4438
4272
|
const progress = options.onProgress ?? (() => {
|
|
4439
4273
|
});
|
|
4440
|
-
const projectRoot =
|
|
4274
|
+
const projectRoot = path24.resolve(options.project);
|
|
4441
4275
|
const state = managedLoginStatePaths(projectRoot);
|
|
4442
4276
|
const failure = (summary, extra = {}) => ({
|
|
4443
4277
|
status: "failed",
|
|
@@ -4531,17 +4365,17 @@ async function managedDevEcoCliLogin(options) {
|
|
|
4531
4365
|
|
|
4532
4366
|
// src/commands/device-evidence.ts
|
|
4533
4367
|
import fs21 from "fs";
|
|
4534
|
-
import
|
|
4368
|
+
import path25 from "path";
|
|
4535
4369
|
import { createHash as createHash6 } from "crypto";
|
|
4536
|
-
import { spawnSync as
|
|
4370
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
4537
4371
|
import { inflateSync } from "zlib";
|
|
4538
4372
|
function safeName(value) {
|
|
4539
4373
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) throw new Error("evidence name must use letters, digits, dot, underscore, or dash");
|
|
4540
4374
|
return value;
|
|
4541
4375
|
}
|
|
4542
|
-
function run(
|
|
4543
|
-
const script = /\.m?js$/i.test(
|
|
4544
|
-
const result =
|
|
4376
|
+
function run(command2, args, encoding = "utf8") {
|
|
4377
|
+
const script = /\.m?js$/i.test(command2);
|
|
4378
|
+
const result = spawnSync7(script ? process.execPath : command2, script ? [command2, ...args] : args, {
|
|
4545
4379
|
encoding: encoding === "buffer" ? null : encoding,
|
|
4546
4380
|
shell: false,
|
|
4547
4381
|
timeout: 3e4,
|
|
@@ -4553,7 +4387,7 @@ function run(command3, args, encoding = "utf8") {
|
|
|
4553
4387
|
status: Number.isInteger(result.status) ? result.status : null,
|
|
4554
4388
|
stdout: result.stdout ?? (encoding === "buffer" ? Buffer.alloc(0) : ""),
|
|
4555
4389
|
stderr: String(result.stderr ?? result.error?.message ?? ""),
|
|
4556
|
-
invocation: [
|
|
4390
|
+
invocation: [command2, ...args]
|
|
4557
4391
|
};
|
|
4558
4392
|
}
|
|
4559
4393
|
function writeResult(file, result) {
|
|
@@ -4649,72 +4483,39 @@ function pixelMetrics(leftFile, rightFile) {
|
|
|
4649
4483
|
return null;
|
|
4650
4484
|
}
|
|
4651
4485
|
}
|
|
4652
|
-
function selectTarget(command3, listArgs, explicit) {
|
|
4653
|
-
if (explicit) return explicit;
|
|
4654
|
-
const listed = run(command3, listArgs);
|
|
4655
|
-
if (!listed.ok) throw new Error(`${command3} target discovery failed: ${listed.stderr}`);
|
|
4656
|
-
const lines = String(listed.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
4657
|
-
if (command3 === "adb") {
|
|
4658
|
-
const found = lines.filter((line) => !line.startsWith("List of devices")).map((line) => line.split(/\s+/)).find((parts) => parts[1] === "device");
|
|
4659
|
-
if (found) return found[0];
|
|
4660
|
-
} else {
|
|
4661
|
-
const found = lines.find((line) => !/^\[?empty\]?$/i.test(line) && !/^list of targets/i.test(line));
|
|
4662
|
-
if (found) return found;
|
|
4663
|
-
}
|
|
4664
|
-
throw new Error(`No responsive ${command3} target found`);
|
|
4665
|
-
}
|
|
4666
4486
|
function captureDeviceEvidence(options) {
|
|
4667
|
-
const projectRoot =
|
|
4487
|
+
const projectRoot = path25.resolve(options.project);
|
|
4668
4488
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
4669
4489
|
const workspaceRoot = migrationWorkspaceRoot(projectRoot);
|
|
4670
4490
|
const name = safeName(options.name);
|
|
4671
4491
|
const capturedAt = /* @__PURE__ */ new Date();
|
|
4672
|
-
const directory =
|
|
4492
|
+
const directory = path25.join(migrationRoot, "evidence", name, options.platform);
|
|
4673
4493
|
fs21.mkdirSync(directory, { recursive: true });
|
|
4674
|
-
const screenshot =
|
|
4675
|
-
const layout =
|
|
4676
|
-
const logs =
|
|
4494
|
+
const screenshot = path25.join(directory, "screenshot.png");
|
|
4495
|
+
const layout = path25.join(directory, "layout.json");
|
|
4496
|
+
const logs = path25.join(directory, "device.log");
|
|
4677
4497
|
const commands = [];
|
|
4678
4498
|
let target;
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
const candidates = String(listed.stdout).split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^\[?empty\]?$/i.test(line) && !/^list of targets/i.test(line));
|
|
4700
|
-
if (!listed.ok || !candidates.length) throw new Error(`${hdc} target discovery failed: ${listed.stderr || "no targets"}`);
|
|
4701
|
-
target = options.target ?? process.env.DROID2HMOS_HDC_TARGET ?? selectPreferredTarget(candidates);
|
|
4702
|
-
const prefix = ["-t", target];
|
|
4703
|
-
const remoteShot = `/data/local/tmp/droid2hmos-${process.pid}.png`;
|
|
4704
|
-
const remoteLayout = `/data/local/tmp/droid2hmos-${process.pid}.json`;
|
|
4705
|
-
const shot = run(hdc, [...prefix, "shell", "snapshot_display", "-f", remoteShot]);
|
|
4706
|
-
commands.push(shot);
|
|
4707
|
-
const recvShot = run(hdc, [...prefix, "file", "recv", remoteShot, screenshot]);
|
|
4708
|
-
commands.push(recvShot);
|
|
4709
|
-
const dump = run(hdc, [...prefix, "shell", "uitest", "dumpLayout", "-p", remoteLayout]);
|
|
4710
|
-
commands.push(dump);
|
|
4711
|
-
const recvLayout = run(hdc, [...prefix, "file", "recv", remoteLayout, layout]);
|
|
4712
|
-
commands.push(recvLayout);
|
|
4713
|
-
run(hdc, [...prefix, "shell", "rm", "-f", remoteShot, remoteLayout]);
|
|
4714
|
-
const log = run(hdc, [...prefix, "shell", "hilog", "-x"]);
|
|
4715
|
-
commands.push(log);
|
|
4716
|
-
writeResult(logs, log);
|
|
4717
|
-
}
|
|
4499
|
+
const hdc = options.hdc ?? process.env.HDC_BINARY ?? "hdc";
|
|
4500
|
+
const listed = run(hdc, ["list", "targets"]);
|
|
4501
|
+
const candidates = String(listed.stdout).split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^\[?empty\]?$/i.test(line) && !/^list of targets/i.test(line));
|
|
4502
|
+
if (!listed.ok || !candidates.length) throw new Error(`${hdc} target discovery failed: ${listed.stderr || "no targets"}`);
|
|
4503
|
+
target = options.target ?? process.env.DROID2HMOS_HDC_TARGET ?? selectPreferredTarget(candidates);
|
|
4504
|
+
const prefix = ["-t", target];
|
|
4505
|
+
const remoteShot = `/data/local/tmp/droid2hmos-${process.pid}.png`;
|
|
4506
|
+
const remoteLayout = `/data/local/tmp/droid2hmos-${process.pid}.json`;
|
|
4507
|
+
const shot = run(hdc, [...prefix, "shell", "snapshot_display", "-f", remoteShot]);
|
|
4508
|
+
commands.push(shot);
|
|
4509
|
+
const recvShot = run(hdc, [...prefix, "file", "recv", remoteShot, screenshot]);
|
|
4510
|
+
commands.push(recvShot);
|
|
4511
|
+
const dump = run(hdc, [...prefix, "shell", "uitest", "dumpLayout", "-p", remoteLayout]);
|
|
4512
|
+
commands.push(dump);
|
|
4513
|
+
const recvLayout = run(hdc, [...prefix, "file", "recv", remoteLayout, layout]);
|
|
4514
|
+
commands.push(recvLayout);
|
|
4515
|
+
run(hdc, [...prefix, "shell", "rm", "-f", remoteShot, remoteLayout]);
|
|
4516
|
+
const log = run(hdc, [...prefix, "shell", "hilog", "-x"]);
|
|
4517
|
+
commands.push(log);
|
|
4518
|
+
writeResult(logs, log);
|
|
4718
4519
|
const artifacts = [screenshot, layout, logs].map(fileInfo).filter(Boolean);
|
|
4719
4520
|
const failures = commands.filter((item) => !item.ok).map((item) => ({ invocation: item.invocation, status: item.status, stderr: item.stderr }));
|
|
4720
4521
|
const metadata = {
|
|
@@ -4724,10 +4525,10 @@ function captureDeviceEvidence(options) {
|
|
|
4724
4525
|
target,
|
|
4725
4526
|
capturedAt: capturedAt.toISOString(),
|
|
4726
4527
|
status: failures.length === 0 && artifacts.length === 3 ? "passed" : "failed",
|
|
4727
|
-
artifacts: artifacts.map((item) => ({ ...item, path:
|
|
4528
|
+
artifacts: artifacts.map((item) => ({ ...item, path: path25.relative(workspaceRoot, item.path).replaceAll("\\", "/") })),
|
|
4728
4529
|
failures
|
|
4729
4530
|
};
|
|
4730
|
-
const metadataPath =
|
|
4531
|
+
const metadataPath = path25.join(directory, "evidence.json");
|
|
4731
4532
|
fs21.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
4732
4533
|
`, "utf8");
|
|
4733
4534
|
return { ...metadata, metadataPath };
|
|
@@ -4737,24 +4538,24 @@ function tokens(file) {
|
|
|
4737
4538
|
return new Set(fs21.readFileSync(file, "utf8").toLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
|
|
4738
4539
|
}
|
|
4739
4540
|
function compareEvidence(options) {
|
|
4740
|
-
const projectRoot =
|
|
4541
|
+
const projectRoot = path25.resolve(options.project);
|
|
4741
4542
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
4742
4543
|
const resolveEvidence = (value) => {
|
|
4743
4544
|
const normalized2 = value.replaceAll("\\", "/");
|
|
4744
|
-
const root =
|
|
4745
|
-
if (normalized2.startsWith(".migration/")) return
|
|
4746
|
-
return
|
|
4545
|
+
const root = path25.isAbsolute(value) ? value : (() => {
|
|
4546
|
+
if (normalized2.startsWith(".migration/")) return path25.resolve(migrationWorkspaceRoot(projectRoot), value);
|
|
4547
|
+
return path25.resolve(projectRoot, value);
|
|
4747
4548
|
})();
|
|
4748
|
-
return fs21.statSync(root).isDirectory() ? root :
|
|
4549
|
+
return fs21.statSync(root).isDirectory() ? root : path25.dirname(root);
|
|
4749
4550
|
};
|
|
4750
4551
|
const androidRoot = resolveEvidence(options.android);
|
|
4751
|
-
const
|
|
4752
|
-
const androidShot = fileInfo(
|
|
4753
|
-
const harmonyShot = fileInfo(
|
|
4754
|
-
const pixels = pixelMetrics(
|
|
4755
|
-
const layoutFile = (root) => [
|
|
4552
|
+
const harmonyRoot2 = resolveEvidence(options.harmony);
|
|
4553
|
+
const androidShot = fileInfo(path25.join(androidRoot, "screenshot.png"));
|
|
4554
|
+
const harmonyShot = fileInfo(path25.join(harmonyRoot2, "screenshot.png"));
|
|
4555
|
+
const pixels = pixelMetrics(path25.join(androidRoot, "screenshot.png"), path25.join(harmonyRoot2, "screenshot.png"));
|
|
4556
|
+
const layoutFile = (root) => [path25.join(root, "layout.json"), path25.join(root, "layout.xml")].find((file) => fs21.existsSync(file)) ?? path25.join(root, "layout.json");
|
|
4756
4557
|
const left = tokens(layoutFile(androidRoot));
|
|
4757
|
-
const right = tokens(layoutFile(
|
|
4558
|
+
const right = tokens(layoutFile(harmonyRoot2));
|
|
4758
4559
|
const intersection = [...left].filter((value) => right.has(value));
|
|
4759
4560
|
const union = /* @__PURE__ */ new Set([...left, ...right]);
|
|
4760
4561
|
const result = {
|
|
@@ -4762,7 +4563,7 @@ function compareEvidence(options) {
|
|
|
4762
4563
|
name: safeName(options.name),
|
|
4763
4564
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4764
4565
|
android: { root: androidRoot, screenshot: androidShot, layoutTokens: left.size },
|
|
4765
|
-
harmony: { root:
|
|
4566
|
+
harmony: { root: harmonyRoot2, screenshot: harmonyShot, layoutTokens: right.size },
|
|
4766
4567
|
screenshot: {
|
|
4767
4568
|
sameBytes: Boolean(androidShot && harmonyShot && androidShot.sha256 === harmonyShot.sha256),
|
|
4768
4569
|
sameDimensions: Boolean(androidShot && harmonyShot && androidShot.width === harmonyShot.width && androidShot.height === harmonyShot.height),
|
|
@@ -4771,27 +4572,27 @@ function compareEvidence(options) {
|
|
|
4771
4572
|
layout: { sharedTokens: intersection.length, unionTokens: union.size, jaccard: union.size ? intersection.length / union.size : 1 },
|
|
4772
4573
|
limitation: "This deterministic comparison checks PNG pixels when decodable plus screenshot identity/dimensions and layout-token overlap; semantic visual parity still requires image inspection."
|
|
4773
4574
|
};
|
|
4774
|
-
const output =
|
|
4775
|
-
fs21.mkdirSync(
|
|
4575
|
+
const output = path25.join(migrationRoot, "evidence", safeName(options.name), "comparison.json");
|
|
4576
|
+
fs21.mkdirSync(path25.dirname(output), { recursive: true });
|
|
4776
4577
|
fs21.writeFileSync(output, `${JSON.stringify(result, null, 2)}
|
|
4777
4578
|
`, "utf8");
|
|
4778
4579
|
return { status: "passed", output, result };
|
|
4779
4580
|
}
|
|
4780
4581
|
function writeFlowBundle(options) {
|
|
4781
|
-
const projectRoot =
|
|
4582
|
+
const projectRoot = path25.resolve(options.project);
|
|
4782
4583
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
4783
|
-
const output =
|
|
4584
|
+
const output = path25.join(migrationRoot, "evidence", safeName(options.name), "flow.json");
|
|
4784
4585
|
const value = {
|
|
4785
4586
|
flowVersion: "1.0.0",
|
|
4786
4587
|
name: options.name,
|
|
4787
4588
|
platform: options.platform,
|
|
4788
4589
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4789
4590
|
status: options.before.status === "passed" && options.execution.status === "passed" && options.after.status === "passed" ? "passed" : "failed",
|
|
4790
|
-
before:
|
|
4591
|
+
before: path25.relative(migrationWorkspaceRoot(projectRoot), options.before.metadataPath).replaceAll("\\", "/"),
|
|
4791
4592
|
execution: options.execution,
|
|
4792
|
-
after:
|
|
4593
|
+
after: path25.relative(migrationWorkspaceRoot(projectRoot), options.after.metadataPath).replaceAll("\\", "/")
|
|
4793
4594
|
};
|
|
4794
|
-
fs21.mkdirSync(
|
|
4595
|
+
fs21.mkdirSync(path25.dirname(output), { recursive: true });
|
|
4795
4596
|
fs21.writeFileSync(output, `${JSON.stringify(value, null, 2)}
|
|
4796
4597
|
`, "utf8");
|
|
4797
4598
|
return { ...value, output };
|
|
@@ -4799,7 +4600,7 @@ function writeFlowBundle(options) {
|
|
|
4799
4600
|
|
|
4800
4601
|
// src/commands/report-update.ts
|
|
4801
4602
|
import fs22 from "fs";
|
|
4802
|
-
import
|
|
4603
|
+
import path26 from "path";
|
|
4803
4604
|
var allowedStatuses = /* @__PURE__ */ new Set(["pending", "implemented", "verified", "blocked"]);
|
|
4804
4605
|
function readJson3(file) {
|
|
4805
4606
|
return JSON.parse(fs22.readFileSync(file, "utf8"));
|
|
@@ -4811,48 +4612,48 @@ function atomicJson3(file, value) {
|
|
|
4811
4612
|
fs22.renameSync(temporary, file);
|
|
4812
4613
|
}
|
|
4813
4614
|
function readHarmonyRoot(projectRoot) {
|
|
4814
|
-
const configPath =
|
|
4615
|
+
const configPath = path26.join(migrationDirectory(projectRoot), "config.json");
|
|
4815
4616
|
if (!fs22.existsSync(configPath)) return null;
|
|
4816
4617
|
const config = readJson3(configPath);
|
|
4817
4618
|
const harmonyProject = config?.harmonyProject;
|
|
4818
4619
|
if (typeof harmonyProject !== "string" || !harmonyProject) return null;
|
|
4819
|
-
const
|
|
4820
|
-
return fs22.existsSync(
|
|
4620
|
+
const harmonyRoot2 = path26.isAbsolute(harmonyProject) ? harmonyProject : path26.resolve(projectRoot, harmonyProject);
|
|
4621
|
+
return fs22.existsSync(harmonyRoot2) ? harmonyRoot2 : null;
|
|
4821
4622
|
}
|
|
4822
|
-
function insideProject(projectRoot,
|
|
4823
|
-
const absolute = value.replaceAll("\\", "/").startsWith(".migration/") ?
|
|
4824
|
-
const relative2 =
|
|
4825
|
-
const inProject = !!relative2 && !relative2.startsWith("../") && !
|
|
4623
|
+
function insideProject(projectRoot, harmonyRoot2, workspaceRoot, value, mustExist) {
|
|
4624
|
+
const absolute = value.replaceAll("\\", "/").startsWith(".migration/") ? path26.resolve(workspaceRoot, value) : path26.resolve(projectRoot, value);
|
|
4625
|
+
const relative2 = path26.relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
4626
|
+
const inProject = !!relative2 && !relative2.startsWith("../") && !path26.isAbsolute(relative2);
|
|
4826
4627
|
let inHarmony = false;
|
|
4827
|
-
if (!inProject &&
|
|
4828
|
-
const relativeHarmony =
|
|
4829
|
-
inHarmony = !!relativeHarmony && !relativeHarmony.startsWith("../") && !
|
|
4628
|
+
if (!inProject && harmonyRoot2) {
|
|
4629
|
+
const relativeHarmony = path26.relative(harmonyRoot2, absolute).replaceAll("\\", "/");
|
|
4630
|
+
inHarmony = !!relativeHarmony && !relativeHarmony.startsWith("../") && !path26.isAbsolute(relativeHarmony) && !relative2.startsWith("/");
|
|
4830
4631
|
}
|
|
4831
|
-
const migrationRelative =
|
|
4832
|
-
const inMigration = !!migrationRelative && !migrationRelative.startsWith("../") && !
|
|
4632
|
+
const migrationRelative = path26.relative(workspaceRoot, absolute).replaceAll("\\", "/");
|
|
4633
|
+
const inMigration = !!migrationRelative && !migrationRelative.startsWith("../") && !path26.isAbsolute(migrationRelative);
|
|
4833
4634
|
if (!inProject && !inHarmony && !inMigration) throw new Error(`Path must be inside the project: ${value}`);
|
|
4834
4635
|
if (mustExist && !fs22.existsSync(absolute)) throw new Error(`Referenced file not found: ${migrationRelative !== "" && inMigration ? migrationRelative : relative2}`);
|
|
4835
4636
|
return inMigration ? migrationRelative : relative2;
|
|
4836
4637
|
}
|
|
4837
4638
|
function updateReportItem(options) {
|
|
4838
4639
|
if (!options.status && options.harmonyosFiles === void 0 && options.androidSourceFiles === void 0 && options.sourceProvenance === void 0 && options.testEvidence === void 0 && options.difference === void 0) throw new Error("report update requires at least one mutable field");
|
|
4839
|
-
const projectRoot =
|
|
4840
|
-
const
|
|
4841
|
-
const reportRoot =
|
|
4842
|
-
const indexPath =
|
|
4640
|
+
const projectRoot = path26.resolve(options.project);
|
|
4641
|
+
const harmonyRoot2 = readHarmonyRoot(projectRoot);
|
|
4642
|
+
const reportRoot = path26.join(migrationDirectory(projectRoot), "report");
|
|
4643
|
+
const indexPath = path26.join(reportRoot, "index.json");
|
|
4843
4644
|
if (!fs22.existsSync(indexPath)) throw new Error("Migration report is not initialized");
|
|
4844
4645
|
if (options.status && !allowedStatuses.has(options.status)) throw new Error(`Invalid report status: ${options.status}`);
|
|
4845
4646
|
const index = readJson3(indexPath);
|
|
4846
4647
|
const reportFile = (value) => {
|
|
4847
|
-
if (typeof value !== "string" || !value.trim() ||
|
|
4848
|
-
const absolute =
|
|
4849
|
-
const relation =
|
|
4850
|
-
if (!relation || relation === ".." || relation.startsWith(`..${
|
|
4648
|
+
if (typeof value !== "string" || !value.trim() || path26.isAbsolute(value)) throw new Error(`Report shard path must be report-relative: ${String(value)}`);
|
|
4649
|
+
const absolute = path26.resolve(reportRoot, value);
|
|
4650
|
+
const relation = path26.relative(reportRoot, absolute);
|
|
4651
|
+
if (!relation || relation === ".." || relation.startsWith(`..${path26.sep}`) || path26.isAbsolute(relation)) throw new Error(`Report shard escapes report root: ${value}`);
|
|
4851
4652
|
if (!fs22.existsSync(absolute) || !fs22.statSync(absolute).isFile()) throw new Error(`Report shard not found: ${value}`);
|
|
4852
4653
|
const realRoot = fs22.realpathSync(reportRoot);
|
|
4853
4654
|
const realFile = fs22.realpathSync(absolute);
|
|
4854
|
-
const realRelation =
|
|
4855
|
-
if (realRelation === ".." || realRelation.startsWith(`..${
|
|
4655
|
+
const realRelation = path26.relative(realRoot, realFile);
|
|
4656
|
+
if (realRelation === ".." || realRelation.startsWith(`..${path26.sep}`) || path26.isAbsolute(realRelation)) throw new Error(`Report shard resolves outside report root: ${value}`);
|
|
4856
4657
|
return realFile;
|
|
4857
4658
|
};
|
|
4858
4659
|
const candidates = [
|
|
@@ -4875,19 +4676,19 @@ function updateReportItem(options) {
|
|
|
4875
4676
|
if (options.harmonyosFiles) match.item.harmonyosFiles = [...new Set(options.harmonyosFiles.map((file) => managedHarmonyFile(projectRoot, file).relative))];
|
|
4876
4677
|
if (options.androidSourceFiles) match.item.sourceProvenance = provenanceForFiles(projectRoot, options.androidSourceFiles);
|
|
4877
4678
|
if (options.sourceProvenance) match.item.sourceProvenance = options.sourceProvenance;
|
|
4878
|
-
if (options.testEvidence) match.item.testEvidence = [...new Set(options.testEvidence.map((file) => insideProject(projectRoot,
|
|
4679
|
+
if (options.testEvidence) match.item.testEvidence = [...new Set(options.testEvidence.map((file) => insideProject(projectRoot, harmonyRoot2, workspaceRoot, file, true)))];
|
|
4879
4680
|
if (options.difference !== void 0) match.item.difference = options.difference?.trim() || null;
|
|
4880
4681
|
if (["implemented", "verified"].includes(match.item.status) && !match.item.harmonyosFiles?.length) throw new Error(`${match.item.status} requires at least one HarmonyOS implementation file`);
|
|
4881
4682
|
if (["implemented", "verified"].includes(match.item.status) && !match.item.sourceProvenance?.length) throw new Error(`${match.item.status} requires at least one Android source provenance entry`);
|
|
4882
4683
|
if (match.item.status === "verified" && !match.item.testEvidence?.length) throw new Error("verified requires at least one test evidence file");
|
|
4883
4684
|
if (match.item.status === "blocked" && !match.item.difference) throw new Error("blocked requires --difference");
|
|
4884
4685
|
atomicJson3(match.file, match.document);
|
|
4885
|
-
return { status: "passed", ref: options.ref, file:
|
|
4686
|
+
return { status: "passed", ref: options.ref, file: path26.relative(projectRoot, match.file).replaceAll("\\", "/"), item: match.item };
|
|
4886
4687
|
}
|
|
4887
4688
|
|
|
4888
4689
|
// src/commands/mock-server-generator.ts
|
|
4889
4690
|
import fs23 from "fs";
|
|
4890
|
-
import
|
|
4691
|
+
import path27 from "path";
|
|
4891
4692
|
var requiredScenarios = ["success", "empty", "business-failure", "boundary"];
|
|
4892
4693
|
function validateSpec(spec) {
|
|
4893
4694
|
const errors = [];
|
|
@@ -4947,21 +4748,21 @@ const server = http.createServer(async (request, response) => {
|
|
|
4947
4748
|
server.listen(port, host, () => console.log(JSON.stringify({ status: "ready", host, port })));
|
|
4948
4749
|
`;
|
|
4949
4750
|
function generateMockServer(options) {
|
|
4950
|
-
const projectRoot =
|
|
4951
|
-
const specFile =
|
|
4751
|
+
const projectRoot = path27.resolve(options.project);
|
|
4752
|
+
const specFile = path27.resolve(options.specFile);
|
|
4952
4753
|
if (!fs23.existsSync(specFile)) throw new Error(`Protocol spec not found: ${specFile}`);
|
|
4953
4754
|
const spec = JSON.parse(fs23.readFileSync(specFile, "utf8"));
|
|
4954
4755
|
const errors = validateSpec(spec);
|
|
4955
4756
|
if (errors.length) throw new Error(`Invalid protocol spec: ${errors.join("; ")}`);
|
|
4956
|
-
const root =
|
|
4757
|
+
const root = path27.join(projectRoot, "mock-server");
|
|
4957
4758
|
if (fs23.existsSync(root) && fs23.readdirSync(root).length && !options.force) throw new Error(`Mock server directory is not empty: ${root}; use --force to replace generated files`);
|
|
4958
4759
|
fs23.mkdirSync(root, { recursive: true });
|
|
4959
|
-
fs23.writeFileSync(
|
|
4760
|
+
fs23.writeFileSync(path27.join(root, "scenarios.json"), `${JSON.stringify(spec, null, 2)}
|
|
4960
4761
|
`, "utf8");
|
|
4961
|
-
fs23.writeFileSync(
|
|
4962
|
-
fs23.writeFileSync(
|
|
4762
|
+
fs23.writeFileSync(path27.join(root, "server.mjs"), serverSource, "utf8");
|
|
4763
|
+
fs23.writeFileSync(path27.join(root, "README.md"), `# Mock Server
|
|
4963
4764
|
|
|
4964
|
-
Generated from \`${
|
|
4765
|
+
Generated from \`${path27.relative(projectRoot, specFile).replaceAll("\\", "/")}\`.
|
|
4965
4766
|
|
|
4966
4767
|
Start safely:
|
|
4967
4768
|
|
|
@@ -4976,16 +4777,16 @@ Select a scenario with query parameter \`?scenario=empty\` or header \`x-droid2h
|
|
|
4976
4777
|
|
|
4977
4778
|
// src/commands/logs.ts
|
|
4978
4779
|
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
4979
|
-
import
|
|
4780
|
+
import path28 from "path";
|
|
4980
4781
|
function loadRecords(projectRoot) {
|
|
4981
|
-
const directory =
|
|
4782
|
+
const directory = path28.join(migrationDirectory(projectRoot), "logs", "daily");
|
|
4982
4783
|
if (!existsSync7(directory)) return [];
|
|
4983
4784
|
return readdirSync3(directory).filter((file) => file.endsWith(".jsonl")).sort().flatMap(
|
|
4984
|
-
(file) => readFileSync6(
|
|
4785
|
+
(file) => readFileSync6(path28.join(directory, file), "utf8").split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
4985
4786
|
);
|
|
4986
4787
|
}
|
|
4987
4788
|
function queryCommandLogs(project, action, value) {
|
|
4988
|
-
const projectRoot =
|
|
4789
|
+
const projectRoot = path28.resolve(project);
|
|
4989
4790
|
const records = loadRecords(projectRoot);
|
|
4990
4791
|
if (action === "list") {
|
|
4991
4792
|
const selected = value ? records.filter((record) => record.date === value) : records;
|
|
@@ -5016,7 +4817,7 @@ function queryCommandLogs(project, action, value) {
|
|
|
5016
4817
|
// src/logging/command-log.ts
|
|
5017
4818
|
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5018
4819
|
import { randomUUID } from "crypto";
|
|
5019
|
-
import
|
|
4820
|
+
import path29 from "path";
|
|
5020
4821
|
var COMMAND_LOG_SCHEMA_VERSION = 1;
|
|
5021
4822
|
var SECRET_OPTIONS = /^(--?(?:password|pass|token|secret|api[-_]?key|access[-_]?key|signing[-_]?key))$/i;
|
|
5022
4823
|
function localDate(date) {
|
|
@@ -5056,10 +4857,10 @@ function discoverProjectRoot(argv, cwd = process.cwd()) {
|
|
|
5056
4857
|
continue;
|
|
5057
4858
|
}
|
|
5058
4859
|
if (argument.startsWith("-") || reserved.has(argument)) continue;
|
|
5059
|
-
const candidate =
|
|
4860
|
+
const candidate = path29.resolve(cwd, argument);
|
|
5060
4861
|
if (existsSync8(candidate)) return candidate;
|
|
5061
4862
|
}
|
|
5062
|
-
return
|
|
4863
|
+
return path29.resolve(cwd);
|
|
5063
4864
|
}
|
|
5064
4865
|
function knownArtifacts(projectRoot) {
|
|
5065
4866
|
const candidates = [
|
|
@@ -5072,12 +4873,12 @@ function knownArtifacts(projectRoot) {
|
|
|
5072
4873
|
["managed-evidence", ".migration/evidence/index.json"]
|
|
5073
4874
|
];
|
|
5074
4875
|
const workspaceRoot = migrationWorkspaceRoot(projectRoot);
|
|
5075
|
-
return candidates.filter(([, relative2]) => existsSync8(
|
|
4876
|
+
return candidates.filter(([, relative2]) => existsSync8(path29.join(workspaceRoot, relative2))).map(([kind, relative2]) => ({ kind, path: relative2.replaceAll("\\", "/") }));
|
|
5076
4877
|
}
|
|
5077
4878
|
function commandName(argv) {
|
|
5078
|
-
const
|
|
4879
|
+
const command2 = argv[0] ?? "unknown";
|
|
5079
4880
|
const grouped = /* @__PURE__ */ new Set(["analyze", "report", "build", "test", "tools", "config", "checkpoint", "device", "evidence", "mock", "service"]);
|
|
5080
|
-
return grouped.has(
|
|
4881
|
+
return grouped.has(command2) && argv[1] ? `${command2} ${argv[1]}` : command2;
|
|
5081
4882
|
}
|
|
5082
4883
|
function atomicJson4(file, value) {
|
|
5083
4884
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -5120,18 +4921,18 @@ async function withCommandLog(argv, version, execute) {
|
|
|
5120
4921
|
artifacts: knownArtifacts(projectRoot)
|
|
5121
4922
|
};
|
|
5122
4923
|
ensureMigrationWorkspace(projectRoot);
|
|
5123
|
-
const logsRoot =
|
|
5124
|
-
const runDirectory =
|
|
5125
|
-
const dailyDirectory =
|
|
4924
|
+
const logsRoot = path29.join(migrationWorkspaceRoot(projectRoot), ".migration", "logs");
|
|
4925
|
+
const runDirectory = path29.join(logsRoot, "runs", date);
|
|
4926
|
+
const dailyDirectory = path29.join(logsRoot, "daily");
|
|
5126
4927
|
mkdirSync4(runDirectory, { recursive: true });
|
|
5127
4928
|
mkdirSync4(dailyDirectory, { recursive: true });
|
|
5128
|
-
atomicJson4(
|
|
5129
|
-
appendFileSync(
|
|
4929
|
+
atomicJson4(path29.join(runDirectory, `${runId}.json`), record);
|
|
4930
|
+
appendFileSync(path29.join(dailyDirectory, `${date}.jsonl`), `${JSON.stringify(record)}
|
|
5130
4931
|
`, "utf8");
|
|
5131
|
-
atomicJson4(
|
|
4932
|
+
atomicJson4(path29.join(logsRoot, "index.json"), {
|
|
5132
4933
|
schemaVersion: COMMAND_LOG_SCHEMA_VERSION,
|
|
5133
4934
|
lastRunId: runId,
|
|
5134
|
-
lastRun:
|
|
4935
|
+
lastRun: path29.relative(path29.dirname(logsRoot), path29.join(runDirectory, `${runId}.json`)).replaceAll("\\", "/"),
|
|
5135
4936
|
updatedAt: finished.toISOString()
|
|
5136
4937
|
});
|
|
5137
4938
|
} catch (logError) {
|
|
@@ -5142,30 +4943,30 @@ async function withCommandLog(argv, version, execute) {
|
|
|
5142
4943
|
|
|
5143
4944
|
// src/runtime-paths.ts
|
|
5144
4945
|
import fs24 from "fs";
|
|
5145
|
-
import
|
|
4946
|
+
import path30 from "path";
|
|
5146
4947
|
import { fileURLToPath } from "url";
|
|
5147
4948
|
function resolveRuntimePaths(entryUrl) {
|
|
5148
4949
|
const entryFile = fileURLToPath(entryUrl);
|
|
5149
|
-
const directory =
|
|
5150
|
-
const roots = [
|
|
5151
|
-
const packageRoot = roots.find((candidate) => fs24.existsSync(
|
|
5152
|
-
const runtimeRoot = packageRoot ??
|
|
4950
|
+
const directory = path30.dirname(entryFile);
|
|
4951
|
+
const roots = [path30.resolve(directory, ".."), directory];
|
|
4952
|
+
const packageRoot = roots.find((candidate) => fs24.existsSync(path30.join(candidate, "package.json")));
|
|
4953
|
+
const runtimeRoot = packageRoot ?? path30.resolve(directory, "..");
|
|
5153
4954
|
const templatesRoot = [
|
|
5154
|
-
packageRoot &&
|
|
4955
|
+
packageRoot && path30.join(packageRoot, "templates")
|
|
5155
4956
|
].filter(Boolean).find((candidate) => fs24.existsSync(candidate));
|
|
5156
|
-
const preflightChecklistPath = packageRoot &&
|
|
4957
|
+
const preflightChecklistPath = packageRoot && path30.join(packageRoot, "config", "preflight-checklist.json");
|
|
5157
4958
|
return { entryFile, runtimeRoot, packageRoot, templatesRoot, preflightChecklistPath };
|
|
5158
4959
|
}
|
|
5159
4960
|
|
|
5160
4961
|
// src/kernel/migration-kernel.ts
|
|
5161
4962
|
import { createHash as createHash8, randomUUID as randomUUID2 } from "crypto";
|
|
5162
4963
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5163
|
-
import
|
|
4964
|
+
import fs29 from "fs";
|
|
5164
4965
|
import path35 from "path";
|
|
5165
4966
|
|
|
5166
4967
|
// src/planning/facts/reader.ts
|
|
5167
4968
|
import fs25 from "fs";
|
|
5168
|
-
import
|
|
4969
|
+
import path31 from "path";
|
|
5169
4970
|
|
|
5170
4971
|
// src/planning/util.ts
|
|
5171
4972
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -5210,17 +5011,17 @@ function readJson4(file) {
|
|
|
5210
5011
|
}
|
|
5211
5012
|
function inside3(root, relative2, label) {
|
|
5212
5013
|
if (typeof relative2 !== "string" || !relative2.trim()) throw new PlanningFactsError("FACTS_SHARD_MISSING", `${label} path is missing`);
|
|
5213
|
-
const resolvedRoot =
|
|
5214
|
-
const absolute =
|
|
5215
|
-
const rel =
|
|
5216
|
-
if (!rel || rel.startsWith(`..${
|
|
5014
|
+
const resolvedRoot = path31.resolve(root);
|
|
5015
|
+
const absolute = path31.resolve(resolvedRoot, relative2);
|
|
5016
|
+
const rel = path31.relative(resolvedRoot, absolute);
|
|
5017
|
+
if (!rel || rel.startsWith(`..${path31.sep}`) || path31.isAbsolute(rel)) {
|
|
5217
5018
|
throw new PlanningFactsError("FACTS_PATH_OUTSIDE_ROOT", `${label} must stay inside ${resolvedRoot}: ${relative2}`);
|
|
5218
5019
|
}
|
|
5219
5020
|
if (!fs25.existsSync(absolute) || !fs25.statSync(absolute).isFile()) throw new PlanningFactsError("FACTS_SHARD_MISSING", `${label} not found: ${absolute}`);
|
|
5220
5021
|
const realRoot = fs25.realpathSync(resolvedRoot);
|
|
5221
5022
|
const realFile = fs25.realpathSync(absolute);
|
|
5222
|
-
const realRelative =
|
|
5223
|
-
if (!realRelative || realRelative.startsWith(`..${
|
|
5023
|
+
const realRelative = path31.relative(realRoot, realFile);
|
|
5024
|
+
if (!realRelative || realRelative.startsWith(`..${path31.sep}`) || path31.isAbsolute(realRelative)) {
|
|
5224
5025
|
throw new PlanningFactsError("FACTS_PATH_OUTSIDE_ROOT", `${label} resolves outside ${realRoot}: ${relative2}`);
|
|
5225
5026
|
}
|
|
5226
5027
|
return realFile;
|
|
@@ -5233,9 +5034,9 @@ function requireVersion(value, expected, label) {
|
|
|
5233
5034
|
if (value !== expected) throw new PlanningFactsError("FACTS_VERSION_UNSUPPORTED", `${label} must be ${expected}; found ${value ?? "<missing>"}`);
|
|
5234
5035
|
}
|
|
5235
5036
|
function readFactsBundleV2(project) {
|
|
5236
|
-
const projectRoot =
|
|
5237
|
-
const specRoot =
|
|
5238
|
-
const indexFile =
|
|
5037
|
+
const projectRoot = path31.resolve(project);
|
|
5038
|
+
const specRoot = path31.join(migrationDirectory(projectRoot), "spec");
|
|
5039
|
+
const indexFile = path31.join(specRoot, "index.json");
|
|
5239
5040
|
if (!fs25.existsSync(indexFile)) throw new PlanningFactsError("FACTS_SHARD_MISSING", `SPEC index not found: ${indexFile}`);
|
|
5240
5041
|
const index = readJson4(indexFile);
|
|
5241
5042
|
requireVersion(index.specVersion, "2.0.0", "SPEC specVersion");
|
|
@@ -5303,17 +5104,30 @@ function readFactsBundleV2(project) {
|
|
|
5303
5104
|
}
|
|
5304
5105
|
|
|
5305
5106
|
// src/planning/types.ts
|
|
5306
|
-
var TASK_PLAN_VERSION = "
|
|
5307
|
-
var TASK_ENVELOPE_VERSION = "
|
|
5308
|
-
var
|
|
5107
|
+
var TASK_PLAN_VERSION = "2.3.0";
|
|
5108
|
+
var TASK_ENVELOPE_VERSION = "3.0.0";
|
|
5109
|
+
var BEHAVIOR_PLAN_VERSION = "1.0.0";
|
|
5110
|
+
var EXECUTION_PLAN_VERSION = "1.3.0";
|
|
5111
|
+
var PLANNING_MODEL_VERSION = "2.0.0";
|
|
5309
5112
|
var FACTS_ADAPTER_VERSION = "1.0.0";
|
|
5310
|
-
var PLANNER_POLICY_VERSION = "
|
|
5113
|
+
var PLANNER_POLICY_VERSION = "2.3.0";
|
|
5311
5114
|
var DEFAULT_CLOSURE_BUDGET = {
|
|
5312
5115
|
maxFactCount: 30,
|
|
5313
5116
|
maxSourceFileCount: 15,
|
|
5314
5117
|
maxTraceNodeCount: 50,
|
|
5315
5118
|
maxEstimatedFactTokens: 12e3
|
|
5316
5119
|
};
|
|
5120
|
+
var DEFAULT_EXECUTION_BUDGET = {
|
|
5121
|
+
maxBehaviorCount: 5,
|
|
5122
|
+
maxObservableOutcomeCount: 3,
|
|
5123
|
+
maxProductionFileCount: 4,
|
|
5124
|
+
maxTestFileCount: 2,
|
|
5125
|
+
maxCapabilityCount: 2,
|
|
5126
|
+
maxFactCount: 45,
|
|
5127
|
+
maxSourceFileCount: 8,
|
|
5128
|
+
maxTraceNodeCount: 60,
|
|
5129
|
+
maxEstimatedFactTokens: 14e3
|
|
5130
|
+
};
|
|
5317
5131
|
|
|
5318
5132
|
// src/planning/facts/v2-adapter.ts
|
|
5319
5133
|
function evidence(values) {
|
|
@@ -5617,7 +5431,8 @@ function candidateForEntry(entryId, facts) {
|
|
|
5617
5431
|
...effects.flatMap((effect) => [effect.id, effect.targetId])
|
|
5618
5432
|
]));
|
|
5619
5433
|
const issues = facts.issues.filter((issue) => issue.relatedRefs.some((ref) => related.has(ref)));
|
|
5620
|
-
const
|
|
5434
|
+
const unresolvedIssues = issues.filter((issue) => issue.blocking && !issue.resolved);
|
|
5435
|
+
const blocking = unresolvedIssues.length > 0;
|
|
5621
5436
|
const isLaunch = facts.app.launcherEntryIds.includes(entry.id) || entry.triggerCategory === "application" && entry.event === "launch";
|
|
5622
5437
|
const type = blocking ? "resolution" : (() => {
|
|
5623
5438
|
if (isLaunch) return "bootstrap";
|
|
@@ -5650,7 +5465,7 @@ function candidateForEntry(entryId, facts) {
|
|
|
5650
5465
|
surfaceRefs: new Set(surfaces),
|
|
5651
5466
|
controlRefs: new Set(controls.map((control) => control.id)),
|
|
5652
5467
|
interactionRefs: new Set(effects.map((effect) => effect.id)),
|
|
5653
|
-
issueRefs: new Set(
|
|
5468
|
+
issueRefs: new Set(unresolvedIssues.map((issue) => issue.id)),
|
|
5654
5469
|
warningRefs: new Set(warningRefs),
|
|
5655
5470
|
sourceSurfaceRefs: new Set(entry.ownerSurfaceId ? [entry.ownerSurfaceId] : []),
|
|
5656
5471
|
targetEdges: effects.flatMap((effect) => targetSurfaceIds(effect, facts).map((targetSurfaceRef) => ({ sourceSurfaceRef: entry.ownerSurfaceId, targetSurfaceRef, effectRef: effect.id }))),
|
|
@@ -5658,7 +5473,7 @@ function candidateForEntry(entryId, facts) {
|
|
|
5658
5473
|
...entry.evidence,
|
|
5659
5474
|
...controls.flatMap((control) => control.evidence),
|
|
5660
5475
|
...effects.flatMap((effect) => effect.evidence),
|
|
5661
|
-
...
|
|
5476
|
+
...unresolvedIssues.flatMap((issue) => issue.evidence)
|
|
5662
5477
|
], evidenceKey),
|
|
5663
5478
|
codeRefs: entry.trace.codeRefs,
|
|
5664
5479
|
requiresSourceReview: effects.length === 0 || blocking || entry.trace.warnings.length > 0,
|
|
@@ -5954,8 +5769,8 @@ function chooseSurfaceOwners(candidates, facts) {
|
|
|
5954
5769
|
function reportableRefs(facts) {
|
|
5955
5770
|
return new Set(facts.requiredFactRefs);
|
|
5956
5771
|
}
|
|
5957
|
-
function
|
|
5958
|
-
return `
|
|
5772
|
+
function behaviorIdFor(candidate) {
|
|
5773
|
+
return `behavior:${sha2562({
|
|
5959
5774
|
type: candidate.type,
|
|
5960
5775
|
primaryFactRefs: uniqueSorted(candidate.primary),
|
|
5961
5776
|
path: {
|
|
@@ -5967,7 +5782,7 @@ function taskIdFor(candidate) {
|
|
|
5967
5782
|
}
|
|
5968
5783
|
function downstreamCounts(tasks) {
|
|
5969
5784
|
const children = /* @__PURE__ */ new Map();
|
|
5970
|
-
for (const task of tasks) for (const dependency of task.
|
|
5785
|
+
for (const task of tasks) for (const dependency of task.dependsOnBehaviorIds) children.set(dependency, [...children.get(dependency) ?? [], task.id]);
|
|
5971
5786
|
const out = /* @__PURE__ */ new Map();
|
|
5972
5787
|
for (const task of tasks) {
|
|
5973
5788
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5982,7 +5797,7 @@ function downstreamCounts(tasks) {
|
|
|
5982
5797
|
}
|
|
5983
5798
|
return out;
|
|
5984
5799
|
}
|
|
5985
|
-
function
|
|
5800
|
+
function compileBehaviorPlanFromPlanningFacts(facts, androidFingerprint) {
|
|
5986
5801
|
let candidates = mergeCompatibleEntryCandidates(facts.entries.map((entry) => candidateForEntry(entry.id, facts)), facts);
|
|
5987
5802
|
for (const surfaceRef of facts.app.launcherSurfaceIds) {
|
|
5988
5803
|
if (!candidates.some((candidate) => candidate.type === "bootstrap" && candidate.surfaceRefs.has(surfaceRef))) {
|
|
@@ -6036,8 +5851,8 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6036
5851
|
const closureRefs = /* @__PURE__ */ new Set([...candidate.entryRefs, ...candidate.surfaceRefs, ...candidate.controlRefs, ...candidate.interactionRefs, ...candidate.issueRefs]);
|
|
6037
5852
|
candidate.supporting = new Set([...closureRefs].filter((ref) => !candidate.primary.has(ref)));
|
|
6038
5853
|
}
|
|
6039
|
-
const idByKey = new Map(candidates.map((candidate) => [candidate.key,
|
|
6040
|
-
if (new Set(idByKey.values()).size !== candidates.length) throw new Error("
|
|
5854
|
+
const idByKey = new Map(candidates.map((candidate) => [candidate.key, behaviorIdFor(candidate)]));
|
|
5855
|
+
if (new Set(idByKey.values()).size !== candidates.length) throw new Error("Behavior ID collision while compiling plan");
|
|
6041
5856
|
const launcherResolvers = candidates.filter((candidate) => candidate.type === "resolution" && candidate.issueRefs.has("issue:launcher-unresolved"));
|
|
6042
5857
|
const taskDrafts = candidates.map((candidate) => {
|
|
6043
5858
|
const dependencies = /* @__PURE__ */ new Set();
|
|
@@ -6049,11 +5864,11 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6049
5864
|
}
|
|
6050
5865
|
if (facts.app.launcherOrigin === "unresolved") for (const resolver of launcherResolvers) dependencies.add(idByKey.get(resolver.key));
|
|
6051
5866
|
}
|
|
6052
|
-
return { candidate, id: idByKey.get(candidate.key),
|
|
5867
|
+
return { candidate, id: idByKey.get(candidate.key), dependsOnBehaviorIds: uniqueSorted(dependencies) };
|
|
6053
5868
|
});
|
|
6054
5869
|
const downstream = downstreamCounts(taskDrafts);
|
|
6055
5870
|
const reportable = reportableRefs(facts);
|
|
6056
|
-
const
|
|
5871
|
+
const responsibilities = taskDrafts.map(({ candidate, id, dependsOnBehaviorIds }) => {
|
|
6057
5872
|
const primaryFactRefs = uniqueSorted(candidate.primary);
|
|
6058
5873
|
const supportingFactRefs = uniqueSorted(candidate.supporting);
|
|
6059
5874
|
const core = {
|
|
@@ -6070,7 +5885,7 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6070
5885
|
title: candidate.title,
|
|
6071
5886
|
primaryFactRefs,
|
|
6072
5887
|
supportingFactRefs,
|
|
6073
|
-
|
|
5888
|
+
dependsOnBehaviorIds,
|
|
6074
5889
|
closure: {
|
|
6075
5890
|
entryRefs: core.entries,
|
|
6076
5891
|
surfaceRefs: core.surfaces,
|
|
@@ -6098,12 +5913,12 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6098
5913
|
};
|
|
6099
5914
|
}).sort((a, b) => typeOrder[a.type] - typeOrder[b.type] || a.id.localeCompare(b.id));
|
|
6100
5915
|
const primaryOwnerByFactRef = Object.fromEntries([...ownerKeyByFactRef.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ref, key]) => [ref, idByKey.get(key)]));
|
|
6101
|
-
const
|
|
6102
|
-
for (const
|
|
5916
|
+
const supportingBehaviorIdsByFactRef = {};
|
|
5917
|
+
for (const responsibility of responsibilities) for (const ref of responsibility.supportingFactRefs) supportingBehaviorIdsByFactRef[ref] = uniqueSorted([...supportingBehaviorIdsByFactRef[ref] ?? [], responsibility.id]);
|
|
6103
5918
|
const planningWarnings = [...facts.diagnostics];
|
|
6104
|
-
for (const
|
|
5919
|
+
for (const responsibility of responsibilities.filter((item) => item.closure.requiresSourceReview && item.type !== "resolution")) planningWarnings.push({ code: "SOURCE_REVIEW_REQUIRED", message: `${responsibility.id} has no direct observable interaction and requires Android source review`, relatedRefs: responsibility.closure.entryRefs });
|
|
6105
5920
|
return {
|
|
6106
|
-
|
|
5921
|
+
behaviorPlanVersion: BEHAVIOR_PLAN_VERSION,
|
|
6107
5922
|
planningModelVersion: PLANNING_MODEL_VERSION,
|
|
6108
5923
|
factsAdapterVersion: FACTS_ADAPTER_VERSION,
|
|
6109
5924
|
plannerPolicyVersion: PLANNER_POLICY_VERSION,
|
|
@@ -6119,22 +5934,716 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6119
5934
|
launcherEntryRefs: facts.app.launcherEntryIds,
|
|
6120
5935
|
origin: facts.app.launcherOrigin
|
|
6121
5936
|
},
|
|
6122
|
-
|
|
6123
|
-
ownership: { primaryOwnerByFactRef,
|
|
5937
|
+
responsibilities,
|
|
5938
|
+
ownership: { primaryOwnerByFactRef, supportingBehaviorIdsByFactRef: Object.fromEntries(Object.entries(supportingBehaviorIdsByFactRef).sort(([a], [b]) => a.localeCompare(b))) },
|
|
6124
5939
|
planningWarnings: uniqueBy(planningWarnings, (item) => `${item.code}:${item.message}:${item.relatedRefs.join(",")}`)
|
|
6125
5940
|
};
|
|
6126
5941
|
}
|
|
6127
5942
|
|
|
6128
|
-
// src/planning/
|
|
5943
|
+
// src/planning/execution-context.ts
|
|
6129
5944
|
import fs26 from "fs";
|
|
5945
|
+
import path32 from "path";
|
|
5946
|
+
function readJson6(file) {
|
|
5947
|
+
try {
|
|
5948
|
+
return JSON.parse(fs26.readFileSync(file, "utf8"));
|
|
5949
|
+
} catch {
|
|
5950
|
+
return null;
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
function inside4(root, target) {
|
|
5954
|
+
const relative2 = path32.relative(root, target);
|
|
5955
|
+
return relative2 === "" || relative2 !== ".." && !relative2.startsWith(`..${path32.sep}`) && !path32.isAbsolute(relative2);
|
|
5956
|
+
}
|
|
5957
|
+
function validatedHarmonyFile(projectRoot, root, value) {
|
|
5958
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("Report HarmonyOS mapping must be a non-empty string");
|
|
5959
|
+
const absolute = path32.resolve(projectRoot, value);
|
|
5960
|
+
if (!inside4(root, absolute)) throw new Error(`Report HarmonyOS mapping escapes the configured project: ${value}`);
|
|
5961
|
+
if (!fs26.existsSync(absolute) || !fs26.statSync(absolute).isFile()) throw new Error(`Report HarmonyOS mapping does not exist: ${value}`);
|
|
5962
|
+
const realRoot = fs26.realpathSync(root);
|
|
5963
|
+
const realFile = fs26.realpathSync(absolute);
|
|
5964
|
+
if (!inside4(realRoot, realFile)) throw new Error(`Report HarmonyOS mapping resolves outside the configured project: ${value}`);
|
|
5965
|
+
return path32.relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
5966
|
+
}
|
|
5967
|
+
function confinedReportFile(reportRoot, relative2) {
|
|
5968
|
+
if (!relative2.trim() || path32.isAbsolute(relative2)) throw new Error(`Report shard path must be relative: ${relative2}`);
|
|
5969
|
+
const absolute = path32.resolve(reportRoot, relative2);
|
|
5970
|
+
if (!inside4(reportRoot, absolute) || absolute === reportRoot) throw new Error(`Report shard escapes report root: ${relative2}`);
|
|
5971
|
+
if (!fs26.existsSync(absolute) || !fs26.statSync(absolute).isFile()) throw new Error(`Report shard does not exist: ${relative2}`);
|
|
5972
|
+
const realRoot = fs26.realpathSync(reportRoot);
|
|
5973
|
+
const realFile = fs26.realpathSync(absolute);
|
|
5974
|
+
if (!inside4(realRoot, realFile)) throw new Error(`Report shard resolves outside report root: ${relative2}`);
|
|
5975
|
+
return realFile;
|
|
5976
|
+
}
|
|
5977
|
+
function reportHarmonyFiles(projectRoot, root) {
|
|
5978
|
+
const reportRoot = path32.join(migrationDirectory(projectRoot), "report");
|
|
5979
|
+
const index = readJson6(path32.join(reportRoot, "index.json"));
|
|
5980
|
+
if (!index) return {};
|
|
5981
|
+
const shardFiles = [
|
|
5982
|
+
...(index.pages ?? []).map((item) => item.file),
|
|
5983
|
+
index.unassignedEntriesFile,
|
|
5984
|
+
index.interactionsFile,
|
|
5985
|
+
index.discoveriesFile
|
|
5986
|
+
].filter((value) => typeof value === "string");
|
|
5987
|
+
const result = {};
|
|
5988
|
+
for (const relative2 of shardFiles) {
|
|
5989
|
+
const document = readJson6(confinedReportFile(reportRoot, relative2));
|
|
5990
|
+
if (!document) continue;
|
|
5991
|
+
const items = [
|
|
5992
|
+
document.page,
|
|
5993
|
+
...document.controls ?? [],
|
|
5994
|
+
...document.entries ?? [],
|
|
5995
|
+
...document.interactions ?? [],
|
|
5996
|
+
...document.items ?? []
|
|
5997
|
+
].filter(Boolean);
|
|
5998
|
+
for (const item of items) {
|
|
5999
|
+
if (typeof item.ref !== "string" || !Array.isArray(item.harmonyosFiles)) continue;
|
|
6000
|
+
result[item.ref] = uniqueSorted(item.harmonyosFiles.map((value) => validatedHarmonyFile(projectRoot, root, value)));
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
return Object.fromEntries(Object.entries(result).sort(([left], [right]) => left.localeCompare(right)));
|
|
6004
|
+
}
|
|
6005
|
+
function harmonyRoot(projectRoot) {
|
|
6006
|
+
const config = readJson6(path32.join(migrationDirectory(projectRoot), "config.json"));
|
|
6007
|
+
const configured = typeof config?.harmonyProject === "string" ? config.harmonyProject : "ohos";
|
|
6008
|
+
return path32.isAbsolute(configured) ? configured : path32.resolve(projectRoot, configured);
|
|
6009
|
+
}
|
|
6010
|
+
function existingGlobalHotspots(projectRoot) {
|
|
6011
|
+
const root = harmonyRoot(projectRoot);
|
|
6012
|
+
if (!fs26.existsSync(root) || !fs26.statSync(root).isDirectory()) return [];
|
|
6013
|
+
const relativeRoot = path32.relative(projectRoot, root).replaceAll("\\", "/");
|
|
6014
|
+
const candidates = [
|
|
6015
|
+
"build-profile.json5",
|
|
6016
|
+
"AppScope/app.json5",
|
|
6017
|
+
"oh-package.json5",
|
|
6018
|
+
"entry/oh-package.json5",
|
|
6019
|
+
"entry/src/main/module.json5",
|
|
6020
|
+
"entry/src/main/resources/base/profile/main_pages.json",
|
|
6021
|
+
"entry/src/main/ets/entryability/EntryAbility.ets",
|
|
6022
|
+
"entry/src/main/ets/pages/Index.ets",
|
|
6023
|
+
"entry/src/main/ets/common/RouteConstants.ets"
|
|
6024
|
+
];
|
|
6025
|
+
return candidates.filter((relative2) => fs26.existsSync(path32.join(root, relative2))).map((relative2) => `${relativeRoot}/${relative2}`.replace(/^\.\//, "")).sort();
|
|
6026
|
+
}
|
|
6027
|
+
function loadExecutionPlanningContext(projectRootArgument) {
|
|
6028
|
+
const projectRoot = path32.resolve(projectRootArgument);
|
|
6029
|
+
const root = harmonyRoot(projectRoot);
|
|
6030
|
+
const rootAvailable = fs26.existsSync(root) && fs26.statSync(root).isDirectory();
|
|
6031
|
+
return {
|
|
6032
|
+
harmonyFilesByFactRef: rootAvailable ? reportHarmonyFiles(projectRoot, root) : {},
|
|
6033
|
+
globalHotspotFiles: existingGlobalHotspots(projectRoot)
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
var emptyExecutionPlanningContext = {
|
|
6037
|
+
harmonyFilesByFactRef: {},
|
|
6038
|
+
globalHotspotFiles: []
|
|
6039
|
+
};
|
|
6040
|
+
|
|
6041
|
+
// src/planning/execution-planner.ts
|
|
6042
|
+
var typeOrder2 = {
|
|
6043
|
+
resolution: 0,
|
|
6044
|
+
bootstrap: 1,
|
|
6045
|
+
capability: 2,
|
|
6046
|
+
journey: 3,
|
|
6047
|
+
assembly: 4,
|
|
6048
|
+
regression: 5
|
|
6049
|
+
};
|
|
6050
|
+
function isReusableSurface(kind) {
|
|
6051
|
+
return /(?:list[-_ ]?item|item|reusable|layout|custom[-_ ]?view|cell|row)/i.test(kind);
|
|
6052
|
+
}
|
|
6053
|
+
function isHostedOverlay(kind) {
|
|
6054
|
+
return /(?:dialog|bottom[-_ ]?sheet|sheet|popup|overlay)/i.test(kind);
|
|
6055
|
+
}
|
|
6056
|
+
function canonicalPageRefs(facts) {
|
|
6057
|
+
const byId = new Map(facts.surfaces.map((surface) => [surface.id, surface]));
|
|
6058
|
+
const memo = /* @__PURE__ */ new Map();
|
|
6059
|
+
const resolve4 = (surfaceRef, visiting = /* @__PURE__ */ new Set()) => {
|
|
6060
|
+
if (memo.has(surfaceRef)) return memo.get(surfaceRef);
|
|
6061
|
+
const surface = byId.get(surfaceRef);
|
|
6062
|
+
if (!surface || visiting.has(surfaceRef)) return surfaceRef;
|
|
6063
|
+
visiting.add(surfaceRef);
|
|
6064
|
+
const parentPages = uniqueSorted(surface.parentSurfaceIds.filter((ref) => byId.has(ref)).map((ref) => resolve4(ref, visiting)));
|
|
6065
|
+
const canonical2 = parentPages.length === 1 && (isReusableSurface(surface.kind) || isHostedOverlay(surface.kind) || !surface.topLevel) ? parentPages[0] : surfaceRef;
|
|
6066
|
+
visiting.delete(surfaceRef);
|
|
6067
|
+
memo.set(surfaceRef, canonical2);
|
|
6068
|
+
return canonical2;
|
|
6069
|
+
};
|
|
6070
|
+
for (const surface of facts.surfaces) resolve4(surface.id);
|
|
6071
|
+
return Object.fromEntries([...memo.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
6072
|
+
}
|
|
6073
|
+
function behaviorSourceSurfaceRefs(behavior, facts) {
|
|
6074
|
+
const entryRefs = new Set(behavior.closure.entryRefs);
|
|
6075
|
+
const fromEntries = facts.entries.filter((entry) => entryRefs.has(entry.id) && entry.ownerSurfaceId).map((entry) => entry.ownerSurfaceId);
|
|
6076
|
+
if (fromEntries.length) return uniqueSorted(fromEntries);
|
|
6077
|
+
const primarySurfaces = behavior.primaryFactRefs.filter((ref) => facts.surfaces.some((surface) => surface.id === ref));
|
|
6078
|
+
return uniqueSorted(primarySurfaces.length ? primarySurfaces : behavior.closure.surfaceRefs);
|
|
6079
|
+
}
|
|
6080
|
+
function behaviorCanonicalPage(behavior, facts, canonicalBySurface) {
|
|
6081
|
+
const pages = uniqueSorted(behaviorSourceSurfaceRefs(behavior, facts).map((ref) => canonicalBySurface[ref] ?? ref));
|
|
6082
|
+
return pages[0] ?? null;
|
|
6083
|
+
}
|
|
6084
|
+
function codeRefKey(value) {
|
|
6085
|
+
if (typeof value.id === "string") return value.id;
|
|
6086
|
+
return stableJson(Object.fromEntries(["file", "name", "symbol", "kind", "type", "capabilityRef"].filter((key) => value[key] !== void 0).map((key) => [key, value[key]])));
|
|
6087
|
+
}
|
|
6088
|
+
function behaviorCapabilityRefs(behavior) {
|
|
6089
|
+
return uniqueSorted(behavior.closure.codeRefs.flatMap((item) => {
|
|
6090
|
+
if (typeof item.capabilityRef === "string" && item.capabilityRef.length > 0) return [item.capabilityRef];
|
|
6091
|
+
if (item.kind === "capability" && typeof item.id === "string" && item.id.length > 0) return [item.id];
|
|
6092
|
+
return [];
|
|
6093
|
+
}));
|
|
6094
|
+
}
|
|
6095
|
+
function candidateForBehavior(behavior, facts, canonicalBySurface) {
|
|
6096
|
+
const effectRefs = new Set(behavior.closure.interactionRefs);
|
|
6097
|
+
const targets = facts.effects.filter((effect) => effectRefs.has(effect.id) && effect.targetKind === "surface").map((effect) => canonicalBySurface[effect.targetId] ?? effect.targetId);
|
|
6098
|
+
return {
|
|
6099
|
+
behaviorIds: [behavior.id],
|
|
6100
|
+
canonicalPageRef: behaviorCanonicalPage(behavior, facts, canonicalBySurface),
|
|
6101
|
+
type: behavior.type,
|
|
6102
|
+
sourceFiles: uniqueSorted([
|
|
6103
|
+
...behavior.closure.sourceEvidence.map((item) => item.file),
|
|
6104
|
+
...behavior.closure.codeRefs.flatMap((item) => typeof item.file === "string" ? [item.file] : [])
|
|
6105
|
+
]),
|
|
6106
|
+
targetPageRefs: uniqueSorted(targets),
|
|
6107
|
+
codeRefKeys: uniqueSorted(behavior.closure.codeRefs.map(codeRefKey)),
|
|
6108
|
+
capabilityRefs: behaviorCapabilityRefs(behavior)
|
|
6109
|
+
};
|
|
6110
|
+
}
|
|
6111
|
+
function combineCandidates2(left, right) {
|
|
6112
|
+
return {
|
|
6113
|
+
behaviorIds: uniqueSorted([...left.behaviorIds, ...right.behaviorIds]),
|
|
6114
|
+
canonicalPageRef: left.canonicalPageRef,
|
|
6115
|
+
type: left.type,
|
|
6116
|
+
sourceFiles: uniqueSorted([...left.sourceFiles, ...right.sourceFiles]),
|
|
6117
|
+
targetPageRefs: uniqueSorted([...left.targetPageRefs, ...right.targetPageRefs]),
|
|
6118
|
+
codeRefKeys: uniqueSorted([...left.codeRefKeys, ...right.codeRefKeys]),
|
|
6119
|
+
capabilityRefs: uniqueSorted([...left.capabilityRefs, ...right.capabilityRefs])
|
|
6120
|
+
};
|
|
6121
|
+
}
|
|
6122
|
+
function intersects(left, right) {
|
|
6123
|
+
const values = new Set(left);
|
|
6124
|
+
return right.some((value) => values.has(value));
|
|
6125
|
+
}
|
|
6126
|
+
function behaviorRefs(candidate, behaviors) {
|
|
6127
|
+
return candidate.behaviorIds.map((id) => behaviors.get(id)).filter(Boolean);
|
|
6128
|
+
}
|
|
6129
|
+
function isRoutingAssembly(candidate) {
|
|
6130
|
+
return candidate.type === "journey" && candidate.targetPageRefs.some((target) => target !== candidate.canonicalPageRef);
|
|
6131
|
+
}
|
|
6132
|
+
function routingHotspotFiles(context) {
|
|
6133
|
+
return context.globalHotspotFiles.filter((file) => /(?:main_pages\.json|RouteConstants\.ets|\/pages\/Index\.ets)$/i.test(file));
|
|
6134
|
+
}
|
|
6135
|
+
function mappedTargetFiles(candidate, primaryRefs, context) {
|
|
6136
|
+
const mapped = uniqueSorted(primaryRefs.flatMap((ref) => context.harmonyFilesByFactRef[ref] ?? []));
|
|
6137
|
+
if (candidate.type === "bootstrap") return uniqueSorted([...mapped, ...context.globalHotspotFiles]);
|
|
6138
|
+
if (isRoutingAssembly(candidate)) return uniqueSorted([...mapped, ...routingHotspotFiles(context)]);
|
|
6139
|
+
return mapped;
|
|
6140
|
+
}
|
|
6141
|
+
function executionCost(candidate, behaviors, context) {
|
|
6142
|
+
const members = behaviorRefs(candidate, behaviors);
|
|
6143
|
+
const factRefs = uniqueSorted(members.flatMap((item) => [...item.primaryFactRefs, ...item.supportingFactRefs]));
|
|
6144
|
+
const mappedFiles = mappedTargetFiles(candidate, factRefs, context);
|
|
6145
|
+
const productionFiles = mappedFiles.filter((file) => !/(?:^|\/)(?:ohosTest|test|tests)(?:\/|$)|(?:Test|\.test)\.(?:ets|ts)$/i.test(file));
|
|
6146
|
+
const testFiles = mappedFiles.filter((file) => !productionFiles.includes(file));
|
|
6147
|
+
return {
|
|
6148
|
+
behaviorCount: members.length,
|
|
6149
|
+
observableOutcomeCount: uniqueSorted(members.map((item) => item.acceptance.observableOutcome)).length,
|
|
6150
|
+
productionFileCount: productionFiles.length,
|
|
6151
|
+
testFileCount: testFiles.length,
|
|
6152
|
+
capabilityCount: candidate.capabilityRefs.length,
|
|
6153
|
+
factCount: new Set(members.flatMap((item) => [...item.primaryFactRefs, ...item.supportingFactRefs])).size,
|
|
6154
|
+
sourceFileCount: uniqueSorted(members.flatMap((item) => [
|
|
6155
|
+
...item.closure.sourceEvidence.map((evidence2) => evidence2.file),
|
|
6156
|
+
...item.closure.codeRefs.flatMap((codeRef) => typeof codeRef.file === "string" ? [codeRef.file] : [])
|
|
6157
|
+
])).length,
|
|
6158
|
+
traceNodeCount: uniqueBy(members.flatMap((item) => item.closure.codeRefs), codeRefKey).length,
|
|
6159
|
+
estimatedFactTokens: members.reduce((total, item) => total + item.estimate.estimatedFactTokens, 0)
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
function withinExecutionBudget(candidate, behaviors, context) {
|
|
6163
|
+
const cost = executionCost(candidate, behaviors, context);
|
|
6164
|
+
return cost.behaviorCount <= DEFAULT_EXECUTION_BUDGET.maxBehaviorCount && cost.observableOutcomeCount <= DEFAULT_EXECUTION_BUDGET.maxObservableOutcomeCount && cost.productionFileCount <= DEFAULT_EXECUTION_BUDGET.maxProductionFileCount && cost.testFileCount <= DEFAULT_EXECUTION_BUDGET.maxTestFileCount && cost.capabilityCount <= DEFAULT_EXECUTION_BUDGET.maxCapabilityCount && cost.factCount <= DEFAULT_EXECUTION_BUDGET.maxFactCount && cost.sourceFileCount <= DEFAULT_EXECUTION_BUDGET.maxSourceFileCount && cost.traceNodeCount <= DEFAULT_EXECUTION_BUDGET.maxTraceNodeCount && cost.estimatedFactTokens <= DEFAULT_EXECUTION_BUDGET.maxEstimatedFactTokens;
|
|
6165
|
+
}
|
|
6166
|
+
function shouldMerge(left, right, behaviors, facts, context) {
|
|
6167
|
+
if (left.type !== "journey" || right.type !== "journey" || left.canonicalPageRef !== right.canonicalPageRef) return false;
|
|
6168
|
+
const members = [...behaviorRefs(left, behaviors), ...behaviorRefs(right, behaviors)];
|
|
6169
|
+
if (members.some((item) => item.closure.requiresSourceReview || item.closure.unresolvedRefs.length > 0)) return false;
|
|
6170
|
+
const combined = combineCandidates2(left, right);
|
|
6171
|
+
if (!withinExecutionBudget(combined, behaviors, context)) return false;
|
|
6172
|
+
const behaviorEntries = (candidate) => uniqueSorted(behaviorRefs(candidate, behaviors).flatMap((item) => item.closure.entryRefs));
|
|
6173
|
+
const targetedEntries = (candidate) => {
|
|
6174
|
+
const effects = new Set(behaviorRefs(candidate, behaviors).flatMap((item) => item.closure.interactionRefs));
|
|
6175
|
+
return uniqueSorted([...effects].flatMap((effectRef) => {
|
|
6176
|
+
const effect = facts.effects.find((item) => item.id === effectRef);
|
|
6177
|
+
return effect?.targetKind === "entry" ? [effect.targetId] : [];
|
|
6178
|
+
}));
|
|
6179
|
+
};
|
|
6180
|
+
const callbackLinked = intersects(targetedEntries(left), behaviorEntries(right)) || intersects(targetedEntries(right), behaviorEntries(left));
|
|
6181
|
+
if (callbackLinked || intersects(left.codeRefKeys, right.codeRefKeys)) return true;
|
|
6182
|
+
const sharedSource = intersects(left.sourceFiles, right.sourceFiles);
|
|
6183
|
+
const sharedTarget = left.targetPageRefs.length > 0 && right.targetPageRefs.length > 0 && intersects(left.targetPageRefs, right.targetPageRefs);
|
|
6184
|
+
const sharedCapability = intersects(left.capabilityRefs, right.capabilityRefs);
|
|
6185
|
+
return sharedSource && (sharedTarget || sharedCapability);
|
|
6186
|
+
}
|
|
6187
|
+
function clusterBehaviors(plan, facts, context) {
|
|
6188
|
+
const behaviors = new Map(plan.responsibilities.map((item) => [item.id, item]));
|
|
6189
|
+
const canonicalBySurface = canonicalPageRefs(facts);
|
|
6190
|
+
const clusters = [];
|
|
6191
|
+
for (const next of plan.responsibilities.map((item) => candidateForBehavior(item, facts, canonicalBySurface))) {
|
|
6192
|
+
const compatible = clusters.findIndex((current) => shouldMerge(current, next, behaviors, facts, context));
|
|
6193
|
+
if (compatible < 0) clusters.push(next);
|
|
6194
|
+
else clusters[compatible] = combineCandidates2(clusters[compatible], next);
|
|
6195
|
+
}
|
|
6196
|
+
return clusters;
|
|
6197
|
+
}
|
|
6198
|
+
function workPackageId(candidate) {
|
|
6199
|
+
if (candidate.type === "resolution") return null;
|
|
6200
|
+
return `package:${sha2562({ kind: "page", page: candidate.canonicalPageRef ?? "unassigned" }).slice(0, 16)}`;
|
|
6201
|
+
}
|
|
6202
|
+
function taskId(candidate) {
|
|
6203
|
+
return `task:${sha2562({ page: candidate.canonicalPageRef, type: candidate.type, behaviorIds: candidate.behaviorIds }).slice(0, 16)}`;
|
|
6204
|
+
}
|
|
6205
|
+
function taskTitle(candidate, members, facts) {
|
|
6206
|
+
if (members.length === 1) return members[0].title;
|
|
6207
|
+
const pageName = facts.surfaces.find((surface) => surface.id === candidate.canonicalPageRef)?.name ?? candidate.canonicalPageRef ?? "\u672A\u5F52\u5C5E\u6A21\u5757";
|
|
6208
|
+
return `${pageName}\uFF1A\u5B8C\u6210 ${members.length} \u4E2A\u76F8\u5173\u884C\u4E3A\u95ED\u73AF`;
|
|
6209
|
+
}
|
|
6210
|
+
function aggregateClosure(members) {
|
|
6211
|
+
return {
|
|
6212
|
+
entryRefs: uniqueSorted(members.flatMap((item) => item.closure.entryRefs)),
|
|
6213
|
+
surfaceRefs: uniqueSorted(members.flatMap((item) => item.closure.surfaceRefs)),
|
|
6214
|
+
controlRefs: uniqueSorted(members.flatMap((item) => item.closure.controlRefs)),
|
|
6215
|
+
interactionRefs: uniqueSorted(members.flatMap((item) => item.closure.interactionRefs)),
|
|
6216
|
+
sourceEvidence: uniqueBy(members.flatMap((item) => item.closure.sourceEvidence), (item) => `${item.file}:${item.symbol ?? ""}:${item.line ?? ""}:${item.endLine ?? ""}`),
|
|
6217
|
+
codeRefs: uniqueBy(members.flatMap((item) => item.closure.codeRefs), codeRefKey),
|
|
6218
|
+
unresolvedRefs: uniqueSorted(members.flatMap((item) => item.closure.unresolvedRefs)),
|
|
6219
|
+
warningRefs: uniqueSorted(members.flatMap((item) => item.closure.warningRefs)),
|
|
6220
|
+
requiresSourceReview: members.some((item) => item.closure.requiresSourceReview)
|
|
6221
|
+
};
|
|
6222
|
+
}
|
|
6223
|
+
function primaryModule(candidate) {
|
|
6224
|
+
if (candidate.type === "bootstrap") return "foundation:application-shell";
|
|
6225
|
+
if (candidate.type === "resolution") return `resolution:${candidate.behaviorIds[0]}`;
|
|
6226
|
+
if (isRoutingAssembly(candidate)) return "assembly:routing";
|
|
6227
|
+
return candidate.canonicalPageRef ? `page:${candidate.canonicalPageRef}` : "page:unassigned";
|
|
6228
|
+
}
|
|
6229
|
+
function initialTasks(clusters, behaviorPlan, facts, canonicalBySurface, context) {
|
|
6230
|
+
const behaviors = new Map(behaviorPlan.responsibilities.map((item) => [item.id, item]));
|
|
6231
|
+
return clusters.map((candidate) => {
|
|
6232
|
+
const members = behaviorRefs(candidate, behaviors);
|
|
6233
|
+
const primaryFactRefs = uniqueSorted(members.flatMap((item2) => item2.primaryFactRefs));
|
|
6234
|
+
const supportingFactRefs = uniqueSorted(members.flatMap((item2) => item2.supportingFactRefs).filter((ref) => !primaryFactRefs.includes(ref)));
|
|
6235
|
+
const mappedFiles = mappedTargetFiles(candidate, [...primaryFactRefs, ...supportingFactRefs], context);
|
|
6236
|
+
let ownedFiles;
|
|
6237
|
+
if (candidate.type === "resolution") ownedFiles = [];
|
|
6238
|
+
else if (candidate.type === "bootstrap") ownedFiles = mappedFiles;
|
|
6239
|
+
else ownedFiles = mappedFiles.filter((file) => isRoutingAssembly(candidate) || !context.globalHotspotFiles.includes(file));
|
|
6240
|
+
const cost = executionCost(candidate, behaviors, context);
|
|
6241
|
+
const outcomes = uniqueSorted(members.map((item2) => item2.acceptance.observableOutcome));
|
|
6242
|
+
const closure = aggregateClosure(members);
|
|
6243
|
+
const pageScopes = uniqueSorted(closure.surfaceRefs.map((surfaceRef) => `page:${canonicalBySurface[surfaceRef] ?? surfaceRef}`));
|
|
6244
|
+
const exclusiveScopes = candidate.type === "resolution" ? [`resolution:${candidate.behaviorIds[0]}`] : uniqueSorted([
|
|
6245
|
+
...candidate.type === "bootstrap" ? ["foundation:application-shell"] : [],
|
|
6246
|
+
...isRoutingAssembly(candidate) ? ["assembly:routing"] : [],
|
|
6247
|
+
...pageScopes,
|
|
6248
|
+
...pageScopes.length === 0 ? [candidate.canonicalPageRef ? `page:${candidate.canonicalPageRef}` : `${candidate.type}:unassigned`] : []
|
|
6249
|
+
]);
|
|
6250
|
+
const item = {
|
|
6251
|
+
id: taskId(candidate),
|
|
6252
|
+
type: candidate.type,
|
|
6253
|
+
title: taskTitle(candidate, members, facts),
|
|
6254
|
+
workPackageId: workPackageId(candidate),
|
|
6255
|
+
canonicalPageRef: candidate.canonicalPageRef,
|
|
6256
|
+
primaryModule: primaryModule(candidate),
|
|
6257
|
+
behaviorIds: candidate.behaviorIds,
|
|
6258
|
+
primaryFactRefs,
|
|
6259
|
+
supportingFactRefs,
|
|
6260
|
+
dependsOnTaskIds: [],
|
|
6261
|
+
conflictsWithTaskIds: [],
|
|
6262
|
+
writeOwnership: {
|
|
6263
|
+
ownedFiles,
|
|
6264
|
+
readOnlyFiles: [],
|
|
6265
|
+
exclusiveScopes,
|
|
6266
|
+
forbiddenGlobalFiles: candidate.type === "bootstrap" ? [] : context.globalHotspotFiles.filter((file) => !ownedFiles.includes(file))
|
|
6267
|
+
},
|
|
6268
|
+
dependencyInterfaces: [],
|
|
6269
|
+
readyGate: {
|
|
6270
|
+
semanticsResolved: candidate.type === "resolution" || closure.unresolvedRefs.length === 0,
|
|
6271
|
+
dependencyInterfacesStable: true,
|
|
6272
|
+
writeOwnershipResolved: ownedFiles.length > 0 || exclusiveScopes.length > 0,
|
|
6273
|
+
contextWithinBudget: members.length === 1 || withinExecutionBudget(candidate, behaviors, context),
|
|
6274
|
+
acceptanceExecutable: members.every((member) => member.acceptance.verificationRequirements.length > 0)
|
|
6275
|
+
},
|
|
6276
|
+
closure,
|
|
6277
|
+
acceptance: {
|
|
6278
|
+
observableOutcome: outcomes.join("\uFF1B"),
|
|
6279
|
+
observableOutcomes: outcomes,
|
|
6280
|
+
requiredFactRefs: uniqueSorted(members.flatMap((item2) => item2.acceptance.requiredFactRefs)),
|
|
6281
|
+
verificationRequirements: uniqueSorted(members.flatMap((item2) => item2.acceptance.verificationRequirements)),
|
|
6282
|
+
reportRefs: uniqueSorted(members.flatMap((item2) => item2.acceptance.reportRefs))
|
|
6283
|
+
},
|
|
6284
|
+
estimate: {
|
|
6285
|
+
factCount: cost.factCount,
|
|
6286
|
+
sourceFileCount: cost.sourceFileCount,
|
|
6287
|
+
traceNodeCount: cost.traceNodeCount,
|
|
6288
|
+
estimatedFactTokens: cost.estimatedFactTokens,
|
|
6289
|
+
downstreamUnlockCount: 0,
|
|
6290
|
+
behaviorCount: cost.behaviorCount,
|
|
6291
|
+
observableOutcomeCount: cost.observableOutcomeCount,
|
|
6292
|
+
productionFileCount: cost.productionFileCount,
|
|
6293
|
+
testFileCount: cost.testFileCount,
|
|
6294
|
+
capabilityCount: cost.capabilityCount
|
|
6295
|
+
}
|
|
6296
|
+
};
|
|
6297
|
+
return item;
|
|
6298
|
+
}).sort((left, right) => typeOrder2[left.type] - typeOrder2[right.type] || left.id.localeCompare(right.id));
|
|
6299
|
+
}
|
|
6300
|
+
function hasPath(tasks, from, to) {
|
|
6301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6302
|
+
const queue = [...tasks.get(from)?.dependsOnTaskIds ?? []];
|
|
6303
|
+
while (queue.length) {
|
|
6304
|
+
const next = queue.shift();
|
|
6305
|
+
if (next === to) return true;
|
|
6306
|
+
if (seen.has(next)) continue;
|
|
6307
|
+
seen.add(next);
|
|
6308
|
+
queue.push(...tasks.get(next)?.dependsOnTaskIds ?? []);
|
|
6309
|
+
}
|
|
6310
|
+
return false;
|
|
6311
|
+
}
|
|
6312
|
+
function addDependency(tasks, taskId2, dependencyId) {
|
|
6313
|
+
if (taskId2 === dependencyId || hasPath(tasks, dependencyId, taskId2)) return;
|
|
6314
|
+
const task = tasks.get(taskId2);
|
|
6315
|
+
task.dependsOnTaskIds = uniqueSorted([...task.dependsOnTaskIds, dependencyId]);
|
|
6316
|
+
}
|
|
6317
|
+
function orderPackageLane(items) {
|
|
6318
|
+
const ids = new Set(items.map((item) => item.id));
|
|
6319
|
+
const remaining = new Set(ids);
|
|
6320
|
+
const result = [];
|
|
6321
|
+
while (remaining.size) {
|
|
6322
|
+
const ready = items.filter((item) => remaining.has(item.id) && item.dependsOnTaskIds.filter((dependency) => ids.has(dependency)).every((dependency) => !remaining.has(dependency))).sort((left, right) => typeOrder2[left.type] - typeOrder2[right.type] || left.id.localeCompare(right.id));
|
|
6323
|
+
const selected = ready[0] ?? items.filter((item) => remaining.has(item.id)).sort((left, right) => left.id.localeCompare(right.id))[0];
|
|
6324
|
+
result.push(selected);
|
|
6325
|
+
remaining.delete(selected.id);
|
|
6326
|
+
}
|
|
6327
|
+
return result;
|
|
6328
|
+
}
|
|
6329
|
+
function packageSerializationRanks(tasks) {
|
|
6330
|
+
const packageIds = uniqueSorted(tasks.flatMap((task) => task.workPackageId ? [task.workPackageId] : []));
|
|
6331
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
6332
|
+
const dependencies = new Map(packageIds.map((packageId) => [packageId, /* @__PURE__ */ new Set()]));
|
|
6333
|
+
for (const task of tasks) {
|
|
6334
|
+
if (!task.workPackageId) continue;
|
|
6335
|
+
for (const dependencyId of task.dependsOnTaskIds) {
|
|
6336
|
+
const dependencyPackageId = taskById.get(dependencyId)?.workPackageId;
|
|
6337
|
+
if (dependencyPackageId && dependencyPackageId !== task.workPackageId) dependencies.get(task.workPackageId).add(dependencyPackageId);
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
const remaining = new Set(packageIds);
|
|
6341
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
6342
|
+
while (remaining.size) {
|
|
6343
|
+
const ready = packageIds.filter((packageId) => remaining.has(packageId) && [...dependencies.get(packageId)].every((dependencyId) => !remaining.has(dependencyId)));
|
|
6344
|
+
const selected = ready[0] ?? packageIds.find((packageId) => remaining.has(packageId));
|
|
6345
|
+
ranks.set(selected, ranks.size);
|
|
6346
|
+
remaining.delete(selected);
|
|
6347
|
+
}
|
|
6348
|
+
return ranks;
|
|
6349
|
+
}
|
|
6350
|
+
function conflictReasons(left, right) {
|
|
6351
|
+
const ownedOverlap = left.writeOwnership.ownedFiles.filter((file) => right.writeOwnership.ownedFiles.includes(file)).map((file) => `owned-file:${file}`);
|
|
6352
|
+
const scopeOverlap = left.writeOwnership.exclusiveScopes.filter((scope) => right.writeOwnership.exclusiveScopes.includes(scope)).map((scope) => `exclusive-scope:${scope}`);
|
|
6353
|
+
return uniqueSorted([...ownedOverlap, ...scopeOverlap]);
|
|
6354
|
+
}
|
|
6355
|
+
function wireDependenciesAndConflicts(tasks, behaviorPlan) {
|
|
6356
|
+
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
6357
|
+
const taskByBehavior = new Map(tasks.flatMap((item) => item.behaviorIds.map((behaviorId) => [behaviorId, item.id])));
|
|
6358
|
+
const behaviorById = new Map(behaviorPlan.responsibilities.map((item) => [item.id, item]));
|
|
6359
|
+
for (const task of tasks) {
|
|
6360
|
+
const dependencies = task.behaviorIds.flatMap((behaviorId) => behaviorById.get(behaviorId)?.dependsOnBehaviorIds ?? []).map((behaviorId) => taskByBehavior.get(behaviorId)).filter((id) => Boolean(id) && id !== task.id);
|
|
6361
|
+
task.dependsOnTaskIds = uniqueSorted(dependencies);
|
|
6362
|
+
}
|
|
6363
|
+
const packages = /* @__PURE__ */ new Map();
|
|
6364
|
+
for (const task of tasks) {
|
|
6365
|
+
if (task.workPackageId === null) continue;
|
|
6366
|
+
packages.set(task.workPackageId, [...packages.get(task.workPackageId) ?? [], task]);
|
|
6367
|
+
}
|
|
6368
|
+
for (const items of packages.values()) {
|
|
6369
|
+
const lane = orderPackageLane(items);
|
|
6370
|
+
for (let index = 1; index < lane.length; index++) addDependency(byId, lane[index].id, lane[index - 1].id);
|
|
6371
|
+
}
|
|
6372
|
+
const packageRanks = packageSerializationRanks(tasks);
|
|
6373
|
+
const edges = [];
|
|
6374
|
+
for (let leftIndex = 0; leftIndex < tasks.length; leftIndex++) {
|
|
6375
|
+
for (let rightIndex = leftIndex + 1; rightIndex < tasks.length; rightIndex++) {
|
|
6376
|
+
const left = tasks[leftIndex];
|
|
6377
|
+
const right = tasks[rightIndex];
|
|
6378
|
+
const reasons = conflictReasons(left, right);
|
|
6379
|
+
if (!reasons.length) continue;
|
|
6380
|
+
edges.push({ leftTaskId: left.id, rightTaskId: right.id, reasons });
|
|
6381
|
+
left.conflictsWithTaskIds = uniqueSorted([...left.conflictsWithTaskIds, right.id]);
|
|
6382
|
+
right.conflictsWithTaskIds = uniqueSorted([...right.conflictsWithTaskIds, left.id]);
|
|
6383
|
+
if (hasPath(byId, left.id, right.id) || hasPath(byId, right.id, left.id)) continue;
|
|
6384
|
+
const leftPackageRank = left.workPackageId ? packageRanks.get(left.workPackageId) : void 0;
|
|
6385
|
+
const rightPackageRank = right.workPackageId ? packageRanks.get(right.workPackageId) : void 0;
|
|
6386
|
+
if (leftPackageRank !== void 0 && rightPackageRank !== void 0 && leftPackageRank !== rightPackageRank) {
|
|
6387
|
+
const [later, earlier] = leftPackageRank > rightPackageRank ? [left, right] : [right, left];
|
|
6388
|
+
addDependency(byId, later.id, earlier.id);
|
|
6389
|
+
} else {
|
|
6390
|
+
addDependency(byId, right.id, left.id);
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
for (const task of tasks) {
|
|
6395
|
+
const providers = task.dependsOnTaskIds.map((id) => byId.get(id)).filter((item) => Boolean(item));
|
|
6396
|
+
const businessProviderIds = new Set(task.behaviorIds.flatMap((behaviorId) => behaviorById.get(behaviorId)?.dependsOnBehaviorIds ?? []).map((behaviorId) => taskByBehavior.get(behaviorId)).filter((id) => Boolean(id)));
|
|
6397
|
+
task.writeOwnership.readOnlyFiles = uniqueSorted(providers.flatMap((provider) => provider.writeOwnership.ownedFiles).filter((file) => !task.writeOwnership.ownedFiles.includes(file)));
|
|
6398
|
+
task.dependencyInterfaces = providers.map((provider) => {
|
|
6399
|
+
const kind = businessProviderIds.has(provider.id) ? "module" : "ordering";
|
|
6400
|
+
return {
|
|
6401
|
+
kind,
|
|
6402
|
+
ref: kind === "module" ? `interface:${provider.primaryModule}` : `ordering:${provider.id}`,
|
|
6403
|
+
providerTaskId: provider.id,
|
|
6404
|
+
files: provider.writeOwnership.ownedFiles.filter((file) => !task.writeOwnership.ownedFiles.includes(file)),
|
|
6405
|
+
errorModes: kind === "module" ? ["provider task is not locally verified", "provider Interface changed after this execution plan was generated"] : ["serialized predecessor is not locally verified", "write ownership changed after this execution plan was generated"],
|
|
6406
|
+
orderingConstraints: [`${provider.id} must be locally verified before ${task.id}`]
|
|
6407
|
+
};
|
|
6408
|
+
});
|
|
6409
|
+
}
|
|
6410
|
+
return edges.sort((left, right) => `${left.leftTaskId}:${left.rightTaskId}`.localeCompare(`${right.leftTaskId}:${right.rightTaskId}`));
|
|
6411
|
+
}
|
|
6412
|
+
function downstreamCounts2(tasks) {
|
|
6413
|
+
const children = /* @__PURE__ */ new Map();
|
|
6414
|
+
for (const task of tasks) for (const dependency of task.dependsOnTaskIds) children.set(dependency, [...children.get(dependency) ?? [], task.id]);
|
|
6415
|
+
const result = /* @__PURE__ */ new Map();
|
|
6416
|
+
for (const task of tasks) {
|
|
6417
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6418
|
+
const queue = [...children.get(task.id) ?? []];
|
|
6419
|
+
while (queue.length) {
|
|
6420
|
+
const next = queue.shift();
|
|
6421
|
+
if (seen.has(next)) continue;
|
|
6422
|
+
seen.add(next);
|
|
6423
|
+
queue.push(...children.get(next) ?? []);
|
|
6424
|
+
}
|
|
6425
|
+
result.set(task.id, seen.size);
|
|
6426
|
+
}
|
|
6427
|
+
return result;
|
|
6428
|
+
}
|
|
6429
|
+
function pageWorkPackages(tasks, facts, canonicalBySurface) {
|
|
6430
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6431
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
6432
|
+
for (const task of tasks) {
|
|
6433
|
+
if (task.workPackageId === null) continue;
|
|
6434
|
+
grouped.set(task.workPackageId, [...grouped.get(task.workPackageId) ?? [], task]);
|
|
6435
|
+
}
|
|
6436
|
+
return [...grouped.entries()].map(([id, items]) => {
|
|
6437
|
+
const canonicalPageRef = items[0]?.canonicalPageRef ?? null;
|
|
6438
|
+
const title = facts.surfaces.find((surface) => surface.id === canonicalPageRef)?.name ?? canonicalPageRef ?? "Unassigned";
|
|
6439
|
+
const ordered = orderPackageLane(items);
|
|
6440
|
+
return {
|
|
6441
|
+
id,
|
|
6442
|
+
canonicalPageRef,
|
|
6443
|
+
title: `${title} Page Work Package`,
|
|
6444
|
+
surfaceRefs: uniqueSorted(facts.surfaces.filter((surface) => (canonicalBySurface[surface.id] ?? surface.id) === canonicalPageRef).map((surface) => surface.id)),
|
|
6445
|
+
workItemIds: ordered.map((item) => item.id),
|
|
6446
|
+
dependsOnPackageIds: uniqueSorted(items.flatMap((item) => item.dependsOnTaskIds).map((taskId2) => taskById.get(taskId2)?.workPackageId).filter((packageId) => Boolean(packageId && packageId !== id))),
|
|
6447
|
+
serialLane: true,
|
|
6448
|
+
exclusiveScopes: uniqueSorted(items.flatMap((item) => item.writeOwnership.exclusiveScopes))
|
|
6449
|
+
};
|
|
6450
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
6451
|
+
}
|
|
6452
|
+
function compileExecutionPlan(behaviorPlan, facts, context = emptyExecutionPlanningContext) {
|
|
6453
|
+
const planningPhase = behaviorPlan.responsibilities.some((behavior) => behavior.type === "resolution") ? "resolution" : "execution";
|
|
6454
|
+
const activeBehaviorPlan = planningPhase === "resolution" ? { ...behaviorPlan, responsibilities: behaviorPlan.responsibilities.filter((behavior) => behavior.type === "resolution") } : behaviorPlan;
|
|
6455
|
+
const canonicalBySurface = canonicalPageRefs(facts);
|
|
6456
|
+
const clusters = planningPhase === "resolution" ? activeBehaviorPlan.responsibilities.map((behavior) => candidateForBehavior(behavior, facts, canonicalBySurface)) : clusterBehaviors(activeBehaviorPlan, facts, context);
|
|
6457
|
+
const tasks = initialTasks(clusters, activeBehaviorPlan, facts, canonicalBySurface, context);
|
|
6458
|
+
const fileConflictEdges = wireDependenciesAndConflicts(tasks, activeBehaviorPlan);
|
|
6459
|
+
const downstream = downstreamCounts2(tasks);
|
|
6460
|
+
for (const task of tasks) task.estimate.downstreamUnlockCount = downstream.get(task.id) ?? 0;
|
|
6461
|
+
const taskByBehavior = new Map(tasks.flatMap((task) => task.behaviorIds.map((behaviorId) => [behaviorId, task.id])));
|
|
6462
|
+
const primaryOwnerByFactRef = Object.fromEntries(Object.entries(behaviorPlan.ownership.primaryOwnerByFactRef).flatMap(([ref, behaviorId]) => {
|
|
6463
|
+
const taskId2 = taskByBehavior.get(behaviorId);
|
|
6464
|
+
return taskId2 ? [[ref, taskId2]] : [];
|
|
6465
|
+
}).sort(([left], [right]) => left.localeCompare(right)));
|
|
6466
|
+
const supportingTaskIdsByFactRef = {};
|
|
6467
|
+
for (const task of tasks) for (const ref of task.supportingFactRefs) supportingTaskIdsByFactRef[ref] = uniqueSorted([...supportingTaskIdsByFactRef[ref] ?? [], task.id]);
|
|
6468
|
+
return {
|
|
6469
|
+
taskPlanVersion: TASK_PLAN_VERSION,
|
|
6470
|
+
executionPlanVersion: EXECUTION_PLAN_VERSION,
|
|
6471
|
+
planningModelVersion: behaviorPlan.planningModelVersion,
|
|
6472
|
+
factsAdapterVersion: behaviorPlan.factsAdapterVersion,
|
|
6473
|
+
plannerPolicyVersion: behaviorPlan.plannerPolicyVersion,
|
|
6474
|
+
baseline: behaviorPlan.baseline,
|
|
6475
|
+
planningPhase,
|
|
6476
|
+
entryResolution: behaviorPlan.entryResolution,
|
|
6477
|
+
behaviorResponsibilities: behaviorPlan.responsibilities,
|
|
6478
|
+
pageWorkPackages: pageWorkPackages(tasks, facts, canonicalBySurface),
|
|
6479
|
+
tasks,
|
|
6480
|
+
ownership: {
|
|
6481
|
+
primaryOwnerByFactRef,
|
|
6482
|
+
supportingTaskIdsByFactRef: Object.fromEntries(Object.entries(supportingTaskIdsByFactRef).sort(([left], [right]) => left.localeCompare(right)))
|
|
6483
|
+
},
|
|
6484
|
+
behaviorOwnership: behaviorPlan.ownership,
|
|
6485
|
+
fileConflictEdges,
|
|
6486
|
+
planningWarnings: behaviorPlan.planningWarnings
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
function compileFromPlanningFacts(facts, androidFingerprint, context = emptyExecutionPlanningContext) {
|
|
6490
|
+
return compileExecutionPlan(compileBehaviorPlanFromPlanningFacts(facts, androidFingerprint), facts, context);
|
|
6491
|
+
}
|
|
6492
|
+
|
|
6493
|
+
// src/planning/storage.ts
|
|
6494
|
+
import fs27 from "fs";
|
|
6130
6495
|
import path33 from "path";
|
|
6131
6496
|
function taskPlanRoot(projectRoot) {
|
|
6132
6497
|
return path33.join(migrationDirectory(projectRoot), "tasks");
|
|
6133
6498
|
}
|
|
6134
|
-
function taskArtifactStem(
|
|
6135
|
-
return
|
|
6499
|
+
function taskArtifactStem(taskId2) {
|
|
6500
|
+
return taskId2.replace(/^task:/, "");
|
|
6501
|
+
}
|
|
6502
|
+
function workPackageArtifactStem(workPackageId2) {
|
|
6503
|
+
return workPackageId2.replace(/^package:/, "");
|
|
6504
|
+
}
|
|
6505
|
+
function codeRefIndex(value) {
|
|
6506
|
+
const allowed = ["id", "file", "name", "symbol", "kind", "type", "capabilityRef", "line", "startLine", "endLine"];
|
|
6507
|
+
return Object.fromEntries(allowed.filter((key) => value[key] !== void 0).map((key) => [key, value[key]]));
|
|
6508
|
+
}
|
|
6509
|
+
function taskContractView(task) {
|
|
6510
|
+
return { ...task, closure: { ...task.closure, codeRefs: task.closure.codeRefs.map(codeRefIndex) } };
|
|
6136
6511
|
}
|
|
6137
|
-
function
|
|
6512
|
+
function coreFactsForTask(task, facts) {
|
|
6513
|
+
return coreFactsForRefs([...task.primaryFactRefs, ...task.supportingFactRefs], task.closure.unresolvedRefs, facts);
|
|
6514
|
+
}
|
|
6515
|
+
function coreFactsForRefs(refsInput, unresolvedRefs, facts) {
|
|
6516
|
+
const refs = new Set(refsInput);
|
|
6517
|
+
return [
|
|
6518
|
+
...facts.surfaces.filter((item) => refs.has(item.id)).map((value) => ({ kind: "surface", value })),
|
|
6519
|
+
...facts.controls.filter((item) => refs.has(item.id)).map((value) => ({ kind: "control", value })),
|
|
6520
|
+
...facts.entries.filter((item) => refs.has(item.id)).map((value) => ({
|
|
6521
|
+
kind: "entry",
|
|
6522
|
+
value: { ...value, trace: { ...value.trace, codeRefs: value.trace.codeRefs.map(codeRefIndex) } }
|
|
6523
|
+
})),
|
|
6524
|
+
...facts.effects.filter((item) => refs.has(item.id)).map((value) => ({ kind: "effect", value })),
|
|
6525
|
+
...facts.issues.filter((item) => refs.has(item.id) || unresolvedRefs.includes(item.id)).map((value) => ({ kind: "issue", value }))
|
|
6526
|
+
];
|
|
6527
|
+
}
|
|
6528
|
+
function sourceContextForTask(task) {
|
|
6529
|
+
return uniqueBy([
|
|
6530
|
+
...task.closure.sourceEvidence.map((value) => ({ kind: "source-evidence", value })),
|
|
6531
|
+
...task.closure.codeRefs.map((value) => ({ kind: "code-ref", value: codeRefIndex(value) }))
|
|
6532
|
+
], stableJson);
|
|
6533
|
+
}
|
|
6534
|
+
function tasksForPackage(workPackage, plan) {
|
|
6535
|
+
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
6536
|
+
return workPackage.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
6537
|
+
}
|
|
6538
|
+
function effectOutcome2(effectRef, facts) {
|
|
6539
|
+
const effect = facts.effects.find((item) => item.id === effectRef);
|
|
6540
|
+
if (!effect) return effectRef;
|
|
6541
|
+
const target = effect.targetKind === "surface" ? facts.surfaces.find((surface) => surface.id === effect.targetId)?.name : effect.targetKind === "entry" ? facts.entries.find((entry) => entry.id === effect.targetId)?.label : null;
|
|
6542
|
+
const label = target ?? effect.targetId;
|
|
6543
|
+
if (effect.kind === "opens") return `\u6253\u5F00 ${label}`;
|
|
6544
|
+
if (effect.kind === "navigates-to") return `\u8FDB\u5165 ${label}`;
|
|
6545
|
+
if (effect.kind === "launches-for-result") return `\u542F\u52A8 ${label} \u5E76\u5904\u7406\u8FD4\u56DE\u7ED3\u679C`;
|
|
6546
|
+
return `\u5904\u7406 ${label}`;
|
|
6547
|
+
}
|
|
6548
|
+
function pageWorkPackageContractView(workPackage, plan, facts) {
|
|
6549
|
+
const tasks = tasksForPackage(workPackage, plan);
|
|
6550
|
+
const taskByBehavior = new Map(tasks.flatMap((task) => task.behaviorIds.map((behaviorId) => [behaviorId, task])));
|
|
6551
|
+
const behaviorById = new Map(plan.behaviorResponsibilities.map((behavior) => [behavior.id, behavior]));
|
|
6552
|
+
const orderedBehaviors = tasks.flatMap((task) => task.behaviorIds.map((id) => behaviorById.get(id))).filter((item) => Boolean(item));
|
|
6553
|
+
const ownedFiles = uniqueSorted(tasks.flatMap((task) => task.writeOwnership.ownedFiles));
|
|
6554
|
+
const packageByTask = new Map(plan.tasks.flatMap((task) => task.workPackageId ? [[task.id, task.workPackageId]] : []));
|
|
6555
|
+
const dependencyGroups = /* @__PURE__ */ new Map();
|
|
6556
|
+
for (const task of tasks) for (const dependency of task.dependencyInterfaces) {
|
|
6557
|
+
const providerTaskId = dependency.providerTaskId;
|
|
6558
|
+
const providerPackageId = providerTaskId ? packageByTask.get(providerTaskId) : null;
|
|
6559
|
+
if (!providerTaskId || !providerPackageId || providerPackageId === workPackage.id) continue;
|
|
6560
|
+
const key = `${dependency.kind}:${dependency.ref}:${providerPackageId}`;
|
|
6561
|
+
const current = dependencyGroups.get(key) ?? {
|
|
6562
|
+
kind: dependency.kind,
|
|
6563
|
+
ref: dependency.ref,
|
|
6564
|
+
providerPackageId,
|
|
6565
|
+
providerWorkItemIds: [],
|
|
6566
|
+
files: [],
|
|
6567
|
+
errorModes: [],
|
|
6568
|
+
orderingConstraints: []
|
|
6569
|
+
};
|
|
6570
|
+
current.providerWorkItemIds = uniqueSorted([...current.providerWorkItemIds, providerTaskId]);
|
|
6571
|
+
current.files = uniqueSorted([...current.files, ...dependency.files]);
|
|
6572
|
+
current.errorModes = uniqueSorted([...current.errorModes, ...dependency.errorModes]);
|
|
6573
|
+
current.orderingConstraints = uniqueSorted([...current.orderingConstraints, ...dependency.orderingConstraints]);
|
|
6574
|
+
dependencyGroups.set(key, current);
|
|
6575
|
+
}
|
|
6576
|
+
const sourceFiles = uniqueSorted(tasks.flatMap((task) => [
|
|
6577
|
+
...task.closure.sourceEvidence.map((item) => item.file),
|
|
6578
|
+
...task.closure.codeRefs.flatMap((item) => typeof item.file === "string" ? [item.file] : [])
|
|
6579
|
+
]));
|
|
6580
|
+
return {
|
|
6581
|
+
...workPackage,
|
|
6582
|
+
behaviorIds: orderedBehaviors.map((behavior) => behavior.id),
|
|
6583
|
+
workItems: tasks.map((task, index) => ({
|
|
6584
|
+
ordinal: index + 1,
|
|
6585
|
+
id: task.id,
|
|
6586
|
+
type: task.type,
|
|
6587
|
+
title: task.title,
|
|
6588
|
+
behaviorIds: task.behaviorIds
|
|
6589
|
+
})),
|
|
6590
|
+
features: orderedBehaviors.map((behavior, index) => ({
|
|
6591
|
+
ordinal: index + 1,
|
|
6592
|
+
behaviorId: behavior.id,
|
|
6593
|
+
workItemId: taskByBehavior.get(behavior.id).id,
|
|
6594
|
+
type: behavior.type,
|
|
6595
|
+
title: behavior.title,
|
|
6596
|
+
triggerLabels: uniqueSorted(facts.entries.filter((entry) => behavior.closure.entryRefs.includes(entry.id)).map((entry) => entry.label)),
|
|
6597
|
+
controlNames: uniqueSorted(facts.controls.filter((control) => behavior.closure.controlRefs.includes(control.id)).map((control) => control.name)),
|
|
6598
|
+
interactionOutcomes: uniqueSorted(behavior.closure.interactionRefs.map((ref) => effectOutcome2(ref, facts))),
|
|
6599
|
+
entryRefs: behavior.closure.entryRefs,
|
|
6600
|
+
surfaceRefs: behavior.closure.surfaceRefs,
|
|
6601
|
+
controlRefs: behavior.closure.controlRefs,
|
|
6602
|
+
interactionRefs: behavior.closure.interactionRefs,
|
|
6603
|
+
unresolvedRefs: behavior.closure.unresolvedRefs,
|
|
6604
|
+
primaryFactRefs: behavior.primaryFactRefs,
|
|
6605
|
+
supportingFactRefs: behavior.supportingFactRefs,
|
|
6606
|
+
requiresSourceReview: behavior.closure.requiresSourceReview,
|
|
6607
|
+
observableOutcome: behavior.acceptance.observableOutcome,
|
|
6608
|
+
requiredFactRefs: behavior.acceptance.requiredFactRefs,
|
|
6609
|
+
verificationRequirements: behavior.acceptance.verificationRequirements,
|
|
6610
|
+
reportRefs: behavior.acceptance.reportRefs
|
|
6611
|
+
})),
|
|
6612
|
+
writeOwnership: {
|
|
6613
|
+
ownedFiles,
|
|
6614
|
+
readOnlyFiles: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.readOnlyFiles).filter((file) => !ownedFiles.includes(file))),
|
|
6615
|
+
exclusiveScopes: workPackage.exclusiveScopes,
|
|
6616
|
+
forbiddenGlobalFiles: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.forbiddenGlobalFiles).filter((file) => !ownedFiles.includes(file)))
|
|
6617
|
+
},
|
|
6618
|
+
dependencyInterfaces: [...dependencyGroups.values()].sort((left, right) => `${left.providerPackageId}:${left.ref}`.localeCompare(`${right.providerPackageId}:${right.ref}`)),
|
|
6619
|
+
acceptance: {
|
|
6620
|
+
observableOutcomes: uniqueSorted(orderedBehaviors.map((behavior) => behavior.acceptance.observableOutcome)),
|
|
6621
|
+
requiredFactRefs: uniqueSorted(orderedBehaviors.flatMap((behavior) => behavior.acceptance.requiredFactRefs)),
|
|
6622
|
+
verificationRequirements: uniqueSorted(orderedBehaviors.flatMap((behavior) => behavior.acceptance.verificationRequirements)),
|
|
6623
|
+
reportRefs: uniqueSorted(orderedBehaviors.flatMap((behavior) => behavior.acceptance.reportRefs))
|
|
6624
|
+
},
|
|
6625
|
+
estimate: {
|
|
6626
|
+
behaviorCount: orderedBehaviors.length,
|
|
6627
|
+
workItemCount: tasks.length,
|
|
6628
|
+
factCount: uniqueSorted(tasks.flatMap((task) => [...task.primaryFactRefs, ...task.supportingFactRefs])).length,
|
|
6629
|
+
sourceFileCount: sourceFiles.length,
|
|
6630
|
+
traceNodeCount: uniqueBy(tasks.flatMap((task) => task.closure.codeRefs), stableJson).length,
|
|
6631
|
+
estimatedFactTokens: tasks.reduce((total, task) => total + task.estimate.estimatedFactTokens, 0),
|
|
6632
|
+
productionFileCount: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.ownedFiles).filter((file) => !/(^|\/)(test|ohosTest)(\/|$)|\.(test|spec)\./i.test(file))).length,
|
|
6633
|
+
testFileCount: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.ownedFiles).filter((file) => /(^|\/)(test|ohosTest)(\/|$)|\.(test|spec)\./i.test(file))).length,
|
|
6634
|
+
capabilityCount: tasks.reduce((total, task) => total + task.estimate.capabilityCount, 0)
|
|
6635
|
+
}
|
|
6636
|
+
};
|
|
6637
|
+
}
|
|
6638
|
+
function coreFactsForPageWorkPackage(workPackage, facts) {
|
|
6639
|
+
const tasksRefs = workPackage.features.flatMap((feature) => [...feature.primaryFactRefs, ...feature.supportingFactRefs]);
|
|
6640
|
+
return coreFactsForRefs(tasksRefs, workPackage.features.flatMap((feature) => feature.unresolvedRefs), facts);
|
|
6641
|
+
}
|
|
6642
|
+
function sourceContextForPageWorkPackage(workPackage, plan) {
|
|
6643
|
+
return uniqueBy(tasksForPackage(workPackage, plan).flatMap(sourceContextForTask), stableJson);
|
|
6644
|
+
}
|
|
6645
|
+
function renderResolutionTaskBrief(task, plan) {
|
|
6646
|
+
if (task.type !== "resolution") throw new Error("Resolution brief requires a Resolution Task");
|
|
6138
6647
|
const list = (values) => values.length ? values.map((value) => `- \`${value}\``).join("\n") : "- \u65E0";
|
|
6139
6648
|
const sourceLocations = [.../* @__PURE__ */ new Set([
|
|
6140
6649
|
...task.closure.sourceEvidence.map((item) => `${item.file}${item.line ? `:${item.line}${(() => {
|
|
@@ -6152,15 +6661,54 @@ function renderTaskBrief(task) {
|
|
|
6152
6661
|
return "";
|
|
6153
6662
|
})()}`] : [])
|
|
6154
6663
|
])].sort();
|
|
6155
|
-
|
|
6664
|
+
const sourcePreview = sourceLocations.slice(0, 20);
|
|
6665
|
+
const hiddenSourceCount = sourceLocations.length - sourcePreview.length;
|
|
6666
|
+
const taskKind = "Resolution Task";
|
|
6667
|
+
const identity = [
|
|
6668
|
+
`- ${taskKind} ID\uFF1A\`${task.id}\``,
|
|
6669
|
+
`- Execution Plan Hash\uFF1A\`${sha2562(plan)}\``,
|
|
6670
|
+
`- Facts SHA256\uFF1A\`${plan.baseline.factsSha256}\``,
|
|
6671
|
+
`- Android Fingerprint\uFF1A${plan.baseline.androidFingerprint ? `\`${plan.baseline.androidFingerprint}\`` : "\u672A\u63D0\u4F9B"}`,
|
|
6672
|
+
`- Behavior IDs\uFF1A${task.behaviorIds.map((id) => `\`${id}\``).join("\u3001")}`,
|
|
6673
|
+
`- Primary Page / Module\uFF1A\`${task.canonicalPageRef ?? "unassigned"}\` / \`${task.primaryModule}\``
|
|
6674
|
+
].join("\n");
|
|
6675
|
+
const ownership = (values, fallback) => values.length ? values.map((value) => `- \`${value}\``).join("\n") : `- ${fallback}`;
|
|
6676
|
+
return `# ${taskKind}\uFF1A${task.title}
|
|
6156
6677
|
|
|
6157
|
-
## \u76EE\u6807
|
|
6678
|
+
## 1. \u6700\u7EC8\u76EE\u6807
|
|
6158
6679
|
|
|
6159
6680
|
${task.acceptance.observableOutcome}
|
|
6160
6681
|
|
|
6161
|
-
## \
|
|
6682
|
+
## 2. \u8EAB\u4EFD\u4E0E\u57FA\u7EBF
|
|
6683
|
+
|
|
6684
|
+
${identity}
|
|
6685
|
+
|
|
6686
|
+
## 3. \u5B8C\u6210\u5B9A\u4E49
|
|
6687
|
+
|
|
6688
|
+
${task.acceptance.observableOutcomes.map((outcome) => `- ${outcome}`).join("\n")}
|
|
6689
|
+
|
|
6690
|
+
## 4. \u5199\u5165\u6240\u6709\u6743
|
|
6691
|
+
|
|
6692
|
+
### \u5141\u8BB8\u4FEE\u6539
|
|
6693
|
+
|
|
6694
|
+
${ownership(task.writeOwnership.ownedFiles, `\u65E0\uFF1B\`${task.writeOwnership.exclusiveScopes.join("\u3001")}\` \u662F\u88C1\u51B3\u4E92\u65A5\u8303\u56F4\uFF0C\u4E0D\u6388\u4E88 HarmonyOS \u6587\u4EF6\u5199\u5165\u6743`)}
|
|
6695
|
+
|
|
6696
|
+
### \u53EA\u8BFB\u4F9D\u8D56
|
|
6697
|
+
|
|
6698
|
+
${ownership(task.writeOwnership.readOnlyFiles, "\u65E0")}
|
|
6699
|
+
|
|
6700
|
+
### \u7981\u6B62\u4FEE\u6539
|
|
6701
|
+
|
|
6702
|
+
${ownership(task.writeOwnership.forbiddenGlobalFiles, "\u65E0")}
|
|
6703
|
+
|
|
6704
|
+
## 5. \u524D\u7F6E\u4F9D\u8D56\u4E0E Interface
|
|
6705
|
+
|
|
6706
|
+
${task.dependencyInterfaces.length ? task.dependencyInterfaces.map((item) => `- ${item.kind === "module" ? "Module Interface" : "\u8C03\u5EA6\u987A\u5E8F"} \`${item.ref}\`\uFF0C\u63D0\u4F9B\u8005\uFF1A${item.providerTaskId ? `\`${item.providerTaskId}\`` : "\u5916\u90E8\u7A33\u5B9A Interface"}\uFF1B\u987A\u5E8F\uFF1A${item.orderingConstraints.join("\uFF1B") || "\u65E0"}\uFF1B\u9519\u8BEF\u6A21\u5F0F\uFF1A${item.errorModes.join("\uFF1B") || "\u65E0"}`).join("\n") : "- \u65E0"}
|
|
6707
|
+
|
|
6708
|
+
## 6. \u884C\u4E3A\u4E0E\u72B6\u6001\u673A
|
|
6162
6709
|
|
|
6163
6710
|
- Task \u7C7B\u578B\uFF1A\`${task.type}\`
|
|
6711
|
+
- Work Package\uFF1A${task.workPackageId ? `\`${task.workPackageId}\`` : "\u4E0D\u9002\u7528\uFF08Resolution Task \u4E0D\u53C2\u4E0E Coding Work Package\uFF09"}
|
|
6164
6712
|
- \u5165\u53E3\u6570\uFF1A${task.closure.entryRefs.length}
|
|
6165
6713
|
- \u9875\u9762\u6570\uFF1A${task.closure.surfaceRefs.length}
|
|
6166
6714
|
- \u63A7\u4EF6\u6570\uFF1A${task.closure.controlRefs.length}
|
|
@@ -6172,171 +6720,388 @@ ${task.acceptance.observableOutcome}
|
|
|
6172
6720
|
- \u4EA4\u4E92\uFF1A${task.closure.interactionRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6173
6721
|
- \u9700\u8981\u6E90\u7801\u590D\u6838\uFF1A${task.closure.requiresSourceReview ? "\u662F" : "\u5426"}
|
|
6174
6722
|
|
|
6175
|
-
## \
|
|
6723
|
+
## 7. Android \u8BC1\u636E
|
|
6724
|
+
|
|
6725
|
+
${sourcePreview.length ? `${sourcePreview.map((value) => `- ${value}`).join("\n")}${hiddenSourceCount > 0 ? `
|
|
6726
|
+
- \u5176\u4F59 ${hiddenSourceCount} \u4E2A\u4F4D\u7F6E\u89C1 source-context.json \u6216 task show \u5206\u9875` : ""}` : "- \u65E0\u76F4\u63A5\u6E90\u7801\u4F4D\u7F6E\uFF0C\u8BF7\u4ECE coreFacts \u548C unresolvedRefs \u5F00\u59CB\u88C1\u51B3"}
|
|
6176
6727
|
|
|
6177
|
-
|
|
6728
|
+
## 8. Facts \u95ED\u5305
|
|
6178
6729
|
|
|
6179
|
-
|
|
6730
|
+
### \u5F85\u88C1\u51B3\u95EE\u9898
|
|
6180
6731
|
|
|
6181
6732
|
${list(task.closure.unresolvedRefs)}
|
|
6182
6733
|
|
|
6183
|
-
|
|
6734
|
+
### \u4E3B\u6240\u6709\u6743 Facts
|
|
6184
6735
|
|
|
6185
6736
|
${list(task.primaryFactRefs)}
|
|
6186
6737
|
|
|
6187
|
-
|
|
6738
|
+
### Supporting Facts
|
|
6188
6739
|
|
|
6189
6740
|
${list(task.supportingFactRefs)}
|
|
6190
6741
|
|
|
6191
|
-
## \
|
|
6742
|
+
## 9. \u5B9E\u73B0\u7EA6\u675F
|
|
6743
|
+
|
|
6744
|
+
- \u672C Resolution Task \u53EA\u88C1\u51B3 Planning Facts\uFF1B\u4E0D\u5F97\u4FEE\u6539 HarmonyOS \u4E1A\u52A1\u5B9E\u73B0\u3002
|
|
6745
|
+
- \u5FC5\u987B\u628A\u7ED3\u8BBA\u5199\u5165\u53D7\u7BA1 fact correction \u6216 unresolved review\uFF1BReport discovery \u4E0D\u80FD\u5355\u72EC\u89E3\u9664\u89C4\u5212\u963B\u585E\u3002
|
|
6746
|
+
- \u89C4\u5212\u8F93\u5165\u53D8\u5316\u540E\u5FC5\u987B\u91CD\u65B0\u751F\u6210 Behavior Plan \u4E0E Resolution Plan\uFF1B\u53EA\u6709 Resolution \u6E05\u96F6\u540E\u624D\u7EC4\u88C5 Coding Work Item\u3002
|
|
6747
|
+
- Ready Gate\uFF1A\u8BED\u4E49=${task.readyGate.semanticsResolved ? "\u901A\u8FC7" : "\u672A\u901A\u8FC7"}\uFF0C\u4F9D\u8D56 Interface=${task.readyGate.dependencyInterfacesStable ? "\u7A33\u5B9A" : "\u4E0D\u7A33\u5B9A"}\uFF0C\u5199\u5165\u6240\u6709\u6743=${task.readyGate.writeOwnershipResolved ? "\u5DF2\u786E\u5B9A" : "\u672A\u786E\u5B9A"}\uFF0C\u4E0A\u4E0B\u6587\u9884\u7B97=${task.readyGate.contextWithinBudget ? "\u901A\u8FC7" : "\u8D85\u9650"}\uFF0C\u9A8C\u6536=${task.readyGate.acceptanceExecutable ? "\u53EF\u6267\u884C" : "\u4E0D\u53EF\u6267\u884C"}\u3002
|
|
6748
|
+
|
|
6749
|
+
## 10. \u975E\u76EE\u6807
|
|
6750
|
+
|
|
6751
|
+
- \u4E0D\u6269\u5927\u5230\u5F53\u524D Behavior IDs \u4E4B\u5916\u7684\u72EC\u7ACB\u4E1A\u52A1\u7ED3\u679C\u3002
|
|
6752
|
+
- \u4E0D\u7F16\u5199 ArkTS/ArkUI\uFF0C\u4E0D\u628A\u6E90\u7801\u9605\u8BFB\u672C\u8EAB\u89C6\u4E3A\u88C1\u51B3\u5B8C\u6210\u3002
|
|
6753
|
+
- \u4E0D\u4EE5\u62A5\u544A\u5B57\u6BB5\u3001\u6CE8\u91CA\u3001\u6869\u4EE3\u7801\u6216\u4EC5\u7F16\u8BD1\u6210\u529F\u66FF\u4EE3\u771F\u5B9E\u884C\u4E3A\u5B9E\u73B0\u3002
|
|
6192
6754
|
|
|
6193
|
-
|
|
6755
|
+
## 11. \u9A8C\u6536\u547D\u4EE4\u4E0E\u573A\u666F
|
|
6756
|
+
|
|
6757
|
+
${task.acceptance.verificationRequirements.map((value) => `- ${value}`).join("\n")}
|
|
6758
|
+
|
|
6759
|
+
\u9A8C\u6536\u5FC5\u987B\u8986\u76D6\u7684 Report refs\uFF1A${task.acceptance.reportRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}\u3002
|
|
6760
|
+
|
|
6761
|
+
## 12. \u5B8C\u6210\u56DE\u62A5\u683C\u5F0F
|
|
6762
|
+
|
|
6763
|
+
- \u88C1\u51B3\u7684 issue refs\u3001\u8BFB\u53D6\u7684 Android \u6E90\u7801/\u8FD0\u884C\u8BC1\u636E\uFF0C\u4EE5\u53CA\u5199\u5165\u7684 correction/review\u3002
|
|
6764
|
+
- \u6BCF\u4E2A Behavior ID \u5BF9\u5E94\u7684\u88C1\u51B3\u7ED3\u679C\u3002
|
|
6765
|
+
- \u91CD\u65B0\u89C4\u5212\u540E\u7684 Plan Hash\u3001planningPhase\uFF0C\u4EE5\u53CA\u662F\u5426\u4ECD\u6709 Resolution\uFF1B\u6E05\u96F6\u540E\u8BB0\u5F55\u65B0\u751F\u6210\u7684\u51BB\u7ED3 Coding Plan Hash\u3002
|
|
6766
|
+
- \u672A\u89E3\u51B3\u95EE\u9898\uFF1B\u6CA1\u6709\u5219\u660E\u786E\u5199\u201C\u65E0\u201D\u3002
|
|
6194
6767
|
|
|
6195
6768
|
## \u5DE5\u4F5C\u91CF\u63D0\u793A
|
|
6196
6769
|
|
|
6197
6770
|
- Fact \u6570\uFF1A${task.estimate.factCount}
|
|
6771
|
+
- Behavior \u6570\uFF1A${task.estimate.behaviorCount}
|
|
6772
|
+
- \u53EF\u89C2\u5BDF\u7ED3\u679C\u6570\uFF1A${task.estimate.observableOutcomeCount}
|
|
6198
6773
|
- \u6E90\u7801\u6587\u4EF6\u6570\uFF1A${task.estimate.sourceFileCount}
|
|
6774
|
+
- \u751F\u4EA7\u4EE3\u7801\u6587\u4EF6\u6570\uFF1A${task.estimate.productionFileCount}
|
|
6775
|
+
- \u6D4B\u8BD5\u6587\u4EF6\u6570\uFF1A${task.estimate.testFileCount}
|
|
6776
|
+
- Capability \u6570\uFF1A${task.estimate.capabilityCount}
|
|
6199
6777
|
- \u8C03\u7528\u94FE\u8282\u70B9\u6570\uFF1A${task.estimate.traceNodeCount}
|
|
6200
6778
|
- \u4F30\u7B97 Facts Token\uFF1A${task.estimate.estimatedFactTokens}
|
|
6201
6779
|
- \u53EF\u89E3\u9501\u4E0B\u6E38 Task\uFF1A${task.estimate.downstreamUnlockCount}
|
|
6780
|
+
`;
|
|
6781
|
+
}
|
|
6782
|
+
function renderPageWorkPackageBrief(workPackage, plan, facts) {
|
|
6783
|
+
const quoteList = (values, fallback = "\u65E0") => values.length ? values.map((value) => `- \`${value}\``).join("\n") : `- ${fallback}`;
|
|
6784
|
+
const surfaceNames = workPackage.surfaceRefs.map((ref) => `${facts.surfaces.find((surface) => surface.id === ref)?.name ?? ref}\uFF08\`${ref}\`\uFF09`);
|
|
6785
|
+
const featureSections = workPackage.features.map((feature) => {
|
|
6786
|
+
const behavior = plan.behaviorResponsibilities.find((item) => item.id === feature.behaviorId);
|
|
6787
|
+
const sources = uniqueSorted([
|
|
6788
|
+
...behavior.closure.sourceEvidence.map((item) => `${item.file}${item.line ? `:${item.line}${item.endLine && item.endLine !== item.line ? `-${item.endLine}` : ""}` : ""}${item.symbol ? ` \u2014 ${item.symbol}` : ""}`),
|
|
6789
|
+
...behavior.closure.codeRefs.flatMap((item) => typeof item.file === "string" ? [`${item.file}${typeof item.startLine === "number" ? `:${item.startLine}${typeof item.endLine === "number" && item.endLine !== item.startLine ? `-${item.endLine}` : ""}` : ""}${typeof item.name === "string" ? ` \u2014 ${item.name}` : ""}`] : [])
|
|
6790
|
+
]);
|
|
6791
|
+
return `### F-${String(feature.ordinal).padStart(3, "0")}\uFF1A${feature.title}
|
|
6202
6792
|
|
|
6203
|
-
|
|
6793
|
+
- Behavior / Work Item\uFF1A\`${feature.behaviorId}\` / \`${feature.workItemId}\`
|
|
6794
|
+
- \u89E6\u53D1\uFF1A${feature.triggerLabels.join("\u3001") || "\u7531 Android \u6E90\u7801\u5165\u53E3\u786E\u5B9A"}
|
|
6795
|
+
- \u63A7\u4EF6\uFF1A${feature.controlNames.join("\u3001") || "\u65E0\u663E\u5F0F\u63A7\u4EF6"}
|
|
6796
|
+
- \u4EA4\u4E92\u7ED3\u679C\uFF1A${feature.interactionOutcomes.join("\u3001") || "\u5FC5\u987B\u6CBF\u6E90\u7801\u8C03\u7528\u94FE\u786E\u8BA4\u72B6\u6001\u3001\u6570\u636E\u6216\u7CFB\u7EDF\u526F\u4F5C\u7528"}
|
|
6797
|
+
- \u5B9E\u73B0\u7ED3\u679C\uFF1A${feature.observableOutcome}
|
|
6798
|
+
- Required Fact refs\uFF1A${feature.requiredFactRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6799
|
+
- \u6E90\u7801\u590D\u6838\uFF1A${feature.requiresSourceReview ? "\u5FC5\u987B\uFF1BFacts \u672A\u5B8C\u6574\u8868\u8FBE\u53EF\u89C2\u5BDF\u7ED3\u679C\u6216\u5206\u652F" : "\u5FC5\u987B\u8BFB\u53D6\u4E0B\u5217\u8BC1\u636E\u5E76\u8986\u76D6\u5176\u4E2D\u5168\u90E8\u5B9E\u9645\u5206\u652F"}
|
|
6800
|
+
- Android \u8BC1\u636E\uFF1A
|
|
6801
|
+
${sources.length ? sources.map((value) => ` - ${value}`).join("\n") : " - \u65E0\u76F4\u63A5\u4F4D\u7F6E\uFF1B\u4E0D\u5F97\u731C\u6D4B\u5B9E\u73B0\uFF0C\u5148\u4ECE\u5BF9\u5E94 core facts \u7EE7\u7EED\u5B9A\u4F4D"}
|
|
6802
|
+
- Report refs\uFF1A${feature.reportRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}`;
|
|
6803
|
+
}).join("\n\n");
|
|
6804
|
+
const tasksById = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
6805
|
+
const packageTaskIds = new Set(workPackage.workItemIds);
|
|
6806
|
+
const internalSequence = workPackage.workItems.map((item) => {
|
|
6807
|
+
const dependencies = (tasksById.get(item.id)?.dependsOnTaskIds ?? []).filter((id) => packageTaskIds.has(id));
|
|
6808
|
+
return `${item.ordinal}. \`${item.id}\` \u2014 ${item.title}${dependencies.length ? `\uFF1B\u9875\u9762\u5185\u524D\u7F6E\uFF1A${dependencies.map((id) => `\`${id}\``).join("\u3001")}` : ""}`;
|
|
6809
|
+
}).join("\n");
|
|
6810
|
+
const dependencyText = workPackage.dependencyInterfaces.length ? workPackage.dependencyInterfaces.map((item) => `- \`${item.providerPackageId}\` \u63D0\u4F9B ${item.kind === "module" ? "Module Interface" : "\u8C03\u5EA6\u524D\u7F6E"} \`${item.ref}\`\uFF1B\u6765\u6E90 Work Items\uFF1A${item.providerWorkItemIds.map((id) => `\`${id}\``).join("\u3001")}\uFF1B\u9519\u8BEF\u6A21\u5F0F\uFF1A${item.errorModes.join("\uFF1B") || "\u65E0"}`).join("\n") : "- \u65E0";
|
|
6811
|
+
const acceptanceSequences = workPackage.features.map((feature) => `### F-${String(feature.ordinal).padStart(3, "0")}
|
|
6204
6812
|
|
|
6205
|
-
|
|
6813
|
+
1. \u4ECE\u771F\u5B9E\u5E94\u7528\u5165\u53E3\u5230\u8FBE\u672C Page Work Package \u627F\u8F7D\u9762\u3002
|
|
6814
|
+
2. \u89E6\u53D1\u201C${feature.triggerLabels.join(" / ") || feature.title}\u201D\u3002
|
|
6815
|
+
3. \u65AD\u8A00\uFF1A${feature.observableOutcome}
|
|
6816
|
+
4. \u6309 Android \u8BC1\u636E\u8986\u76D6\u6210\u529F\u3001\u53D6\u6D88\u3001\u5931\u8D25\u3001\u7A7A\u503C\u548C\u7CFB\u7EDF\u56DE\u8C03\u5206\u652F\u4E2D\u5B9E\u9645\u5B58\u5728\u7684\u573A\u666F\uFF1B\u4E0D\u5F97\u7528\u6869\u4EE3\u7801\u6216\u4EC5\u7F16\u8BD1\u6210\u529F\u4EE3\u66FF\u3002`).join("\n\n");
|
|
6817
|
+
return `# Page Work Package\uFF1A${workPackage.title.replace(/ Page Work Package$/, "")}
|
|
6818
|
+
|
|
6819
|
+
> \u672C Markdown \u662F execution \u9636\u6BB5\u4E00\u6B21\u6A21\u578B\u8C03\u7528\u7684\u6700\u7EC8\u9875\u9762\u5B9E\u73B0\u8F93\u5165\uFF0C\u7531\u51BB\u7ED3\u7684 Execution Plan \u548C Canonical Facts \u786E\u5B9A\u6027\u751F\u6210\u3002Work Item \u4E0E Behavior \u4EC5\u4F5C\u4E3A\u672C\u5305\u5185\u90E8\u987A\u5E8F\u3001\u8BC1\u636E\u548C\u9A8C\u6536\u8D23\u4EFB\uFF0C\u4E0D\u518D\u5206\u522B\u751F\u6210\u53EF\u6267\u884C Markdown\u3002
|
|
6820
|
+
|
|
6821
|
+
## 1. \u9875\u9762\u6267\u884C\u5951\u7EA6
|
|
6822
|
+
|
|
6823
|
+
- Page Work Package ID\uFF1A\`${workPackage.id}\`
|
|
6824
|
+
- Execution Plan Hash\uFF1A\`${sha2562(plan)}\`
|
|
6825
|
+
- Facts SHA256\uFF1A\`${plan.baseline.factsSha256}\`
|
|
6826
|
+
- Android Fingerprint\uFF1A${plan.baseline.androidFingerprint ? `\`${plan.baseline.androidFingerprint}\`` : "\u672A\u63D0\u4F9B"}
|
|
6827
|
+
- Canonical Page\uFF1A\`${workPackage.canonicalPageRef ?? "unassigned"}\`
|
|
6828
|
+
- \u627F\u8F7D\u9762\uFF1A${surfaceNames.join("\u3001") || "\u65E0"}
|
|
6829
|
+
- Work Item \u6570\uFF1A${workPackage.workItems.length}
|
|
6830
|
+
- Behavior / \u9875\u9762\u529F\u80FD\u6570\uFF1A${workPackage.features.length}
|
|
6831
|
+
|
|
6832
|
+
## 2. \u9875\u9762\u8303\u56F4\u4E0E\u6700\u7EC8\u76EE\u6807
|
|
6833
|
+
|
|
6834
|
+
\u672C\u5305\u5FC5\u987B\u5728\u540C\u4E00\u4E2A\u5B9E\u73B0 epoch \u5185\u5B8C\u6210\u4EE5\u4E0B\u5168\u90E8\u9875\u9762\u529F\u80FD\uFF0C\u5E76\u4FDD\u6301\u9875\u9762\u5185\u987A\u5E8F\u3002\u627F\u8F7D\u9762\u5185\u7684 Dialog\u3001Sheet \u6216\u7CFB\u7EDF\u56DE\u8C03\u5C5E\u4E8E\u672C\u5305\uFF1B\u672A\u51FA\u73B0\u5728\u529F\u80FD\u5217\u8868\u548C Facts \u95ED\u5305\u4E2D\u7684\u72EC\u7ACB\u4E1A\u52A1\u7ED3\u679C\u4E0D\u5C5E\u4E8E\u672C\u5305\u3002
|
|
6835
|
+
|
|
6836
|
+
${workPackage.acceptance.observableOutcomes.map((outcome) => `- ${outcome}`).join("\n")}
|
|
6837
|
+
|
|
6838
|
+
## 3. \u5177\u4F53\u5B9E\u73B0\u529F\u80FD
|
|
6839
|
+
|
|
6840
|
+
${featureSections}
|
|
6841
|
+
|
|
6842
|
+
## 4. \u9875\u9762\u72B6\u6001\u4E0E Facts \u95ED\u5305
|
|
6843
|
+
|
|
6844
|
+
- Entry refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.entryRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6845
|
+
- Surface refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.surfaceRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6846
|
+
- Control refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.controlRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6847
|
+
- Interaction refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.interactionRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6848
|
+
- \u5F85\u88C1\u51B3 refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.unresolvedRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6849
|
+
|
|
6850
|
+
\u5B9E\u73B0\u524D\u5FC5\u987B\u6CBF source-context \u7D22\u5F15\u8BFB\u53D6\u5F53\u524D\u9875\u9762\u6D89\u53CA\u7684 Android \u6E90\u7801\u4E0E\u5FC5\u8981\u8C03\u7528\u94FE\uFF1B\u672C\u8282\u7684 refs \u548C\u6458\u8981\u4E0D\u80FD\u66FF\u4EE3\u6E90\u7801\u9605\u8BFB\u3002
|
|
6851
|
+
|
|
6852
|
+
## 5. \u9875\u9762\u5185\u5B9E\u65BD\u987A\u5E8F
|
|
6853
|
+
|
|
6854
|
+
${internalSequence}
|
|
6855
|
+
|
|
6856
|
+
\u9875\u9762\u5185 Work Item \u987A\u5E8F\u662F\u51BB\u7ED3\u8BA1\u5212\u7684\u4E00\u90E8\u5206\u3002\u53EF\u4EE5\u5728\u4E00\u6B21\u6A21\u578B\u8C03\u7528\u4E2D\u8FDE\u7EED\u5B8C\u6210\uFF0C\u4F46\u4E0D\u5F97\u9057\u6F0F\u3001\u91CD\u6392\u6216\u628A\u5176\u4E2D\u4E00\u9879\u5355\u72EC\u58F0\u660E\u4E3A\u6574\u4E2A\u9875\u9762\u5B8C\u6210\u3002
|
|
6857
|
+
|
|
6858
|
+
## 6. \u5199\u5165\u6240\u6709\u6743
|
|
6859
|
+
|
|
6860
|
+
### \u5141\u8BB8\u4FEE\u6539
|
|
6861
|
+
|
|
6862
|
+
${quoteList(workPackage.writeOwnership.ownedFiles, `\u5C1A\u65E0\u5DF2\u5B58\u5728\u6587\u4EF6\uFF1B\u4EC5\u53EF\u5728\u72EC\u5360\u8303\u56F4 ${workPackage.writeOwnership.exclusiveScopes.map((scope) => `\`${scope}\``).join("\u3001")} \u5185\u521B\u5EFA\u6216\u8BA4\u9886\u6587\u4EF6`)}
|
|
6863
|
+
|
|
6864
|
+
### \u53EA\u8BFB\u4F9D\u8D56
|
|
6865
|
+
|
|
6866
|
+
${quoteList(workPackage.writeOwnership.readOnlyFiles)}
|
|
6867
|
+
|
|
6868
|
+
### \u7981\u6B62\u4FEE\u6539
|
|
6869
|
+
|
|
6870
|
+
${quoteList(workPackage.writeOwnership.forbiddenGlobalFiles)}
|
|
6871
|
+
|
|
6872
|
+
### \u72EC\u5360\u8303\u56F4
|
|
6873
|
+
|
|
6874
|
+
${quoteList(workPackage.writeOwnership.exclusiveScopes)}
|
|
6875
|
+
|
|
6876
|
+
## 7. \u8DE8\u9875\u9762\u4F9D\u8D56\u4E0E Interface
|
|
6877
|
+
|
|
6878
|
+
${dependencyText}
|
|
6879
|
+
|
|
6880
|
+
\u53EA\u6709\u5168\u90E8\u524D\u7F6E Page Work Package \u5DF2\u53D6\u5F97\u65B0\u9C9C\u7684\u672C\u5730\u9A8C\u8BC1\u8BC1\u636E\uFF0C\u5F53\u524D\u5305\u624D\u53EF\u6267\u884C\u3002\u4E0D\u5F97\u901A\u8FC7\u4FEE\u6539\u4E0A\u6E38\u5B9E\u73B0\u7ED5\u8FC7 Interface\u3002
|
|
6206
6881
|
|
|
6207
|
-
## \
|
|
6882
|
+
## 8. UI \u64CD\u4F5C\u4E0E\u9A8C\u6536\u5E8F\u5217
|
|
6208
6883
|
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6884
|
+
${acceptanceSequences}
|
|
6885
|
+
|
|
6886
|
+
\u901A\u7528\u9A8C\u8BC1\u8981\u6C42\uFF1A
|
|
6887
|
+
|
|
6888
|
+
${workPackage.acceptance.verificationRequirements.map((value) => `- ${value}`).join("\n")}
|
|
6889
|
+
|
|
6890
|
+
\u9A8C\u6536\u5FC5\u987B\u8986\u76D6\u7684 Report refs\uFF1A${workPackage.acceptance.reportRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}\u3002
|
|
6891
|
+
|
|
6892
|
+
## 9. \u975E\u76EE\u6807\u4E0E\u5B9E\u73B0\u7EA6\u675F
|
|
6893
|
+
|
|
6894
|
+
- \u4E0D\u6269\u5927\u5230\u672C\u5305 Behavior IDs \u4E4B\u5916\u7684\u72EC\u7ACB\u4E1A\u52A1\u7ED3\u679C\u3002
|
|
6895
|
+
- \u4E0D\u4FEE\u6539\u53EA\u8BFB\u6216\u7981\u6B62\u6587\u4EF6\uFF0C\u4E0D\u91CD\u6784\u4E0A\u6E38 Module \u7684 Implementation\u3002
|
|
6896
|
+
- \u4E0D\u9884\u5148\u865A\u6784\u5C1A\u65E0\u8BC1\u636E\u7684 ArkTS \u6587\u4EF6\u540D\u3001\u72B6\u6001\u5B57\u6BB5\u6216\u5931\u8D25\u884C\u4E3A\u3002
|
|
6897
|
+
- \u4E0D\u4EE5\u62A5\u544A\u5B57\u6BB5\u3001\u6CE8\u91CA\u3001\u6869\u4EE3\u7801\u6216\u4EC5\u7F16\u8BD1\u6210\u529F\u66FF\u4EE3\u771F\u5B9E\u884C\u4E3A\u5B9E\u73B0\u3002
|
|
6898
|
+
- \u53D1\u73B0 Facts\u3001\u9875\u9762\u5F52\u5C5E\u6216 Android \u884C\u4E3A\u9519\u8BEF\u65F6\uFF0C\u7EC8\u6B62\u5F53\u524D execution epoch \u5E76\u8FD4\u56DE\u89C4\u5212\u9636\u6BB5\uFF0C\u4E0D\u5728\u5B9E\u73B0\u4E2D\u6539\u5199\u6D3E\u751F\u8BA1\u5212\u3002
|
|
6899
|
+
|
|
6900
|
+
## 10. \u5B8C\u6210\u56DE\u62A5\u683C\u5F0F
|
|
6901
|
+
|
|
6902
|
+
- \u4FEE\u6539\u6587\u4EF6\u53CA\u5BF9\u5E94\u5199\u5165\u6240\u6709\u6743\u3002
|
|
6903
|
+
- F-001 \u81F3 F-${String(workPackage.features.length).padStart(3, "0")} \u6BCF\u9879\u7684\u5B9E\u73B0\u3001\u6D4B\u8BD5\u548C Report refs \u7ED3\u679C\u3002
|
|
6904
|
+
- \u6784\u5EFA\u3001\u5355\u5143\u6D4B\u8BD5\u3001\u9875\u9762/Journey \u6D4B\u8BD5\u547D\u4EE4\u4E0E\u7ED3\u679C\u3002
|
|
6905
|
+
- \u672A\u89E3\u51B3\u95EE\u9898\uFF1B\u6CA1\u6709\u5219\u660E\u786E\u5199\u201C\u65E0\u201D\u3002
|
|
6906
|
+
|
|
6907
|
+
## \u5DE5\u4F5C\u91CF\u63D0\u793A
|
|
6908
|
+
|
|
6909
|
+
- Work Item \u6570\uFF1A${workPackage.estimate.workItemCount}
|
|
6910
|
+
- Behavior \u6570\uFF1A${workPackage.estimate.behaviorCount}
|
|
6911
|
+
- Fact \u6570\uFF1A${workPackage.estimate.factCount}
|
|
6912
|
+
- \u6E90\u7801\u6587\u4EF6\u6570\uFF1A${workPackage.estimate.sourceFileCount}
|
|
6913
|
+
- \u751F\u4EA7\u4EE3\u7801\u6587\u4EF6\u6570\uFF1A${workPackage.estimate.productionFileCount}
|
|
6914
|
+
- \u6D4B\u8BD5\u6587\u4EF6\u6570\uFF1A${workPackage.estimate.testFileCount}
|
|
6915
|
+
- Capability \u6570\uFF1A${workPackage.estimate.capabilityCount}
|
|
6916
|
+
- \u8C03\u7528\u94FE\u8282\u70B9\u6570\uFF1A${workPackage.estimate.traceNodeCount}
|
|
6917
|
+
- \u4F30\u7B97 Facts Token\uFF1A${workPackage.estimate.estimatedFactTokens}
|
|
6212
6918
|
`;
|
|
6213
6919
|
}
|
|
6214
6920
|
function writeFile(file, content) {
|
|
6215
|
-
|
|
6216
|
-
|
|
6921
|
+
fs27.mkdirSync(path33.dirname(file), { recursive: true });
|
|
6922
|
+
fs27.writeFileSync(file, content, "utf8");
|
|
6217
6923
|
}
|
|
6218
|
-
function writeTaskPlan(projectRoot, plan) {
|
|
6924
|
+
function writeTaskPlan(projectRoot, plan, facts) {
|
|
6219
6925
|
const root = taskPlanRoot(projectRoot);
|
|
6220
6926
|
const parent = path33.dirname(root);
|
|
6221
|
-
|
|
6927
|
+
fs27.mkdirSync(parent, { recursive: true });
|
|
6222
6928
|
const temporary = path33.join(parent, `tasks.${process.pid}.${Date.now()}.tmp`);
|
|
6223
6929
|
const backup = path33.join(parent, `tasks.${process.pid}.backup`);
|
|
6224
|
-
|
|
6225
|
-
fs26.mkdirSync(path33.join(temporary, "acceptance"), { recursive: true });
|
|
6226
|
-
const graphFile = path33.join(temporary, "graph.json");
|
|
6227
|
-
writeFile(graphFile, prettyStableJson(plan));
|
|
6930
|
+
fs27.mkdirSync(temporary, { recursive: true });
|
|
6228
6931
|
const taskPlanSha256 = sha2562(plan);
|
|
6229
|
-
const
|
|
6932
|
+
const graphFile = path33.join(temporary, "execution-graph.json");
|
|
6933
|
+
writeFile(graphFile, prettyStableJson(plan));
|
|
6934
|
+
writeFile(path33.join(temporary, "behavior-plan.json"), prettyStableJson({
|
|
6935
|
+
behaviorPlanVersion: BEHAVIOR_PLAN_VERSION,
|
|
6936
|
+
planningModelVersion: plan.planningModelVersion,
|
|
6937
|
+
factsAdapterVersion: plan.factsAdapterVersion,
|
|
6938
|
+
plannerPolicyVersion: plan.plannerPolicyVersion,
|
|
6939
|
+
baseline: plan.baseline,
|
|
6940
|
+
entryResolution: plan.entryResolution,
|
|
6941
|
+
responsibilities: plan.behaviorResponsibilities,
|
|
6942
|
+
ownership: plan.behaviorOwnership,
|
|
6943
|
+
planningWarnings: plan.planningWarnings
|
|
6944
|
+
}));
|
|
6945
|
+
const resolutionTargets = plan.tasks.filter((task) => task.type === "resolution").map((task) => {
|
|
6230
6946
|
const stem = taskArtifactStem(task.id);
|
|
6231
|
-
const
|
|
6232
|
-
const
|
|
6233
|
-
|
|
6947
|
+
const taskRoot = `resolution-tasks/${stem}`;
|
|
6948
|
+
const briefFile = `${taskRoot}/brief.md`;
|
|
6949
|
+
const contractFile = `${taskRoot}/contract.json`;
|
|
6950
|
+
const coreFactsFile = `${taskRoot}/core-facts.json`;
|
|
6951
|
+
const sourceContextFile = `${taskRoot}/source-context.json`;
|
|
6952
|
+
const acceptanceFile = `${taskRoot}/acceptance.json`;
|
|
6953
|
+
writeFile(path33.join(temporary, briefFile), renderResolutionTaskBrief(task, plan));
|
|
6954
|
+
writeFile(path33.join(temporary, contractFile), prettyStableJson({ executionPlanSha256: taskPlanSha256, baseline: plan.baseline, task: taskContractView(task) }));
|
|
6955
|
+
writeFile(path33.join(temporary, coreFactsFile), prettyStableJson(coreFactsForTask(task, facts)));
|
|
6956
|
+
writeFile(path33.join(temporary, sourceContextFile), prettyStableJson(sourceContextForTask(task)));
|
|
6234
6957
|
writeFile(path33.join(temporary, acceptanceFile), prettyStableJson(task.acceptance));
|
|
6235
|
-
return { id: task.id,
|
|
6958
|
+
return { id: task.id, kind: "resolution-task", title: task.title, briefFile, contractFile, coreFactsFile, sourceContextFile, acceptanceFile };
|
|
6959
|
+
});
|
|
6960
|
+
const pageWorkPackageTargets = plan.pageWorkPackages.map((workPackage) => {
|
|
6961
|
+
const stem = workPackageArtifactStem(workPackage.id);
|
|
6962
|
+
const packageRoot = `page-work-packages/${stem}`;
|
|
6963
|
+
const briefFile = `page-work-packages/${stem}.md`;
|
|
6964
|
+
const contractFile = `${packageRoot}/contract.json`;
|
|
6965
|
+
const coreFactsFile = `${packageRoot}/core-facts.json`;
|
|
6966
|
+
const sourceContextFile = `${packageRoot}/source-context.json`;
|
|
6967
|
+
const acceptanceFile = `${packageRoot}/acceptance.json`;
|
|
6968
|
+
const contract = pageWorkPackageContractView(workPackage, plan, facts);
|
|
6969
|
+
writeFile(path33.join(temporary, briefFile), renderPageWorkPackageBrief(contract, plan, facts));
|
|
6970
|
+
writeFile(path33.join(temporary, contractFile), prettyStableJson({ executionPlanSha256: taskPlanSha256, baseline: plan.baseline, pageWorkPackage: contract }));
|
|
6971
|
+
writeFile(path33.join(temporary, coreFactsFile), prettyStableJson(coreFactsForPageWorkPackage(contract, facts)));
|
|
6972
|
+
writeFile(path33.join(temporary, sourceContextFile), prettyStableJson(sourceContextForPageWorkPackage(workPackage, plan)));
|
|
6973
|
+
writeFile(path33.join(temporary, acceptanceFile), prettyStableJson(contract.acceptance));
|
|
6974
|
+
return { id: workPackage.id, kind: "page-work-package", title: workPackage.title, workItemIds: workPackage.workItemIds, briefFile, contractFile, coreFactsFile, sourceContextFile, acceptanceFile };
|
|
6236
6975
|
});
|
|
6976
|
+
const targets = plan.planningPhase === "resolution" ? resolutionTargets : pageWorkPackageTargets;
|
|
6237
6977
|
const index = {
|
|
6238
6978
|
taskPlanVersion: plan.taskPlanVersion,
|
|
6239
6979
|
taskPlanSha256,
|
|
6240
6980
|
baseline: plan.baseline,
|
|
6241
|
-
|
|
6981
|
+
executionPlanVersion: plan.executionPlanVersion,
|
|
6982
|
+
planningPhase: plan.planningPhase,
|
|
6983
|
+
graphFile: "execution-graph.json",
|
|
6984
|
+
behaviorPlanFile: "behavior-plan.json",
|
|
6242
6985
|
counts: {
|
|
6243
6986
|
total: plan.tasks.length,
|
|
6244
6987
|
resolution: plan.tasks.filter((task) => task.type === "resolution").length,
|
|
6245
6988
|
bootstrap: plan.tasks.filter((task) => task.type === "bootstrap").length,
|
|
6246
|
-
journey: plan.tasks.filter((task) => task.type === "journey").length
|
|
6989
|
+
journey: plan.tasks.filter((task) => task.type === "journey").length,
|
|
6990
|
+
workPackages: plan.pageWorkPackages.length,
|
|
6991
|
+
behaviors: plan.behaviorResponsibilities.length,
|
|
6992
|
+
executionTargets: targets.length
|
|
6247
6993
|
},
|
|
6248
|
-
|
|
6994
|
+
targets,
|
|
6995
|
+
workItems: plan.planningPhase === "execution" ? plan.tasks.map((task) => ({ id: task.id, type: task.type, title: task.title, workPackageId: task.workPackageId, behaviorIds: task.behaviorIds })) : []
|
|
6249
6996
|
};
|
|
6250
6997
|
writeFile(path33.join(temporary, "index.json"), prettyStableJson(index));
|
|
6251
6998
|
try {
|
|
6252
|
-
if (
|
|
6253
|
-
if (
|
|
6254
|
-
|
|
6255
|
-
if (
|
|
6999
|
+
if (fs27.existsSync(backup)) fs27.rmSync(backup, { recursive: true, force: true });
|
|
7000
|
+
if (fs27.existsSync(root)) fs27.renameSync(root, backup);
|
|
7001
|
+
fs27.renameSync(temporary, root);
|
|
7002
|
+
if (fs27.existsSync(backup)) fs27.rmSync(backup, { recursive: true, force: true });
|
|
6256
7003
|
} catch (error) {
|
|
6257
|
-
if (!
|
|
6258
|
-
if (
|
|
7004
|
+
if (!fs27.existsSync(root) && fs27.existsSync(backup)) fs27.renameSync(backup, root);
|
|
7005
|
+
if (fs27.existsSync(temporary)) fs27.rmSync(temporary, { recursive: true, force: true });
|
|
6259
7006
|
throw error;
|
|
6260
7007
|
}
|
|
6261
7008
|
return {
|
|
6262
7009
|
taskPlanSha256,
|
|
6263
|
-
graph: path33.relative(path33.resolve(projectRoot), path33.join(root, "graph.json")).replaceAll("\\", "/"),
|
|
7010
|
+
graph: path33.relative(path33.resolve(projectRoot), path33.join(root, "execution-graph.json")).replaceAll("\\", "/"),
|
|
6264
7011
|
index: path33.relative(path33.resolve(projectRoot), path33.join(root, "index.json")).replaceAll("\\", "/")
|
|
6265
7012
|
};
|
|
6266
7013
|
}
|
|
6267
7014
|
function readTaskPlan(projectRoot) {
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
|
|
7015
|
+
const root = taskPlanRoot(projectRoot);
|
|
7016
|
+
const file = fs27.existsSync(path33.join(root, "execution-graph.json")) ? path33.join(root, "execution-graph.json") : path33.join(root, "graph.json");
|
|
7017
|
+
if (!fs27.existsSync(file)) return null;
|
|
7018
|
+
return JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
6271
7019
|
}
|
|
6272
7020
|
function hasTaskPlan(projectRoot) {
|
|
6273
|
-
return
|
|
7021
|
+
return fs27.existsSync(path33.join(taskPlanRoot(projectRoot), "execution-graph.json")) || fs27.existsSync(path33.join(taskPlanRoot(projectRoot), "graph.json"));
|
|
6274
7022
|
}
|
|
6275
7023
|
|
|
6276
7024
|
// src/planning/materialize.ts
|
|
6277
|
-
function cursorFor(
|
|
6278
|
-
return Buffer.from(JSON.stringify({
|
|
7025
|
+
function cursorFor(targetId, taskPlanSha256, offset) {
|
|
7026
|
+
return Buffer.from(JSON.stringify({ targetId, taskPlanSha256, offset }), "utf8").toString("base64url");
|
|
6279
7027
|
}
|
|
6280
|
-
function cursorOffset(cursor,
|
|
7028
|
+
function cursorOffset(cursor, targetId, taskPlanSha256) {
|
|
6281
7029
|
if (!cursor) return 0;
|
|
6282
7030
|
try {
|
|
6283
7031
|
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
6284
|
-
if (value.
|
|
7032
|
+
if (value.targetId !== targetId || value.taskPlanSha256 !== taskPlanSha256 || !Number.isInteger(value.offset) || value.offset < 0) throw new Error("mismatch");
|
|
6285
7033
|
return value.offset;
|
|
6286
7034
|
} catch {
|
|
6287
7035
|
throw new Error("Invalid or stale task envelope cursor");
|
|
6288
7036
|
}
|
|
6289
7037
|
}
|
|
6290
|
-
function
|
|
6291
|
-
const allowed = ["id", "file", "name", "symbol", "kind", "type", "line", "startLine", "endLine"];
|
|
6292
|
-
return Object.fromEntries(allowed.filter((key) => value[key] !== void 0).map((key) => [key, value[key]]));
|
|
6293
|
-
}
|
|
6294
|
-
function materializeEnvelope(plan, facts, taskId, options = {}) {
|
|
6295
|
-
const task = plan.tasks.find((item) => item.id === taskId);
|
|
6296
|
-
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
7038
|
+
function materializeEnvelope(plan, facts, targetId, options = {}) {
|
|
6297
7039
|
const taskPlanSha256 = sha2562(plan);
|
|
6298
|
-
const refs = /* @__PURE__ */ new Set([...task.primaryFactRefs, ...task.supportingFactRefs]);
|
|
6299
|
-
const coreFacts = [
|
|
6300
|
-
...facts.surfaces.filter((item) => refs.has(item.id)).map((value) => ({ kind: "surface", value })),
|
|
6301
|
-
...facts.controls.filter((item) => refs.has(item.id)).map((value) => ({ kind: "control", value })),
|
|
6302
|
-
...facts.entries.filter((item) => refs.has(item.id)).map((value) => ({
|
|
6303
|
-
kind: "entry",
|
|
6304
|
-
value: { ...value, trace: { ...value.trace, codeRefs: value.trace.codeRefs.map(codeRefIndex) } }
|
|
6305
|
-
})),
|
|
6306
|
-
...facts.effects.filter((item) => refs.has(item.id)).map((value) => ({ kind: "effect", value })),
|
|
6307
|
-
...facts.issues.filter((item) => refs.has(item.id) || task.closure.unresolvedRefs.includes(item.id)).map((value) => ({ kind: "issue", value }))
|
|
6308
|
-
];
|
|
6309
|
-
const envelopeTask = { ...task, closure: { ...task.closure, codeRefs: task.closure.codeRefs.map(codeRefIndex) } };
|
|
6310
|
-
const sourceContext = uniqueBy([
|
|
6311
|
-
...task.closure.sourceEvidence.map((value) => ({ kind: "source-evidence", value })),
|
|
6312
|
-
...task.closure.codeRefs.map((value) => ({ kind: "code-ref", value: codeRefIndex(value) }))
|
|
6313
|
-
], stableJson);
|
|
6314
7040
|
const limit = Math.max(1, Math.min(200, Math.floor(options.maxSourceContext ?? 25)));
|
|
6315
|
-
const offset = cursorOffset(options.cursor,
|
|
7041
|
+
const offset = cursorOffset(options.cursor, targetId, taskPlanSha256);
|
|
7042
|
+
const task = plan.tasks.find((item) => item.id === targetId);
|
|
7043
|
+
if (task) {
|
|
7044
|
+
if (plan.planningPhase !== "resolution" || task.type !== "resolution") throw new Error("Execution phase materializes a Page Work Package, not an internal Coding Work Item");
|
|
7045
|
+
const coreFacts2 = coreFactsForTask(task, facts);
|
|
7046
|
+
const envelopeTask = taskContractView(task);
|
|
7047
|
+
const sourceContext2 = sourceContextForTask(task);
|
|
7048
|
+
if (offset > sourceContext2.length) throw new Error("Task envelope cursor is past the available source context");
|
|
7049
|
+
const page2 = sourceContext2.slice(offset, offset + limit);
|
|
7050
|
+
const nextOffset2 = offset + page2.length;
|
|
7051
|
+
return {
|
|
7052
|
+
taskEnvelopeVersion: TASK_ENVELOPE_VERSION,
|
|
7053
|
+
taskPlanSha256,
|
|
7054
|
+
targetKind: "resolution-task",
|
|
7055
|
+
targetId,
|
|
7056
|
+
task: envelopeTask,
|
|
7057
|
+
pageWorkPackage: null,
|
|
7058
|
+
contract: { executionPlanSha256: taskPlanSha256, baseline: plan.baseline, task: envelopeTask },
|
|
7059
|
+
brief: renderResolutionTaskBrief(task, plan),
|
|
7060
|
+
coreFacts: coreFacts2,
|
|
7061
|
+
dependencyInterfaces: task.dependencyInterfaces,
|
|
7062
|
+
acceptance: task.acceptance,
|
|
7063
|
+
sourceContext: page2,
|
|
7064
|
+
continuation: {
|
|
7065
|
+
cursor: nextOffset2 < sourceContext2.length ? cursorFor(targetId, taskPlanSha256, nextOffset2) : null,
|
|
7066
|
+
remainingSourceContext: Math.max(0, sourceContext2.length - nextOffset2)
|
|
7067
|
+
}
|
|
7068
|
+
};
|
|
7069
|
+
}
|
|
7070
|
+
const workPackage = plan.pageWorkPackages.find((item) => item.id === targetId);
|
|
7071
|
+
if (!workPackage) throw new Error(`Execution target not found: ${targetId}`);
|
|
7072
|
+
if (plan.planningPhase !== "execution") throw new Error("Resolution phase materializes a Resolution Task, not a Page Work Package");
|
|
7073
|
+
const contract = pageWorkPackageContractView(workPackage, plan, facts);
|
|
7074
|
+
const coreFacts = coreFactsForPageWorkPackage(contract, facts);
|
|
7075
|
+
const sourceContext = sourceContextForPageWorkPackage(workPackage, plan);
|
|
6316
7076
|
if (offset > sourceContext.length) throw new Error("Task envelope cursor is past the available source context");
|
|
6317
7077
|
const page = sourceContext.slice(offset, offset + limit);
|
|
6318
7078
|
const nextOffset = offset + page.length;
|
|
6319
7079
|
return {
|
|
6320
7080
|
taskEnvelopeVersion: TASK_ENVELOPE_VERSION,
|
|
6321
7081
|
taskPlanSha256,
|
|
6322
|
-
|
|
6323
|
-
|
|
7082
|
+
targetKind: "page-work-package",
|
|
7083
|
+
targetId,
|
|
7084
|
+
task: null,
|
|
7085
|
+
pageWorkPackage: contract,
|
|
7086
|
+
contract: { executionPlanSha256: taskPlanSha256, baseline: plan.baseline, pageWorkPackage: contract },
|
|
7087
|
+
brief: renderPageWorkPackageBrief(contract, plan, facts),
|
|
6324
7088
|
coreFacts,
|
|
7089
|
+
dependencyInterfaces: contract.dependencyInterfaces,
|
|
7090
|
+
acceptance: contract.acceptance,
|
|
6325
7091
|
sourceContext: page,
|
|
6326
7092
|
continuation: {
|
|
6327
|
-
cursor: nextOffset < sourceContext.length ? cursorFor(
|
|
7093
|
+
cursor: nextOffset < sourceContext.length ? cursorFor(targetId, taskPlanSha256, nextOffset) : null,
|
|
6328
7094
|
remainingSourceContext: Math.max(0, sourceContext.length - nextOffset)
|
|
6329
7095
|
}
|
|
6330
7096
|
};
|
|
6331
7097
|
}
|
|
6332
7098
|
|
|
6333
7099
|
// src/planning/runtime.ts
|
|
6334
|
-
import
|
|
7100
|
+
import fs28 from "fs";
|
|
6335
7101
|
import path34 from "path";
|
|
6336
7102
|
|
|
6337
7103
|
// src/planning/validate.ts
|
|
6338
|
-
function
|
|
6339
|
-
const dependencies = new Map(plan.tasks.map((task) => [task.id, task.dependsOnTaskIds]));
|
|
7104
|
+
function findDependencyCycle(dependencies) {
|
|
6340
7105
|
const visiting = /* @__PURE__ */ new Set();
|
|
6341
7106
|
const visited = /* @__PURE__ */ new Set();
|
|
6342
7107
|
const stack = [];
|
|
@@ -6360,33 +7125,101 @@ function findCycle(plan) {
|
|
|
6360
7125
|
}
|
|
6361
7126
|
return null;
|
|
6362
7127
|
}
|
|
7128
|
+
function findCycle(plan) {
|
|
7129
|
+
return findDependencyCycle(new Map(plan.tasks.map((task) => [task.id, task.dependsOnTaskIds])));
|
|
7130
|
+
}
|
|
7131
|
+
function hasDependencyPath(plan, from, to) {
|
|
7132
|
+
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
7133
|
+
const queue = [...byId.get(from)?.dependsOnTaskIds ?? []];
|
|
7134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7135
|
+
while (queue.length) {
|
|
7136
|
+
const next = queue.shift();
|
|
7137
|
+
if (next === to) return true;
|
|
7138
|
+
if (seen.has(next)) continue;
|
|
7139
|
+
seen.add(next);
|
|
7140
|
+
queue.push(...byId.get(next)?.dependsOnTaskIds ?? []);
|
|
7141
|
+
}
|
|
7142
|
+
return false;
|
|
7143
|
+
}
|
|
6363
7144
|
function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
6364
7145
|
const errors = [];
|
|
6365
7146
|
const warnings = [];
|
|
6366
7147
|
if (plan.taskPlanVersion !== TASK_PLAN_VERSION) errors.push(`taskPlanVersion must be ${TASK_PLAN_VERSION}`);
|
|
7148
|
+
if (plan.executionPlanVersion !== EXECUTION_PLAN_VERSION) errors.push(`executionPlanVersion must be ${EXECUTION_PLAN_VERSION}`);
|
|
6367
7149
|
if (plan.planningModelVersion !== PLANNING_MODEL_VERSION) errors.push(`planningModelVersion must be ${PLANNING_MODEL_VERSION}`);
|
|
6368
7150
|
if (plan.factsAdapterVersion !== FACTS_ADAPTER_VERSION) errors.push(`factsAdapterVersion must be ${FACTS_ADAPTER_VERSION}`);
|
|
6369
7151
|
if (plan.plannerPolicyVersion !== PLANNER_POLICY_VERSION) errors.push(`plannerPolicyVersion must be ${PLANNER_POLICY_VERSION}`);
|
|
6370
7152
|
const fresh = plan.baseline?.factsSha256 === facts.baseline.factsSha256 && plan.baseline?.planningInputSha256 === facts.baseline.planningInputSha256 && plan.baseline?.factsWireVersion === facts.baseline.wireVersion && plan.baseline?.androidFingerprint === androidFingerprint;
|
|
6371
7153
|
if (!fresh) warnings.push("Task plan baseline does not match current Facts, planning inputs, or Android source fingerprint");
|
|
7154
|
+
const behaviorIds = /* @__PURE__ */ new Set();
|
|
7155
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) {
|
|
7156
|
+
if (!/^behavior:[a-f0-9]{16}$/.test(behavior.id)) errors.push(`invalid behavior id ${behavior.id}`);
|
|
7157
|
+
if (behaviorIds.has(behavior.id)) errors.push(`duplicate behavior id ${behavior.id}`);
|
|
7158
|
+
else behaviorIds.add(behavior.id);
|
|
7159
|
+
if (!behavior.primaryFactRefs?.length) errors.push(`${behavior.id}: primaryFactRefs must not be empty`);
|
|
7160
|
+
if (!behavior.acceptance?.observableOutcome?.trim()) errors.push(`${behavior.id}: observableOutcome is required`);
|
|
7161
|
+
for (const ref of behavior.supportingFactRefs ?? []) if (behavior.primaryFactRefs.includes(ref)) errors.push(`${behavior.id}: ${ref} cannot be both primary and supporting`);
|
|
7162
|
+
}
|
|
7163
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) for (const dependency of behavior.dependsOnBehaviorIds ?? []) {
|
|
7164
|
+
if (!behaviorIds.has(dependency)) errors.push(`${behavior.id}: unknown behavior dependency ${dependency}`);
|
|
7165
|
+
if (dependency === behavior.id) errors.push(`${behavior.id}: behavior cannot depend on itself`);
|
|
7166
|
+
}
|
|
7167
|
+
const expectedPlanningPhase = (plan.behaviorResponsibilities ?? []).some((behavior) => behavior.type === "resolution") ? "resolution" : "execution";
|
|
7168
|
+
if (plan.planningPhase !== expectedPlanningPhase) errors.push(`planningPhase must be ${expectedPlanningPhase}`);
|
|
7169
|
+
const activeBehaviors = (plan.behaviorResponsibilities ?? []).filter((behavior) => expectedPlanningPhase === "execution" || behavior.type === "resolution");
|
|
7170
|
+
const activeBehaviorIds = new Set(activeBehaviors.map((behavior) => behavior.id));
|
|
7171
|
+
if (expectedPlanningPhase === "resolution" && (plan.tasks ?? []).some((task) => task.type !== "resolution")) {
|
|
7172
|
+
errors.push("resolution phase must materialize only Resolution Tasks; Coding Work Items are assembled after Resolution is cleared");
|
|
7173
|
+
}
|
|
7174
|
+
if (expectedPlanningPhase === "execution" && (plan.tasks ?? []).some((task) => task.type === "resolution")) {
|
|
7175
|
+
errors.push("execution phase must not contain Resolution Tasks");
|
|
7176
|
+
}
|
|
7177
|
+
if (expectedPlanningPhase === "resolution" && (plan.pageWorkPackages ?? []).length > 0) {
|
|
7178
|
+
errors.push("resolution phase must not materialize Coding Work Packages");
|
|
7179
|
+
}
|
|
7180
|
+
const behaviorOwners = /* @__PURE__ */ new Map();
|
|
7181
|
+
const behaviorSupporters = /* @__PURE__ */ new Map();
|
|
7182
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) for (const ref of behavior.primaryFactRefs ?? []) behaviorOwners.set(ref, [...behaviorOwners.get(ref) ?? [], behavior.id]);
|
|
7183
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) for (const ref of behavior.supportingFactRefs ?? []) behaviorSupporters.set(ref, [...behaviorSupporters.get(ref) ?? [], behavior.id]);
|
|
6372
7184
|
const taskIds = /* @__PURE__ */ new Set();
|
|
6373
7185
|
for (const task of plan.tasks ?? []) {
|
|
6374
7186
|
if (!/^task:[a-f0-9]{16}$/.test(task.id)) errors.push(`invalid task id ${task.id}`);
|
|
6375
7187
|
if (taskIds.has(task.id)) errors.push(`duplicate task id ${task.id}`);
|
|
6376
7188
|
else taskIds.add(task.id);
|
|
6377
7189
|
if (!task.title?.trim()) errors.push(`${task.id}: title is required`);
|
|
7190
|
+
if (expectedPlanningPhase === "resolution" && task.workPackageId !== null) errors.push(`${task.id}: Resolution Task must not belong to a Work Package`);
|
|
7191
|
+
if (expectedPlanningPhase === "execution" && !task.workPackageId?.trim()) errors.push(`${task.id}: Coding Work Item requires workPackageId`);
|
|
7192
|
+
if (!task.primaryModule?.trim()) errors.push(`${task.id}: primaryModule is required`);
|
|
7193
|
+
if (!task.behaviorIds?.length) errors.push(`${task.id}: behaviorIds must not be empty`);
|
|
7194
|
+
if (expectedPlanningPhase === "resolution" && task.behaviorIds?.length !== 1) errors.push(`${task.id}: Resolution Task must project exactly one Resolution Behavior`);
|
|
7195
|
+
for (const behaviorId of task.behaviorIds ?? []) if (!behaviorIds.has(behaviorId)) errors.push(`${task.id}: unknown behavior ${behaviorId}`);
|
|
6378
7196
|
if (!task.primaryFactRefs?.length) errors.push(`${task.id}: primaryFactRefs must not be empty`);
|
|
6379
7197
|
if (!task.acceptance?.observableOutcome?.trim()) errors.push(`${task.id}: observableOutcome is required`);
|
|
6380
7198
|
if (!task.acceptance?.verificationRequirements?.length) errors.push(`${task.id}: verificationRequirements must not be empty`);
|
|
6381
|
-
const overBudget = task.estimate.factCount >
|
|
6382
|
-
|
|
6383
|
-
if (overBudget
|
|
6384
|
-
|
|
7199
|
+
const overBudget = task.estimate.behaviorCount > DEFAULT_EXECUTION_BUDGET.maxBehaviorCount || task.estimate.observableOutcomeCount > DEFAULT_EXECUTION_BUDGET.maxObservableOutcomeCount || task.estimate.productionFileCount > DEFAULT_EXECUTION_BUDGET.maxProductionFileCount || task.estimate.testFileCount > DEFAULT_EXECUTION_BUDGET.maxTestFileCount || task.estimate.capabilityCount > DEFAULT_EXECUTION_BUDGET.maxCapabilityCount || task.estimate.factCount > DEFAULT_EXECUTION_BUDGET.maxFactCount || task.estimate.sourceFileCount > DEFAULT_EXECUTION_BUDGET.maxSourceFileCount || task.estimate.traceNodeCount > DEFAULT_EXECUTION_BUDGET.maxTraceNodeCount || task.estimate.estimatedFactTokens > DEFAULT_EXECUTION_BUDGET.maxEstimatedFactTokens;
|
|
7200
|
+
if (overBudget && (task.behaviorIds?.length ?? 0) > 1) errors.push(`${task.id}: merged Coding Work Item exceeds the execution budget`);
|
|
7201
|
+
else if (overBudget) warnings.push(`${task.id}: single-Behavior ${task.type === "resolution" ? "Resolution Task" : "Coding Work Item"} exceeds a soft execution budget; responsibility is preserved and source context must be paged`);
|
|
7202
|
+
const owned = task.writeOwnership?.ownedFiles ?? [];
|
|
7203
|
+
const readOnly = task.writeOwnership?.readOnlyFiles ?? [];
|
|
7204
|
+
const forbidden = task.writeOwnership?.forbiddenGlobalFiles ?? [];
|
|
7205
|
+
for (const file of owned) if (readOnly.includes(file)) errors.push(`${task.id}: ${file} cannot be both owned and read-only`);
|
|
7206
|
+
for (const file of owned) if (forbidden.includes(file)) errors.push(`${task.id}: ${file} cannot be both owned and forbidden`);
|
|
7207
|
+
if (!(task.writeOwnership?.exclusiveScopes?.length || owned.length)) errors.push(`${task.id}: write ownership requires ownedFiles or an exclusive scope`);
|
|
7208
|
+
if (task.readyGate?.writeOwnershipResolved !== Boolean(task.writeOwnership?.exclusiveScopes?.length || owned.length)) errors.push(`${task.id}: writeOwnershipResolved gate disagrees with the contract`);
|
|
7209
|
+
if (task.readyGate?.semanticsResolved !== (task.type === "resolution" || task.closure.unresolvedRefs.length === 0)) errors.push(`${task.id}: semanticsResolved gate disagrees with unresolved refs`);
|
|
7210
|
+
if (task.readyGate?.contextWithinBudget !== (!overBudget || (task.behaviorIds?.length ?? 0) === 1)) errors.push(`${task.id}: contextWithinBudget gate disagrees with execution estimate`);
|
|
6385
7211
|
}
|
|
6386
7212
|
for (const task of plan.tasks ?? []) for (const dependency of task.dependsOnTaskIds ?? []) {
|
|
6387
7213
|
if (!taskIds.has(dependency)) errors.push(`${task.id}: unknown dependency ${dependency}`);
|
|
6388
7214
|
if (dependency === task.id) errors.push(`${task.id}: task cannot depend on itself`);
|
|
6389
7215
|
}
|
|
7216
|
+
for (const task of plan.tasks ?? []) {
|
|
7217
|
+
const interfaceProviders = (task.dependencyInterfaces ?? []).map((item) => item.providerTaskId).filter((id) => Boolean(id)).sort();
|
|
7218
|
+
if (JSON.stringify(interfaceProviders) !== JSON.stringify([...task.dependsOnTaskIds ?? []].sort())) errors.push(`${task.id}: dependencyInterfaces do not match dependsOnTaskIds`);
|
|
7219
|
+
for (const dependencyInterface of task.dependencyInterfaces ?? []) {
|
|
7220
|
+
if (dependencyInterface.providerTaskId && !taskIds.has(dependencyInterface.providerTaskId)) errors.push(`${task.id}: dependency Interface references unknown provider ${dependencyInterface.providerTaskId}`);
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
6390
7223
|
const cycle = findCycle(plan);
|
|
6391
7224
|
if (cycle) errors.push(`task dependency cycle: ${cycle.join(" -> ")}`);
|
|
6392
7225
|
const owners = /* @__PURE__ */ new Map();
|
|
@@ -6398,14 +7231,20 @@ function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
|
6398
7231
|
supporters.set(ref, [...supporters.get(ref) ?? [], task.id]);
|
|
6399
7232
|
}
|
|
6400
7233
|
}
|
|
6401
|
-
const
|
|
6402
|
-
|
|
7234
|
+
const activePrimaryFactRefs = uniqueSorted(activeBehaviors.flatMap((behavior) => behavior.primaryFactRefs));
|
|
7235
|
+
const behaviorOrphanFactRefs = facts.requiredFactRefs.filter((ref) => (behaviorOwners.get(ref) ?? []).length === 0);
|
|
7236
|
+
const activeTaskOrphanFactRefs = activePrimaryFactRefs.filter((ref) => (owners.get(ref) ?? []).length === 0);
|
|
7237
|
+
const orphanFactRefs = uniqueSorted([
|
|
7238
|
+
...behaviorOrphanFactRefs,
|
|
7239
|
+
...activeTaskOrphanFactRefs.filter((ref) => facts.requiredFactRefs.includes(ref))
|
|
7240
|
+
]);
|
|
7241
|
+
for (const ref of activeTaskOrphanFactRefs) errors.push(`active Behavior fact has no primary Task owner: ${ref}`);
|
|
6403
7242
|
for (const [ref, ownerIds] of owners) if (ownerIds.length > 1) errors.push(`fact has multiple primary owners: ${ref} -> ${ownerIds.join(", ")}`);
|
|
6404
7243
|
for (const [ref, ownerId] of Object.entries(plan.ownership?.primaryOwnerByFactRef ?? {})) {
|
|
6405
7244
|
if (!taskIds.has(ownerId)) errors.push(`ownership ${ref} references unknown task ${ownerId}`);
|
|
6406
7245
|
if (!(owners.get(ref) ?? []).includes(ownerId)) errors.push(`ownership map disagrees with task primaryFactRefs for ${ref}`);
|
|
6407
7246
|
}
|
|
6408
|
-
for (const ref of
|
|
7247
|
+
for (const ref of activePrimaryFactRefs) if (plan.ownership?.primaryOwnerByFactRef?.[ref] !== owners.get(ref)?.[0]) errors.push(`active Task ownership map is missing or incorrect for ${ref}`);
|
|
6409
7248
|
for (const [ref, taskIdsForRef] of Object.entries(plan.ownership?.supportingTaskIdsByFactRef ?? {})) {
|
|
6410
7249
|
const expected = [...supporters.get(ref) ?? []].sort();
|
|
6411
7250
|
const actual = [...taskIdsForRef ?? []].sort();
|
|
@@ -6414,6 +7253,125 @@ function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
|
6414
7253
|
for (const [ref, expectedIds] of supporters) {
|
|
6415
7254
|
if (!(ref in (plan.ownership?.supportingTaskIdsByFactRef ?? {}))) errors.push(`supporting ownership map is missing for ${ref}: ${[...expectedIds].sort().join(", ")}`);
|
|
6416
7255
|
}
|
|
7256
|
+
const behaviorMembership = /* @__PURE__ */ new Map();
|
|
7257
|
+
for (const task of plan.tasks ?? []) for (const behaviorId of task.behaviorIds ?? []) behaviorMembership.set(behaviorId, [...behaviorMembership.get(behaviorId) ?? [], task.id]);
|
|
7258
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) {
|
|
7259
|
+
const ownersForBehavior = behaviorMembership.get(behavior.id) ?? [];
|
|
7260
|
+
const expectedMembership = activeBehaviorIds.has(behavior.id) ? 1 : 0;
|
|
7261
|
+
if (ownersForBehavior.length !== expectedMembership) errors.push(`${behavior.id}: must belong to ${expectedMembership} ${expectedPlanningPhase === "resolution" ? "Resolution Task" : "Coding Work Item"}, found ${ownersForBehavior.length}`);
|
|
7262
|
+
const task = plan.tasks.find((item) => item.id === ownersForBehavior[0]);
|
|
7263
|
+
if (!task) continue;
|
|
7264
|
+
for (const ref of behavior.primaryFactRefs) if (!task.primaryFactRefs.includes(ref)) errors.push(`${task.id}: missing primary fact ${ref} from ${behavior.id}`);
|
|
7265
|
+
for (const ref of behavior.closure.entryRefs) if (!task.closure.entryRefs.includes(ref)) errors.push(`${task.id}: missing entry ${ref} from ${behavior.id}`);
|
|
7266
|
+
for (const ref of behavior.closure.interactionRefs) if (!task.closure.interactionRefs.includes(ref)) errors.push(`${task.id}: missing interaction ${ref} from ${behavior.id}`);
|
|
7267
|
+
}
|
|
7268
|
+
const behaviorById = new Map((plan.behaviorResponsibilities ?? []).map((behavior) => [behavior.id, behavior]));
|
|
7269
|
+
for (const task of plan.tasks ?? []) {
|
|
7270
|
+
const members = (task.behaviorIds ?? []).map((id) => behaviorById.get(id)).filter((item) => Boolean(item));
|
|
7271
|
+
const expectedPrimary = uniqueSorted(members.flatMap((item) => item.primaryFactRefs));
|
|
7272
|
+
const expectedSupporting = uniqueSorted(members.flatMap((item) => item.supportingFactRefs).filter((ref) => !expectedPrimary.includes(ref)));
|
|
7273
|
+
if (JSON.stringify(expectedPrimary) !== JSON.stringify([...task.primaryFactRefs ?? []].sort())) errors.push(`${task.id}: primaryFactRefs disagree with member Behaviors`);
|
|
7274
|
+
if (JSON.stringify(expectedSupporting) !== JSON.stringify([...task.supportingFactRefs ?? []].sort())) errors.push(`${task.id}: supportingFactRefs disagree with member Behaviors`);
|
|
7275
|
+
for (const key of ["entryRefs", "surfaceRefs", "controlRefs", "interactionRefs", "unresolvedRefs", "warningRefs"]) {
|
|
7276
|
+
const expected = uniqueSorted(members.flatMap((item) => item.closure[key]));
|
|
7277
|
+
if (JSON.stringify(expected) !== JSON.stringify([...task.closure?.[key] ?? []].sort())) errors.push(`${task.id}: closure.${key} disagrees with member Behaviors`);
|
|
7278
|
+
}
|
|
7279
|
+
const expectedOutcomes = uniqueSorted(members.map((item) => item.acceptance.observableOutcome));
|
|
7280
|
+
const expectedRequired = uniqueSorted(members.flatMap((item) => item.acceptance.requiredFactRefs));
|
|
7281
|
+
const expectedReports = uniqueSorted(members.flatMap((item) => item.acceptance.reportRefs));
|
|
7282
|
+
if (JSON.stringify(expectedOutcomes) !== JSON.stringify([...task.acceptance?.observableOutcomes ?? []].sort())) errors.push(`${task.id}: observableOutcomes disagree with member Behaviors`);
|
|
7283
|
+
if (JSON.stringify(expectedRequired) !== JSON.stringify([...task.acceptance?.requiredFactRefs ?? []].sort())) errors.push(`${task.id}: requiredFactRefs disagree with member Behaviors`);
|
|
7284
|
+
if (JSON.stringify(expectedReports) !== JSON.stringify([...task.acceptance?.reportRefs ?? []].sort())) errors.push(`${task.id}: reportRefs disagree with member Behaviors`);
|
|
7285
|
+
if (members.some((member) => member.type !== task.type)) errors.push(`${task.id}: task type disagrees with a member Behavior`);
|
|
7286
|
+
}
|
|
7287
|
+
for (const ref of facts.requiredFactRefs) {
|
|
7288
|
+
const expectedBehavior = behaviorOwners.get(ref)?.[0];
|
|
7289
|
+
if (!expectedBehavior || behaviorOwners.get(ref)?.length !== 1) errors.push(`required fact must have exactly one Behavior owner: ${ref}`);
|
|
7290
|
+
if (plan.behaviorOwnership?.primaryOwnerByFactRef?.[ref] !== expectedBehavior) errors.push(`behavior ownership map is missing or incorrect for ${ref}`);
|
|
7291
|
+
}
|
|
7292
|
+
for (const [ref, behaviorId] of Object.entries(plan.behaviorOwnership?.primaryOwnerByFactRef ?? {})) {
|
|
7293
|
+
if (!behaviorIds.has(behaviorId)) errors.push(`behavior ownership ${ref} references unknown Behavior ${behaviorId}`);
|
|
7294
|
+
if (!(behaviorOwners.get(ref) ?? []).includes(behaviorId)) errors.push(`behavior ownership map disagrees with Behavior primaryFactRefs for ${ref}`);
|
|
7295
|
+
}
|
|
7296
|
+
for (const [ref, expectedIds] of behaviorSupporters) {
|
|
7297
|
+
const actual = [...plan.behaviorOwnership?.supportingBehaviorIdsByFactRef?.[ref] ?? []].sort();
|
|
7298
|
+
if (JSON.stringify(actual) !== JSON.stringify([...expectedIds].sort())) errors.push(`behavior supporting ownership map is missing or incorrect for ${ref}`);
|
|
7299
|
+
}
|
|
7300
|
+
for (const [ref, actualIds] of Object.entries(plan.behaviorOwnership?.supportingBehaviorIdsByFactRef ?? {})) {
|
|
7301
|
+
const expected = [...behaviorSupporters.get(ref) ?? []].sort();
|
|
7302
|
+
if (JSON.stringify([...actualIds].sort()) !== JSON.stringify(expected)) errors.push(`behavior supporting ownership map disagrees with Behaviors for ${ref}`);
|
|
7303
|
+
}
|
|
7304
|
+
const packageIds = /* @__PURE__ */ new Set();
|
|
7305
|
+
const knownPackageIds = new Set((plan.pageWorkPackages ?? []).map((workPackage) => workPackage.id));
|
|
7306
|
+
const packageMembership = /* @__PURE__ */ new Map();
|
|
7307
|
+
const canonicalBySurface = canonicalPageRefs(facts);
|
|
7308
|
+
for (const workPackage of plan.pageWorkPackages ?? []) {
|
|
7309
|
+
if (!/^package:[a-f0-9]{16}$/.test(workPackage.id)) errors.push(`invalid work package id ${workPackage.id}`);
|
|
7310
|
+
if (packageIds.has(workPackage.id)) errors.push(`duplicate work package id ${workPackage.id}`);
|
|
7311
|
+
else packageIds.add(workPackage.id);
|
|
7312
|
+
for (const dependency of workPackage.dependsOnPackageIds ?? []) {
|
|
7313
|
+
if (!knownPackageIds.has(dependency)) errors.push(`${workPackage.id}: unknown Page Work Package dependency ${dependency}`);
|
|
7314
|
+
if (dependency === workPackage.id) errors.push(`${workPackage.id}: Page Work Package cannot depend on itself`);
|
|
7315
|
+
}
|
|
7316
|
+
for (const taskId2 of workPackage.workItemIds ?? []) {
|
|
7317
|
+
if (!taskIds.has(taskId2)) errors.push(`${workPackage.id}: unknown Coding Work Item ${taskId2}`);
|
|
7318
|
+
packageMembership.set(taskId2, [...packageMembership.get(taskId2) ?? [], workPackage.id]);
|
|
7319
|
+
}
|
|
7320
|
+
if ((workPackage.workItemIds ?? []).some((taskId2) => plan.tasks.find((task) => task.id === taskId2)?.type === "resolution")) errors.push(`${workPackage.id}: Resolution Tasks must not enter a Coding Work Package`);
|
|
7321
|
+
if (workPackage.canonicalPageRef !== null && !facts.surfaces.some((surface) => surface.id === workPackage.canonicalPageRef)) errors.push(`${workPackage.id}: unknown canonical page ${workPackage.canonicalPageRef}`);
|
|
7322
|
+
for (const surfaceRef of workPackage.surfaceRefs ?? []) {
|
|
7323
|
+
if (!facts.surfaces.some((surface) => surface.id === surfaceRef)) errors.push(`${workPackage.id}: unknown surface ${surfaceRef}`);
|
|
7324
|
+
if ((canonicalBySurface[surfaceRef] ?? surfaceRef) !== workPackage.canonicalPageRef) errors.push(`${workPackage.id}: ${surfaceRef} does not belong to canonical page ${workPackage.canonicalPageRef}`);
|
|
7325
|
+
}
|
|
7326
|
+
if (workPackage.serialLane) for (let index = 1; index < workPackage.workItemIds.length; index++) {
|
|
7327
|
+
if (!hasDependencyPath(plan, workPackage.workItemIds[index], workPackage.workItemIds[index - 1])) errors.push(`${workPackage.id}: serial lane is missing dependency ${workPackage.workItemIds[index]} -> ${workPackage.workItemIds[index - 1]}`);
|
|
7328
|
+
}
|
|
7329
|
+
const expectedScopes = [...new Set(plan.tasks.filter((task) => task.workPackageId === workPackage.id).flatMap((task) => task.writeOwnership?.exclusiveScopes ?? []))].sort();
|
|
7330
|
+
if (JSON.stringify([...workPackage.exclusiveScopes ?? []].sort()) !== JSON.stringify(expectedScopes)) errors.push(`${workPackage.id}: exclusiveScopes disagree with member Work Items`);
|
|
7331
|
+
const expectedDependencies = uniqueSorted((workPackage.workItemIds ?? []).flatMap((taskId2) => plan.tasks.find((task) => task.id === taskId2)?.dependsOnTaskIds ?? []).map((taskId2) => plan.tasks.find((task) => task.id === taskId2)?.workPackageId).filter((packageId) => Boolean(packageId && packageId !== workPackage.id)));
|
|
7332
|
+
if (JSON.stringify([...workPackage.dependsOnPackageIds ?? []].sort()) !== JSON.stringify(expectedDependencies)) errors.push(`${workPackage.id}: dependsOnPackageIds disagree with cross-package Work Item dependencies`);
|
|
7333
|
+
}
|
|
7334
|
+
const packageCycle = findDependencyCycle(new Map((plan.pageWorkPackages ?? []).map((workPackage) => [workPackage.id, workPackage.dependsOnPackageIds ?? []])));
|
|
7335
|
+
if (packageCycle) errors.push(`Page Work Package dependency cycle: ${packageCycle.join(" -> ")}`);
|
|
7336
|
+
for (const task of plan.tasks ?? []) {
|
|
7337
|
+
const memberships = packageMembership.get(task.id) ?? [];
|
|
7338
|
+
if (task.type === "resolution") {
|
|
7339
|
+
if (memberships.length > 0 || task.workPackageId !== null) errors.push(`${task.id}: Resolution Task must not have Work Package membership`);
|
|
7340
|
+
continue;
|
|
7341
|
+
}
|
|
7342
|
+
if (memberships.length !== 1 || memberships[0] !== task.workPackageId) errors.push(`${task.id}: work package membership is missing or inconsistent`);
|
|
7343
|
+
const workPackage = plan.pageWorkPackages?.find((item) => item.id === task.workPackageId);
|
|
7344
|
+
if (workPackage && workPackage.canonicalPageRef !== task.canonicalPageRef) errors.push(`${task.id}: canonicalPageRef disagrees with its Work Package`);
|
|
7345
|
+
}
|
|
7346
|
+
const conflictEdges = /* @__PURE__ */ new Map();
|
|
7347
|
+
for (const edge of plan.fileConflictEdges ?? []) {
|
|
7348
|
+
if (!taskIds.has(edge.leftTaskId) || !taskIds.has(edge.rightTaskId)) errors.push(`file conflict references an unknown task: ${edge.leftTaskId}, ${edge.rightTaskId}`);
|
|
7349
|
+
if (edge.leftTaskId === edge.rightTaskId) errors.push(`${edge.leftTaskId}: file conflict cannot reference itself`);
|
|
7350
|
+
if (!edge.reasons?.length) errors.push(`${edge.leftTaskId}/${edge.rightTaskId}: file conflict requires a reason`);
|
|
7351
|
+
const key = [edge.leftTaskId, edge.rightTaskId].sort().join(":");
|
|
7352
|
+
if (conflictEdges.has(key)) errors.push(`duplicate file conflict edge: ${key}`);
|
|
7353
|
+
else conflictEdges.set(key, edge);
|
|
7354
|
+
}
|
|
7355
|
+
for (let leftIndex = 0; leftIndex < (plan.tasks ?? []).length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < plan.tasks.length; rightIndex++) {
|
|
7356
|
+
const left = plan.tasks[leftIndex];
|
|
7357
|
+
const right = plan.tasks[rightIndex];
|
|
7358
|
+
const leftOwned = left.writeOwnership?.ownedFiles ?? [];
|
|
7359
|
+
const rightOwned = right.writeOwnership?.ownedFiles ?? [];
|
|
7360
|
+
const leftScopes = left.writeOwnership?.exclusiveScopes ?? [];
|
|
7361
|
+
const rightScopes = right.writeOwnership?.exclusiveScopes ?? [];
|
|
7362
|
+
const overlappingFiles = leftOwned.filter((file) => rightOwned.includes(file));
|
|
7363
|
+
const overlappingScopes = leftScopes.filter((scope) => rightScopes.includes(scope));
|
|
7364
|
+
const hasConflict = overlappingFiles.length > 0 || overlappingScopes.length > 0;
|
|
7365
|
+
const edge = conflictEdges.get([left.id, right.id].sort().join(":"));
|
|
7366
|
+
if (hasConflict && !edge) errors.push(`${left.id} and ${right.id}: overlapping write ownership requires a conflict edge`);
|
|
7367
|
+
if (!hasConflict && edge) errors.push(`${left.id} and ${right.id}: file conflict edge has no overlapping write ownership`);
|
|
7368
|
+
if (edge) {
|
|
7369
|
+
const expectedReasons = [...overlappingFiles.map((file) => `owned-file:${file}`), ...overlappingScopes.map((scope) => `exclusive-scope:${scope}`)].sort();
|
|
7370
|
+
if (JSON.stringify([...edge.reasons].sort()) !== JSON.stringify(expectedReasons)) errors.push(`${left.id} and ${right.id}: file conflict reasons disagree with write ownership`);
|
|
7371
|
+
}
|
|
7372
|
+
if (hasConflict && !(hasDependencyPath(plan, left.id, right.id) || hasDependencyPath(plan, right.id, left.id))) errors.push(`${left.id} and ${right.id}: file conflict is not serialized`);
|
|
7373
|
+
if ((left.conflictsWithTaskIds ?? []).includes(right.id) !== hasConflict || (right.conflictsWithTaskIds ?? []).includes(left.id) !== hasConflict) errors.push(`${left.id} and ${right.id}: conflictsWithTaskIds disagrees with write ownership`);
|
|
7374
|
+
}
|
|
6417
7375
|
const knownRefs = /* @__PURE__ */ new Set([...facts.requiredFactRefs, ...facts.issues.map((issue) => issue.id)]);
|
|
6418
7376
|
const entryRefs = new Set(facts.entries.map((entry) => entry.id));
|
|
6419
7377
|
const surfaceRefs = new Set(facts.surfaces.map((surface) => surface.id));
|
|
@@ -6447,16 +7405,16 @@ function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
|
6447
7405
|
}
|
|
6448
7406
|
|
|
6449
7407
|
// src/planning/runtime.ts
|
|
6450
|
-
function
|
|
7408
|
+
function readJson7(file) {
|
|
6451
7409
|
try {
|
|
6452
|
-
return JSON.parse(
|
|
7410
|
+
return JSON.parse(fs28.readFileSync(file, "utf8"));
|
|
6453
7411
|
} catch {
|
|
6454
7412
|
return null;
|
|
6455
7413
|
}
|
|
6456
7414
|
}
|
|
6457
7415
|
function reportItems(projectRoot) {
|
|
6458
7416
|
const root = path34.join(migrationDirectory(projectRoot), "report");
|
|
6459
|
-
const index =
|
|
7417
|
+
const index = readJson7(path34.join(root, "index.json"));
|
|
6460
7418
|
const out = /* @__PURE__ */ new Map();
|
|
6461
7419
|
if (!index) return out;
|
|
6462
7420
|
const files = [
|
|
@@ -6466,7 +7424,7 @@ function reportItems(projectRoot) {
|
|
|
6466
7424
|
index.discoveriesFile
|
|
6467
7425
|
].filter((value) => typeof value === "string");
|
|
6468
7426
|
for (const relative2 of files) {
|
|
6469
|
-
const document =
|
|
7427
|
+
const document = readJson7(path34.join(root, relative2));
|
|
6470
7428
|
if (!document) continue;
|
|
6471
7429
|
const values = [document.page, ...document.controls ?? [], ...document.entries ?? [], ...document.interactions ?? [], ...document.items ?? []].filter(Boolean);
|
|
6472
7430
|
for (const item of values) if (typeof item.ref === "string") out.set(item.ref, { ref: item.ref, status: item.status ?? "pending", testEvidence: Array.isArray(item.testEvidence) ? item.testEvidence : [] });
|
|
@@ -6477,7 +7435,7 @@ function managedReceipt(projectRoot, reference) {
|
|
|
6477
7435
|
const managedRoot = path34.resolve(migrationDirectory(projectRoot), "evidence", "managed");
|
|
6478
7436
|
const absolute = reference.replaceAll("\\", "/").startsWith(".migration/") ? path34.resolve(migrationWorkspaceRoot(projectRoot), reference) : path34.resolve(projectRoot, reference);
|
|
6479
7437
|
if (absolute !== managedRoot && !absolute.startsWith(`${managedRoot}${path34.sep}`)) return null;
|
|
6480
|
-
return
|
|
7438
|
+
return readJson7(absolute);
|
|
6481
7439
|
}
|
|
6482
7440
|
function freshTaskEvidence(projectRoot, reference, factsSha256, harmonyFingerprint) {
|
|
6483
7441
|
const receipt = managedReceipt(projectRoot, reference);
|
|
@@ -6503,7 +7461,24 @@ function stateReasons(task, state, dependencyStates) {
|
|
|
6503
7461
|
})(),
|
|
6504
7462
|
task.estimate.downstreamUnlockCount > 0 ? `\u5B8C\u6210\u540E\u53EF\u89E3\u9664 ${task.estimate.downstreamUnlockCount} \u4E2A\u4E0B\u6E38\u4EFB\u52A1` : null
|
|
6505
7463
|
]);
|
|
6506
|
-
if (state === "blocked")
|
|
7464
|
+
if (state === "blocked") {
|
|
7465
|
+
const gate = task.readyGate ?? {
|
|
7466
|
+
semanticsResolved: false,
|
|
7467
|
+
dependencyInterfacesStable: false,
|
|
7468
|
+
writeOwnershipResolved: false,
|
|
7469
|
+
contextWithinBudget: false,
|
|
7470
|
+
acceptanceExecutable: false
|
|
7471
|
+
};
|
|
7472
|
+
return uniqueSorted([
|
|
7473
|
+
dependencyStates.length ? "\u4F9D\u8D56\u4EFB\u52A1\u5C1A\u672A\u53D6\u5F97\u65B0\u9C9C\u7684\u5B9A\u5411\u9A8C\u8BC1\u8BC1\u636E" : null,
|
|
7474
|
+
!gate.semanticsResolved ? "\u4ECD\u6709\u672A\u88C1\u51B3\u8BED\u4E49\u95EE\u9898" : null,
|
|
7475
|
+
!gate.dependencyInterfacesStable ? "\u4E0A\u6E38 Module Interface \u5C1A\u672A\u7A33\u5B9A" : null,
|
|
7476
|
+
!gate.writeOwnershipResolved ? "\u5199\u5165\u6587\u4EF6\u6216\u72EC\u5360\u8303\u56F4\u5C1A\u672A\u5206\u914D" : null,
|
|
7477
|
+
!gate.contextWithinBudget ? "\u5185\u90E8 Work Item \u95ED\u5305\u8D85\u51FA\u5F53\u524D\u6267\u884C\u9884\u7B97" : null,
|
|
7478
|
+
!gate.acceptanceExecutable ? "\u9A8C\u6536\u6761\u4EF6\u5F53\u524D\u4E0D\u53EF\u6267\u884C" : null,
|
|
7479
|
+
!dependencyStates.length && Object.values(gate).every(Boolean) ? "\u4EFB\u52A1\u5BF9\u5E94\u7684\u62A5\u544A\u9879\u6216\u8BA1\u5212\u6821\u9A8C\u4ECD\u5904\u4E8E\u963B\u585E\u72B6\u6001" : null
|
|
7480
|
+
]);
|
|
7481
|
+
}
|
|
6507
7482
|
if (state === "implemented") return ["\u5B9E\u73B0\u6620\u5C04\u5DF2\u5B58\u5728\uFF0C\u4F46\u5B9A\u5411\u8BBE\u5907\u9A8C\u8BC1\u5C1A\u672A\u5B8C\u6210"];
|
|
6508
7483
|
if (state === "stale") return ["\u62A5\u544A\u58F0\u660E\u7684\u6D4B\u8BD5\u8BC1\u636E\u4E0E\u5F53\u524D Facts \u6216 HarmonyOS \u6E90\u7801\u4E0D\u4E00\u81F4"];
|
|
6509
7484
|
if (state === "locally-verified") return ["\u5F53\u524D\u4EFB\u52A1\u5DF2\u6709\u65B0\u9C9C\u5B9A\u5411\u8BBE\u5907\u8BC1\u636E"];
|
|
@@ -6512,25 +7487,72 @@ function stateReasons(task, state, dependencyStates) {
|
|
|
6512
7487
|
}
|
|
6513
7488
|
function rankReady(tasks, states) {
|
|
6514
7489
|
const priority = (task) => {
|
|
6515
|
-
if (task.type === "resolution")
|
|
6516
|
-
const blocksLauncher = task.closure.unresolvedRefs.some((ref) => ref.includes("launcher"));
|
|
6517
|
-
return blocksLauncher || task.estimate.downstreamUnlockCount > 0 ? 0 : 5;
|
|
6518
|
-
}
|
|
7490
|
+
if (task.type === "resolution") return 0;
|
|
6519
7491
|
const order = { bootstrap: 1, journey: 2, capability: 3, assembly: 4, regression: 6 };
|
|
6520
7492
|
return order[task.type] ?? 6;
|
|
6521
7493
|
};
|
|
6522
7494
|
return tasks.filter((task) => states.get(task.id) === "ready").sort((a, b) => priority(a) - priority(b) || b.estimate.downstreamUnlockCount - a.estimate.downstreamUnlockCount || a.estimate.estimatedFactTokens - b.estimate.estimatedFactTokens || a.id.localeCompare(b.id));
|
|
6523
7495
|
}
|
|
7496
|
+
function completedState(state) {
|
|
7497
|
+
return ["locally-verified", "globally-verified"].includes(state);
|
|
7498
|
+
}
|
|
7499
|
+
function aggregatePackageOwnState(states) {
|
|
7500
|
+
if (states.length > 0 && states.every((state) => state === "globally-verified")) return "globally-verified";
|
|
7501
|
+
if (states.length > 0 && states.every(completedState)) return "locally-verified";
|
|
7502
|
+
if (states.some((state) => state === "blocked")) return "blocked";
|
|
7503
|
+
if (states.some((state) => state === "stale")) return "stale";
|
|
7504
|
+
if (states.length > 0 && states.every((state) => ["implemented", "locally-verified", "globally-verified"].includes(state))) return "implemented";
|
|
7505
|
+
return "pending";
|
|
7506
|
+
}
|
|
7507
|
+
function packageStateReasons(workPackage, state, dependencyStates, ownStates) {
|
|
7508
|
+
if (state === "ready") return [
|
|
7509
|
+
`\u9875\u9762\u5305\u5C06\u5728\u4E00\u6B21\u5B9E\u73B0 epoch \u5185\u6309\u51BB\u7ED3\u987A\u5E8F\u5B8C\u6210 ${workPackage.workItemIds.length} \u4E2A Work Item`,
|
|
7510
|
+
"\u8DE8\u9875\u9762\u4F9D\u8D56\u5DF2\u6EE1\u8DB3\uFF0C\u53EF\u52A0\u8F7D\u9875\u9762\u7EA7\u6A21\u578B\u8F93\u5165"
|
|
7511
|
+
];
|
|
7512
|
+
if (state === "blocked") return uniqueSorted([
|
|
7513
|
+
dependencyStates.some((dependency) => !completedState(dependency)) ? "\u524D\u7F6E Page Work Package \u5C1A\u672A\u53D6\u5F97\u65B0\u9C9C\u7684\u5B9A\u5411\u9A8C\u8BC1\u8BC1\u636E" : null,
|
|
7514
|
+
ownStates.some((own) => own === "blocked") ? "\u9875\u9762\u5305\u5185\u5B58\u5728\u88AB\u62A5\u544A\u6807\u8BB0\u4E3A blocked \u7684\u884C\u4E3A" : null,
|
|
7515
|
+
!dependencyStates.length && !ownStates.some((own) => own === "blocked") ? "\u9875\u9762\u5305\u7684\u9759\u6001 Ready Gate \u672A\u5168\u90E8\u901A\u8FC7" : null
|
|
7516
|
+
]);
|
|
7517
|
+
if (state === "implemented") return ["\u9875\u9762\u5305\u5185\u5168\u90E8\u884C\u4E3A\u5DF2\u6709\u5B9E\u73B0\u6620\u5C04\uFF0C\u4F46\u4ECD\u7F3A\u5C11\u5B8C\u6574\u7684\u5B9A\u5411\u8BBE\u5907\u9A8C\u8BC1"];
|
|
7518
|
+
if (state === "stale") return ["\u9875\u9762\u5305\u5185\u81F3\u5C11\u4E00\u9879\u9A8C\u8BC1\u8BC1\u636E\u4E0E\u5F53\u524D Facts \u6216 HarmonyOS \u6E90\u7801\u4E0D\u4E00\u81F4"];
|
|
7519
|
+
if (state === "locally-verified") return ["\u9875\u9762\u5305\u5185\u5168\u90E8\u884C\u4E3A\u5747\u6709\u65B0\u9C9C\u5B9A\u5411\u8BBE\u5907\u8BC1\u636E"];
|
|
7520
|
+
if (state === "globally-verified") return ["\u9875\u9762\u5305\u5185\u5168\u90E8\u884C\u4E3A\u53CA\u5168\u91CF\u56DE\u5F52\u8BC1\u636E\u5747\u65B0\u9C9C"];
|
|
7521
|
+
return [];
|
|
7522
|
+
}
|
|
7523
|
+
function packageDownstreamCounts(packages) {
|
|
7524
|
+
const children = /* @__PURE__ */ new Map();
|
|
7525
|
+
for (const workPackage of packages) for (const dependency of workPackage.dependsOnPackageIds) children.set(dependency, [...children.get(dependency) ?? [], workPackage.id]);
|
|
7526
|
+
const result = /* @__PURE__ */ new Map();
|
|
7527
|
+
for (const workPackage of packages) {
|
|
7528
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7529
|
+
const queue = [...children.get(workPackage.id) ?? []];
|
|
7530
|
+
while (queue.length) {
|
|
7531
|
+
const next = queue.shift();
|
|
7532
|
+
if (seen.has(next)) continue;
|
|
7533
|
+
seen.add(next);
|
|
7534
|
+
queue.push(...children.get(next) ?? []);
|
|
7535
|
+
}
|
|
7536
|
+
result.set(workPackage.id, seen.size);
|
|
7537
|
+
}
|
|
7538
|
+
return result;
|
|
7539
|
+
}
|
|
6524
7540
|
function emptyTaskPlanSnapshot(error) {
|
|
6525
7541
|
return {
|
|
6526
7542
|
present: false,
|
|
6527
7543
|
valid: false,
|
|
6528
7544
|
fresh: false,
|
|
6529
7545
|
taskPlanSha256: null,
|
|
7546
|
+
planningPhase: null,
|
|
6530
7547
|
totalTasks: 0,
|
|
7548
|
+
resolutionTasks: 0,
|
|
7549
|
+
totalTargets: 0,
|
|
7550
|
+
readyTargets: 0,
|
|
7551
|
+
completedTargets: 0,
|
|
6531
7552
|
readyTasks: 0,
|
|
6532
7553
|
blockedTasks: 0,
|
|
6533
7554
|
completedTasks: 0,
|
|
7555
|
+
recommendedTargetId: null,
|
|
6534
7556
|
recommendedTaskId: null,
|
|
6535
7557
|
alternatives: [],
|
|
6536
7558
|
reasons: {},
|
|
@@ -6539,7 +7561,8 @@ function emptyTaskPlanSnapshot(error) {
|
|
|
6539
7561
|
orphanFactRefs: [],
|
|
6540
7562
|
errors: error ? [error] : [],
|
|
6541
7563
|
warnings: [],
|
|
6542
|
-
tasks: []
|
|
7564
|
+
tasks: [],
|
|
7565
|
+
targets: []
|
|
6543
7566
|
};
|
|
6544
7567
|
}
|
|
6545
7568
|
function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
@@ -6548,6 +7571,7 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6548
7571
|
if (!plan) return emptyTaskPlanSnapshot();
|
|
6549
7572
|
const validation = validateTaskPlan(plan, facts, source.androidFingerprint);
|
|
6550
7573
|
const reports = reportItems(projectRoot);
|
|
7574
|
+
const ownStates = new Map(plan.tasks.map((task) => [task.id, initialState(task, reports, projectRoot, plan, source)]));
|
|
6551
7575
|
const states = /* @__PURE__ */ new Map();
|
|
6552
7576
|
const resolving = /* @__PURE__ */ new Set();
|
|
6553
7577
|
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
@@ -6557,12 +7581,13 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6557
7581
|
if (!validation.fresh) return "stale";
|
|
6558
7582
|
if (resolving.has(task.id)) return "blocked";
|
|
6559
7583
|
resolving.add(task.id);
|
|
6560
|
-
const own =
|
|
7584
|
+
const own = ownStates.get(task.id);
|
|
6561
7585
|
let state = own;
|
|
6562
7586
|
if (own === "pending") {
|
|
6563
7587
|
const dependencyStates = task.dependsOnTaskIds.map((id) => byId.get(id)).filter((item) => Boolean(item)).map(resolveState);
|
|
6564
7588
|
const satisfied = dependencyStates.every((value) => ["locally-verified", "globally-verified"].includes(value));
|
|
6565
|
-
|
|
7589
|
+
const readyGatePassed = Object.values(task.readyGate).every(Boolean);
|
|
7590
|
+
state = satisfied && readyGatePassed ? "ready" : "blocked";
|
|
6566
7591
|
}
|
|
6567
7592
|
resolving.delete(task.id);
|
|
6568
7593
|
states.set(task.id, state);
|
|
@@ -6575,8 +7600,59 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6575
7600
|
const dependencyStates = task.dependsOnTaskIds.map((id) => states.get(id)).filter(Boolean);
|
|
6576
7601
|
return { taskId: task.id, state, reasons: stateReasons(task, state, dependencyStates) };
|
|
6577
7602
|
});
|
|
6578
|
-
|
|
6579
|
-
|
|
7603
|
+
let targets;
|
|
7604
|
+
let orderedReadyTargetIds;
|
|
7605
|
+
if (plan.planningPhase === "resolution") {
|
|
7606
|
+
targets = runtimeItems.map((item) => ({ targetId: item.taskId, kind: "resolution-task", state: item.state, reasons: item.reasons, workItemIds: [item.taskId] }));
|
|
7607
|
+
orderedReadyTargetIds = ready.map((task) => task.id);
|
|
7608
|
+
} else {
|
|
7609
|
+
const packageById = new Map(plan.pageWorkPackages.map((workPackage) => [workPackage.id, workPackage]));
|
|
7610
|
+
const packageStates = /* @__PURE__ */ new Map();
|
|
7611
|
+
const packageResolving = /* @__PURE__ */ new Set();
|
|
7612
|
+
const resolvePackageState = (workPackage) => {
|
|
7613
|
+
if (packageStates.has(workPackage.id)) return packageStates.get(workPackage.id);
|
|
7614
|
+
if (!validation.valid) return "blocked";
|
|
7615
|
+
if (!validation.fresh) return "stale";
|
|
7616
|
+
if (packageResolving.has(workPackage.id)) return "blocked";
|
|
7617
|
+
packageResolving.add(workPackage.id);
|
|
7618
|
+
const memberTasks = workPackage.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
7619
|
+
const memberOwnStates = memberTasks.map((task) => ownStates.get(task.id));
|
|
7620
|
+
const own = aggregatePackageOwnState(memberOwnStates);
|
|
7621
|
+
let state = own;
|
|
7622
|
+
if (own === "pending") {
|
|
7623
|
+
const dependencyStates = workPackage.dependsOnPackageIds.map((id) => packageById.get(id)).filter((item) => Boolean(item)).map(resolvePackageState);
|
|
7624
|
+
const dependenciesSatisfied = dependencyStates.every(completedState);
|
|
7625
|
+
const readyGatePassed = memberTasks.every((task) => Object.values(task.readyGate).every(Boolean));
|
|
7626
|
+
state = dependenciesSatisfied && readyGatePassed ? "ready" : "blocked";
|
|
7627
|
+
}
|
|
7628
|
+
packageResolving.delete(workPackage.id);
|
|
7629
|
+
packageStates.set(workPackage.id, state);
|
|
7630
|
+
return state;
|
|
7631
|
+
};
|
|
7632
|
+
for (const workPackage of plan.pageWorkPackages) resolvePackageState(workPackage);
|
|
7633
|
+
targets = plan.pageWorkPackages.map((workPackage) => {
|
|
7634
|
+
const state = packageStates.get(workPackage.id);
|
|
7635
|
+
const dependencyStates = workPackage.dependsOnPackageIds.map((id) => packageStates.get(id)).filter(Boolean);
|
|
7636
|
+
const memberOwnStates = workPackage.workItemIds.map((id) => ownStates.get(id)).filter(Boolean);
|
|
7637
|
+
return { targetId: workPackage.id, kind: "page-work-package", state, reasons: packageStateReasons(workPackage, state, dependencyStates, memberOwnStates), workItemIds: workPackage.workItemIds };
|
|
7638
|
+
});
|
|
7639
|
+
const downstream = packageDownstreamCounts(plan.pageWorkPackages);
|
|
7640
|
+
const targetById = new Map(targets.map((target) => [target.targetId, target]));
|
|
7641
|
+
orderedReadyTargetIds = plan.pageWorkPackages.filter((workPackage) => targetById.get(workPackage.id)?.state === "ready").sort((left, right) => {
|
|
7642
|
+
const leftTasks = left.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
7643
|
+
const rightTasks = right.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
7644
|
+
const leftBootstrap = leftTasks.some((task) => task.type === "bootstrap") ? 0 : 1;
|
|
7645
|
+
const rightBootstrap = rightTasks.some((task) => task.type === "bootstrap") ? 0 : 1;
|
|
7646
|
+
const leftTokens = leftTasks.reduce((total, task) => total + task.estimate.estimatedFactTokens, 0);
|
|
7647
|
+
const rightTokens = rightTasks.reduce((total, task) => total + task.estimate.estimatedFactTokens, 0);
|
|
7648
|
+
return leftBootstrap - rightBootstrap || (downstream.get(right.id) ?? 0) - (downstream.get(left.id) ?? 0) || leftTokens - rightTokens || left.id.localeCompare(right.id);
|
|
7649
|
+
}).map((workPackage) => workPackage.id);
|
|
7650
|
+
}
|
|
7651
|
+
const reasons = Object.fromEntries([
|
|
7652
|
+
...runtimeItems.filter((item) => item.reasons.length).map((item) => [item.taskId, item.reasons]),
|
|
7653
|
+
...targets.filter((item) => item.reasons.length).map((item) => [item.targetId, item.reasons])
|
|
7654
|
+
]);
|
|
7655
|
+
const ownerCount = facts.requiredFactRefs.filter((ref) => plan.behaviorOwnership.primaryOwnerByFactRef[ref]).length;
|
|
6580
7656
|
const behaviorTasks = plan.tasks.filter((task) => task.type !== "resolution");
|
|
6581
7657
|
const closedTasks = behaviorTasks.filter((task) => !task.closure.requiresSourceReview && (task.closure.interactionRefs.length > 0 || task.type === "bootstrap" && task.closure.surfaceRefs.length > 0)).length;
|
|
6582
7658
|
return {
|
|
@@ -6584,19 +7660,26 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6584
7660
|
valid: validation.valid,
|
|
6585
7661
|
fresh: validation.fresh,
|
|
6586
7662
|
taskPlanSha256: validation.taskPlanSha256,
|
|
7663
|
+
planningPhase: plan.planningPhase,
|
|
6587
7664
|
totalTasks: plan.tasks.length,
|
|
7665
|
+
resolutionTasks: plan.tasks.filter((task) => task.type === "resolution").length,
|
|
7666
|
+
totalTargets: targets.length,
|
|
7667
|
+
readyTargets: orderedReadyTargetIds.length,
|
|
7668
|
+
completedTargets: targets.filter((item) => completedState(item.state)).length,
|
|
6588
7669
|
readyTasks: ready.length,
|
|
6589
7670
|
blockedTasks: runtimeItems.filter((item) => ["blocked", "stale"].includes(item.state)).length,
|
|
6590
|
-
completedTasks: runtimeItems.filter((item) =>
|
|
6591
|
-
|
|
6592
|
-
|
|
7671
|
+
completedTasks: runtimeItems.filter((item) => completedState(item.state)).length,
|
|
7672
|
+
recommendedTargetId: orderedReadyTargetIds[0] ?? null,
|
|
7673
|
+
recommendedTaskId: plan.planningPhase === "resolution" ? orderedReadyTargetIds[0] ?? null : null,
|
|
7674
|
+
alternatives: orderedReadyTargetIds.slice(1, 4),
|
|
6593
7675
|
reasons,
|
|
6594
7676
|
ownershipCoverage: facts.requiredFactRefs.length ? ownerCount / facts.requiredFactRefs.length : 1,
|
|
6595
|
-
closureCoverage: behaviorTasks.length ? closedTasks / behaviorTasks.length : 1,
|
|
7677
|
+
closureCoverage: plan.planningPhase === "resolution" ? 0 : behaviorTasks.length ? closedTasks / behaviorTasks.length : 1,
|
|
6596
7678
|
orphanFactRefs: validation.orphanFactRefs,
|
|
6597
7679
|
errors: validation.errors,
|
|
6598
7680
|
warnings: validation.warnings,
|
|
6599
|
-
tasks: runtimeItems
|
|
7681
|
+
tasks: runtimeItems,
|
|
7682
|
+
targets
|
|
6600
7683
|
};
|
|
6601
7684
|
}
|
|
6602
7685
|
|
|
@@ -6605,10 +7688,10 @@ function createTaskPlanningModule(factsSource = v2PlanningFactsSource) {
|
|
|
6605
7688
|
return {
|
|
6606
7689
|
compile(projectRoot, options) {
|
|
6607
7690
|
const facts = factsSource.load(projectRoot);
|
|
6608
|
-
const plan = compileFromPlanningFacts(facts, options.androidFingerprint);
|
|
7691
|
+
const plan = compileFromPlanningFacts(facts, options.androidFingerprint, loadExecutionPlanningContext(projectRoot));
|
|
6609
7692
|
const validation = validateTaskPlan(plan, facts, options.androidFingerprint);
|
|
6610
7693
|
if (!validation.valid || !validation.fresh) throw new Error(`Task plan compilation failed: ${[...validation.errors, ...validation.warnings].join("; ")}`);
|
|
6611
|
-
writeTaskPlan(projectRoot, plan);
|
|
7694
|
+
writeTaskPlan(projectRoot, plan, facts);
|
|
6612
7695
|
return plan;
|
|
6613
7696
|
},
|
|
6614
7697
|
inspect(projectRoot, source) {
|
|
@@ -6620,19 +7703,19 @@ function createTaskPlanningModule(factsSource = v2PlanningFactsSource) {
|
|
|
6620
7703
|
return { ...emptyTaskPlanSnapshot(error instanceof Error ? error.message : String(error)), present: hasTaskPlan(projectRoot) };
|
|
6621
7704
|
}
|
|
6622
7705
|
},
|
|
6623
|
-
materialize(projectRoot,
|
|
7706
|
+
materialize(projectRoot, target, request) {
|
|
6624
7707
|
const facts = factsSource.load(projectRoot);
|
|
6625
7708
|
const plan = readTaskPlan(projectRoot);
|
|
6626
7709
|
if (!plan) throw new Error("Task plan is not present; run droid2hmos task plan first");
|
|
6627
7710
|
const validation = validateTaskPlan(plan, facts, request.source.androidFingerprint);
|
|
6628
7711
|
if (!validation.valid || !validation.fresh) throw new Error(`Task plan is invalid or stale: ${[...validation.errors, ...validation.warnings].join("; ")}`);
|
|
6629
|
-
let
|
|
6630
|
-
if (
|
|
7712
|
+
let targetId = target;
|
|
7713
|
+
if (target === "next") {
|
|
6631
7714
|
const snapshot = inspectCompiledTaskPlan(projectRoot, facts, request.source);
|
|
6632
|
-
if (!snapshot.
|
|
6633
|
-
|
|
7715
|
+
if (!snapshot.recommendedTargetId) throw new Error("No ready execution target is available");
|
|
7716
|
+
targetId = snapshot.recommendedTargetId;
|
|
6634
7717
|
}
|
|
6635
|
-
return materializeEnvelope(plan, facts,
|
|
7718
|
+
return materializeEnvelope(plan, facts, targetId, request);
|
|
6636
7719
|
}
|
|
6637
7720
|
};
|
|
6638
7721
|
}
|
|
@@ -6648,9 +7731,7 @@ var actions = [
|
|
|
6648
7731
|
"init",
|
|
6649
7732
|
"analyze:android",
|
|
6650
7733
|
"analyze:harmony",
|
|
6651
|
-
"build:android",
|
|
6652
7734
|
"build:harmony",
|
|
6653
|
-
"test:android",
|
|
6654
7735
|
"test:harmony:targeted",
|
|
6655
7736
|
"test:harmony:all",
|
|
6656
7737
|
"report:init",
|
|
@@ -6672,9 +7753,9 @@ var actions = [
|
|
|
6672
7753
|
"complete"
|
|
6673
7754
|
];
|
|
6674
7755
|
var ignoredDirectories = /* @__PURE__ */ new Set([".git", ".gradle", ".hvigor", ".idea", ".migration", ".codegraph-appgraph", "build", "node_modules", "oh_modules"]);
|
|
6675
|
-
function
|
|
7756
|
+
function readJson8(file) {
|
|
6676
7757
|
try {
|
|
6677
|
-
return JSON.parse(
|
|
7758
|
+
return JSON.parse(fs29.readFileSync(file, "utf8"));
|
|
6678
7759
|
} catch {
|
|
6679
7760
|
return null;
|
|
6680
7761
|
}
|
|
@@ -6691,11 +7772,11 @@ function sha2563(value) {
|
|
|
6691
7772
|
return createHash8("sha256").update(typeof value === "string" ? value : stableJson2(value)).digest("hex");
|
|
6692
7773
|
}
|
|
6693
7774
|
function hashDirectory(root, excludedRoots = []) {
|
|
6694
|
-
if (!root || !
|
|
7775
|
+
if (!root || !fs29.existsSync(root) || !fs29.statSync(root).isDirectory()) return null;
|
|
6695
7776
|
const excluded = excludedRoots.map((value) => path35.resolve(value));
|
|
6696
7777
|
const hash2 = createHash8("sha256");
|
|
6697
7778
|
const visit = (directory) => {
|
|
6698
|
-
const entries =
|
|
7779
|
+
const entries = fs29.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
6699
7780
|
for (const entry of entries) {
|
|
6700
7781
|
const absolute = path35.join(directory, entry.name);
|
|
6701
7782
|
if (entry.isDirectory() && (ignoredDirectories.has(entry.name) || excluded.some((value) => absolute === value || absolute.startsWith(`${value}${path35.sep}`)))) continue;
|
|
@@ -6705,10 +7786,10 @@ function hashDirectory(root, excludedRoots = []) {
|
|
|
6705
7786
|
hash2.update(`d:${relative2}\0`);
|
|
6706
7787
|
visit(absolute);
|
|
6707
7788
|
} else if (entry.isSymbolicLink()) {
|
|
6708
|
-
hash2.update(`l:${relative2}\0${
|
|
7789
|
+
hash2.update(`l:${relative2}\0${fs29.readlinkSync(absolute)}\0`);
|
|
6709
7790
|
} else if (entry.isFile()) {
|
|
6710
7791
|
hash2.update(`f:${relative2}\0`);
|
|
6711
|
-
hash2.update(
|
|
7792
|
+
hash2.update(fs29.readFileSync(absolute));
|
|
6712
7793
|
hash2.update("\0");
|
|
6713
7794
|
}
|
|
6714
7795
|
}
|
|
@@ -6732,21 +7813,21 @@ function androidProjectFor(projectRoot, config) {
|
|
|
6732
7813
|
return path35.isAbsolute(configured) ? configured : path35.resolve(projectRoot, configured);
|
|
6733
7814
|
}
|
|
6734
7815
|
function isAndroidProject(root) {
|
|
6735
|
-
if (!
|
|
6736
|
-
const hasBuildRoot = ["settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts", "gradlew", "gradlew.bat"].some((name) =>
|
|
7816
|
+
if (!fs29.existsSync(root) || !fs29.statSync(root).isDirectory()) return false;
|
|
7817
|
+
const hasBuildRoot = ["settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts", "gradlew", "gradlew.bat"].some((name) => fs29.existsSync(path35.join(root, name)));
|
|
6737
7818
|
if (!hasBuildRoot) return false;
|
|
6738
|
-
if (
|
|
6739
|
-
return
|
|
7819
|
+
if (fs29.existsSync(path35.join(root, "app", "src", "main", "AndroidManifest.xml"))) return true;
|
|
7820
|
+
return fs29.readdirSync(root, { withFileTypes: true }).some((entry) => entry.isDirectory() && !ignoredDirectories.has(entry.name) && fs29.existsSync(path35.join(root, entry.name, "src", "main", "AndroidManifest.xml")));
|
|
6740
7821
|
}
|
|
6741
7822
|
function androidProjectCandidates(root) {
|
|
6742
|
-
if (!
|
|
6743
|
-
return
|
|
7823
|
+
if (!fs29.existsSync(root) || !fs29.statSync(root).isDirectory()) return [];
|
|
7824
|
+
return fs29.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !ignoredDirectories.has(entry.name)).map((entry) => path35.join(root, entry.name)).filter(isAndroidProject).sort();
|
|
6744
7825
|
}
|
|
6745
7826
|
function evidenceRoot(projectRoot) {
|
|
6746
7827
|
return path35.join(migrationDirectory(projectRoot), "evidence");
|
|
6747
7828
|
}
|
|
6748
7829
|
function evidenceIndex(projectRoot) {
|
|
6749
|
-
return
|
|
7830
|
+
return readJson8(path35.join(evidenceRoot(projectRoot), "index.json")) ?? { evidenceIndexVersion: "1.0.0", latestByAction: {} };
|
|
6750
7831
|
}
|
|
6751
7832
|
function projectRelative(projectRoot, absolute) {
|
|
6752
7833
|
return path35.relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
@@ -6759,7 +7840,7 @@ function managedReceipt2(projectRoot, reference) {
|
|
|
6759
7840
|
const root = path35.resolve(evidenceRoot(projectRoot), "managed");
|
|
6760
7841
|
const absolute = path35.resolve(migrationWorkspaceRoot(projectRoot), reference);
|
|
6761
7842
|
if (absolute !== root && !absolute.startsWith(`${root}${path35.sep}`)) return null;
|
|
6762
|
-
return
|
|
7843
|
+
return readJson8(absolute);
|
|
6763
7844
|
}
|
|
6764
7845
|
function reportClaims(projectRoot, reportIndex) {
|
|
6765
7846
|
if (!reportIndex) return [];
|
|
@@ -6775,16 +7856,16 @@ function reportClaims(projectRoot, reportIndex) {
|
|
|
6775
7856
|
});
|
|
6776
7857
|
};
|
|
6777
7858
|
for (const descriptor of reportIndex.pages ?? []) {
|
|
6778
|
-
const shard =
|
|
7859
|
+
const shard = readJson8(path35.join(reportRoot, descriptor.file));
|
|
6779
7860
|
if (!shard) continue;
|
|
6780
7861
|
addItems([shard.page], descriptor.file);
|
|
6781
7862
|
addItems(shard.controls, descriptor.file);
|
|
6782
7863
|
addItems(shard.entries, descriptor.file);
|
|
6783
7864
|
}
|
|
6784
|
-
if (reportIndex.unassignedEntriesFile) addItems(
|
|
6785
|
-
if (reportIndex.interactionsFile) addItems(
|
|
6786
|
-
if (reportIndex.discoveriesFile) addItems(
|
|
6787
|
-
const acceptance = reportIndex.finalAcceptanceFile ?
|
|
7865
|
+
if (reportIndex.unassignedEntriesFile) addItems(readJson8(path35.join(reportRoot, reportIndex.unassignedEntriesFile))?.entries, reportIndex.unassignedEntriesFile);
|
|
7866
|
+
if (reportIndex.interactionsFile) addItems(readJson8(path35.join(reportRoot, reportIndex.interactionsFile))?.interactions, reportIndex.interactionsFile);
|
|
7867
|
+
if (reportIndex.discoveriesFile) addItems(readJson8(path35.join(reportRoot, reportIndex.discoveriesFile))?.items, "discoveries", true);
|
|
7868
|
+
const acceptance = reportIndex.finalAcceptanceFile ? readJson8(path35.join(reportRoot, reportIndex.finalAcceptanceFile)) : null;
|
|
6788
7869
|
for (const [index, test] of (acceptance?.tests ?? []).entries()) claims.push({
|
|
6789
7870
|
ref: test?.ref ?? `final-acceptance:${test?.name ?? index + 1}`,
|
|
6790
7871
|
source: reportIndex.finalAcceptanceFile,
|
|
@@ -6799,8 +7880,8 @@ function receiptArtifactsAreIntact(projectRoot, receipt) {
|
|
|
6799
7880
|
return receipt.artifacts.every((artifact) => {
|
|
6800
7881
|
if (!artifact || typeof artifact.path !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256) || !Number.isInteger(artifact.size)) return false;
|
|
6801
7882
|
const absolute = path35.resolve(migrationWorkspaceRoot(projectRoot), artifact.path);
|
|
6802
|
-
if (!absolute.startsWith(`${blobsRoot}${path35.sep}`) || !
|
|
6803
|
-
const content =
|
|
7883
|
+
if (!absolute.startsWith(`${blobsRoot}${path35.sep}`) || !fs29.existsSync(absolute) || !fs29.statSync(absolute).isFile()) return false;
|
|
7884
|
+
const content = fs29.readFileSync(absolute);
|
|
6804
7885
|
return content.byteLength === artifact.size && createHash8("sha256").update(content).digest("hex") === artifact.sha256;
|
|
6805
7886
|
});
|
|
6806
7887
|
}
|
|
@@ -6862,7 +7943,7 @@ function actionRequirements(action, snapshot) {
|
|
|
6862
7943
|
if (action === "config:init" && snapshot.workspace.configPresent) missing.push(unmet("config-present", ".migration/config.json \u5DF2\u5B58\u5728"));
|
|
6863
7944
|
if (action === "config:validate") requireConfig();
|
|
6864
7945
|
if (action === "init" && snapshot.workspace.harmonyProjectPresent) missing.push(unmet("harmony-project-present", "HarmonyOS \u5DE5\u7A0B\u5DF2\u5B58\u5728\uFF0C\u65E0\u9700\u91CD\u590D\u521D\u59CB\u5316"));
|
|
6865
|
-
if (["analyze:android", "analyze:harmony", "build:
|
|
7946
|
+
if (["analyze:android", "analyze:harmony", "build:harmony", "test:harmony:targeted", "test:harmony:all", "evidence:flow"].includes(action)) requireConfig();
|
|
6866
7947
|
if (["analyze:harmony", "build:harmony", "test:harmony:targeted", "test:harmony:all"].includes(action)) requireHarmony();
|
|
6867
7948
|
if (action === "report:init") {
|
|
6868
7949
|
requireFacts();
|
|
@@ -6874,6 +7955,10 @@ function actionRequirements(action, snapshot) {
|
|
|
6874
7955
|
requireFacts();
|
|
6875
7956
|
requireReport();
|
|
6876
7957
|
requireFreshAnalysis();
|
|
7958
|
+
if (snapshot.planning.present && snapshot.planning.valid && snapshot.planning.fresh) {
|
|
7959
|
+
const message = snapshot.planning.planningPhase === "execution" ? "\u5DF2\u6709\u65B0\u9C9C\u7684\u51BB\u7ED3 Execution Plan\uFF1B\u5B9E\u73B0\u9636\u6BB5\u5FC5\u987B\u4E25\u683C\u6267\u884C\u73B0\u6709 Page Work Package\uFF0C\u7981\u6B62\u52A8\u6001 task plan" : "\u5DF2\u6709\u65B0\u9C9C\u7684 Resolution Plan\uFF1B\u5FC5\u987B\u5148\u5199\u5165\u6709\u6548 correction/review \u4F7F\u8BA1\u5212\u8FC7\u671F\uFF0C\u518D\u91CD\u65B0 task plan";
|
|
7960
|
+
missing.push(unmet("task-plan-already-fresh", message));
|
|
7961
|
+
}
|
|
6877
7962
|
}
|
|
6878
7963
|
if (action === "task:validate" && !snapshot.planning.present) missing.push(unmet("task-plan-missing", "Task Plan \u5C1A\u672A\u751F\u6210"));
|
|
6879
7964
|
if (action === "checkpoint:create") {
|
|
@@ -6897,14 +7982,14 @@ function derivePhase(snapshot) {
|
|
|
6897
7982
|
function inspectMigration(project = ".") {
|
|
6898
7983
|
const projectRoot = path35.resolve(project);
|
|
6899
7984
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
6900
|
-
const config =
|
|
7985
|
+
const config = readJson8(path35.join(migrationRoot, "config.json"));
|
|
6901
7986
|
const harmonyProject = harmonyProjectFor(projectRoot, config);
|
|
6902
7987
|
const androidProject = androidProjectFor(projectRoot, config);
|
|
6903
7988
|
const projectPresent = isAndroidProject(androidProject);
|
|
6904
7989
|
const projectCandidates = projectPresent ? [] : androidProjectCandidates(projectRoot);
|
|
6905
|
-
const preflight =
|
|
6906
|
-
const specIndex =
|
|
6907
|
-
const reportIndex =
|
|
7990
|
+
const preflight = readJson8(path35.join(migrationRoot, "preflight-report.json"));
|
|
7991
|
+
const specIndex = readJson8(path35.join(migrationRoot, "spec", "index.json"));
|
|
7992
|
+
const reportIndex = readJson8(path35.join(migrationRoot, "report", "index.json"));
|
|
6908
7993
|
let validation;
|
|
6909
7994
|
try {
|
|
6910
7995
|
validation = validateMigrationReport([projectRoot], { writeResult: false });
|
|
@@ -6928,7 +8013,7 @@ function inspectMigration(project = ".") {
|
|
|
6928
8013
|
projectCandidates,
|
|
6929
8014
|
configPresent: Boolean(config),
|
|
6930
8015
|
harmonyProject,
|
|
6931
|
-
harmonyProjectPresent:
|
|
8016
|
+
harmonyProjectPresent: fs29.existsSync(harmonyProject) && fs29.statSync(harmonyProject).isDirectory(),
|
|
6932
8017
|
gitHead: projectPresent ? gitHead(androidProject) : null
|
|
6933
8018
|
},
|
|
6934
8019
|
preflight: {
|
|
@@ -6960,10 +8045,16 @@ function inspectMigration(project = ".") {
|
|
|
6960
8045
|
valid: false,
|
|
6961
8046
|
fresh: false,
|
|
6962
8047
|
taskPlanSha256: null,
|
|
8048
|
+
planningPhase: null,
|
|
6963
8049
|
totalTasks: 0,
|
|
8050
|
+
resolutionTasks: 0,
|
|
8051
|
+
totalTargets: 0,
|
|
8052
|
+
readyTargets: 0,
|
|
8053
|
+
completedTargets: 0,
|
|
6964
8054
|
readyTasks: 0,
|
|
6965
8055
|
blockedTasks: 0,
|
|
6966
8056
|
completedTasks: 0,
|
|
8057
|
+
recommendedTargetId: null,
|
|
6967
8058
|
recommendedTaskId: null,
|
|
6968
8059
|
alternatives: [],
|
|
6969
8060
|
reasons: {},
|
|
@@ -6972,7 +8063,8 @@ function inspectMigration(project = ".") {
|
|
|
6972
8063
|
orphanFactRefs: [],
|
|
6973
8064
|
errors: [],
|
|
6974
8065
|
warnings: [],
|
|
6975
|
-
tasks: []
|
|
8066
|
+
tasks: [],
|
|
8067
|
+
targets: []
|
|
6976
8068
|
},
|
|
6977
8069
|
evidence: {
|
|
6978
8070
|
androidAnalysis: { reference: null, present: false, passed: false, fresh: false },
|
|
@@ -7018,7 +8110,7 @@ function inspectMigration(project = ".") {
|
|
|
7018
8110
|
else {
|
|
7019
8111
|
if (!snapshot.planning.valid) completionUnmet.push(unmet("task-plan-invalid", "Task Plan \u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25"));
|
|
7020
8112
|
if (!snapshot.planning.fresh) completionUnmet.push(unmet("task-plan-stale", "Task Plan \u4E0E\u5F53\u524D Facts\u3001\u5BA1\u6838\u8F93\u5165\u6216 Android \u6E90\u7801\u4E0D\u4E00\u81F4"));
|
|
7021
|
-
if (snapshot.planning.completedTasks !== snapshot.planning.totalTasks) completionUnmet.push(unmet("task-plan-incomplete", `${snapshot.planning.totalTasks - snapshot.planning.completedTasks} \u4E2A
|
|
8113
|
+
if (snapshot.planning.completedTasks !== snapshot.planning.totalTasks) completionUnmet.push(unmet("task-plan-incomplete", `${snapshot.planning.totalTasks - snapshot.planning.completedTasks} \u4E2A\u5185\u90E8 Work Item \u5C1A\u672A\u53D6\u5F97\u65B0\u9C9C\u5B9A\u5411\u9A8C\u8BC1\u8BC1\u636E`));
|
|
7022
8114
|
if (snapshot.planning.orphanFactRefs.length) completionUnmet.push(unmet("task-plan-orphans", `${snapshot.planning.orphanFactRefs.length} \u4E2A\u5FC5\u8981 Fact \u6CA1\u6709\u4E3B\u6240\u6709\u6743`));
|
|
7023
8115
|
}
|
|
7024
8116
|
if (!snapshot.evidence.fullRegression.fresh) completionUnmet.push(unmet("full-regression-missing-or-stale", "\u7F3A\u5C11\u4E0E\u5F53\u524D facts \u548C HarmonyOS \u6E90\u7801\u4E00\u81F4\u7684\u5168\u91CF\u53D7\u7BA1\u56DE\u5F52\u8BC1\u636E"));
|
|
@@ -7104,13 +8196,13 @@ function archiveArtifacts(projectRoot, artifacts) {
|
|
|
7104
8196
|
const archived = [];
|
|
7105
8197
|
for (const value of artifacts) {
|
|
7106
8198
|
const absolute = path35.isAbsolute(value) ? value : path35.resolve(projectRoot, value);
|
|
7107
|
-
if (!
|
|
7108
|
-
const content =
|
|
8199
|
+
if (!fs29.existsSync(absolute) || !fs29.statSync(absolute).isFile()) continue;
|
|
8200
|
+
const content = fs29.readFileSync(absolute);
|
|
7109
8201
|
const digest = createHash8("sha256").update(content).digest("hex");
|
|
7110
8202
|
const destination = path35.join(evidenceRoot(projectRoot), "blobs", digest, path35.basename(absolute));
|
|
7111
|
-
|
|
7112
|
-
const existingIsIntact =
|
|
7113
|
-
if (!existingIsIntact)
|
|
8203
|
+
fs29.mkdirSync(path35.dirname(destination), { recursive: true });
|
|
8204
|
+
const existingIsIntact = fs29.existsSync(destination) && fs29.statSync(destination).isFile() && fs29.statSync(destination).size === content.byteLength && createHash8("sha256").update(fs29.readFileSync(destination)).digest("hex") === digest;
|
|
8205
|
+
if (!existingIsIntact) fs29.copyFileSync(absolute, destination);
|
|
7114
8206
|
archived.push({
|
|
7115
8207
|
path: workspaceRelative(projectRoot, destination),
|
|
7116
8208
|
source: projectRelative(projectRoot, absolute),
|
|
@@ -7135,11 +8227,11 @@ function receiptResult(action, result) {
|
|
|
7135
8227
|
};
|
|
7136
8228
|
}
|
|
7137
8229
|
function writeJsonAtomic(file, value) {
|
|
7138
|
-
|
|
8230
|
+
fs29.mkdirSync(path35.dirname(file), { recursive: true });
|
|
7139
8231
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
7140
|
-
|
|
8232
|
+
fs29.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
7141
8233
|
`, "utf8");
|
|
7142
|
-
|
|
8234
|
+
fs29.renameSync(temporary, file);
|
|
7143
8235
|
}
|
|
7144
8236
|
function recordObservation(options) {
|
|
7145
8237
|
const projectRoot = path35.resolve(options.project ?? ".");
|
|
@@ -7201,9 +8293,8 @@ Usage:
|
|
|
7201
8293
|
droid2hmos complete [project]
|
|
7202
8294
|
droid2hmos analyze <android|harmony|all> [project]
|
|
7203
8295
|
droid2hmos preflight [project] [--sdk-path <path>] [--timeout-ms <ms>] [--no-init-harmony]
|
|
7204
|
-
droid2hmos config <init|validate> [project] [--harmony-project <path>]
|
|
7205
|
-
droid2hmos build
|
|
7206
|
-
droid2hmos test android [project] [--task <task>] [--timeout-ms <ms>]
|
|
8296
|
+
droid2hmos config <init|validate> [project] [--harmony-project <path>]
|
|
8297
|
+
droid2hmos build harmony [project] [--timeout-ms <ms>]
|
|
7207
8298
|
droid2hmos test harmony [project] [--runner arkts] (--class <class> [--case <case>] | --all) [--target <serial>] [--case-timeout-ms <ms>] [--ref <report-ref>...] [--timeout-ms <ms>]
|
|
7208
8299
|
droid2hmos test harmony [project] --runner python --hypium-project <path> --test-list <name>... [--target <serial>] [--python <path>] [--report-path <path>] [--ref <report-ref>...] [--timeout-ms <ms>]
|
|
7209
8300
|
droid2hmos report [project] [--require-complete]
|
|
@@ -7212,16 +8303,16 @@ Usage:
|
|
|
7212
8303
|
droid2hmos task plan [project]
|
|
7213
8304
|
droid2hmos task status [project]
|
|
7214
8305
|
droid2hmos task next [project]
|
|
7215
|
-
droid2hmos task show [project] <task-id> [--cursor <cursor>] [--max-source-context <count>]
|
|
8306
|
+
droid2hmos task show [project] <task-or-package-id> [--cursor <cursor>] [--max-source-context <count>]
|
|
7216
8307
|
droid2hmos task validate [project]
|
|
7217
8308
|
droid2hmos checkpoint status [project]
|
|
7218
8309
|
droid2hmos checkpoint create [project] --message <message> (--include <path>...|--all)
|
|
7219
|
-
droid2hmos device check
|
|
8310
|
+
droid2hmos device check harmony [--hdc <path>]
|
|
7220
8311
|
droid2hmos auth login [project] [--timeout-ms <ms>]
|
|
7221
8312
|
droid2hmos sign [project] [--target <serial>] [--force] [--timeout-ms <ms>]
|
|
7222
|
-
droid2hmos evidence capture
|
|
8313
|
+
droid2hmos evidence capture harmony [project] --name <name> [--target <id>]
|
|
7223
8314
|
droid2hmos evidence compare [project] --name <name> --android <evidence-dir> --harmony <evidence-dir>
|
|
7224
|
-
droid2hmos evidence flow
|
|
8315
|
+
droid2hmos evidence flow harmony [project] --name <name> [--target <id>] [--class <class> [--case <case>]|--all] [--case-timeout-ms <ms>] [--ref <report-ref>...]
|
|
7225
8316
|
droid2hmos mock init [project] --spec <protocol-spec.json> [--force]
|
|
7226
8317
|
droid2hmos logs <list|show|failures|stats> [project] [value]
|
|
7227
8318
|
droid2hmos upgrade
|
|
@@ -7274,13 +8365,13 @@ function observed(project, action, result, scope, guardResult) {
|
|
|
7274
8365
|
}
|
|
7275
8366
|
async function runCli(argv, runtime) {
|
|
7276
8367
|
const args = [...argv];
|
|
7277
|
-
let
|
|
7278
|
-
if (
|
|
7279
|
-
if (args.length > 0) throw new CliError(`${
|
|
8368
|
+
let command2 = args.shift();
|
|
8369
|
+
if (command2 && ["-v", "--version"].includes(command2)) {
|
|
8370
|
+
if (args.length > 0) throw new CliError(`${command2} does not accept arguments`);
|
|
7280
8371
|
console.log(readPackageMetadata(runtime.packageRoot).version);
|
|
7281
8372
|
return 0;
|
|
7282
8373
|
}
|
|
7283
|
-
if (!
|
|
8374
|
+
if (!command2 || ["help", "-h", "--help"].includes(command2)) {
|
|
7284
8375
|
console.log(HELP);
|
|
7285
8376
|
return 0;
|
|
7286
8377
|
}
|
|
@@ -7288,13 +8379,13 @@ async function runCli(argv, runtime) {
|
|
|
7288
8379
|
console.log(HELP);
|
|
7289
8380
|
return 0;
|
|
7290
8381
|
}
|
|
7291
|
-
if (
|
|
8382
|
+
if (command2 === "status") {
|
|
7292
8383
|
const projects = positionals(args);
|
|
7293
8384
|
if (projects.length > 1) throw new CliError("status accepts at most one project directory");
|
|
7294
8385
|
json(inspectMigration(projects[0] ?? "."));
|
|
7295
8386
|
return 0;
|
|
7296
8387
|
}
|
|
7297
|
-
if (
|
|
8388
|
+
if (command2 === "complete") {
|
|
7298
8389
|
const projects = positionals(args);
|
|
7299
8390
|
if (projects.length > 1) throw new CliError("complete accepts at most one project directory");
|
|
7300
8391
|
const project = projects[0] ?? ".";
|
|
@@ -7309,7 +8400,7 @@ async function runCli(argv, runtime) {
|
|
|
7309
8400
|
if (exitCode !== 0) return exitCode;
|
|
7310
8401
|
return 0;
|
|
7311
8402
|
}
|
|
7312
|
-
if (
|
|
8403
|
+
if (command2 === "init") {
|
|
7313
8404
|
const projects = positionals(args, ["--target"]);
|
|
7314
8405
|
if (projects.length > 1) throw new Error("init accepts at most one Android project directory");
|
|
7315
8406
|
if (!runtime.templatesRoot) throw new Error(`Cannot locate bundled HarmonyOS template (templates/ohos) from ${runtime.entryFile}`);
|
|
@@ -7335,28 +8426,26 @@ async function runCli(argv, runtime) {
|
|
|
7335
8426
|
json(observed(project, "init", { ...result, signature }, { target: optionValue(args, "--target") ?? null }, guardResult));
|
|
7336
8427
|
return 0;
|
|
7337
8428
|
}
|
|
7338
|
-
if (
|
|
8429
|
+
if (command2 === "upgrade") {
|
|
7339
8430
|
if (args.length > 0) throw new CliError("upgrade does not accept arguments");
|
|
7340
8431
|
json(upgradeCli({ packageRoot: runtime.packageRoot }));
|
|
7341
8432
|
return 0;
|
|
7342
8433
|
}
|
|
7343
|
-
if (
|
|
8434
|
+
if (command2 === "config") {
|
|
7344
8435
|
const action = args.shift();
|
|
7345
8436
|
if (!(/* @__PURE__ */ new Set(["init", "validate"])).has(action)) throw new CliError("config requires init or validate");
|
|
7346
|
-
const projects = positionals(args, ["--harmony-project"
|
|
8437
|
+
const projects = positionals(args, ["--harmony-project"]);
|
|
7347
8438
|
if (projects.length > 1) throw new CliError("config accepts at most one project directory");
|
|
7348
8439
|
const project = projects[0] ?? ".";
|
|
7349
8440
|
const guardResult = guard(`config:${action}`, project);
|
|
7350
8441
|
const result = action === "init" ? initMigrationConfig({
|
|
7351
8442
|
project,
|
|
7352
|
-
harmonyProject: optionValue(args, "--harmony-project")
|
|
7353
|
-
androidBuild: optionValue(args, "--android-build"),
|
|
7354
|
-
androidTest: optionValue(args, "--android-test")
|
|
8443
|
+
harmonyProject: optionValue(args, "--harmony-project")
|
|
7355
8444
|
}) : validateMigrationConfig(project);
|
|
7356
8445
|
json(observed(project, `config:${action}`, result, null, guardResult));
|
|
7357
8446
|
return action === "validate" && !result.valid ? 1 : 0;
|
|
7358
8447
|
}
|
|
7359
|
-
if (
|
|
8448
|
+
if (command2 === "preflight") {
|
|
7360
8449
|
const projects = positionals(args, ["--sdk-path", "--timeout-ms"]);
|
|
7361
8450
|
if (projects.length > 1) throw new CliError("preflight accepts at most one project directory");
|
|
7362
8451
|
if (!runtime.templatesRoot) throw new Error("Cannot locate bundled HarmonyOS template");
|
|
@@ -7368,7 +8457,7 @@ async function runCli(argv, runtime) {
|
|
|
7368
8457
|
json(observed(project, "preflight", result, { initializeHarmony: !args.includes("--no-init-harmony") }, guardResult));
|
|
7369
8458
|
return result.status === "passed" ? 0 : 1;
|
|
7370
8459
|
}
|
|
7371
|
-
if (
|
|
8460
|
+
if (command2 === "analyze") {
|
|
7372
8461
|
const target = args.shift();
|
|
7373
8462
|
if (!(/* @__PURE__ */ new Set(["android", "harmony", "all"])).has(target)) throw new CliError("analyze target must be android, harmony, or all");
|
|
7374
8463
|
const projects = positionals(args);
|
|
@@ -7401,7 +8490,7 @@ async function runCli(argv, runtime) {
|
|
|
7401
8490
|
}
|
|
7402
8491
|
return 0;
|
|
7403
8492
|
}
|
|
7404
|
-
if (
|
|
8493
|
+
if (command2 === "report") {
|
|
7405
8494
|
const explicitAction = ["init", "validate", "render", "update"].includes(args[0] ?? "") ? args.shift() : null;
|
|
7406
8495
|
const action = explicitAction ?? "render";
|
|
7407
8496
|
const projects = positionals(args, ["--ref", "--status", "--harmony-file", "--android-source", "--evidence", "--difference"]);
|
|
@@ -7464,19 +8553,19 @@ async function runCli(argv, runtime) {
|
|
|
7464
8553
|
}
|
|
7465
8554
|
throw new Error("report requires init, validate, or render");
|
|
7466
8555
|
}
|
|
7467
|
-
if (
|
|
8556
|
+
if (command2 === "task") {
|
|
7468
8557
|
const action = args.shift();
|
|
7469
8558
|
if (!(/* @__PURE__ */ new Set(["plan", "status", "next", "show", "validate"])).has(action)) throw new CliError("task requires plan, status, next, show, or validate");
|
|
7470
8559
|
if (action === "show") {
|
|
7471
8560
|
const values = positionals(args, ["--cursor", "--max-source-context"]);
|
|
7472
|
-
if (values.length < 1 || values.length > 2) throw new CliError("task show requires [project] <task-id>");
|
|
8561
|
+
if (values.length < 1 || values.length > 2) throw new CliError("task show requires [project] <task-or-package-id>");
|
|
7473
8562
|
const project2 = values.length === 2 ? values[0] : ".";
|
|
7474
|
-
const
|
|
7475
|
-
if (!/^task:[a-f0-9]{16}$/.test(
|
|
8563
|
+
const targetId = values.at(-1);
|
|
8564
|
+
if (!/^(task|package):[a-f0-9]{16}$/.test(targetId)) throw new CliError("task show requires a valid Resolution Task or Page Work Package id");
|
|
7476
8565
|
const maxSourceContext = Number(optionValue(args, "--max-source-context") ?? 25);
|
|
7477
8566
|
if (!Number.isInteger(maxSourceContext) || maxSourceContext <= 0 || maxSourceContext > 200) throw new CliError("--max-source-context must be an integer from 1 to 200");
|
|
7478
8567
|
const snapshot2 = inspectMigration(project2);
|
|
7479
|
-
json(taskPlanning.materialize(project2,
|
|
8568
|
+
json(taskPlanning.materialize(project2, targetId, {
|
|
7480
8569
|
source: {
|
|
7481
8570
|
androidFingerprint: snapshot2.source.androidFingerprint,
|
|
7482
8571
|
harmonyFingerprint: snapshot2.source.harmonyFingerprint,
|
|
@@ -7498,8 +8587,13 @@ async function runCli(argv, runtime) {
|
|
|
7498
8587
|
const result = {
|
|
7499
8588
|
status: planning.valid && planning.fresh ? "passed" : "failed",
|
|
7500
8589
|
taskPlanVersion: plan.taskPlanVersion,
|
|
8590
|
+
executionPlanVersion: plan.executionPlanVersion,
|
|
8591
|
+
planningPhase: plan.planningPhase,
|
|
7501
8592
|
taskPlanSha256: planning.taskPlanSha256,
|
|
7502
8593
|
totalTasks: plan.tasks.length,
|
|
8594
|
+
behaviorResponsibilities: plan.behaviorResponsibilities.length,
|
|
8595
|
+
pageWorkPackages: plan.pageWorkPackages.length,
|
|
8596
|
+
executionTargets: plan.planningPhase === "resolution" ? plan.tasks.length : plan.pageWorkPackages.length,
|
|
7503
8597
|
resolutionTasks: plan.tasks.filter((task) => task.type === "resolution").length,
|
|
7504
8598
|
errors: planning.errors,
|
|
7505
8599
|
warnings: planning.warnings
|
|
@@ -7525,19 +8619,19 @@ async function runCli(argv, runtime) {
|
|
|
7525
8619
|
}
|
|
7526
8620
|
const allComplete = snapshot.totalTasks > 0 && snapshot.completedTasks === snapshot.totalTasks;
|
|
7527
8621
|
json({
|
|
7528
|
-
status: snapshot.
|
|
8622
|
+
status: snapshot.recommendedTargetId ? "ready" : (() => {
|
|
7529
8623
|
if (allComplete) return "complete";
|
|
7530
8624
|
return "blocked";
|
|
7531
8625
|
})(),
|
|
7532
|
-
recommended: snapshot.
|
|
8626
|
+
recommended: snapshot.recommendedTargetId,
|
|
7533
8627
|
alternatives: snapshot.alternatives,
|
|
7534
8628
|
reasons: snapshot.reasons
|
|
7535
8629
|
});
|
|
7536
|
-
return snapshot.
|
|
8630
|
+
return snapshot.recommendedTargetId || allComplete ? 0 : 1;
|
|
7537
8631
|
}
|
|
7538
|
-
if (
|
|
8632
|
+
if (command2 === "build") {
|
|
7539
8633
|
const target = args.shift();
|
|
7540
|
-
if (
|
|
8634
|
+
if (target !== "harmony") throw new CliError("build target must be harmony (Android \u6784\u5EFA\u5DF2\u79FB\u9664\uFF1A\u8FC1\u79FB\u6D41\u7A0B\u4E0D\u4F9D\u8D56 Android \u7F16\u8BD1\u73AF\u5883)");
|
|
7541
8635
|
const projects = positionals(args, ["--timeout-ms"]);
|
|
7542
8636
|
if (projects.length > 1) throw new CliError("build accepts at most one project directory");
|
|
7543
8637
|
const projectRoot = path36.resolve(projects[0] ?? ".");
|
|
@@ -7545,52 +8639,29 @@ async function runCli(argv, runtime) {
|
|
|
7545
8639
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CliError("--timeout-ms must be a positive number");
|
|
7546
8640
|
const configPath = path36.join(migrationDirectory(projectRoot), "config.json");
|
|
7547
8641
|
if (!existsSync9(configPath)) throw new Error(`Migration config not found: ${configPath}`);
|
|
7548
|
-
const
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
result = key === "harmonyBuild" ? runHarmonyBuild({ projectRoot, harmonyProject, timeoutMs }) : runConfigCommand({ configPath, key, timeoutMs });
|
|
7558
|
-
} catch (error) {
|
|
7559
|
-
result = { status: "failed", key, summary: error?.message ?? String(error) };
|
|
7560
|
-
}
|
|
7561
|
-
if (key === "androidBuild" && result?.status === "not_applicable") {
|
|
7562
|
-
result = {
|
|
7563
|
-
...result,
|
|
7564
|
-
summary: 'androidBuild \u672A\u914D\u7F6E\uFF08\u8FC1\u79FB\u9ED8\u8BA4\u4E0D\u7F16\u8BD1 Android \u5DE5\u7A0B\uFF09\u3002\u5982\u9700 Android/HarmonyOS \u53CC\u7AEF\u8FD0\u884C\u5BF9\u7167\uFF0C\u53EF\u8FD0\u884C droid2hmos config init <project> --android-build "./gradlew --no-daemon assembleDebug" \u663E\u5F0F\u542F\u7528\u3002'
|
|
7565
|
-
};
|
|
7566
|
-
}
|
|
7567
|
-
result = observed(projectRoot, action, result, null, guardResult);
|
|
7568
|
-
json(result);
|
|
7569
|
-
if (result.status !== "passed") return 1;
|
|
8642
|
+
const guardResult = guard("build:harmony", projectRoot);
|
|
8643
|
+
const config = JSON.parse(readFileSync7(configPath, "utf8"));
|
|
8644
|
+
const configuredHarmony = config.harmonyProject ?? defaultHarmonyProjectPath(projectRoot);
|
|
8645
|
+
const harmonyProject = path36.isAbsolute(configuredHarmony) ? configuredHarmony : path36.resolve(projectRoot, configuredHarmony);
|
|
8646
|
+
let result;
|
|
8647
|
+
try {
|
|
8648
|
+
result = runHarmonyBuild({ projectRoot, harmonyProject, timeoutMs });
|
|
8649
|
+
} catch (error) {
|
|
8650
|
+
result = { status: "failed", key: "harmonyBuild", summary: error?.message ?? String(error) };
|
|
7570
8651
|
}
|
|
7571
|
-
|
|
8652
|
+
result = observed(projectRoot, "build:harmony", result, null, guardResult);
|
|
8653
|
+
json(result);
|
|
8654
|
+
return result.status === "passed" ? 0 : 1;
|
|
7572
8655
|
}
|
|
7573
|
-
if (
|
|
8656
|
+
if (command2 === "test") {
|
|
7574
8657
|
const target = args.shift();
|
|
7575
|
-
if (
|
|
7576
|
-
const projects = positionals(args, ["--timeout-ms", "--case-timeout-ms", "--class", "--case", "--
|
|
8658
|
+
if (target !== "harmony") throw new CliError("test target must be harmony (Android \u6D4B\u8BD5\u5DF2\u79FB\u9664\uFF1A\u8FC1\u79FB\u6D41\u7A0B\u4E0D\u4F9D\u8D56 Android \u8FD0\u884C\u73AF\u5883)");
|
|
8659
|
+
const projects = positionals(args, ["--timeout-ms", "--case-timeout-ms", "--class", "--case", "--target", "--ref", "--runner", "--hypium-project", "--test-list", "--python", "--report-path"]);
|
|
7577
8660
|
if (projects.length > 1) throw new CliError("test accepts at most one project directory");
|
|
7578
8661
|
const projectRoot = path36.resolve(projects[0] ?? ".");
|
|
7579
8662
|
const timeoutMs = Number(optionValue(args, "--timeout-ms") ?? 30 * 60 * 1e3);
|
|
7580
8663
|
const harmonyTarget = optionValue(args, "--target") ?? null;
|
|
7581
8664
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CliError("--timeout-ms must be a positive number");
|
|
7582
|
-
if (target === "android") {
|
|
7583
|
-
const guardResult2 = guard("test:android", projectRoot);
|
|
7584
|
-
if (args.includes("--runner")) throw new CliError("--runner is only supported by test harmony");
|
|
7585
|
-
const configPath2 = path36.join(migrationDirectory(projectRoot), "config.json");
|
|
7586
|
-
if (!existsSync9(configPath2)) throw new Error(`Migration config not found: ${configPath2}`);
|
|
7587
|
-
const task = optionValue(args, "--task");
|
|
7588
|
-
if (task && !/^[A-Za-z0-9_:.-]+$/.test(task)) throw new CliError("--task contains unsupported characters");
|
|
7589
|
-
let result2 = runConfigCommand({ configPath: configPath2, key: "androidTest", timeoutMs, replacements: task ? { "{task}": task } : {}, requiredTokens: task ? ["{task}"] : [] });
|
|
7590
|
-
result2 = observed(projectRoot, "test:android", result2, { task: task ?? null }, guardResult2);
|
|
7591
|
-
json(result2);
|
|
7592
|
-
return result2.status === "passed" || result2.status === "not_applicable" ? 0 : 1;
|
|
7593
|
-
}
|
|
7594
8665
|
const runner = optionValue(args, "--runner") ?? "arkts";
|
|
7595
8666
|
if (!(/* @__PURE__ */ new Set(["arkts", "python"])).has(runner)) throw new CliError("--runner must be arkts or python");
|
|
7596
8667
|
const testClass = optionValue(args, "--class") ?? null;
|
|
@@ -7651,7 +8722,7 @@ async function runCli(argv, runtime) {
|
|
|
7651
8722
|
json(result);
|
|
7652
8723
|
return result.status === "passed" ? 0 : 1;
|
|
7653
8724
|
}
|
|
7654
|
-
if (
|
|
8725
|
+
if (command2 === "checkpoint") {
|
|
7655
8726
|
const action = args.shift();
|
|
7656
8727
|
if (!(/* @__PURE__ */ new Set(["status", "create"])).has(action)) throw new CliError("checkpoint requires status or create");
|
|
7657
8728
|
const projects = positionals(args, ["--message", "--include"]);
|
|
@@ -7688,16 +8759,16 @@ async function runCli(argv, runtime) {
|
|
|
7688
8759
|
json(createGitCheckpoint({ project, harmonyProject: roots.harmonyRoot, message: optionValue(args, "--message") ?? "", include: optionValues(args, "--include"), all: args.includes("--all"), provenanceReceipt, provenanceFiles }));
|
|
7689
8760
|
return 0;
|
|
7690
8761
|
}
|
|
7691
|
-
if (
|
|
8762
|
+
if (command2 === "device") {
|
|
7692
8763
|
if (args.shift() !== "check") throw new CliError("device requires check");
|
|
7693
8764
|
const platform = args.shift();
|
|
7694
|
-
if (
|
|
8765
|
+
if (platform !== "harmony") throw new CliError("device check requires harmony (Android \u8BBE\u5907\u68C0\u6D4B\u5DF2\u79FB\u9664)");
|
|
7695
8766
|
guard("device:check", ".");
|
|
7696
|
-
const result =
|
|
8767
|
+
const result = checkHarmonyDevice({ hdc: optionValue(args, "--hdc") });
|
|
7697
8768
|
json(result);
|
|
7698
8769
|
return result.status === "failed" ? 1 : 0;
|
|
7699
8770
|
}
|
|
7700
|
-
if (
|
|
8771
|
+
if (command2 === "auth") {
|
|
7701
8772
|
if (args.shift() !== "login") throw new CliError("auth requires login");
|
|
7702
8773
|
const projects = positionals(args, ["--timeout-ms"]);
|
|
7703
8774
|
if (projects.length > 1) throw new CliError("auth accepts at most one project directory");
|
|
@@ -7712,7 +8783,7 @@ async function runCli(argv, runtime) {
|
|
|
7712
8783
|
json(result);
|
|
7713
8784
|
return result.status === "passed" ? 0 : 1;
|
|
7714
8785
|
}
|
|
7715
|
-
if (
|
|
8786
|
+
if (command2 === "sign") {
|
|
7716
8787
|
const projects = positionals(args, ["--target", "--timeout-ms"]);
|
|
7717
8788
|
if (projects.length > 1) throw new CliError("sign accepts at most one project directory");
|
|
7718
8789
|
const project = projects[0] ?? ".";
|
|
@@ -7734,7 +8805,7 @@ async function runCli(argv, runtime) {
|
|
|
7734
8805
|
json(result);
|
|
7735
8806
|
return result.status === "passed" || result.status === "skipped" ? 0 : 1;
|
|
7736
8807
|
}
|
|
7737
|
-
if (
|
|
8808
|
+
if (command2 === "evidence") {
|
|
7738
8809
|
const action = args.shift();
|
|
7739
8810
|
if (!(/* @__PURE__ */ new Set(["capture", "compare", "flow"])).has(action)) throw new CliError("evidence requires capture, compare, or flow");
|
|
7740
8811
|
if (action === "compare") {
|
|
@@ -7749,8 +8820,8 @@ async function runCli(argv, runtime) {
|
|
|
7749
8820
|
return 0;
|
|
7750
8821
|
}
|
|
7751
8822
|
const platform = args.shift();
|
|
7752
|
-
if (
|
|
7753
|
-
const projects = positionals(args, ["--name", "--target", "--class", "--case", "--
|
|
8823
|
+
if (platform !== "harmony") throw new CliError(`evidence ${action} requires harmony (Android \u8BC1\u636E\u91C7\u96C6\u5DF2\u79FB\u9664)`);
|
|
8824
|
+
const projects = positionals(args, ["--name", "--target", "--class", "--case", "--timeout-ms", "--case-timeout-ms", "--ref"]);
|
|
7754
8825
|
const project = projects[0] ?? ".";
|
|
7755
8826
|
const name = optionValue(args, "--name");
|
|
7756
8827
|
if (!name) throw new CliError(`evidence ${action} requires --name`);
|
|
@@ -7766,11 +8837,7 @@ async function runCli(argv, runtime) {
|
|
|
7766
8837
|
const timeoutMs = Number(optionValue(args, "--timeout-ms") ?? 30 * 60 * 1e3);
|
|
7767
8838
|
let execution;
|
|
7768
8839
|
if (before.status !== "passed") execution = { status: "failed", skipped: true, summary: "Baseline evidence capture failed; interaction was not executed" };
|
|
7769
|
-
else
|
|
7770
|
-
const task = optionValue(args, "--task");
|
|
7771
|
-
if (task && !/^[A-Za-z0-9_:.-]+$/.test(task)) throw new CliError("--task contains unsupported characters");
|
|
7772
|
-
execution = runConfigCommand({ configPath: path36.join(migrationDirectory(projectRoot), "config.json"), key: "androidTest", timeoutMs, replacements: task ? { "{task}": task } : {}, requiredTokens: task ? ["{task}"] : [] });
|
|
7773
|
-
} else {
|
|
8840
|
+
else {
|
|
7774
8841
|
const config = JSON.parse(readFileSync7(path36.join(migrationDirectory(projectRoot), "config.json"), "utf8"));
|
|
7775
8842
|
const harmonyProject = path36.isAbsolute(config.harmonyProject) ? config.harmonyProject : path36.resolve(projectRoot, config.harmonyProject);
|
|
7776
8843
|
const testClass = optionValue(args, "--class") ?? null;
|
|
@@ -7782,7 +8849,7 @@ async function runCli(argv, runtime) {
|
|
|
7782
8849
|
}
|
|
7783
8850
|
const after = captureDeviceEvidence({ project, platform, name: `${name}-after`, target: optionValue(args, "--target") });
|
|
7784
8851
|
let result = writeFlowBundle({ project, name, platform, before, execution, after });
|
|
7785
|
-
|
|
8852
|
+
{
|
|
7786
8853
|
const testAction = args.includes("--all") ? "test:harmony:all" : "test:harmony:targeted";
|
|
7787
8854
|
result = observed(project, testAction, result, {
|
|
7788
8855
|
evidenceFlow: name,
|
|
@@ -7797,7 +8864,7 @@ async function runCli(argv, runtime) {
|
|
|
7797
8864
|
json(result);
|
|
7798
8865
|
return result.status === "passed" ? 0 : 1;
|
|
7799
8866
|
}
|
|
7800
|
-
if (
|
|
8867
|
+
if (command2 === "mock") {
|
|
7801
8868
|
if (args.shift() !== "init") throw new CliError("mock requires init");
|
|
7802
8869
|
const projects = positionals(args, ["--spec"]);
|
|
7803
8870
|
const specFile = optionValue(args, "--spec");
|
|
@@ -7807,14 +8874,14 @@ async function runCli(argv, runtime) {
|
|
|
7807
8874
|
json(generateMockServer({ project, specFile, force: args.includes("--force") }));
|
|
7808
8875
|
return 0;
|
|
7809
8876
|
}
|
|
7810
|
-
if (
|
|
8877
|
+
if (command2 === "logs") {
|
|
7811
8878
|
const action = args.shift();
|
|
7812
8879
|
if (!(/* @__PURE__ */ new Set(["list", "show", "failures", "stats"])).has(action)) throw new CliError("logs requires list, show, failures, or stats");
|
|
7813
8880
|
const project = args.shift() ?? ".";
|
|
7814
8881
|
json(queryCommandLogs(project, action, args.shift()));
|
|
7815
8882
|
return 0;
|
|
7816
8883
|
}
|
|
7817
|
-
if (
|
|
8884
|
+
if (command2 === "service") {
|
|
7818
8885
|
const action = args.shift();
|
|
7819
8886
|
if (!(/* @__PURE__ */ new Set(["start", "status", "stop"])).has(action)) throw new CliError("service action must be start, status, or stop");
|
|
7820
8887
|
const name = args.shift();
|
|
@@ -7858,7 +8925,7 @@ async function runCli(argv, runtime) {
|
|
|
7858
8925
|
}));
|
|
7859
8926
|
return 0;
|
|
7860
8927
|
}
|
|
7861
|
-
if (
|
|
8928
|
+
if (command2 === "internal") {
|
|
7862
8929
|
const action = args.shift();
|
|
7863
8930
|
if (action === "detect-harmony-sdk") {
|
|
7864
8931
|
const projects = positionals(args, ["--sdk-path"]);
|
|
@@ -7890,14 +8957,14 @@ async function runCli(argv, runtime) {
|
|
|
7890
8957
|
}
|
|
7891
8958
|
throw new Error("unknown internal command");
|
|
7892
8959
|
}
|
|
7893
|
-
if (
|
|
8960
|
+
if (command2 === "dev") {
|
|
7894
8961
|
const action = args.shift();
|
|
7895
8962
|
if (!runtime.packageRoot || action !== "build") throw new Error("dev requires build from a source checkout");
|
|
7896
8963
|
const result = spawnCommandSync("npm", ["run", "build"], { cwd: runtime.packageRoot, stdio: "inherit" });
|
|
7897
8964
|
if (result.error) throw result.error;
|
|
7898
8965
|
return result.status ?? 1;
|
|
7899
8966
|
}
|
|
7900
|
-
throw new CliError(`Unknown command: ${
|
|
8967
|
+
throw new CliError(`Unknown command: ${command2}`);
|
|
7901
8968
|
}
|
|
7902
8969
|
var entryHref = import.meta.url;
|
|
7903
8970
|
var invokedHref = (() => {
|