@harmonyos-arkts/droid2hmos 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +249 -251
- package/config/preflight-checklist.json +65 -65
- package/dist/cli.js +1827 -745
- package/dist/cli.js.map +1 -1
- package/package.json +36 -36
- package/templates/ohos/AppScope/app.json5 +10 -10
- package/templates/ohos/AppScope/resources/base/element/string.json +8 -8
- package/templates/ohos/AppScope/resources/base/media/layered_image.json +6 -6
- package/templates/ohos/build-profile.json5 +41 -41
- package/templates/ohos/code-linter.json5 +31 -31
- package/templates/ohos/entry/build-profile.json5 +32 -32
- package/templates/ohos/entry/hvigorfile.ts +5 -5
- package/templates/ohos/entry/obfuscation-rules.txt +22 -22
- package/templates/ohos/entry/oh-package.json5 +10 -10
- package/templates/ohos/entry/src/main/ets/entryability/EntryAbility.ets +47 -47
- package/templates/ohos/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +15 -15
- package/templates/ohos/entry/src/main/ets/pages/Index.ets +22 -22
- package/templates/ohos/entry/src/main/module.json5 +49 -49
- package/templates/ohos/entry/src/main/resources/base/element/color.json +7 -7
- package/templates/ohos/entry/src/main/resources/base/element/float.json +8 -8
- package/templates/ohos/entry/src/main/resources/base/element/string.json +15 -15
- package/templates/ohos/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/templates/ohos/entry/src/main/resources/base/profile/backup_config.json +2 -2
- package/templates/ohos/entry/src/main/resources/base/profile/main_pages.json +5 -5
- package/templates/ohos/entry/src/main/resources/dark/element/color.json +7 -7
- package/templates/ohos/entry/src/mock/mock-config.json5 +1 -1
- package/templates/ohos/entry/src/ohosTest/ets/test/Ability.test.ets +34 -34
- package/templates/ohos/entry/src/ohosTest/ets/test/List.test.ets +4 -4
- package/templates/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets +48 -48
- package/templates/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets +14 -14
- package/templates/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ets +41 -41
- package/templates/ohos/entry/src/ohosTest/module.json5 +25 -25
- package/templates/ohos/entry/src/ohosTest/resources/base/element/color.json +7 -7
- package/templates/ohos/entry/src/ohosTest/resources/base/element/string.json +12 -12
- package/templates/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json +5 -5
- package/templates/ohos/entry/src/test/List.test.ets +4 -4
- package/templates/ohos/entry/src/test/LocalUnit.test.ets +32 -32
- package/templates/ohos/hvigor/hvigor-config.json5 +23 -23
- package/templates/ohos/hvigorfile.ts +5 -5
- package/templates/ohos/oh-package-lock.json5 +27 -27
- package/templates/ohos/oh-package.json5 +10 -10
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";
|
|
@@ -2490,6 +2395,14 @@ import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
|
2490
2395
|
import path13 from "path";
|
|
2491
2396
|
var DEVECO_CLI_PACKAGE = "@deveco/deveco-cli@latest";
|
|
2492
2397
|
var DEVECO_CLI_INSTALL_COMMAND = `npm install -g ${DEVECO_CLI_PACKAGE}`;
|
|
2398
|
+
var NODE_INSPECTOR_OPTION = /(?:^|\s)--inspect(?:-brk|-port|-wait)?(?:=(?:"[^"]*"|'[^']*'|\S+))?(?=\s|$)/gu;
|
|
2399
|
+
function devEcoCliEnvironment(environment = process.env) {
|
|
2400
|
+
const result = { ...environment, DEVECO_CLI_SKIP_VERSION_CHECK: "1" };
|
|
2401
|
+
const nodeOptions = String(environment.NODE_OPTIONS ?? "").replace(NODE_INSPECTOR_OPTION, "").trim();
|
|
2402
|
+
if (nodeOptions) result.NODE_OPTIONS = nodeOptions;
|
|
2403
|
+
else delete result.NODE_OPTIONS;
|
|
2404
|
+
return result;
|
|
2405
|
+
}
|
|
2493
2406
|
function executableName() {
|
|
2494
2407
|
return process.env.DEVECO_CLI_BINARY || "devecocli";
|
|
2495
2408
|
}
|
|
@@ -2497,14 +2410,14 @@ function devEcoCliSpawn(executable, args) {
|
|
|
2497
2410
|
const scriptWrapper = /\.m?js$/i.test(executable);
|
|
2498
2411
|
return scriptWrapper ? [process.execPath, executable, ...args] : [executable, ...args];
|
|
2499
2412
|
}
|
|
2500
|
-
function runDevEcoCli(args, { executable = executableName(), runner = spawnCommandSync, timeoutMs = 15e3 } = {}, cwd) {
|
|
2501
|
-
const [
|
|
2502
|
-
const result = runner(
|
|
2413
|
+
function runDevEcoCli(args, { executable = executableName(), runner = spawnCommandSync, timeoutMs = 15e3, environment = process.env } = {}, cwd) {
|
|
2414
|
+
const [command2, ...commandArgs] = devEcoCliSpawn(executable, args);
|
|
2415
|
+
const result = runner(command2, commandArgs, {
|
|
2503
2416
|
cwd,
|
|
2504
2417
|
encoding: "utf8",
|
|
2505
2418
|
timeout: timeoutMs,
|
|
2506
2419
|
windowsHide: true,
|
|
2507
|
-
env:
|
|
2420
|
+
env: devEcoCliEnvironment(environment)
|
|
2508
2421
|
});
|
|
2509
2422
|
return {
|
|
2510
2423
|
executable,
|
|
@@ -2516,13 +2429,13 @@ function runDevEcoCli(args, { executable = executableName(), runner = spawnComma
|
|
|
2516
2429
|
errorCode: result.error && "code" in result.error ? result.error.code : null
|
|
2517
2430
|
};
|
|
2518
2431
|
}
|
|
2519
|
-
async function runDevEcoCliAsync(args, { executable = executableName(), timeoutMs = 15e3 } = {}, cwd) {
|
|
2520
|
-
const [
|
|
2521
|
-
const child = spawnCommand(
|
|
2432
|
+
async function runDevEcoCliAsync(args, { executable = executableName(), timeoutMs = 15e3, environment = process.env } = {}, cwd) {
|
|
2433
|
+
const [command2, ...commandArgs] = devEcoCliSpawn(executable, args);
|
|
2434
|
+
const child = spawnCommand(command2, commandArgs, {
|
|
2522
2435
|
cwd,
|
|
2523
2436
|
windowsHide: true,
|
|
2524
2437
|
detached: process.platform !== "win32",
|
|
2525
|
-
env:
|
|
2438
|
+
env: devEcoCliEnvironment(environment),
|
|
2526
2439
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2527
2440
|
});
|
|
2528
2441
|
let stdout = "";
|
|
@@ -2603,7 +2516,7 @@ function checkDevEcoCliAuth(cli = {}) {
|
|
|
2603
2516
|
const result = runDevEcoCli(["auth", "status"], cli);
|
|
2604
2517
|
const output = [result.stdout, result.stderr].map((value) => value.trim()).filter(Boolean).join("\n");
|
|
2605
2518
|
const loggedOut = /(?:^|\n)Not logged in(?:\n|$)/i.test(output);
|
|
2606
|
-
const currentUser = output.match(/
|
|
2519
|
+
const currentUser = output.match(/Current user:\s*([^\r\n]+)/i)?.[1]?.trim();
|
|
2607
2520
|
if (result.status !== "passed" || loggedOut || !currentUser) {
|
|
2608
2521
|
return {
|
|
2609
2522
|
status: "failed",
|
|
@@ -2773,8 +2686,8 @@ function isRealDeviceTarget(target) {
|
|
|
2773
2686
|
}
|
|
2774
2687
|
return true;
|
|
2775
2688
|
}
|
|
2776
|
-
function readSigningMaterialFingerprints(
|
|
2777
|
-
const profile = path14.join(
|
|
2689
|
+
function readSigningMaterialFingerprints(harmonyRoot2) {
|
|
2690
|
+
const profile = path14.join(harmonyRoot2, "build-profile.json5");
|
|
2778
2691
|
if (!fs13.existsSync(profile)) return [];
|
|
2779
2692
|
let parsed;
|
|
2780
2693
|
try {
|
|
@@ -2783,7 +2696,7 @@ function readSigningMaterialFingerprints(harmonyRoot) {
|
|
|
2783
2696
|
return [];
|
|
2784
2697
|
}
|
|
2785
2698
|
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(
|
|
2699
|
+
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
2700
|
return [...new Set(files)].flatMap((file) => {
|
|
2788
2701
|
if (!fs13.existsSync(file)) return [];
|
|
2789
2702
|
const stat = fs13.statSync(file);
|
|
@@ -2900,10 +2813,10 @@ function detectDeviceSdkVersion(target) {
|
|
|
2900
2813
|
diverged: systemAbc !== null && systemAbc.major < claimedApiLevel
|
|
2901
2814
|
};
|
|
2902
2815
|
}
|
|
2903
|
-
function ensureDeviceInstallable(
|
|
2816
|
+
function ensureDeviceInstallable(harmonyRoot2, target) {
|
|
2904
2817
|
const device = detectDeviceSdkVersion(target);
|
|
2905
2818
|
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(
|
|
2819
|
+
const capped = capCompatibleSdkVersionTo(harmonyRoot2, device.sdkVersion);
|
|
2907
2820
|
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
2821
|
return {
|
|
2909
2822
|
...capped,
|
|
@@ -2994,10 +2907,10 @@ function readJson5(file) {
|
|
|
2994
2907
|
function firstExisting(values) {
|
|
2995
2908
|
return values.find((value) => Boolean(value && existsSync4(value)));
|
|
2996
2909
|
}
|
|
2997
|
-
function pathLookup(
|
|
2910
|
+
function pathLookup(command2) {
|
|
2998
2911
|
try {
|
|
2999
2912
|
const lookup = process.platform === "win32" ? "where.exe" : "which";
|
|
3000
|
-
return execFileSync(lookup, [
|
|
2913
|
+
return execFileSync(lookup, [command2], { encoding: "utf8", windowsHide: true }).split(/\r?\n/).map((value) => value.trim()).find(Boolean);
|
|
3001
2914
|
} catch {
|
|
3002
2915
|
return null;
|
|
3003
2916
|
}
|
|
@@ -3158,8 +3071,8 @@ function runHarmonyTest({ projectRoot, harmonyProject, timeoutMs, testClass = nu
|
|
|
3158
3071
|
if (appBuild.status !== "passed") return { status: "failed", key: "harmonyTest", stage: "app-build", appBuild };
|
|
3159
3072
|
const ohpmInstall = ensureOhpmDependencies({ projectRoot, harmonyProject, timeoutMs });
|
|
3160
3073
|
if (ohpmInstall.status !== "passed") return { status: "failed", key: "harmonyTest", stage: "ohpm-install", appBuild, ohpmInstall };
|
|
3161
|
-
const { info, command:
|
|
3162
|
-
const testBuild = runFiniteCommand({ command:
|
|
3074
|
+
const { info, command: command2 } = buildHarmonyTestCommand({ projectRoot, harmonyProject });
|
|
3075
|
+
const testBuild = runFiniteCommand({ command: command2, key: "harmonyTest-build", cwd: info.root, projectRoot, timeoutMs });
|
|
3163
3076
|
if (testBuild.status !== "passed") return { status: "failed", key: "harmonyTest", stage: "test-build", appBuild, testBuild };
|
|
3164
3077
|
const testArtifact = newestHap(info.moduleRoot, (name) => name.toLowerCase().includes(info.testTarget.toLowerCase()));
|
|
3165
3078
|
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 +3258,8 @@ function detectHarmonySdk({ project, sdkPath, writeConfig = false, env = process
|
|
|
3345
3258
|
push(sdkPath, "explicit");
|
|
3346
3259
|
push(config?.harmonySdk?.path, "existing-config");
|
|
3347
3260
|
const configuredHarmony = config?.harmonyProject ?? defaultHarmonyProjectPath(projectRoot);
|
|
3348
|
-
const
|
|
3349
|
-
const localProperties = path17.join(
|
|
3261
|
+
const harmonyRoot2 = path17.isAbsolute(configuredHarmony) ? configuredHarmony : path17.resolve(projectRoot, configuredHarmony);
|
|
3262
|
+
const localProperties = path17.join(harmonyRoot2, "local.properties");
|
|
3350
3263
|
if (fs14.existsSync(localProperties)) {
|
|
3351
3264
|
const properties = parseProperties(fs14.readFileSync(localProperties, "utf8"));
|
|
3352
3265
|
push(properties["sdk.dir"] ?? properties["harmony.sdk.dir"] ?? properties["ohos.sdk.dir"], "local-properties");
|
|
@@ -3485,22 +3398,19 @@ var atomicJson = (file, value) => {
|
|
|
3485
3398
|
`, "utf8");
|
|
3486
3399
|
fs15.renameSync(temporary, file);
|
|
3487
3400
|
};
|
|
3488
|
-
function
|
|
3489
|
-
return
|
|
3490
|
-
}
|
|
3491
|
-
function normalizeAndroidBuildCommand(command3, platform = process.platform) {
|
|
3492
|
-
if (typeof command3 !== "string") return command3;
|
|
3401
|
+
function normalizeAndroidBuildCommand(command2, platform = process.platform) {
|
|
3402
|
+
if (typeof command2 !== "string") return command2;
|
|
3493
3403
|
if (platform === "win32") {
|
|
3494
|
-
return
|
|
3404
|
+
return command2.replace(/^(\s*)(?:&\s*)?["']?\.\/gradlew["']?(?=\s|$)/i, "$1.\\gradlew.bat");
|
|
3495
3405
|
}
|
|
3496
|
-
return
|
|
3406
|
+
return command2.replace(/^(\s*)(?:&\s*)?["']?(?:\.\\gradlew(?:\.bat)?|gradlew\.bat)["']?(?=\s|$)/i, "$1./gradlew");
|
|
3497
3407
|
}
|
|
3498
|
-
function validateAndroidBuildPlatform(
|
|
3499
|
-
if (typeof
|
|
3500
|
-
if (platform === "win32" && /(?:^|\s)\.\/gradlew(?:\s|$)/i.test(
|
|
3408
|
+
function validateAndroidBuildPlatform(command2, platform = process.platform) {
|
|
3409
|
+
if (typeof command2 !== "string") return null;
|
|
3410
|
+
if (platform === "win32" && /(?:^|\s)\.\/gradlew(?:\s|$)/i.test(command2)) {
|
|
3501
3411
|
return "commands.androidBuild uses a Unix Gradle wrapper on Windows; use .\\gradlew.bat";
|
|
3502
3412
|
}
|
|
3503
|
-
if (platform !== "win32" && /(?:gradlew\.bat|\.\\gradlew)(?:\s|$)/i.test(
|
|
3413
|
+
if (platform !== "win32" && /(?:gradlew\.bat|\.\\gradlew)(?:\s|$)/i.test(command2)) {
|
|
3504
3414
|
return `commands.androidBuild uses a Windows Gradle wrapper on ${platform}; use ./gradlew`;
|
|
3505
3415
|
}
|
|
3506
3416
|
return null;
|
|
@@ -3522,12 +3432,12 @@ function validateMigrationConfigValue(config, projectRoot) {
|
|
|
3522
3432
|
if (path19.isAbsolute(config.androidProject)) return config.androidProject;
|
|
3523
3433
|
return path19.resolve(projectRoot, config.androidProject);
|
|
3524
3434
|
})() : null;
|
|
3525
|
-
const
|
|
3435
|
+
const harmonyRoot2 = config?.harmonyProject ? (() => {
|
|
3526
3436
|
if (path19.isAbsolute(config.harmonyProject)) return config.harmonyProject;
|
|
3527
3437
|
return path19.resolve(projectRoot, config.harmonyProject);
|
|
3528
3438
|
})() : null;
|
|
3529
3439
|
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 };
|
|
3440
|
+
return { valid: errors.length === 0, errors, projectRoot, androidRoot, harmonyRoot: harmonyRoot2 };
|
|
3531
3441
|
}
|
|
3532
3442
|
function initMigrationConfig(options) {
|
|
3533
3443
|
const projectRoot = path19.resolve(options.project);
|
|
@@ -3536,7 +3446,6 @@ function initMigrationConfig(options) {
|
|
|
3536
3446
|
const configPath = path19.join(migrationDirectory(projectRoot), "config.json");
|
|
3537
3447
|
const created = !fs15.existsSync(configPath);
|
|
3538
3448
|
const existing = created ? {} : JSON.parse(fs15.readFileSync(configPath, "utf8"));
|
|
3539
|
-
const configuredAndroidBuild = options.androidBuild ?? existing.commands?.androidBuild ?? defaultAndroidBuild(projectRoot);
|
|
3540
3449
|
const config = {
|
|
3541
3450
|
...existing,
|
|
3542
3451
|
configVersion: existing.configVersion ?? "1.0.0",
|
|
@@ -3545,8 +3454,8 @@ function initMigrationConfig(options) {
|
|
|
3545
3454
|
...existing.harmonySdk ? { harmonySdk: existing.harmonySdk } : {},
|
|
3546
3455
|
commands: {
|
|
3547
3456
|
...existing.commands ?? {},
|
|
3548
|
-
androidBuild: normalizeAndroidBuildCommand(
|
|
3549
|
-
androidTest:
|
|
3457
|
+
androidBuild: normalizeAndroidBuildCommand(existing.commands?.androidBuild ?? null),
|
|
3458
|
+
androidTest: existing.commands?.androidTest ?? null,
|
|
3550
3459
|
harmonyStatic: existing.commands?.harmonyStatic ?? null
|
|
3551
3460
|
}
|
|
3552
3461
|
};
|
|
@@ -3570,7 +3479,7 @@ function validateMigrationConfig(project) {
|
|
|
3570
3479
|
|
|
3571
3480
|
// src/commands/preflight.ts
|
|
3572
3481
|
import fs20 from "fs";
|
|
3573
|
-
import
|
|
3482
|
+
import path23 from "path";
|
|
3574
3483
|
|
|
3575
3484
|
// src/commands/detect-android-ui-stack.ts
|
|
3576
3485
|
import fs16 from "fs";
|
|
@@ -3641,69 +3550,13 @@ function detectAndroidUiStack(projectRoot) {
|
|
|
3641
3550
|
};
|
|
3642
3551
|
}
|
|
3643
3552
|
|
|
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
3553
|
// src/commands/git-checkpoint.ts
|
|
3701
3554
|
import path21 from "path";
|
|
3702
3555
|
import fs17 from "fs";
|
|
3703
3556
|
import { createHash as createHash5 } from "crypto";
|
|
3704
|
-
import { spawnSync as
|
|
3557
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
3705
3558
|
function git(projectRoot, args) {
|
|
3706
|
-
const result =
|
|
3559
|
+
const result = spawnSync5("git", ["-C", projectRoot, ...args], { encoding: "utf8", shell: false, timeout: 3e4, windowsHide: true });
|
|
3707
3560
|
return {
|
|
3708
3561
|
ok: !result.error && result.status === 0,
|
|
3709
3562
|
exitCode: Number.isInteger(result.status) ? result.status : null,
|
|
@@ -3733,17 +3586,17 @@ function unstage(root, files) {
|
|
|
3733
3586
|
if (head.ok) git(root, ["restore", "--staged", "--", ...files]);
|
|
3734
3587
|
else git(root, ["rm", "--cached", "-r", "--ignore-unmatch", "--", ...files]);
|
|
3735
3588
|
}
|
|
3736
|
-
function initializeHarmonyRepository(androidRoot,
|
|
3737
|
-
const existing = repositoryRoot(
|
|
3589
|
+
function initializeHarmonyRepository(androidRoot, harmonyRoot2) {
|
|
3590
|
+
const existing = repositoryRoot(harmonyRoot2);
|
|
3738
3591
|
if (existing) return { root: existing, initialized: false };
|
|
3739
3592
|
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: ${
|
|
3593
|
+
const initialized = git(harmonyRoot2, ["init", "-b", androidBranch]);
|
|
3594
|
+
if (!initialized.ok) throw new Error(initialized.stderr || `Cannot initialize HarmonyOS Git repository: ${harmonyRoot2}`);
|
|
3742
3595
|
for (const key of ["user.name", "user.email"]) {
|
|
3743
3596
|
const value = git(androidRoot, ["config", "--get", key]);
|
|
3744
|
-
if (value.ok && value.stdout) git(
|
|
3597
|
+
if (value.ok && value.stdout) git(harmonyRoot2, ["config", key, value.stdout]);
|
|
3745
3598
|
}
|
|
3746
|
-
return { root: path21.resolve(
|
|
3599
|
+
return { root: path21.resolve(harmonyRoot2), initialized: true };
|
|
3747
3600
|
}
|
|
3748
3601
|
function relevantStatus(status, ignoreUntrackedRoots = []) {
|
|
3749
3602
|
const ignored = ignoreUntrackedRoots.map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
|
|
@@ -3756,8 +3609,8 @@ function relevantStatus(status, ignoreUntrackedRoots = []) {
|
|
|
3756
3609
|
}
|
|
3757
3610
|
function gitWorkspaceStatus(project, options = {}) {
|
|
3758
3611
|
const projectRoot = path21.resolve(project);
|
|
3759
|
-
const
|
|
3760
|
-
if (!
|
|
3612
|
+
const inside5 = git(projectRoot, ["rev-parse", "--is-inside-work-tree"]);
|
|
3613
|
+
if (!inside5.ok || inside5.stdout !== "true") return { status: "failed", repository: false, projectRoot, errors: [inside5.stderr || "not a Git worktree"] };
|
|
3761
3614
|
const head = git(projectRoot, ["rev-parse", "HEAD"]);
|
|
3762
3615
|
const branch = git(projectRoot, ["branch", "--show-current"]);
|
|
3763
3616
|
const porcelain = git(projectRoot, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
@@ -3777,13 +3630,13 @@ function createGitCheckpoint(options) {
|
|
|
3777
3630
|
const projectRoot = fs17.realpathSync(path21.resolve(options.project));
|
|
3778
3631
|
const requestedHarmonyRoot = options.harmonyProject ? path21.resolve(options.harmonyProject) : null;
|
|
3779
3632
|
if (requestedHarmonyRoot && (!fs17.existsSync(requestedHarmonyRoot) || !fs17.statSync(requestedHarmonyRoot).isDirectory())) throw new Error(`HarmonyOS project not found: ${requestedHarmonyRoot}`);
|
|
3780
|
-
const
|
|
3633
|
+
const harmonyRoot2 = requestedHarmonyRoot ? fs17.realpathSync(requestedHarmonyRoot) : null;
|
|
3781
3634
|
if (!options.message?.trim()) throw new Error("checkpoint create requires --message");
|
|
3782
3635
|
if (!options.all && options.include.length === 0) throw new Error("checkpoint create requires at least one --include or explicit --all");
|
|
3783
3636
|
const baseline = gitWorkspaceStatus(projectRoot);
|
|
3784
3637
|
if (!baseline.repository) throw new Error(baseline.errors?.[0] ?? "not a Git repository");
|
|
3785
3638
|
const androidRepository = repositoryRoot(projectRoot);
|
|
3786
|
-
const harmonyRepository =
|
|
3639
|
+
const harmonyRepository = harmonyRoot2 ? repositoryRoot(harmonyRoot2) ?? initializeHarmonyRepository(androidRepository, harmonyRoot2).root : androidRepository;
|
|
3787
3640
|
const repositories = [.../* @__PURE__ */ new Set([androidRepository, harmonyRepository])];
|
|
3788
3641
|
for (const root of repositories) {
|
|
3789
3642
|
const before = stagedFiles(root);
|
|
@@ -3796,18 +3649,18 @@ function createGitCheckpoint(options) {
|
|
|
3796
3649
|
if (options.all) {
|
|
3797
3650
|
const androidAdd = git(projectRoot, ["add", "-A", "--", ".", ":!.migration"]);
|
|
3798
3651
|
if (!androidAdd.ok) throw new Error(androidAdd.stderr || "git add failed for Android project");
|
|
3799
|
-
if (
|
|
3800
|
-
const harmonyAdd = git(
|
|
3652
|
+
if (harmonyRoot2 && harmonyRepository !== androidRepository) {
|
|
3653
|
+
const harmonyAdd = git(harmonyRoot2, ["add", "-A", "--", ".", ":!.migration"]);
|
|
3801
3654
|
if (!harmonyAdd.ok) throw new Error(harmonyAdd.stderr || "git add failed for HarmonyOS project");
|
|
3802
|
-
} else if (
|
|
3803
|
-
const harmonyPath = normalized(path21.relative(androidRepository,
|
|
3655
|
+
} else if (harmonyRoot2 && !inside2(projectRoot, harmonyRoot2)) {
|
|
3656
|
+
const harmonyPath = normalized(path21.relative(androidRepository, harmonyRoot2));
|
|
3804
3657
|
const harmonyAdd = git(androidRepository, ["add", "-A", "--", harmonyPath]);
|
|
3805
3658
|
if (!harmonyAdd.ok) throw new Error(harmonyAdd.stderr || "git add failed for HarmonyOS project");
|
|
3806
3659
|
}
|
|
3807
3660
|
} else {
|
|
3808
3661
|
for (const value of options.include) {
|
|
3809
3662
|
const absolute = path21.resolve(projectRoot, value);
|
|
3810
|
-
const root =
|
|
3663
|
+
const root = harmonyRoot2 && inside2(harmonyRoot2, absolute) ? harmonyRepository : (() => {
|
|
3811
3664
|
if (inside2(projectRoot, absolute)) return androidRepository;
|
|
3812
3665
|
return null;
|
|
3813
3666
|
})();
|
|
@@ -3829,7 +3682,7 @@ function createGitCheckpoint(options) {
|
|
|
3829
3682
|
try {
|
|
3830
3683
|
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
3684
|
const absolute = path21.resolve(projectRoot, item.file);
|
|
3832
|
-
const expectedRoot = item.role === "harmony-implementation" ?
|
|
3685
|
+
const expectedRoot = item.role === "harmony-implementation" ? harmonyRoot2 : projectRoot;
|
|
3833
3686
|
const root = item.role === "harmony-implementation" ? harmonyRepository : androidRepository;
|
|
3834
3687
|
const relative2 = normalized(path21.relative(projectRoot, absolute));
|
|
3835
3688
|
if (!expectedRoot || !inside2(expectedRoot, absolute) || !fs17.existsSync(absolute) || !fs17.statSync(absolute).isFile()) {
|
|
@@ -3859,12 +3712,12 @@ function createGitCheckpoint(options) {
|
|
|
3859
3712
|
committedFiles,
|
|
3860
3713
|
repositories: {
|
|
3861
3714
|
android: { root: ".", gitRoot: normalized(path21.relative(projectRoot, androidRepository)) || "." },
|
|
3862
|
-
harmony:
|
|
3715
|
+
harmony: harmonyRoot2 ? { root: normalized(path21.relative(projectRoot, harmonyRoot2)), gitRoot: normalized(path21.relative(projectRoot, harmonyRepository)) } : null
|
|
3863
3716
|
}
|
|
3864
3717
|
};
|
|
3865
3718
|
}
|
|
3866
3719
|
const commitResults = [];
|
|
3867
|
-
const separateHarmony =
|
|
3720
|
+
const separateHarmony = harmonyRoot2 && harmonyRepository !== androidRepository;
|
|
3868
3721
|
if (separateHarmony) {
|
|
3869
3722
|
const files2 = stagedFiles(harmonyRepository);
|
|
3870
3723
|
if (files2.length) {
|
|
@@ -3886,9 +3739,9 @@ function createGitCheckpoint(options) {
|
|
|
3886
3739
|
commitResults.push({ kind: "android", root: androidRepository, commit: git(androidRepository, ["rev-parse", "HEAD"]).stdout, files: androidFiles, summary: androidCommit.stdout });
|
|
3887
3740
|
}
|
|
3888
3741
|
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 (
|
|
3742
|
+
if (harmonyRoot2 && harmonyRepository === androidRepository) {
|
|
3743
|
+
commitResults.push({ kind: "harmony", root: harmonyRepository, commit: androidCheckpoint.commit, files: androidFiles.filter((file) => inside2(harmonyRoot2, path21.resolve(harmonyRepository, file))), summary: androidCheckpoint.summary });
|
|
3744
|
+
} else if (harmonyRoot2 && !commitResults.some((item) => item.kind === "harmony")) {
|
|
3892
3745
|
const harmonyHead = git(harmonyRepository, ["rev-parse", "HEAD"]).stdout;
|
|
3893
3746
|
if (harmonyHead) commitResults.push({ kind: "harmony", root: harmonyRepository, commit: harmonyHead, files: [], summary: "HarmonyOS repository already clean" });
|
|
3894
3747
|
}
|
|
@@ -3921,12 +3774,10 @@ function createGitCheckpoint(options) {
|
|
|
3921
3774
|
|
|
3922
3775
|
// src/commands/system-checks.ts
|
|
3923
3776
|
import fs18 from "fs";
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
function probe(command3, args, timeout = 15e3) {
|
|
3927
|
-
const result = spawnCommandSync(command3, args, { encoding: "utf8", timeout, windowsHide: true });
|
|
3777
|
+
function probe(command2, args, timeout = 15e3) {
|
|
3778
|
+
const result = spawnCommandSync(command2, args, { encoding: "utf8", timeout, windowsHide: true });
|
|
3928
3779
|
return {
|
|
3929
|
-
command:
|
|
3780
|
+
command: command2,
|
|
3930
3781
|
available: !result.error && result.status === 0,
|
|
3931
3782
|
exitCode: Number.isInteger(result.status) ? result.status : null,
|
|
3932
3783
|
output: String(result.stdout || result.stderr || result.error?.message || "").trim().split(/\r?\n/)[0] ?? ""
|
|
@@ -3935,8 +3786,7 @@ function probe(command3, args, timeout = 15e3) {
|
|
|
3935
3786
|
function firstDirectory(values) {
|
|
3936
3787
|
return values.find((value) => Boolean(value && fs18.existsSync(value) && fs18.statSync(value).isDirectory())) ?? null;
|
|
3937
3788
|
}
|
|
3938
|
-
function checkToolchain(
|
|
3939
|
-
const projectRoot = path22.resolve(project);
|
|
3789
|
+
function checkToolchain(_project, env = process.env) {
|
|
3940
3790
|
const pythonCandidates = [
|
|
3941
3791
|
env.PYTHON_BINARY,
|
|
3942
3792
|
process.platform === "win32" ? "python" : "python3",
|
|
@@ -3956,23 +3806,14 @@ function checkToolchain(project, env = process.env) {
|
|
|
3956
3806
|
java: probe("java", ["-version"]),
|
|
3957
3807
|
appgraph: probe("appgraph", ["--version"])
|
|
3958
3808
|
};
|
|
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
3809
|
const devecoHome = firstDirectory([
|
|
3966
3810
|
env.DEVECO_STUDIO_HOME,
|
|
3967
3811
|
env.DEVECO_HOME,
|
|
3968
3812
|
process.platform === "darwin" ? "/Applications/DevEco-Studio.app/Contents" : null
|
|
3969
3813
|
]);
|
|
3970
|
-
const androidBuildEntry = ["gradlew", "gradlew.bat", "settings.gradle", "settings.gradle.kts"].map((name) => path22.join(projectRoot, name)).find((file) => fs18.existsSync(file)) ?? null;
|
|
3971
3814
|
const checks = [
|
|
3972
3815
|
...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] : [] }
|
|
3816
|
+
{ id: "deveco", status: devecoHome ? "passed" : "failed", summary: devecoHome ?? "DevEco Studio not found", evidence: devecoHome ? [devecoHome] : [] }
|
|
3976
3817
|
];
|
|
3977
3818
|
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
3978
3819
|
const pythonMatch = commands.python.output.match(/(\d+)\.(\d+)/);
|
|
@@ -3981,7 +3822,7 @@ function checkToolchain(project, env = process.env) {
|
|
|
3981
3822
|
if (nodeCheck && (nodeMajor < 20 || nodeMajor > 24)) Object.assign(nodeCheck, { status: "failed", summary: `Node.js ${process.versions.node} is outside supported range 20-24` });
|
|
3982
3823
|
const pythonCheck = checks.find((item) => item.id === "python");
|
|
3983
3824
|
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,
|
|
3825
|
+
return { status: checks.some((item) => item.status === "failed") ? "failed" : "passed", checks, commands, devecoHome };
|
|
3985
3826
|
}
|
|
3986
3827
|
function checkAppGraphHealth() {
|
|
3987
3828
|
const version = probe("appgraph", ["--version"]);
|
|
@@ -3993,7 +3834,7 @@ function checkAppGraphHealth() {
|
|
|
3993
3834
|
|
|
3994
3835
|
// src/commands/preflight-checklist.ts
|
|
3995
3836
|
import fs19 from "fs";
|
|
3996
|
-
import
|
|
3837
|
+
import path22 from "path";
|
|
3997
3838
|
var PREFLIGHT_CHECKS = [
|
|
3998
3839
|
{ id: "android-ui-stack", name: "Android UI \u6280\u672F\u6808", required: true },
|
|
3999
3840
|
{ id: "git", name: "Git \u4ED3\u5E93\u548C\u5DE5\u4F5C\u533A", required: true },
|
|
@@ -4027,7 +3868,7 @@ function validateChecklist(value) {
|
|
|
4027
3868
|
return value;
|
|
4028
3869
|
}
|
|
4029
3870
|
function loadPreflightChecklist(file) {
|
|
4030
|
-
const checklistPath =
|
|
3871
|
+
const checklistPath = path22.resolve(file);
|
|
4031
3872
|
if (!fs19.existsSync(checklistPath)) throw new Error(`Preflight checklist not found: ${checklistPath}`);
|
|
4032
3873
|
let checklist;
|
|
4033
3874
|
try {
|
|
@@ -4045,7 +3886,7 @@ function loadPreflightChecklist(file) {
|
|
|
4045
3886
|
|
|
4046
3887
|
// src/commands/preflight.ts
|
|
4047
3888
|
var atomicJson2 = (file, value) => {
|
|
4048
|
-
fs20.mkdirSync(
|
|
3889
|
+
fs20.mkdirSync(path23.dirname(file), { recursive: true });
|
|
4049
3890
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
4050
3891
|
fs20.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
4051
3892
|
`, "utf8");
|
|
@@ -4059,7 +3900,7 @@ function check(id, result, required = true) {
|
|
|
4059
3900
|
return entry;
|
|
4060
3901
|
}
|
|
4061
3902
|
async function runPreflight(options) {
|
|
4062
|
-
const projectRoot =
|
|
3903
|
+
const projectRoot = path23.resolve(options.project);
|
|
4063
3904
|
if (!fs20.existsSync(projectRoot) || !fs20.statSync(projectRoot).isDirectory()) throw new Error(`Android project directory not found: ${projectRoot}`);
|
|
4064
3905
|
const timeoutMs = options.timeoutMs ?? 30 * 60 * 1e3;
|
|
4065
3906
|
const checks = [];
|
|
@@ -4071,7 +3912,7 @@ async function runPreflight(options) {
|
|
|
4071
3912
|
const enabled = (id) => checklist.isEnabled(id);
|
|
4072
3913
|
const skip = (id) => checks.push(check(id, { status: "skipped", summary: "Skipped by preflight checklist", evidence: [checklist.checklistPath] }, required(id)));
|
|
4073
3914
|
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\
|
|
3915
|
+
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
3916
|
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
3917
|
"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
3918
|
"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 +3945,12 @@ async function runPreflight(options) {
|
|
|
4104
3945
|
let harmonyProject = defaultHarmonyProjectPath(projectRoot);
|
|
4105
3946
|
let existingConfig = null;
|
|
4106
3947
|
try {
|
|
4107
|
-
existingConfig = JSON.parse(fs20.readFileSync(
|
|
3948
|
+
existingConfig = JSON.parse(fs20.readFileSync(path23.join(migrationDirectory(projectRoot), "config.json"), "utf8"));
|
|
4108
3949
|
const configuredHarmony = existingConfig.harmonyProject ?? defaultHarmonyProjectPath(projectRoot);
|
|
4109
|
-
harmonyProject =
|
|
3950
|
+
harmonyProject = path23.isAbsolute(configuredHarmony) ? configuredHarmony : path23.resolve(projectRoot, configuredHarmony);
|
|
4110
3951
|
} catch {
|
|
4111
3952
|
}
|
|
4112
|
-
const relativeHarmony =
|
|
3953
|
+
const relativeHarmony = path23.relative(projectRoot, harmonyProject).replaceAll("\\", "/");
|
|
4113
3954
|
const ignoreUntrackedRoots = existingConfig && relativeHarmony && !relativeHarmony.startsWith("../") ? [relativeHarmony] : [];
|
|
4114
3955
|
git2 = gitWorkspaceStatus(projectRoot, { ignoreUntrackedRoots });
|
|
4115
3956
|
const hasRequiredFailures = () => checks.some((item) => item.required && item.status !== "passed" && item.status !== "skipped");
|
|
@@ -4126,7 +3967,7 @@ async function runPreflight(options) {
|
|
|
4126
3967
|
result,
|
|
4127
3968
|
checks: checks.map(({ id, status, summary, evidence: evidence2, retry }) => retry ? { id, status, summary, evidence: evidence2, retry } : { id, status, summary, evidence: evidence2 })
|
|
4128
3969
|
};
|
|
4129
|
-
const reportPath =
|
|
3970
|
+
const reportPath = path23.join(migrationDirectory(projectRoot), "preflight-report.json");
|
|
4130
3971
|
atomicJson2(reportPath, report);
|
|
4131
3972
|
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
3973
|
`);
|
|
@@ -4174,7 +4015,7 @@ async function runPreflight(options) {
|
|
|
4174
4015
|
let configInit;
|
|
4175
4016
|
if (enabled("config")) displayGate("config");
|
|
4176
4017
|
try {
|
|
4177
|
-
const existingConfigPath =
|
|
4018
|
+
const existingConfigPath = path23.join(migrationDirectory(projectRoot), "config.json");
|
|
4178
4019
|
configInit = !enabled("config") && fs20.existsSync(existingConfigPath) ? { configPath: existingConfigPath, config: JSON.parse(fs20.readFileSync(existingConfigPath, "utf8")) } : initMigrationConfig({ project: projectRoot });
|
|
4179
4020
|
} catch (error) {
|
|
4180
4021
|
checks.push(check("config", { status: "failed", summary: error instanceof Error ? error.message : String(error) }));
|
|
@@ -4182,26 +4023,27 @@ async function runPreflight(options) {
|
|
|
4182
4023
|
}
|
|
4183
4024
|
const configPath = configInit.configPath;
|
|
4184
4025
|
const config = configInit.config;
|
|
4185
|
-
const configuredHarmonyProject = typeof config.harmonyProject === "string" ? config.harmonyProject :
|
|
4186
|
-
harmonyProject =
|
|
4026
|
+
const configuredHarmonyProject = typeof config.harmonyProject === "string" ? config.harmonyProject : path23.relative(projectRoot, defaultHarmonyProjectPath(projectRoot));
|
|
4027
|
+
harmonyProject = path23.isAbsolute(configuredHarmonyProject) ? configuredHarmonyProject : path23.resolve(projectRoot, configuredHarmonyProject);
|
|
4187
4028
|
if (enabled("config")) checks.push(check("config", { ...validateMigrationConfig(projectRoot), summary: "Migration config initialized and validated", evidence: [configPath] }));
|
|
4188
4029
|
else skip("config");
|
|
4189
4030
|
if (hasRequiredFailures()) return finish();
|
|
4190
4031
|
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 {
|
|
4032
|
+
else {
|
|
4195
4033
|
displayGate("android-build");
|
|
4196
4034
|
checks.push(check("android-build", {
|
|
4197
4035
|
status: "not_applicable",
|
|
4198
|
-
summary: "Android \u6784\u5EFA\
|
|
4036
|
+
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
4037
|
}, required("android-build")));
|
|
4200
4038
|
}
|
|
4201
|
-
if (enabled("android-emulator"))
|
|
4039
|
+
if (!enabled("android-emulator")) skip("android-emulator");
|
|
4040
|
+
else {
|
|
4202
4041
|
displayGate("android-emulator");
|
|
4203
|
-
checks.push(check("android-emulator",
|
|
4204
|
-
|
|
4042
|
+
checks.push(check("android-emulator", {
|
|
4043
|
+
status: "not_applicable",
|
|
4044
|
+
summary: "Android \u6A21\u62DF\u5668\u68C0\u6D4B\u5DF2\u79FB\u9664\uFF08\u8FC1\u79FB\u6D41\u7A0B\u4E0D\u4F9D\u8D56 Android \u8FD0\u884C\u73AF\u5883\uFF09"
|
|
4045
|
+
}, required("android-emulator")));
|
|
4046
|
+
}
|
|
4205
4047
|
if (hasRequiredFailures()) return finish();
|
|
4206
4048
|
if (enabled("harmony-project")) {
|
|
4207
4049
|
displayGate("harmony-project");
|
|
@@ -4240,9 +4082,9 @@ async function runPreflight(options) {
|
|
|
4240
4082
|
}
|
|
4241
4083
|
|
|
4242
4084
|
// src/commands/deveco-auth-login.ts
|
|
4243
|
-
import {
|
|
4085
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
4244
4086
|
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
|
|
4087
|
+
import path24 from "path";
|
|
4246
4088
|
import { setTimeout as delay2 } from "timers/promises";
|
|
4247
4089
|
var MANAGED_LOGIN_SERVICE_NAME = "devecocli-auth";
|
|
4248
4090
|
var LOGIN_URL_PATTERN = /https?:\/\/[^\s"'<>]+/i;
|
|
@@ -4251,13 +4093,13 @@ function isDevEcoCliLoginCommand(commandLine) {
|
|
|
4251
4093
|
return DEVECO_LOGIN_COMMAND_PATTERN.test(commandLine);
|
|
4252
4094
|
}
|
|
4253
4095
|
function managedLoginStatePaths(project) {
|
|
4254
|
-
const directory =
|
|
4255
|
-
const pidFile =
|
|
4096
|
+
const directory = path24.join(migrationDirectory(project), "services");
|
|
4097
|
+
const pidFile = path24.join(directory, `${MANAGED_LOGIN_SERVICE_NAME}.pid`);
|
|
4256
4098
|
return {
|
|
4257
4099
|
directory,
|
|
4258
4100
|
pidFile,
|
|
4259
|
-
stdoutLog:
|
|
4260
|
-
stderrLog:
|
|
4101
|
+
stdoutLog: path24.join(directory, `${MANAGED_LOGIN_SERVICE_NAME}.stdout.log`),
|
|
4102
|
+
stderrLog: path24.join(directory, `${MANAGED_LOGIN_SERVICE_NAME}.stderr.log`),
|
|
4261
4103
|
exitFile: `${pidFile}.exit`,
|
|
4262
4104
|
errorFile: `${pidFile}.error`
|
|
4263
4105
|
};
|
|
@@ -4288,7 +4130,7 @@ function tailFile(file, maxChars) {
|
|
|
4288
4130
|
}
|
|
4289
4131
|
}
|
|
4290
4132
|
function listPosixProcesses() {
|
|
4291
|
-
const result =
|
|
4133
|
+
const result = spawnSync6("ps", ["-eo", "pid=,command="], { encoding: "utf8", timeout: 15e3 });
|
|
4292
4134
|
if (result.error || result.status !== 0) return [];
|
|
4293
4135
|
return result.stdout.split("\n").flatMap((line) => {
|
|
4294
4136
|
const trimmed = line.trim();
|
|
@@ -4296,15 +4138,15 @@ function listPosixProcesses() {
|
|
|
4296
4138
|
const separator = trimmed.indexOf(" ");
|
|
4297
4139
|
if (separator <= 0) return [];
|
|
4298
4140
|
const pid = Number.parseInt(trimmed.slice(0, separator), 10);
|
|
4299
|
-
const
|
|
4300
|
-
return Number.isInteger(pid) && pid > 0 ? [{ pid, command:
|
|
4141
|
+
const command2 = trimmed.slice(separator + 1);
|
|
4142
|
+
return Number.isInteger(pid) && pid > 0 ? [{ pid, command: command2 }] : [];
|
|
4301
4143
|
});
|
|
4302
4144
|
}
|
|
4303
4145
|
function listWindowsProcesses() {
|
|
4304
4146
|
const source = 'Get-CimInstance Win32_Process -Filter "CommandLine IS NOT NULL" | ForEach-Object { "$($_.ProcessId)`t$($_.CommandLine)" }';
|
|
4305
4147
|
const encoded = Buffer.from(source, "utf16le").toString("base64");
|
|
4306
|
-
const powershell = process.env.SystemRoot ?
|
|
4307
|
-
const result =
|
|
4148
|
+
const powershell = process.env.SystemRoot ? path24.resolve(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
|
|
4149
|
+
const result = spawnSync6(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
|
|
4308
4150
|
encoding: "utf8",
|
|
4309
4151
|
timeout: 3e4,
|
|
4310
4152
|
windowsHide: true
|
|
@@ -4314,8 +4156,8 @@ function listWindowsProcesses() {
|
|
|
4314
4156
|
const separator = line.indexOf(" ");
|
|
4315
4157
|
if (separator <= 0) return [];
|
|
4316
4158
|
const pid = Number.parseInt(line.slice(0, separator), 10);
|
|
4317
|
-
const
|
|
4318
|
-
return Number.isInteger(pid) && pid > 0 ? [{ pid, command:
|
|
4159
|
+
const command2 = line.slice(separator + 1);
|
|
4160
|
+
return Number.isInteger(pid) && pid > 0 ? [{ pid, command: command2 }] : [];
|
|
4319
4161
|
});
|
|
4320
4162
|
}
|
|
4321
4163
|
function listDevEcoCliLoginProcesses(options = {}) {
|
|
@@ -4327,7 +4169,7 @@ function listDevEcoCliLoginProcesses(options = {}) {
|
|
|
4327
4169
|
}
|
|
4328
4170
|
async function terminateProcessTree(pid) {
|
|
4329
4171
|
if (process.platform === "win32") {
|
|
4330
|
-
|
|
4172
|
+
spawnSync6("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", timeout: 15e3, windowsHide: true });
|
|
4331
4173
|
return;
|
|
4332
4174
|
}
|
|
4333
4175
|
try {
|
|
@@ -4345,7 +4187,7 @@ async function terminateProcessTree(pid) {
|
|
|
4345
4187
|
}
|
|
4346
4188
|
}
|
|
4347
4189
|
async function killOrphanDevEcoCliLogins(options) {
|
|
4348
|
-
const state = managedLoginStatePaths(
|
|
4190
|
+
const state = managedLoginStatePaths(path24.resolve(options.project));
|
|
4349
4191
|
const terminate = options.terminator ?? terminateProcessTree;
|
|
4350
4192
|
const targets = /* @__PURE__ */ new Set();
|
|
4351
4193
|
const managedPid = readPidFile(state.pidFile);
|
|
@@ -4373,12 +4215,12 @@ async function killOrphanDevEcoCliLogins(options) {
|
|
|
4373
4215
|
}
|
|
4374
4216
|
function runDetachedDevEcoAuthHost(requestPath) {
|
|
4375
4217
|
const request = JSON.parse(readFileSync5(requestPath, "utf8"));
|
|
4376
|
-
mkdirSync3(
|
|
4377
|
-
mkdirSync3(
|
|
4218
|
+
mkdirSync3(path24.dirname(request.stdout), { recursive: true });
|
|
4219
|
+
mkdirSync3(path24.dirname(request.stderr), { recursive: true });
|
|
4378
4220
|
const stdout = openSync3(request.stdout, "a");
|
|
4379
4221
|
const stderr = openSync3(request.stderr, "a");
|
|
4380
4222
|
try {
|
|
4381
|
-
const child =
|
|
4223
|
+
const child = spawnCommand(request.command[0], request.command.slice(1), {
|
|
4382
4224
|
cwd: request.cwd,
|
|
4383
4225
|
detached: true,
|
|
4384
4226
|
env: { ...process.env, ...request.env ?? {} },
|
|
@@ -4386,6 +4228,13 @@ function runDetachedDevEcoAuthHost(requestPath) {
|
|
|
4386
4228
|
windowsHide: true
|
|
4387
4229
|
});
|
|
4388
4230
|
const stdin = child.stdin;
|
|
4231
|
+
if (!stdin) {
|
|
4232
|
+
try {
|
|
4233
|
+
child.kill();
|
|
4234
|
+
} catch {
|
|
4235
|
+
}
|
|
4236
|
+
throw new Error("DevEco CLI \u767B\u5F55\u8FDB\u7A0B\u7684 stdin \u4E0D\u53EF\u7528");
|
|
4237
|
+
}
|
|
4389
4238
|
stdin.on("error", () => {
|
|
4390
4239
|
});
|
|
4391
4240
|
stdin.write("\n");
|
|
@@ -4437,7 +4286,7 @@ async function managedDevEcoCliLogin(options) {
|
|
|
4437
4286
|
const statusTimeoutMs = options.statusTimeoutMs ?? 15e3;
|
|
4438
4287
|
const progress = options.onProgress ?? (() => {
|
|
4439
4288
|
});
|
|
4440
|
-
const projectRoot =
|
|
4289
|
+
const projectRoot = path24.resolve(options.project);
|
|
4441
4290
|
const state = managedLoginStatePaths(projectRoot);
|
|
4442
4291
|
const failure = (summary, extra = {}) => ({
|
|
4443
4292
|
status: "failed",
|
|
@@ -4476,7 +4325,7 @@ async function managedDevEcoCliLogin(options) {
|
|
|
4476
4325
|
stdout: state.stdoutLog,
|
|
4477
4326
|
stderr: state.stderrLog,
|
|
4478
4327
|
command: devEcoCliSpawn(executable, ["auth", "login"]),
|
|
4479
|
-
env:
|
|
4328
|
+
env: devEcoCliEnvironment()
|
|
4480
4329
|
}, null, 2), "utf8");
|
|
4481
4330
|
launchHostOutsideRunner(requestPath, options.hostCommand);
|
|
4482
4331
|
progress(`\u5DF2\u6301\u4E45\u62C9\u8D77\u767B\u5F55\u8FDB\u7A0B\uFF08\u56DE\u8C03\u670D\u52A1\u5668\u968F\u8FDB\u7A0B\u5B58\u6D3B\uFF09\uFF0C\u65E5\u5FD7: ${state.stdoutLog}`);
|
|
@@ -4531,17 +4380,17 @@ async function managedDevEcoCliLogin(options) {
|
|
|
4531
4380
|
|
|
4532
4381
|
// src/commands/device-evidence.ts
|
|
4533
4382
|
import fs21 from "fs";
|
|
4534
|
-
import
|
|
4383
|
+
import path25 from "path";
|
|
4535
4384
|
import { createHash as createHash6 } from "crypto";
|
|
4536
|
-
import { spawnSync as
|
|
4385
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
4537
4386
|
import { inflateSync } from "zlib";
|
|
4538
4387
|
function safeName(value) {
|
|
4539
4388
|
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
4389
|
return value;
|
|
4541
4390
|
}
|
|
4542
|
-
function run(
|
|
4543
|
-
const script = /\.m?js$/i.test(
|
|
4544
|
-
const result =
|
|
4391
|
+
function run(command2, args, encoding = "utf8") {
|
|
4392
|
+
const script = /\.m?js$/i.test(command2);
|
|
4393
|
+
const result = spawnSync7(script ? process.execPath : command2, script ? [command2, ...args] : args, {
|
|
4545
4394
|
encoding: encoding === "buffer" ? null : encoding,
|
|
4546
4395
|
shell: false,
|
|
4547
4396
|
timeout: 3e4,
|
|
@@ -4553,7 +4402,7 @@ function run(command3, args, encoding = "utf8") {
|
|
|
4553
4402
|
status: Number.isInteger(result.status) ? result.status : null,
|
|
4554
4403
|
stdout: result.stdout ?? (encoding === "buffer" ? Buffer.alloc(0) : ""),
|
|
4555
4404
|
stderr: String(result.stderr ?? result.error?.message ?? ""),
|
|
4556
|
-
invocation: [
|
|
4405
|
+
invocation: [command2, ...args]
|
|
4557
4406
|
};
|
|
4558
4407
|
}
|
|
4559
4408
|
function writeResult(file, result) {
|
|
@@ -4649,72 +4498,39 @@ function pixelMetrics(leftFile, rightFile) {
|
|
|
4649
4498
|
return null;
|
|
4650
4499
|
}
|
|
4651
4500
|
}
|
|
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
4501
|
function captureDeviceEvidence(options) {
|
|
4667
|
-
const projectRoot =
|
|
4502
|
+
const projectRoot = path25.resolve(options.project);
|
|
4668
4503
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
4669
4504
|
const workspaceRoot = migrationWorkspaceRoot(projectRoot);
|
|
4670
4505
|
const name = safeName(options.name);
|
|
4671
4506
|
const capturedAt = /* @__PURE__ */ new Date();
|
|
4672
|
-
const directory =
|
|
4507
|
+
const directory = path25.join(migrationRoot, "evidence", name, options.platform);
|
|
4673
4508
|
fs21.mkdirSync(directory, { recursive: true });
|
|
4674
|
-
const screenshot =
|
|
4675
|
-
const layout =
|
|
4676
|
-
const logs =
|
|
4509
|
+
const screenshot = path25.join(directory, "screenshot.png");
|
|
4510
|
+
const layout = path25.join(directory, "layout.json");
|
|
4511
|
+
const logs = path25.join(directory, "device.log");
|
|
4677
4512
|
const commands = [];
|
|
4678
4513
|
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
|
-
}
|
|
4514
|
+
const hdc = options.hdc ?? process.env.HDC_BINARY ?? "hdc";
|
|
4515
|
+
const listed = run(hdc, ["list", "targets"]);
|
|
4516
|
+
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));
|
|
4517
|
+
if (!listed.ok || !candidates.length) throw new Error(`${hdc} target discovery failed: ${listed.stderr || "no targets"}`);
|
|
4518
|
+
target = options.target ?? process.env.DROID2HMOS_HDC_TARGET ?? selectPreferredTarget(candidates);
|
|
4519
|
+
const prefix = ["-t", target];
|
|
4520
|
+
const remoteShot = `/data/local/tmp/droid2hmos-${process.pid}.png`;
|
|
4521
|
+
const remoteLayout = `/data/local/tmp/droid2hmos-${process.pid}.json`;
|
|
4522
|
+
const shot = run(hdc, [...prefix, "shell", "snapshot_display", "-f", remoteShot]);
|
|
4523
|
+
commands.push(shot);
|
|
4524
|
+
const recvShot = run(hdc, [...prefix, "file", "recv", remoteShot, screenshot]);
|
|
4525
|
+
commands.push(recvShot);
|
|
4526
|
+
const dump = run(hdc, [...prefix, "shell", "uitest", "dumpLayout", "-p", remoteLayout]);
|
|
4527
|
+
commands.push(dump);
|
|
4528
|
+
const recvLayout = run(hdc, [...prefix, "file", "recv", remoteLayout, layout]);
|
|
4529
|
+
commands.push(recvLayout);
|
|
4530
|
+
run(hdc, [...prefix, "shell", "rm", "-f", remoteShot, remoteLayout]);
|
|
4531
|
+
const log = run(hdc, [...prefix, "shell", "hilog", "-x"]);
|
|
4532
|
+
commands.push(log);
|
|
4533
|
+
writeResult(logs, log);
|
|
4718
4534
|
const artifacts = [screenshot, layout, logs].map(fileInfo).filter(Boolean);
|
|
4719
4535
|
const failures = commands.filter((item) => !item.ok).map((item) => ({ invocation: item.invocation, status: item.status, stderr: item.stderr }));
|
|
4720
4536
|
const metadata = {
|
|
@@ -4724,10 +4540,10 @@ function captureDeviceEvidence(options) {
|
|
|
4724
4540
|
target,
|
|
4725
4541
|
capturedAt: capturedAt.toISOString(),
|
|
4726
4542
|
status: failures.length === 0 && artifacts.length === 3 ? "passed" : "failed",
|
|
4727
|
-
artifacts: artifacts.map((item) => ({ ...item, path:
|
|
4543
|
+
artifacts: artifacts.map((item) => ({ ...item, path: path25.relative(workspaceRoot, item.path).replaceAll("\\", "/") })),
|
|
4728
4544
|
failures
|
|
4729
4545
|
};
|
|
4730
|
-
const metadataPath =
|
|
4546
|
+
const metadataPath = path25.join(directory, "evidence.json");
|
|
4731
4547
|
fs21.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}
|
|
4732
4548
|
`, "utf8");
|
|
4733
4549
|
return { ...metadata, metadataPath };
|
|
@@ -4737,24 +4553,24 @@ function tokens(file) {
|
|
|
4737
4553
|
return new Set(fs21.readFileSync(file, "utf8").toLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
|
|
4738
4554
|
}
|
|
4739
4555
|
function compareEvidence(options) {
|
|
4740
|
-
const projectRoot =
|
|
4556
|
+
const projectRoot = path25.resolve(options.project);
|
|
4741
4557
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
4742
4558
|
const resolveEvidence = (value) => {
|
|
4743
4559
|
const normalized2 = value.replaceAll("\\", "/");
|
|
4744
|
-
const root =
|
|
4745
|
-
if (normalized2.startsWith(".migration/")) return
|
|
4746
|
-
return
|
|
4560
|
+
const root = path25.isAbsolute(value) ? value : (() => {
|
|
4561
|
+
if (normalized2.startsWith(".migration/")) return path25.resolve(migrationWorkspaceRoot(projectRoot), value);
|
|
4562
|
+
return path25.resolve(projectRoot, value);
|
|
4747
4563
|
})();
|
|
4748
|
-
return fs21.statSync(root).isDirectory() ? root :
|
|
4564
|
+
return fs21.statSync(root).isDirectory() ? root : path25.dirname(root);
|
|
4749
4565
|
};
|
|
4750
4566
|
const androidRoot = resolveEvidence(options.android);
|
|
4751
|
-
const
|
|
4752
|
-
const androidShot = fileInfo(
|
|
4753
|
-
const harmonyShot = fileInfo(
|
|
4754
|
-
const pixels = pixelMetrics(
|
|
4755
|
-
const layoutFile = (root) => [
|
|
4567
|
+
const harmonyRoot2 = resolveEvidence(options.harmony);
|
|
4568
|
+
const androidShot = fileInfo(path25.join(androidRoot, "screenshot.png"));
|
|
4569
|
+
const harmonyShot = fileInfo(path25.join(harmonyRoot2, "screenshot.png"));
|
|
4570
|
+
const pixels = pixelMetrics(path25.join(androidRoot, "screenshot.png"), path25.join(harmonyRoot2, "screenshot.png"));
|
|
4571
|
+
const layoutFile = (root) => [path25.join(root, "layout.json"), path25.join(root, "layout.xml")].find((file) => fs21.existsSync(file)) ?? path25.join(root, "layout.json");
|
|
4756
4572
|
const left = tokens(layoutFile(androidRoot));
|
|
4757
|
-
const right = tokens(layoutFile(
|
|
4573
|
+
const right = tokens(layoutFile(harmonyRoot2));
|
|
4758
4574
|
const intersection = [...left].filter((value) => right.has(value));
|
|
4759
4575
|
const union = /* @__PURE__ */ new Set([...left, ...right]);
|
|
4760
4576
|
const result = {
|
|
@@ -4762,7 +4578,7 @@ function compareEvidence(options) {
|
|
|
4762
4578
|
name: safeName(options.name),
|
|
4763
4579
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4764
4580
|
android: { root: androidRoot, screenshot: androidShot, layoutTokens: left.size },
|
|
4765
|
-
harmony: { root:
|
|
4581
|
+
harmony: { root: harmonyRoot2, screenshot: harmonyShot, layoutTokens: right.size },
|
|
4766
4582
|
screenshot: {
|
|
4767
4583
|
sameBytes: Boolean(androidShot && harmonyShot && androidShot.sha256 === harmonyShot.sha256),
|
|
4768
4584
|
sameDimensions: Boolean(androidShot && harmonyShot && androidShot.width === harmonyShot.width && androidShot.height === harmonyShot.height),
|
|
@@ -4771,27 +4587,27 @@ function compareEvidence(options) {
|
|
|
4771
4587
|
layout: { sharedTokens: intersection.length, unionTokens: union.size, jaccard: union.size ? intersection.length / union.size : 1 },
|
|
4772
4588
|
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
4589
|
};
|
|
4774
|
-
const output =
|
|
4775
|
-
fs21.mkdirSync(
|
|
4590
|
+
const output = path25.join(migrationRoot, "evidence", safeName(options.name), "comparison.json");
|
|
4591
|
+
fs21.mkdirSync(path25.dirname(output), { recursive: true });
|
|
4776
4592
|
fs21.writeFileSync(output, `${JSON.stringify(result, null, 2)}
|
|
4777
4593
|
`, "utf8");
|
|
4778
4594
|
return { status: "passed", output, result };
|
|
4779
4595
|
}
|
|
4780
4596
|
function writeFlowBundle(options) {
|
|
4781
|
-
const projectRoot =
|
|
4597
|
+
const projectRoot = path25.resolve(options.project);
|
|
4782
4598
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
4783
|
-
const output =
|
|
4599
|
+
const output = path25.join(migrationRoot, "evidence", safeName(options.name), "flow.json");
|
|
4784
4600
|
const value = {
|
|
4785
4601
|
flowVersion: "1.0.0",
|
|
4786
4602
|
name: options.name,
|
|
4787
4603
|
platform: options.platform,
|
|
4788
4604
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4789
4605
|
status: options.before.status === "passed" && options.execution.status === "passed" && options.after.status === "passed" ? "passed" : "failed",
|
|
4790
|
-
before:
|
|
4606
|
+
before: path25.relative(migrationWorkspaceRoot(projectRoot), options.before.metadataPath).replaceAll("\\", "/"),
|
|
4791
4607
|
execution: options.execution,
|
|
4792
|
-
after:
|
|
4608
|
+
after: path25.relative(migrationWorkspaceRoot(projectRoot), options.after.metadataPath).replaceAll("\\", "/")
|
|
4793
4609
|
};
|
|
4794
|
-
fs21.mkdirSync(
|
|
4610
|
+
fs21.mkdirSync(path25.dirname(output), { recursive: true });
|
|
4795
4611
|
fs21.writeFileSync(output, `${JSON.stringify(value, null, 2)}
|
|
4796
4612
|
`, "utf8");
|
|
4797
4613
|
return { ...value, output };
|
|
@@ -4799,7 +4615,7 @@ function writeFlowBundle(options) {
|
|
|
4799
4615
|
|
|
4800
4616
|
// src/commands/report-update.ts
|
|
4801
4617
|
import fs22 from "fs";
|
|
4802
|
-
import
|
|
4618
|
+
import path26 from "path";
|
|
4803
4619
|
var allowedStatuses = /* @__PURE__ */ new Set(["pending", "implemented", "verified", "blocked"]);
|
|
4804
4620
|
function readJson3(file) {
|
|
4805
4621
|
return JSON.parse(fs22.readFileSync(file, "utf8"));
|
|
@@ -4811,48 +4627,48 @@ function atomicJson3(file, value) {
|
|
|
4811
4627
|
fs22.renameSync(temporary, file);
|
|
4812
4628
|
}
|
|
4813
4629
|
function readHarmonyRoot(projectRoot) {
|
|
4814
|
-
const configPath =
|
|
4630
|
+
const configPath = path26.join(migrationDirectory(projectRoot), "config.json");
|
|
4815
4631
|
if (!fs22.existsSync(configPath)) return null;
|
|
4816
4632
|
const config = readJson3(configPath);
|
|
4817
4633
|
const harmonyProject = config?.harmonyProject;
|
|
4818
4634
|
if (typeof harmonyProject !== "string" || !harmonyProject) return null;
|
|
4819
|
-
const
|
|
4820
|
-
return fs22.existsSync(
|
|
4635
|
+
const harmonyRoot2 = path26.isAbsolute(harmonyProject) ? harmonyProject : path26.resolve(projectRoot, harmonyProject);
|
|
4636
|
+
return fs22.existsSync(harmonyRoot2) ? harmonyRoot2 : null;
|
|
4821
4637
|
}
|
|
4822
|
-
function insideProject(projectRoot,
|
|
4823
|
-
const absolute = value.replaceAll("\\", "/").startsWith(".migration/") ?
|
|
4824
|
-
const relative2 =
|
|
4825
|
-
const inProject = !!relative2 && !relative2.startsWith("../") && !
|
|
4638
|
+
function insideProject(projectRoot, harmonyRoot2, workspaceRoot, value, mustExist) {
|
|
4639
|
+
const absolute = value.replaceAll("\\", "/").startsWith(".migration/") ? path26.resolve(workspaceRoot, value) : path26.resolve(projectRoot, value);
|
|
4640
|
+
const relative2 = path26.relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
4641
|
+
const inProject = !!relative2 && !relative2.startsWith("../") && !path26.isAbsolute(relative2);
|
|
4826
4642
|
let inHarmony = false;
|
|
4827
|
-
if (!inProject &&
|
|
4828
|
-
const relativeHarmony =
|
|
4829
|
-
inHarmony = !!relativeHarmony && !relativeHarmony.startsWith("../") && !
|
|
4643
|
+
if (!inProject && harmonyRoot2) {
|
|
4644
|
+
const relativeHarmony = path26.relative(harmonyRoot2, absolute).replaceAll("\\", "/");
|
|
4645
|
+
inHarmony = !!relativeHarmony && !relativeHarmony.startsWith("../") && !path26.isAbsolute(relativeHarmony) && !relative2.startsWith("/");
|
|
4830
4646
|
}
|
|
4831
|
-
const migrationRelative =
|
|
4832
|
-
const inMigration = !!migrationRelative && !migrationRelative.startsWith("../") && !
|
|
4647
|
+
const migrationRelative = path26.relative(workspaceRoot, absolute).replaceAll("\\", "/");
|
|
4648
|
+
const inMigration = !!migrationRelative && !migrationRelative.startsWith("../") && !path26.isAbsolute(migrationRelative);
|
|
4833
4649
|
if (!inProject && !inHarmony && !inMigration) throw new Error(`Path must be inside the project: ${value}`);
|
|
4834
4650
|
if (mustExist && !fs22.existsSync(absolute)) throw new Error(`Referenced file not found: ${migrationRelative !== "" && inMigration ? migrationRelative : relative2}`);
|
|
4835
4651
|
return inMigration ? migrationRelative : relative2;
|
|
4836
4652
|
}
|
|
4837
4653
|
function updateReportItem(options) {
|
|
4838
4654
|
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 =
|
|
4655
|
+
const projectRoot = path26.resolve(options.project);
|
|
4656
|
+
const harmonyRoot2 = readHarmonyRoot(projectRoot);
|
|
4657
|
+
const reportRoot = path26.join(migrationDirectory(projectRoot), "report");
|
|
4658
|
+
const indexPath = path26.join(reportRoot, "index.json");
|
|
4843
4659
|
if (!fs22.existsSync(indexPath)) throw new Error("Migration report is not initialized");
|
|
4844
4660
|
if (options.status && !allowedStatuses.has(options.status)) throw new Error(`Invalid report status: ${options.status}`);
|
|
4845
4661
|
const index = readJson3(indexPath);
|
|
4846
4662
|
const reportFile = (value) => {
|
|
4847
|
-
if (typeof value !== "string" || !value.trim() ||
|
|
4848
|
-
const absolute =
|
|
4849
|
-
const relation =
|
|
4850
|
-
if (!relation || relation === ".." || relation.startsWith(`..${
|
|
4663
|
+
if (typeof value !== "string" || !value.trim() || path26.isAbsolute(value)) throw new Error(`Report shard path must be report-relative: ${String(value)}`);
|
|
4664
|
+
const absolute = path26.resolve(reportRoot, value);
|
|
4665
|
+
const relation = path26.relative(reportRoot, absolute);
|
|
4666
|
+
if (!relation || relation === ".." || relation.startsWith(`..${path26.sep}`) || path26.isAbsolute(relation)) throw new Error(`Report shard escapes report root: ${value}`);
|
|
4851
4667
|
if (!fs22.existsSync(absolute) || !fs22.statSync(absolute).isFile()) throw new Error(`Report shard not found: ${value}`);
|
|
4852
4668
|
const realRoot = fs22.realpathSync(reportRoot);
|
|
4853
4669
|
const realFile = fs22.realpathSync(absolute);
|
|
4854
|
-
const realRelation =
|
|
4855
|
-
if (realRelation === ".." || realRelation.startsWith(`..${
|
|
4670
|
+
const realRelation = path26.relative(realRoot, realFile);
|
|
4671
|
+
if (realRelation === ".." || realRelation.startsWith(`..${path26.sep}`) || path26.isAbsolute(realRelation)) throw new Error(`Report shard resolves outside report root: ${value}`);
|
|
4856
4672
|
return realFile;
|
|
4857
4673
|
};
|
|
4858
4674
|
const candidates = [
|
|
@@ -4875,19 +4691,19 @@ function updateReportItem(options) {
|
|
|
4875
4691
|
if (options.harmonyosFiles) match.item.harmonyosFiles = [...new Set(options.harmonyosFiles.map((file) => managedHarmonyFile(projectRoot, file).relative))];
|
|
4876
4692
|
if (options.androidSourceFiles) match.item.sourceProvenance = provenanceForFiles(projectRoot, options.androidSourceFiles);
|
|
4877
4693
|
if (options.sourceProvenance) match.item.sourceProvenance = options.sourceProvenance;
|
|
4878
|
-
if (options.testEvidence) match.item.testEvidence = [...new Set(options.testEvidence.map((file) => insideProject(projectRoot,
|
|
4694
|
+
if (options.testEvidence) match.item.testEvidence = [...new Set(options.testEvidence.map((file) => insideProject(projectRoot, harmonyRoot2, workspaceRoot, file, true)))];
|
|
4879
4695
|
if (options.difference !== void 0) match.item.difference = options.difference?.trim() || null;
|
|
4880
4696
|
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
4697
|
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
4698
|
if (match.item.status === "verified" && !match.item.testEvidence?.length) throw new Error("verified requires at least one test evidence file");
|
|
4883
4699
|
if (match.item.status === "blocked" && !match.item.difference) throw new Error("blocked requires --difference");
|
|
4884
4700
|
atomicJson3(match.file, match.document);
|
|
4885
|
-
return { status: "passed", ref: options.ref, file:
|
|
4701
|
+
return { status: "passed", ref: options.ref, file: path26.relative(projectRoot, match.file).replaceAll("\\", "/"), item: match.item };
|
|
4886
4702
|
}
|
|
4887
4703
|
|
|
4888
4704
|
// src/commands/mock-server-generator.ts
|
|
4889
4705
|
import fs23 from "fs";
|
|
4890
|
-
import
|
|
4706
|
+
import path27 from "path";
|
|
4891
4707
|
var requiredScenarios = ["success", "empty", "business-failure", "boundary"];
|
|
4892
4708
|
function validateSpec(spec) {
|
|
4893
4709
|
const errors = [];
|
|
@@ -4947,21 +4763,21 @@ const server = http.createServer(async (request, response) => {
|
|
|
4947
4763
|
server.listen(port, host, () => console.log(JSON.stringify({ status: "ready", host, port })));
|
|
4948
4764
|
`;
|
|
4949
4765
|
function generateMockServer(options) {
|
|
4950
|
-
const projectRoot =
|
|
4951
|
-
const specFile =
|
|
4766
|
+
const projectRoot = path27.resolve(options.project);
|
|
4767
|
+
const specFile = path27.resolve(options.specFile);
|
|
4952
4768
|
if (!fs23.existsSync(specFile)) throw new Error(`Protocol spec not found: ${specFile}`);
|
|
4953
4769
|
const spec = JSON.parse(fs23.readFileSync(specFile, "utf8"));
|
|
4954
4770
|
const errors = validateSpec(spec);
|
|
4955
4771
|
if (errors.length) throw new Error(`Invalid protocol spec: ${errors.join("; ")}`);
|
|
4956
|
-
const root =
|
|
4772
|
+
const root = path27.join(projectRoot, "mock-server");
|
|
4957
4773
|
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
4774
|
fs23.mkdirSync(root, { recursive: true });
|
|
4959
|
-
fs23.writeFileSync(
|
|
4775
|
+
fs23.writeFileSync(path27.join(root, "scenarios.json"), `${JSON.stringify(spec, null, 2)}
|
|
4960
4776
|
`, "utf8");
|
|
4961
|
-
fs23.writeFileSync(
|
|
4962
|
-
fs23.writeFileSync(
|
|
4777
|
+
fs23.writeFileSync(path27.join(root, "server.mjs"), serverSource, "utf8");
|
|
4778
|
+
fs23.writeFileSync(path27.join(root, "README.md"), `# Mock Server
|
|
4963
4779
|
|
|
4964
|
-
Generated from \`${
|
|
4780
|
+
Generated from \`${path27.relative(projectRoot, specFile).replaceAll("\\", "/")}\`.
|
|
4965
4781
|
|
|
4966
4782
|
Start safely:
|
|
4967
4783
|
|
|
@@ -4976,16 +4792,16 @@ Select a scenario with query parameter \`?scenario=empty\` or header \`x-droid2h
|
|
|
4976
4792
|
|
|
4977
4793
|
// src/commands/logs.ts
|
|
4978
4794
|
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
|
|
4979
|
-
import
|
|
4795
|
+
import path28 from "path";
|
|
4980
4796
|
function loadRecords(projectRoot) {
|
|
4981
|
-
const directory =
|
|
4797
|
+
const directory = path28.join(migrationDirectory(projectRoot), "logs", "daily");
|
|
4982
4798
|
if (!existsSync7(directory)) return [];
|
|
4983
4799
|
return readdirSync3(directory).filter((file) => file.endsWith(".jsonl")).sort().flatMap(
|
|
4984
|
-
(file) => readFileSync6(
|
|
4800
|
+
(file) => readFileSync6(path28.join(directory, file), "utf8").split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line))
|
|
4985
4801
|
);
|
|
4986
4802
|
}
|
|
4987
4803
|
function queryCommandLogs(project, action, value) {
|
|
4988
|
-
const projectRoot =
|
|
4804
|
+
const projectRoot = path28.resolve(project);
|
|
4989
4805
|
const records = loadRecords(projectRoot);
|
|
4990
4806
|
if (action === "list") {
|
|
4991
4807
|
const selected = value ? records.filter((record) => record.date === value) : records;
|
|
@@ -5016,7 +4832,7 @@ function queryCommandLogs(project, action, value) {
|
|
|
5016
4832
|
// src/logging/command-log.ts
|
|
5017
4833
|
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
5018
4834
|
import { randomUUID } from "crypto";
|
|
5019
|
-
import
|
|
4835
|
+
import path29 from "path";
|
|
5020
4836
|
var COMMAND_LOG_SCHEMA_VERSION = 1;
|
|
5021
4837
|
var SECRET_OPTIONS = /^(--?(?:password|pass|token|secret|api[-_]?key|access[-_]?key|signing[-_]?key))$/i;
|
|
5022
4838
|
function localDate(date) {
|
|
@@ -5056,10 +4872,10 @@ function discoverProjectRoot(argv, cwd = process.cwd()) {
|
|
|
5056
4872
|
continue;
|
|
5057
4873
|
}
|
|
5058
4874
|
if (argument.startsWith("-") || reserved.has(argument)) continue;
|
|
5059
|
-
const candidate =
|
|
4875
|
+
const candidate = path29.resolve(cwd, argument);
|
|
5060
4876
|
if (existsSync8(candidate)) return candidate;
|
|
5061
4877
|
}
|
|
5062
|
-
return
|
|
4878
|
+
return path29.resolve(cwd);
|
|
5063
4879
|
}
|
|
5064
4880
|
function knownArtifacts(projectRoot) {
|
|
5065
4881
|
const candidates = [
|
|
@@ -5072,12 +4888,12 @@ function knownArtifacts(projectRoot) {
|
|
|
5072
4888
|
["managed-evidence", ".migration/evidence/index.json"]
|
|
5073
4889
|
];
|
|
5074
4890
|
const workspaceRoot = migrationWorkspaceRoot(projectRoot);
|
|
5075
|
-
return candidates.filter(([, relative2]) => existsSync8(
|
|
4891
|
+
return candidates.filter(([, relative2]) => existsSync8(path29.join(workspaceRoot, relative2))).map(([kind, relative2]) => ({ kind, path: relative2.replaceAll("\\", "/") }));
|
|
5076
4892
|
}
|
|
5077
4893
|
function commandName(argv) {
|
|
5078
|
-
const
|
|
4894
|
+
const command2 = argv[0] ?? "unknown";
|
|
5079
4895
|
const grouped = /* @__PURE__ */ new Set(["analyze", "report", "build", "test", "tools", "config", "checkpoint", "device", "evidence", "mock", "service"]);
|
|
5080
|
-
return grouped.has(
|
|
4896
|
+
return grouped.has(command2) && argv[1] ? `${command2} ${argv[1]}` : command2;
|
|
5081
4897
|
}
|
|
5082
4898
|
function atomicJson4(file, value) {
|
|
5083
4899
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -5120,18 +4936,18 @@ async function withCommandLog(argv, version, execute) {
|
|
|
5120
4936
|
artifacts: knownArtifacts(projectRoot)
|
|
5121
4937
|
};
|
|
5122
4938
|
ensureMigrationWorkspace(projectRoot);
|
|
5123
|
-
const logsRoot =
|
|
5124
|
-
const runDirectory =
|
|
5125
|
-
const dailyDirectory =
|
|
4939
|
+
const logsRoot = path29.join(migrationWorkspaceRoot(projectRoot), ".migration", "logs");
|
|
4940
|
+
const runDirectory = path29.join(logsRoot, "runs", date);
|
|
4941
|
+
const dailyDirectory = path29.join(logsRoot, "daily");
|
|
5126
4942
|
mkdirSync4(runDirectory, { recursive: true });
|
|
5127
4943
|
mkdirSync4(dailyDirectory, { recursive: true });
|
|
5128
|
-
atomicJson4(
|
|
5129
|
-
appendFileSync(
|
|
4944
|
+
atomicJson4(path29.join(runDirectory, `${runId}.json`), record);
|
|
4945
|
+
appendFileSync(path29.join(dailyDirectory, `${date}.jsonl`), `${JSON.stringify(record)}
|
|
5130
4946
|
`, "utf8");
|
|
5131
|
-
atomicJson4(
|
|
4947
|
+
atomicJson4(path29.join(logsRoot, "index.json"), {
|
|
5132
4948
|
schemaVersion: COMMAND_LOG_SCHEMA_VERSION,
|
|
5133
4949
|
lastRunId: runId,
|
|
5134
|
-
lastRun:
|
|
4950
|
+
lastRun: path29.relative(path29.dirname(logsRoot), path29.join(runDirectory, `${runId}.json`)).replaceAll("\\", "/"),
|
|
5135
4951
|
updatedAt: finished.toISOString()
|
|
5136
4952
|
});
|
|
5137
4953
|
} catch (logError) {
|
|
@@ -5142,30 +4958,30 @@ async function withCommandLog(argv, version, execute) {
|
|
|
5142
4958
|
|
|
5143
4959
|
// src/runtime-paths.ts
|
|
5144
4960
|
import fs24 from "fs";
|
|
5145
|
-
import
|
|
4961
|
+
import path30 from "path";
|
|
5146
4962
|
import { fileURLToPath } from "url";
|
|
5147
4963
|
function resolveRuntimePaths(entryUrl) {
|
|
5148
4964
|
const entryFile = fileURLToPath(entryUrl);
|
|
5149
|
-
const directory =
|
|
5150
|
-
const roots = [
|
|
5151
|
-
const packageRoot = roots.find((candidate) => fs24.existsSync(
|
|
5152
|
-
const runtimeRoot = packageRoot ??
|
|
4965
|
+
const directory = path30.dirname(entryFile);
|
|
4966
|
+
const roots = [path30.resolve(directory, ".."), directory];
|
|
4967
|
+
const packageRoot = roots.find((candidate) => fs24.existsSync(path30.join(candidate, "package.json")));
|
|
4968
|
+
const runtimeRoot = packageRoot ?? path30.resolve(directory, "..");
|
|
5153
4969
|
const templatesRoot = [
|
|
5154
|
-
packageRoot &&
|
|
4970
|
+
packageRoot && path30.join(packageRoot, "templates")
|
|
5155
4971
|
].filter(Boolean).find((candidate) => fs24.existsSync(candidate));
|
|
5156
|
-
const preflightChecklistPath = packageRoot &&
|
|
4972
|
+
const preflightChecklistPath = packageRoot && path30.join(packageRoot, "config", "preflight-checklist.json");
|
|
5157
4973
|
return { entryFile, runtimeRoot, packageRoot, templatesRoot, preflightChecklistPath };
|
|
5158
4974
|
}
|
|
5159
4975
|
|
|
5160
4976
|
// src/kernel/migration-kernel.ts
|
|
5161
4977
|
import { createHash as createHash8, randomUUID as randomUUID2 } from "crypto";
|
|
5162
4978
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5163
|
-
import
|
|
4979
|
+
import fs29 from "fs";
|
|
5164
4980
|
import path35 from "path";
|
|
5165
4981
|
|
|
5166
4982
|
// src/planning/facts/reader.ts
|
|
5167
4983
|
import fs25 from "fs";
|
|
5168
|
-
import
|
|
4984
|
+
import path31 from "path";
|
|
5169
4985
|
|
|
5170
4986
|
// src/planning/util.ts
|
|
5171
4987
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -5210,17 +5026,17 @@ function readJson4(file) {
|
|
|
5210
5026
|
}
|
|
5211
5027
|
function inside3(root, relative2, label) {
|
|
5212
5028
|
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(`..${
|
|
5029
|
+
const resolvedRoot = path31.resolve(root);
|
|
5030
|
+
const absolute = path31.resolve(resolvedRoot, relative2);
|
|
5031
|
+
const rel = path31.relative(resolvedRoot, absolute);
|
|
5032
|
+
if (!rel || rel.startsWith(`..${path31.sep}`) || path31.isAbsolute(rel)) {
|
|
5217
5033
|
throw new PlanningFactsError("FACTS_PATH_OUTSIDE_ROOT", `${label} must stay inside ${resolvedRoot}: ${relative2}`);
|
|
5218
5034
|
}
|
|
5219
5035
|
if (!fs25.existsSync(absolute) || !fs25.statSync(absolute).isFile()) throw new PlanningFactsError("FACTS_SHARD_MISSING", `${label} not found: ${absolute}`);
|
|
5220
5036
|
const realRoot = fs25.realpathSync(resolvedRoot);
|
|
5221
5037
|
const realFile = fs25.realpathSync(absolute);
|
|
5222
|
-
const realRelative =
|
|
5223
|
-
if (!realRelative || realRelative.startsWith(`..${
|
|
5038
|
+
const realRelative = path31.relative(realRoot, realFile);
|
|
5039
|
+
if (!realRelative || realRelative.startsWith(`..${path31.sep}`) || path31.isAbsolute(realRelative)) {
|
|
5224
5040
|
throw new PlanningFactsError("FACTS_PATH_OUTSIDE_ROOT", `${label} resolves outside ${realRoot}: ${relative2}`);
|
|
5225
5041
|
}
|
|
5226
5042
|
return realFile;
|
|
@@ -5233,9 +5049,9 @@ function requireVersion(value, expected, label) {
|
|
|
5233
5049
|
if (value !== expected) throw new PlanningFactsError("FACTS_VERSION_UNSUPPORTED", `${label} must be ${expected}; found ${value ?? "<missing>"}`);
|
|
5234
5050
|
}
|
|
5235
5051
|
function readFactsBundleV2(project) {
|
|
5236
|
-
const projectRoot =
|
|
5237
|
-
const specRoot =
|
|
5238
|
-
const indexFile =
|
|
5052
|
+
const projectRoot = path31.resolve(project);
|
|
5053
|
+
const specRoot = path31.join(migrationDirectory(projectRoot), "spec");
|
|
5054
|
+
const indexFile = path31.join(specRoot, "index.json");
|
|
5239
5055
|
if (!fs25.existsSync(indexFile)) throw new PlanningFactsError("FACTS_SHARD_MISSING", `SPEC index not found: ${indexFile}`);
|
|
5240
5056
|
const index = readJson4(indexFile);
|
|
5241
5057
|
requireVersion(index.specVersion, "2.0.0", "SPEC specVersion");
|
|
@@ -5303,17 +5119,30 @@ function readFactsBundleV2(project) {
|
|
|
5303
5119
|
}
|
|
5304
5120
|
|
|
5305
5121
|
// src/planning/types.ts
|
|
5306
|
-
var TASK_PLAN_VERSION = "
|
|
5307
|
-
var TASK_ENVELOPE_VERSION = "
|
|
5308
|
-
var
|
|
5122
|
+
var TASK_PLAN_VERSION = "2.3.0";
|
|
5123
|
+
var TASK_ENVELOPE_VERSION = "3.0.0";
|
|
5124
|
+
var BEHAVIOR_PLAN_VERSION = "1.0.0";
|
|
5125
|
+
var EXECUTION_PLAN_VERSION = "1.3.0";
|
|
5126
|
+
var PLANNING_MODEL_VERSION = "2.0.0";
|
|
5309
5127
|
var FACTS_ADAPTER_VERSION = "1.0.0";
|
|
5310
|
-
var PLANNER_POLICY_VERSION = "
|
|
5128
|
+
var PLANNER_POLICY_VERSION = "2.3.0";
|
|
5311
5129
|
var DEFAULT_CLOSURE_BUDGET = {
|
|
5312
5130
|
maxFactCount: 30,
|
|
5313
5131
|
maxSourceFileCount: 15,
|
|
5314
5132
|
maxTraceNodeCount: 50,
|
|
5315
5133
|
maxEstimatedFactTokens: 12e3
|
|
5316
5134
|
};
|
|
5135
|
+
var DEFAULT_EXECUTION_BUDGET = {
|
|
5136
|
+
maxBehaviorCount: 5,
|
|
5137
|
+
maxObservableOutcomeCount: 3,
|
|
5138
|
+
maxProductionFileCount: 4,
|
|
5139
|
+
maxTestFileCount: 2,
|
|
5140
|
+
maxCapabilityCount: 2,
|
|
5141
|
+
maxFactCount: 45,
|
|
5142
|
+
maxSourceFileCount: 8,
|
|
5143
|
+
maxTraceNodeCount: 60,
|
|
5144
|
+
maxEstimatedFactTokens: 14e3
|
|
5145
|
+
};
|
|
5317
5146
|
|
|
5318
5147
|
// src/planning/facts/v2-adapter.ts
|
|
5319
5148
|
function evidence(values) {
|
|
@@ -5617,7 +5446,8 @@ function candidateForEntry(entryId, facts) {
|
|
|
5617
5446
|
...effects.flatMap((effect) => [effect.id, effect.targetId])
|
|
5618
5447
|
]));
|
|
5619
5448
|
const issues = facts.issues.filter((issue) => issue.relatedRefs.some((ref) => related.has(ref)));
|
|
5620
|
-
const
|
|
5449
|
+
const unresolvedIssues = issues.filter((issue) => issue.blocking && !issue.resolved);
|
|
5450
|
+
const blocking = unresolvedIssues.length > 0;
|
|
5621
5451
|
const isLaunch = facts.app.launcherEntryIds.includes(entry.id) || entry.triggerCategory === "application" && entry.event === "launch";
|
|
5622
5452
|
const type = blocking ? "resolution" : (() => {
|
|
5623
5453
|
if (isLaunch) return "bootstrap";
|
|
@@ -5650,7 +5480,7 @@ function candidateForEntry(entryId, facts) {
|
|
|
5650
5480
|
surfaceRefs: new Set(surfaces),
|
|
5651
5481
|
controlRefs: new Set(controls.map((control) => control.id)),
|
|
5652
5482
|
interactionRefs: new Set(effects.map((effect) => effect.id)),
|
|
5653
|
-
issueRefs: new Set(
|
|
5483
|
+
issueRefs: new Set(unresolvedIssues.map((issue) => issue.id)),
|
|
5654
5484
|
warningRefs: new Set(warningRefs),
|
|
5655
5485
|
sourceSurfaceRefs: new Set(entry.ownerSurfaceId ? [entry.ownerSurfaceId] : []),
|
|
5656
5486
|
targetEdges: effects.flatMap((effect) => targetSurfaceIds(effect, facts).map((targetSurfaceRef) => ({ sourceSurfaceRef: entry.ownerSurfaceId, targetSurfaceRef, effectRef: effect.id }))),
|
|
@@ -5658,7 +5488,7 @@ function candidateForEntry(entryId, facts) {
|
|
|
5658
5488
|
...entry.evidence,
|
|
5659
5489
|
...controls.flatMap((control) => control.evidence),
|
|
5660
5490
|
...effects.flatMap((effect) => effect.evidence),
|
|
5661
|
-
...
|
|
5491
|
+
...unresolvedIssues.flatMap((issue) => issue.evidence)
|
|
5662
5492
|
], evidenceKey),
|
|
5663
5493
|
codeRefs: entry.trace.codeRefs,
|
|
5664
5494
|
requiresSourceReview: effects.length === 0 || blocking || entry.trace.warnings.length > 0,
|
|
@@ -5954,8 +5784,8 @@ function chooseSurfaceOwners(candidates, facts) {
|
|
|
5954
5784
|
function reportableRefs(facts) {
|
|
5955
5785
|
return new Set(facts.requiredFactRefs);
|
|
5956
5786
|
}
|
|
5957
|
-
function
|
|
5958
|
-
return `
|
|
5787
|
+
function behaviorIdFor(candidate) {
|
|
5788
|
+
return `behavior:${sha2562({
|
|
5959
5789
|
type: candidate.type,
|
|
5960
5790
|
primaryFactRefs: uniqueSorted(candidate.primary),
|
|
5961
5791
|
path: {
|
|
@@ -5967,7 +5797,7 @@ function taskIdFor(candidate) {
|
|
|
5967
5797
|
}
|
|
5968
5798
|
function downstreamCounts(tasks) {
|
|
5969
5799
|
const children = /* @__PURE__ */ new Map();
|
|
5970
|
-
for (const task of tasks) for (const dependency of task.
|
|
5800
|
+
for (const task of tasks) for (const dependency of task.dependsOnBehaviorIds) children.set(dependency, [...children.get(dependency) ?? [], task.id]);
|
|
5971
5801
|
const out = /* @__PURE__ */ new Map();
|
|
5972
5802
|
for (const task of tasks) {
|
|
5973
5803
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5982,7 +5812,7 @@ function downstreamCounts(tasks) {
|
|
|
5982
5812
|
}
|
|
5983
5813
|
return out;
|
|
5984
5814
|
}
|
|
5985
|
-
function
|
|
5815
|
+
function compileBehaviorPlanFromPlanningFacts(facts, androidFingerprint) {
|
|
5986
5816
|
let candidates = mergeCompatibleEntryCandidates(facts.entries.map((entry) => candidateForEntry(entry.id, facts)), facts);
|
|
5987
5817
|
for (const surfaceRef of facts.app.launcherSurfaceIds) {
|
|
5988
5818
|
if (!candidates.some((candidate) => candidate.type === "bootstrap" && candidate.surfaceRefs.has(surfaceRef))) {
|
|
@@ -6036,8 +5866,8 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6036
5866
|
const closureRefs = /* @__PURE__ */ new Set([...candidate.entryRefs, ...candidate.surfaceRefs, ...candidate.controlRefs, ...candidate.interactionRefs, ...candidate.issueRefs]);
|
|
6037
5867
|
candidate.supporting = new Set([...closureRefs].filter((ref) => !candidate.primary.has(ref)));
|
|
6038
5868
|
}
|
|
6039
|
-
const idByKey = new Map(candidates.map((candidate) => [candidate.key,
|
|
6040
|
-
if (new Set(idByKey.values()).size !== candidates.length) throw new Error("
|
|
5869
|
+
const idByKey = new Map(candidates.map((candidate) => [candidate.key, behaviorIdFor(candidate)]));
|
|
5870
|
+
if (new Set(idByKey.values()).size !== candidates.length) throw new Error("Behavior ID collision while compiling plan");
|
|
6041
5871
|
const launcherResolvers = candidates.filter((candidate) => candidate.type === "resolution" && candidate.issueRefs.has("issue:launcher-unresolved"));
|
|
6042
5872
|
const taskDrafts = candidates.map((candidate) => {
|
|
6043
5873
|
const dependencies = /* @__PURE__ */ new Set();
|
|
@@ -6049,11 +5879,11 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6049
5879
|
}
|
|
6050
5880
|
if (facts.app.launcherOrigin === "unresolved") for (const resolver of launcherResolvers) dependencies.add(idByKey.get(resolver.key));
|
|
6051
5881
|
}
|
|
6052
|
-
return { candidate, id: idByKey.get(candidate.key),
|
|
5882
|
+
return { candidate, id: idByKey.get(candidate.key), dependsOnBehaviorIds: uniqueSorted(dependencies) };
|
|
6053
5883
|
});
|
|
6054
5884
|
const downstream = downstreamCounts(taskDrafts);
|
|
6055
5885
|
const reportable = reportableRefs(facts);
|
|
6056
|
-
const
|
|
5886
|
+
const responsibilities = taskDrafts.map(({ candidate, id, dependsOnBehaviorIds }) => {
|
|
6057
5887
|
const primaryFactRefs = uniqueSorted(candidate.primary);
|
|
6058
5888
|
const supportingFactRefs = uniqueSorted(candidate.supporting);
|
|
6059
5889
|
const core = {
|
|
@@ -6070,7 +5900,7 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6070
5900
|
title: candidate.title,
|
|
6071
5901
|
primaryFactRefs,
|
|
6072
5902
|
supportingFactRefs,
|
|
6073
|
-
|
|
5903
|
+
dependsOnBehaviorIds,
|
|
6074
5904
|
closure: {
|
|
6075
5905
|
entryRefs: core.entries,
|
|
6076
5906
|
surfaceRefs: core.surfaces,
|
|
@@ -6098,12 +5928,12 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6098
5928
|
};
|
|
6099
5929
|
}).sort((a, b) => typeOrder[a.type] - typeOrder[b.type] || a.id.localeCompare(b.id));
|
|
6100
5930
|
const primaryOwnerByFactRef = Object.fromEntries([...ownerKeyByFactRef.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ref, key]) => [ref, idByKey.get(key)]));
|
|
6101
|
-
const
|
|
6102
|
-
for (const
|
|
5931
|
+
const supportingBehaviorIdsByFactRef = {};
|
|
5932
|
+
for (const responsibility of responsibilities) for (const ref of responsibility.supportingFactRefs) supportingBehaviorIdsByFactRef[ref] = uniqueSorted([...supportingBehaviorIdsByFactRef[ref] ?? [], responsibility.id]);
|
|
6103
5933
|
const planningWarnings = [...facts.diagnostics];
|
|
6104
|
-
for (const
|
|
5934
|
+
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
5935
|
return {
|
|
6106
|
-
|
|
5936
|
+
behaviorPlanVersion: BEHAVIOR_PLAN_VERSION,
|
|
6107
5937
|
planningModelVersion: PLANNING_MODEL_VERSION,
|
|
6108
5938
|
factsAdapterVersion: FACTS_ADAPTER_VERSION,
|
|
6109
5939
|
plannerPolicyVersion: PLANNER_POLICY_VERSION,
|
|
@@ -6119,22 +5949,716 @@ function compileFromPlanningFacts(facts, androidFingerprint) {
|
|
|
6119
5949
|
launcherEntryRefs: facts.app.launcherEntryIds,
|
|
6120
5950
|
origin: facts.app.launcherOrigin
|
|
6121
5951
|
},
|
|
6122
|
-
|
|
6123
|
-
ownership: { primaryOwnerByFactRef,
|
|
5952
|
+
responsibilities,
|
|
5953
|
+
ownership: { primaryOwnerByFactRef, supportingBehaviorIdsByFactRef: Object.fromEntries(Object.entries(supportingBehaviorIdsByFactRef).sort(([a], [b]) => a.localeCompare(b))) },
|
|
6124
5954
|
planningWarnings: uniqueBy(planningWarnings, (item) => `${item.code}:${item.message}:${item.relatedRefs.join(",")}`)
|
|
6125
5955
|
};
|
|
6126
5956
|
}
|
|
6127
5957
|
|
|
6128
|
-
// src/planning/
|
|
5958
|
+
// src/planning/execution-context.ts
|
|
6129
5959
|
import fs26 from "fs";
|
|
5960
|
+
import path32 from "path";
|
|
5961
|
+
function readJson6(file) {
|
|
5962
|
+
try {
|
|
5963
|
+
return JSON.parse(fs26.readFileSync(file, "utf8"));
|
|
5964
|
+
} catch {
|
|
5965
|
+
return null;
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
function inside4(root, target) {
|
|
5969
|
+
const relative2 = path32.relative(root, target);
|
|
5970
|
+
return relative2 === "" || relative2 !== ".." && !relative2.startsWith(`..${path32.sep}`) && !path32.isAbsolute(relative2);
|
|
5971
|
+
}
|
|
5972
|
+
function validatedHarmonyFile(projectRoot, root, value) {
|
|
5973
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("Report HarmonyOS mapping must be a non-empty string");
|
|
5974
|
+
const absolute = path32.resolve(projectRoot, value);
|
|
5975
|
+
if (!inside4(root, absolute)) throw new Error(`Report HarmonyOS mapping escapes the configured project: ${value}`);
|
|
5976
|
+
if (!fs26.existsSync(absolute) || !fs26.statSync(absolute).isFile()) throw new Error(`Report HarmonyOS mapping does not exist: ${value}`);
|
|
5977
|
+
const realRoot = fs26.realpathSync(root);
|
|
5978
|
+
const realFile = fs26.realpathSync(absolute);
|
|
5979
|
+
if (!inside4(realRoot, realFile)) throw new Error(`Report HarmonyOS mapping resolves outside the configured project: ${value}`);
|
|
5980
|
+
return path32.relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
5981
|
+
}
|
|
5982
|
+
function confinedReportFile(reportRoot, relative2) {
|
|
5983
|
+
if (!relative2.trim() || path32.isAbsolute(relative2)) throw new Error(`Report shard path must be relative: ${relative2}`);
|
|
5984
|
+
const absolute = path32.resolve(reportRoot, relative2);
|
|
5985
|
+
if (!inside4(reportRoot, absolute) || absolute === reportRoot) throw new Error(`Report shard escapes report root: ${relative2}`);
|
|
5986
|
+
if (!fs26.existsSync(absolute) || !fs26.statSync(absolute).isFile()) throw new Error(`Report shard does not exist: ${relative2}`);
|
|
5987
|
+
const realRoot = fs26.realpathSync(reportRoot);
|
|
5988
|
+
const realFile = fs26.realpathSync(absolute);
|
|
5989
|
+
if (!inside4(realRoot, realFile)) throw new Error(`Report shard resolves outside report root: ${relative2}`);
|
|
5990
|
+
return realFile;
|
|
5991
|
+
}
|
|
5992
|
+
function reportHarmonyFiles(projectRoot, root) {
|
|
5993
|
+
const reportRoot = path32.join(migrationDirectory(projectRoot), "report");
|
|
5994
|
+
const index = readJson6(path32.join(reportRoot, "index.json"));
|
|
5995
|
+
if (!index) return {};
|
|
5996
|
+
const shardFiles = [
|
|
5997
|
+
...(index.pages ?? []).map((item) => item.file),
|
|
5998
|
+
index.unassignedEntriesFile,
|
|
5999
|
+
index.interactionsFile,
|
|
6000
|
+
index.discoveriesFile
|
|
6001
|
+
].filter((value) => typeof value === "string");
|
|
6002
|
+
const result = {};
|
|
6003
|
+
for (const relative2 of shardFiles) {
|
|
6004
|
+
const document = readJson6(confinedReportFile(reportRoot, relative2));
|
|
6005
|
+
if (!document) continue;
|
|
6006
|
+
const items = [
|
|
6007
|
+
document.page,
|
|
6008
|
+
...document.controls ?? [],
|
|
6009
|
+
...document.entries ?? [],
|
|
6010
|
+
...document.interactions ?? [],
|
|
6011
|
+
...document.items ?? []
|
|
6012
|
+
].filter(Boolean);
|
|
6013
|
+
for (const item of items) {
|
|
6014
|
+
if (typeof item.ref !== "string" || !Array.isArray(item.harmonyosFiles)) continue;
|
|
6015
|
+
result[item.ref] = uniqueSorted(item.harmonyosFiles.map((value) => validatedHarmonyFile(projectRoot, root, value)));
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
return Object.fromEntries(Object.entries(result).sort(([left], [right]) => left.localeCompare(right)));
|
|
6019
|
+
}
|
|
6020
|
+
function harmonyRoot(projectRoot) {
|
|
6021
|
+
const config = readJson6(path32.join(migrationDirectory(projectRoot), "config.json"));
|
|
6022
|
+
const configured = typeof config?.harmonyProject === "string" ? config.harmonyProject : "ohos";
|
|
6023
|
+
return path32.isAbsolute(configured) ? configured : path32.resolve(projectRoot, configured);
|
|
6024
|
+
}
|
|
6025
|
+
function existingGlobalHotspots(projectRoot) {
|
|
6026
|
+
const root = harmonyRoot(projectRoot);
|
|
6027
|
+
if (!fs26.existsSync(root) || !fs26.statSync(root).isDirectory()) return [];
|
|
6028
|
+
const relativeRoot = path32.relative(projectRoot, root).replaceAll("\\", "/");
|
|
6029
|
+
const candidates = [
|
|
6030
|
+
"build-profile.json5",
|
|
6031
|
+
"AppScope/app.json5",
|
|
6032
|
+
"oh-package.json5",
|
|
6033
|
+
"entry/oh-package.json5",
|
|
6034
|
+
"entry/src/main/module.json5",
|
|
6035
|
+
"entry/src/main/resources/base/profile/main_pages.json",
|
|
6036
|
+
"entry/src/main/ets/entryability/EntryAbility.ets",
|
|
6037
|
+
"entry/src/main/ets/pages/Index.ets",
|
|
6038
|
+
"entry/src/main/ets/common/RouteConstants.ets"
|
|
6039
|
+
];
|
|
6040
|
+
return candidates.filter((relative2) => fs26.existsSync(path32.join(root, relative2))).map((relative2) => `${relativeRoot}/${relative2}`.replace(/^\.\//, "")).sort();
|
|
6041
|
+
}
|
|
6042
|
+
function loadExecutionPlanningContext(projectRootArgument) {
|
|
6043
|
+
const projectRoot = path32.resolve(projectRootArgument);
|
|
6044
|
+
const root = harmonyRoot(projectRoot);
|
|
6045
|
+
const rootAvailable = fs26.existsSync(root) && fs26.statSync(root).isDirectory();
|
|
6046
|
+
return {
|
|
6047
|
+
harmonyFilesByFactRef: rootAvailable ? reportHarmonyFiles(projectRoot, root) : {},
|
|
6048
|
+
globalHotspotFiles: existingGlobalHotspots(projectRoot)
|
|
6049
|
+
};
|
|
6050
|
+
}
|
|
6051
|
+
var emptyExecutionPlanningContext = {
|
|
6052
|
+
harmonyFilesByFactRef: {},
|
|
6053
|
+
globalHotspotFiles: []
|
|
6054
|
+
};
|
|
6055
|
+
|
|
6056
|
+
// src/planning/execution-planner.ts
|
|
6057
|
+
var typeOrder2 = {
|
|
6058
|
+
resolution: 0,
|
|
6059
|
+
bootstrap: 1,
|
|
6060
|
+
capability: 2,
|
|
6061
|
+
journey: 3,
|
|
6062
|
+
assembly: 4,
|
|
6063
|
+
regression: 5
|
|
6064
|
+
};
|
|
6065
|
+
function isReusableSurface(kind) {
|
|
6066
|
+
return /(?:list[-_ ]?item|item|reusable|layout|custom[-_ ]?view|cell|row)/i.test(kind);
|
|
6067
|
+
}
|
|
6068
|
+
function isHostedOverlay(kind) {
|
|
6069
|
+
return /(?:dialog|bottom[-_ ]?sheet|sheet|popup|overlay)/i.test(kind);
|
|
6070
|
+
}
|
|
6071
|
+
function canonicalPageRefs(facts) {
|
|
6072
|
+
const byId = new Map(facts.surfaces.map((surface) => [surface.id, surface]));
|
|
6073
|
+
const memo = /* @__PURE__ */ new Map();
|
|
6074
|
+
const resolve4 = (surfaceRef, visiting = /* @__PURE__ */ new Set()) => {
|
|
6075
|
+
if (memo.has(surfaceRef)) return memo.get(surfaceRef);
|
|
6076
|
+
const surface = byId.get(surfaceRef);
|
|
6077
|
+
if (!surface || visiting.has(surfaceRef)) return surfaceRef;
|
|
6078
|
+
visiting.add(surfaceRef);
|
|
6079
|
+
const parentPages = uniqueSorted(surface.parentSurfaceIds.filter((ref) => byId.has(ref)).map((ref) => resolve4(ref, visiting)));
|
|
6080
|
+
const canonical2 = parentPages.length === 1 && (isReusableSurface(surface.kind) || isHostedOverlay(surface.kind) || !surface.topLevel) ? parentPages[0] : surfaceRef;
|
|
6081
|
+
visiting.delete(surfaceRef);
|
|
6082
|
+
memo.set(surfaceRef, canonical2);
|
|
6083
|
+
return canonical2;
|
|
6084
|
+
};
|
|
6085
|
+
for (const surface of facts.surfaces) resolve4(surface.id);
|
|
6086
|
+
return Object.fromEntries([...memo.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
6087
|
+
}
|
|
6088
|
+
function behaviorSourceSurfaceRefs(behavior, facts) {
|
|
6089
|
+
const entryRefs = new Set(behavior.closure.entryRefs);
|
|
6090
|
+
const fromEntries = facts.entries.filter((entry) => entryRefs.has(entry.id) && entry.ownerSurfaceId).map((entry) => entry.ownerSurfaceId);
|
|
6091
|
+
if (fromEntries.length) return uniqueSorted(fromEntries);
|
|
6092
|
+
const primarySurfaces = behavior.primaryFactRefs.filter((ref) => facts.surfaces.some((surface) => surface.id === ref));
|
|
6093
|
+
return uniqueSorted(primarySurfaces.length ? primarySurfaces : behavior.closure.surfaceRefs);
|
|
6094
|
+
}
|
|
6095
|
+
function behaviorCanonicalPage(behavior, facts, canonicalBySurface) {
|
|
6096
|
+
const pages = uniqueSorted(behaviorSourceSurfaceRefs(behavior, facts).map((ref) => canonicalBySurface[ref] ?? ref));
|
|
6097
|
+
return pages[0] ?? null;
|
|
6098
|
+
}
|
|
6099
|
+
function codeRefKey(value) {
|
|
6100
|
+
if (typeof value.id === "string") return value.id;
|
|
6101
|
+
return stableJson(Object.fromEntries(["file", "name", "symbol", "kind", "type", "capabilityRef"].filter((key) => value[key] !== void 0).map((key) => [key, value[key]])));
|
|
6102
|
+
}
|
|
6103
|
+
function behaviorCapabilityRefs(behavior) {
|
|
6104
|
+
return uniqueSorted(behavior.closure.codeRefs.flatMap((item) => {
|
|
6105
|
+
if (typeof item.capabilityRef === "string" && item.capabilityRef.length > 0) return [item.capabilityRef];
|
|
6106
|
+
if (item.kind === "capability" && typeof item.id === "string" && item.id.length > 0) return [item.id];
|
|
6107
|
+
return [];
|
|
6108
|
+
}));
|
|
6109
|
+
}
|
|
6110
|
+
function candidateForBehavior(behavior, facts, canonicalBySurface) {
|
|
6111
|
+
const effectRefs = new Set(behavior.closure.interactionRefs);
|
|
6112
|
+
const targets = facts.effects.filter((effect) => effectRefs.has(effect.id) && effect.targetKind === "surface").map((effect) => canonicalBySurface[effect.targetId] ?? effect.targetId);
|
|
6113
|
+
return {
|
|
6114
|
+
behaviorIds: [behavior.id],
|
|
6115
|
+
canonicalPageRef: behaviorCanonicalPage(behavior, facts, canonicalBySurface),
|
|
6116
|
+
type: behavior.type,
|
|
6117
|
+
sourceFiles: uniqueSorted([
|
|
6118
|
+
...behavior.closure.sourceEvidence.map((item) => item.file),
|
|
6119
|
+
...behavior.closure.codeRefs.flatMap((item) => typeof item.file === "string" ? [item.file] : [])
|
|
6120
|
+
]),
|
|
6121
|
+
targetPageRefs: uniqueSorted(targets),
|
|
6122
|
+
codeRefKeys: uniqueSorted(behavior.closure.codeRefs.map(codeRefKey)),
|
|
6123
|
+
capabilityRefs: behaviorCapabilityRefs(behavior)
|
|
6124
|
+
};
|
|
6125
|
+
}
|
|
6126
|
+
function combineCandidates2(left, right) {
|
|
6127
|
+
return {
|
|
6128
|
+
behaviorIds: uniqueSorted([...left.behaviorIds, ...right.behaviorIds]),
|
|
6129
|
+
canonicalPageRef: left.canonicalPageRef,
|
|
6130
|
+
type: left.type,
|
|
6131
|
+
sourceFiles: uniqueSorted([...left.sourceFiles, ...right.sourceFiles]),
|
|
6132
|
+
targetPageRefs: uniqueSorted([...left.targetPageRefs, ...right.targetPageRefs]),
|
|
6133
|
+
codeRefKeys: uniqueSorted([...left.codeRefKeys, ...right.codeRefKeys]),
|
|
6134
|
+
capabilityRefs: uniqueSorted([...left.capabilityRefs, ...right.capabilityRefs])
|
|
6135
|
+
};
|
|
6136
|
+
}
|
|
6137
|
+
function intersects(left, right) {
|
|
6138
|
+
const values = new Set(left);
|
|
6139
|
+
return right.some((value) => values.has(value));
|
|
6140
|
+
}
|
|
6141
|
+
function behaviorRefs(candidate, behaviors) {
|
|
6142
|
+
return candidate.behaviorIds.map((id) => behaviors.get(id)).filter(Boolean);
|
|
6143
|
+
}
|
|
6144
|
+
function isRoutingAssembly(candidate) {
|
|
6145
|
+
return candidate.type === "journey" && candidate.targetPageRefs.some((target) => target !== candidate.canonicalPageRef);
|
|
6146
|
+
}
|
|
6147
|
+
function routingHotspotFiles(context) {
|
|
6148
|
+
return context.globalHotspotFiles.filter((file) => /(?:main_pages\.json|RouteConstants\.ets|\/pages\/Index\.ets)$/i.test(file));
|
|
6149
|
+
}
|
|
6150
|
+
function mappedTargetFiles(candidate, primaryRefs, context) {
|
|
6151
|
+
const mapped = uniqueSorted(primaryRefs.flatMap((ref) => context.harmonyFilesByFactRef[ref] ?? []));
|
|
6152
|
+
if (candidate.type === "bootstrap") return uniqueSorted([...mapped, ...context.globalHotspotFiles]);
|
|
6153
|
+
if (isRoutingAssembly(candidate)) return uniqueSorted([...mapped, ...routingHotspotFiles(context)]);
|
|
6154
|
+
return mapped;
|
|
6155
|
+
}
|
|
6156
|
+
function executionCost(candidate, behaviors, context) {
|
|
6157
|
+
const members = behaviorRefs(candidate, behaviors);
|
|
6158
|
+
const factRefs = uniqueSorted(members.flatMap((item) => [...item.primaryFactRefs, ...item.supportingFactRefs]));
|
|
6159
|
+
const mappedFiles = mappedTargetFiles(candidate, factRefs, context);
|
|
6160
|
+
const productionFiles = mappedFiles.filter((file) => !/(?:^|\/)(?:ohosTest|test|tests)(?:\/|$)|(?:Test|\.test)\.(?:ets|ts)$/i.test(file));
|
|
6161
|
+
const testFiles = mappedFiles.filter((file) => !productionFiles.includes(file));
|
|
6162
|
+
return {
|
|
6163
|
+
behaviorCount: members.length,
|
|
6164
|
+
observableOutcomeCount: uniqueSorted(members.map((item) => item.acceptance.observableOutcome)).length,
|
|
6165
|
+
productionFileCount: productionFiles.length,
|
|
6166
|
+
testFileCount: testFiles.length,
|
|
6167
|
+
capabilityCount: candidate.capabilityRefs.length,
|
|
6168
|
+
factCount: new Set(members.flatMap((item) => [...item.primaryFactRefs, ...item.supportingFactRefs])).size,
|
|
6169
|
+
sourceFileCount: uniqueSorted(members.flatMap((item) => [
|
|
6170
|
+
...item.closure.sourceEvidence.map((evidence2) => evidence2.file),
|
|
6171
|
+
...item.closure.codeRefs.flatMap((codeRef) => typeof codeRef.file === "string" ? [codeRef.file] : [])
|
|
6172
|
+
])).length,
|
|
6173
|
+
traceNodeCount: uniqueBy(members.flatMap((item) => item.closure.codeRefs), codeRefKey).length,
|
|
6174
|
+
estimatedFactTokens: members.reduce((total, item) => total + item.estimate.estimatedFactTokens, 0)
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
6177
|
+
function withinExecutionBudget(candidate, behaviors, context) {
|
|
6178
|
+
const cost = executionCost(candidate, behaviors, context);
|
|
6179
|
+
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;
|
|
6180
|
+
}
|
|
6181
|
+
function shouldMerge(left, right, behaviors, facts, context) {
|
|
6182
|
+
if (left.type !== "journey" || right.type !== "journey" || left.canonicalPageRef !== right.canonicalPageRef) return false;
|
|
6183
|
+
const members = [...behaviorRefs(left, behaviors), ...behaviorRefs(right, behaviors)];
|
|
6184
|
+
if (members.some((item) => item.closure.requiresSourceReview || item.closure.unresolvedRefs.length > 0)) return false;
|
|
6185
|
+
const combined = combineCandidates2(left, right);
|
|
6186
|
+
if (!withinExecutionBudget(combined, behaviors, context)) return false;
|
|
6187
|
+
const behaviorEntries = (candidate) => uniqueSorted(behaviorRefs(candidate, behaviors).flatMap((item) => item.closure.entryRefs));
|
|
6188
|
+
const targetedEntries = (candidate) => {
|
|
6189
|
+
const effects = new Set(behaviorRefs(candidate, behaviors).flatMap((item) => item.closure.interactionRefs));
|
|
6190
|
+
return uniqueSorted([...effects].flatMap((effectRef) => {
|
|
6191
|
+
const effect = facts.effects.find((item) => item.id === effectRef);
|
|
6192
|
+
return effect?.targetKind === "entry" ? [effect.targetId] : [];
|
|
6193
|
+
}));
|
|
6194
|
+
};
|
|
6195
|
+
const callbackLinked = intersects(targetedEntries(left), behaviorEntries(right)) || intersects(targetedEntries(right), behaviorEntries(left));
|
|
6196
|
+
if (callbackLinked || intersects(left.codeRefKeys, right.codeRefKeys)) return true;
|
|
6197
|
+
const sharedSource = intersects(left.sourceFiles, right.sourceFiles);
|
|
6198
|
+
const sharedTarget = left.targetPageRefs.length > 0 && right.targetPageRefs.length > 0 && intersects(left.targetPageRefs, right.targetPageRefs);
|
|
6199
|
+
const sharedCapability = intersects(left.capabilityRefs, right.capabilityRefs);
|
|
6200
|
+
return sharedSource && (sharedTarget || sharedCapability);
|
|
6201
|
+
}
|
|
6202
|
+
function clusterBehaviors(plan, facts, context) {
|
|
6203
|
+
const behaviors = new Map(plan.responsibilities.map((item) => [item.id, item]));
|
|
6204
|
+
const canonicalBySurface = canonicalPageRefs(facts);
|
|
6205
|
+
const clusters = [];
|
|
6206
|
+
for (const next of plan.responsibilities.map((item) => candidateForBehavior(item, facts, canonicalBySurface))) {
|
|
6207
|
+
const compatible = clusters.findIndex((current) => shouldMerge(current, next, behaviors, facts, context));
|
|
6208
|
+
if (compatible < 0) clusters.push(next);
|
|
6209
|
+
else clusters[compatible] = combineCandidates2(clusters[compatible], next);
|
|
6210
|
+
}
|
|
6211
|
+
return clusters;
|
|
6212
|
+
}
|
|
6213
|
+
function workPackageId(candidate) {
|
|
6214
|
+
if (candidate.type === "resolution") return null;
|
|
6215
|
+
return `package:${sha2562({ kind: "page", page: candidate.canonicalPageRef ?? "unassigned" }).slice(0, 16)}`;
|
|
6216
|
+
}
|
|
6217
|
+
function taskId(candidate) {
|
|
6218
|
+
return `task:${sha2562({ page: candidate.canonicalPageRef, type: candidate.type, behaviorIds: candidate.behaviorIds }).slice(0, 16)}`;
|
|
6219
|
+
}
|
|
6220
|
+
function taskTitle(candidate, members, facts) {
|
|
6221
|
+
if (members.length === 1) return members[0].title;
|
|
6222
|
+
const pageName = facts.surfaces.find((surface) => surface.id === candidate.canonicalPageRef)?.name ?? candidate.canonicalPageRef ?? "\u672A\u5F52\u5C5E\u6A21\u5757";
|
|
6223
|
+
return `${pageName}\uFF1A\u5B8C\u6210 ${members.length} \u4E2A\u76F8\u5173\u884C\u4E3A\u95ED\u73AF`;
|
|
6224
|
+
}
|
|
6225
|
+
function aggregateClosure(members) {
|
|
6226
|
+
return {
|
|
6227
|
+
entryRefs: uniqueSorted(members.flatMap((item) => item.closure.entryRefs)),
|
|
6228
|
+
surfaceRefs: uniqueSorted(members.flatMap((item) => item.closure.surfaceRefs)),
|
|
6229
|
+
controlRefs: uniqueSorted(members.flatMap((item) => item.closure.controlRefs)),
|
|
6230
|
+
interactionRefs: uniqueSorted(members.flatMap((item) => item.closure.interactionRefs)),
|
|
6231
|
+
sourceEvidence: uniqueBy(members.flatMap((item) => item.closure.sourceEvidence), (item) => `${item.file}:${item.symbol ?? ""}:${item.line ?? ""}:${item.endLine ?? ""}`),
|
|
6232
|
+
codeRefs: uniqueBy(members.flatMap((item) => item.closure.codeRefs), codeRefKey),
|
|
6233
|
+
unresolvedRefs: uniqueSorted(members.flatMap((item) => item.closure.unresolvedRefs)),
|
|
6234
|
+
warningRefs: uniqueSorted(members.flatMap((item) => item.closure.warningRefs)),
|
|
6235
|
+
requiresSourceReview: members.some((item) => item.closure.requiresSourceReview)
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
function primaryModule(candidate) {
|
|
6239
|
+
if (candidate.type === "bootstrap") return "foundation:application-shell";
|
|
6240
|
+
if (candidate.type === "resolution") return `resolution:${candidate.behaviorIds[0]}`;
|
|
6241
|
+
if (isRoutingAssembly(candidate)) return "assembly:routing";
|
|
6242
|
+
return candidate.canonicalPageRef ? `page:${candidate.canonicalPageRef}` : "page:unassigned";
|
|
6243
|
+
}
|
|
6244
|
+
function initialTasks(clusters, behaviorPlan, facts, canonicalBySurface, context) {
|
|
6245
|
+
const behaviors = new Map(behaviorPlan.responsibilities.map((item) => [item.id, item]));
|
|
6246
|
+
return clusters.map((candidate) => {
|
|
6247
|
+
const members = behaviorRefs(candidate, behaviors);
|
|
6248
|
+
const primaryFactRefs = uniqueSorted(members.flatMap((item2) => item2.primaryFactRefs));
|
|
6249
|
+
const supportingFactRefs = uniqueSorted(members.flatMap((item2) => item2.supportingFactRefs).filter((ref) => !primaryFactRefs.includes(ref)));
|
|
6250
|
+
const mappedFiles = mappedTargetFiles(candidate, [...primaryFactRefs, ...supportingFactRefs], context);
|
|
6251
|
+
let ownedFiles;
|
|
6252
|
+
if (candidate.type === "resolution") ownedFiles = [];
|
|
6253
|
+
else if (candidate.type === "bootstrap") ownedFiles = mappedFiles;
|
|
6254
|
+
else ownedFiles = mappedFiles.filter((file) => isRoutingAssembly(candidate) || !context.globalHotspotFiles.includes(file));
|
|
6255
|
+
const cost = executionCost(candidate, behaviors, context);
|
|
6256
|
+
const outcomes = uniqueSorted(members.map((item2) => item2.acceptance.observableOutcome));
|
|
6257
|
+
const closure = aggregateClosure(members);
|
|
6258
|
+
const pageScopes = uniqueSorted(closure.surfaceRefs.map((surfaceRef) => `page:${canonicalBySurface[surfaceRef] ?? surfaceRef}`));
|
|
6259
|
+
const exclusiveScopes = candidate.type === "resolution" ? [`resolution:${candidate.behaviorIds[0]}`] : uniqueSorted([
|
|
6260
|
+
...candidate.type === "bootstrap" ? ["foundation:application-shell"] : [],
|
|
6261
|
+
...isRoutingAssembly(candidate) ? ["assembly:routing"] : [],
|
|
6262
|
+
...pageScopes,
|
|
6263
|
+
...pageScopes.length === 0 ? [candidate.canonicalPageRef ? `page:${candidate.canonicalPageRef}` : `${candidate.type}:unassigned`] : []
|
|
6264
|
+
]);
|
|
6265
|
+
const item = {
|
|
6266
|
+
id: taskId(candidate),
|
|
6267
|
+
type: candidate.type,
|
|
6268
|
+
title: taskTitle(candidate, members, facts),
|
|
6269
|
+
workPackageId: workPackageId(candidate),
|
|
6270
|
+
canonicalPageRef: candidate.canonicalPageRef,
|
|
6271
|
+
primaryModule: primaryModule(candidate),
|
|
6272
|
+
behaviorIds: candidate.behaviorIds,
|
|
6273
|
+
primaryFactRefs,
|
|
6274
|
+
supportingFactRefs,
|
|
6275
|
+
dependsOnTaskIds: [],
|
|
6276
|
+
conflictsWithTaskIds: [],
|
|
6277
|
+
writeOwnership: {
|
|
6278
|
+
ownedFiles,
|
|
6279
|
+
readOnlyFiles: [],
|
|
6280
|
+
exclusiveScopes,
|
|
6281
|
+
forbiddenGlobalFiles: candidate.type === "bootstrap" ? [] : context.globalHotspotFiles.filter((file) => !ownedFiles.includes(file))
|
|
6282
|
+
},
|
|
6283
|
+
dependencyInterfaces: [],
|
|
6284
|
+
readyGate: {
|
|
6285
|
+
semanticsResolved: candidate.type === "resolution" || closure.unresolvedRefs.length === 0,
|
|
6286
|
+
dependencyInterfacesStable: true,
|
|
6287
|
+
writeOwnershipResolved: ownedFiles.length > 0 || exclusiveScopes.length > 0,
|
|
6288
|
+
contextWithinBudget: members.length === 1 || withinExecutionBudget(candidate, behaviors, context),
|
|
6289
|
+
acceptanceExecutable: members.every((member) => member.acceptance.verificationRequirements.length > 0)
|
|
6290
|
+
},
|
|
6291
|
+
closure,
|
|
6292
|
+
acceptance: {
|
|
6293
|
+
observableOutcome: outcomes.join("\uFF1B"),
|
|
6294
|
+
observableOutcomes: outcomes,
|
|
6295
|
+
requiredFactRefs: uniqueSorted(members.flatMap((item2) => item2.acceptance.requiredFactRefs)),
|
|
6296
|
+
verificationRequirements: uniqueSorted(members.flatMap((item2) => item2.acceptance.verificationRequirements)),
|
|
6297
|
+
reportRefs: uniqueSorted(members.flatMap((item2) => item2.acceptance.reportRefs))
|
|
6298
|
+
},
|
|
6299
|
+
estimate: {
|
|
6300
|
+
factCount: cost.factCount,
|
|
6301
|
+
sourceFileCount: cost.sourceFileCount,
|
|
6302
|
+
traceNodeCount: cost.traceNodeCount,
|
|
6303
|
+
estimatedFactTokens: cost.estimatedFactTokens,
|
|
6304
|
+
downstreamUnlockCount: 0,
|
|
6305
|
+
behaviorCount: cost.behaviorCount,
|
|
6306
|
+
observableOutcomeCount: cost.observableOutcomeCount,
|
|
6307
|
+
productionFileCount: cost.productionFileCount,
|
|
6308
|
+
testFileCount: cost.testFileCount,
|
|
6309
|
+
capabilityCount: cost.capabilityCount
|
|
6310
|
+
}
|
|
6311
|
+
};
|
|
6312
|
+
return item;
|
|
6313
|
+
}).sort((left, right) => typeOrder2[left.type] - typeOrder2[right.type] || left.id.localeCompare(right.id));
|
|
6314
|
+
}
|
|
6315
|
+
function hasPath(tasks, from, to) {
|
|
6316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6317
|
+
const queue = [...tasks.get(from)?.dependsOnTaskIds ?? []];
|
|
6318
|
+
while (queue.length) {
|
|
6319
|
+
const next = queue.shift();
|
|
6320
|
+
if (next === to) return true;
|
|
6321
|
+
if (seen.has(next)) continue;
|
|
6322
|
+
seen.add(next);
|
|
6323
|
+
queue.push(...tasks.get(next)?.dependsOnTaskIds ?? []);
|
|
6324
|
+
}
|
|
6325
|
+
return false;
|
|
6326
|
+
}
|
|
6327
|
+
function addDependency(tasks, taskId2, dependencyId) {
|
|
6328
|
+
if (taskId2 === dependencyId || hasPath(tasks, dependencyId, taskId2)) return;
|
|
6329
|
+
const task = tasks.get(taskId2);
|
|
6330
|
+
task.dependsOnTaskIds = uniqueSorted([...task.dependsOnTaskIds, dependencyId]);
|
|
6331
|
+
}
|
|
6332
|
+
function orderPackageLane(items) {
|
|
6333
|
+
const ids = new Set(items.map((item) => item.id));
|
|
6334
|
+
const remaining = new Set(ids);
|
|
6335
|
+
const result = [];
|
|
6336
|
+
while (remaining.size) {
|
|
6337
|
+
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));
|
|
6338
|
+
const selected = ready[0] ?? items.filter((item) => remaining.has(item.id)).sort((left, right) => left.id.localeCompare(right.id))[0];
|
|
6339
|
+
result.push(selected);
|
|
6340
|
+
remaining.delete(selected.id);
|
|
6341
|
+
}
|
|
6342
|
+
return result;
|
|
6343
|
+
}
|
|
6344
|
+
function packageSerializationRanks(tasks) {
|
|
6345
|
+
const packageIds = uniqueSorted(tasks.flatMap((task) => task.workPackageId ? [task.workPackageId] : []));
|
|
6346
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
6347
|
+
const dependencies = new Map(packageIds.map((packageId) => [packageId, /* @__PURE__ */ new Set()]));
|
|
6348
|
+
for (const task of tasks) {
|
|
6349
|
+
if (!task.workPackageId) continue;
|
|
6350
|
+
for (const dependencyId of task.dependsOnTaskIds) {
|
|
6351
|
+
const dependencyPackageId = taskById.get(dependencyId)?.workPackageId;
|
|
6352
|
+
if (dependencyPackageId && dependencyPackageId !== task.workPackageId) dependencies.get(task.workPackageId).add(dependencyPackageId);
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
const remaining = new Set(packageIds);
|
|
6356
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
6357
|
+
while (remaining.size) {
|
|
6358
|
+
const ready = packageIds.filter((packageId) => remaining.has(packageId) && [...dependencies.get(packageId)].every((dependencyId) => !remaining.has(dependencyId)));
|
|
6359
|
+
const selected = ready[0] ?? packageIds.find((packageId) => remaining.has(packageId));
|
|
6360
|
+
ranks.set(selected, ranks.size);
|
|
6361
|
+
remaining.delete(selected);
|
|
6362
|
+
}
|
|
6363
|
+
return ranks;
|
|
6364
|
+
}
|
|
6365
|
+
function conflictReasons(left, right) {
|
|
6366
|
+
const ownedOverlap = left.writeOwnership.ownedFiles.filter((file) => right.writeOwnership.ownedFiles.includes(file)).map((file) => `owned-file:${file}`);
|
|
6367
|
+
const scopeOverlap = left.writeOwnership.exclusiveScopes.filter((scope) => right.writeOwnership.exclusiveScopes.includes(scope)).map((scope) => `exclusive-scope:${scope}`);
|
|
6368
|
+
return uniqueSorted([...ownedOverlap, ...scopeOverlap]);
|
|
6369
|
+
}
|
|
6370
|
+
function wireDependenciesAndConflicts(tasks, behaviorPlan) {
|
|
6371
|
+
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
6372
|
+
const taskByBehavior = new Map(tasks.flatMap((item) => item.behaviorIds.map((behaviorId) => [behaviorId, item.id])));
|
|
6373
|
+
const behaviorById = new Map(behaviorPlan.responsibilities.map((item) => [item.id, item]));
|
|
6374
|
+
for (const task of tasks) {
|
|
6375
|
+
const dependencies = task.behaviorIds.flatMap((behaviorId) => behaviorById.get(behaviorId)?.dependsOnBehaviorIds ?? []).map((behaviorId) => taskByBehavior.get(behaviorId)).filter((id) => Boolean(id) && id !== task.id);
|
|
6376
|
+
task.dependsOnTaskIds = uniqueSorted(dependencies);
|
|
6377
|
+
}
|
|
6378
|
+
const packages = /* @__PURE__ */ new Map();
|
|
6379
|
+
for (const task of tasks) {
|
|
6380
|
+
if (task.workPackageId === null) continue;
|
|
6381
|
+
packages.set(task.workPackageId, [...packages.get(task.workPackageId) ?? [], task]);
|
|
6382
|
+
}
|
|
6383
|
+
for (const items of packages.values()) {
|
|
6384
|
+
const lane = orderPackageLane(items);
|
|
6385
|
+
for (let index = 1; index < lane.length; index++) addDependency(byId, lane[index].id, lane[index - 1].id);
|
|
6386
|
+
}
|
|
6387
|
+
const packageRanks = packageSerializationRanks(tasks);
|
|
6388
|
+
const edges = [];
|
|
6389
|
+
for (let leftIndex = 0; leftIndex < tasks.length; leftIndex++) {
|
|
6390
|
+
for (let rightIndex = leftIndex + 1; rightIndex < tasks.length; rightIndex++) {
|
|
6391
|
+
const left = tasks[leftIndex];
|
|
6392
|
+
const right = tasks[rightIndex];
|
|
6393
|
+
const reasons = conflictReasons(left, right);
|
|
6394
|
+
if (!reasons.length) continue;
|
|
6395
|
+
edges.push({ leftTaskId: left.id, rightTaskId: right.id, reasons });
|
|
6396
|
+
left.conflictsWithTaskIds = uniqueSorted([...left.conflictsWithTaskIds, right.id]);
|
|
6397
|
+
right.conflictsWithTaskIds = uniqueSorted([...right.conflictsWithTaskIds, left.id]);
|
|
6398
|
+
if (hasPath(byId, left.id, right.id) || hasPath(byId, right.id, left.id)) continue;
|
|
6399
|
+
const leftPackageRank = left.workPackageId ? packageRanks.get(left.workPackageId) : void 0;
|
|
6400
|
+
const rightPackageRank = right.workPackageId ? packageRanks.get(right.workPackageId) : void 0;
|
|
6401
|
+
if (leftPackageRank !== void 0 && rightPackageRank !== void 0 && leftPackageRank !== rightPackageRank) {
|
|
6402
|
+
const [later, earlier] = leftPackageRank > rightPackageRank ? [left, right] : [right, left];
|
|
6403
|
+
addDependency(byId, later.id, earlier.id);
|
|
6404
|
+
} else {
|
|
6405
|
+
addDependency(byId, right.id, left.id);
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
for (const task of tasks) {
|
|
6410
|
+
const providers = task.dependsOnTaskIds.map((id) => byId.get(id)).filter((item) => Boolean(item));
|
|
6411
|
+
const businessProviderIds = new Set(task.behaviorIds.flatMap((behaviorId) => behaviorById.get(behaviorId)?.dependsOnBehaviorIds ?? []).map((behaviorId) => taskByBehavior.get(behaviorId)).filter((id) => Boolean(id)));
|
|
6412
|
+
task.writeOwnership.readOnlyFiles = uniqueSorted(providers.flatMap((provider) => provider.writeOwnership.ownedFiles).filter((file) => !task.writeOwnership.ownedFiles.includes(file)));
|
|
6413
|
+
task.dependencyInterfaces = providers.map((provider) => {
|
|
6414
|
+
const kind = businessProviderIds.has(provider.id) ? "module" : "ordering";
|
|
6415
|
+
return {
|
|
6416
|
+
kind,
|
|
6417
|
+
ref: kind === "module" ? `interface:${provider.primaryModule}` : `ordering:${provider.id}`,
|
|
6418
|
+
providerTaskId: provider.id,
|
|
6419
|
+
files: provider.writeOwnership.ownedFiles.filter((file) => !task.writeOwnership.ownedFiles.includes(file)),
|
|
6420
|
+
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"],
|
|
6421
|
+
orderingConstraints: [`${provider.id} must be locally verified before ${task.id}`]
|
|
6422
|
+
};
|
|
6423
|
+
});
|
|
6424
|
+
}
|
|
6425
|
+
return edges.sort((left, right) => `${left.leftTaskId}:${left.rightTaskId}`.localeCompare(`${right.leftTaskId}:${right.rightTaskId}`));
|
|
6426
|
+
}
|
|
6427
|
+
function downstreamCounts2(tasks) {
|
|
6428
|
+
const children = /* @__PURE__ */ new Map();
|
|
6429
|
+
for (const task of tasks) for (const dependency of task.dependsOnTaskIds) children.set(dependency, [...children.get(dependency) ?? [], task.id]);
|
|
6430
|
+
const result = /* @__PURE__ */ new Map();
|
|
6431
|
+
for (const task of tasks) {
|
|
6432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6433
|
+
const queue = [...children.get(task.id) ?? []];
|
|
6434
|
+
while (queue.length) {
|
|
6435
|
+
const next = queue.shift();
|
|
6436
|
+
if (seen.has(next)) continue;
|
|
6437
|
+
seen.add(next);
|
|
6438
|
+
queue.push(...children.get(next) ?? []);
|
|
6439
|
+
}
|
|
6440
|
+
result.set(task.id, seen.size);
|
|
6441
|
+
}
|
|
6442
|
+
return result;
|
|
6443
|
+
}
|
|
6444
|
+
function pageWorkPackages(tasks, facts, canonicalBySurface) {
|
|
6445
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6446
|
+
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
6447
|
+
for (const task of tasks) {
|
|
6448
|
+
if (task.workPackageId === null) continue;
|
|
6449
|
+
grouped.set(task.workPackageId, [...grouped.get(task.workPackageId) ?? [], task]);
|
|
6450
|
+
}
|
|
6451
|
+
return [...grouped.entries()].map(([id, items]) => {
|
|
6452
|
+
const canonicalPageRef = items[0]?.canonicalPageRef ?? null;
|
|
6453
|
+
const title = facts.surfaces.find((surface) => surface.id === canonicalPageRef)?.name ?? canonicalPageRef ?? "Unassigned";
|
|
6454
|
+
const ordered = orderPackageLane(items);
|
|
6455
|
+
return {
|
|
6456
|
+
id,
|
|
6457
|
+
canonicalPageRef,
|
|
6458
|
+
title: `${title} Page Work Package`,
|
|
6459
|
+
surfaceRefs: uniqueSorted(facts.surfaces.filter((surface) => (canonicalBySurface[surface.id] ?? surface.id) === canonicalPageRef).map((surface) => surface.id)),
|
|
6460
|
+
workItemIds: ordered.map((item) => item.id),
|
|
6461
|
+
dependsOnPackageIds: uniqueSorted(items.flatMap((item) => item.dependsOnTaskIds).map((taskId2) => taskById.get(taskId2)?.workPackageId).filter((packageId) => Boolean(packageId && packageId !== id))),
|
|
6462
|
+
serialLane: true,
|
|
6463
|
+
exclusiveScopes: uniqueSorted(items.flatMap((item) => item.writeOwnership.exclusiveScopes))
|
|
6464
|
+
};
|
|
6465
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
6466
|
+
}
|
|
6467
|
+
function compileExecutionPlan(behaviorPlan, facts, context = emptyExecutionPlanningContext) {
|
|
6468
|
+
const planningPhase = behaviorPlan.responsibilities.some((behavior) => behavior.type === "resolution") ? "resolution" : "execution";
|
|
6469
|
+
const activeBehaviorPlan = planningPhase === "resolution" ? { ...behaviorPlan, responsibilities: behaviorPlan.responsibilities.filter((behavior) => behavior.type === "resolution") } : behaviorPlan;
|
|
6470
|
+
const canonicalBySurface = canonicalPageRefs(facts);
|
|
6471
|
+
const clusters = planningPhase === "resolution" ? activeBehaviorPlan.responsibilities.map((behavior) => candidateForBehavior(behavior, facts, canonicalBySurface)) : clusterBehaviors(activeBehaviorPlan, facts, context);
|
|
6472
|
+
const tasks = initialTasks(clusters, activeBehaviorPlan, facts, canonicalBySurface, context);
|
|
6473
|
+
const fileConflictEdges = wireDependenciesAndConflicts(tasks, activeBehaviorPlan);
|
|
6474
|
+
const downstream = downstreamCounts2(tasks);
|
|
6475
|
+
for (const task of tasks) task.estimate.downstreamUnlockCount = downstream.get(task.id) ?? 0;
|
|
6476
|
+
const taskByBehavior = new Map(tasks.flatMap((task) => task.behaviorIds.map((behaviorId) => [behaviorId, task.id])));
|
|
6477
|
+
const primaryOwnerByFactRef = Object.fromEntries(Object.entries(behaviorPlan.ownership.primaryOwnerByFactRef).flatMap(([ref, behaviorId]) => {
|
|
6478
|
+
const taskId2 = taskByBehavior.get(behaviorId);
|
|
6479
|
+
return taskId2 ? [[ref, taskId2]] : [];
|
|
6480
|
+
}).sort(([left], [right]) => left.localeCompare(right)));
|
|
6481
|
+
const supportingTaskIdsByFactRef = {};
|
|
6482
|
+
for (const task of tasks) for (const ref of task.supportingFactRefs) supportingTaskIdsByFactRef[ref] = uniqueSorted([...supportingTaskIdsByFactRef[ref] ?? [], task.id]);
|
|
6483
|
+
return {
|
|
6484
|
+
taskPlanVersion: TASK_PLAN_VERSION,
|
|
6485
|
+
executionPlanVersion: EXECUTION_PLAN_VERSION,
|
|
6486
|
+
planningModelVersion: behaviorPlan.planningModelVersion,
|
|
6487
|
+
factsAdapterVersion: behaviorPlan.factsAdapterVersion,
|
|
6488
|
+
plannerPolicyVersion: behaviorPlan.plannerPolicyVersion,
|
|
6489
|
+
baseline: behaviorPlan.baseline,
|
|
6490
|
+
planningPhase,
|
|
6491
|
+
entryResolution: behaviorPlan.entryResolution,
|
|
6492
|
+
behaviorResponsibilities: behaviorPlan.responsibilities,
|
|
6493
|
+
pageWorkPackages: pageWorkPackages(tasks, facts, canonicalBySurface),
|
|
6494
|
+
tasks,
|
|
6495
|
+
ownership: {
|
|
6496
|
+
primaryOwnerByFactRef,
|
|
6497
|
+
supportingTaskIdsByFactRef: Object.fromEntries(Object.entries(supportingTaskIdsByFactRef).sort(([left], [right]) => left.localeCompare(right)))
|
|
6498
|
+
},
|
|
6499
|
+
behaviorOwnership: behaviorPlan.ownership,
|
|
6500
|
+
fileConflictEdges,
|
|
6501
|
+
planningWarnings: behaviorPlan.planningWarnings
|
|
6502
|
+
};
|
|
6503
|
+
}
|
|
6504
|
+
function compileFromPlanningFacts(facts, androidFingerprint, context = emptyExecutionPlanningContext) {
|
|
6505
|
+
return compileExecutionPlan(compileBehaviorPlanFromPlanningFacts(facts, androidFingerprint), facts, context);
|
|
6506
|
+
}
|
|
6507
|
+
|
|
6508
|
+
// src/planning/storage.ts
|
|
6509
|
+
import fs27 from "fs";
|
|
6130
6510
|
import path33 from "path";
|
|
6131
6511
|
function taskPlanRoot(projectRoot) {
|
|
6132
6512
|
return path33.join(migrationDirectory(projectRoot), "tasks");
|
|
6133
6513
|
}
|
|
6134
|
-
function taskArtifactStem(
|
|
6135
|
-
return
|
|
6514
|
+
function taskArtifactStem(taskId2) {
|
|
6515
|
+
return taskId2.replace(/^task:/, "");
|
|
6516
|
+
}
|
|
6517
|
+
function workPackageArtifactStem(workPackageId2) {
|
|
6518
|
+
return workPackageId2.replace(/^package:/, "");
|
|
6519
|
+
}
|
|
6520
|
+
function codeRefIndex(value) {
|
|
6521
|
+
const allowed = ["id", "file", "name", "symbol", "kind", "type", "capabilityRef", "line", "startLine", "endLine"];
|
|
6522
|
+
return Object.fromEntries(allowed.filter((key) => value[key] !== void 0).map((key) => [key, value[key]]));
|
|
6136
6523
|
}
|
|
6137
|
-
function
|
|
6524
|
+
function taskContractView(task) {
|
|
6525
|
+
return { ...task, closure: { ...task.closure, codeRefs: task.closure.codeRefs.map(codeRefIndex) } };
|
|
6526
|
+
}
|
|
6527
|
+
function coreFactsForTask(task, facts) {
|
|
6528
|
+
return coreFactsForRefs([...task.primaryFactRefs, ...task.supportingFactRefs], task.closure.unresolvedRefs, facts);
|
|
6529
|
+
}
|
|
6530
|
+
function coreFactsForRefs(refsInput, unresolvedRefs, facts) {
|
|
6531
|
+
const refs = new Set(refsInput);
|
|
6532
|
+
return [
|
|
6533
|
+
...facts.surfaces.filter((item) => refs.has(item.id)).map((value) => ({ kind: "surface", value })),
|
|
6534
|
+
...facts.controls.filter((item) => refs.has(item.id)).map((value) => ({ kind: "control", value })),
|
|
6535
|
+
...facts.entries.filter((item) => refs.has(item.id)).map((value) => ({
|
|
6536
|
+
kind: "entry",
|
|
6537
|
+
value: { ...value, trace: { ...value.trace, codeRefs: value.trace.codeRefs.map(codeRefIndex) } }
|
|
6538
|
+
})),
|
|
6539
|
+
...facts.effects.filter((item) => refs.has(item.id)).map((value) => ({ kind: "effect", value })),
|
|
6540
|
+
...facts.issues.filter((item) => refs.has(item.id) || unresolvedRefs.includes(item.id)).map((value) => ({ kind: "issue", value }))
|
|
6541
|
+
];
|
|
6542
|
+
}
|
|
6543
|
+
function sourceContextForTask(task) {
|
|
6544
|
+
return uniqueBy([
|
|
6545
|
+
...task.closure.sourceEvidence.map((value) => ({ kind: "source-evidence", value })),
|
|
6546
|
+
...task.closure.codeRefs.map((value) => ({ kind: "code-ref", value: codeRefIndex(value) }))
|
|
6547
|
+
], stableJson);
|
|
6548
|
+
}
|
|
6549
|
+
function tasksForPackage(workPackage, plan) {
|
|
6550
|
+
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
6551
|
+
return workPackage.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
6552
|
+
}
|
|
6553
|
+
function effectOutcome2(effectRef, facts) {
|
|
6554
|
+
const effect = facts.effects.find((item) => item.id === effectRef);
|
|
6555
|
+
if (!effect) return effectRef;
|
|
6556
|
+
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;
|
|
6557
|
+
const label = target ?? effect.targetId;
|
|
6558
|
+
if (effect.kind === "opens") return `\u6253\u5F00 ${label}`;
|
|
6559
|
+
if (effect.kind === "navigates-to") return `\u8FDB\u5165 ${label}`;
|
|
6560
|
+
if (effect.kind === "launches-for-result") return `\u542F\u52A8 ${label} \u5E76\u5904\u7406\u8FD4\u56DE\u7ED3\u679C`;
|
|
6561
|
+
return `\u5904\u7406 ${label}`;
|
|
6562
|
+
}
|
|
6563
|
+
function pageWorkPackageContractView(workPackage, plan, facts) {
|
|
6564
|
+
const tasks = tasksForPackage(workPackage, plan);
|
|
6565
|
+
const taskByBehavior = new Map(tasks.flatMap((task) => task.behaviorIds.map((behaviorId) => [behaviorId, task])));
|
|
6566
|
+
const behaviorById = new Map(plan.behaviorResponsibilities.map((behavior) => [behavior.id, behavior]));
|
|
6567
|
+
const orderedBehaviors = tasks.flatMap((task) => task.behaviorIds.map((id) => behaviorById.get(id))).filter((item) => Boolean(item));
|
|
6568
|
+
const ownedFiles = uniqueSorted(tasks.flatMap((task) => task.writeOwnership.ownedFiles));
|
|
6569
|
+
const packageByTask = new Map(plan.tasks.flatMap((task) => task.workPackageId ? [[task.id, task.workPackageId]] : []));
|
|
6570
|
+
const dependencyGroups = /* @__PURE__ */ new Map();
|
|
6571
|
+
for (const task of tasks) for (const dependency of task.dependencyInterfaces) {
|
|
6572
|
+
const providerTaskId = dependency.providerTaskId;
|
|
6573
|
+
const providerPackageId = providerTaskId ? packageByTask.get(providerTaskId) : null;
|
|
6574
|
+
if (!providerTaskId || !providerPackageId || providerPackageId === workPackage.id) continue;
|
|
6575
|
+
const key = `${dependency.kind}:${dependency.ref}:${providerPackageId}`;
|
|
6576
|
+
const current = dependencyGroups.get(key) ?? {
|
|
6577
|
+
kind: dependency.kind,
|
|
6578
|
+
ref: dependency.ref,
|
|
6579
|
+
providerPackageId,
|
|
6580
|
+
providerWorkItemIds: [],
|
|
6581
|
+
files: [],
|
|
6582
|
+
errorModes: [],
|
|
6583
|
+
orderingConstraints: []
|
|
6584
|
+
};
|
|
6585
|
+
current.providerWorkItemIds = uniqueSorted([...current.providerWorkItemIds, providerTaskId]);
|
|
6586
|
+
current.files = uniqueSorted([...current.files, ...dependency.files]);
|
|
6587
|
+
current.errorModes = uniqueSorted([...current.errorModes, ...dependency.errorModes]);
|
|
6588
|
+
current.orderingConstraints = uniqueSorted([...current.orderingConstraints, ...dependency.orderingConstraints]);
|
|
6589
|
+
dependencyGroups.set(key, current);
|
|
6590
|
+
}
|
|
6591
|
+
const sourceFiles = uniqueSorted(tasks.flatMap((task) => [
|
|
6592
|
+
...task.closure.sourceEvidence.map((item) => item.file),
|
|
6593
|
+
...task.closure.codeRefs.flatMap((item) => typeof item.file === "string" ? [item.file] : [])
|
|
6594
|
+
]));
|
|
6595
|
+
return {
|
|
6596
|
+
...workPackage,
|
|
6597
|
+
behaviorIds: orderedBehaviors.map((behavior) => behavior.id),
|
|
6598
|
+
workItems: tasks.map((task, index) => ({
|
|
6599
|
+
ordinal: index + 1,
|
|
6600
|
+
id: task.id,
|
|
6601
|
+
type: task.type,
|
|
6602
|
+
title: task.title,
|
|
6603
|
+
behaviorIds: task.behaviorIds
|
|
6604
|
+
})),
|
|
6605
|
+
features: orderedBehaviors.map((behavior, index) => ({
|
|
6606
|
+
ordinal: index + 1,
|
|
6607
|
+
behaviorId: behavior.id,
|
|
6608
|
+
workItemId: taskByBehavior.get(behavior.id).id,
|
|
6609
|
+
type: behavior.type,
|
|
6610
|
+
title: behavior.title,
|
|
6611
|
+
triggerLabels: uniqueSorted(facts.entries.filter((entry) => behavior.closure.entryRefs.includes(entry.id)).map((entry) => entry.label)),
|
|
6612
|
+
controlNames: uniqueSorted(facts.controls.filter((control) => behavior.closure.controlRefs.includes(control.id)).map((control) => control.name)),
|
|
6613
|
+
interactionOutcomes: uniqueSorted(behavior.closure.interactionRefs.map((ref) => effectOutcome2(ref, facts))),
|
|
6614
|
+
entryRefs: behavior.closure.entryRefs,
|
|
6615
|
+
surfaceRefs: behavior.closure.surfaceRefs,
|
|
6616
|
+
controlRefs: behavior.closure.controlRefs,
|
|
6617
|
+
interactionRefs: behavior.closure.interactionRefs,
|
|
6618
|
+
unresolvedRefs: behavior.closure.unresolvedRefs,
|
|
6619
|
+
primaryFactRefs: behavior.primaryFactRefs,
|
|
6620
|
+
supportingFactRefs: behavior.supportingFactRefs,
|
|
6621
|
+
requiresSourceReview: behavior.closure.requiresSourceReview,
|
|
6622
|
+
observableOutcome: behavior.acceptance.observableOutcome,
|
|
6623
|
+
requiredFactRefs: behavior.acceptance.requiredFactRefs,
|
|
6624
|
+
verificationRequirements: behavior.acceptance.verificationRequirements,
|
|
6625
|
+
reportRefs: behavior.acceptance.reportRefs
|
|
6626
|
+
})),
|
|
6627
|
+
writeOwnership: {
|
|
6628
|
+
ownedFiles,
|
|
6629
|
+
readOnlyFiles: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.readOnlyFiles).filter((file) => !ownedFiles.includes(file))),
|
|
6630
|
+
exclusiveScopes: workPackage.exclusiveScopes,
|
|
6631
|
+
forbiddenGlobalFiles: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.forbiddenGlobalFiles).filter((file) => !ownedFiles.includes(file)))
|
|
6632
|
+
},
|
|
6633
|
+
dependencyInterfaces: [...dependencyGroups.values()].sort((left, right) => `${left.providerPackageId}:${left.ref}`.localeCompare(`${right.providerPackageId}:${right.ref}`)),
|
|
6634
|
+
acceptance: {
|
|
6635
|
+
observableOutcomes: uniqueSorted(orderedBehaviors.map((behavior) => behavior.acceptance.observableOutcome)),
|
|
6636
|
+
requiredFactRefs: uniqueSorted(orderedBehaviors.flatMap((behavior) => behavior.acceptance.requiredFactRefs)),
|
|
6637
|
+
verificationRequirements: uniqueSorted(orderedBehaviors.flatMap((behavior) => behavior.acceptance.verificationRequirements)),
|
|
6638
|
+
reportRefs: uniqueSorted(orderedBehaviors.flatMap((behavior) => behavior.acceptance.reportRefs))
|
|
6639
|
+
},
|
|
6640
|
+
estimate: {
|
|
6641
|
+
behaviorCount: orderedBehaviors.length,
|
|
6642
|
+
workItemCount: tasks.length,
|
|
6643
|
+
factCount: uniqueSorted(tasks.flatMap((task) => [...task.primaryFactRefs, ...task.supportingFactRefs])).length,
|
|
6644
|
+
sourceFileCount: sourceFiles.length,
|
|
6645
|
+
traceNodeCount: uniqueBy(tasks.flatMap((task) => task.closure.codeRefs), stableJson).length,
|
|
6646
|
+
estimatedFactTokens: tasks.reduce((total, task) => total + task.estimate.estimatedFactTokens, 0),
|
|
6647
|
+
productionFileCount: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.ownedFiles).filter((file) => !/(^|\/)(test|ohosTest)(\/|$)|\.(test|spec)\./i.test(file))).length,
|
|
6648
|
+
testFileCount: uniqueSorted(tasks.flatMap((task) => task.writeOwnership.ownedFiles).filter((file) => /(^|\/)(test|ohosTest)(\/|$)|\.(test|spec)\./i.test(file))).length,
|
|
6649
|
+
capabilityCount: tasks.reduce((total, task) => total + task.estimate.capabilityCount, 0)
|
|
6650
|
+
}
|
|
6651
|
+
};
|
|
6652
|
+
}
|
|
6653
|
+
function coreFactsForPageWorkPackage(workPackage, facts) {
|
|
6654
|
+
const tasksRefs = workPackage.features.flatMap((feature) => [...feature.primaryFactRefs, ...feature.supportingFactRefs]);
|
|
6655
|
+
return coreFactsForRefs(tasksRefs, workPackage.features.flatMap((feature) => feature.unresolvedRefs), facts);
|
|
6656
|
+
}
|
|
6657
|
+
function sourceContextForPageWorkPackage(workPackage, plan) {
|
|
6658
|
+
return uniqueBy(tasksForPackage(workPackage, plan).flatMap(sourceContextForTask), stableJson);
|
|
6659
|
+
}
|
|
6660
|
+
function renderResolutionTaskBrief(task, plan) {
|
|
6661
|
+
if (task.type !== "resolution") throw new Error("Resolution brief requires a Resolution Task");
|
|
6138
6662
|
const list = (values) => values.length ? values.map((value) => `- \`${value}\``).join("\n") : "- \u65E0";
|
|
6139
6663
|
const sourceLocations = [.../* @__PURE__ */ new Set([
|
|
6140
6664
|
...task.closure.sourceEvidence.map((item) => `${item.file}${item.line ? `:${item.line}${(() => {
|
|
@@ -6152,15 +6676,54 @@ function renderTaskBrief(task) {
|
|
|
6152
6676
|
return "";
|
|
6153
6677
|
})()}`] : [])
|
|
6154
6678
|
])].sort();
|
|
6155
|
-
|
|
6679
|
+
const sourcePreview = sourceLocations.slice(0, 20);
|
|
6680
|
+
const hiddenSourceCount = sourceLocations.length - sourcePreview.length;
|
|
6681
|
+
const taskKind = "Resolution Task";
|
|
6682
|
+
const identity = [
|
|
6683
|
+
`- ${taskKind} ID\uFF1A\`${task.id}\``,
|
|
6684
|
+
`- Execution Plan Hash\uFF1A\`${sha2562(plan)}\``,
|
|
6685
|
+
`- Facts SHA256\uFF1A\`${plan.baseline.factsSha256}\``,
|
|
6686
|
+
`- Android Fingerprint\uFF1A${plan.baseline.androidFingerprint ? `\`${plan.baseline.androidFingerprint}\`` : "\u672A\u63D0\u4F9B"}`,
|
|
6687
|
+
`- Behavior IDs\uFF1A${task.behaviorIds.map((id) => `\`${id}\``).join("\u3001")}`,
|
|
6688
|
+
`- Primary Page / Module\uFF1A\`${task.canonicalPageRef ?? "unassigned"}\` / \`${task.primaryModule}\``
|
|
6689
|
+
].join("\n");
|
|
6690
|
+
const ownership = (values, fallback) => values.length ? values.map((value) => `- \`${value}\``).join("\n") : `- ${fallback}`;
|
|
6691
|
+
return `# ${taskKind}\uFF1A${task.title}
|
|
6156
6692
|
|
|
6157
|
-
## \u76EE\u6807
|
|
6693
|
+
## 1. \u6700\u7EC8\u76EE\u6807
|
|
6158
6694
|
|
|
6159
6695
|
${task.acceptance.observableOutcome}
|
|
6160
6696
|
|
|
6161
|
-
## \
|
|
6697
|
+
## 2. \u8EAB\u4EFD\u4E0E\u57FA\u7EBF
|
|
6698
|
+
|
|
6699
|
+
${identity}
|
|
6700
|
+
|
|
6701
|
+
## 3. \u5B8C\u6210\u5B9A\u4E49
|
|
6702
|
+
|
|
6703
|
+
${task.acceptance.observableOutcomes.map((outcome) => `- ${outcome}`).join("\n")}
|
|
6704
|
+
|
|
6705
|
+
## 4. \u5199\u5165\u6240\u6709\u6743
|
|
6706
|
+
|
|
6707
|
+
### \u5141\u8BB8\u4FEE\u6539
|
|
6708
|
+
|
|
6709
|
+
${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`)}
|
|
6710
|
+
|
|
6711
|
+
### \u53EA\u8BFB\u4F9D\u8D56
|
|
6712
|
+
|
|
6713
|
+
${ownership(task.writeOwnership.readOnlyFiles, "\u65E0")}
|
|
6714
|
+
|
|
6715
|
+
### \u7981\u6B62\u4FEE\u6539
|
|
6716
|
+
|
|
6717
|
+
${ownership(task.writeOwnership.forbiddenGlobalFiles, "\u65E0")}
|
|
6718
|
+
|
|
6719
|
+
## 5. \u524D\u7F6E\u4F9D\u8D56\u4E0E Interface
|
|
6720
|
+
|
|
6721
|
+
${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"}
|
|
6722
|
+
|
|
6723
|
+
## 6. \u884C\u4E3A\u4E0E\u72B6\u6001\u673A
|
|
6162
6724
|
|
|
6163
6725
|
- Task \u7C7B\u578B\uFF1A\`${task.type}\`
|
|
6726
|
+
- Work Package\uFF1A${task.workPackageId ? `\`${task.workPackageId}\`` : "\u4E0D\u9002\u7528\uFF08Resolution Task \u4E0D\u53C2\u4E0E Coding Work Package\uFF09"}
|
|
6164
6727
|
- \u5165\u53E3\u6570\uFF1A${task.closure.entryRefs.length}
|
|
6165
6728
|
- \u9875\u9762\u6570\uFF1A${task.closure.surfaceRefs.length}
|
|
6166
6729
|
- \u63A7\u4EF6\u6570\uFF1A${task.closure.controlRefs.length}
|
|
@@ -6172,171 +6735,388 @@ ${task.acceptance.observableOutcome}
|
|
|
6172
6735
|
- \u4EA4\u4E92\uFF1A${task.closure.interactionRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6173
6736
|
- \u9700\u8981\u6E90\u7801\u590D\u6838\uFF1A${task.closure.requiresSourceReview ? "\u662F" : "\u5426"}
|
|
6174
6737
|
|
|
6175
|
-
## \
|
|
6738
|
+
## 7. Android \u8BC1\u636E
|
|
6739
|
+
|
|
6740
|
+
${sourcePreview.length ? `${sourcePreview.map((value) => `- ${value}`).join("\n")}${hiddenSourceCount > 0 ? `
|
|
6741
|
+
- \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
6742
|
|
|
6177
|
-
|
|
6743
|
+
## 8. Facts \u95ED\u5305
|
|
6178
6744
|
|
|
6179
|
-
|
|
6745
|
+
### \u5F85\u88C1\u51B3\u95EE\u9898
|
|
6180
6746
|
|
|
6181
6747
|
${list(task.closure.unresolvedRefs)}
|
|
6182
6748
|
|
|
6183
|
-
|
|
6749
|
+
### \u4E3B\u6240\u6709\u6743 Facts
|
|
6184
6750
|
|
|
6185
6751
|
${list(task.primaryFactRefs)}
|
|
6186
6752
|
|
|
6187
|
-
|
|
6753
|
+
### Supporting Facts
|
|
6188
6754
|
|
|
6189
6755
|
${list(task.supportingFactRefs)}
|
|
6190
6756
|
|
|
6191
|
-
## \
|
|
6757
|
+
## 9. \u5B9E\u73B0\u7EA6\u675F
|
|
6758
|
+
|
|
6759
|
+
- \u672C Resolution Task \u53EA\u88C1\u51B3 Planning Facts\uFF1B\u4E0D\u5F97\u4FEE\u6539 HarmonyOS \u4E1A\u52A1\u5B9E\u73B0\u3002
|
|
6760
|
+
- \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
|
|
6761
|
+
- \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
|
|
6762
|
+
- 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
|
|
6763
|
+
|
|
6764
|
+
## 10. \u975E\u76EE\u6807
|
|
6765
|
+
|
|
6766
|
+
- \u4E0D\u6269\u5927\u5230\u5F53\u524D Behavior IDs \u4E4B\u5916\u7684\u72EC\u7ACB\u4E1A\u52A1\u7ED3\u679C\u3002
|
|
6767
|
+
- \u4E0D\u7F16\u5199 ArkTS/ArkUI\uFF0C\u4E0D\u628A\u6E90\u7801\u9605\u8BFB\u672C\u8EAB\u89C6\u4E3A\u88C1\u51B3\u5B8C\u6210\u3002
|
|
6768
|
+
- \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
|
|
6769
|
+
|
|
6770
|
+
## 11. \u9A8C\u6536\u547D\u4EE4\u4E0E\u573A\u666F
|
|
6771
|
+
|
|
6772
|
+
${task.acceptance.verificationRequirements.map((value) => `- ${value}`).join("\n")}
|
|
6773
|
+
|
|
6774
|
+
\u9A8C\u6536\u5FC5\u987B\u8986\u76D6\u7684 Report refs\uFF1A${task.acceptance.reportRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}\u3002
|
|
6192
6775
|
|
|
6193
|
-
|
|
6776
|
+
## 12. \u5B8C\u6210\u56DE\u62A5\u683C\u5F0F
|
|
6777
|
+
|
|
6778
|
+
- \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
|
|
6779
|
+
- \u6BCF\u4E2A Behavior ID \u5BF9\u5E94\u7684\u88C1\u51B3\u7ED3\u679C\u3002
|
|
6780
|
+
- \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
|
|
6781
|
+
- \u672A\u89E3\u51B3\u95EE\u9898\uFF1B\u6CA1\u6709\u5219\u660E\u786E\u5199\u201C\u65E0\u201D\u3002
|
|
6194
6782
|
|
|
6195
6783
|
## \u5DE5\u4F5C\u91CF\u63D0\u793A
|
|
6196
6784
|
|
|
6197
6785
|
- Fact \u6570\uFF1A${task.estimate.factCount}
|
|
6786
|
+
- Behavior \u6570\uFF1A${task.estimate.behaviorCount}
|
|
6787
|
+
- \u53EF\u89C2\u5BDF\u7ED3\u679C\u6570\uFF1A${task.estimate.observableOutcomeCount}
|
|
6198
6788
|
- \u6E90\u7801\u6587\u4EF6\u6570\uFF1A${task.estimate.sourceFileCount}
|
|
6789
|
+
- \u751F\u4EA7\u4EE3\u7801\u6587\u4EF6\u6570\uFF1A${task.estimate.productionFileCount}
|
|
6790
|
+
- \u6D4B\u8BD5\u6587\u4EF6\u6570\uFF1A${task.estimate.testFileCount}
|
|
6791
|
+
- Capability \u6570\uFF1A${task.estimate.capabilityCount}
|
|
6199
6792
|
- \u8C03\u7528\u94FE\u8282\u70B9\u6570\uFF1A${task.estimate.traceNodeCount}
|
|
6200
6793
|
- \u4F30\u7B97 Facts Token\uFF1A${task.estimate.estimatedFactTokens}
|
|
6201
6794
|
- \u53EF\u89E3\u9501\u4E0B\u6E38 Task\uFF1A${task.estimate.downstreamUnlockCount}
|
|
6795
|
+
`;
|
|
6796
|
+
}
|
|
6797
|
+
function renderPageWorkPackageBrief(workPackage, plan, facts) {
|
|
6798
|
+
const quoteList = (values, fallback = "\u65E0") => values.length ? values.map((value) => `- \`${value}\``).join("\n") : `- ${fallback}`;
|
|
6799
|
+
const surfaceNames = workPackage.surfaceRefs.map((ref) => `${facts.surfaces.find((surface) => surface.id === ref)?.name ?? ref}\uFF08\`${ref}\`\uFF09`);
|
|
6800
|
+
const featureSections = workPackage.features.map((feature) => {
|
|
6801
|
+
const behavior = plan.behaviorResponsibilities.find((item) => item.id === feature.behaviorId);
|
|
6802
|
+
const sources = uniqueSorted([
|
|
6803
|
+
...behavior.closure.sourceEvidence.map((item) => `${item.file}${item.line ? `:${item.line}${item.endLine && item.endLine !== item.line ? `-${item.endLine}` : ""}` : ""}${item.symbol ? ` \u2014 ${item.symbol}` : ""}`),
|
|
6804
|
+
...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}` : ""}`] : [])
|
|
6805
|
+
]);
|
|
6806
|
+
return `### F-${String(feature.ordinal).padStart(3, "0")}\uFF1A${feature.title}
|
|
6202
6807
|
|
|
6203
|
-
|
|
6808
|
+
- Behavior / Work Item\uFF1A\`${feature.behaviorId}\` / \`${feature.workItemId}\`
|
|
6809
|
+
- \u89E6\u53D1\uFF1A${feature.triggerLabels.join("\u3001") || "\u7531 Android \u6E90\u7801\u5165\u53E3\u786E\u5B9A"}
|
|
6810
|
+
- \u63A7\u4EF6\uFF1A${feature.controlNames.join("\u3001") || "\u65E0\u663E\u5F0F\u63A7\u4EF6"}
|
|
6811
|
+
- \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"}
|
|
6812
|
+
- \u5B9E\u73B0\u7ED3\u679C\uFF1A${feature.observableOutcome}
|
|
6813
|
+
- Required Fact refs\uFF1A${feature.requiredFactRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6814
|
+
- \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"}
|
|
6815
|
+
- Android \u8BC1\u636E\uFF1A
|
|
6816
|
+
${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"}
|
|
6817
|
+
- Report refs\uFF1A${feature.reportRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}`;
|
|
6818
|
+
}).join("\n\n");
|
|
6819
|
+
const tasksById = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
6820
|
+
const packageTaskIds = new Set(workPackage.workItemIds);
|
|
6821
|
+
const internalSequence = workPackage.workItems.map((item) => {
|
|
6822
|
+
const dependencies = (tasksById.get(item.id)?.dependsOnTaskIds ?? []).filter((id) => packageTaskIds.has(id));
|
|
6823
|
+
return `${item.ordinal}. \`${item.id}\` \u2014 ${item.title}${dependencies.length ? `\uFF1B\u9875\u9762\u5185\u524D\u7F6E\uFF1A${dependencies.map((id) => `\`${id}\``).join("\u3001")}` : ""}`;
|
|
6824
|
+
}).join("\n");
|
|
6825
|
+
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";
|
|
6826
|
+
const acceptanceSequences = workPackage.features.map((feature) => `### F-${String(feature.ordinal).padStart(3, "0")}
|
|
6204
6827
|
|
|
6205
|
-
|
|
6828
|
+
1. \u4ECE\u771F\u5B9E\u5E94\u7528\u5165\u53E3\u5230\u8FBE\u672C Page Work Package \u627F\u8F7D\u9762\u3002
|
|
6829
|
+
2. \u89E6\u53D1\u201C${feature.triggerLabels.join(" / ") || feature.title}\u201D\u3002
|
|
6830
|
+
3. \u65AD\u8A00\uFF1A${feature.observableOutcome}
|
|
6831
|
+
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");
|
|
6832
|
+
return `# Page Work Package\uFF1A${workPackage.title.replace(/ Page Work Package$/, "")}
|
|
6833
|
+
|
|
6834
|
+
> \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
|
|
6835
|
+
|
|
6836
|
+
## 1. \u9875\u9762\u6267\u884C\u5951\u7EA6
|
|
6837
|
+
|
|
6838
|
+
- Page Work Package ID\uFF1A\`${workPackage.id}\`
|
|
6839
|
+
- Execution Plan Hash\uFF1A\`${sha2562(plan)}\`
|
|
6840
|
+
- Facts SHA256\uFF1A\`${plan.baseline.factsSha256}\`
|
|
6841
|
+
- Android Fingerprint\uFF1A${plan.baseline.androidFingerprint ? `\`${plan.baseline.androidFingerprint}\`` : "\u672A\u63D0\u4F9B"}
|
|
6842
|
+
- Canonical Page\uFF1A\`${workPackage.canonicalPageRef ?? "unassigned"}\`
|
|
6843
|
+
- \u627F\u8F7D\u9762\uFF1A${surfaceNames.join("\u3001") || "\u65E0"}
|
|
6844
|
+
- Work Item \u6570\uFF1A${workPackage.workItems.length}
|
|
6845
|
+
- Behavior / \u9875\u9762\u529F\u80FD\u6570\uFF1A${workPackage.features.length}
|
|
6846
|
+
|
|
6847
|
+
## 2. \u9875\u9762\u8303\u56F4\u4E0E\u6700\u7EC8\u76EE\u6807
|
|
6848
|
+
|
|
6849
|
+
\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
|
|
6850
|
+
|
|
6851
|
+
${workPackage.acceptance.observableOutcomes.map((outcome) => `- ${outcome}`).join("\n")}
|
|
6852
|
+
|
|
6853
|
+
## 3. \u5177\u4F53\u5B9E\u73B0\u529F\u80FD
|
|
6854
|
+
|
|
6855
|
+
${featureSections}
|
|
6856
|
+
|
|
6857
|
+
## 4. \u9875\u9762\u72B6\u6001\u4E0E Facts \u95ED\u5305
|
|
6858
|
+
|
|
6859
|
+
- Entry refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.entryRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6860
|
+
- Surface refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.surfaceRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6861
|
+
- Control refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.controlRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6862
|
+
- Interaction refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.interactionRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6863
|
+
- \u5F85\u88C1\u51B3 refs\uFF1A${uniqueSorted(workPackage.features.flatMap((feature) => feature.unresolvedRefs)).map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}
|
|
6864
|
+
|
|
6865
|
+
\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
|
|
6866
|
+
|
|
6867
|
+
## 5. \u9875\u9762\u5185\u5B9E\u65BD\u987A\u5E8F
|
|
6868
|
+
|
|
6869
|
+
${internalSequence}
|
|
6870
|
+
|
|
6871
|
+
\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
|
|
6872
|
+
|
|
6873
|
+
## 6. \u5199\u5165\u6240\u6709\u6743
|
|
6874
|
+
|
|
6875
|
+
### \u5141\u8BB8\u4FEE\u6539
|
|
6876
|
+
|
|
6877
|
+
${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`)}
|
|
6878
|
+
|
|
6879
|
+
### \u53EA\u8BFB\u4F9D\u8D56
|
|
6880
|
+
|
|
6881
|
+
${quoteList(workPackage.writeOwnership.readOnlyFiles)}
|
|
6882
|
+
|
|
6883
|
+
### \u7981\u6B62\u4FEE\u6539
|
|
6884
|
+
|
|
6885
|
+
${quoteList(workPackage.writeOwnership.forbiddenGlobalFiles)}
|
|
6886
|
+
|
|
6887
|
+
### \u72EC\u5360\u8303\u56F4
|
|
6888
|
+
|
|
6889
|
+
${quoteList(workPackage.writeOwnership.exclusiveScopes)}
|
|
6890
|
+
|
|
6891
|
+
## 7. \u8DE8\u9875\u9762\u4F9D\u8D56\u4E0E Interface
|
|
6206
6892
|
|
|
6207
|
-
|
|
6893
|
+
${dependencyText}
|
|
6208
6894
|
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6895
|
+
\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
|
|
6896
|
+
|
|
6897
|
+
## 8. UI \u64CD\u4F5C\u4E0E\u9A8C\u6536\u5E8F\u5217
|
|
6898
|
+
|
|
6899
|
+
${acceptanceSequences}
|
|
6900
|
+
|
|
6901
|
+
\u901A\u7528\u9A8C\u8BC1\u8981\u6C42\uFF1A
|
|
6902
|
+
|
|
6903
|
+
${workPackage.acceptance.verificationRequirements.map((value) => `- ${value}`).join("\n")}
|
|
6904
|
+
|
|
6905
|
+
\u9A8C\u6536\u5FC5\u987B\u8986\u76D6\u7684 Report refs\uFF1A${workPackage.acceptance.reportRefs.map((ref) => `\`${ref}\``).join("\u3001") || "\u65E0"}\u3002
|
|
6906
|
+
|
|
6907
|
+
## 9. \u975E\u76EE\u6807\u4E0E\u5B9E\u73B0\u7EA6\u675F
|
|
6908
|
+
|
|
6909
|
+
- \u4E0D\u6269\u5927\u5230\u672C\u5305 Behavior IDs \u4E4B\u5916\u7684\u72EC\u7ACB\u4E1A\u52A1\u7ED3\u679C\u3002
|
|
6910
|
+
- \u4E0D\u4FEE\u6539\u53EA\u8BFB\u6216\u7981\u6B62\u6587\u4EF6\uFF0C\u4E0D\u91CD\u6784\u4E0A\u6E38 Module \u7684 Implementation\u3002
|
|
6911
|
+
- \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
|
|
6912
|
+
- \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
|
|
6913
|
+
- \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
|
|
6914
|
+
|
|
6915
|
+
## 10. \u5B8C\u6210\u56DE\u62A5\u683C\u5F0F
|
|
6916
|
+
|
|
6917
|
+
- \u4FEE\u6539\u6587\u4EF6\u53CA\u5BF9\u5E94\u5199\u5165\u6240\u6709\u6743\u3002
|
|
6918
|
+
- 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
|
|
6919
|
+
- \u6784\u5EFA\u3001\u5355\u5143\u6D4B\u8BD5\u3001\u9875\u9762/Journey \u6D4B\u8BD5\u547D\u4EE4\u4E0E\u7ED3\u679C\u3002
|
|
6920
|
+
- \u672A\u89E3\u51B3\u95EE\u9898\uFF1B\u6CA1\u6709\u5219\u660E\u786E\u5199\u201C\u65E0\u201D\u3002
|
|
6921
|
+
|
|
6922
|
+
## \u5DE5\u4F5C\u91CF\u63D0\u793A
|
|
6923
|
+
|
|
6924
|
+
- Work Item \u6570\uFF1A${workPackage.estimate.workItemCount}
|
|
6925
|
+
- Behavior \u6570\uFF1A${workPackage.estimate.behaviorCount}
|
|
6926
|
+
- Fact \u6570\uFF1A${workPackage.estimate.factCount}
|
|
6927
|
+
- \u6E90\u7801\u6587\u4EF6\u6570\uFF1A${workPackage.estimate.sourceFileCount}
|
|
6928
|
+
- \u751F\u4EA7\u4EE3\u7801\u6587\u4EF6\u6570\uFF1A${workPackage.estimate.productionFileCount}
|
|
6929
|
+
- \u6D4B\u8BD5\u6587\u4EF6\u6570\uFF1A${workPackage.estimate.testFileCount}
|
|
6930
|
+
- Capability \u6570\uFF1A${workPackage.estimate.capabilityCount}
|
|
6931
|
+
- \u8C03\u7528\u94FE\u8282\u70B9\u6570\uFF1A${workPackage.estimate.traceNodeCount}
|
|
6932
|
+
- \u4F30\u7B97 Facts Token\uFF1A${workPackage.estimate.estimatedFactTokens}
|
|
6212
6933
|
`;
|
|
6213
6934
|
}
|
|
6214
6935
|
function writeFile(file, content) {
|
|
6215
|
-
|
|
6216
|
-
|
|
6936
|
+
fs27.mkdirSync(path33.dirname(file), { recursive: true });
|
|
6937
|
+
fs27.writeFileSync(file, content, "utf8");
|
|
6217
6938
|
}
|
|
6218
|
-
function writeTaskPlan(projectRoot, plan) {
|
|
6939
|
+
function writeTaskPlan(projectRoot, plan, facts) {
|
|
6219
6940
|
const root = taskPlanRoot(projectRoot);
|
|
6220
6941
|
const parent = path33.dirname(root);
|
|
6221
|
-
|
|
6942
|
+
fs27.mkdirSync(parent, { recursive: true });
|
|
6222
6943
|
const temporary = path33.join(parent, `tasks.${process.pid}.${Date.now()}.tmp`);
|
|
6223
6944
|
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));
|
|
6945
|
+
fs27.mkdirSync(temporary, { recursive: true });
|
|
6228
6946
|
const taskPlanSha256 = sha2562(plan);
|
|
6229
|
-
const
|
|
6947
|
+
const graphFile = path33.join(temporary, "execution-graph.json");
|
|
6948
|
+
writeFile(graphFile, prettyStableJson(plan));
|
|
6949
|
+
writeFile(path33.join(temporary, "behavior-plan.json"), prettyStableJson({
|
|
6950
|
+
behaviorPlanVersion: BEHAVIOR_PLAN_VERSION,
|
|
6951
|
+
planningModelVersion: plan.planningModelVersion,
|
|
6952
|
+
factsAdapterVersion: plan.factsAdapterVersion,
|
|
6953
|
+
plannerPolicyVersion: plan.plannerPolicyVersion,
|
|
6954
|
+
baseline: plan.baseline,
|
|
6955
|
+
entryResolution: plan.entryResolution,
|
|
6956
|
+
responsibilities: plan.behaviorResponsibilities,
|
|
6957
|
+
ownership: plan.behaviorOwnership,
|
|
6958
|
+
planningWarnings: plan.planningWarnings
|
|
6959
|
+
}));
|
|
6960
|
+
const resolutionTargets = plan.tasks.filter((task) => task.type === "resolution").map((task) => {
|
|
6230
6961
|
const stem = taskArtifactStem(task.id);
|
|
6231
|
-
const
|
|
6232
|
-
const
|
|
6233
|
-
|
|
6962
|
+
const taskRoot = `resolution-tasks/${stem}`;
|
|
6963
|
+
const briefFile = `${taskRoot}/brief.md`;
|
|
6964
|
+
const contractFile = `${taskRoot}/contract.json`;
|
|
6965
|
+
const coreFactsFile = `${taskRoot}/core-facts.json`;
|
|
6966
|
+
const sourceContextFile = `${taskRoot}/source-context.json`;
|
|
6967
|
+
const acceptanceFile = `${taskRoot}/acceptance.json`;
|
|
6968
|
+
writeFile(path33.join(temporary, briefFile), renderResolutionTaskBrief(task, plan));
|
|
6969
|
+
writeFile(path33.join(temporary, contractFile), prettyStableJson({ executionPlanSha256: taskPlanSha256, baseline: plan.baseline, task: taskContractView(task) }));
|
|
6970
|
+
writeFile(path33.join(temporary, coreFactsFile), prettyStableJson(coreFactsForTask(task, facts)));
|
|
6971
|
+
writeFile(path33.join(temporary, sourceContextFile), prettyStableJson(sourceContextForTask(task)));
|
|
6234
6972
|
writeFile(path33.join(temporary, acceptanceFile), prettyStableJson(task.acceptance));
|
|
6235
|
-
return { id: task.id,
|
|
6973
|
+
return { id: task.id, kind: "resolution-task", title: task.title, briefFile, contractFile, coreFactsFile, sourceContextFile, acceptanceFile };
|
|
6974
|
+
});
|
|
6975
|
+
const pageWorkPackageTargets = plan.pageWorkPackages.map((workPackage) => {
|
|
6976
|
+
const stem = workPackageArtifactStem(workPackage.id);
|
|
6977
|
+
const packageRoot = `page-work-packages/${stem}`;
|
|
6978
|
+
const briefFile = `page-work-packages/${stem}.md`;
|
|
6979
|
+
const contractFile = `${packageRoot}/contract.json`;
|
|
6980
|
+
const coreFactsFile = `${packageRoot}/core-facts.json`;
|
|
6981
|
+
const sourceContextFile = `${packageRoot}/source-context.json`;
|
|
6982
|
+
const acceptanceFile = `${packageRoot}/acceptance.json`;
|
|
6983
|
+
const contract = pageWorkPackageContractView(workPackage, plan, facts);
|
|
6984
|
+
writeFile(path33.join(temporary, briefFile), renderPageWorkPackageBrief(contract, plan, facts));
|
|
6985
|
+
writeFile(path33.join(temporary, contractFile), prettyStableJson({ executionPlanSha256: taskPlanSha256, baseline: plan.baseline, pageWorkPackage: contract }));
|
|
6986
|
+
writeFile(path33.join(temporary, coreFactsFile), prettyStableJson(coreFactsForPageWorkPackage(contract, facts)));
|
|
6987
|
+
writeFile(path33.join(temporary, sourceContextFile), prettyStableJson(sourceContextForPageWorkPackage(workPackage, plan)));
|
|
6988
|
+
writeFile(path33.join(temporary, acceptanceFile), prettyStableJson(contract.acceptance));
|
|
6989
|
+
return { id: workPackage.id, kind: "page-work-package", title: workPackage.title, workItemIds: workPackage.workItemIds, briefFile, contractFile, coreFactsFile, sourceContextFile, acceptanceFile };
|
|
6236
6990
|
});
|
|
6991
|
+
const targets = plan.planningPhase === "resolution" ? resolutionTargets : pageWorkPackageTargets;
|
|
6237
6992
|
const index = {
|
|
6238
6993
|
taskPlanVersion: plan.taskPlanVersion,
|
|
6239
6994
|
taskPlanSha256,
|
|
6240
6995
|
baseline: plan.baseline,
|
|
6241
|
-
|
|
6996
|
+
executionPlanVersion: plan.executionPlanVersion,
|
|
6997
|
+
planningPhase: plan.planningPhase,
|
|
6998
|
+
graphFile: "execution-graph.json",
|
|
6999
|
+
behaviorPlanFile: "behavior-plan.json",
|
|
6242
7000
|
counts: {
|
|
6243
7001
|
total: plan.tasks.length,
|
|
6244
7002
|
resolution: plan.tasks.filter((task) => task.type === "resolution").length,
|
|
6245
7003
|
bootstrap: plan.tasks.filter((task) => task.type === "bootstrap").length,
|
|
6246
|
-
journey: plan.tasks.filter((task) => task.type === "journey").length
|
|
7004
|
+
journey: plan.tasks.filter((task) => task.type === "journey").length,
|
|
7005
|
+
workPackages: plan.pageWorkPackages.length,
|
|
7006
|
+
behaviors: plan.behaviorResponsibilities.length,
|
|
7007
|
+
executionTargets: targets.length
|
|
6247
7008
|
},
|
|
6248
|
-
|
|
7009
|
+
targets,
|
|
7010
|
+
workItems: plan.planningPhase === "execution" ? plan.tasks.map((task) => ({ id: task.id, type: task.type, title: task.title, workPackageId: task.workPackageId, behaviorIds: task.behaviorIds })) : []
|
|
6249
7011
|
};
|
|
6250
7012
|
writeFile(path33.join(temporary, "index.json"), prettyStableJson(index));
|
|
6251
7013
|
try {
|
|
6252
|
-
if (
|
|
6253
|
-
if (
|
|
6254
|
-
|
|
6255
|
-
if (
|
|
7014
|
+
if (fs27.existsSync(backup)) fs27.rmSync(backup, { recursive: true, force: true });
|
|
7015
|
+
if (fs27.existsSync(root)) fs27.renameSync(root, backup);
|
|
7016
|
+
fs27.renameSync(temporary, root);
|
|
7017
|
+
if (fs27.existsSync(backup)) fs27.rmSync(backup, { recursive: true, force: true });
|
|
6256
7018
|
} catch (error) {
|
|
6257
|
-
if (!
|
|
6258
|
-
if (
|
|
7019
|
+
if (!fs27.existsSync(root) && fs27.existsSync(backup)) fs27.renameSync(backup, root);
|
|
7020
|
+
if (fs27.existsSync(temporary)) fs27.rmSync(temporary, { recursive: true, force: true });
|
|
6259
7021
|
throw error;
|
|
6260
7022
|
}
|
|
6261
7023
|
return {
|
|
6262
7024
|
taskPlanSha256,
|
|
6263
|
-
graph: path33.relative(path33.resolve(projectRoot), path33.join(root, "graph.json")).replaceAll("\\", "/"),
|
|
7025
|
+
graph: path33.relative(path33.resolve(projectRoot), path33.join(root, "execution-graph.json")).replaceAll("\\", "/"),
|
|
6264
7026
|
index: path33.relative(path33.resolve(projectRoot), path33.join(root, "index.json")).replaceAll("\\", "/")
|
|
6265
7027
|
};
|
|
6266
7028
|
}
|
|
6267
7029
|
function readTaskPlan(projectRoot) {
|
|
6268
|
-
const
|
|
6269
|
-
|
|
6270
|
-
|
|
7030
|
+
const root = taskPlanRoot(projectRoot);
|
|
7031
|
+
const file = fs27.existsSync(path33.join(root, "execution-graph.json")) ? path33.join(root, "execution-graph.json") : path33.join(root, "graph.json");
|
|
7032
|
+
if (!fs27.existsSync(file)) return null;
|
|
7033
|
+
return JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
6271
7034
|
}
|
|
6272
7035
|
function hasTaskPlan(projectRoot) {
|
|
6273
|
-
return
|
|
7036
|
+
return fs27.existsSync(path33.join(taskPlanRoot(projectRoot), "execution-graph.json")) || fs27.existsSync(path33.join(taskPlanRoot(projectRoot), "graph.json"));
|
|
6274
7037
|
}
|
|
6275
7038
|
|
|
6276
7039
|
// src/planning/materialize.ts
|
|
6277
|
-
function cursorFor(
|
|
6278
|
-
return Buffer.from(JSON.stringify({
|
|
7040
|
+
function cursorFor(targetId, taskPlanSha256, offset) {
|
|
7041
|
+
return Buffer.from(JSON.stringify({ targetId, taskPlanSha256, offset }), "utf8").toString("base64url");
|
|
6279
7042
|
}
|
|
6280
|
-
function cursorOffset(cursor,
|
|
7043
|
+
function cursorOffset(cursor, targetId, taskPlanSha256) {
|
|
6281
7044
|
if (!cursor) return 0;
|
|
6282
7045
|
try {
|
|
6283
7046
|
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
6284
|
-
if (value.
|
|
7047
|
+
if (value.targetId !== targetId || value.taskPlanSha256 !== taskPlanSha256 || !Number.isInteger(value.offset) || value.offset < 0) throw new Error("mismatch");
|
|
6285
7048
|
return value.offset;
|
|
6286
7049
|
} catch {
|
|
6287
7050
|
throw new Error("Invalid or stale task envelope cursor");
|
|
6288
7051
|
}
|
|
6289
7052
|
}
|
|
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}`);
|
|
7053
|
+
function materializeEnvelope(plan, facts, targetId, options = {}) {
|
|
6297
7054
|
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
7055
|
const limit = Math.max(1, Math.min(200, Math.floor(options.maxSourceContext ?? 25)));
|
|
6315
|
-
const offset = cursorOffset(options.cursor,
|
|
7056
|
+
const offset = cursorOffset(options.cursor, targetId, taskPlanSha256);
|
|
7057
|
+
const task = plan.tasks.find((item) => item.id === targetId);
|
|
7058
|
+
if (task) {
|
|
7059
|
+
if (plan.planningPhase !== "resolution" || task.type !== "resolution") throw new Error("Execution phase materializes a Page Work Package, not an internal Coding Work Item");
|
|
7060
|
+
const coreFacts2 = coreFactsForTask(task, facts);
|
|
7061
|
+
const envelopeTask = taskContractView(task);
|
|
7062
|
+
const sourceContext2 = sourceContextForTask(task);
|
|
7063
|
+
if (offset > sourceContext2.length) throw new Error("Task envelope cursor is past the available source context");
|
|
7064
|
+
const page2 = sourceContext2.slice(offset, offset + limit);
|
|
7065
|
+
const nextOffset2 = offset + page2.length;
|
|
7066
|
+
return {
|
|
7067
|
+
taskEnvelopeVersion: TASK_ENVELOPE_VERSION,
|
|
7068
|
+
taskPlanSha256,
|
|
7069
|
+
targetKind: "resolution-task",
|
|
7070
|
+
targetId,
|
|
7071
|
+
task: envelopeTask,
|
|
7072
|
+
pageWorkPackage: null,
|
|
7073
|
+
contract: { executionPlanSha256: taskPlanSha256, baseline: plan.baseline, task: envelopeTask },
|
|
7074
|
+
brief: renderResolutionTaskBrief(task, plan),
|
|
7075
|
+
coreFacts: coreFacts2,
|
|
7076
|
+
dependencyInterfaces: task.dependencyInterfaces,
|
|
7077
|
+
acceptance: task.acceptance,
|
|
7078
|
+
sourceContext: page2,
|
|
7079
|
+
continuation: {
|
|
7080
|
+
cursor: nextOffset2 < sourceContext2.length ? cursorFor(targetId, taskPlanSha256, nextOffset2) : null,
|
|
7081
|
+
remainingSourceContext: Math.max(0, sourceContext2.length - nextOffset2)
|
|
7082
|
+
}
|
|
7083
|
+
};
|
|
7084
|
+
}
|
|
7085
|
+
const workPackage = plan.pageWorkPackages.find((item) => item.id === targetId);
|
|
7086
|
+
if (!workPackage) throw new Error(`Execution target not found: ${targetId}`);
|
|
7087
|
+
if (plan.planningPhase !== "execution") throw new Error("Resolution phase materializes a Resolution Task, not a Page Work Package");
|
|
7088
|
+
const contract = pageWorkPackageContractView(workPackage, plan, facts);
|
|
7089
|
+
const coreFacts = coreFactsForPageWorkPackage(contract, facts);
|
|
7090
|
+
const sourceContext = sourceContextForPageWorkPackage(workPackage, plan);
|
|
6316
7091
|
if (offset > sourceContext.length) throw new Error("Task envelope cursor is past the available source context");
|
|
6317
7092
|
const page = sourceContext.slice(offset, offset + limit);
|
|
6318
7093
|
const nextOffset = offset + page.length;
|
|
6319
7094
|
return {
|
|
6320
7095
|
taskEnvelopeVersion: TASK_ENVELOPE_VERSION,
|
|
6321
7096
|
taskPlanSha256,
|
|
6322
|
-
|
|
6323
|
-
|
|
7097
|
+
targetKind: "page-work-package",
|
|
7098
|
+
targetId,
|
|
7099
|
+
task: null,
|
|
7100
|
+
pageWorkPackage: contract,
|
|
7101
|
+
contract: { executionPlanSha256: taskPlanSha256, baseline: plan.baseline, pageWorkPackage: contract },
|
|
7102
|
+
brief: renderPageWorkPackageBrief(contract, plan, facts),
|
|
6324
7103
|
coreFacts,
|
|
7104
|
+
dependencyInterfaces: contract.dependencyInterfaces,
|
|
7105
|
+
acceptance: contract.acceptance,
|
|
6325
7106
|
sourceContext: page,
|
|
6326
7107
|
continuation: {
|
|
6327
|
-
cursor: nextOffset < sourceContext.length ? cursorFor(
|
|
7108
|
+
cursor: nextOffset < sourceContext.length ? cursorFor(targetId, taskPlanSha256, nextOffset) : null,
|
|
6328
7109
|
remainingSourceContext: Math.max(0, sourceContext.length - nextOffset)
|
|
6329
7110
|
}
|
|
6330
7111
|
};
|
|
6331
7112
|
}
|
|
6332
7113
|
|
|
6333
7114
|
// src/planning/runtime.ts
|
|
6334
|
-
import
|
|
7115
|
+
import fs28 from "fs";
|
|
6335
7116
|
import path34 from "path";
|
|
6336
7117
|
|
|
6337
7118
|
// src/planning/validate.ts
|
|
6338
|
-
function
|
|
6339
|
-
const dependencies = new Map(plan.tasks.map((task) => [task.id, task.dependsOnTaskIds]));
|
|
7119
|
+
function findDependencyCycle(dependencies) {
|
|
6340
7120
|
const visiting = /* @__PURE__ */ new Set();
|
|
6341
7121
|
const visited = /* @__PURE__ */ new Set();
|
|
6342
7122
|
const stack = [];
|
|
@@ -6360,33 +7140,101 @@ function findCycle(plan) {
|
|
|
6360
7140
|
}
|
|
6361
7141
|
return null;
|
|
6362
7142
|
}
|
|
7143
|
+
function findCycle(plan) {
|
|
7144
|
+
return findDependencyCycle(new Map(plan.tasks.map((task) => [task.id, task.dependsOnTaskIds])));
|
|
7145
|
+
}
|
|
7146
|
+
function hasDependencyPath(plan, from, to) {
|
|
7147
|
+
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
7148
|
+
const queue = [...byId.get(from)?.dependsOnTaskIds ?? []];
|
|
7149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7150
|
+
while (queue.length) {
|
|
7151
|
+
const next = queue.shift();
|
|
7152
|
+
if (next === to) return true;
|
|
7153
|
+
if (seen.has(next)) continue;
|
|
7154
|
+
seen.add(next);
|
|
7155
|
+
queue.push(...byId.get(next)?.dependsOnTaskIds ?? []);
|
|
7156
|
+
}
|
|
7157
|
+
return false;
|
|
7158
|
+
}
|
|
6363
7159
|
function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
6364
7160
|
const errors = [];
|
|
6365
7161
|
const warnings = [];
|
|
6366
7162
|
if (plan.taskPlanVersion !== TASK_PLAN_VERSION) errors.push(`taskPlanVersion must be ${TASK_PLAN_VERSION}`);
|
|
7163
|
+
if (plan.executionPlanVersion !== EXECUTION_PLAN_VERSION) errors.push(`executionPlanVersion must be ${EXECUTION_PLAN_VERSION}`);
|
|
6367
7164
|
if (plan.planningModelVersion !== PLANNING_MODEL_VERSION) errors.push(`planningModelVersion must be ${PLANNING_MODEL_VERSION}`);
|
|
6368
7165
|
if (plan.factsAdapterVersion !== FACTS_ADAPTER_VERSION) errors.push(`factsAdapterVersion must be ${FACTS_ADAPTER_VERSION}`);
|
|
6369
7166
|
if (plan.plannerPolicyVersion !== PLANNER_POLICY_VERSION) errors.push(`plannerPolicyVersion must be ${PLANNER_POLICY_VERSION}`);
|
|
6370
7167
|
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
7168
|
if (!fresh) warnings.push("Task plan baseline does not match current Facts, planning inputs, or Android source fingerprint");
|
|
7169
|
+
const behaviorIds = /* @__PURE__ */ new Set();
|
|
7170
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) {
|
|
7171
|
+
if (!/^behavior:[a-f0-9]{16}$/.test(behavior.id)) errors.push(`invalid behavior id ${behavior.id}`);
|
|
7172
|
+
if (behaviorIds.has(behavior.id)) errors.push(`duplicate behavior id ${behavior.id}`);
|
|
7173
|
+
else behaviorIds.add(behavior.id);
|
|
7174
|
+
if (!behavior.primaryFactRefs?.length) errors.push(`${behavior.id}: primaryFactRefs must not be empty`);
|
|
7175
|
+
if (!behavior.acceptance?.observableOutcome?.trim()) errors.push(`${behavior.id}: observableOutcome is required`);
|
|
7176
|
+
for (const ref of behavior.supportingFactRefs ?? []) if (behavior.primaryFactRefs.includes(ref)) errors.push(`${behavior.id}: ${ref} cannot be both primary and supporting`);
|
|
7177
|
+
}
|
|
7178
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) for (const dependency of behavior.dependsOnBehaviorIds ?? []) {
|
|
7179
|
+
if (!behaviorIds.has(dependency)) errors.push(`${behavior.id}: unknown behavior dependency ${dependency}`);
|
|
7180
|
+
if (dependency === behavior.id) errors.push(`${behavior.id}: behavior cannot depend on itself`);
|
|
7181
|
+
}
|
|
7182
|
+
const expectedPlanningPhase = (plan.behaviorResponsibilities ?? []).some((behavior) => behavior.type === "resolution") ? "resolution" : "execution";
|
|
7183
|
+
if (plan.planningPhase !== expectedPlanningPhase) errors.push(`planningPhase must be ${expectedPlanningPhase}`);
|
|
7184
|
+
const activeBehaviors = (plan.behaviorResponsibilities ?? []).filter((behavior) => expectedPlanningPhase === "execution" || behavior.type === "resolution");
|
|
7185
|
+
const activeBehaviorIds = new Set(activeBehaviors.map((behavior) => behavior.id));
|
|
7186
|
+
if (expectedPlanningPhase === "resolution" && (plan.tasks ?? []).some((task) => task.type !== "resolution")) {
|
|
7187
|
+
errors.push("resolution phase must materialize only Resolution Tasks; Coding Work Items are assembled after Resolution is cleared");
|
|
7188
|
+
}
|
|
7189
|
+
if (expectedPlanningPhase === "execution" && (plan.tasks ?? []).some((task) => task.type === "resolution")) {
|
|
7190
|
+
errors.push("execution phase must not contain Resolution Tasks");
|
|
7191
|
+
}
|
|
7192
|
+
if (expectedPlanningPhase === "resolution" && (plan.pageWorkPackages ?? []).length > 0) {
|
|
7193
|
+
errors.push("resolution phase must not materialize Coding Work Packages");
|
|
7194
|
+
}
|
|
7195
|
+
const behaviorOwners = /* @__PURE__ */ new Map();
|
|
7196
|
+
const behaviorSupporters = /* @__PURE__ */ new Map();
|
|
7197
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) for (const ref of behavior.primaryFactRefs ?? []) behaviorOwners.set(ref, [...behaviorOwners.get(ref) ?? [], behavior.id]);
|
|
7198
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) for (const ref of behavior.supportingFactRefs ?? []) behaviorSupporters.set(ref, [...behaviorSupporters.get(ref) ?? [], behavior.id]);
|
|
6372
7199
|
const taskIds = /* @__PURE__ */ new Set();
|
|
6373
7200
|
for (const task of plan.tasks ?? []) {
|
|
6374
7201
|
if (!/^task:[a-f0-9]{16}$/.test(task.id)) errors.push(`invalid task id ${task.id}`);
|
|
6375
7202
|
if (taskIds.has(task.id)) errors.push(`duplicate task id ${task.id}`);
|
|
6376
7203
|
else taskIds.add(task.id);
|
|
6377
7204
|
if (!task.title?.trim()) errors.push(`${task.id}: title is required`);
|
|
7205
|
+
if (expectedPlanningPhase === "resolution" && task.workPackageId !== null) errors.push(`${task.id}: Resolution Task must not belong to a Work Package`);
|
|
7206
|
+
if (expectedPlanningPhase === "execution" && !task.workPackageId?.trim()) errors.push(`${task.id}: Coding Work Item requires workPackageId`);
|
|
7207
|
+
if (!task.primaryModule?.trim()) errors.push(`${task.id}: primaryModule is required`);
|
|
7208
|
+
if (!task.behaviorIds?.length) errors.push(`${task.id}: behaviorIds must not be empty`);
|
|
7209
|
+
if (expectedPlanningPhase === "resolution" && task.behaviorIds?.length !== 1) errors.push(`${task.id}: Resolution Task must project exactly one Resolution Behavior`);
|
|
7210
|
+
for (const behaviorId of task.behaviorIds ?? []) if (!behaviorIds.has(behaviorId)) errors.push(`${task.id}: unknown behavior ${behaviorId}`);
|
|
6378
7211
|
if (!task.primaryFactRefs?.length) errors.push(`${task.id}: primaryFactRefs must not be empty`);
|
|
6379
7212
|
if (!task.acceptance?.observableOutcome?.trim()) errors.push(`${task.id}: observableOutcome is required`);
|
|
6380
7213
|
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
|
-
|
|
7214
|
+
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;
|
|
7215
|
+
if (overBudget && (task.behaviorIds?.length ?? 0) > 1) errors.push(`${task.id}: merged Coding Work Item exceeds the execution budget`);
|
|
7216
|
+
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`);
|
|
7217
|
+
const owned = task.writeOwnership?.ownedFiles ?? [];
|
|
7218
|
+
const readOnly = task.writeOwnership?.readOnlyFiles ?? [];
|
|
7219
|
+
const forbidden = task.writeOwnership?.forbiddenGlobalFiles ?? [];
|
|
7220
|
+
for (const file of owned) if (readOnly.includes(file)) errors.push(`${task.id}: ${file} cannot be both owned and read-only`);
|
|
7221
|
+
for (const file of owned) if (forbidden.includes(file)) errors.push(`${task.id}: ${file} cannot be both owned and forbidden`);
|
|
7222
|
+
if (!(task.writeOwnership?.exclusiveScopes?.length || owned.length)) errors.push(`${task.id}: write ownership requires ownedFiles or an exclusive scope`);
|
|
7223
|
+
if (task.readyGate?.writeOwnershipResolved !== Boolean(task.writeOwnership?.exclusiveScopes?.length || owned.length)) errors.push(`${task.id}: writeOwnershipResolved gate disagrees with the contract`);
|
|
7224
|
+
if (task.readyGate?.semanticsResolved !== (task.type === "resolution" || task.closure.unresolvedRefs.length === 0)) errors.push(`${task.id}: semanticsResolved gate disagrees with unresolved refs`);
|
|
7225
|
+
if (task.readyGate?.contextWithinBudget !== (!overBudget || (task.behaviorIds?.length ?? 0) === 1)) errors.push(`${task.id}: contextWithinBudget gate disagrees with execution estimate`);
|
|
6385
7226
|
}
|
|
6386
7227
|
for (const task of plan.tasks ?? []) for (const dependency of task.dependsOnTaskIds ?? []) {
|
|
6387
7228
|
if (!taskIds.has(dependency)) errors.push(`${task.id}: unknown dependency ${dependency}`);
|
|
6388
7229
|
if (dependency === task.id) errors.push(`${task.id}: task cannot depend on itself`);
|
|
6389
7230
|
}
|
|
7231
|
+
for (const task of plan.tasks ?? []) {
|
|
7232
|
+
const interfaceProviders = (task.dependencyInterfaces ?? []).map((item) => item.providerTaskId).filter((id) => Boolean(id)).sort();
|
|
7233
|
+
if (JSON.stringify(interfaceProviders) !== JSON.stringify([...task.dependsOnTaskIds ?? []].sort())) errors.push(`${task.id}: dependencyInterfaces do not match dependsOnTaskIds`);
|
|
7234
|
+
for (const dependencyInterface of task.dependencyInterfaces ?? []) {
|
|
7235
|
+
if (dependencyInterface.providerTaskId && !taskIds.has(dependencyInterface.providerTaskId)) errors.push(`${task.id}: dependency Interface references unknown provider ${dependencyInterface.providerTaskId}`);
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
6390
7238
|
const cycle = findCycle(plan);
|
|
6391
7239
|
if (cycle) errors.push(`task dependency cycle: ${cycle.join(" -> ")}`);
|
|
6392
7240
|
const owners = /* @__PURE__ */ new Map();
|
|
@@ -6398,14 +7246,20 @@ function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
|
6398
7246
|
supporters.set(ref, [...supporters.get(ref) ?? [], task.id]);
|
|
6399
7247
|
}
|
|
6400
7248
|
}
|
|
6401
|
-
const
|
|
6402
|
-
|
|
7249
|
+
const activePrimaryFactRefs = uniqueSorted(activeBehaviors.flatMap((behavior) => behavior.primaryFactRefs));
|
|
7250
|
+
const behaviorOrphanFactRefs = facts.requiredFactRefs.filter((ref) => (behaviorOwners.get(ref) ?? []).length === 0);
|
|
7251
|
+
const activeTaskOrphanFactRefs = activePrimaryFactRefs.filter((ref) => (owners.get(ref) ?? []).length === 0);
|
|
7252
|
+
const orphanFactRefs = uniqueSorted([
|
|
7253
|
+
...behaviorOrphanFactRefs,
|
|
7254
|
+
...activeTaskOrphanFactRefs.filter((ref) => facts.requiredFactRefs.includes(ref))
|
|
7255
|
+
]);
|
|
7256
|
+
for (const ref of activeTaskOrphanFactRefs) errors.push(`active Behavior fact has no primary Task owner: ${ref}`);
|
|
6403
7257
|
for (const [ref, ownerIds] of owners) if (ownerIds.length > 1) errors.push(`fact has multiple primary owners: ${ref} -> ${ownerIds.join(", ")}`);
|
|
6404
7258
|
for (const [ref, ownerId] of Object.entries(plan.ownership?.primaryOwnerByFactRef ?? {})) {
|
|
6405
7259
|
if (!taskIds.has(ownerId)) errors.push(`ownership ${ref} references unknown task ${ownerId}`);
|
|
6406
7260
|
if (!(owners.get(ref) ?? []).includes(ownerId)) errors.push(`ownership map disagrees with task primaryFactRefs for ${ref}`);
|
|
6407
7261
|
}
|
|
6408
|
-
for (const ref of
|
|
7262
|
+
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
7263
|
for (const [ref, taskIdsForRef] of Object.entries(plan.ownership?.supportingTaskIdsByFactRef ?? {})) {
|
|
6410
7264
|
const expected = [...supporters.get(ref) ?? []].sort();
|
|
6411
7265
|
const actual = [...taskIdsForRef ?? []].sort();
|
|
@@ -6414,6 +7268,125 @@ function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
|
6414
7268
|
for (const [ref, expectedIds] of supporters) {
|
|
6415
7269
|
if (!(ref in (plan.ownership?.supportingTaskIdsByFactRef ?? {}))) errors.push(`supporting ownership map is missing for ${ref}: ${[...expectedIds].sort().join(", ")}`);
|
|
6416
7270
|
}
|
|
7271
|
+
const behaviorMembership = /* @__PURE__ */ new Map();
|
|
7272
|
+
for (const task of plan.tasks ?? []) for (const behaviorId of task.behaviorIds ?? []) behaviorMembership.set(behaviorId, [...behaviorMembership.get(behaviorId) ?? [], task.id]);
|
|
7273
|
+
for (const behavior of plan.behaviorResponsibilities ?? []) {
|
|
7274
|
+
const ownersForBehavior = behaviorMembership.get(behavior.id) ?? [];
|
|
7275
|
+
const expectedMembership = activeBehaviorIds.has(behavior.id) ? 1 : 0;
|
|
7276
|
+
if (ownersForBehavior.length !== expectedMembership) errors.push(`${behavior.id}: must belong to ${expectedMembership} ${expectedPlanningPhase === "resolution" ? "Resolution Task" : "Coding Work Item"}, found ${ownersForBehavior.length}`);
|
|
7277
|
+
const task = plan.tasks.find((item) => item.id === ownersForBehavior[0]);
|
|
7278
|
+
if (!task) continue;
|
|
7279
|
+
for (const ref of behavior.primaryFactRefs) if (!task.primaryFactRefs.includes(ref)) errors.push(`${task.id}: missing primary fact ${ref} from ${behavior.id}`);
|
|
7280
|
+
for (const ref of behavior.closure.entryRefs) if (!task.closure.entryRefs.includes(ref)) errors.push(`${task.id}: missing entry ${ref} from ${behavior.id}`);
|
|
7281
|
+
for (const ref of behavior.closure.interactionRefs) if (!task.closure.interactionRefs.includes(ref)) errors.push(`${task.id}: missing interaction ${ref} from ${behavior.id}`);
|
|
7282
|
+
}
|
|
7283
|
+
const behaviorById = new Map((plan.behaviorResponsibilities ?? []).map((behavior) => [behavior.id, behavior]));
|
|
7284
|
+
for (const task of plan.tasks ?? []) {
|
|
7285
|
+
const members = (task.behaviorIds ?? []).map((id) => behaviorById.get(id)).filter((item) => Boolean(item));
|
|
7286
|
+
const expectedPrimary = uniqueSorted(members.flatMap((item) => item.primaryFactRefs));
|
|
7287
|
+
const expectedSupporting = uniqueSorted(members.flatMap((item) => item.supportingFactRefs).filter((ref) => !expectedPrimary.includes(ref)));
|
|
7288
|
+
if (JSON.stringify(expectedPrimary) !== JSON.stringify([...task.primaryFactRefs ?? []].sort())) errors.push(`${task.id}: primaryFactRefs disagree with member Behaviors`);
|
|
7289
|
+
if (JSON.stringify(expectedSupporting) !== JSON.stringify([...task.supportingFactRefs ?? []].sort())) errors.push(`${task.id}: supportingFactRefs disagree with member Behaviors`);
|
|
7290
|
+
for (const key of ["entryRefs", "surfaceRefs", "controlRefs", "interactionRefs", "unresolvedRefs", "warningRefs"]) {
|
|
7291
|
+
const expected = uniqueSorted(members.flatMap((item) => item.closure[key]));
|
|
7292
|
+
if (JSON.stringify(expected) !== JSON.stringify([...task.closure?.[key] ?? []].sort())) errors.push(`${task.id}: closure.${key} disagrees with member Behaviors`);
|
|
7293
|
+
}
|
|
7294
|
+
const expectedOutcomes = uniqueSorted(members.map((item) => item.acceptance.observableOutcome));
|
|
7295
|
+
const expectedRequired = uniqueSorted(members.flatMap((item) => item.acceptance.requiredFactRefs));
|
|
7296
|
+
const expectedReports = uniqueSorted(members.flatMap((item) => item.acceptance.reportRefs));
|
|
7297
|
+
if (JSON.stringify(expectedOutcomes) !== JSON.stringify([...task.acceptance?.observableOutcomes ?? []].sort())) errors.push(`${task.id}: observableOutcomes disagree with member Behaviors`);
|
|
7298
|
+
if (JSON.stringify(expectedRequired) !== JSON.stringify([...task.acceptance?.requiredFactRefs ?? []].sort())) errors.push(`${task.id}: requiredFactRefs disagree with member Behaviors`);
|
|
7299
|
+
if (JSON.stringify(expectedReports) !== JSON.stringify([...task.acceptance?.reportRefs ?? []].sort())) errors.push(`${task.id}: reportRefs disagree with member Behaviors`);
|
|
7300
|
+
if (members.some((member) => member.type !== task.type)) errors.push(`${task.id}: task type disagrees with a member Behavior`);
|
|
7301
|
+
}
|
|
7302
|
+
for (const ref of facts.requiredFactRefs) {
|
|
7303
|
+
const expectedBehavior = behaviorOwners.get(ref)?.[0];
|
|
7304
|
+
if (!expectedBehavior || behaviorOwners.get(ref)?.length !== 1) errors.push(`required fact must have exactly one Behavior owner: ${ref}`);
|
|
7305
|
+
if (plan.behaviorOwnership?.primaryOwnerByFactRef?.[ref] !== expectedBehavior) errors.push(`behavior ownership map is missing or incorrect for ${ref}`);
|
|
7306
|
+
}
|
|
7307
|
+
for (const [ref, behaviorId] of Object.entries(plan.behaviorOwnership?.primaryOwnerByFactRef ?? {})) {
|
|
7308
|
+
if (!behaviorIds.has(behaviorId)) errors.push(`behavior ownership ${ref} references unknown Behavior ${behaviorId}`);
|
|
7309
|
+
if (!(behaviorOwners.get(ref) ?? []).includes(behaviorId)) errors.push(`behavior ownership map disagrees with Behavior primaryFactRefs for ${ref}`);
|
|
7310
|
+
}
|
|
7311
|
+
for (const [ref, expectedIds] of behaviorSupporters) {
|
|
7312
|
+
const actual = [...plan.behaviorOwnership?.supportingBehaviorIdsByFactRef?.[ref] ?? []].sort();
|
|
7313
|
+
if (JSON.stringify(actual) !== JSON.stringify([...expectedIds].sort())) errors.push(`behavior supporting ownership map is missing or incorrect for ${ref}`);
|
|
7314
|
+
}
|
|
7315
|
+
for (const [ref, actualIds] of Object.entries(plan.behaviorOwnership?.supportingBehaviorIdsByFactRef ?? {})) {
|
|
7316
|
+
const expected = [...behaviorSupporters.get(ref) ?? []].sort();
|
|
7317
|
+
if (JSON.stringify([...actualIds].sort()) !== JSON.stringify(expected)) errors.push(`behavior supporting ownership map disagrees with Behaviors for ${ref}`);
|
|
7318
|
+
}
|
|
7319
|
+
const packageIds = /* @__PURE__ */ new Set();
|
|
7320
|
+
const knownPackageIds = new Set((plan.pageWorkPackages ?? []).map((workPackage) => workPackage.id));
|
|
7321
|
+
const packageMembership = /* @__PURE__ */ new Map();
|
|
7322
|
+
const canonicalBySurface = canonicalPageRefs(facts);
|
|
7323
|
+
for (const workPackage of plan.pageWorkPackages ?? []) {
|
|
7324
|
+
if (!/^package:[a-f0-9]{16}$/.test(workPackage.id)) errors.push(`invalid work package id ${workPackage.id}`);
|
|
7325
|
+
if (packageIds.has(workPackage.id)) errors.push(`duplicate work package id ${workPackage.id}`);
|
|
7326
|
+
else packageIds.add(workPackage.id);
|
|
7327
|
+
for (const dependency of workPackage.dependsOnPackageIds ?? []) {
|
|
7328
|
+
if (!knownPackageIds.has(dependency)) errors.push(`${workPackage.id}: unknown Page Work Package dependency ${dependency}`);
|
|
7329
|
+
if (dependency === workPackage.id) errors.push(`${workPackage.id}: Page Work Package cannot depend on itself`);
|
|
7330
|
+
}
|
|
7331
|
+
for (const taskId2 of workPackage.workItemIds ?? []) {
|
|
7332
|
+
if (!taskIds.has(taskId2)) errors.push(`${workPackage.id}: unknown Coding Work Item ${taskId2}`);
|
|
7333
|
+
packageMembership.set(taskId2, [...packageMembership.get(taskId2) ?? [], workPackage.id]);
|
|
7334
|
+
}
|
|
7335
|
+
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`);
|
|
7336
|
+
if (workPackage.canonicalPageRef !== null && !facts.surfaces.some((surface) => surface.id === workPackage.canonicalPageRef)) errors.push(`${workPackage.id}: unknown canonical page ${workPackage.canonicalPageRef}`);
|
|
7337
|
+
for (const surfaceRef of workPackage.surfaceRefs ?? []) {
|
|
7338
|
+
if (!facts.surfaces.some((surface) => surface.id === surfaceRef)) errors.push(`${workPackage.id}: unknown surface ${surfaceRef}`);
|
|
7339
|
+
if ((canonicalBySurface[surfaceRef] ?? surfaceRef) !== workPackage.canonicalPageRef) errors.push(`${workPackage.id}: ${surfaceRef} does not belong to canonical page ${workPackage.canonicalPageRef}`);
|
|
7340
|
+
}
|
|
7341
|
+
if (workPackage.serialLane) for (let index = 1; index < workPackage.workItemIds.length; index++) {
|
|
7342
|
+
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]}`);
|
|
7343
|
+
}
|
|
7344
|
+
const expectedScopes = [...new Set(plan.tasks.filter((task) => task.workPackageId === workPackage.id).flatMap((task) => task.writeOwnership?.exclusiveScopes ?? []))].sort();
|
|
7345
|
+
if (JSON.stringify([...workPackage.exclusiveScopes ?? []].sort()) !== JSON.stringify(expectedScopes)) errors.push(`${workPackage.id}: exclusiveScopes disagree with member Work Items`);
|
|
7346
|
+
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)));
|
|
7347
|
+
if (JSON.stringify([...workPackage.dependsOnPackageIds ?? []].sort()) !== JSON.stringify(expectedDependencies)) errors.push(`${workPackage.id}: dependsOnPackageIds disagree with cross-package Work Item dependencies`);
|
|
7348
|
+
}
|
|
7349
|
+
const packageCycle = findDependencyCycle(new Map((plan.pageWorkPackages ?? []).map((workPackage) => [workPackage.id, workPackage.dependsOnPackageIds ?? []])));
|
|
7350
|
+
if (packageCycle) errors.push(`Page Work Package dependency cycle: ${packageCycle.join(" -> ")}`);
|
|
7351
|
+
for (const task of plan.tasks ?? []) {
|
|
7352
|
+
const memberships = packageMembership.get(task.id) ?? [];
|
|
7353
|
+
if (task.type === "resolution") {
|
|
7354
|
+
if (memberships.length > 0 || task.workPackageId !== null) errors.push(`${task.id}: Resolution Task must not have Work Package membership`);
|
|
7355
|
+
continue;
|
|
7356
|
+
}
|
|
7357
|
+
if (memberships.length !== 1 || memberships[0] !== task.workPackageId) errors.push(`${task.id}: work package membership is missing or inconsistent`);
|
|
7358
|
+
const workPackage = plan.pageWorkPackages?.find((item) => item.id === task.workPackageId);
|
|
7359
|
+
if (workPackage && workPackage.canonicalPageRef !== task.canonicalPageRef) errors.push(`${task.id}: canonicalPageRef disagrees with its Work Package`);
|
|
7360
|
+
}
|
|
7361
|
+
const conflictEdges = /* @__PURE__ */ new Map();
|
|
7362
|
+
for (const edge of plan.fileConflictEdges ?? []) {
|
|
7363
|
+
if (!taskIds.has(edge.leftTaskId) || !taskIds.has(edge.rightTaskId)) errors.push(`file conflict references an unknown task: ${edge.leftTaskId}, ${edge.rightTaskId}`);
|
|
7364
|
+
if (edge.leftTaskId === edge.rightTaskId) errors.push(`${edge.leftTaskId}: file conflict cannot reference itself`);
|
|
7365
|
+
if (!edge.reasons?.length) errors.push(`${edge.leftTaskId}/${edge.rightTaskId}: file conflict requires a reason`);
|
|
7366
|
+
const key = [edge.leftTaskId, edge.rightTaskId].sort().join(":");
|
|
7367
|
+
if (conflictEdges.has(key)) errors.push(`duplicate file conflict edge: ${key}`);
|
|
7368
|
+
else conflictEdges.set(key, edge);
|
|
7369
|
+
}
|
|
7370
|
+
for (let leftIndex = 0; leftIndex < (plan.tasks ?? []).length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < plan.tasks.length; rightIndex++) {
|
|
7371
|
+
const left = plan.tasks[leftIndex];
|
|
7372
|
+
const right = plan.tasks[rightIndex];
|
|
7373
|
+
const leftOwned = left.writeOwnership?.ownedFiles ?? [];
|
|
7374
|
+
const rightOwned = right.writeOwnership?.ownedFiles ?? [];
|
|
7375
|
+
const leftScopes = left.writeOwnership?.exclusiveScopes ?? [];
|
|
7376
|
+
const rightScopes = right.writeOwnership?.exclusiveScopes ?? [];
|
|
7377
|
+
const overlappingFiles = leftOwned.filter((file) => rightOwned.includes(file));
|
|
7378
|
+
const overlappingScopes = leftScopes.filter((scope) => rightScopes.includes(scope));
|
|
7379
|
+
const hasConflict = overlappingFiles.length > 0 || overlappingScopes.length > 0;
|
|
7380
|
+
const edge = conflictEdges.get([left.id, right.id].sort().join(":"));
|
|
7381
|
+
if (hasConflict && !edge) errors.push(`${left.id} and ${right.id}: overlapping write ownership requires a conflict edge`);
|
|
7382
|
+
if (!hasConflict && edge) errors.push(`${left.id} and ${right.id}: file conflict edge has no overlapping write ownership`);
|
|
7383
|
+
if (edge) {
|
|
7384
|
+
const expectedReasons = [...overlappingFiles.map((file) => `owned-file:${file}`), ...overlappingScopes.map((scope) => `exclusive-scope:${scope}`)].sort();
|
|
7385
|
+
if (JSON.stringify([...edge.reasons].sort()) !== JSON.stringify(expectedReasons)) errors.push(`${left.id} and ${right.id}: file conflict reasons disagree with write ownership`);
|
|
7386
|
+
}
|
|
7387
|
+
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`);
|
|
7388
|
+
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`);
|
|
7389
|
+
}
|
|
6417
7390
|
const knownRefs = /* @__PURE__ */ new Set([...facts.requiredFactRefs, ...facts.issues.map((issue) => issue.id)]);
|
|
6418
7391
|
const entryRefs = new Set(facts.entries.map((entry) => entry.id));
|
|
6419
7392
|
const surfaceRefs = new Set(facts.surfaces.map((surface) => surface.id));
|
|
@@ -6447,16 +7420,16 @@ function validateTaskPlan(plan, facts, androidFingerprint) {
|
|
|
6447
7420
|
}
|
|
6448
7421
|
|
|
6449
7422
|
// src/planning/runtime.ts
|
|
6450
|
-
function
|
|
7423
|
+
function readJson7(file) {
|
|
6451
7424
|
try {
|
|
6452
|
-
return JSON.parse(
|
|
7425
|
+
return JSON.parse(fs28.readFileSync(file, "utf8"));
|
|
6453
7426
|
} catch {
|
|
6454
7427
|
return null;
|
|
6455
7428
|
}
|
|
6456
7429
|
}
|
|
6457
7430
|
function reportItems(projectRoot) {
|
|
6458
7431
|
const root = path34.join(migrationDirectory(projectRoot), "report");
|
|
6459
|
-
const index =
|
|
7432
|
+
const index = readJson7(path34.join(root, "index.json"));
|
|
6460
7433
|
const out = /* @__PURE__ */ new Map();
|
|
6461
7434
|
if (!index) return out;
|
|
6462
7435
|
const files = [
|
|
@@ -6466,7 +7439,7 @@ function reportItems(projectRoot) {
|
|
|
6466
7439
|
index.discoveriesFile
|
|
6467
7440
|
].filter((value) => typeof value === "string");
|
|
6468
7441
|
for (const relative2 of files) {
|
|
6469
|
-
const document =
|
|
7442
|
+
const document = readJson7(path34.join(root, relative2));
|
|
6470
7443
|
if (!document) continue;
|
|
6471
7444
|
const values = [document.page, ...document.controls ?? [], ...document.entries ?? [], ...document.interactions ?? [], ...document.items ?? []].filter(Boolean);
|
|
6472
7445
|
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 +7450,7 @@ function managedReceipt(projectRoot, reference) {
|
|
|
6477
7450
|
const managedRoot = path34.resolve(migrationDirectory(projectRoot), "evidence", "managed");
|
|
6478
7451
|
const absolute = reference.replaceAll("\\", "/").startsWith(".migration/") ? path34.resolve(migrationWorkspaceRoot(projectRoot), reference) : path34.resolve(projectRoot, reference);
|
|
6479
7452
|
if (absolute !== managedRoot && !absolute.startsWith(`${managedRoot}${path34.sep}`)) return null;
|
|
6480
|
-
return
|
|
7453
|
+
return readJson7(absolute);
|
|
6481
7454
|
}
|
|
6482
7455
|
function freshTaskEvidence(projectRoot, reference, factsSha256, harmonyFingerprint) {
|
|
6483
7456
|
const receipt = managedReceipt(projectRoot, reference);
|
|
@@ -6503,7 +7476,24 @@ function stateReasons(task, state, dependencyStates) {
|
|
|
6503
7476
|
})(),
|
|
6504
7477
|
task.estimate.downstreamUnlockCount > 0 ? `\u5B8C\u6210\u540E\u53EF\u89E3\u9664 ${task.estimate.downstreamUnlockCount} \u4E2A\u4E0B\u6E38\u4EFB\u52A1` : null
|
|
6505
7478
|
]);
|
|
6506
|
-
if (state === "blocked")
|
|
7479
|
+
if (state === "blocked") {
|
|
7480
|
+
const gate = task.readyGate ?? {
|
|
7481
|
+
semanticsResolved: false,
|
|
7482
|
+
dependencyInterfacesStable: false,
|
|
7483
|
+
writeOwnershipResolved: false,
|
|
7484
|
+
contextWithinBudget: false,
|
|
7485
|
+
acceptanceExecutable: false
|
|
7486
|
+
};
|
|
7487
|
+
return uniqueSorted([
|
|
7488
|
+
dependencyStates.length ? "\u4F9D\u8D56\u4EFB\u52A1\u5C1A\u672A\u53D6\u5F97\u65B0\u9C9C\u7684\u5B9A\u5411\u9A8C\u8BC1\u8BC1\u636E" : null,
|
|
7489
|
+
!gate.semanticsResolved ? "\u4ECD\u6709\u672A\u88C1\u51B3\u8BED\u4E49\u95EE\u9898" : null,
|
|
7490
|
+
!gate.dependencyInterfacesStable ? "\u4E0A\u6E38 Module Interface \u5C1A\u672A\u7A33\u5B9A" : null,
|
|
7491
|
+
!gate.writeOwnershipResolved ? "\u5199\u5165\u6587\u4EF6\u6216\u72EC\u5360\u8303\u56F4\u5C1A\u672A\u5206\u914D" : null,
|
|
7492
|
+
!gate.contextWithinBudget ? "\u5185\u90E8 Work Item \u95ED\u5305\u8D85\u51FA\u5F53\u524D\u6267\u884C\u9884\u7B97" : null,
|
|
7493
|
+
!gate.acceptanceExecutable ? "\u9A8C\u6536\u6761\u4EF6\u5F53\u524D\u4E0D\u53EF\u6267\u884C" : null,
|
|
7494
|
+
!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
|
|
7495
|
+
]);
|
|
7496
|
+
}
|
|
6507
7497
|
if (state === "implemented") return ["\u5B9E\u73B0\u6620\u5C04\u5DF2\u5B58\u5728\uFF0C\u4F46\u5B9A\u5411\u8BBE\u5907\u9A8C\u8BC1\u5C1A\u672A\u5B8C\u6210"];
|
|
6508
7498
|
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
7499
|
if (state === "locally-verified") return ["\u5F53\u524D\u4EFB\u52A1\u5DF2\u6709\u65B0\u9C9C\u5B9A\u5411\u8BBE\u5907\u8BC1\u636E"];
|
|
@@ -6512,25 +7502,72 @@ function stateReasons(task, state, dependencyStates) {
|
|
|
6512
7502
|
}
|
|
6513
7503
|
function rankReady(tasks, states) {
|
|
6514
7504
|
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
|
-
}
|
|
7505
|
+
if (task.type === "resolution") return 0;
|
|
6519
7506
|
const order = { bootstrap: 1, journey: 2, capability: 3, assembly: 4, regression: 6 };
|
|
6520
7507
|
return order[task.type] ?? 6;
|
|
6521
7508
|
};
|
|
6522
7509
|
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
7510
|
}
|
|
7511
|
+
function completedState(state) {
|
|
7512
|
+
return ["locally-verified", "globally-verified"].includes(state);
|
|
7513
|
+
}
|
|
7514
|
+
function aggregatePackageOwnState(states) {
|
|
7515
|
+
if (states.length > 0 && states.every((state) => state === "globally-verified")) return "globally-verified";
|
|
7516
|
+
if (states.length > 0 && states.every(completedState)) return "locally-verified";
|
|
7517
|
+
if (states.some((state) => state === "blocked")) return "blocked";
|
|
7518
|
+
if (states.some((state) => state === "stale")) return "stale";
|
|
7519
|
+
if (states.length > 0 && states.every((state) => ["implemented", "locally-verified", "globally-verified"].includes(state))) return "implemented";
|
|
7520
|
+
return "pending";
|
|
7521
|
+
}
|
|
7522
|
+
function packageStateReasons(workPackage, state, dependencyStates, ownStates) {
|
|
7523
|
+
if (state === "ready") return [
|
|
7524
|
+
`\u9875\u9762\u5305\u5C06\u5728\u4E00\u6B21\u5B9E\u73B0 epoch \u5185\u6309\u51BB\u7ED3\u987A\u5E8F\u5B8C\u6210 ${workPackage.workItemIds.length} \u4E2A Work Item`,
|
|
7525
|
+
"\u8DE8\u9875\u9762\u4F9D\u8D56\u5DF2\u6EE1\u8DB3\uFF0C\u53EF\u52A0\u8F7D\u9875\u9762\u7EA7\u6A21\u578B\u8F93\u5165"
|
|
7526
|
+
];
|
|
7527
|
+
if (state === "blocked") return uniqueSorted([
|
|
7528
|
+
dependencyStates.some((dependency) => !completedState(dependency)) ? "\u524D\u7F6E Page Work Package \u5C1A\u672A\u53D6\u5F97\u65B0\u9C9C\u7684\u5B9A\u5411\u9A8C\u8BC1\u8BC1\u636E" : null,
|
|
7529
|
+
ownStates.some((own) => own === "blocked") ? "\u9875\u9762\u5305\u5185\u5B58\u5728\u88AB\u62A5\u544A\u6807\u8BB0\u4E3A blocked \u7684\u884C\u4E3A" : null,
|
|
7530
|
+
!dependencyStates.length && !ownStates.some((own) => own === "blocked") ? "\u9875\u9762\u5305\u7684\u9759\u6001 Ready Gate \u672A\u5168\u90E8\u901A\u8FC7" : null
|
|
7531
|
+
]);
|
|
7532
|
+
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"];
|
|
7533
|
+
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"];
|
|
7534
|
+
if (state === "locally-verified") return ["\u9875\u9762\u5305\u5185\u5168\u90E8\u884C\u4E3A\u5747\u6709\u65B0\u9C9C\u5B9A\u5411\u8BBE\u5907\u8BC1\u636E"];
|
|
7535
|
+
if (state === "globally-verified") return ["\u9875\u9762\u5305\u5185\u5168\u90E8\u884C\u4E3A\u53CA\u5168\u91CF\u56DE\u5F52\u8BC1\u636E\u5747\u65B0\u9C9C"];
|
|
7536
|
+
return [];
|
|
7537
|
+
}
|
|
7538
|
+
function packageDownstreamCounts(packages) {
|
|
7539
|
+
const children = /* @__PURE__ */ new Map();
|
|
7540
|
+
for (const workPackage of packages) for (const dependency of workPackage.dependsOnPackageIds) children.set(dependency, [...children.get(dependency) ?? [], workPackage.id]);
|
|
7541
|
+
const result = /* @__PURE__ */ new Map();
|
|
7542
|
+
for (const workPackage of packages) {
|
|
7543
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7544
|
+
const queue = [...children.get(workPackage.id) ?? []];
|
|
7545
|
+
while (queue.length) {
|
|
7546
|
+
const next = queue.shift();
|
|
7547
|
+
if (seen.has(next)) continue;
|
|
7548
|
+
seen.add(next);
|
|
7549
|
+
queue.push(...children.get(next) ?? []);
|
|
7550
|
+
}
|
|
7551
|
+
result.set(workPackage.id, seen.size);
|
|
7552
|
+
}
|
|
7553
|
+
return result;
|
|
7554
|
+
}
|
|
6524
7555
|
function emptyTaskPlanSnapshot(error) {
|
|
6525
7556
|
return {
|
|
6526
7557
|
present: false,
|
|
6527
7558
|
valid: false,
|
|
6528
7559
|
fresh: false,
|
|
6529
7560
|
taskPlanSha256: null,
|
|
7561
|
+
planningPhase: null,
|
|
6530
7562
|
totalTasks: 0,
|
|
7563
|
+
resolutionTasks: 0,
|
|
7564
|
+
totalTargets: 0,
|
|
7565
|
+
readyTargets: 0,
|
|
7566
|
+
completedTargets: 0,
|
|
6531
7567
|
readyTasks: 0,
|
|
6532
7568
|
blockedTasks: 0,
|
|
6533
7569
|
completedTasks: 0,
|
|
7570
|
+
recommendedTargetId: null,
|
|
6534
7571
|
recommendedTaskId: null,
|
|
6535
7572
|
alternatives: [],
|
|
6536
7573
|
reasons: {},
|
|
@@ -6539,7 +7576,8 @@ function emptyTaskPlanSnapshot(error) {
|
|
|
6539
7576
|
orphanFactRefs: [],
|
|
6540
7577
|
errors: error ? [error] : [],
|
|
6541
7578
|
warnings: [],
|
|
6542
|
-
tasks: []
|
|
7579
|
+
tasks: [],
|
|
7580
|
+
targets: []
|
|
6543
7581
|
};
|
|
6544
7582
|
}
|
|
6545
7583
|
function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
@@ -6548,6 +7586,7 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6548
7586
|
if (!plan) return emptyTaskPlanSnapshot();
|
|
6549
7587
|
const validation = validateTaskPlan(plan, facts, source.androidFingerprint);
|
|
6550
7588
|
const reports = reportItems(projectRoot);
|
|
7589
|
+
const ownStates = new Map(plan.tasks.map((task) => [task.id, initialState(task, reports, projectRoot, plan, source)]));
|
|
6551
7590
|
const states = /* @__PURE__ */ new Map();
|
|
6552
7591
|
const resolving = /* @__PURE__ */ new Set();
|
|
6553
7592
|
const byId = new Map(plan.tasks.map((task) => [task.id, task]));
|
|
@@ -6557,12 +7596,13 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6557
7596
|
if (!validation.fresh) return "stale";
|
|
6558
7597
|
if (resolving.has(task.id)) return "blocked";
|
|
6559
7598
|
resolving.add(task.id);
|
|
6560
|
-
const own =
|
|
7599
|
+
const own = ownStates.get(task.id);
|
|
6561
7600
|
let state = own;
|
|
6562
7601
|
if (own === "pending") {
|
|
6563
7602
|
const dependencyStates = task.dependsOnTaskIds.map((id) => byId.get(id)).filter((item) => Boolean(item)).map(resolveState);
|
|
6564
7603
|
const satisfied = dependencyStates.every((value) => ["locally-verified", "globally-verified"].includes(value));
|
|
6565
|
-
|
|
7604
|
+
const readyGatePassed = Object.values(task.readyGate).every(Boolean);
|
|
7605
|
+
state = satisfied && readyGatePassed ? "ready" : "blocked";
|
|
6566
7606
|
}
|
|
6567
7607
|
resolving.delete(task.id);
|
|
6568
7608
|
states.set(task.id, state);
|
|
@@ -6575,8 +7615,59 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6575
7615
|
const dependencyStates = task.dependsOnTaskIds.map((id) => states.get(id)).filter(Boolean);
|
|
6576
7616
|
return { taskId: task.id, state, reasons: stateReasons(task, state, dependencyStates) };
|
|
6577
7617
|
});
|
|
6578
|
-
|
|
6579
|
-
|
|
7618
|
+
let targets;
|
|
7619
|
+
let orderedReadyTargetIds;
|
|
7620
|
+
if (plan.planningPhase === "resolution") {
|
|
7621
|
+
targets = runtimeItems.map((item) => ({ targetId: item.taskId, kind: "resolution-task", state: item.state, reasons: item.reasons, workItemIds: [item.taskId] }));
|
|
7622
|
+
orderedReadyTargetIds = ready.map((task) => task.id);
|
|
7623
|
+
} else {
|
|
7624
|
+
const packageById = new Map(plan.pageWorkPackages.map((workPackage) => [workPackage.id, workPackage]));
|
|
7625
|
+
const packageStates = /* @__PURE__ */ new Map();
|
|
7626
|
+
const packageResolving = /* @__PURE__ */ new Set();
|
|
7627
|
+
const resolvePackageState = (workPackage) => {
|
|
7628
|
+
if (packageStates.has(workPackage.id)) return packageStates.get(workPackage.id);
|
|
7629
|
+
if (!validation.valid) return "blocked";
|
|
7630
|
+
if (!validation.fresh) return "stale";
|
|
7631
|
+
if (packageResolving.has(workPackage.id)) return "blocked";
|
|
7632
|
+
packageResolving.add(workPackage.id);
|
|
7633
|
+
const memberTasks = workPackage.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
7634
|
+
const memberOwnStates = memberTasks.map((task) => ownStates.get(task.id));
|
|
7635
|
+
const own = aggregatePackageOwnState(memberOwnStates);
|
|
7636
|
+
let state = own;
|
|
7637
|
+
if (own === "pending") {
|
|
7638
|
+
const dependencyStates = workPackage.dependsOnPackageIds.map((id) => packageById.get(id)).filter((item) => Boolean(item)).map(resolvePackageState);
|
|
7639
|
+
const dependenciesSatisfied = dependencyStates.every(completedState);
|
|
7640
|
+
const readyGatePassed = memberTasks.every((task) => Object.values(task.readyGate).every(Boolean));
|
|
7641
|
+
state = dependenciesSatisfied && readyGatePassed ? "ready" : "blocked";
|
|
7642
|
+
}
|
|
7643
|
+
packageResolving.delete(workPackage.id);
|
|
7644
|
+
packageStates.set(workPackage.id, state);
|
|
7645
|
+
return state;
|
|
7646
|
+
};
|
|
7647
|
+
for (const workPackage of plan.pageWorkPackages) resolvePackageState(workPackage);
|
|
7648
|
+
targets = plan.pageWorkPackages.map((workPackage) => {
|
|
7649
|
+
const state = packageStates.get(workPackage.id);
|
|
7650
|
+
const dependencyStates = workPackage.dependsOnPackageIds.map((id) => packageStates.get(id)).filter(Boolean);
|
|
7651
|
+
const memberOwnStates = workPackage.workItemIds.map((id) => ownStates.get(id)).filter(Boolean);
|
|
7652
|
+
return { targetId: workPackage.id, kind: "page-work-package", state, reasons: packageStateReasons(workPackage, state, dependencyStates, memberOwnStates), workItemIds: workPackage.workItemIds };
|
|
7653
|
+
});
|
|
7654
|
+
const downstream = packageDownstreamCounts(plan.pageWorkPackages);
|
|
7655
|
+
const targetById = new Map(targets.map((target) => [target.targetId, target]));
|
|
7656
|
+
orderedReadyTargetIds = plan.pageWorkPackages.filter((workPackage) => targetById.get(workPackage.id)?.state === "ready").sort((left, right) => {
|
|
7657
|
+
const leftTasks = left.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
7658
|
+
const rightTasks = right.workItemIds.map((id) => byId.get(id)).filter((task) => Boolean(task));
|
|
7659
|
+
const leftBootstrap = leftTasks.some((task) => task.type === "bootstrap") ? 0 : 1;
|
|
7660
|
+
const rightBootstrap = rightTasks.some((task) => task.type === "bootstrap") ? 0 : 1;
|
|
7661
|
+
const leftTokens = leftTasks.reduce((total, task) => total + task.estimate.estimatedFactTokens, 0);
|
|
7662
|
+
const rightTokens = rightTasks.reduce((total, task) => total + task.estimate.estimatedFactTokens, 0);
|
|
7663
|
+
return leftBootstrap - rightBootstrap || (downstream.get(right.id) ?? 0) - (downstream.get(left.id) ?? 0) || leftTokens - rightTokens || left.id.localeCompare(right.id);
|
|
7664
|
+
}).map((workPackage) => workPackage.id);
|
|
7665
|
+
}
|
|
7666
|
+
const reasons = Object.fromEntries([
|
|
7667
|
+
...runtimeItems.filter((item) => item.reasons.length).map((item) => [item.taskId, item.reasons]),
|
|
7668
|
+
...targets.filter((item) => item.reasons.length).map((item) => [item.targetId, item.reasons])
|
|
7669
|
+
]);
|
|
7670
|
+
const ownerCount = facts.requiredFactRefs.filter((ref) => plan.behaviorOwnership.primaryOwnerByFactRef[ref]).length;
|
|
6580
7671
|
const behaviorTasks = plan.tasks.filter((task) => task.type !== "resolution");
|
|
6581
7672
|
const closedTasks = behaviorTasks.filter((task) => !task.closure.requiresSourceReview && (task.closure.interactionRefs.length > 0 || task.type === "bootstrap" && task.closure.surfaceRefs.length > 0)).length;
|
|
6582
7673
|
return {
|
|
@@ -6584,19 +7675,26 @@ function inspectCompiledTaskPlan(projectRootArgument, facts, source) {
|
|
|
6584
7675
|
valid: validation.valid,
|
|
6585
7676
|
fresh: validation.fresh,
|
|
6586
7677
|
taskPlanSha256: validation.taskPlanSha256,
|
|
7678
|
+
planningPhase: plan.planningPhase,
|
|
6587
7679
|
totalTasks: plan.tasks.length,
|
|
7680
|
+
resolutionTasks: plan.tasks.filter((task) => task.type === "resolution").length,
|
|
7681
|
+
totalTargets: targets.length,
|
|
7682
|
+
readyTargets: orderedReadyTargetIds.length,
|
|
7683
|
+
completedTargets: targets.filter((item) => completedState(item.state)).length,
|
|
6588
7684
|
readyTasks: ready.length,
|
|
6589
7685
|
blockedTasks: runtimeItems.filter((item) => ["blocked", "stale"].includes(item.state)).length,
|
|
6590
|
-
completedTasks: runtimeItems.filter((item) =>
|
|
6591
|
-
|
|
6592
|
-
|
|
7686
|
+
completedTasks: runtimeItems.filter((item) => completedState(item.state)).length,
|
|
7687
|
+
recommendedTargetId: orderedReadyTargetIds[0] ?? null,
|
|
7688
|
+
recommendedTaskId: plan.planningPhase === "resolution" ? orderedReadyTargetIds[0] ?? null : null,
|
|
7689
|
+
alternatives: orderedReadyTargetIds.slice(1, 4),
|
|
6593
7690
|
reasons,
|
|
6594
7691
|
ownershipCoverage: facts.requiredFactRefs.length ? ownerCount / facts.requiredFactRefs.length : 1,
|
|
6595
|
-
closureCoverage: behaviorTasks.length ? closedTasks / behaviorTasks.length : 1,
|
|
7692
|
+
closureCoverage: plan.planningPhase === "resolution" ? 0 : behaviorTasks.length ? closedTasks / behaviorTasks.length : 1,
|
|
6596
7693
|
orphanFactRefs: validation.orphanFactRefs,
|
|
6597
7694
|
errors: validation.errors,
|
|
6598
7695
|
warnings: validation.warnings,
|
|
6599
|
-
tasks: runtimeItems
|
|
7696
|
+
tasks: runtimeItems,
|
|
7697
|
+
targets
|
|
6600
7698
|
};
|
|
6601
7699
|
}
|
|
6602
7700
|
|
|
@@ -6605,10 +7703,10 @@ function createTaskPlanningModule(factsSource = v2PlanningFactsSource) {
|
|
|
6605
7703
|
return {
|
|
6606
7704
|
compile(projectRoot, options) {
|
|
6607
7705
|
const facts = factsSource.load(projectRoot);
|
|
6608
|
-
const plan = compileFromPlanningFacts(facts, options.androidFingerprint);
|
|
7706
|
+
const plan = compileFromPlanningFacts(facts, options.androidFingerprint, loadExecutionPlanningContext(projectRoot));
|
|
6609
7707
|
const validation = validateTaskPlan(plan, facts, options.androidFingerprint);
|
|
6610
7708
|
if (!validation.valid || !validation.fresh) throw new Error(`Task plan compilation failed: ${[...validation.errors, ...validation.warnings].join("; ")}`);
|
|
6611
|
-
writeTaskPlan(projectRoot, plan);
|
|
7709
|
+
writeTaskPlan(projectRoot, plan, facts);
|
|
6612
7710
|
return plan;
|
|
6613
7711
|
},
|
|
6614
7712
|
inspect(projectRoot, source) {
|
|
@@ -6620,19 +7718,19 @@ function createTaskPlanningModule(factsSource = v2PlanningFactsSource) {
|
|
|
6620
7718
|
return { ...emptyTaskPlanSnapshot(error instanceof Error ? error.message : String(error)), present: hasTaskPlan(projectRoot) };
|
|
6621
7719
|
}
|
|
6622
7720
|
},
|
|
6623
|
-
materialize(projectRoot,
|
|
7721
|
+
materialize(projectRoot, target, request) {
|
|
6624
7722
|
const facts = factsSource.load(projectRoot);
|
|
6625
7723
|
const plan = readTaskPlan(projectRoot);
|
|
6626
7724
|
if (!plan) throw new Error("Task plan is not present; run droid2hmos task plan first");
|
|
6627
7725
|
const validation = validateTaskPlan(plan, facts, request.source.androidFingerprint);
|
|
6628
7726
|
if (!validation.valid || !validation.fresh) throw new Error(`Task plan is invalid or stale: ${[...validation.errors, ...validation.warnings].join("; ")}`);
|
|
6629
|
-
let
|
|
6630
|
-
if (
|
|
7727
|
+
let targetId = target;
|
|
7728
|
+
if (target === "next") {
|
|
6631
7729
|
const snapshot = inspectCompiledTaskPlan(projectRoot, facts, request.source);
|
|
6632
|
-
if (!snapshot.
|
|
6633
|
-
|
|
7730
|
+
if (!snapshot.recommendedTargetId) throw new Error("No ready execution target is available");
|
|
7731
|
+
targetId = snapshot.recommendedTargetId;
|
|
6634
7732
|
}
|
|
6635
|
-
return materializeEnvelope(plan, facts,
|
|
7733
|
+
return materializeEnvelope(plan, facts, targetId, request);
|
|
6636
7734
|
}
|
|
6637
7735
|
};
|
|
6638
7736
|
}
|
|
@@ -6648,9 +7746,7 @@ var actions = [
|
|
|
6648
7746
|
"init",
|
|
6649
7747
|
"analyze:android",
|
|
6650
7748
|
"analyze:harmony",
|
|
6651
|
-
"build:android",
|
|
6652
7749
|
"build:harmony",
|
|
6653
|
-
"test:android",
|
|
6654
7750
|
"test:harmony:targeted",
|
|
6655
7751
|
"test:harmony:all",
|
|
6656
7752
|
"report:init",
|
|
@@ -6672,9 +7768,9 @@ var actions = [
|
|
|
6672
7768
|
"complete"
|
|
6673
7769
|
];
|
|
6674
7770
|
var ignoredDirectories = /* @__PURE__ */ new Set([".git", ".gradle", ".hvigor", ".idea", ".migration", ".codegraph-appgraph", "build", "node_modules", "oh_modules"]);
|
|
6675
|
-
function
|
|
7771
|
+
function readJson8(file) {
|
|
6676
7772
|
try {
|
|
6677
|
-
return JSON.parse(
|
|
7773
|
+
return JSON.parse(fs29.readFileSync(file, "utf8"));
|
|
6678
7774
|
} catch {
|
|
6679
7775
|
return null;
|
|
6680
7776
|
}
|
|
@@ -6691,11 +7787,11 @@ function sha2563(value) {
|
|
|
6691
7787
|
return createHash8("sha256").update(typeof value === "string" ? value : stableJson2(value)).digest("hex");
|
|
6692
7788
|
}
|
|
6693
7789
|
function hashDirectory(root, excludedRoots = []) {
|
|
6694
|
-
if (!root || !
|
|
7790
|
+
if (!root || !fs29.existsSync(root) || !fs29.statSync(root).isDirectory()) return null;
|
|
6695
7791
|
const excluded = excludedRoots.map((value) => path35.resolve(value));
|
|
6696
7792
|
const hash2 = createHash8("sha256");
|
|
6697
7793
|
const visit = (directory) => {
|
|
6698
|
-
const entries =
|
|
7794
|
+
const entries = fs29.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
6699
7795
|
for (const entry of entries) {
|
|
6700
7796
|
const absolute = path35.join(directory, entry.name);
|
|
6701
7797
|
if (entry.isDirectory() && (ignoredDirectories.has(entry.name) || excluded.some((value) => absolute === value || absolute.startsWith(`${value}${path35.sep}`)))) continue;
|
|
@@ -6705,10 +7801,10 @@ function hashDirectory(root, excludedRoots = []) {
|
|
|
6705
7801
|
hash2.update(`d:${relative2}\0`);
|
|
6706
7802
|
visit(absolute);
|
|
6707
7803
|
} else if (entry.isSymbolicLink()) {
|
|
6708
|
-
hash2.update(`l:${relative2}\0${
|
|
7804
|
+
hash2.update(`l:${relative2}\0${fs29.readlinkSync(absolute)}\0`);
|
|
6709
7805
|
} else if (entry.isFile()) {
|
|
6710
7806
|
hash2.update(`f:${relative2}\0`);
|
|
6711
|
-
hash2.update(
|
|
7807
|
+
hash2.update(fs29.readFileSync(absolute));
|
|
6712
7808
|
hash2.update("\0");
|
|
6713
7809
|
}
|
|
6714
7810
|
}
|
|
@@ -6732,21 +7828,21 @@ function androidProjectFor(projectRoot, config) {
|
|
|
6732
7828
|
return path35.isAbsolute(configured) ? configured : path35.resolve(projectRoot, configured);
|
|
6733
7829
|
}
|
|
6734
7830
|
function isAndroidProject(root) {
|
|
6735
|
-
if (!
|
|
6736
|
-
const hasBuildRoot = ["settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts", "gradlew", "gradlew.bat"].some((name) =>
|
|
7831
|
+
if (!fs29.existsSync(root) || !fs29.statSync(root).isDirectory()) return false;
|
|
7832
|
+
const hasBuildRoot = ["settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts", "gradlew", "gradlew.bat"].some((name) => fs29.existsSync(path35.join(root, name)));
|
|
6737
7833
|
if (!hasBuildRoot) return false;
|
|
6738
|
-
if (
|
|
6739
|
-
return
|
|
7834
|
+
if (fs29.existsSync(path35.join(root, "app", "src", "main", "AndroidManifest.xml"))) return true;
|
|
7835
|
+
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
7836
|
}
|
|
6741
7837
|
function androidProjectCandidates(root) {
|
|
6742
|
-
if (!
|
|
6743
|
-
return
|
|
7838
|
+
if (!fs29.existsSync(root) || !fs29.statSync(root).isDirectory()) return [];
|
|
7839
|
+
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
7840
|
}
|
|
6745
7841
|
function evidenceRoot(projectRoot) {
|
|
6746
7842
|
return path35.join(migrationDirectory(projectRoot), "evidence");
|
|
6747
7843
|
}
|
|
6748
7844
|
function evidenceIndex(projectRoot) {
|
|
6749
|
-
return
|
|
7845
|
+
return readJson8(path35.join(evidenceRoot(projectRoot), "index.json")) ?? { evidenceIndexVersion: "1.0.0", latestByAction: {} };
|
|
6750
7846
|
}
|
|
6751
7847
|
function projectRelative(projectRoot, absolute) {
|
|
6752
7848
|
return path35.relative(projectRoot, absolute).replaceAll("\\", "/");
|
|
@@ -6759,7 +7855,7 @@ function managedReceipt2(projectRoot, reference) {
|
|
|
6759
7855
|
const root = path35.resolve(evidenceRoot(projectRoot), "managed");
|
|
6760
7856
|
const absolute = path35.resolve(migrationWorkspaceRoot(projectRoot), reference);
|
|
6761
7857
|
if (absolute !== root && !absolute.startsWith(`${root}${path35.sep}`)) return null;
|
|
6762
|
-
return
|
|
7858
|
+
return readJson8(absolute);
|
|
6763
7859
|
}
|
|
6764
7860
|
function reportClaims(projectRoot, reportIndex) {
|
|
6765
7861
|
if (!reportIndex) return [];
|
|
@@ -6775,16 +7871,16 @@ function reportClaims(projectRoot, reportIndex) {
|
|
|
6775
7871
|
});
|
|
6776
7872
|
};
|
|
6777
7873
|
for (const descriptor of reportIndex.pages ?? []) {
|
|
6778
|
-
const shard =
|
|
7874
|
+
const shard = readJson8(path35.join(reportRoot, descriptor.file));
|
|
6779
7875
|
if (!shard) continue;
|
|
6780
7876
|
addItems([shard.page], descriptor.file);
|
|
6781
7877
|
addItems(shard.controls, descriptor.file);
|
|
6782
7878
|
addItems(shard.entries, descriptor.file);
|
|
6783
7879
|
}
|
|
6784
|
-
if (reportIndex.unassignedEntriesFile) addItems(
|
|
6785
|
-
if (reportIndex.interactionsFile) addItems(
|
|
6786
|
-
if (reportIndex.discoveriesFile) addItems(
|
|
6787
|
-
const acceptance = reportIndex.finalAcceptanceFile ?
|
|
7880
|
+
if (reportIndex.unassignedEntriesFile) addItems(readJson8(path35.join(reportRoot, reportIndex.unassignedEntriesFile))?.entries, reportIndex.unassignedEntriesFile);
|
|
7881
|
+
if (reportIndex.interactionsFile) addItems(readJson8(path35.join(reportRoot, reportIndex.interactionsFile))?.interactions, reportIndex.interactionsFile);
|
|
7882
|
+
if (reportIndex.discoveriesFile) addItems(readJson8(path35.join(reportRoot, reportIndex.discoveriesFile))?.items, "discoveries", true);
|
|
7883
|
+
const acceptance = reportIndex.finalAcceptanceFile ? readJson8(path35.join(reportRoot, reportIndex.finalAcceptanceFile)) : null;
|
|
6788
7884
|
for (const [index, test] of (acceptance?.tests ?? []).entries()) claims.push({
|
|
6789
7885
|
ref: test?.ref ?? `final-acceptance:${test?.name ?? index + 1}`,
|
|
6790
7886
|
source: reportIndex.finalAcceptanceFile,
|
|
@@ -6799,8 +7895,8 @@ function receiptArtifactsAreIntact(projectRoot, receipt) {
|
|
|
6799
7895
|
return receipt.artifacts.every((artifact) => {
|
|
6800
7896
|
if (!artifact || typeof artifact.path !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256) || !Number.isInteger(artifact.size)) return false;
|
|
6801
7897
|
const absolute = path35.resolve(migrationWorkspaceRoot(projectRoot), artifact.path);
|
|
6802
|
-
if (!absolute.startsWith(`${blobsRoot}${path35.sep}`) || !
|
|
6803
|
-
const content =
|
|
7898
|
+
if (!absolute.startsWith(`${blobsRoot}${path35.sep}`) || !fs29.existsSync(absolute) || !fs29.statSync(absolute).isFile()) return false;
|
|
7899
|
+
const content = fs29.readFileSync(absolute);
|
|
6804
7900
|
return content.byteLength === artifact.size && createHash8("sha256").update(content).digest("hex") === artifact.sha256;
|
|
6805
7901
|
});
|
|
6806
7902
|
}
|
|
@@ -6862,7 +7958,7 @@ function actionRequirements(action, snapshot) {
|
|
|
6862
7958
|
if (action === "config:init" && snapshot.workspace.configPresent) missing.push(unmet("config-present", ".migration/config.json \u5DF2\u5B58\u5728"));
|
|
6863
7959
|
if (action === "config:validate") requireConfig();
|
|
6864
7960
|
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:
|
|
7961
|
+
if (["analyze:android", "analyze:harmony", "build:harmony", "test:harmony:targeted", "test:harmony:all", "evidence:flow"].includes(action)) requireConfig();
|
|
6866
7962
|
if (["analyze:harmony", "build:harmony", "test:harmony:targeted", "test:harmony:all"].includes(action)) requireHarmony();
|
|
6867
7963
|
if (action === "report:init") {
|
|
6868
7964
|
requireFacts();
|
|
@@ -6874,6 +7970,10 @@ function actionRequirements(action, snapshot) {
|
|
|
6874
7970
|
requireFacts();
|
|
6875
7971
|
requireReport();
|
|
6876
7972
|
requireFreshAnalysis();
|
|
7973
|
+
if (snapshot.planning.present && snapshot.planning.valid && snapshot.planning.fresh) {
|
|
7974
|
+
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";
|
|
7975
|
+
missing.push(unmet("task-plan-already-fresh", message));
|
|
7976
|
+
}
|
|
6877
7977
|
}
|
|
6878
7978
|
if (action === "task:validate" && !snapshot.planning.present) missing.push(unmet("task-plan-missing", "Task Plan \u5C1A\u672A\u751F\u6210"));
|
|
6879
7979
|
if (action === "checkpoint:create") {
|
|
@@ -6897,14 +7997,14 @@ function derivePhase(snapshot) {
|
|
|
6897
7997
|
function inspectMigration(project = ".") {
|
|
6898
7998
|
const projectRoot = path35.resolve(project);
|
|
6899
7999
|
const migrationRoot = migrationDirectory(projectRoot);
|
|
6900
|
-
const config =
|
|
8000
|
+
const config = readJson8(path35.join(migrationRoot, "config.json"));
|
|
6901
8001
|
const harmonyProject = harmonyProjectFor(projectRoot, config);
|
|
6902
8002
|
const androidProject = androidProjectFor(projectRoot, config);
|
|
6903
8003
|
const projectPresent = isAndroidProject(androidProject);
|
|
6904
8004
|
const projectCandidates = projectPresent ? [] : androidProjectCandidates(projectRoot);
|
|
6905
|
-
const preflight =
|
|
6906
|
-
const specIndex =
|
|
6907
|
-
const reportIndex =
|
|
8005
|
+
const preflight = readJson8(path35.join(migrationRoot, "preflight-report.json"));
|
|
8006
|
+
const specIndex = readJson8(path35.join(migrationRoot, "spec", "index.json"));
|
|
8007
|
+
const reportIndex = readJson8(path35.join(migrationRoot, "report", "index.json"));
|
|
6908
8008
|
let validation;
|
|
6909
8009
|
try {
|
|
6910
8010
|
validation = validateMigrationReport([projectRoot], { writeResult: false });
|
|
@@ -6928,7 +8028,7 @@ function inspectMigration(project = ".") {
|
|
|
6928
8028
|
projectCandidates,
|
|
6929
8029
|
configPresent: Boolean(config),
|
|
6930
8030
|
harmonyProject,
|
|
6931
|
-
harmonyProjectPresent:
|
|
8031
|
+
harmonyProjectPresent: fs29.existsSync(harmonyProject) && fs29.statSync(harmonyProject).isDirectory(),
|
|
6932
8032
|
gitHead: projectPresent ? gitHead(androidProject) : null
|
|
6933
8033
|
},
|
|
6934
8034
|
preflight: {
|
|
@@ -6960,10 +8060,16 @@ function inspectMigration(project = ".") {
|
|
|
6960
8060
|
valid: false,
|
|
6961
8061
|
fresh: false,
|
|
6962
8062
|
taskPlanSha256: null,
|
|
8063
|
+
planningPhase: null,
|
|
6963
8064
|
totalTasks: 0,
|
|
8065
|
+
resolutionTasks: 0,
|
|
8066
|
+
totalTargets: 0,
|
|
8067
|
+
readyTargets: 0,
|
|
8068
|
+
completedTargets: 0,
|
|
6964
8069
|
readyTasks: 0,
|
|
6965
8070
|
blockedTasks: 0,
|
|
6966
8071
|
completedTasks: 0,
|
|
8072
|
+
recommendedTargetId: null,
|
|
6967
8073
|
recommendedTaskId: null,
|
|
6968
8074
|
alternatives: [],
|
|
6969
8075
|
reasons: {},
|
|
@@ -6972,7 +8078,8 @@ function inspectMigration(project = ".") {
|
|
|
6972
8078
|
orphanFactRefs: [],
|
|
6973
8079
|
errors: [],
|
|
6974
8080
|
warnings: [],
|
|
6975
|
-
tasks: []
|
|
8081
|
+
tasks: [],
|
|
8082
|
+
targets: []
|
|
6976
8083
|
},
|
|
6977
8084
|
evidence: {
|
|
6978
8085
|
androidAnalysis: { reference: null, present: false, passed: false, fresh: false },
|
|
@@ -7018,7 +8125,7 @@ function inspectMigration(project = ".") {
|
|
|
7018
8125
|
else {
|
|
7019
8126
|
if (!snapshot.planning.valid) completionUnmet.push(unmet("task-plan-invalid", "Task Plan \u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25"));
|
|
7020
8127
|
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
|
|
8128
|
+
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
8129
|
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
8130
|
}
|
|
7024
8131
|
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 +8211,13 @@ function archiveArtifacts(projectRoot, artifacts) {
|
|
|
7104
8211
|
const archived = [];
|
|
7105
8212
|
for (const value of artifacts) {
|
|
7106
8213
|
const absolute = path35.isAbsolute(value) ? value : path35.resolve(projectRoot, value);
|
|
7107
|
-
if (!
|
|
7108
|
-
const content =
|
|
8214
|
+
if (!fs29.existsSync(absolute) || !fs29.statSync(absolute).isFile()) continue;
|
|
8215
|
+
const content = fs29.readFileSync(absolute);
|
|
7109
8216
|
const digest = createHash8("sha256").update(content).digest("hex");
|
|
7110
8217
|
const destination = path35.join(evidenceRoot(projectRoot), "blobs", digest, path35.basename(absolute));
|
|
7111
|
-
|
|
7112
|
-
const existingIsIntact =
|
|
7113
|
-
if (!existingIsIntact)
|
|
8218
|
+
fs29.mkdirSync(path35.dirname(destination), { recursive: true });
|
|
8219
|
+
const existingIsIntact = fs29.existsSync(destination) && fs29.statSync(destination).isFile() && fs29.statSync(destination).size === content.byteLength && createHash8("sha256").update(fs29.readFileSync(destination)).digest("hex") === digest;
|
|
8220
|
+
if (!existingIsIntact) fs29.copyFileSync(absolute, destination);
|
|
7114
8221
|
archived.push({
|
|
7115
8222
|
path: workspaceRelative(projectRoot, destination),
|
|
7116
8223
|
source: projectRelative(projectRoot, absolute),
|
|
@@ -7135,11 +8242,11 @@ function receiptResult(action, result) {
|
|
|
7135
8242
|
};
|
|
7136
8243
|
}
|
|
7137
8244
|
function writeJsonAtomic(file, value) {
|
|
7138
|
-
|
|
8245
|
+
fs29.mkdirSync(path35.dirname(file), { recursive: true });
|
|
7139
8246
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
7140
|
-
|
|
8247
|
+
fs29.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
|
|
7141
8248
|
`, "utf8");
|
|
7142
|
-
|
|
8249
|
+
fs29.renameSync(temporary, file);
|
|
7143
8250
|
}
|
|
7144
8251
|
function recordObservation(options) {
|
|
7145
8252
|
const projectRoot = path35.resolve(options.project ?? ".");
|
|
@@ -7201,9 +8308,8 @@ Usage:
|
|
|
7201
8308
|
droid2hmos complete [project]
|
|
7202
8309
|
droid2hmos analyze <android|harmony|all> [project]
|
|
7203
8310
|
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>]
|
|
8311
|
+
droid2hmos config <init|validate> [project] [--harmony-project <path>]
|
|
8312
|
+
droid2hmos build harmony [project] [--timeout-ms <ms>]
|
|
7207
8313
|
droid2hmos test harmony [project] [--runner arkts] (--class <class> [--case <case>] | --all) [--target <serial>] [--case-timeout-ms <ms>] [--ref <report-ref>...] [--timeout-ms <ms>]
|
|
7208
8314
|
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
8315
|
droid2hmos report [project] [--require-complete]
|
|
@@ -7212,16 +8318,16 @@ Usage:
|
|
|
7212
8318
|
droid2hmos task plan [project]
|
|
7213
8319
|
droid2hmos task status [project]
|
|
7214
8320
|
droid2hmos task next [project]
|
|
7215
|
-
droid2hmos task show [project] <task-id> [--cursor <cursor>] [--max-source-context <count>]
|
|
8321
|
+
droid2hmos task show [project] <task-or-package-id> [--cursor <cursor>] [--max-source-context <count>]
|
|
7216
8322
|
droid2hmos task validate [project]
|
|
7217
8323
|
droid2hmos checkpoint status [project]
|
|
7218
8324
|
droid2hmos checkpoint create [project] --message <message> (--include <path>...|--all)
|
|
7219
|
-
droid2hmos device check
|
|
8325
|
+
droid2hmos device check harmony [--hdc <path>]
|
|
7220
8326
|
droid2hmos auth login [project] [--timeout-ms <ms>]
|
|
7221
8327
|
droid2hmos sign [project] [--target <serial>] [--force] [--timeout-ms <ms>]
|
|
7222
|
-
droid2hmos evidence capture
|
|
8328
|
+
droid2hmos evidence capture harmony [project] --name <name> [--target <id>]
|
|
7223
8329
|
droid2hmos evidence compare [project] --name <name> --android <evidence-dir> --harmony <evidence-dir>
|
|
7224
|
-
droid2hmos evidence flow
|
|
8330
|
+
droid2hmos evidence flow harmony [project] --name <name> [--target <id>] [--class <class> [--case <case>]|--all] [--case-timeout-ms <ms>] [--ref <report-ref>...]
|
|
7225
8331
|
droid2hmos mock init [project] --spec <protocol-spec.json> [--force]
|
|
7226
8332
|
droid2hmos logs <list|show|failures|stats> [project] [value]
|
|
7227
8333
|
droid2hmos upgrade
|
|
@@ -7274,13 +8380,13 @@ function observed(project, action, result, scope, guardResult) {
|
|
|
7274
8380
|
}
|
|
7275
8381
|
async function runCli(argv, runtime) {
|
|
7276
8382
|
const args = [...argv];
|
|
7277
|
-
let
|
|
7278
|
-
if (
|
|
7279
|
-
if (args.length > 0) throw new CliError(`${
|
|
8383
|
+
let command2 = args.shift();
|
|
8384
|
+
if (command2 && ["-v", "--version"].includes(command2)) {
|
|
8385
|
+
if (args.length > 0) throw new CliError(`${command2} does not accept arguments`);
|
|
7280
8386
|
console.log(readPackageMetadata(runtime.packageRoot).version);
|
|
7281
8387
|
return 0;
|
|
7282
8388
|
}
|
|
7283
|
-
if (!
|
|
8389
|
+
if (!command2 || ["help", "-h", "--help"].includes(command2)) {
|
|
7284
8390
|
console.log(HELP);
|
|
7285
8391
|
return 0;
|
|
7286
8392
|
}
|
|
@@ -7288,13 +8394,13 @@ async function runCli(argv, runtime) {
|
|
|
7288
8394
|
console.log(HELP);
|
|
7289
8395
|
return 0;
|
|
7290
8396
|
}
|
|
7291
|
-
if (
|
|
8397
|
+
if (command2 === "status") {
|
|
7292
8398
|
const projects = positionals(args);
|
|
7293
8399
|
if (projects.length > 1) throw new CliError("status accepts at most one project directory");
|
|
7294
8400
|
json(inspectMigration(projects[0] ?? "."));
|
|
7295
8401
|
return 0;
|
|
7296
8402
|
}
|
|
7297
|
-
if (
|
|
8403
|
+
if (command2 === "complete") {
|
|
7298
8404
|
const projects = positionals(args);
|
|
7299
8405
|
if (projects.length > 1) throw new CliError("complete accepts at most one project directory");
|
|
7300
8406
|
const project = projects[0] ?? ".";
|
|
@@ -7309,7 +8415,7 @@ async function runCli(argv, runtime) {
|
|
|
7309
8415
|
if (exitCode !== 0) return exitCode;
|
|
7310
8416
|
return 0;
|
|
7311
8417
|
}
|
|
7312
|
-
if (
|
|
8418
|
+
if (command2 === "init") {
|
|
7313
8419
|
const projects = positionals(args, ["--target"]);
|
|
7314
8420
|
if (projects.length > 1) throw new Error("init accepts at most one Android project directory");
|
|
7315
8421
|
if (!runtime.templatesRoot) throw new Error(`Cannot locate bundled HarmonyOS template (templates/ohos) from ${runtime.entryFile}`);
|
|
@@ -7335,28 +8441,26 @@ async function runCli(argv, runtime) {
|
|
|
7335
8441
|
json(observed(project, "init", { ...result, signature }, { target: optionValue(args, "--target") ?? null }, guardResult));
|
|
7336
8442
|
return 0;
|
|
7337
8443
|
}
|
|
7338
|
-
if (
|
|
8444
|
+
if (command2 === "upgrade") {
|
|
7339
8445
|
if (args.length > 0) throw new CliError("upgrade does not accept arguments");
|
|
7340
8446
|
json(upgradeCli({ packageRoot: runtime.packageRoot }));
|
|
7341
8447
|
return 0;
|
|
7342
8448
|
}
|
|
7343
|
-
if (
|
|
8449
|
+
if (command2 === "config") {
|
|
7344
8450
|
const action = args.shift();
|
|
7345
8451
|
if (!(/* @__PURE__ */ new Set(["init", "validate"])).has(action)) throw new CliError("config requires init or validate");
|
|
7346
|
-
const projects = positionals(args, ["--harmony-project"
|
|
8452
|
+
const projects = positionals(args, ["--harmony-project"]);
|
|
7347
8453
|
if (projects.length > 1) throw new CliError("config accepts at most one project directory");
|
|
7348
8454
|
const project = projects[0] ?? ".";
|
|
7349
8455
|
const guardResult = guard(`config:${action}`, project);
|
|
7350
8456
|
const result = action === "init" ? initMigrationConfig({
|
|
7351
8457
|
project,
|
|
7352
|
-
harmonyProject: optionValue(args, "--harmony-project")
|
|
7353
|
-
androidBuild: optionValue(args, "--android-build"),
|
|
7354
|
-
androidTest: optionValue(args, "--android-test")
|
|
8458
|
+
harmonyProject: optionValue(args, "--harmony-project")
|
|
7355
8459
|
}) : validateMigrationConfig(project);
|
|
7356
8460
|
json(observed(project, `config:${action}`, result, null, guardResult));
|
|
7357
8461
|
return action === "validate" && !result.valid ? 1 : 0;
|
|
7358
8462
|
}
|
|
7359
|
-
if (
|
|
8463
|
+
if (command2 === "preflight") {
|
|
7360
8464
|
const projects = positionals(args, ["--sdk-path", "--timeout-ms"]);
|
|
7361
8465
|
if (projects.length > 1) throw new CliError("preflight accepts at most one project directory");
|
|
7362
8466
|
if (!runtime.templatesRoot) throw new Error("Cannot locate bundled HarmonyOS template");
|
|
@@ -7368,7 +8472,7 @@ async function runCli(argv, runtime) {
|
|
|
7368
8472
|
json(observed(project, "preflight", result, { initializeHarmony: !args.includes("--no-init-harmony") }, guardResult));
|
|
7369
8473
|
return result.status === "passed" ? 0 : 1;
|
|
7370
8474
|
}
|
|
7371
|
-
if (
|
|
8475
|
+
if (command2 === "analyze") {
|
|
7372
8476
|
const target = args.shift();
|
|
7373
8477
|
if (!(/* @__PURE__ */ new Set(["android", "harmony", "all"])).has(target)) throw new CliError("analyze target must be android, harmony, or all");
|
|
7374
8478
|
const projects = positionals(args);
|
|
@@ -7401,7 +8505,7 @@ async function runCli(argv, runtime) {
|
|
|
7401
8505
|
}
|
|
7402
8506
|
return 0;
|
|
7403
8507
|
}
|
|
7404
|
-
if (
|
|
8508
|
+
if (command2 === "report") {
|
|
7405
8509
|
const explicitAction = ["init", "validate", "render", "update"].includes(args[0] ?? "") ? args.shift() : null;
|
|
7406
8510
|
const action = explicitAction ?? "render";
|
|
7407
8511
|
const projects = positionals(args, ["--ref", "--status", "--harmony-file", "--android-source", "--evidence", "--difference"]);
|
|
@@ -7464,19 +8568,19 @@ async function runCli(argv, runtime) {
|
|
|
7464
8568
|
}
|
|
7465
8569
|
throw new Error("report requires init, validate, or render");
|
|
7466
8570
|
}
|
|
7467
|
-
if (
|
|
8571
|
+
if (command2 === "task") {
|
|
7468
8572
|
const action = args.shift();
|
|
7469
8573
|
if (!(/* @__PURE__ */ new Set(["plan", "status", "next", "show", "validate"])).has(action)) throw new CliError("task requires plan, status, next, show, or validate");
|
|
7470
8574
|
if (action === "show") {
|
|
7471
8575
|
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>");
|
|
8576
|
+
if (values.length < 1 || values.length > 2) throw new CliError("task show requires [project] <task-or-package-id>");
|
|
7473
8577
|
const project2 = values.length === 2 ? values[0] : ".";
|
|
7474
|
-
const
|
|
7475
|
-
if (!/^task:[a-f0-9]{16}$/.test(
|
|
8578
|
+
const targetId = values.at(-1);
|
|
8579
|
+
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
8580
|
const maxSourceContext = Number(optionValue(args, "--max-source-context") ?? 25);
|
|
7477
8581
|
if (!Number.isInteger(maxSourceContext) || maxSourceContext <= 0 || maxSourceContext > 200) throw new CliError("--max-source-context must be an integer from 1 to 200");
|
|
7478
8582
|
const snapshot2 = inspectMigration(project2);
|
|
7479
|
-
json(taskPlanning.materialize(project2,
|
|
8583
|
+
json(taskPlanning.materialize(project2, targetId, {
|
|
7480
8584
|
source: {
|
|
7481
8585
|
androidFingerprint: snapshot2.source.androidFingerprint,
|
|
7482
8586
|
harmonyFingerprint: snapshot2.source.harmonyFingerprint,
|
|
@@ -7498,8 +8602,13 @@ async function runCli(argv, runtime) {
|
|
|
7498
8602
|
const result = {
|
|
7499
8603
|
status: planning.valid && planning.fresh ? "passed" : "failed",
|
|
7500
8604
|
taskPlanVersion: plan.taskPlanVersion,
|
|
8605
|
+
executionPlanVersion: plan.executionPlanVersion,
|
|
8606
|
+
planningPhase: plan.planningPhase,
|
|
7501
8607
|
taskPlanSha256: planning.taskPlanSha256,
|
|
7502
8608
|
totalTasks: plan.tasks.length,
|
|
8609
|
+
behaviorResponsibilities: plan.behaviorResponsibilities.length,
|
|
8610
|
+
pageWorkPackages: plan.pageWorkPackages.length,
|
|
8611
|
+
executionTargets: plan.planningPhase === "resolution" ? plan.tasks.length : plan.pageWorkPackages.length,
|
|
7503
8612
|
resolutionTasks: plan.tasks.filter((task) => task.type === "resolution").length,
|
|
7504
8613
|
errors: planning.errors,
|
|
7505
8614
|
warnings: planning.warnings
|
|
@@ -7525,19 +8634,19 @@ async function runCli(argv, runtime) {
|
|
|
7525
8634
|
}
|
|
7526
8635
|
const allComplete = snapshot.totalTasks > 0 && snapshot.completedTasks === snapshot.totalTasks;
|
|
7527
8636
|
json({
|
|
7528
|
-
status: snapshot.
|
|
8637
|
+
status: snapshot.recommendedTargetId ? "ready" : (() => {
|
|
7529
8638
|
if (allComplete) return "complete";
|
|
7530
8639
|
return "blocked";
|
|
7531
8640
|
})(),
|
|
7532
|
-
recommended: snapshot.
|
|
8641
|
+
recommended: snapshot.recommendedTargetId,
|
|
7533
8642
|
alternatives: snapshot.alternatives,
|
|
7534
8643
|
reasons: snapshot.reasons
|
|
7535
8644
|
});
|
|
7536
|
-
return snapshot.
|
|
8645
|
+
return snapshot.recommendedTargetId || allComplete ? 0 : 1;
|
|
7537
8646
|
}
|
|
7538
|
-
if (
|
|
8647
|
+
if (command2 === "build") {
|
|
7539
8648
|
const target = args.shift();
|
|
7540
|
-
if (
|
|
8649
|
+
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
8650
|
const projects = positionals(args, ["--timeout-ms"]);
|
|
7542
8651
|
if (projects.length > 1) throw new CliError("build accepts at most one project directory");
|
|
7543
8652
|
const projectRoot = path36.resolve(projects[0] ?? ".");
|
|
@@ -7545,52 +8654,29 @@ async function runCli(argv, runtime) {
|
|
|
7545
8654
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new CliError("--timeout-ms must be a positive number");
|
|
7546
8655
|
const configPath = path36.join(migrationDirectory(projectRoot), "config.json");
|
|
7547
8656
|
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;
|
|
8657
|
+
const guardResult = guard("build:harmony", projectRoot);
|
|
8658
|
+
const config = JSON.parse(readFileSync7(configPath, "utf8"));
|
|
8659
|
+
const configuredHarmony = config.harmonyProject ?? defaultHarmonyProjectPath(projectRoot);
|
|
8660
|
+
const harmonyProject = path36.isAbsolute(configuredHarmony) ? configuredHarmony : path36.resolve(projectRoot, configuredHarmony);
|
|
8661
|
+
let result;
|
|
8662
|
+
try {
|
|
8663
|
+
result = runHarmonyBuild({ projectRoot, harmonyProject, timeoutMs });
|
|
8664
|
+
} catch (error) {
|
|
8665
|
+
result = { status: "failed", key: "harmonyBuild", summary: error?.message ?? String(error) };
|
|
7570
8666
|
}
|
|
7571
|
-
|
|
8667
|
+
result = observed(projectRoot, "build:harmony", result, null, guardResult);
|
|
8668
|
+
json(result);
|
|
8669
|
+
return result.status === "passed" ? 0 : 1;
|
|
7572
8670
|
}
|
|
7573
|
-
if (
|
|
8671
|
+
if (command2 === "test") {
|
|
7574
8672
|
const target = args.shift();
|
|
7575
|
-
if (
|
|
7576
|
-
const projects = positionals(args, ["--timeout-ms", "--case-timeout-ms", "--class", "--case", "--
|
|
8673
|
+
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)");
|
|
8674
|
+
const projects = positionals(args, ["--timeout-ms", "--case-timeout-ms", "--class", "--case", "--target", "--ref", "--runner", "--hypium-project", "--test-list", "--python", "--report-path"]);
|
|
7577
8675
|
if (projects.length > 1) throw new CliError("test accepts at most one project directory");
|
|
7578
8676
|
const projectRoot = path36.resolve(projects[0] ?? ".");
|
|
7579
8677
|
const timeoutMs = Number(optionValue(args, "--timeout-ms") ?? 30 * 60 * 1e3);
|
|
7580
8678
|
const harmonyTarget = optionValue(args, "--target") ?? null;
|
|
7581
8679
|
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
8680
|
const runner = optionValue(args, "--runner") ?? "arkts";
|
|
7595
8681
|
if (!(/* @__PURE__ */ new Set(["arkts", "python"])).has(runner)) throw new CliError("--runner must be arkts or python");
|
|
7596
8682
|
const testClass = optionValue(args, "--class") ?? null;
|
|
@@ -7651,7 +8737,7 @@ async function runCli(argv, runtime) {
|
|
|
7651
8737
|
json(result);
|
|
7652
8738
|
return result.status === "passed" ? 0 : 1;
|
|
7653
8739
|
}
|
|
7654
|
-
if (
|
|
8740
|
+
if (command2 === "checkpoint") {
|
|
7655
8741
|
const action = args.shift();
|
|
7656
8742
|
if (!(/* @__PURE__ */ new Set(["status", "create"])).has(action)) throw new CliError("checkpoint requires status or create");
|
|
7657
8743
|
const projects = positionals(args, ["--message", "--include"]);
|
|
@@ -7688,16 +8774,16 @@ async function runCli(argv, runtime) {
|
|
|
7688
8774
|
json(createGitCheckpoint({ project, harmonyProject: roots.harmonyRoot, message: optionValue(args, "--message") ?? "", include: optionValues(args, "--include"), all: args.includes("--all"), provenanceReceipt, provenanceFiles }));
|
|
7689
8775
|
return 0;
|
|
7690
8776
|
}
|
|
7691
|
-
if (
|
|
8777
|
+
if (command2 === "device") {
|
|
7692
8778
|
if (args.shift() !== "check") throw new CliError("device requires check");
|
|
7693
8779
|
const platform = args.shift();
|
|
7694
|
-
if (
|
|
8780
|
+
if (platform !== "harmony") throw new CliError("device check requires harmony (Android \u8BBE\u5907\u68C0\u6D4B\u5DF2\u79FB\u9664)");
|
|
7695
8781
|
guard("device:check", ".");
|
|
7696
|
-
const result =
|
|
8782
|
+
const result = checkHarmonyDevice({ hdc: optionValue(args, "--hdc") });
|
|
7697
8783
|
json(result);
|
|
7698
8784
|
return result.status === "failed" ? 1 : 0;
|
|
7699
8785
|
}
|
|
7700
|
-
if (
|
|
8786
|
+
if (command2 === "auth") {
|
|
7701
8787
|
if (args.shift() !== "login") throw new CliError("auth requires login");
|
|
7702
8788
|
const projects = positionals(args, ["--timeout-ms"]);
|
|
7703
8789
|
if (projects.length > 1) throw new CliError("auth accepts at most one project directory");
|
|
@@ -7712,7 +8798,7 @@ async function runCli(argv, runtime) {
|
|
|
7712
8798
|
json(result);
|
|
7713
8799
|
return result.status === "passed" ? 0 : 1;
|
|
7714
8800
|
}
|
|
7715
|
-
if (
|
|
8801
|
+
if (command2 === "sign") {
|
|
7716
8802
|
const projects = positionals(args, ["--target", "--timeout-ms"]);
|
|
7717
8803
|
if (projects.length > 1) throw new CliError("sign accepts at most one project directory");
|
|
7718
8804
|
const project = projects[0] ?? ".";
|
|
@@ -7734,7 +8820,7 @@ async function runCli(argv, runtime) {
|
|
|
7734
8820
|
json(result);
|
|
7735
8821
|
return result.status === "passed" || result.status === "skipped" ? 0 : 1;
|
|
7736
8822
|
}
|
|
7737
|
-
if (
|
|
8823
|
+
if (command2 === "evidence") {
|
|
7738
8824
|
const action = args.shift();
|
|
7739
8825
|
if (!(/* @__PURE__ */ new Set(["capture", "compare", "flow"])).has(action)) throw new CliError("evidence requires capture, compare, or flow");
|
|
7740
8826
|
if (action === "compare") {
|
|
@@ -7749,8 +8835,8 @@ async function runCli(argv, runtime) {
|
|
|
7749
8835
|
return 0;
|
|
7750
8836
|
}
|
|
7751
8837
|
const platform = args.shift();
|
|
7752
|
-
if (
|
|
7753
|
-
const projects = positionals(args, ["--name", "--target", "--class", "--case", "--
|
|
8838
|
+
if (platform !== "harmony") throw new CliError(`evidence ${action} requires harmony (Android \u8BC1\u636E\u91C7\u96C6\u5DF2\u79FB\u9664)`);
|
|
8839
|
+
const projects = positionals(args, ["--name", "--target", "--class", "--case", "--timeout-ms", "--case-timeout-ms", "--ref"]);
|
|
7754
8840
|
const project = projects[0] ?? ".";
|
|
7755
8841
|
const name = optionValue(args, "--name");
|
|
7756
8842
|
if (!name) throw new CliError(`evidence ${action} requires --name`);
|
|
@@ -7766,11 +8852,7 @@ async function runCli(argv, runtime) {
|
|
|
7766
8852
|
const timeoutMs = Number(optionValue(args, "--timeout-ms") ?? 30 * 60 * 1e3);
|
|
7767
8853
|
let execution;
|
|
7768
8854
|
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 {
|
|
8855
|
+
else {
|
|
7774
8856
|
const config = JSON.parse(readFileSync7(path36.join(migrationDirectory(projectRoot), "config.json"), "utf8"));
|
|
7775
8857
|
const harmonyProject = path36.isAbsolute(config.harmonyProject) ? config.harmonyProject : path36.resolve(projectRoot, config.harmonyProject);
|
|
7776
8858
|
const testClass = optionValue(args, "--class") ?? null;
|
|
@@ -7782,7 +8864,7 @@ async function runCli(argv, runtime) {
|
|
|
7782
8864
|
}
|
|
7783
8865
|
const after = captureDeviceEvidence({ project, platform, name: `${name}-after`, target: optionValue(args, "--target") });
|
|
7784
8866
|
let result = writeFlowBundle({ project, name, platform, before, execution, after });
|
|
7785
|
-
|
|
8867
|
+
{
|
|
7786
8868
|
const testAction = args.includes("--all") ? "test:harmony:all" : "test:harmony:targeted";
|
|
7787
8869
|
result = observed(project, testAction, result, {
|
|
7788
8870
|
evidenceFlow: name,
|
|
@@ -7797,7 +8879,7 @@ async function runCli(argv, runtime) {
|
|
|
7797
8879
|
json(result);
|
|
7798
8880
|
return result.status === "passed" ? 0 : 1;
|
|
7799
8881
|
}
|
|
7800
|
-
if (
|
|
8882
|
+
if (command2 === "mock") {
|
|
7801
8883
|
if (args.shift() !== "init") throw new CliError("mock requires init");
|
|
7802
8884
|
const projects = positionals(args, ["--spec"]);
|
|
7803
8885
|
const specFile = optionValue(args, "--spec");
|
|
@@ -7807,14 +8889,14 @@ async function runCli(argv, runtime) {
|
|
|
7807
8889
|
json(generateMockServer({ project, specFile, force: args.includes("--force") }));
|
|
7808
8890
|
return 0;
|
|
7809
8891
|
}
|
|
7810
|
-
if (
|
|
8892
|
+
if (command2 === "logs") {
|
|
7811
8893
|
const action = args.shift();
|
|
7812
8894
|
if (!(/* @__PURE__ */ new Set(["list", "show", "failures", "stats"])).has(action)) throw new CliError("logs requires list, show, failures, or stats");
|
|
7813
8895
|
const project = args.shift() ?? ".";
|
|
7814
8896
|
json(queryCommandLogs(project, action, args.shift()));
|
|
7815
8897
|
return 0;
|
|
7816
8898
|
}
|
|
7817
|
-
if (
|
|
8899
|
+
if (command2 === "service") {
|
|
7818
8900
|
const action = args.shift();
|
|
7819
8901
|
if (!(/* @__PURE__ */ new Set(["start", "status", "stop"])).has(action)) throw new CliError("service action must be start, status, or stop");
|
|
7820
8902
|
const name = args.shift();
|
|
@@ -7858,7 +8940,7 @@ async function runCli(argv, runtime) {
|
|
|
7858
8940
|
}));
|
|
7859
8941
|
return 0;
|
|
7860
8942
|
}
|
|
7861
|
-
if (
|
|
8943
|
+
if (command2 === "internal") {
|
|
7862
8944
|
const action = args.shift();
|
|
7863
8945
|
if (action === "detect-harmony-sdk") {
|
|
7864
8946
|
const projects = positionals(args, ["--sdk-path"]);
|
|
@@ -7890,14 +8972,14 @@ async function runCli(argv, runtime) {
|
|
|
7890
8972
|
}
|
|
7891
8973
|
throw new Error("unknown internal command");
|
|
7892
8974
|
}
|
|
7893
|
-
if (
|
|
8975
|
+
if (command2 === "dev") {
|
|
7894
8976
|
const action = args.shift();
|
|
7895
8977
|
if (!runtime.packageRoot || action !== "build") throw new Error("dev requires build from a source checkout");
|
|
7896
8978
|
const result = spawnCommandSync("npm", ["run", "build"], { cwd: runtime.packageRoot, stdio: "inherit" });
|
|
7897
8979
|
if (result.error) throw result.error;
|
|
7898
8980
|
return result.status ?? 1;
|
|
7899
8981
|
}
|
|
7900
|
-
throw new CliError(`Unknown command: ${
|
|
8982
|
+
throw new CliError(`Unknown command: ${command2}`);
|
|
7901
8983
|
}
|
|
7902
8984
|
var entryHref = import.meta.url;
|
|
7903
8985
|
var invokedHref = (() => {
|