Files
reverse-engineering-anything/src/domain/analysisView/environmentStateWriteCreateCalls.ts
T
Edgar ArakelyanandClaude Opus 5.5 212063ff6c fix(browser): address review of saved values and slug spellings
- Basic credentials and values saved with `secret: true` are redacted however
  short, as declared secrets are; page values keep the 8-character floor
- a literal Basic password other than empty is refused, since Evidence
  records literals as written
- `save_response` takes only a request the step starts, so an earlier
  autosave cannot answer it, matches its path as the browser spells it, and
  stops watching requests when the step ends
- saving reads are bounded: a request step streams its answer up to
  4,000,000 characters; a UI step refuses a larger received size unread
- goto placeholders fill the destination URL before structured query values
  join it
- reading a composed string stops at the reading cap before comparing or
  allocating more
- a slug's `&` spelling follows every create the plan accepted and every held
  entity; direct slug rules and composed strings take it from held entities
  when the create slugs none, and a slug rule they contradict is not learned

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:49:33 -07:00

370 lines
13 KiB
TypeScript

import type { JsonValue } from "../jsonValue.js";
import { same, type JsonObject } from "./environmentStateRules.js";
import { storedKinds } from "./environmentStateWriteApply.js";
import { fittedClockFormat } from "./environmentStateWriteClockFormats.js";
import { composedValue } from "./environmentStateWriteComposed.js";
import {
alikeRules,
type FieldRule,
type WriteBody,
} from "./environmentStateWriteRules.js";
import { slugOf, type Ampersand } from "./environmentStateWriteValues.js";
type Default = { readonly member: string; readonly value: JsonValue };
/** What earlier calls showed that a merge must respect: members stored unsorted as sent, and the value showing each clock's width. */
export interface EarlierEvidence {
readonly kept: ReadonlySet<string>;
readonly clockOf: (field: string) => JsonValue | undefined;
/** How the plan's accepted calls and the type's held entities spelled `&` in each field's slug; absent where none showed one. */
readonly ampersands?: ReadonlyMap<string, AmpersandSpelling>;
}
/** How a field's slug showed `&`: dropped, or as the word. */
export type AmpersandSpelling = "dropped" | Ampersand;
/** The plan's rule for a field and a later call's agree: equal, or clocks one spelling covers given what earlier calls showed. */
const agreeing = (
held: FieldRule,
later: FieldRule,
values: {
readonly field: string;
readonly later: JsonValue | undefined;
readonly earlier: EarlierEvidence;
readonly exemplar: JsonObject;
readonly laterEntity: JsonObject;
},
): FieldRule | undefined => {
const alike = alikeRules(held, later, values.earlier.kept);
if (alike !== undefined) return alike;
const spelled = ampersandSpelled(
held,
{ rule: later, entity: values.laterEntity },
values.earlier.ampersands?.get(values.field),
);
if (spelled !== undefined) return spelled;
if (held.rule !== "clock" || later.rule !== "clock") return undefined;
const format = fittedClockFormat(
held.format,
values.later,
values.earlier.clockOf(values.field),
);
return format === undefined ? undefined : { ...held, format };
};
/** A rule with its slugs spelled the default way. */
const unspelled = (rule: FieldRule): FieldRule => {
if (rule.rule === "slug") {
const { ampersand: _ampersand, ...plain } = rule;
return plain;
}
if (rule.rule !== "composed") return rule;
return {
...rule,
parts: rule.parts.map((part) =>
"as" in part && part.as === "slug"
? { from: part.from, as: "slug" as const }
: part,
),
};
};
/** Whether a call's entity holds `&` in a field the rule slugs, which shows how it spells one. */
const showsAmpersand = (rule: FieldRule, entity: JsonObject): boolean =>
(rule.rule === "slug"
? [rule.from]
: rule.rule === "composed"
? rule.parts.flatMap((part) =>
"as" in part && part.as === "slug" ? [part.from] : [],
)
: []
).some((field) => {
const value = entity[field];
return typeof value === "string" && value.includes("&");
});
/**
* Two calls' rules that differ only in how a slug spells `&`: the one that spells it as the word, when the call
* spelling it otherwise slugged no `&`, and no call the plan accepted nor entity the type held dropped one (`shown`).
*/
const ampersandSpelled = (
held: FieldRule,
later: { readonly rule: FieldRule; readonly entity: JsonObject },
shown: AmpersandSpelling | undefined,
): FieldRule | undefined => {
const plainHeld = unspelled(held);
if (!same(plainHeld, unspelled(later.rule))) return undefined;
const heldSpells = !same(held, plainHeld);
if (heldSpells === !same(later.rule, unspelled(later.rule))) return undefined;
if (heldSpells)
return showsAmpersand(later.rule, later.entity) ? undefined : held;
return shown === "dropped" ? undefined : later.rule;
};
/** How a rule's slug spells `&`, for a rule that slugs a field. */
const slugSpelling = (rule: FieldRule): AmpersandSpelling | undefined => {
if (rule.rule === "slug") return rule.ampersand ?? "dropped";
if (rule.rule !== "composed") return undefined;
for (const part of rule.parts)
if ("as" in part && part.as === "slug") return part.ampersand ?? "dropped";
return undefined;
};
/** Note how a call that slugged `&` spelled it in each field its own rules slug. */
export const noteAmpersands = (
ampersands: Map<string, AmpersandSpelling>,
rules: readonly FieldRule[],
entity: JsonObject,
): void => {
for (const rule of rules) {
const spelling = slugSpelling(rule);
if (spelling !== undefined && showsAmpersand(rule, entity))
ampersands.set(rule.field, spelling);
}
};
/** Note how held entities whose slugged source holds `&` spelled each field the plan's rules slug. */
export const noteHeldAmpersands = (
ampersands: Map<string, AmpersandSpelling>,
rules: readonly FieldRule[],
held: readonly JsonObject[],
): void => {
for (const rule of rules) {
if (slugSpelling(rule) === undefined) continue;
const plain = unspelled(rule);
for (const entity of held) {
if (!showsAmpersand(rule, entity)) continue;
const spelled = (spelling: Ampersand | undefined): string | undefined =>
rule.rule === "slug"
? typeof entity[rule.from] === "string"
? slugOf(entity[rule.from] as string, spelling)
: undefined
: plain.rule === "composed"
? composedValue(
plain.parts.map((part) =>
"as" in part && part.as === "slug" && spelling !== undefined
? { ...part, ampersand: spelling }
: part,
),
entity,
)
: undefined;
// A field that spells neither way shows nothing of how its slug spells `&`; dropping it wins over the word.
if (spelled(undefined) === entity[rule.field])
ampersands.set(rule.field, "dropped");
else if (
spelled("and") === entity[rule.field] &&
ampersands.get(rule.field) !== "dropped"
)
ampersands.set(rule.field, "and");
}
}
};
/** Defaults joined, undefined when two calls showed one member left out at different values. */
const joinedDefaults = (
held: readonly Default[],
added: readonly Default[],
): Default[] | undefined => {
const joined = new Map(held.map((entry) => [entry.member, entry.value]));
for (const { member, value } of added) {
if (joined.has(member) && !same(joined.get(member), value))
return undefined;
joined.set(member, value);
}
return [...joined].map(([member, value]) => ({ member, value }));
};
/** What two calls show of one field. */
interface FieldCalls {
readonly before: FieldRule | undefined;
readonly after: FieldRule | undefined;
readonly exemplar: JsonObject;
readonly later: { readonly entity: JsonObject; readonly body: JsonObject };
readonly sentBefore: ReadonlySet<string>;
readonly required: readonly string[];
readonly earlier: EarlierEvidence;
}
/** A reference an earlier later call added: it stays while a call leaves its member out, and agrees with this call's when it shows it. */
const addedReference = (
field: string,
added: Extract<FieldRule, { rule: "reference" }>,
calls: FieldCalls,
): { readonly rule: FieldRule } | undefined => {
const { after, later } = calls;
if (!Object.hasOwn(later.entity, field))
return Object.hasOwn(later.body, added.member)
? undefined
: { rule: added };
const rule =
after === undefined
? undefined
: agreeing(added, after, {
field,
later: later.entity[field],
earlier: calls.earlier,
exemplar: calls.exemplar,
laterEntity: later.entity,
});
return rule === undefined ? undefined : { rule };
};
/** A field one call shows alone, which must be a reference the other call did not send the member of. */
const shownAlone = (
inExemplar: boolean,
calls: FieldCalls,
): { readonly rule: FieldRule } | undefined => {
const own = inExemplar ? calls.before : calls.after;
if (own?.rule !== "reference") return undefined;
const sent = inExemplar
? Object.hasOwn(calls.later.body, own.member)
: calls.sentBefore.has(own.member);
return sent ? undefined : { rule: own };
};
/** One field's rule in the merged plan, and the default it gives a member; undefined when the calls disagree on it. */
const mergedField = (
field: string,
calls: FieldCalls,
): { readonly rule?: FieldRule; readonly default?: Default } | undefined => {
const { before, after, exemplar, later } = calls;
const inExemplar = Object.hasOwn(exemplar, field);
if (!inExemplar && before?.rule === "reference")
return addedReference(field, before, calls);
if (!inExemplar || !Object.hasOwn(later.entity, field))
return shownAlone(inExemplar, calls);
if (before !== undefined && after !== undefined) {
const rule = agreeing(before, after, {
field,
later: later.entity[field],
earlier: calls.earlier,
exemplar,
laterEntity: later.entity,
});
return rule === undefined ? undefined : { rule };
}
// The plan echoes a member this call left out: what it stored there is the member's default.
if (before !== undefined)
return before.rule === "echo" && !Object.hasOwn(later.body, field)
? {
rule: before,
default: { member: field, value: later.entity[field]! },
}
: undefined;
// This call echoes a member the plan's calls left out: what they stored there is its default.
if (after !== undefined)
return after.rule === "echo" && !calls.required.includes(field)
? { rule: after, default: { member: field, value: exemplar[field]! } }
: undefined;
return same(exemplar[field], later.entity[field]) ? {} : undefined;
};
/**
* A create plan widened by a later call, explained on its own: both calls show the same fields, but for a reference
* whose member a call without it did not send (one an earlier call added stays), every field both rule they rule alike
* as far as earlier calls allow, a field neither rules holds one value, and where only one echoes a member the other
* left out, the member becomes optional with the value the call that left it out stored. Members either call sent are
* accepted, and only those every call sent are required. Undefined when the calls disagree otherwise, so the later call
* stays unexplained.
*/
export const mergedCreatePlan = (
plan: { readonly rules: readonly FieldRule[]; readonly body: WriteBody },
exemplar: JsonObject,
later: {
readonly entity: JsonObject;
readonly body: JsonObject;
readonly rules: readonly FieldRule[];
readonly contract: WriteBody;
},
earlier: EarlierEvidence = {
kept: new Set(),
clockOf: (field) => exemplar[field],
},
): { readonly rules: FieldRule[]; readonly body: WriteBody } | undefined => {
const held = new Map(plan.rules.map((rule) => [rule.field, rule]));
const shown = new Map(later.rules.map((rule) => [rule.field, rule]));
const sentBefore = new Set([
...plan.body.accepted,
...plan.body.fixed.map(({ member }) => member),
...plan.body.required,
]);
const merged = new Map<string, FieldRule>();
const defaults: Default[] = [];
for (const field of new Set([
...Object.keys(exemplar),
...Object.keys(later.entity),
...held.keys(),
])) {
const outcome = mergedField(field, {
before: held.get(field),
after: shown.get(field),
exemplar,
later,
sentBefore,
required: plan.body.required,
earlier,
});
if (outcome === undefined) return undefined;
if (outcome.rule !== undefined) merged.set(field, outcome.rule);
if (outcome.default !== undefined) defaults.push(outcome.default);
}
const body = mergedBody(plan.body, later, exemplar);
const joined =
body === undefined
? undefined
: joinedDefaults(plan.body.defaults ?? [], defaults);
if (body === undefined || joined === undefined) return undefined;
// The plan's rules keep their order; rules the later call adds follow.
return {
rules: [
...plan.rules.flatMap((rule) => {
const kept = merged.get(rule.field);
return kept === undefined ? [] : [kept];
}),
...[...merged.values()].filter((rule) => !held.has(rule.field)),
],
body: joined.length === 0 ? body : { ...body, defaults: joined },
};
};
/** Two calls' body contracts joined, undefined when a member one accepts with any value the other fixes, or both fix it at different values. */
const mergedBody = (
body: WriteBody,
later: {
readonly body: JsonObject;
readonly contract: WriteBody;
readonly entity: JsonObject;
},
exemplar: JsonObject,
): WriteBody | undefined => {
const fixed = new Map(body.fixed.map((entry) => [entry.member, entry.sent]));
const { contract } = later;
if (
contract.accepted.some((member) => fixed.has(member)) ||
contract.fixed.some(
(entry) =>
body.accepted.includes(entry.member) ||
(fixed.has(entry.member) && !same(fixed.get(entry.member), entry.sent)),
) ||
contract.envelope !== body.envelope
)
return undefined;
const added = contract.accepted.filter(
(member) => !body.accepted.includes(member),
);
return {
...body,
accepted: [...body.accepted, ...added],
fixed: [
...body.fixed,
...contract.fixed.filter((entry) => !fixed.has(entry.member)),
],
required: body.required.filter((member) =>
Object.hasOwn(later.body, member),
),
identity: body.identity ?? contract.identity,
kinds: [...body.kinds, ...storedKinds(added, exemplar, later.entity)],
};
};