perf(flutter): add opt-in retained AOT summary and string pages

Add explicit MCP Dart AOT summary delivery and exact retained string-pool pages while preserving complete defaults, CLI behavior, ABI selection, original Evidence identity, partial coverage and lower bounds.

Real LocalSend SDK replies shrink 92.05% with approximately unchanged SDK time. Two actual Codex pairs preserve all selected facts and reduce total input 48.75% / 42.38%, uncached input 33.63% / 42.52%, and task time 23.95% / 30.30%; all client errors/repeated calls remain counted. Preserve old portable Evidence and the real CLI complete result after rebasing onto SAZ/qlog main. Curated results keep measured and integration runtime pins separate; raw artifacts stay outside Git.

Refs #2016.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Ysopwnable 2026-10-13 11:39:30 +08:00 committed by GitHub
parent bb154d1067
commit 23de4f77c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 867 additions and 19 deletions

View File

@ -25,6 +25,26 @@ The equivalent MCP methods are `identify_flutter_build` (with `path`) and
arm64-v8a, then x86_64, then x86, then armeabi-v7a, then the remaining
ABIs alphabetically).
## Optional retained AOT delivery
MCP `inspect_dart_aot` keeps complete output when `detail` is omitted or
`"complete"`. Explicit `detail: "summary"` returns the selected target/ABI/library
identity, all snapshot headers, structural coverage, string-pool coverage and
original lower-bound counts without the string lists. It retains complete source
Evidence and returns a distinct linked `inspect_analysis_view` summary. Analysis
and the caller's ABI choice are unchanged; one-shot CLI output stays complete.
Use `normalized_result.parent_evidence_id` as the retained source for
`inspect_analysis_view`, with `view: {kind: "dart-aot", collection: "package_uris",
offset: 0, limit: 64}`. Collections are `package_uris`, `dart_uris` and
`class_like_tokens`. Pages preserve the original strings and order, selected ABI,
library identity and source coverage. `returned_counts` describe producer-returned
rows; counts ending in `_at_least` remain lower bounds. Exhausting a page sequence
does not make a partial pool complete or turn byte-pattern tokens into typed
classes/functions. Views do not reopen the APK. Original Evidence remains
exportable under its existing session lifetime. Summary requires a retaining
server; older servers keep the complete request after schema discovery.
## What the AOT inspection returns
`inspect_dart_aot` reads one ABI's `libapp.so` in full and reports:

View File

@ -7,6 +7,14 @@ JSON measurements belong outside the checkout. This document keeps the measured
findings, verification scope and revision references; the scripts and `corpus.json`
keep the reproduction inputs. Generated `results/` is ignored.
Follow-up: [latest-main capability audit](latest-main-audit.md) records the
Flutter, SQLite, TShark and sigrok priorities and the first shipped Dart AOT
baseline, preserving existing controls before introducing another selector.
Follow-up: [Dart AOT summary and retained string pools](flutter-aot-summary.md)
records real LocalSend delivery pairs and old Evidence continuation, preserving
ABI selection, snapshot identity, original lower bounds and partial coverage.
Follow-up: [native PE/DOS entry views and real model verification](native-entry-views.md)
records the post-merge native continuation, including a small-function case where
four selected replies cost more bytes than the complete dossier.

View File

@ -235,6 +235,21 @@
},
"immutable_scope": "Published Hardhat artifact does not record an immutable-reference map; absence is unknown, not proof of no immutables. Runtime template bytes remain exact."
}
},
{
"id": "localsend-arm64.apk",
"file": "LocalSend-1.18.2-android-arm64v8.apk",
"bytes": 46558706,
"sha256": "82ec3568fba2aa5295b9aae8b76f701d7a4703d86b9f8bad749472038fbaeab3",
"provenance": {
"project": "LocalSend",
"version": "1.18.2",
"upstreamFile": "LocalSend-1.18.2-android-arm64v8.apk",
"url": "https://github.com/localsend/localsend/releases/download/v1.18.2/LocalSend-1.18.2-android-arm64v8.apk",
"transport_sha256": "82ec3568fba2aa5295b9aae8b76f701d7a4703d86b9f8bad749472038fbaeab3",
"transport_bytes": 46558706,
"derivation": "Official arm64 release APK, compared with GitHub release asset digest; byte-only analysis without execution"
}
}
]
}

View File

