mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 07:16:13 +00:00
fix(environment): keep first sightings, one list pointer and linear field unions
Entities took the order of the first plan observed, and item plans run before list plans, so a complete list read before its detail reads came out reordered and lost "all" membership; each entity now keeps its earliest recorded sighting. A list plan reused the first recording's pointer for later recordings, so a later response that was itself an entity wrapping a list was read as that list; every recording must now locate the same list. The field union scanned its ordered list for every key, quadratic in an object's width; a set now answers membership. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2c9a2e446
commit
2999128bae
4 files changed
+55
-3
No files matched your search
@@ -286,7 +286,9 @@ export class EnvironmentStateExtraction {
|
||||
for (const record of recordings) {
|
||||
const found = this.#withBody(record.ordinal, (body) => {
|
||||
if (body === undefined) return null;
|
||||
const located = pointer ?? locateList(body, proved);
|
||||
// Every recording must locate the same list, so a later entity envelope is not taken as one.
|
||||
const located = locateList(body, proved);
|
||||
if (pointer !== undefined && located !== pointer) return undefined;
|
||||
const items =
|
||||
located === undefined ? undefined : listItems(body, located);
|
||||
if (items === undefined) return undefined;
|
||||
@@ -359,6 +361,12 @@ export class EnvironmentStateExtraction {
|
||||
observations: [],
|
||||
};
|
||||
this.#entities.set(key, entity);
|
||||
} else {
|
||||
// Plans are observed item tools first, so keep the earliest sighting in timeline order.
|
||||
const position = this.#position.get(record.ordinal)!;
|
||||
const [first, at] = entity.order;
|
||||
if (position < first || (position === first && index < at))
|
||||
entity.order = [position, index];
|
||||
}
|
||||
// Only a repeated request line shows a value changing; other lines may differ by query.
|
||||
const line = requestLine(record);
|
||||
|
||||
@@ -53,11 +53,16 @@ export interface StateGateContext {
|
||||
/** Ordered key union and always-present keys, grown one object at a time. */
|
||||
class FieldUnion {
|
||||
readonly fields: string[] = [];
|
||||
/** Membership beside the ordered list, so a wide object is not scanned per key. */
|
||||
readonly #seen = new Set<string>();
|
||||
#required: Set<string> | undefined;
|
||||
|
||||
add(object: JsonObject): void {
|
||||
for (const key of Object.keys(object))
|
||||
if (!this.fields.includes(key)) this.fields.push(key);
|
||||
if (!this.#seen.has(key)) {
|
||||
this.#seen.add(key);
|
||||
this.fields.push(key);
|
||||
}
|
||||
const keys = new Set(Object.keys(object));
|
||||
this.#required =
|
||||
this.#required === undefined
|
||||
|
||||
@@ -21,7 +21,8 @@ export interface ObservedEntity {
|
||||
readonly collection: string;
|
||||
readonly id: string;
|
||||
/** Timeline position of the first observation, then its index within that body. */
|
||||
readonly order: readonly [number, number];
|
||||
/** The earliest recorded sighting: timeline position, then position in its list. */
|
||||
order: readonly [number, number];
|
||||
readonly observations: StateObservation[];
|
||||
}
|
||||
|
||||
|
||||
@@ -393,3 +393,41 @@ it("does not call a value volatile when it differs between request lines", () =>
|
||||
),
|
||||
).toEqual([["get_items_by_id", "fields-differ"]]);
|
||||
});
|
||||
|
||||
it("orders entities by their earliest sighting, so a complete list keeps all membership", () => {
|
||||
const state = stateOf(
|
||||
captureOf([
|
||||
[
|
||||
"GET",
|
||||
"/api/items",
|
||||
[
|
||||
{ id: 1, title: "Granite Ledger" },
|
||||
{ id: 2, title: "Blue Heron" },
|
||||
],
|
||||
],
|
||||
["GET", "/api/items/2", { id: 2, title: "Blue Heron" }],
|
||||
["GET", "/api/items/1", { id: 1, title: "Granite Ledger" }],
|
||||
]),
|
||||
);
|
||||
expect(state.entities.map((entity) => entity.id)).toEqual(["1", "2"]);
|
||||
expect(state.views).toContainEqual(
|
||||
expect.objectContaining({ tool: "get_items", membership: "all" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not take a later recording that is itself an entity as the list", () => {
|
||||
const state = stateOf(
|
||||
captureOf([
|
||||
["GET", "/api/items", { items: [{ id: 1, title: "Granite Ledger" }] }],
|
||||
[
|
||||
"GET",
|
||||
"/api/items",
|
||||
{ id: 99, items: [{ id: 2, title: "Blue Heron" }] },
|
||||
],
|
||||
]),
|
||||
);
|
||||
expect(state.views).toContainEqual(
|
||||
expect.objectContaining({ tool: "get_items", shape: "recorded" }),
|
||||
);
|
||||
expect(state.entities).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user