mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
Recordings now vary what a type's two creates and updates send, and REA withheld writes where a later call left out what the first sent. An update plan's required members narrow to those every call sent; a member one create sent and another left for the application to number is a sequence that takes the value a call sends; a PUT's left-out member the application cleared becomes that member's default; and an update's clock is held as the read after it spells it. The skill spaces creates (skill 54). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
598 lines
20 KiB
TypeScript
598 lines
20 KiB
TypeScript
import { z } from "zod";
|
|
import type { AnalysisError } from "../analysisErrorBase.js";
|
|
import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js";
|
|
import { jsonRecordSchema } from "../jsonRecord.js";
|
|
import { ok, type Result } from "../result.js";
|
|
import type {
|
|
AnalysisViewParent,
|
|
AnalysisViewRequest,
|
|
UnsignedAnalysisView,
|
|
} from "./analysisView.js";
|
|
import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js";
|
|
import { DELETED_FLAGS } from "./environmentStateRules.js";
|
|
import { isClockZone } from "./environmentStateWriteValues.js";
|
|
import { CLOCK_PATTERNS } from "./environmentStateWriteClockFormats.js";
|
|
import {
|
|
groupCaptureRecords,
|
|
templateRulesSchema,
|
|
} from "./environmentManifestView.js";
|
|
|
|
export const environmentStateViewRequestSchema = z.strictObject({
|
|
kind: z.literal("environment-state"),
|
|
template_rules: templateRulesSchema.describe(
|
|
"Opt-in template rules as for environment-manifest, each listed at most once. Use the rules of the manifest the state accompanies so tool names match.",
|
|
),
|
|
});
|
|
|
|
const queryParameterSchema = z.discriminatedUnion("role", [
|
|
z.strictObject({
|
|
role: z.literal("filter"),
|
|
field: z
|
|
.string()
|
|
.describe(
|
|
"Entities whose field equals the value as text, or whose array field holds it directly or as an object's id, slug or name.",
|
|
),
|
|
}),
|
|
z.strictObject({
|
|
role: z.literal("search"),
|
|
fields: z
|
|
.array(z.string())
|
|
.describe(
|
|
"Entities with a string field containing a plain substring value, ignoring case using Unicode simple case folding; an empty value matches every entity. Syntax-like operators replay instead.",
|
|
),
|
|
}),
|
|
z
|
|
.strictObject({ role: z.literal("sort") })
|
|
.describe(
|
|
"Orders by the field the value names (`name`, `-name`, `name:desc`, `name-ASC`): numbers by value, text ignoring case, null last ascending.",
|
|
),
|
|
z
|
|
.strictObject({ role: z.literal("direction") })
|
|
.describe("asc or desc for the sort parameter."),
|
|
z
|
|
.strictObject({ role: z.literal("limit") })
|
|
.describe("Page size; zero or a negative value lists every match."),
|
|
z
|
|
.strictObject({ role: z.literal("offset") })
|
|
.describe("Matches skipped before the page."),
|
|
z.strictObject({
|
|
role: z.literal("page"),
|
|
first: z
|
|
.number()
|
|
.int()
|
|
.describe(
|
|
"The first page, 0 or 1, when recordings distinguish the convention.",
|
|
),
|
|
}),
|
|
z.strictObject({
|
|
role: z.literal("inert"),
|
|
values: z
|
|
.array(z.string())
|
|
.describe(
|
|
"Recorded values that change nothing the view lists; other values are not computed.",
|
|
),
|
|
}),
|
|
]);
|
|
const listQuerySchema = z.strictObject({
|
|
parameters: jsonRecordSchema(queryParameterSchema).describe(
|
|
"What each query parameter does. A call whose every parameter has a role is answered by evaluating them over the state.",
|
|
),
|
|
record_ordinals: z
|
|
.array(z.number().int().nonnegative())
|
|
.describe("Checked lines the query reproduces, answered by evaluating it."),
|
|
totals: z
|
|
.array(z.string())
|
|
.describe(
|
|
"Observed envelope members with reproducible counts of every match before paging, named as counters are; absent or unknown representations stay recorded.",
|
|
),
|
|
});
|
|
const checkedSchema = z
|
|
.array(z.number().int().nonnegative())
|
|
.describe(
|
|
"Latest init record per request line that the view reproduced from the state.",
|
|
);
|
|
const clockFormatSchema = z.discriminatedUnion("kind", [
|
|
z.strictObject({
|
|
kind: z.literal("iso"),
|
|
separator: z.enum(["T", " "]),
|
|
fraction: z.number().int().nonnegative().max(9),
|
|
zone: z.string().refine(isClockZone),
|
|
trimmed: z.literal(true).exactOptional(),
|
|
}),
|
|
z.strictObject({ kind: z.literal("epoch"), unit: z.enum(["s", "ms"]) }),
|
|
]);
|
|
const perResponseSchema = z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe(
|
|
"Top-level members minted per response, such as a request id: a UUID or clock that differed on every repeat of every concrete path the view answers with no write between. Checks leave them out; serving keeps the recorded value, so a served response repeats it.",
|
|
);
|
|
const entityViewFields = {
|
|
tool: z.string(),
|
|
type: z.string(),
|
|
pointer: z
|
|
.string()
|
|
.describe(
|
|
"JSON pointer to the entity object or array within the response: the root or one member, or for an item the element of a one-element array there (/0 or /<member>/0).",
|
|
),
|
|
fields: z
|
|
.array(z.string())
|
|
.describe("Ordered fields the view shows of each entity."),
|
|
required_fields: z.array(z.string()),
|
|
checked_record_ordinals: checkedSchema,
|
|
per_response_members: perResponseSchema,
|
|
clock_formats: jsonRecordSchema(clockFormatSchema)
|
|
.optional()
|
|
.describe(
|
|
"How the view spells clock fields it always spells otherwise than the entities' stored values; a stored value the view never recorded is spelled this way.",
|
|
),
|
|
nested_fields: jsonRecordSchema(z.array(z.string()))
|
|
.optional()
|
|
.describe(
|
|
"The keys the view shows of object fields it always shows with fewer keys than an entity stores, such as a parent listed without its own nested type; a stored object the view never recorded shows those keys.",
|
|
),
|
|
sorted_arrays: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe(
|
|
"Fields whose string arrays the view lists in code-point order, whatever order they were written in; learned from a read that shows a written array sorted.",
|
|
),
|
|
};
|
|
const replayedReadFields = {
|
|
projected_record_ordinals: z
|
|
.array(z.number().int().nonnegative())
|
|
.optional()
|
|
.describe(
|
|
"Recorded reads left to replay: they asked for some fields only (`fields`, `select`), or show the entities in another representation than the checked lines (an augmenting or reducing query). Replayed calls do not show served writes.",
|
|
),
|
|
projected_query_keys: z
|
|
.array(
|
|
z.strictObject({
|
|
name: z.string(),
|
|
value: z.string().optional(),
|
|
absent: z.literal(true).optional(),
|
|
}),
|
|
)
|
|
.optional()
|
|
.describe(
|
|
"Query keys that pick calls of the replayed reads out: a parameter only they name, a value only they give a parameter, or (absent) a parameter every checked line names and they leave out. A call matching any key replays; a projection parameter's value is tested on its own: a call replays unless every value it gives is `*` or one a checked line gave, and no replayed read gave it.",
|
|
),
|
|
};
|
|
const viewSchema = z.discriminatedUnion("shape", [
|
|
z.strictObject({
|
|
...entityViewFields,
|
|
shape: z.literal("item"),
|
|
envelope_ordinal: z
|
|
.number()
|
|
.int()
|
|
.nonnegative()
|
|
.describe(
|
|
"Record whose response, with the entity object replaced, every item response shares.",
|
|
),
|
|
deleted_flag: z
|
|
.enum(DELETED_FLAGS)
|
|
.optional()
|
|
.describe(
|
|
"Member its reads set true on an entity the application soft-deleted.",
|
|
),
|
|
...replayedReadFields,
|
|
}),
|
|
z.strictObject({
|
|
...entityViewFields,
|
|
shape: z.literal("list"),
|
|
membership: z
|
|
.enum(["all", "recorded"])
|
|
.describe(
|
|
"all: every entity of the collection in state order; recorded: the entities each recorded response listed, except on the lines all_record_ordinals names.",
|
|
),
|
|
all_record_ordinals: z
|
|
.array(z.number().int().nonnegative())
|
|
.optional()
|
|
.describe(
|
|
"With recorded membership, the checked lines that each listed every entity of their collection in state order, with only paging parameters starting at zero; a call answered on one lists the whole collection. Absent when none does.",
|
|
),
|
|
counters: z
|
|
.array(z.string())
|
|
.describe(
|
|
"Envelope members that count the listed entities: a top-level member's name, or a JSON pointer to one in a top-level object beside the list (`/pageInfo/totalRows`); a top-level name starting with `/` is written as its pointer (`/~1total`).",
|
|
),
|
|
listed_ids: z
|
|
.array(z.strictObject({ member: z.string(), field: z.string() }))
|
|
.optional()
|
|
.describe(
|
|
"Top-level envelope members listing each listed entity's identity field in list order, as Paperless's `all` does.",
|
|
),
|
|
query: listQuerySchema
|
|
.optional()
|
|
.describe(
|
|
"How the view's other checked lines select and order entities, when filters, a search, a sort or paging explain them; absent when none does.",
|
|
),
|
|
...replayedReadFields,
|
|
}),
|
|
z.strictObject({
|
|
tool: z.string(),
|
|
shape: z.literal("document"),
|
|
checked_record_ordinals: checkedSchema,
|
|
per_response_members: perResponseSchema,
|
|
}),
|
|
z.strictObject({
|
|
tool: z.string(),
|
|
shape: z.literal("recorded"),
|
|
reason: z.enum([
|
|
"query-body",
|
|
"no-init-recording",
|
|
"body-unusable",
|
|
"no-identity",
|
|
"path-varies",
|
|
"query-varies",
|
|
"fields-differ",
|
|
"envelope-varies",
|
|
"list-differs",
|
|
"rendered-oversize",
|
|
]),
|
|
}),
|
|
]);
|
|
|
|
const ordinalSchema = z.number().int().nonnegative();
|
|
const ruled = { field: z.string() };
|
|
const fieldRuleSchema = z.discriminatedUnion("rule", [
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("identity"),
|
|
generator: z.enum(["sequence", "uuid", "token"]),
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("sequence"),
|
|
scope: z.enum(["collection", "type"]),
|
|
start: z.number().int().exactOptional(),
|
|
sent: z.literal(true).exactOptional(),
|
|
}),
|
|
z.strictObject({ ...ruled, rule: z.literal("unique") }),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("renewed"),
|
|
pointers: z.array(z.string()).min(1),
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("clock"),
|
|
format: clockFormatSchema,
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("nested-clock"),
|
|
members: z.array(z.string()).min(1),
|
|
format: clockFormatSchema,
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("made-object"),
|
|
clocks: z.array(z.string()).min(1),
|
|
format: clockFormatSchema,
|
|
own: z.array(z.string()),
|
|
counts: z.array(
|
|
z.strictObject({ member: z.string(), start: z.number().int() }),
|
|
),
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("held-clock"),
|
|
format: clockFormatSchema,
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("echo"),
|
|
sorted: z.literal(true).exactOptional(),
|
|
}),
|
|
z.strictObject({ ...ruled, rule: z.literal("copy"), from: z.string() }),
|
|
z.strictObject({ ...ruled, rule: z.literal("path"), from: z.string() }),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("slug"),
|
|
from: z.string(),
|
|
ampersand: z.literal("and").exactOptional(),
|
|
}),
|
|
z.strictObject({ ...ruled, rule: z.literal("template"), from: z.string() }),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("composed"),
|
|
parts: z
|
|
.array(
|
|
z.union([
|
|
z.strictObject({ text: z.string().min(1) }),
|
|
z.strictObject({ from: z.string(), as: z.literal("value") }),
|
|
z.strictObject({
|
|
from: z.string(),
|
|
as: z.literal("slug"),
|
|
ampersand: z.literal("and").exactOptional(),
|
|
}),
|
|
z.strictObject({
|
|
from: z.string(),
|
|
as: z.literal("clock"),
|
|
pattern: z.enum(CLOCK_PATTERNS),
|
|
}),
|
|
]),
|
|
)
|
|
.min(1),
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("reference"),
|
|
member: z.string(),
|
|
type: z.string(),
|
|
fields: z.array(z.string()).min(1),
|
|
}),
|
|
z.strictObject({ ...ruled, rule: z.literal("increment") }),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("constant"),
|
|
value: jsonValueSchema,
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("stamp"),
|
|
value: jsonValueSchema,
|
|
}),
|
|
z.strictObject({
|
|
...ruled,
|
|
rule: z.literal("derived"),
|
|
value: jsonValueSchema,
|
|
from: jsonValueSchema,
|
|
member: z.string(),
|
|
}),
|
|
z.strictObject({ ...ruled, rule: z.literal("set"), value: jsonValueSchema }),
|
|
]);
|
|
const insertRuleSchema = z.union([
|
|
z.strictObject({ rule: z.enum(["first", "last"]) }),
|
|
z.strictObject({
|
|
rule: z.literal("sorted"),
|
|
field: z.string(),
|
|
direction: z.enum(["ascending", "descending"]),
|
|
}),
|
|
]);
|
|
const writePlanSchema = z.strictObject({
|
|
tool: z.string(),
|
|
effect: z.enum(["create", "update", "delete", "document", "none"]),
|
|
target: z
|
|
.string()
|
|
.nullable()
|
|
.describe(
|
|
"The entity type an entity effect writes, or the document view a document write changes.",
|
|
),
|
|
served: z.boolean(),
|
|
reason: z
|
|
.enum([
|
|
"no-state-target",
|
|
"no-call",
|
|
"body-unusable",
|
|
"no-observation",
|
|
"unexplained-change",
|
|
"contradicted",
|
|
"side-effect",
|
|
"unconfirmed",
|
|
"incomplete-entities",
|
|
"unknown-clock",
|
|
])
|
|
.nullable(),
|
|
exemplar_ordinal: ordinalSchema
|
|
.nullable()
|
|
.describe("The first successful recorded call the plan was built from."),
|
|
observation: z
|
|
.strictObject({
|
|
ordinal: ordinalSchema,
|
|
pointer: z.string(),
|
|
read: z
|
|
.strictObject({
|
|
ordinal: ordinalSchema,
|
|
pointer: z.string(),
|
|
shape: z.enum(["item", "list"]),
|
|
unloaded: z
|
|
.array(z.string())
|
|
.exactOptional()
|
|
.describe(
|
|
"JSON pointers into the entity to values the response had not loaded, which answers keep as the response showed them.",
|
|
),
|
|
})
|
|
.exactOptional()
|
|
.describe(
|
|
"A create's later read of the entity: an item read lends fields the response lacks and its clock spellings, a list read only adds fields the response lacks.",
|
|
),
|
|
})
|
|
.nullable()
|
|
.describe(
|
|
"The record whose response showed the written entity or document, and a JSON pointer to it.",
|
|
),
|
|
response: z
|
|
.strictObject({
|
|
ordinal: ordinalSchema,
|
|
shape: z.enum(["entity", "identity", "empty", "recorded"]),
|
|
pointer: z.string().nullable(),
|
|
})
|
|
.nullable(),
|
|
body: z
|
|
.strictObject({
|
|
accepted: z.array(z.string()),
|
|
fixed: z.array(
|
|
z.strictObject({ member: z.string(), sent: jsonValueSchema }),
|
|
),
|
|
required: z.array(z.string()),
|
|
identity: z.string().nullable(),
|
|
kinds: z.array(
|
|
z.strictObject({
|
|
member: z.string(),
|
|
kind: z.enum(["array", "boolean", "number", "object", "string"]),
|
|
}),
|
|
),
|
|
envelope: z.string().optional(),
|
|
defaults: z
|
|
.array(z.strictObject({ member: z.string(), value: jsonValueSchema }))
|
|
.optional(),
|
|
blank: z
|
|
.array(z.strictObject({ member: z.string(), sent: jsonValueSchema }))
|
|
.optional(),
|
|
})
|
|
.nullable()
|
|
.describe(
|
|
"Body members the application stored as sent (accepted), members accepted only with their recorded value (fixed), members every call sends (required), the JSON type a later call stored an accepted member as when the observation does not show it (kinds), the member a call wraps its object in as a one-element array (envelope), the value a create stores for an accepted member a call leaves out, as a call that left it out showed (defaults), and members a recorded create sent empty, which a call that leaves them out is applied as sending (blank).",
|
|
),
|
|
rules: z
|
|
.array(fieldRuleSchema)
|
|
.describe(
|
|
"How written fields get their values; unlisted fields of a created entity keep the observed value.",
|
|
),
|
|
collection: z
|
|
.string()
|
|
.nullable()
|
|
.describe(
|
|
"For a create whose recorded values are tied to its collection, the only collection it writes to.",
|
|
),
|
|
insert: insertRuleSchema.nullable(),
|
|
merge: z.enum(["merge", "replace"]).nullable(),
|
|
checked_record_ordinals: z
|
|
.array(ordinalSchema)
|
|
.describe("Later reads that reproduced from the state after this write."),
|
|
contradicted_by: ordinalSchema.nullable(),
|
|
address: z
|
|
.discriminatedUnion("by", [
|
|
z.strictObject({ by: z.literal("action"), segment: z.string() }),
|
|
z.strictObject({
|
|
by: z.literal("ids"),
|
|
member: z.string().nullable(),
|
|
kind: z.enum(["number", "string"]),
|
|
}),
|
|
z.strictObject({
|
|
by: z.literal("elements"),
|
|
member: z.string().nullable(),
|
|
}),
|
|
])
|
|
.optional()
|
|
.describe(
|
|
"How a call names its entities when its path does not end in one: the action segment after the item's id, or the body's ids as an array of one JSON `kind` or as objects carrying the id field, at the root or under `member`.",
|
|
),
|
|
});
|
|
|
|
export const environmentStateFields = {
|
|
origin: z
|
|
.string()
|
|
.describe("Primary origin, as the environment manifest selects it."),
|
|
name: z.string(),
|
|
timeline: z
|
|
.enum(["recorded-start-time", "record-order"])
|
|
.describe(
|
|
"Records are ordered by recorded start time, or by record order when any start time is missing.",
|
|
),
|
|
types: z.array(
|
|
z.strictObject({
|
|
collection_template: z.string(),
|
|
id_field: z.string(),
|
|
id_basis: z
|
|
.enum(["path-parameter", "field-name"])
|
|
.describe(
|
|
"path-parameter: item responses repeat the requested path value in this field; field-name: inferred from listed items as the first identity name, else the first field ending in Id or _id shown distinct across at least two items, that every listed item carries distinctly.",
|
|
),
|
|
init_before_ordinal: z
|
|
.number()
|
|
.int()
|
|
.nonnegative()
|
|
.nullable()
|
|
.describe(
|
|
"First recorded write whose path root relates to the type; state comes from records that finished before it.",
|
|
),
|
|
volatile_fields: z
|
|
.array(z.string())
|
|
.describe(
|
|
"Fields whose recorded values changed with no recorded write between; the state keeps the latest.",
|
|
),
|
|
}),
|
|
),
|
|
entities: z
|
|
.array(
|
|
z.strictObject({
|
|
type: z.string(),
|
|
collection: z.string(),
|
|
id: z.string(),
|
|
fields: jsonObjectSchema,
|
|
view_fields: z.array(
|
|
z.strictObject({ tool: z.string(), fields: jsonObjectSchema }),
|
|
),
|
|
record_ordinals: z.array(z.number().int().nonnegative()),
|
|
}),
|
|
)
|
|
.describe(
|
|
"Entities in collection order: type, collection, then first sighting.",
|
|
),
|
|
documents: z.array(
|
|
z.strictObject({
|
|
tool: z.string(),
|
|
path: z.string(),
|
|
value: jsonValueSchema,
|
|
record_ordinal: z.number().int().nonnegative(),
|
|
}),
|
|
),
|
|
views: z.array(viewSchema),
|
|
writes: z
|
|
.array(writePlanSchema)
|
|
.describe(
|
|
"One plan per tool whose method may change state: its modelled effect, and whether the capture's later reads confirmed it (served).",
|
|
),
|
|
write_clock: z
|
|
.string()
|
|
.nullable()
|
|
.describe(
|
|
"The instant a served clock starts from: the end of the capture's last timed record.",
|
|
),
|
|
unknowns: z.array(
|
|
z.strictObject({
|
|
kind: z.enum([
|
|
"authorization-model",
|
|
"response-semantics",
|
|
"state-transition",
|
|
]),
|
|
statement: z.string(),
|
|
}),
|
|
),
|
|
};
|
|
|
|
/** State extraction and coverage share the same budget and template-rule policy. */
|
|
export const createEnvironmentStateAccumulator = (
|
|
view: Extract<
|
|
AnalysisViewRequest,
|
|
{ kind: "environment-state" | "environment-coverage" }
|
|
>,
|
|
): EnvironmentStateAccumulator =>
|
|
new EnvironmentStateAccumulator(undefined, view.template_rules);
|
|
|
|
/** Extract the init state a capture's reads observed and each read tool's render plan; types and identity are inferences. */
|
|
export const projectEnvironmentStateView = (
|
|
parent: AnalysisViewParent,
|
|
view: Extract<AnalysisViewRequest, { kind: "environment-state" }>,
|
|
accumulator = createEnvironmentStateAccumulator(view),
|
|
): Result<
|
|
Extract<UnsignedAnalysisView, { kind: "environment-state" }>,
|
|
AnalysisError
|
|
> => {
|
|
const grouped = groupCaptureRecords(parent, view.kind, view.template_rules, {
|
|
accumulator,
|
|
noun: "state",
|
|
limitations: [
|
|
"Entity types, identity fields, list membership and counters are inferences from recorded response bodies; the state does not establish server-side schemas, derived values or values copied outside entity objects.",
|
|
],
|
|
});
|
|
if (!grouped.ok) return grouped;
|
|
const state = accumulator.finishState();
|
|
if (!state.ok) return state;
|
|
return ok({
|
|
kind: "environment-state",
|
|
view,
|
|
parent_evidence_id: parent.evidenceId,
|
|
parent_operation: grouped.value.operation,
|
|
parent_digest: parent.evidenceId.slice(3),
|
|
artifact: {
|
|
path: grouped.value.artifact.path,
|
|
sha256: grouped.value.artifact.sha256,
|
|
},
|
|
...state.value,
|
|
});
|
|
};
|