mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
38719a6fa9
* feat(environment): explain values an application derives from other fields
Prompt-built environments withheld creates whose responses carry values the
application derives deterministically, which the write model kept as
constants or could not match across calls:
- A composed string is literal text plus parts read from the fields other
rules write: a field's value, its slug, or a clock's calendar digits. A
create composes a string only when another entity of the type spells it
the same way and none spells it otherwise, and never from two sources
serving could tell apart; an update composes an unsent change from members
stored as sent and unchanged fields that also spell its value before the
call. A composed field no longer pins the members it reads.
- A trimmed clock format drops trailing fraction zeros, as Go spells
nanoseconds. It is learned from the type's values, a later create or a
later update, and spreads to the plan's other clocks unless a value shows
a fixed width.
- A large integer no entity of the type holds, among distinct ones of the
same length and kind, is a generated unique value, served as a seeded
integer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(environment): record the derived-values verification
Gates, tests, write plans over 69 recorded captures and served write tasks on the baseline and on this branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep clock spellings, linked ids and word boundaries honest
- A later update's clocks take the plan's spelling, sibling-trimmed
clocks included, wherever the call's values fit it, so an update whose
later call trims only one of two clocks is served.
- A held value that ends its fraction in a zero keeps a create's clock
at a fixed width, whatever order the held spellings come in.
- A large integer that another field of the created entity also holds
is linked, as a repeated token is: it is never unique, and the create
is withheld as unexplained-change.
- Composed parts are delimited by whole characters through the shared
joinsBefore/joinsAfter, so an astral letter joins a match to its word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(environment): keep a same-width fixed clock out of trimming
A held .123456780 beside a held .12345678 keeps the create's clock at
nine digits in either order; the cases fail on 8ce1127b5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): check composed parts and clock fits before they shape a plan
- A composed string whose part has nothing to spell refuses the create
or update through the apply Result instead of keeping a stale value.
- A composition learned on a later call is rebuilt from each earlier
call's fields, so an earlier call that left the value otherwise
contradicts it.
- An update adopts its fitted clock spellings only after the
unknown-entity and contradiction checks, so a call on an entity an
unmodelled write left unknown no longer trims a served plan's clock.
- Sibling trimming on a later create also checks that call's fields,
so a sibling clock it shows with a trailing zero stays fixed-width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(environment): split the derived-values view tests by rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): keep fixed-width, zone and generated-integer evidence through a plan
- A plan keeps the first value that showed each clock at a fixed width,
from held entities and every accepted create or update. No later fit
or sibling trim widens that field, and a later call that shows it
shorter is withheld.
- Two stamped clocks are one composed source only when they share a
zone offset. A held entity that holds the target field as anything
the parts do not spell, a non-string included, vetoes the
composition. The token index is built once, and only when a string is
left to compose.
- A generated integer that another field repeats as a number at any
depth is linked. Compare treats two integers of one JSON kind and
length under a unique rule as generated values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(setup): share registration prewarm host preparation
* fix(environment): retain write evidence and match trimmed response clocks
* fix(environment): withhold creates whose generated integer another string repeats
A unique integer whose digits show in another top-level string that no
template or composed rule rebuilds from it (an echoed serialText, or a
token like folder-<id>) withholds the create as unexplained-change,
since serving would seed the integer and keep the string. A templated
copy is still served.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): hold later creates and compare to the composed and integer forms
- A later create whose response spells a composed field otherwise than
the plan rebuilds it is withheld as unexplained-change.
- A unique field learned from a generated integer accepts only an
integer of the same length and kind from a later create, as serving
seeds one.
- Compare counts a composed field as equal when each side's response
spells it from its own fields, which compare under their own rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(environment): preserve composed parts and unique generator families
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
import type { JsonValue } from "../jsonValue.js";
|
|
import {
|
|
decodedSegment,
|
|
isParam,
|
|
normalizedPath,
|
|
pathSegmentMatches,
|
|
segmentsOf,
|
|
type JsonObject,
|
|
} from "./environmentStateRules.js";
|
|
import type { ClockFormat } from "./environmentStateWriteValues.js";
|
|
import {
|
|
showsFixedWidth,
|
|
trimmedLike,
|
|
type ClockPattern,
|
|
} from "./environmentStateWriteClockFormats.js";
|
|
|
|
/** What a write tool does to the state, by method and the target its path names. */
|
|
export type WriteEffect = "create" | "update" | "delete" | "document" | "none";
|
|
|
|
/** Why a write tool is not served. */
|
|
export type WriteReason =
|
|
| "no-state-target"
|
|
| "no-call"
|
|
| "body-unusable"
|
|
| "no-observation"
|
|
| "unexplained-change"
|
|
| "contradicted"
|
|
| "side-effect"
|
|
| "unconfirmed"
|
|
| "incomplete-entities";
|
|
|
|
/** Rules that generate a value when served; the gate takes the recorded value instead. */
|
|
export type GeneratedRule =
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "identity";
|
|
readonly generator: "sequence" | "uuid" | "token";
|
|
}
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "sequence";
|
|
readonly scope: "collection" | "type";
|
|
}
|
|
| { readonly field: string; readonly rule: "unique" }
|
|
/** The value kept with each UUID at the pointers (`*` for any array index) replaced by a fresh one, as rewritten child rows are. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "renewed";
|
|
readonly pointers: string[];
|
|
}
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "clock";
|
|
readonly format: ClockFormat;
|
|
}
|
|
/** An object value whose members hold the write's clock, the others kept. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "nested-clock";
|
|
readonly members: string[];
|
|
readonly format: ClockFormat;
|
|
}
|
|
/** A sent clock the application stamps anew, which a call must send as the entity holds it. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "held-clock";
|
|
readonly format: ClockFormat;
|
|
};
|
|
|
|
/** How one field of a written entity gets its value; fields no rule names keep the observed value. */
|
|
export type FieldRule =
|
|
| GeneratedRule
|
|
| { readonly field: string; readonly rule: "echo" }
|
|
| { readonly field: string; readonly rule: "copy"; readonly from: string }
|
|
| { readonly field: string; readonly rule: "path"; readonly from: string }
|
|
| { readonly field: string; readonly rule: "slug"; readonly from: string }
|
|
| { readonly field: string; readonly rule: "template"; readonly from: string }
|
|
/** A string rebuilt from literal text and other written fields, in order. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "composed";
|
|
readonly parts: ComposedPart[];
|
|
}
|
|
| { readonly field: string; readonly rule: "increment" }
|
|
/** An update stores `value` whenever the call sends the member, as the call that showed it did. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "constant";
|
|
readonly value: JsonValue;
|
|
}
|
|
/** An update stores `value`, the acting user's, without being sent it. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "stamp";
|
|
readonly value: JsonValue;
|
|
}
|
|
/** An update that sends `member` stores `value`, derived from it, on an entity holding `from` there. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "derived";
|
|
readonly value: JsonValue;
|
|
readonly from: JsonValue;
|
|
readonly member: string;
|
|
}
|
|
/** An item action stores `value` on every call, sent or not. */
|
|
| {
|
|
readonly field: string;
|
|
readonly rule: "set";
|
|
readonly value: JsonValue;
|
|
};
|
|
|
|
/** Clock rules spelled as a trimmed sibling is, since an application spells its clocks alike: fields whose `values`, and every `later` call's, show no fixed width. */
|
|
export const siblingTrimmedClocks = (
|
|
rules: readonly FieldRule[],
|
|
values: Readonly<Record<string, JsonValue>> | undefined,
|
|
...later: readonly Readonly<Record<string, JsonValue>>[]
|
|
): FieldRule[] => {
|
|
const trimmed = new Map<string, Extract<ClockFormat, { kind: "iso" }>>();
|
|
for (const rule of rules) {
|
|
if (
|
|
rule.rule !== "clock" ||
|
|
rule.format.kind !== "iso" ||
|
|
rule.format.trimmed !== true
|
|
)
|
|
continue;
|
|
const format = rule.format;
|
|
const group = `${format.separator}${format.zone}`;
|
|
const widest = trimmed.get(group);
|
|
if (widest === undefined || format.fraction > widest.fraction)
|
|
trimmed.set(group, format);
|
|
}
|
|
if (trimmed.size === 0) return [...rules];
|
|
return rules.map((rule) => {
|
|
if (rule.rule !== "clock" || rule.format.kind !== "iso") return rule;
|
|
const peer = trimmed.get(`${rule.format.separator}${rule.format.zone}`);
|
|
if (peer === undefined) return rule;
|
|
const format = later.some((fields) =>
|
|
showsFixedWidth(rule.format, fields[rule.field]),
|
|
)
|
|
? rule.format
|
|
: trimmedLike(rule.format, peer, values?.[rule.field]);
|
|
return format === rule.format ? rule : { ...rule, format };
|
|
});
|
|
};
|
|
|
|
/** One part of a composed string: literal text, or another written field as written, slugged, or as a clock's calendar digits. */
|
|
export type ComposedPart =
|
|
| { readonly text: string }
|
|
| { readonly from: string; readonly as: "value" | "slug" }
|
|
| {
|
|
readonly from: string;
|
|
readonly as: "clock";
|
|
readonly pattern: ClockPattern;
|
|
};
|
|
|
|
/** Where a create places its entity in its collection's order. */
|
|
export type InsertRule =
|
|
| { readonly rule: "first" | "last" }
|
|
| {
|
|
readonly rule: "sorted";
|
|
readonly field: string;
|
|
readonly direction: "ascending" | "descending";
|
|
};
|
|
|
|
/** The JSON type of a stored value that is not null. */
|
|
export type JsonKind = "array" | "boolean" | "number" | "object" | "string";
|
|
|
|
/** The body members a write accepts: echoed ones with any value, fixed ones only with the value they were sent. */
|
|
export interface WriteBody {
|
|
readonly accepted: string[];
|
|
readonly fixed: { readonly member: string; readonly sent: JsonValue }[];
|
|
readonly required: string[];
|
|
/** The member that may repeat the target's identity, which must then equal the path's. */
|
|
readonly identity: string | null;
|
|
/** The JSON type a later call stored an accepted member as, for members the observation does not show. */
|
|
readonly kinds: { readonly member: string; readonly kind: JsonKind }[];
|
|
/** The member a call wraps its object in, as a one-element array. */
|
|
readonly envelope?: string | undefined;
|
|
}
|
|
|
|
/**
|
|
* How a write names its entities when its path does not end in one: an action segment after the
|
|
* item's id, or ids in the body of a write at the collection, as an array of one JSON kind (`ids`)
|
|
* or as objects carrying the type's id field (`elements`), at the body's root or under one member.
|
|
*/
|
|
export type WriteAddress =
|
|
| { readonly by: "action"; readonly segment: string }
|
|
| {
|
|
readonly by: "ids";
|
|
readonly member: string | null;
|
|
readonly kind: "number" | "string";
|
|
}
|
|
| { readonly by: "elements"; readonly member: string | null };
|
|
|
|
/** How a bulk write names its entities in its body. */
|
|
export type BodyAddress = Exclude<WriteAddress, { by: "action" }>;
|
|
|
|
/** What a write answers: the written entity or its identity rendered into a recorded body, no body, or the recorded body. */
|
|
export interface WriteResponse {
|
|
readonly ordinal: number;
|
|
readonly shape: "entity" | "identity" | "empty" | "recorded";
|
|
readonly pointer: string | null;
|
|
}
|
|
|
|
/** One write tool's modelled effect and whether the capture confirmed it. */
|
|
export interface StateWritePlan {
|
|
readonly tool: string;
|
|
readonly effect: WriteEffect;
|
|
/** The entity type for entity effects; the document tool for a document write. */
|
|
readonly target: string | null;
|
|
readonly served: boolean;
|
|
readonly reason: WriteReason | null;
|
|
readonly exemplar_ordinal: number | null;
|
|
/** The record whose response showed the written entity or document, and where. */
|
|
readonly observation: {
|
|
readonly ordinal: number;
|
|
readonly pointer: string;
|
|
} | null;
|
|
readonly response: WriteResponse | null;
|
|
readonly body: WriteBody | null;
|
|
readonly rules: FieldRule[];
|
|
/** A create tied to the exemplar's collection writes only there. */
|
|
readonly collection: string | null;
|
|
readonly insert: InsertRule | null;
|
|
readonly merge: "merge" | "replace" | null;
|
|
readonly checked_record_ordinals: number[];
|
|
readonly contradicted_by: number | null;
|
|
/** Set when the call's path does not end in the entity's id. */
|
|
readonly address?: WriteAddress | undefined;
|
|
}
|
|
|
|
/** The tool facts classification reads. */
|
|
interface WriteToolFacts {
|
|
readonly method: string;
|
|
readonly path_template: string;
|
|
}
|
|
|
|
/** The effect a state-changing tool has and its target: a state type, or a document tool by template. */
|
|
export const classifyWrite = (
|
|
tool: WriteToolFacts,
|
|
types: ReadonlySet<string>,
|
|
documents: ReadonlyMap<string, string>,
|
|
): { readonly effect: WriteEffect; readonly target: string | null } => {
|
|
const segments = segmentsOf(tool.path_template);
|
|
const path = normalizedPath(tool.path_template);
|
|
const parent = `/${segments.slice(0, -1).join("/")}`;
|
|
const item = segments.length > 0 && isParam(segments.at(-1)!);
|
|
if (tool.method === "POST" && types.has(path))
|
|
return { effect: "create", target: path };
|
|
if (item && types.has(parent)) {
|
|
if (tool.method === "DELETE") return { effect: "delete", target: parent };
|
|
if (["PUT", "PATCH", "POST"].includes(tool.method))
|
|
return { effect: "update", target: parent };
|
|
}
|
|
const document = documents.get(path);
|
|
if (document !== undefined && ["PUT", "PATCH", "POST"].includes(tool.method))
|
|
return { effect: "document", target: document };
|
|
return { effect: "none", target: null };
|
|
};
|
|
|
|
/** A plan for a tool whose effect is not modelled or cannot be built. */
|
|
export const unservedPlan = (
|
|
tool: string,
|
|
effect: WriteEffect,
|
|
target: string | null,
|
|
reason: WriteReason,
|
|
): StateWritePlan => ({
|
|
tool,
|
|
effect,
|
|
target,
|
|
served: false,
|
|
reason,
|
|
exemplar_ordinal: null,
|
|
observation: null,
|
|
response: null,
|
|
body: null,
|
|
rules: [],
|
|
collection: null,
|
|
insert: null,
|
|
merge: null,
|
|
checked_record_ordinals: [],
|
|
contradicted_by: null,
|
|
});
|
|
|
|
/** One write call: its concrete path, the values its path parameters took and its body. */
|
|
export interface WriteCall {
|
|
readonly path: string;
|
|
readonly pathValues: ReadonlyMap<string, string>;
|
|
readonly body: JsonObject | undefined;
|
|
}
|
|
|
|
/** The value each template parameter takes in a concrete path, decoded; undefined when the path does not instantiate it. */
|
|
export const pathValuesOf = (
|
|
template: string,
|
|
path: string,
|
|
): Map<string, string> | undefined => {
|
|
const pattern = segmentsOf(template);
|
|
const concrete = segmentsOf(path);
|
|
if (pattern.length !== concrete.length) return undefined;
|
|
const values = new Map<string, string>();
|
|
for (const [index, segment] of pattern.entries()) {
|
|
const value = concrete[index]!;
|
|
if (!isParam(segment)) {
|
|
if (segment !== value) return undefined;
|
|
continue;
|
|
}
|
|
values.set(segment.slice(1, -1), decodedSegment(value));
|
|
}
|
|
return values;
|
|
};
|
|
|
|
/** Whether two paths are segment prefixes of one another, a parameter on either side matching any segment, with the shared single-decode spelling rule. */
|
|
export const prefixRelated = (left: string, right: string): boolean => {
|
|
const a = segmentsOf(left);
|
|
const b = segmentsOf(right);
|
|
const shared = Math.min(a.length, b.length);
|
|
for (let index = 0; index < shared; index += 1)
|
|
if (
|
|
!isParam(a[index]!) &&
|
|
!isParam(b[index]!) &&
|
|
!pathSegmentMatches(a[index]!, b[index]!)
|
|
)
|
|
return false;
|
|
return true;
|
|
};
|