Files
reverse-engineering-anything/src/domain/analysisView/environmentStateMerge.ts
T
2e6b38cef0 feat(environment): render more read tools from the state and describe responses (#5352)
* feat(environment): render more read tools from the state and describe responses

Keep main's environment serving and fold in coverage for non-entity reads,
response summaries, and MCP tool annotations.

Squashed rebase of #5352 onto main.

* fix(environment): keep rendered documents and quoted samples inside their budgets

A recorded body of exponential numbers can render past 256 KiB and turn a
working read into a state size error, so that document stays a recorded replay.
Description samples are measured after JSON quoting, and the replay-fallback
catalog check uses a tool count the longer response descriptions still separate.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* test(environment): pin the replay card this branch serves

The replay-only card test pinned main's 11,131-byte card. This branch's descriptions name each tool's recorded responses, so the card it serves is 12,818 bytes; the pin and its name follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:08:44 +08:00

141 lines
5.1 KiB
TypeScript

import type { JsonValue } from "../jsonValue.js";
import { compareUnicodeCodePoints as compare } from "../unicodeCodePointOrder.js";
import {
same,
type JsonObject,
type StateEntity,
} from "./environmentStateRules.js";
/** One entity object as one tool recorded it. */
export interface StateObservation {
readonly position: number;
readonly ordinal: number;
readonly tool: string;
readonly item: boolean;
readonly object: JsonObject;
}
/** Every init observation of one entity, with where it was first seen. */
export interface ObservedEntity {
readonly type: string;
readonly collection: string;
readonly id: string;
/** The earliest recorded sighting: timeline position, then position in its list. */
order: readonly [number, number];
readonly observations: StateObservation[];
}
/** Collection order: type, collection, then first sighting. */
export const compareObservedEntities = (
left: ObservedEntity,
right: ObservedEntity,
): number =>
compare(left.type, right.type) ||
compare(left.collection, right.collection) ||
left.order[0] - right.order[0] ||
left.order[1] - right.order[1];
/** One entity: item views' latest values win over list views', and a view whose latest values differ keeps them. */
export const mergeObservedEntity = (entity: ObservedEntity): StateEntity => {
const latest = new Map<string, StateObservation>();
for (const observation of entity.observations)
latest.set(observation.tool, observation);
const fields = new Map<string, JsonValue>();
const ranked = [...latest.values()].sort(
(left, right) =>
Number(left.item) - Number(right.item) || left.position - right.position,
);
for (const observation of ranked)
for (const [name, value] of Object.entries(observation.object))
fields.set(name, value);
return {
type: entity.type,
collection: entity.collection,
id: entity.id,
fields: Object.fromEntries(fields),
view_fields: [...latest.values()]
.map((observation) => ({
tool: observation.tool,
fields: Object.fromEntries(
Object.entries(observation.object).filter(
([name, value]) => !same(fields.get(name), value),
),
),
}))
.filter((view) => Object.keys(view.fields).length > 0)
.sort((left, right) => compare(left.tool, right.tool)),
// Cited in timeline order, whichever plan observed them first.
record_ordinals: [
...new Set(
entity.observations
.toSorted((left, right) => left.position - right.position)
.map((observation) => observation.ordinal),
),
],
};
};
/**
* How observed values changed, per entity in timeline order. A shared field that differs from the
* previous repeat of the same request line is volatile, unless a recorded write lies between them;
* a field that differs from its previous observation on another line across a write rewrites the
* type. Either across a write means the reads do not agree on one initial state.
*/
export const classifyEntityChanges = (
entities: Iterable<ObservedEntity>,
lineOf: (observation: StateObservation) => readonly string[],
writeBetween: (earlier: StateObservation, later: StateObservation) => boolean,
): {
readonly rewritten: ReadonlySet<string>;
readonly volatile: ReadonlyMap<string, Set<string>>;
} => {
const rewritten = new Set<string>();
const volatile = new Map<string, Set<string>>();
const changed = (before: StateObservation, after: StateObservation) =>
Object.keys(after.object).filter(
(name) =>
Object.hasOwn(before.object, name) &&
!same(before.object[name], after.object[name]),
);
for (const entity of entities) {
const timeline = [...entity.observations].sort(
(left, right) => left.position - right.position,
);
const lastOfLine = new Map<string, StateObservation>();
// Sparse projections do not erase where each field was last observed.
const lastOfField = new Map<
string,
{ readonly observation: StateObservation; readonly line: string }
>();
timeline.forEach((after) => {
const line = JSON.stringify(lineOf(after));
const repeat = lastOfLine.get(line);
if (repeat !== undefined) {
if (writeBetween(repeat, after)) {
// Key additions/removals are changes too, even when every shared field is unchanged.
if (!same(repeat.object, after.object)) rewritten.add(entity.type);
} else
for (const name of changed(repeat, after))
volatile.set(
entity.type,
(volatile.get(entity.type) ?? new Set()).add(name),
);
}
const sighting = { observation: after, line };
for (const name of Object.keys(after.object)) {
const previous = lastOfField.get(name);
if (
previous !== undefined &&
previous.line !== line &&
!same(previous.observation.object[name], after.object[name]) &&
writeBetween(previous.observation, after)
)
rewritten.add(entity.type);
lastOfField.set(name, sighting);
}
lastOfLine.set(line, after);
});
}
return { rewritten, volatile };
};