fix(harmony): classify exact manifest names once (#7206)

Use one exact-case manifest path classifier for Harmony model hints and components. Preserve unavailable content identity, resource exclusions, Stage precedence and Evidence provenance.

33 boundary tests, docs checks, serial static checks and real pinned-HAP CLI/MCP before/after task passed. Canonical complete projection Evidence stays identical; uppercase derivative now reports unknown/no manifests. Shipped verifier source-ID assertion reproduces on unchanged main. CI7pass4fail19skip7cancel; exactmain reproduces17source and20shard8 failures. Greptile4/5soleupstreamfile-readercleanup scoped/resolvednotfixed. Requester authorized readiness and unrelated-main bypass; human review is not attested.

Co-authored-by: gensokyo-dev <[email protected]>
This commit is contained in:
Rudy Mizrahi Celekliandgensokyo-dev authored and GitHub committed 2026-10-27 13:30:24 +08:00
1 parent f59fc19d21
commit 7d6bccb86d
3 files changed
+103 -21

No files matched your search

+4 -1
View File
@@ -36,7 +36,7 @@ The MCP tool `project_harmony_application_graph` takes the same
- exact component paths, artifact IDs, and hashes (manifests, Ark bytecode,
resources, native libraries, JavaScript, signing)
- a packaging-model hint: Stage is claimed only from a `module.json` entry
- a packaging-model hint: Stage is claimed only from an exactly cased `module.json` entry
outside a classified resource path, FA only from `config.json` outside a
classified resource path, otherwise `unknown`
- FA resources under `assets/<module>/resources/` as well as Stage `resources/`
@@ -46,6 +46,9 @@ The MCP tool `project_harmony_application_graph` takes the same
- path-based N-API bridge hypotheses between bytecode and `libs/<abi>` native
libraries
An unavailable manifest can still supply a path-based model hint; its content
identity remains unknown. Manifest names follow case-sensitive ZIP member names.
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. Ark and JavaScript runtime hints summarize the
+25 -20
View File
@@ -20,6 +20,7 @@ import {
projectionCoverage,
projectionEvidenceIdSchema,
unavailableComponentSchema,
type ProjectedInventoryEntry,
} from "../mobileApplicationGraphProjection.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { err, ok, type Result } from "../result.js";
@@ -94,7 +95,8 @@ export const projectHarmonyInventory = (
if (child !== undefined) children.push(child);
}
}
const classified = classify(all);
const manifests = classifyManifests(fileEntries);
const classified = classify(all, manifests.components);
const bridgeProjection = bridgeCandidates(
classified.bytecode,
classified.native_libraries,
@@ -134,9 +136,7 @@ export const projectHarmonyInventory = (
source_evidence_ids: evidence
.map(({ evidence_id: id }) => id)
.sort(compareUnicodeCodePoints),
packaging_model: packagingModel(
fileEntries.filter(({ path }) => !isResourcePath(path)),
),
packaging_model: manifests.packagingModel,
components: classified,
unavailable_components: unavailable,
app_pack_children: children,
@@ -181,24 +181,29 @@ const harmonyRootFormat = (
return ok(root);
};
const packagingModel = (
manifests: readonly { readonly path: string }[],
): HarmonyApplicationProjectionResult["packaging_model"] => {
const stage = manifests.some(({ path }) =>
/(?:^|\/)module\.json$/iu.test(path),
);
const fa = manifests.some(({ path }) => /(?:^|\/)config\.json$/iu.test(path));
if (stage) return "stage";
if (fa) return "fa";
return "unknown";
/**
* ZIP manifest names are case-sensitive. Classify each file path once for
* both consumers; its model hint survives an unavailable content identity.
*/
const classifyManifests = (entries: readonly ProjectedInventoryEntry[]) => {
const components: Component[] = [];
let packagingModel: HarmonyApplicationProjectionResult["packaging_model"] =
"unknown";
for (const { path, component } of entries) {
const name = /(?:^|\/)(module\.json|config\.json|pack\.info)$/u.exec(
path,
)?.[1];
if (name === undefined || isResourcePath(path)) continue;
if (component !== undefined) components.push(component);
if (name === "module.json") packagingModel = "stage";
else if (name === "config.json" && packagingModel === "unknown")
packagingModel = "fa";
}
return { components, packagingModel };
};
const classify = (all: readonly Component[]) => ({
manifests: all.filter(
({ path }) =>
/(?:^|\/)(?:module\.json|config\.json|pack\.info)$/iu.test(path) &&
!isResourcePath(path),
),
const classify = (all: readonly Component[], manifests: Component[]) => ({
manifests,
bytecode: all.filter(({ path }) => /\.(?:abc|pa)$/iu.test(path)),
resources: all.filter(({ path }) => isResourcePath(path)),
native_libraries: all.filter(
@@ -400,3 +400,77 @@ describe("HarmonyOS inventory coverage", () => {
);
});
});
describe("HarmonyOS manifest path rules", () => {
it("claims stage and FA models only from the exact manifest filenames", async () => {
// HAP/HSP/App Pack members are ZIP names. OpenHarmony's stage, FA, and
// app-pack manifests are module.json, config.json, and pack.info.
const path = await writeApplicationZip(
"rea-harmony-manifest-case-",
"Fixture.hap",
[
{ path: "MODULE.JSON", content: "{}" },
{ path: "CONFIG.JSON", content: "{}" },
{ path: "PACK.INFO", content: "{}" },
],
);
const projection = requireDeterministicProjection(
projectHarmonyApplicationEvidence,
await inventoryApplicationPackage(path),
harmonyApplicationProjectionResultSchema,
);
expect(projection.packaging_model).toBe("unknown");
expect(projection.components.manifests).toEqual([]);
});
it.each([
{ paths: ["a/module.json", "z/config.json"] },
{ paths: ["a/config.json", "z/module.json"] },
])("keeps Stage precedence for $paths", async ({ paths }) => {
const path = await writeApplicationZip(
"rea-harmony-mixed-model-",
"Both.hap",
paths.map((path) => ({ path, content: "{}" })),
);
const projection = requireDeterministicProjection(
projectHarmonyApplicationEvidence,
await inventoryApplicationPackage(path),
harmonyApplicationProjectionResultSchema,
);
expect(projection.packaging_model).toBe("stage");
expect(
projection.components.manifests.map(({ path }) => path).sort(),
).toEqual(paths.toSorted());
});
it.each([
{ name: "module.json", model: "stage" },
{ name: "config.json", model: "fa" },
{ name: "MODULE.JSON", model: "unknown" },
])("retains the path hint for unavailable $name", async ({ name, model }) => {
const root = await createTestTempDirectory("rea-harmony-locked-manifest-");
const path = join(root, "Locked.hap");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(name, new TextReader("{}"), { password: "fixture-only" });
await writeFile(path, await writer.close());
const projection = requireDeterministicProjection(
projectHarmonyApplicationEvidence,
await inventoryApplicationPackage(path),
harmonyApplicationProjectionResultSchema,
);
expect(projection.packaging_model).toBe(model);
expect(projection.components.manifests).toEqual([]);
expect(projection.unavailable_components).toEqual([
expect.objectContaining({
path: name,
artifact_id: null,
sha256: null,
encrypted: true,
}),
]);
expect(projection.coverage).toEqual({
status: "partial",
inventory_complete: true,
});
});
});