@ -0,0 +1,160 @@
# Dart AOT summary and retained string pools
Continues [#2016](https://github.com/morluto/rea/issues/2016) and
[#2455](https://github.com/morluto/rea/pull/2455). The selected analyst question
asks for shipped LocalSend target/ABI/library identity, every nullable snapshot
observation, string-pool lower bounds and returned counts, and original coverage
and limitations. It does not require individual observed strings.
## Compatibility and ownership
MCP `inspect_dart_aot` accepts explicit `detail: "summary"`. Omitted and complete
requests preserve the complete result and original provider inputs. The CLI
remains complete. ABI omission and explicit ABI selection retain their original
meaning; the provider never receives the delivery field. Missing retention,
invalid detail and pre-cancellation fail before provider work.
The existing application workflow retains the complete original Evidence, then
returns a distinct derived summary linked to its parent ID. It preserves target
and libapp digests, every snapshot section and nullable fact, original pool and
overall coverage, lower-bound counts and limitations. `returned_counts` reports
the lengths of the producer-returned arrays; it does not upgrade lower bounds
to a complete snapshot inventory. The analysis and original source retention
are unchanged. No new engine, persistent cache or plugin SDK is introduced.
Read exact retained strings using `inspect_analysis_view`, the parent ID, and
`view: {kind: "dart-aot", collection: "package_uris", offset: 0, limit: 64}`.
Other collections are `dart_uris` and `class_like_tokens`. Follow the returned
next offset. Pages preserve source order, ABI/library identity and partial pool
coverage without opening the APK again. Page exhaustion describes retained
strings, not the entire snapshot. Strings remain byte-pattern observations,
rather than recovered typed class/function semantics. Missing historical
subjects remain explicitly unknown; conflicting recorded target digests fail.
## Frozen runtime and real SDK pairs
The official LocalSend 1.18.2 arm64 APK acquisition, size and SHA-256 are pinned
in `corpus.json`. No APK installation, target execution or version-dataset
lookup occurred. Source runtime: `811034a7283c9751a3ecbff33e8b2f28bec55793`;
compiled dist/bridge file-table SHA-256:
`dabbb63536c5862be839ecdc0f3eaad05b29c2a3816f57041528d30f754311be`.
The previous runtime is `28c84e78cf2fbfde4aceed345b773c59eb889f68`, digest
`5332dda13981f043e146b3601ebec1f380d9f698280ca53d75786b638a912e11`.
Its production sources were verified byte-equal to main f254 at acquisition.
Subsequent test-only changes and main's SAZ addition do not relabel these pins.
Node is 24.18.0 and the SDK client/server are 2.3.1. Each workflow starts a fresh
server and validates the selected live advertised schema. Artifact bytes,
provider environment/search-path fingerprints, driver and locked dependencies
stay equal. Host page caches are not flushed. Heavy tasks run serially under
the unchanged CPU, memory, task and host guards.
| Trial | AOT reply bytes | SDK workflow seconds | Payload CPU seconds | Kernel peak bytes |
| -------------- | --------------: | -------------------: | ------------------: | ----------------: |
| sdk-complete-1 | 151,341 | 5.139 | 6.819 | 691,441,664 |
| sdk-summary-1 | 12,037 | 5.219 | 6.608 | 639,430,656 |
| sdk-summary-2 | 12,037 | 5.064 | 6.405 | 640,868,352 |
| sdk-complete-2 | 151,341 | 5.128 | 6.468 | 633,614,336 |
Both pairs use one successful public tool call and pass settings/fact comparison,
including all snapshot fields, coverage, original limitations and all counts.
Reply bytes fall 92.05%. Whole SDK time stays approximately 5.1 seconds; these
trials do not establish engine speed, CPU or memory savings. Payload CPU includes
driver/catalog work beyond the SDK workflow timer. Catalog JSON grows from
2,506,213 bytes on the previous runtime to 2,514,896 (about 0.35%).
A fresh old-runtime complete request passes comparison against candidate
complete, including identical 151,341-byte replies. This is scoped compatibility
evidence, not a guarantee for every historical release or third-party client.
## Real old Evidence continuation
Fifteen successful public MCP calls import genuine old-runtime AOT Evidence,
project retained summary, reconstruct all 500 package URIs, 216 Dart URIs and
2,000 class-like tokens, compare inline portable summary, and export the original
record. The APK is absent during all these operations. Original string order,
lower bounds, partial coverage, nullable snapshot facts, subject and parent ID
remain intact; the original exported record is deeply equal to its source.
The first private verification attempt used a nonexistent receipt filename and
failed before import or artifact relocation. That driver error is retained;
the corrected attempt selects the fresh old-runtime record. It is not a REA
defect or a successful migration trial.
## Actual Codex task outcomes
Four serial trials use the same frozen candidate in complete/summary/summary/
complete order. Requested configuration is Codex CLI 0.161.0, `gpt-5.6-terra`
high, one exact enabled tool (`inspect_dart_aot`), and identical optional owned
context capture/discovery guidance. This requested alias does not certify the
server-resolved model. Temporary configuration points to the modified frozen
REA MCP; all copied credential profiles are removed and target bytes remain
unchanged. The source question is fixed; equal treatment instructions choose
detail and explain the summary carrier and parent reference.
| Trial | Input | Cached | Uncached | Output | Seconds | MCP attempts | Failed attempts | Facts |
| ---------------- | -----: | -----: | -------: | -----: | ------: | -----------: | --------------: | ----- |
| model-complete-1 | 74,641 | 50,176 | 24,465 | 2,765 | 116.898 | 3 | 1 | pass |
| model-summary-1 | 38,253 | 22,016 | 16,237 | 1,449 | 88.904 | 1 | 0 | pass |
| model-summary-2 | 38,363 | 22,016 | 16,347 | 1,515 | 55.190 | 1 | 0 | pass |
| model-complete-2 | 66,585 | 38,144 | 28,441 | 2,217 | 79.180 | 2 | 0 | pass |
Both pairs pass checks for every selected producer fact, exact nullable
observations, target digest, observed response Evidence IDs, original limitations,
artifact preservation and profile cleanup. The grader compares the reported
facts with producer observations; it is not an independent Dart deserializer.
Total input falls 48.75% / 42.38%, uncached input 33.63% / 42.52%, reported output
47.59% / 31.66%, and whole-task time 23.95% / 30.30%. Usage is the final aggregate
reported by the actual CLI, one usage record per trial; cached tokens are not
subtracted from total input. Reported output includes reasoning output (1,262,
321, 383 and 709 tokens respectively).
The first complete trial initially supplied `artifact_path` instead of the
advertised `path`, corrected it, then repeated a successful complete call.
The second complete trial made two successful calls. Both summary trials made
one successful call. All attempts, discovery and correction costs remain
included. The paired whole-task improvement therefore includes actual model
decisions and the convenience of explicit returned counts; it does not isolate
serialization as the sole cause or establish engine acceleration. Two pairs on
one app/question/model configuration support this scope, not a universal gain.
Payload CPU seconds / kernel peak bytes are respectively 12.556 / 876,937,216;
9.213 / 785,272,832; 9.108 / 793,939,968; and 13.164 / 803,209,216. Model payload
observations verify owned cleanup and zero phase limit events, with no guard
aborts. Whole-scope final counters remain unknown, so every strict all-selected
resource grade remains false even though the separate factual grade passes.
## Resource and verification scope
SDK and migration payload-exit observations verify owned cleanup and zero
observed memory/pids limit events, with peaks below 692 MB. Missing whole-scope
terminal counters remain unknown; the strict whole-resource grader stays false.
One-CPU quotas are experiment controls rather than CPU-efficiency claims.
Production-provider fixture tests cover explicit ABI, old complete defaults,
actual SDK-converted schemas, exact pages after APK removal, portable records,
nullable/partial facts, malformed inputs, target digest conflicts and early
refusals. CLI/MCP acceptance covers original report equality, duplicate-member
refusals and unsupported snapshot kinds. Fixture successes are distinguished
from the real LocalSend SDK/migration workflows. Other Flutter apps, architectures,
SDK layouts, typed class recovery and runtime behavior remain unverified here.
Raw replies, binaries, model traces and temporary credentials stay outside Git.
## Integration with the newer main
The rebased verification runtime is `63984ce0898bd7997e6d9b138e6d96d0a24f2b09`
on main `a2d8b94dd50dab8d966239dd63ab84718aa8b4b4`, with compiled file-table
SHA-256 `19c8ccdd3e99bb46a5c54f4edd8c6b2d2eb4f3a4870b3c129061d38145382b68`.
Targeted Flutter/SAZ/shared-view integration passes 35 tests; one opposite
runtime-capacity branch is skipped because this host enforces decoder memory.
Documentation checks and two README conformance tests pass.
Fresh real complete/summary SDK checks on this runtime match the earlier selected
facts and settings, with unchanged 151,341 / 12,037-byte AOT replies. Fifteen
old-Evidence migration calls again preserve every original string and record
with the source APK absent. The real one-shot CLI complete result also matches
the previous-runtime normalized report, subject, parameters and original ID.
An initial CLI-only controller check lacked an owned resource receipt; it is
retained and excluded from resource/performance claims, then repeated in the
bounded owned scope. These checks establish integration, rather than relabeling
the earlier paired model/SDK measurements as the rebased runtime.

View File

@ -0,0 +1,75 @@
# Latest-main efficiency continuation
Continues [#2016](https://github.com/morluto/rea/issues/2016). The 2026-10-13
fetch resolved main to `f254f5ac2b804b353a711264bb178f43fdf07867`, the same
revision as the preceding Android conclusion. Newer capabilities below were
added during that earlier cycle; they are additional measurement scopes, not
new commits after that conclusion.
| Capability | Existing controls | Next evidence needed |
| ----------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Flutter / Dart AOT (#1582) | Explicit ABI, bounded string observations, partial coverage | Real shipped snapshot/count question; opt-in delivery only if unrelated strings dominate |
| SQLite (#2271) | Compact inventory, selected table/columns, row limit | Real application database and model workflow before adding another selector |
| TShark (#2318) | Fields, filter, decode-as, pass, default 50 matching rows, paging, explicit null for all | Real public capture and repeated task cost; paging repeats dissection |
| sigrok (#2339) | Decoder/channels/annotation selection, default 50 display lines, paging | Real public capture; display-line count is not annotation count and paging repeats decoding |
| Retained unknown head results | Existing retained paging | Reuse the existing view rather than duplicate its implementation |
Attached IDA, Frida and other live runtime workflows involve separate authority
and lifecycle questions and are outside this static parsing continuation.
Plugin SDKs and cross-session engine caches remain deferred until an observed
need and benefit justify them.
## First selected scope: Dart AOT
The official LocalSend 1.18.2 arm64 APK is pinned in `corpus.json`. The initial
real public SDK baseline inspected actual bytes without executing/installing
the APK. Its complete default AOT reply was 151,341 compact client-result JSON
bytes (70,487 normalized bytes), with 500 package URIs, 216 Dart URIs and 2,000
class-like tokens. String-pool and overall coverage were partial. A second
explicit arm64 request returned the same normalized facts.
These are exploratory first/warm calls, not an optimization pair. The frozen
runtime source is `28c84e78cf2fbfde4aceed345b773c59eb889f68`, with production
source bytes verified equal to main f254 at baseline acquisition. Its compiled
file-table digest is
`5332dda13981f043e146b3601ebec1f380d9f698280ca53d75786b638a912e11`;
the receipt is not relabeled as a newer build.
The candidate adds explicit MCP `detail: "summary"` and retained pool pages.
Omitted/complete requests and the CLI preserve the old complete path. Analysis,
ABI selection and original Evidence stay intact. Summary retains target/libapp
identity, exact nullable snapshot observations, lower bounds, returned counts,
coverage and limitations. Pages preserve original string order and source
coverage without rescanning. No engine cache, provider protocol or third-party
plugin API is introduced.
Follow-up measurements must compare identical selected facts, portable old
Evidence and actual model tasks. Reply bytes do not establish token or latency
improvements. Whole-scope final counters remain unknown if their cgroup has
disappeared, even when the narrower payload-exit observation is verified.
Raw records, binaries and model traces stay in the private lab outside Git.
Heavy work remains serial under the unchanged one-CPU, 3 GiB early-stop / 4 GiB
hard memory, no-swap and host-availability guards.
## Main advanced during the experiment
Main subsequently advanced to `a2d8b94dd50dab8d966239dd63ab84718aa8b4b4`,
adding native Fiddler SAZ parsing. Its web-network capture contract already
offers opt-in summary and retained capture pages, archive-member coordinates
and exact exposed encoded bytes. It decodes all records before ordinal
selection and rejects malformed/oversized inputs without partial success.
Measure real public SAZ artifacts using these controls before adding another
API or changing decoding or credential-redaction semantics. SAZ performance
remains unverified in this Flutter continuation.
The Flutter cohorts keep their original frozen runtime; subsequent integration
onto this newer main does not relabel earlier measurement receipts.
Main advanced again to `bb154d106` during CI, adding retained aioquic 1.5.0
qlog 0.3 JSON parsing and qlog views. The existing capture summary/page path
already applies. Qlog's trace-scoped STREAM interval unions and endpoint loss
declarations have separate semantics: they do not recover bodies, prove network
loss or establish PCAP correlation. Measure genuine supported recordings and
existing retained views before adding delivery or caching. Other qlog layouts
and qlog performance remain unverified in the Flutter experiment. Shared-view
integration is checked after rebasing; earlier runtime receipts remain pinned.

View File

@ -726,6 +726,54 @@ const workflows = {
},
"sharex.dll": inspectManagedTarget,
"sharex.helpers.dll": inspectManagedTarget,
"localsend-arm64.apk": async () => {
const input = {
path: values.path,
...(values.delivery === "summary" ? { detail: "summary" } : {}),
};
const definition = catalogDefinitions.get("inspect_dart_aot");
if (
definition === undefined ||
!new Ajv2020({ strict: false, validateFormats: false }).validate(
definition.inputSchema,
input,
)
)
throw new Error(
"Selected Dart AOT delivery is not advertised by this runtime",
);
const inspected = await call("inspect_dart_aot", input);
const result = normalized(inspected);
if (
values.delivery === "summary" &&
(result.kind !== "summary" ||
result.parent_operation !== "inspect_dart_aot")
)
throw new Error("Runtime returned no retained Dart AOT summary");
const report = values.delivery === "summary" ? result.summary : result;
const { package_uris, dart_uris, class_like_tokens, ...pool } =
report.string_pool;
return {
artifact_sha256: targetSha(inspected),
target: report.target,
abi: report.abi,
libapp: report.libapp,
string_pool:
values.delivery === "summary"
? pool
: {
...pool,
returned_counts: {
package_uris: package_uris.length,
dart_uris: dart_uris.length,
class_like_tokens: class_like_tokens.length,
},
},
coverage: report.coverage,
limitations: inspected.limitations,
evidence_ids: [inspected.evidence_id],
};
},
"openwrt.bin": async () => {
const info = await call("inspect_firmware_regions", { path: values.path });
return {

View File

@ -0,0 +1 @@
Statically inspect the shipped Flutter APK at {{artifact_path}} using only the registered REA inspect_dart_aot tool. Use its default ABI selection. Report target identity, chosen ABI, every libapp ELF/snapshot field exactly as reported including null, original string-pool coverage, all three counts ending in _at_least, printable_run_count, and returned_counts for package_uris, dart_uris and class_like_tokens. Compute returned_counts from the returned arrays if needed. Preserve original overall coverage and producer limitations exactly, including order. Counts ending in _at_least are lower bounds and class-like tokens are byte-pattern observations, not recovered typed classes. Do not return individual strings or infer Dart SDK versions, class semantics, runtime behavior or complete inventory. Final answer must be JSON with artifact_sha256, target, abi, libapp, string_pool, coverage, limitations, evidence_ids. Use actual response Evidence IDs. Do not execute the APK, use shell or host file tools, access the network, or call other agents. Stop when these facts are supported.

View File

@ -69,7 +69,7 @@ def scope_with_verified_counts(value, expected_scope, returned_counts):
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'flutter-aot', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
parser.add_argument('--trial', type=Path, required=True)
parser.add_argument('--reference', type=Path)
parser.add_argument('--resource-log', type=Path, required=True)
@ -133,6 +133,9 @@ def main():
for key, expected in {'artifact_bytes': n['artifact']['bytes'], 'bytecode_sha256': n['bytecode']['sha256'], 'bytecode_bytes': n['bytecode']['bytes'], 'functions': n['functions'], 'evidence_kind': n['evidence_kind'], 'discovery_completeness': n['discovery_completeness'], 'runtime_execution': n['runtime_execution'], 'bytecode_kind': n['bytecode']['kind'], 'hardfork': n['bytecode']['hardfork'], 'deployment_authenticity': n['bytecode']['deployment_authenticity']}.items():
check(key, answer.get(key), expected)
check('evm_original_limitations', answer.get('limitations'), n['limitations'])
elif domain == 'flutter-aot':
for key in ['target', 'abi', 'libapp', 'string_pool', 'coverage', 'limitations']:
check(key, answer.get(key), reference[key])
elif domain == 'android':
for key in ['package_name', 'version_name', 'version_code', 'matches']:
check(key, answer.get(key), reference[key])

View File

@ -0,0 +1,40 @@
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { FlutterBuildAnalysisService } from "./FlutterBuildAnalysisService.js";
import {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
type AnalysisViewRetention,
} from "../analysisView/AnalysisViewService.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { dartAotInspectionInputSchema } from "../../domain/flutter/flutterBuildAnalysis.js";
import type { Evidence } from "../../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, type Result } from "../../domain/result.js";
/** Keep the caller's ABI choice and complete analysis; select only its delivery. */
export const inspectDartAot = async (
service: FlutterBuildAnalysisService,
rawInput: unknown,
retention: AnalysisViewRetention,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> => {
const operation = "inspect_dart_aot";
if (options?.signal?.aborted === true)
return err(new AnalysisCancelledError(operation));
const parsed = dartAotInspectionInputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput, {
cause: parsed.error,
}),
);
if (parsed.data.detail === "summary") {
const ready = requireAnalysisViewRetention(operation, retention);
if (!ready.ok) return ready;
}
const { detail, ...providerInput } = parsed.data;
const executed = await service.execute(operation, providerInput, options);
if (!executed.ok || detail !== "summary") return executed;
return summarizeRetainedAnalysis(executed.value, retention);
};

View File

@ -16,7 +16,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_evm_interface, inspect_managed_artifact, inspect_android_package or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. For layout, JavaScript, native, inventory, WASM, EVM, managed artifact and Android package sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions; occurrence logical_paths matches exact recorded strings (omitted means all available, [] means none). source_complete distinguishes complete inventories from partial source records; page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.',
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_evm_interface, inspect_managed_artifact, inspect_android_package, inspect_dart_aot or inspect_web_network_capture Evidence without repeating analysis. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. For layout, JavaScript, native, inventory, WASM, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions; occurrence logical_paths matches exact recorded strings (omitted means all available, [] means none). source_complete distinguishes complete inventories from partial source records; page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.',
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [

View File

@ -1,7 +1,10 @@
import { z } from "zod";
import {
flutterInputSchemas,
dartAotInspectionInputSchema,
flutterResultSchemas,
} from "../../domain/flutter/flutterBuildAnalysis.js";
import { dartAotSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
import type { ToolContract } from "../toolContractTypes.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
import { toolContractMetadata } from "../toolEffects.js";
@ -27,10 +30,15 @@ export const FLUTTER_TOOL_CONTRACTS = [
name: "inspect_dart_aot",
...toolContractMetadata("inspect_dart_aot"),
description:
"Inspect one ABI's Dart AOT snapshot inside an APK: the four snapshot symbols located through the ELF dynamic symbol table with their offsets, sizes, and section headers (magic, kind, length), the snapshot hash, and a bounded string-pool projection from the isolate data section (package: source URIs, dart: SDK URIs, identifier-like tokens) exposing the app's dependency structure. String-pool entries are byte-pattern observations, not deserialized cluster semantics; typed class and function recovery needs an SDK-specific parser. No engine, no execution.",
"Inspect one ABI's Dart AOT snapshot inside an APK: the four snapshot symbols located through the ELF dynamic symbol table with their offsets, sizes, and section headers (magic, kind, length), the snapshot hash, and a bounded string-pool projection from the isolate data section (package: source URIs, dart: SDK URIs, identifier-like tokens) exposing the app's dependency structure. String-pool entries are byte-pattern observations, not deserialized cluster semantics; typed class and function recovery needs an SDK-specific parser. Omitted or complete detail returns the full observation. Select detail: \"summary\" for target/ABI/library identity, all snapshot headers, coverage and original lower-bound counts without string lists. The complete source stays retained; read returned strings via inspect_analysis_view with parent_evidence_id and kind dart-aot, collection package_uris/dart_uris/class_like_tokens, offset and limit. Exhausting pages does not imply complete pool coverage. No engine, no execution.",
kind: "flutter-provider",
inputSchema: flutterInputSchemas.inspect_dart_aot,
outputSchema: evidenceResultOf(flutterResultSchemas.inspect_dart_aot),
inputSchema: dartAotInspectionInputSchema,
outputSchema: evidenceResultOf(
z.union([
flutterResultSchemas.inspect_dart_aot,
dartAotSummaryViewResultSchema,
]),
),
examples: [
{
title: "Inspect the preferred ABI's AOT snapshot",
@ -40,6 +48,10 @@ export const FLUTTER_TOOL_CONTRACTS = [
title: "Inspect one ABI explicitly",
input: { path: "/targets/Example.apk", abi: "arm64-v8a" },
},
{
title: "Inspect snapshot identity with retained string lists",
input: { path: "/targets/Example.apk", detail: "summary" },
},
],
},
] as const satisfies readonly ToolContract[];

View File

@ -73,6 +73,12 @@ import {
androidViewRequestSchema,
androidManifestViewItemSchema,
} from "./androidPackageView.js";
import {
projectDartAotView,
dartAotSummarySchema,
dartAotViewRequestSchema,
dartAotViewContextSchema,
} from "./dartAotView.js";
/** Public name of the selected-view workflow. */
export const INSPECT_ANALYSIS_VIEW_OPERATION = "inspect_analysis_view" as const;
@ -190,6 +196,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
apiSurfaceViewSchema,
captureViewRequestSchema,
qlogViewRequestSchema,
dartAotViewRequestSchema,
]);
// The application owns admission and authenticated Evidence parsing. SDK and
@ -305,6 +312,7 @@ const viewResultBase = {
"inspect_managed_artifact",
"inspect_web_network_capture",
"inspect_android_package",
"inspect_dart_aot",
]),
parent_digest: digestSchema,
view_digest: digestSchema,
@ -376,6 +384,13 @@ export const androidPackageSummaryViewResultSchema = z.strictObject({
summary: androidPackageSummarySchema,
});
/** Exact snapshot and pool-coverage carrier for selected AOT delivery. */
export const dartAotSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_dart_aot"),
summary: dartAotSummarySchema,
});
/** Projected facts for one selected view of retained analysis Evidence. */
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
z.strictObject({
@ -400,6 +415,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
managedSummarySchema,
captureSummarySchema,
androidPackageSummarySchema,
dartAotSummarySchema,
]),
}),
z.strictObject({
@ -470,6 +486,14 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
...viewResultBase,
parent_operation: z.literal("inspect_android_package"),
}),
z.strictObject({
kind: z.literal("dart-aot"),
view: dartAotViewRequestSchema,
items: z.array(z.string()),
...viewResultBase,
...dartAotViewContextSchema.shape,
parent_operation: z.literal("inspect_dart_aot"),
}),
z.strictObject({
kind: z.literal("inventory"),
view: inventoryViewSchema,
@ -549,6 +573,10 @@ export type UnsignedAnalysisView =
| Omit<
Extract<AnalysisViewResult, { readonly kind: "android" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "dart-aot" }>,
"view_digest"
>;
/** Attach the content digest that must change when projected bytes change. */
@ -594,7 +622,7 @@ const unsupportedParent = (
new AnalysisUnsupportedTargetError(
INSPECT_ANALYSIS_VIEW_OPERATION,
parentArtifactPath(parent),
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, EVM, managed artifact, Android package, or web network capture Evidence.`,
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, EVM, managed artifact, Android package, Dart AOT, or web network capture Evidence.`,
);
const parentArtifactPath = (parent: AnalysisViewParent): string => {
@ -624,6 +652,7 @@ const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
["inspect_evm_interface", projectEvmInterfaceView],
["inspect_managed_artifact", projectManagedArtifactView],
["inspect_android_package", projectAndroidPackageView],
["inspect_dart_aot", projectDartAotView],
["inspect_artifact", projectArtifactInventoryView],
["inventory_artifact", projectArtifactInventoryView],
]);

View File

@ -0,0 +1,123 @@
import { z } from "zod";
import type { AnalysisError } from "../analysisErrorBase.js";
import { flutterResultSchemas } from "../flutter/flutterBuildAnalysis.js";
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { err, ok, type Result } from "../result.js";
import {
analysisViewInputError,
type AnalysisViewParent,
type AnalysisViewRequest,
type UnsignedAnalysisView,
} from "./analysisView.js";
import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
const aotSchema = flutterResultSchemas.inspect_dart_aot;
/** Snapshot identity and original coverage, without unrelated string inventories. */
export const dartAotSummarySchema = aotSchema.extend({
string_pool: aotSchema.shape.string_pool
.omit({ package_uris: true, dart_uris: true, class_like_tokens: true })
.extend({
returned_counts: z.strictObject({
package_uris: z.number().int().nonnegative(),
dart_uris: z.number().int().nonnegative(),
class_like_tokens: z.number().int().nonnegative(),
}),
}),
});
/** Pages of the producer-returned strings, preserving original order and scope. */
export const dartAotViewRequestSchema = z.strictObject({
kind: z.literal("dart-aot"),
collection: z.enum(["package_uris", "dart_uris", "class_like_tokens"]),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
});
export const dartAotViewContextSchema = z.strictObject({
abi: aotSchema.shape.abi,
libapp: aotSchema.shape.libapp,
source_coverage: aotSchema.shape.coverage,
string_pool: dartAotSummarySchema.shape.string_pool,
});
/** Reuse authenticated AOT Evidence without opening the APK or rescanning its ELF. */
export const projectDartAotView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (view.kind !== "summary" && view.kind !== "dart-aot")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message: "Dart AOT Evidence supports summary and string-pool pages.",
},
]),
);
const parsed = aotSchema.safeParse(parent.normalizedResult);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
"inspect_analysis_view",
parsed.error.issues.map((issue) => ({
...issue,
path: ["source", "normalized_result", ...issue.path],
})),
{ source: { normalized_result: parent.normalizedResult } },
{ cause: parsed.error },
),
);
const report = parsed.data;
const { package_uris, dart_uris, class_like_tokens, ...pool } =
report.string_pool;
const string_pool = {
...pool,
returned_counts: {
package_uris: package_uris.length,
dart_uris: dart_uris.length,
class_like_tokens: class_like_tokens.length,
},
};
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_dart_aot" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: { path: report.target.path, sha256: report.target.sha256 },
limitations: [...parent.limitations],
unknowns: [
"String-pool entries are byte-pattern observations, not recovered typed classes or functions.",
"Counts ending in _at_least remain lower bounds; exhausting returned strings does not establish a complete snapshot inventory.",
...(parent.subject == null
? [
"The parent has no recorded subject; its normalized target identity cannot be cross-checked against a subject digest.",
]
: []),
],
};
if (view.kind === "summary")
return ok({
kind: "summary",
view,
...shared,
summary: { ...report, string_pool },
coverage: completeWithinViewCoverage(1, 1),
});
const collection = report.string_pool[view.collection];
const items = collection.slice(view.offset, view.offset + view.limit);
return ok({
kind: "dart-aot",
view,
...shared,
items,
abi: report.abi,
libapp: report.libapp,
source_coverage: report.coverage,
string_pool,
coverage: pageViewCoverage(view.offset, items.length, collection.length),
});
};

