2026-10-14 14:46:39 +08:00
|
|
|
import { analysisViewInputError } from "./analysisViewErrors.js";
|
2026-10-12 21:17:52 +08:00
|
|
|
import { analysisTextPage } from "./analysisViewText.js";
|
2026-10-10 12:18:50 +08:00
|
|
|
import type { AnalysisError } from "../analysisErrorBase.js";
|
|
|
|
|
import { uniqueSorted } from "../canonicalOrdering.js";
|
2026-10-27 06:43:17 +08:00
|
|
|
import type { FunctionDossier } from "../native/functionDossier.js";
|
2026-10-10 12:18:50 +08:00
|
|
|
import { jsonValueSchema } from "../jsonValue.js";
|
|
|
|
|
import { err, ok, type Result } from "../result.js";
|
2026-10-14 14:46:39 +08:00
|
|
|
import type {
|
|
|
|
|
AnalysisViewParent,
|
|
|
|
|
AnalysisViewRequest,
|
|
|
|
|
UnsignedAnalysisView,
|
2026-10-10 12:18:50 +08:00
|
|
|
} from "./analysisView.js";
|
|
|
|
|
import {
|
|
|
|
|
completeWithinViewCoverage,
|
|
|
|
|
pageViewCoverage,
|
|
|
|
|
} from "./analysisViewCoverage.js";
|
|
|
|
|
|
|
|
|
|
type Artifact = { readonly path: string; readonly sha256: string };
|
|
|
|
|
|
|
|
|
|
const viewError = (message: string): AnalysisError =>
|
|
|
|
|
analysisViewInputError([
|
|
|
|
|
{ path: ["view"], reason: "invalid_value", message },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const parentFields = (
|
|
|
|
|
parent: AnalysisViewParent,
|
|
|
|
|
dossier: FunctionDossier,
|
|
|
|
|
artifact: Artifact,
|
|
|
|
|
) => {
|
|
|
|
|
const flow = dossier.native_value_flow;
|
|
|
|
|
const api = dossier.native_api;
|
|
|
|
|
const unknowns: string[] = [];
|
|
|
|
|
if (flow === null) unknowns.push("native_value_flow was not recorded");
|
|
|
|
|
else if (!flow.available)
|
|
|
|
|
unknowns.push("native_value_flow unavailable: " + flow.reason);
|
|
|
|
|
else if (flow.truncated)
|
|
|
|
|
unknowns.push(
|
|
|
|
|
"native_value_flow is truncated; omitted observations remain unknown",
|
|
|
|
|
);
|
|
|
|
|
if (api === null) unknowns.push("native_api was not recorded");
|
|
|
|
|
else if (!api.available)
|
|
|
|
|
unknowns.push(
|
|
|
|
|
"native_api unavailable: " + api.reason,
|
|
|
|
|
...api.residual_unknowns,
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
parent_evidence_id: parent.evidenceId,
|
|
|
|
|
parent_operation: "analyze_function" as const,
|
|
|
|
|
parent_digest: parent.evidenceId.slice(3),
|
|
|
|
|
procedure_address: dossier.procedure.address,
|
|
|
|
|
artifact,
|
|
|
|
|
limitations: uniqueSorted([
|
|
|
|
|
...parent.limitations,
|
|
|
|
|
...dossier.limitations,
|
|
|
|
|
...(flow?.limitations ?? []),
|
|
|
|
|
...(api?.available ? api.limitations : []),
|
|
|
|
|
]),
|
|
|
|
|
unknowns: uniqueSorted(unknowns),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const nativeRows = (
|
|
|
|
|
dossier: FunctionDossier,
|
|
|
|
|
facet: Exclude<
|
|
|
|
|
Extract<AnalysisViewRequest, { readonly kind: "native" }>["facet"],
|
2026-10-10 01:31:15 -07:00
|
|
|
"procedure" | "pseudocode" | "native_api" | "value_flow_summary"
|
2026-10-10 12:18:50 +08:00
|
|
|
>,
|
2026-10-10 01:31:15 -07:00
|
|
|
): readonly unknown[] | undefined => {
|
2026-10-10 12:18:50 +08:00
|
|
|
const sourceFlow = dossier.native_value_flow;
|
|
|
|
|
const flow = sourceFlow?.available ? sourceFlow : undefined;
|
|
|
|
|
return {
|
|
|
|
|
assembly: dossier.assembly,
|
2026-10-10 01:31:15 -07:00
|
|
|
basic_blocks: dossier.basic_blocks,
|
|
|
|
|
comments: dossier.comments,
|
2026-10-10 12:18:50 +08:00
|
|
|
callers: dossier.callers,
|
|
|
|
|
callees: dossier.callees,
|
|
|
|
|
incoming_references: dossier.incoming_references,
|
|
|
|
|
outgoing_references: dossier.outgoing_references,
|
2026-10-10 01:31:15 -07:00
|
|
|
unresolved_calls: dossier.unresolved_calls,
|
|
|
|
|
referenced_strings: dossier.referenced_strings,
|
|
|
|
|
referenced_names: dossier.referenced_names,
|
2026-10-13 13:41:10 +01:00
|
|
|
swift_string_literals: dossier.swift_string_literals,
|
|
|
|
|
swift_string_literal_omissions: dossier.swift_string_literal_omissions,
|
2026-10-10 12:18:50 +08:00
|
|
|
value_flow_operations: flow?.operations,
|
|
|
|
|
value_flow_def_use: flow?.def_use,
|
|
|
|
|
value_flow_effects: flow?.effects,
|
|
|
|
|
value_flow_parameters: flow?.parameters,
|
|
|
|
|
value_flow_parameter_uses: flow?.parameter_uses,
|
|
|
|
|
}[facet];
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-23 19:18:53 -05:00
|
|
|
type FacetAvailability = NonNullable<FunctionDossier["facet_availability"]>;
|
|
|
|
|
type FacetAvailabilityKey = keyof FacetAvailability;
|
|
|
|
|
|
|
|
|
|
/** Row facets whose provider availability is declared under another key. */
|
|
|
|
|
const facetAvailabilityKeys: Readonly<
|
|
|
|
|
Partial<Record<string, FacetAvailabilityKey>>
|
|
|
|
|
> = {
|
|
|
|
|
incoming_references: "references",
|
|
|
|
|
outgoing_references: "references",
|
|
|
|
|
referenced_strings: "strings_names",
|
|
|
|
|
referenced_names: "strings_names",
|
|
|
|
|
basic_blocks: "cfg",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const facetAvailability = (
|
|
|
|
|
dossier: FunctionDossier,
|
|
|
|
|
facet: string,
|
|
|
|
|
): FacetAvailability[FacetAvailabilityKey] | undefined =>
|
|
|
|
|
dossier.facet_availability?.[
|
|
|
|
|
facetAvailabilityKeys[facet] ?? (facet as FacetAvailabilityKey)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** Reasons the provider declared row facets unavailable; empty rows there are not observations. */
|
|
|
|
|
const unavailableFacetUnknowns = (dossier: FunctionDossier): string[] => {
|
|
|
|
|
const availability = dossier.facet_availability;
|
|
|
|
|
if (availability === undefined) return [];
|
|
|
|
|
return Object.entries(availability).flatMap(([facet, status]) =>
|
|
|
|
|
status.available ? [] : [`${facet} unavailable: ${status.reason}`],
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Optional dossier records; the native facet path reports them "not recorded" when omitted. */
|
|
|
|
|
const optionalFacets = [
|
|
|
|
|
"unresolved_calls",
|
|
|
|
|
"swift_string_literals",
|
|
|
|
|
"swift_string_literal_omissions",
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
/** Omitted optional records are unknown, not empty, so the summary names them. */
|
|
|
|
|
const unrecordedFacetUnknowns = (dossier: FunctionDossier): string[] =>
|
|
|
|
|
optionalFacets.flatMap((facet) =>
|
|
|
|
|
dossier[facet] === undefined ? [`${facet} not recorded`] : [],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** Compact dossier summary: identity, pseudocode size, facet counts and value-flow availability. */
|
|
|
|
|
const summarizeDossier = (dossier: FunctionDossier) => {
|
|
|
|
|
const sourceFlow = dossier.native_value_flow;
|
|
|
|
|
const flow = sourceFlow?.available ? sourceFlow : undefined;
|
|
|
|
|
// A count is unknown, not zero, when the provider declared its facet unavailable.
|
|
|
|
|
const count = (facet: string, rows: readonly unknown[]): number | null =>
|
|
|
|
|
facetAvailability(dossier, facet)?.available === false ? null : rows.length;
|
|
|
|
|
// An omitted optional record is unknown, not zero.
|
|
|
|
|
const recorded = (rows: readonly unknown[] | undefined): number | null =>
|
|
|
|
|
rows === undefined ? null : rows.length;
|
|
|
|
|
return {
|
|
|
|
|
procedure: {
|
|
|
|
|
name: dossier.procedure.name,
|
|
|
|
|
address: dossier.procedure.address,
|
|
|
|
|
},
|
|
|
|
|
pseudocode: {
|
|
|
|
|
available:
|
|
|
|
|
dossier.facet_availability?.pseudocode?.available ??
|
|
|
|
|
dossier.pseudocode.length > 0,
|
|
|
|
|
code_units: dossier.pseudocode.length,
|
|
|
|
|
},
|
|
|
|
|
value_flow: {
|
|
|
|
|
available: flow !== undefined,
|
|
|
|
|
truncated: flow?.truncated ?? false,
|
|
|
|
|
},
|
|
|
|
|
facet_availability: dossier.facet_availability ?? null,
|
|
|
|
|
counts: {
|
|
|
|
|
assembly: count("assembly", dossier.assembly),
|
|
|
|
|
basic_blocks: count("basic_blocks", dossier.basic_blocks),
|
|
|
|
|
comments: count("comments", dossier.comments),
|
|
|
|
|
callers: count("callers", dossier.callers),
|
|
|
|
|
callees: count("callees", dossier.callees),
|
|
|
|
|
incoming_references: count(
|
|
|
|
|
"incoming_references",
|
|
|
|
|
dossier.incoming_references,
|
|
|
|
|
),
|
|
|
|
|
outgoing_references: count(
|
|
|
|
|
"outgoing_references",
|
|
|
|
|
dossier.outgoing_references,
|
|
|
|
|
),
|
|
|
|
|
unresolved_calls: recorded(dossier.unresolved_calls),
|
|
|
|
|
referenced_strings: count(
|
|
|
|
|
"referenced_strings",
|
|
|
|
|
dossier.referenced_strings,
|
|
|
|
|
),
|
|
|
|
|
referenced_names: count("referenced_names", dossier.referenced_names),
|
|
|
|
|
swift_string_literals: recorded(dossier.swift_string_literals),
|
|
|
|
|
swift_string_literal_omissions: recorded(
|
|
|
|
|
dossier.swift_string_literal_omissions,
|
|
|
|
|
),
|
|
|
|
|
// Value-flow rows are unknown, not zero, when value flow was not recorded
|
|
|
|
|
// or the provider declared it unavailable; parentFields names which.
|
|
|
|
|
value_flow_operations: flow?.operations.length ?? null,
|
|
|
|
|
value_flow_def_use: flow?.def_use.length ?? null,
|
|
|
|
|
value_flow_effects: flow?.effects.length ?? null,
|
|
|
|
|
value_flow_parameters: flow?.parameters.length ?? null,
|
|
|
|
|
value_flow_parameter_uses: flow?.parameter_uses.length ?? null,
|
|
|
|
|
},
|
|
|
|
|
limitation_count: dossier.limitations.length,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const summaryView = (
|
|
|
|
|
view: Extract<AnalysisViewRequest, { readonly kind: "summary" }>,
|
|
|
|
|
dossier: FunctionDossier,
|
|
|
|
|
common: ReturnType<typeof parentFields>,
|
|
|
|
|
): UnsignedAnalysisView => ({
|
|
|
|
|
kind: "summary",
|
|
|
|
|
view,
|
|
|
|
|
summary: summarizeDossier(dossier),
|
|
|
|
|
coverage: completeWithinViewCoverage(1, 1),
|
|
|
|
|
...common,
|
|
|
|
|
unknowns: uniqueSorted([
|
|
|
|
|
...common.unknowns,
|
|
|
|
|
...unavailableFacetUnknowns(dossier),
|
|
|
|
|
...unrecordedFacetUnknowns(dossier),
|
|
|
|
|
]),
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-10 12:18:50 +08:00
|
|
|
/** Project bounded native function facets from complete retained Evidence without reanalysis. */
|
|
|
|
|
export const projectNativeFunctionView = (
|
|
|
|
|
parent: AnalysisViewParent,
|
|
|
|
|
dossier: FunctionDossier,
|
|
|
|
|
view: AnalysisViewRequest,
|
|
|
|
|
): Result<UnsignedAnalysisView, AnalysisError> => {
|
2026-10-23 19:18:53 -05:00
|
|
|
if (view.kind !== "native" && view.kind !== "summary")
|
|
|
|
|
return err(
|
|
|
|
|
viewError("analyze_function Evidence requires a native or summary view."),
|
|
|
|
|
);
|
2026-10-10 12:18:50 +08:00
|
|
|
const artifact = parent.artifact;
|
|
|
|
|
if (!artifact)
|
|
|
|
|
return err(
|
|
|
|
|
analysisViewInputError([
|
|
|
|
|
{
|
|
|
|
|
path: ["source", "subject"],
|
|
|
|
|
reason: "invalid_value",
|
|
|
|
|
message:
|
|
|
|
|
"Native function Evidence requires an artifact-bound subject.",
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
const common = parentFields(parent, dossier, artifact);
|
2026-10-23 19:18:53 -05:00
|
|
|
if (view.kind === "summary") return ok(summaryView(view, dossier, common));
|
|
|
|
|
const availability = facetAvailability(dossier, view.facet);
|
2026-10-13 22:15:01 +08:00
|
|
|
if (availability?.available === false)
|
|
|
|
|
common.unknowns = uniqueSorted([...common.unknowns, availability.reason]);
|
2026-10-10 12:18:50 +08:00
|
|
|
const singleton = (item: unknown): UnsignedAnalysisView => ({
|
|
|
|
|
kind: "native",
|
|
|
|
|
view,
|
|
|
|
|
item: jsonValueSchema.parse(item),
|
|
|
|
|
coverage: completeWithinViewCoverage(1, 1),
|
|
|
|
|
...common,
|
|
|
|
|
});
|
2026-10-13 22:15:01 +08:00
|
|
|
if (availability?.available === false) return ok(singleton(availability));
|
2026-10-10 12:18:50 +08:00
|
|
|
const flow = dossier.native_value_flow;
|
2026-10-10 01:31:15 -07:00
|
|
|
if (
|
|
|
|
|
view.facet === "procedure" ||
|
|
|
|
|
view.facet === "native_api" ||
|
|
|
|
|
view.facet === "value_flow_summary"
|
|
|
|
|
) {
|
2026-10-10 12:18:50 +08:00
|
|
|
if (view.offset !== 0)
|
|
|
|
|
return err(viewError("Singleton native facets require offset 0."));
|
|
|
|
|
if (view.facet === "procedure") return ok(singleton(dossier.procedure));
|
2026-10-10 01:31:15 -07:00
|
|
|
if (view.facet === "native_api")
|
|
|
|
|
return ok(
|
|
|
|
|
singleton(
|
|
|
|
|
dossier.native_api ?? { available: false, reason: "not recorded" },
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-10-10 12:18:50 +08:00
|
|
|
const status =
|
|
|
|
|
flow === null
|
|
|
|
|
? { available: false, reason: "not recorded" }
|
|
|
|
|
: !flow.available
|
|
|
|
|
? { available: false, reason: flow.reason }
|
|
|
|
|
: {
|
|
|
|
|
available: true,
|
|
|
|
|
provenance: flow.provenance,
|
|
|
|
|
truncated: flow.truncated,
|
|
|
|
|
omitted_operations_lower_bound:
|
|
|
|
|
flow.omitted_operations_lower_bound,
|
|
|
|
|
known_omitted_inputs: flow.known_omitted_inputs,
|
|
|
|
|
known_omitted_edges: flow.known_omitted_edges,
|
|
|
|
|
operations: flow.operations.length,
|
|
|
|
|
def_use: flow.def_use.length,
|
|
|
|
|
effects: flow.effects.length,
|
|
|
|
|
parameters: flow.parameters.length,
|
|
|
|
|
parameter_uses: flow.parameter_uses.length,
|
|
|
|
|
};
|
|
|
|
|
return ok(singleton(status));
|
|
|
|
|
}
|
|
|
|
|
if (view.facet === "pseudocode") {
|
2026-10-12 21:17:52 +08:00
|
|
|
const page = analysisTextPage(dossier.pseudocode, view.offset, view.limit);
|
2026-10-10 12:18:50 +08:00
|
|
|
return page.ok
|
|
|
|
|
? ok({ kind: "native", view, ...page.value, ...common })
|
|
|
|
|
: page;
|
|
|
|
|
}
|
|
|
|
|
const rows = nativeRows(dossier, view.facet);
|
|
|
|
|
if (rows === undefined)
|
|
|
|
|
return ok(
|
|
|
|
|
singleton({
|
|
|
|
|
available: false,
|
2026-10-10 01:31:15 -07:00
|
|
|
// Only value-flow facets inherit the value-flow failure reason; an
|
2026-10-13 13:41:10 +01:00
|
|
|
// older dossier simply has no unresolved_calls record, and a target
|
|
|
|
|
// whose provider does not decode Swift literals has no such record.
|
2026-10-10 01:31:15 -07:00
|
|
|
reason:
|
|
|
|
|
view.facet.startsWith("value_flow_") && flow?.available === false
|
|
|
|
|
? flow.reason
|
|
|
|
|
: "not recorded",
|
2026-10-10 12:18:50 +08:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const items = rows.slice(view.offset, view.offset + view.limit);
|
|
|
|
|
return ok({
|
|
|
|
|
kind: "native",
|
|
|
|
|
view,
|
|
|
|
|
item: jsonValueSchema.parse(items),
|
|
|
|
|
coverage: pageViewCoverage(view.offset, items.length, rows.length),
|
|
|
|
|
...common,
|
|
|
|
|
});
|
|
|
|
|
};
|