mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
feat(environment): report what an environment serves with an environment-coverage view
A new inspect_analysis_view kind over capture Evidence. It projects the state an environment derives with the given template rules and reports each tool as answered from state, replayed with its view reason, a served write, or a withheld write with its reason, the records behind it and a hint about what to record next. The hints cover every write reason, so a new reason without one fails to compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d4f55652b
commit
da42a23cee
7 files changed
+223
-15
No files matched your search
@@ -189,7 +189,8 @@ export const inspectAnalysisViewValidated = (
|
||||
)
|
||||
: undefined;
|
||||
const stateAccumulator =
|
||||
input.view.kind === "environment-state"
|
||||
input.view.kind === "environment-state" ||
|
||||
input.view.kind === "environment-coverage"
|
||||
? new EnvironmentStateAccumulator(undefined, input.view.template_rules)
|
||||
: undefined;
|
||||
const accumulator =
|
||||
@@ -377,8 +378,9 @@ const createAnalysisViewEvidence = (
|
||||
rawResult: null,
|
||||
confidence:
|
||||
// Grouping is whole-capture, so any templated group or parameter makes the result inferred;
|
||||
// entity types and identity in an extracted state always are.
|
||||
// entity types and identity in an extracted state, and the coverage summarizing it, always are.
|
||||
result.kind === "environment-state" ||
|
||||
result.kind === "environment-coverage" ||
|
||||
((result.kind === "api-surface" ||
|
||||
result.kind === "environment-manifest") &&
|
||||
result.templated_groups > 0)
|
||||
|
||||
@@ -220,6 +220,11 @@ import {
|
||||
environmentStateViewRequestSchema,
|
||||
projectEnvironmentStateView,
|
||||
} from "./environmentStateView.js";
|
||||
import {
|
||||
environmentCoverageFields,
|
||||
environmentCoverageViewRequestSchema,
|
||||
projectEnvironmentCoverageView,
|
||||
} from "./environmentCoverageView.js";
|
||||
import {
|
||||
pcapViewRequestSchema,
|
||||
pcapViewPageSchema,
|
||||
@@ -357,6 +362,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
|
||||
apiSurfaceViewSchema,
|
||||
environmentManifestViewRequestSchema,
|
||||
environmentStateViewRequestSchema,
|
||||
environmentCoverageViewRequestSchema,
|
||||
captureViewRequestSchema,
|
||||
protobufViewRequestSchema,
|
||||
qlogViewRequestSchema,
|
||||
@@ -820,6 +826,16 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
]),
|
||||
view: environmentStateViewRequestSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...viewResultBase,
|
||||
...environmentCoverageFields,
|
||||
kind: z.literal("environment-coverage"),
|
||||
parent_operation: z.enum([
|
||||
"inspect_web_network_capture",
|
||||
"capture_browser_scenario",
|
||||
]),
|
||||
view: environmentCoverageViewRequestSchema,
|
||||
}),
|
||||
z.discriminatedUnion("parent_operation", [
|
||||
nativeExportsSummaryViewResultSchema,
|
||||
byteEntropySummaryViewResultSchema,
|
||||
@@ -1090,7 +1106,7 @@ const unsupportedParent = (
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
parentArtifactPath(parent),
|
||||
parent.operation === "capture_browser_scenario"
|
||||
? `Parent Evidence operation "capture_browser_scenario" supports only the environment-manifest and environment-state views.`
|
||||
? `Parent Evidence operation "capture_browser_scenario" supports only the environment-manifest, environment-state and environment-coverage views.`
|
||||
: `Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed Evidence from a supported parent operation with a compatible view. For a summary receipt, use its normalized_result.parent_evidence_id rather than the receipt's own evidence_id.`,
|
||||
);
|
||||
|
||||
@@ -1151,9 +1167,16 @@ export interface AnalysisViewAccumulators {
|
||||
readonly state?: EnvironmentStateAccumulator | undefined;
|
||||
}
|
||||
|
||||
/** Views that group a capture's records into tools. */
|
||||
export type GroupingViewKind =
|
||||
| "api-surface"
|
||||
| "environment-manifest"
|
||||
| "environment-state"
|
||||
| "environment-coverage";
|
||||
|
||||
type GroupingViewRequest = Extract<
|
||||
AnalysisViewRequest,
|
||||
{ kind: "api-surface" | "environment-manifest" | "environment-state" }
|
||||
{ kind: GroupingViewKind }
|
||||
>;
|
||||
|
||||
const isGroupingView = (
|
||||
@@ -1161,7 +1184,8 @@ const isGroupingView = (
|
||||
): view is GroupingViewRequest =>
|
||||
view.kind === "api-surface" ||
|
||||
view.kind === "environment-manifest" ||
|
||||
view.kind === "environment-state";
|
||||
view.kind === "environment-state" ||
|
||||
view.kind === "environment-coverage";
|
||||
|
||||
/** Views that group a capture's records, each under the budget its caller created. */
|
||||
const projectGroupingView = (
|
||||
@@ -1177,6 +1201,8 @@ const projectGroupingView = (
|
||||
);
|
||||
if (view.kind === "environment-manifest")
|
||||
return projectEnvironmentManifestView(parent, view, accumulators.manifest);
|
||||
if (view.kind === "environment-coverage")
|
||||
return projectEnvironmentCoverageView(parent, view, accumulators.state);
|
||||
return projectEnvironmentStateView(parent, view, accumulators.state);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
AnalysisViewParent,
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
GroupingViewKind,
|
||||
} from "./analysisView.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
@@ -157,10 +158,7 @@ const layoutSummary = (layout: BinaryLayout) => ({
|
||||
export const projectBinaryLayoutView = (
|
||||
parent: AnalysisViewParent,
|
||||
layout: BinaryLayout,
|
||||
view: Exclude<
|
||||
AnalysisViewRequest,
|
||||
{ kind: "api-surface" | "environment-manifest" | "environment-state" }
|
||||
>,
|
||||
view: Exclude<AnalysisViewRequest, { kind: GroupingViewKind }>,
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
const shared = parentFields(parent, layout);
|
||||
if (view.kind === "native")
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { z } from "zod";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { ok, type Result } from "../result.js";
|
||||
import type {
|
||||
AnalysisViewParent,
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js";
|
||||
import { templateRulesSchema } from "./environmentManifestView.js";
|
||||
import type { StateView } from "./environmentStateRules.js";
|
||||
import {
|
||||
environmentStateFields,
|
||||
projectEnvironmentStateView,
|
||||
} from "./environmentStateView.js";
|
||||
import type {
|
||||
StateWritePlan,
|
||||
WriteEffect,
|
||||
WriteReason,
|
||||
} from "./environmentStateWriteRules.js";
|
||||
|
||||
export const environmentCoverageViewRequestSchema = z.strictObject({
|
||||
kind: z.literal("environment-coverage"),
|
||||
template_rules: templateRulesSchema.describe(
|
||||
"Opt-in template rules as for environment-manifest, each listed at most once. Use the rules of the manifest the environment serves so tool names match.",
|
||||
),
|
||||
});
|
||||
|
||||
/** What to record next for each reason a write tool is withheld; advice only, it never changes what is served. */
|
||||
export const WITHHELD_WRITE_HINTS = {
|
||||
"no-state-target":
|
||||
"The write's path names no collection or item the capture reads. Read the collection or item it changes.",
|
||||
"no-call":
|
||||
"No recorded call of this tool was modelled, for example because every call failed. Record a successful call.",
|
||||
"body-unusable": "The request body is not a usable JSON object.",
|
||||
"no-observation":
|
||||
"Nothing showed the written entity. Read it back by id right after the write.",
|
||||
"unexplained-change":
|
||||
"The write changed fields it did not send, in a way no rule explains. A second, different call of the same tool can show the pattern.",
|
||||
contradicted:
|
||||
"A later read disagreed with the modelled effect. Check the read at contradicted_by.",
|
||||
"side-effect":
|
||||
"The write changed something besides its own entity, which the environment does not model.",
|
||||
unconfirmed:
|
||||
"No later read showed the effect. Read the entity and its list after the write.",
|
||||
"incomplete-entities":
|
||||
"The write is confirmed, but the views that show its entity cannot render it in full. Read the written entity through every view that lists it.",
|
||||
} as const satisfies Record<WriteReason, string>;
|
||||
|
||||
const writeReasons = Object.keys(WITHHELD_WRITE_HINTS) as [
|
||||
WriteReason,
|
||||
...WriteReason[],
|
||||
];
|
||||
const effectSchema = z.enum(["create", "update", "delete", "document", "none"]);
|
||||
const ordinalSchema = z.number().int().nonnegative();
|
||||
|
||||
/** Which tools an environment answers from its state, which replay their recordings, and why each write is withheld. */
|
||||
export const environmentCoverageSchema = z.strictObject({
|
||||
reads: z.strictObject({
|
||||
state: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Read tools answered from the state."),
|
||||
recorded: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.describe("Read tools that replay their recordings."),
|
||||
recorded_reasons: z
|
||||
.record(z.string(), z.number().int().positive())
|
||||
.describe(
|
||||
"Replayed read tools per reason, as the environment-state view names the reasons.",
|
||||
),
|
||||
}),
|
||||
writes: z.strictObject({
|
||||
served: z.array(z.strictObject({ tool: z.string(), effect: effectSchema })),
|
||||
withheld: z.array(
|
||||
z.strictObject({
|
||||
tool: z.string(),
|
||||
effect: effectSchema,
|
||||
reason: z.enum(writeReasons),
|
||||
hint: z
|
||||
.string()
|
||||
.describe(
|
||||
"What to record next; advice that never changes what is served.",
|
||||
),
|
||||
exemplar_ordinal: ordinalSchema
|
||||
.nullable()
|
||||
.describe(
|
||||
"The first successful recorded call the plan was built from.",
|
||||
),
|
||||
contradicted_by: ordinalSchema
|
||||
.nullable()
|
||||
.describe("The read that disagreed with the modelled effect."),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type EnvironmentCoverage = z.infer<typeof environmentCoverageSchema>;
|
||||
|
||||
export const environmentCoverageFields = {
|
||||
origin: environmentStateFields.origin,
|
||||
name: environmentStateFields.name,
|
||||
...environmentCoverageSchema.shape,
|
||||
unknowns: environmentStateFields.unknowns,
|
||||
};
|
||||
|
||||
/** Summarize a state's read views and write plans as served, replayed and withheld tools. */
|
||||
export const summarizeEnvironmentCoverage = (state: {
|
||||
readonly views: Iterable<StateView>;
|
||||
readonly writes: Iterable<StateWritePlan>;
|
||||
}): EnvironmentCoverage => {
|
||||
let answered = 0;
|
||||
let recorded = 0;
|
||||
const reasons: Record<string, number> = {};
|
||||
for (const view of state.views)
|
||||
if (view.shape === "recorded") {
|
||||
recorded++;
|
||||
reasons[view.reason] = (reasons[view.reason] ?? 0) + 1;
|
||||
} else answered++;
|
||||
const served: { tool: string; effect: WriteEffect }[] = [];
|
||||
const withheld: EnvironmentCoverage["writes"]["withheld"] = [];
|
||||
for (const plan of state.writes)
|
||||
if (plan.served || plan.reason === null)
|
||||
served.push({ tool: plan.tool, effect: plan.effect });
|
||||
else
|
||||
withheld.push({
|
||||
tool: plan.tool,
|
||||
effect: plan.effect,
|
||||
reason: plan.reason,
|
||||
hint: WITHHELD_WRITE_HINTS[plan.reason],
|
||||
exemplar_ordinal: plan.exemplar_ordinal,
|
||||
contradicted_by: plan.contradicted_by,
|
||||
});
|
||||
return {
|
||||
reads: {
|
||||
state: answered,
|
||||
recorded,
|
||||
recorded_reasons: Object.fromEntries(
|
||||
Object.entries(reasons).sort(([left], [right]) =>
|
||||
left < right ? -1 : left > right ? 1 : 0,
|
||||
),
|
||||
),
|
||||
},
|
||||
writes: { served, withheld },
|
||||
};
|
||||
};
|
||||
|
||||
/** Project the state an environment would serve and summarize which tools it answers from that state. */
|
||||
export const projectEnvironmentCoverageView = (
|
||||
parent: AnalysisViewParent,
|
||||
view: Extract<AnalysisViewRequest, { kind: "environment-coverage" }>,
|
||||
accumulator = new EnvironmentStateAccumulator(undefined, view.template_rules),
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
const projected = projectEnvironmentStateView(
|
||||
parent,
|
||||
{ kind: "environment-state", template_rules: view.template_rules },
|
||||
accumulator,
|
||||
);
|
||||
if (!projected.ok) return projected;
|
||||
const state = projected.value;
|
||||
return ok({
|
||||
kind: "environment-coverage",
|
||||
view,
|
||||
parent_evidence_id: state.parent_evidence_id,
|
||||
parent_operation: state.parent_operation,
|
||||
parent_digest: state.parent_digest,
|
||||
artifact: state.artifact,
|
||||
coverage: state.coverage,
|
||||
limitations: [
|
||||
...state.limitations,
|
||||
"Coverage summarizes the environment-state view the environment derives at startup; it does not run the environment. Hints are advice about what to record next.",
|
||||
],
|
||||
unknowns: state.unknowns,
|
||||
origin: state.origin,
|
||||
name: state.name,
|
||||
...summarizeEnvironmentCoverage(state),
|
||||
});
|
||||
};
|
||||
@@ -392,7 +392,10 @@ export const projectEnvironmentStateView = (
|
||||
parent: AnalysisViewParent,
|
||||
view: Extract<AnalysisViewRequest, { kind: "environment-state" }>,
|
||||
accumulator = new EnvironmentStateAccumulator(undefined, view.template_rules),
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
): Result<
|
||||
Extract<UnsignedAnalysisView, { kind: "environment-state" }>,
|
||||
AnalysisError
|
||||
> => {
|
||||
const grouped = groupCaptureRecords(parent, view.kind, view.template_rules, {
|
||||
accumulator,
|
||||
noun: "state",
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
AnalysisViewParent,
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
GroupingViewKind,
|
||||
} from "./analysisView.js";
|
||||
|
||||
const OPERATION = "inspect_analysis_view";
|
||||
@@ -340,10 +341,7 @@ const selectModule = (
|
||||
export const projectJavaScriptApplicationView = (
|
||||
parent: AnalysisViewParent,
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
view: Exclude<
|
||||
AnalysisViewRequest,
|
||||
{ kind: "api-surface" | "environment-manifest" | "environment-state" }
|
||||
>,
|
||||
view: Exclude<AnalysisViewRequest, { kind: GroupingViewKind }>,
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
if (
|
||||
view.kind !== "summary" &&
|
||||
|
||||
@@ -458,7 +458,7 @@ export const projectWebNetworkCaptureView = (
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Historical captures support summary, capture record pages, qlog fact pages and admitted api-surface, environment-manifest or environment-state views.",
|
||||
"Historical captures support summary, capture record pages, qlog fact pages and admitted api-surface, environment-manifest, environment-state or environment-coverage views.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user