mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(mobile): derive runtime hints from classified components
Use the owning DEX/JVM and Ark/JavaScript component lists for runtime summaries. Remove the independent filename checks and Harmony path copy. Preserve native format checks and framework path conventions. Adapt @eco-null's proposal and regressions from PR #3627. Cover all eight supported source extensions and unrelated content, plus the Android inverse mismatch discovered during consumer review: .dex/.class files had no bytecode components but produced runtime hints. Validation: 44 focused checks, build, check:fast and docs:check pass. Six Harmony assertions and one Android built CLI assertion fail before this fix. 36 actual built CLI cost controls preserve four ordinary complete replies and projection IDs byte for byte. No heavy profiling, target or real deep-provider execution; npm release unverified. Fixes #3350 Fixes #4782
This commit is contained in:
1 parent
a425b3e528
commit
fc935d0df3
7 files changed
+154
-15
No files matched your search
@@ -236,7 +236,9 @@ The projection reports exact component paths and hashes, runtime-family hints,
|
||||
and path-based bridge hypotheses. File and slice occurrences supply component
|
||||
endpoints; directory trees and symlinks retain their identities in the source
|
||||
inventory without being classified as code files. It does not decode DEX or
|
||||
claim observed runtime calls. Framework hints match complete native library names, such as
|
||||
claim observed runtime calls. DEX/JVM runtime hints summarize the classified
|
||||
bytecode component lists. Framework hints match complete native library names,
|
||||
such as
|
||||
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
|
||||
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
|
||||
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
|
||||
|
||||
@@ -47,7 +47,9 @@ The MCP tool `project_harmony_application_graph` takes the same
|
||||
|
||||
File and slice occurrences supply component and child-package paths; directory
|
||||
trees and symlinks retain their identities in the source inventory without
|
||||
being classified as code files.
|
||||
being classified as code files. Ark and JavaScript runtime hints summarize the
|
||||
classified bytecode and JavaScript lists; supported source extensions share the
|
||||
same classification rule as their component rows.
|
||||
|
||||
Every claim is path-presence only. Manifest semantics (module identity,
|
||||
abilities, entry points, permissions, dependencies), Ark bytecode contents and
|
||||
|
||||
@@ -139,7 +139,7 @@ export const projectAndroidApplication = (
|
||||
.sort(compareUnicodeCodePoints),
|
||||
components,
|
||||
unavailable_components: unavailable,
|
||||
runtime_families: runtimeFamilies(all),
|
||||
runtime_families: runtimeFamilies(all, classified),
|
||||
bridge_candidates: bridgeProjection.candidates,
|
||||
bridge_candidate_coverage: bridgeProjection.coverage,
|
||||
coverage: projectionCoverage(
|
||||
@@ -172,14 +172,15 @@ const classify = (all: readonly Component[]) => ({
|
||||
),
|
||||
});
|
||||
|
||||
const runtimeFamilies = (all: readonly Component[]) => {
|
||||
const runtimeFamilies = (
|
||||
all: readonly Component[],
|
||||
components: AndroidApplicationProjectionResult["components"],
|
||||
) => {
|
||||
const families = new Set<
|
||||
AndroidApplicationProjectionResult["runtime_families"][number]
|
||||
>();
|
||||
if (all.some(({ path }) => path.toLowerCase().endsWith(".dex")))
|
||||
families.add("dalvik-art");
|
||||
if (all.some(({ path }) => path.toLowerCase().endsWith(".class")))
|
||||
families.add("java-kotlin");
|
||||
if (components.dex.length > 0) families.add("dalvik-art");
|
||||
if (components.jvm_classes.length > 0) families.add("java-kotlin");
|
||||
if (all.some(({ format }) => format === "elf")) families.add("native");
|
||||
if (all.some(({ format }) => format === "javascript-bundle"))
|
||||
families.add("javascript");
|
||||
|
||||
@@ -135,7 +135,7 @@ export const projectHarmonyApplication = (
|
||||
rootFormat === "app-pack"
|
||||
? all.filter(({ path }) => /\.(?:hap|hsp)$/iu.test(path))
|
||||
: [],
|
||||
runtime_families: runtimeFamilies(all),
|
||||
runtime_families: runtimeFamilies(all, classified),
|
||||
bridge_candidates: bridgeProjection.candidates,
|
||||
bridge_candidate_coverage: bridgeProjection.coverage,
|
||||
coverage: projectionCoverage(
|
||||
@@ -195,16 +195,16 @@ const classify = (all: readonly Component[]) => ({
|
||||
),
|
||||
});
|
||||
|
||||
const runtimeFamilies = (all: readonly Component[]) => {
|
||||
const paths = all.map(({ path }) => path.toLowerCase());
|
||||
const runtimeFamilies = (
|
||||
all: readonly Component[],
|
||||
components: HarmonyApplicationProjectionResult["components"],
|
||||
) => {
|
||||
const families = new Set<
|
||||
HarmonyApplicationProjectionResult["runtime_families"][number]
|
||||
>();
|
||||
if (paths.some((path) => path.endsWith(".abc") || path.endsWith(".pa")))
|
||||
families.add("ark");
|
||||
if (components.bytecode.length > 0) families.add("ark");
|
||||
if (all.some(({ format }) => format === "elf")) families.add("native");
|
||||
if (paths.some((path) => /\.(?:m?js|cjs)$/.test(path)))
|
||||
families.add("javascript");
|
||||
if (components.javascript.length > 0) families.add("javascript");
|
||||
for (const { path } of all) {
|
||||
const convention = nativeRuntimeConvention(path);
|
||||
if (convention !== null) families.add(convention);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { androidApplicationProjectionResultSchema } from "../../../src/domain/android/androidApplication.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"Android runtime hints follow classified bytecode through CLI and MCP",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-android-runtime-hints-");
|
||||
const environment = { HOME: root, USERPROFILE: root };
|
||||
const { call } = await connectLocalToolsMcp(environment);
|
||||
for (const prefix of ["", "named"]) {
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("AndroidManifest.xml", new TextReader("fixture"));
|
||||
for (const extension of ["dex", "class"])
|
||||
await writer.add(
|
||||
`assets/${prefix}.${extension}`,
|
||||
new TextReader("ordinary text"),
|
||||
);
|
||||
const path = join(root, `${prefix || "dot-names"}.apk`);
|
||||
await writeFile(path, await writer.close());
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-artifact", path, "--json"],
|
||||
environment,
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const source = parseEvidence(inspected.json);
|
||||
const input = { inventory_evidence: [source] };
|
||||
const projected = await cli.run({
|
||||
arguments: [
|
||||
"project-android-application-graph",
|
||||
JSON.stringify(input),
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
});
|
||||
expect(projected.exitCode, projected.stderr).toBe(0);
|
||||
const evidence = parseEvidence(projected.json);
|
||||
const graph = androidApplicationProjectionResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(graph.components.dex.map(({ path }) => path)).toEqual(
|
||||
prefix ? ["assets/named.dex"] : [],
|
||||
);
|
||||
expect(graph.components.jvm_classes.map(({ path }) => path)).toEqual(
|
||||
prefix ? ["assets/named.class"] : [],
|
||||
);
|
||||
expect(graph.runtime_families).toEqual(
|
||||
prefix ? ["dalvik-art", "java-kotlin"] : [],
|
||||
);
|
||||
const mcp = await call("project_android_application_graph", input);
|
||||
expect(mcp.isError).not.toBe(true);
|
||||
expect(parseEvidence(mcp.structuredContent)).toEqual(evidence);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -25,6 +25,11 @@ test("preserves Harmony archive formats through the public CLI/MCP lifecycle", a
|
||||
"ets/modules.abc",
|
||||
new TextReader("opaque bytecode fixture"),
|
||||
);
|
||||
const javascriptPaths = ["cts", "jsx", "mts", "ts", "tsx"].map(
|
||||
(extension) => `assets/js/index.${extension}`,
|
||||
);
|
||||
for (const path of javascriptPaths)
|
||||
await writer.add(path, new TextReader("export const value = 1;"));
|
||||
const bytes = await writer.close();
|
||||
const environment = {
|
||||
PATH: process.env.PATH ?? "",
|
||||
@@ -114,6 +119,30 @@ test("preserves Harmony archive formats through the public CLI/MCP lifecycle", a
|
||||
expect(
|
||||
graph.components.bytecode.map(({ path: member }) => member),
|
||||
).toEqual(["ets/modules.abc"]);
|
||||
expect(
|
||||
graph.components.javascript.map(({ path: member }) => member),
|
||||
).toEqual(javascriptPaths);
|
||||
expect(graph.runtime_families).toEqual(["ark", "javascript"]);
|
||||
const projectionInput = workspace.path("projection-input.json");
|
||||
await writeFile(
|
||||
projectionInput,
|
||||
JSON.stringify({ inventory_evidence: [cliEvidence] }),
|
||||
);
|
||||
const cliProjection = await cli.run({
|
||||
arguments: [
|
||||
"project-harmony-application-graph",
|
||||
projectionInput,
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
environment,
|
||||
});
|
||||
expect(cliProjection.exitCode, cliProjection.stderr).toBe(0);
|
||||
expect(
|
||||
harmonyApplicationProjectionResultSchema.parse(
|
||||
parseEvidence(cliProjection.json).normalized_result,
|
||||
),
|
||||
).toEqual(graph);
|
||||
const closed = await client.callTool({
|
||||
name: "close_binary",
|
||||
arguments: {},
|
||||
|
||||
@@ -37,6 +37,48 @@ const stageHapEntries = [
|
||||
] as const;
|
||||
|
||||
describe("HarmonyOS application projection", () => {
|
||||
it.each(["js", "mjs", "cjs", "jsx", "ts", "tsx", "mts", "cts"])(
|
||||
"retains the JavaScript runtime hint for an authenticated .%s component",
|
||||
async (extension) => {
|
||||
const member = `assets/js/index.${extension}`;
|
||||
const path = await writeApplicationZip(
|
||||
"rea-harmony-javascript-",
|
||||
"Fixture.hap",
|
||||
[{ path: member, content: "export const value = 1;" }],
|
||||
);
|
||||
const inventory = await inventoryApplicationPackage(path);
|
||||
const projection = requireDeterministicProjection(
|
||||
projectHarmonyApplicationEvidence,
|
||||
inventory,
|
||||
harmonyApplicationProjectionResultSchema,
|
||||
);
|
||||
expect(projection.components.javascript).toEqual([
|
||||
expect.objectContaining({ path: member, format: "javascript-bundle" }),
|
||||
]);
|
||||
expect(projection.runtime_families).toEqual(["javascript"]);
|
||||
expect(projection.coverage.status).toBe("complete-within-inventory");
|
||||
},
|
||||
);
|
||||
|
||||
it("does not infer JavaScript from unrelated inventory content", async () => {
|
||||
const path = await writeApplicationZip(
|
||||
"rea-harmony-no-javascript-",
|
||||
"Fixture.hap",
|
||||
[
|
||||
{ path: "module.json", content: "{}" },
|
||||
{ path: "assets/js/notes.txt", content: "ordinary text" },
|
||||
],
|
||||
);
|
||||
const inventory = await inventoryApplicationPackage(path);
|
||||
const projection = requireDeterministicProjection(
|
||||
projectHarmonyApplicationEvidence,
|
||||
inventory,
|
||||
harmonyApplicationProjectionResultSchema,
|
||||
);
|
||||
expect(projection.components.javascript).toEqual([]);
|
||||
expect(projection.runtime_families).toEqual([]);
|
||||
});
|
||||
|
||||
it("projects deterministic Stage HAP components and explicit bridge hypotheses", async () => {
|
||||
const path = await writeApplicationZip(
|
||||
"rea-harmony-",
|
||||
|
||||
Reference in new issue
Block a user