View File

@ -30,6 +30,12 @@ export const flutterInputSchemas = {
}),
} as const;
/** Retained delivery is selected independently of the provider's ABI request. */
export const dartAotInspectionInputSchema =
flutterInputSchemas.inspect_dart_aot.extend({
detail: z.enum(["complete", "summary"]).optional(),
});
/** Supported Flutter-backed operations. */
export type FlutterOperation = keyof typeof flutterInputSchemas;
/** Validated Flutter requests carry their selected operation. */

View File

@ -444,6 +444,7 @@ export const createServer = (
toolLogger,
recordEvidence,
analysisAdmission,
evidenceById,
);
registerApktoolTools(
server,

View File

@ -1,10 +1,15 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { FlutterBuildAnalysisService } from "../application/flutter/FlutterBuildAnalysisService.js";
import { inspectDartAot } from "../application/flutter/DartAotInspection.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import type { Logger } from "pino";
import { createContractToolHandler } from "./contractToolHandler.js";
import {
createContractToolHandler,
type ContractToolService,
} from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind Flutter handlers to their exact named contracts through admission. */
@ -14,6 +19,7 @@ export const registerFlutterTools = (
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
evidenceById?: EvidenceLookup,
): void => {
const handler = createContractToolHandler(
server,
@ -22,14 +28,23 @@ export const registerFlutterTools = (
recordEvidence,
withAdmittedAnalysis,
);
for (const contract of [
toolContract("identify_flutter_build"),
toolContract("inspect_dart_aot"),
] as const) {
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
}
const identify = toolContract("identify_flutter_build");
server.registerTool(
identify.name,
toolRegistrationOptions(identify),
handler(identify),
);
const aotService: ContractToolService<"inspect_dart_aot"> = {
execute: (_operation, input, options) =>
inspectDartAot(service, input, { recordEvidence, evidenceById }, options),
};
const aotHandler = createContractToolHandler(
server,
aotService,
logger,
recordEvidence,
withAdmittedAnalysis,
);
const aot = toolContract("inspect_dart_aot");
server.registerTool(aot.name, toolRegistrationOptions(aot), aotHandler(aot));
};

View File

@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { flutterResultSchemas } from "../../src/domain/flutter/flutterBuildAnalysis.js";
import {
aotFixtureImage,
engineFixtureImage,
@ -59,6 +60,9 @@ cliTest(
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract(name).outputSchema.parse(result.json);
const complete = flutterResultSchemas[name].parse(
evidence.normalized_result,
);
const response = await call(name, {
path,
...(abi === undefined ? {} : { abi }),
@ -70,7 +74,7 @@ cliTest(
expect(mcp.subject).toMatchObject({
local_path: path,
format: "apk",
digest: { sha256: evidence.normalized_result.target.sha256 },
digest: { sha256: complete.target.sha256 },
});
expect(mcp.evidence_id).toBe(evidence.evidence_id);
if (name === "inspect_dart_aot") {
@ -134,6 +138,9 @@ cliTest(
const evidence = toolContract("inspect_dart_aot").outputSchema.parse(
result.json,
);
const complete = flutterResultSchemas.inspect_dart_aot.parse(
evidence.normalized_result,
);
const { call } = await connectLocalToolsMcp();
const response = await call("inspect_dart_aot", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
@ -146,7 +153,7 @@ cliTest(
string_pool: { coverage: "complete" },
});
expect(
evidence.normalized_result.libapp.sections
complete.libapp.sections
.filter((section) => section.name.endsWith("Data"))
.map((section) => section.kind),
).toEqual([4, 4]);

View File

@ -0,0 +1,285 @@
import { unlink } from "node:fs/promises";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished, vi } from "vitest";
import { FlutterBuildAnalysisService } from "../../../src/application/flutter/FlutterBuildAnalysisService.js";
import { inspectDartAot } from "../../../src/application/flutter/DartAotInspection.js";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import { createFlutterBuildAnalysisProvider } from "../../../src/composition/flutter.js";
import {
analysisViewResultSchema,
dartAotSummaryViewResultSchema,
} from "../../../src/domain/analysisView/analysisView.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { flutterResultSchemas } from "../../../src/domain/flutter/flutterBuildAnalysis.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { aotFixtureImage } from "../../../src/flutter/FlutterBuild.fixture.js";
import { createFlutterApk } from "../../fixtures/flutterApk.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
it("honors explicit ABI and old full requests, validates SDK schemas and reuses complete Evidence after APK removal", async () => {
const path = await createFlutterApk("views.apk", [
{ path: "lib/arm64-v8a/libapp.so", bytes: aotFixtureImage() },
{ path: "lib/x86_64/libapp.so", bytes: aotFixtureImage("b".repeat(32)) },
]);
const provider = createFlutterBuildAnalysisProvider();
const execute = vi.spyOn(provider, "execute");
const harness = await createApplicationMcpHarness({
serverOptions: { flutterAnalysis: provider },
});
onTestFinished(harness.close);
const { client, session } = harness;
const catalog = (await client.listTools()).tools;
const definition = catalog.find((tool) => tool.name === "inspect_dart_aot");
if (definition?.outputSchema === undefined)
throw new Error("Missing AOT output schema");
const outputSchema: Record<string, unknown> = definition.outputSchema;
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
expect(
ajv.validate(
catalog.find((tool) => tool.name === "identify_flutter_build")!
.inputSchema,
{ path, detail: "summary" },
),
).toBe(false);
const reply = await client.callTool({
name: "inspect_dart_aot",
arguments: { path, abi: "x86_64", detail: "summary" },
});
expect(reply.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, reply.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const receipt = parseEvidence(reply.structuredContent);
const view = dartAotSummaryViewResultSchema.parse(receipt.normalized_result);
const parent = session.evidenceById(view.parent_evidence_id);
if (parent === undefined)
throw new Error("Complete AOT source was not retained");
const report = flutterResultSchemas.inspect_dart_aot.parse(
parent.normalized_result,
);
expect(report.abi).toBe("x86_64");
expect(report.libapp.snapshot_hash).toBe("b".repeat(32));
expect(parent.parameters).toEqual({ path, abi: "x86_64" });
expect(execute).toHaveBeenLastCalledWith(
{ operation: "inspect_dart_aot", input: { path, abi: "x86_64" } },
expect.anything(),
);
expect(view.summary.libapp).toEqual(report.libapp);
expect(view.summary.target).toEqual(report.target);
expect(view.summary.coverage).toBe(report.coverage);
expect(receipt.evidence_id).not.toBe(parent.evidence_id);
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
for (const detail of [undefined, "complete"]) {
const complete = parseEvidence(
(
await client.callTool({
name: "inspect_dart_aot",
arguments: {
path,
abi: "x86_64",
...(detail === undefined ? {} : { detail }),
},
})
).structuredContent,
);
expect(complete.operation).toBe("inspect_dart_aot");
expect(complete.normalized_result).toEqual(parent.normalized_result);
expect(complete.parameters).toEqual(parent.parameters);
}
const preferred = parseEvidence(
(await client.callTool({ name: "inspect_dart_aot", arguments: { path } }))
.structuredContent,
);
expect(
flutterResultSchemas.inspect_dart_aot.parse(preferred.normalized_result)
.abi,
).toBe("arm64-v8a");
const original = JSON.stringify(parent);
await unlink(path);
const calls = execute.mock.calls.length;
for (const collection of [
"package_uris",
"dart_uris",
"class_like_tokens",
] as const) {
const strings: string[] = [];
let offset = 0;
do {
const reply = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
},
view: { kind: "dart-aot", collection, offset, limit: 1 },
},
});
expect(reply.isError).not.toBe(true);
const page = analysisViewResultSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
if (page.kind !== "dart-aot") throw new Error("Wrong pool page kind");
strings.push(...page.items);
expect(page.libapp).toEqual(report.libapp);
expect(page.source_coverage).toBe(report.coverage);
expect(page.string_pool.coverage).toBe(report.string_pool.coverage);
offset =
page.coverage.next_offset ?? report.string_pool[collection].length;
} while (offset < report.string_pool[collection].length);
expect(strings).toEqual(report.string_pool[collection]);
}
expect(execute).toHaveBeenCalledTimes(calls);
expect(JSON.stringify(session.evidenceById(parent.evidence_id))).toBe(
original,
);
expect(
session
.exportEvidenceBundle()
.records.find((record) => record.evidence_id === parent.evidence_id),
).toEqual(parent);
const portable = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
});
if (!portable.ok) throw portable.error;
expect(portable.value.normalized_result).toEqual(view);
}, 30000);
it("preserves null snapshot facts and pool lower bounds after exhaustion, rejecting malformed and conflicting identities", () => {
const report = {
target: { path: "/source.apk", bytes: 10, sha256: "a".repeat(64) },
abi: "arm64-v8a",
libapp: {
coverage: "partial",
bytes: 1,
sha256: "b".repeat(64),
snapshot_hash: null,
snapshot_hash_candidates: [],
snapshot_hash_sources: 0,
sections: [],
},
string_pool: {
coverage: "partial",
package_uris: ["package:opaque/a.dart"],
package_uri_count_at_least: 1000,
dart_uris: [],
dart_uri_count_at_least: 0,
class_like_tokens: ["incidental"],
class_like_token_count_at_least: 4000,
printable_run_count: 12,
},
coverage: "partial",
};
const evidence = (
result: JsonValue = report,
sha: string | null = "a".repeat(64),
) =>
createEvidence(
sha === null
? undefined
: { path: "/source.apk", sha256: sha, format: "apk" },
{ id: "flutter", name: "Flutter", version: null },
{
operation: "inspect_dart_aot",
parameters: {},
result,
limitations: ["Original uncertainty"],
},
);
for (const parent of [evidence(), evidence(report, null)]) {
const summary = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
});
if (!summary.ok) throw summary.error;
const result = dartAotSummaryViewResultSchema.parse(
summary.value.normalized_result,
);
expect(result.summary.libapp.snapshot_hash).toBeNull();
expect(result.summary.string_pool.package_uri_count_at_least).toBe(1000);
expect(result.summary.string_pool.returned_counts.package_uris).toBe(1);
expect(result.limitations).toEqual(parent.limitations);
const page = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: {
kind: "dart-aot",
collection: "package_uris",
offset: 0,
limit: 10,
},
});
if (!page.ok) throw page.error;
expect(page.value.normalized_result).toMatchObject({
kind: "dart-aot",
items: report.string_pool.package_uris,
source_coverage: "partial",
string_pool: { coverage: "partial", package_uri_count_at_least: 1000 },
coverage: { total: 1, exhausted: true },
});
}
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: evidence(report, "c".repeat(64)) },
view: { kind: "summary" },
}),
).toMatchObject({ ok: false, error: { _tag: "EvidenceIntegrityError" } });
expect(
inspectAnalysisView({
source: {
kind: "inline",
evidence: evidence({
...report,
string_pool: {
...report.string_pool,
package_uri_count_at_least: -1,
},
}),
},
view: { kind: "summary" },
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: evidence() },
view: { kind: "android", facet: "manifest", offset: 0, limit: 1 },
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
});
it("rejects unavailable retention, invalid delivery and cancellation before producer work", async () => {
const provider = createFlutterBuildAnalysisProvider();
onTestFinished(() => provider.close());
const execute = vi.spyOn(provider, "execute");
const service = new FlutterBuildAnalysisService(provider);
const retention = { recordEvidence: undefined, evidenceById: undefined };
expect(
await inspectDartAot(
service,
{ path: "/missing.apk", abi: "x86_64", detail: "summary" },
retention,
),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisCapabilityUnavailableError" },
});
expect(
await inspectDartAot(
service,
{ path: "/missing.apk", detail: "other" },
retention,
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
expect(
await inspectDartAot(
service,
{ path: "/missing.apk", detail: "summary" },
retention,
{ signal: AbortSignal.abort() },
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
expect(execute).not.toHaveBeenCalled();
});