mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 05:46:17 +00:00
fix(environment): learn a sort from a rename only when moving its entities reaches it
Serving moves only the entities a write changes in a collection kept sorted, so replay now tries a sort after a rename only when the entities no write the read exercised changed already follow it. Seeded folders listed out of title order no longer teach a title sort that serving would answer with another order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3de7d9a7ae
commit
28cda1f3f6
5 files changed
+67
-5
No files matched your search
@@ -31,7 +31,8 @@ natural-language tasks ask for, or withheld them:
|
||||
- **Sorted collections.** An update that writes the field its type's creates are sorted by moves its entity to where
|
||||
the new value sorts, in replay and serving (a bulk update moving each entity in turn), and its plan carries that
|
||||
`insert`. When a read listing the whole collection fails after an update and the type's creates are not sorted by a
|
||||
field the update writes, replay tries the collection sorted by such a field, unless a create was placed by a sort
|
||||
field the update writes, replay tries the collection sorted by such a field when the entities no exercised write
|
||||
changed already follow it, as serving moves only written entities, and unless a create was placed by a sort
|
||||
that excludes it or a whole-collection read in collection order (not sorted by its query) that placed a create did
|
||||
not follow it; on a match, the type's creates and updates take that order.
|
||||
- **Unloaded answers.** A create's later item read is taken as its observation when it differs from the answer only in
|
||||
|
||||
@@ -857,7 +857,9 @@ update that writes the field its type's creates are sorted by moves its entity
|
||||
to where the new value sorts (its own `insert`); a bulk update moves each entity
|
||||
it writes in turn, as replay applies them. When a read listing the whole
|
||||
collection fails after an update and the type's creates are not sorted by a field
|
||||
that update writes, the view tries the collection sorted by such a field, unless
|
||||
that update writes, the view tries the collection sorted by such a field, when
|
||||
the entities no write it exercised changed already follow it, as serving moves
|
||||
only the entities a write changes, and unless
|
||||
a create was placed by a sort that excludes it or a read listing the whole
|
||||
collection in its own order, not one its query sorts, that placed a create did
|
||||
not follow it; on a match, the type's creates
|
||||
|
||||
@@ -85,7 +85,24 @@ import {
|
||||
} from "./environmentStateWriteRules.js";
|
||||
import { WriteTaint } from "./environmentStateWriteTaint.js";
|
||||
import { writeVerdict } from "./environmentStateWriteVerdict.js";
|
||||
import { orderedBy } from "./environmentStateWriteExplain.js";
|
||||
import { compareValues, orderedBy } from "./environmentStateWriteExplain.js";
|
||||
|
||||
/** Whether members are in a sorted rule's order, ties kept in place as an insert keeps them. */
|
||||
const keptInOrder = (
|
||||
members: readonly JsonObject[],
|
||||
rule: Extract<InsertRule, { rule: "sorted" }>,
|
||||
): boolean =>
|
||||
members.every((member, index) => {
|
||||
if (index === 0) return true;
|
||||
const order = compareValues(
|
||||
ownMember(members[index - 1]!, rule.field),
|
||||
ownMember(member, rule.field),
|
||||
);
|
||||
return (
|
||||
order !== undefined &&
|
||||
(rule.direction === "ascending" ? order <= 0 : order >= 0)
|
||||
);
|
||||
});
|
||||
|
||||
const APPLY = {
|
||||
create: applyCreate,
|
||||
@@ -135,6 +152,8 @@ export class EnvironmentStateWriteSimulation {
|
||||
}[] = [];
|
||||
/** Entities a create inserted, by target, with the create's tool. */
|
||||
readonly #inserted = new Map<string, string>();
|
||||
/** The targets each write tool changed, which serving moves when their collection is kept sorted. */
|
||||
readonly #wrote = new Map<string, Set<string>>();
|
||||
/** Creates whose insert rule a passing list check relied on, so it is no longer changed. */
|
||||
readonly #placed = new Set<string>();
|
||||
/** The order a read showed a type's collections sorted in after an update, when no create of it showed one. */
|
||||
@@ -442,11 +461,15 @@ export class EnvironmentStateWriteSimulation {
|
||||
return;
|
||||
}
|
||||
this.#effects.supersede(this.#touched, write.tool, result);
|
||||
for (const target of result.targets)
|
||||
const wrote = this.#wrote.get(write.tool) ?? new Set<string>();
|
||||
this.#wrote.set(write.tool, wrote);
|
||||
for (const target of result.targets) {
|
||||
wrote.add(target);
|
||||
touchTarget(this.#touched, this.#touchedSince, target, {
|
||||
...write,
|
||||
fields: result.fields,
|
||||
});
|
||||
}
|
||||
this.#log.push(write);
|
||||
if (result.inserted === undefined) return;
|
||||
this.#inserted.set(result.inserted, write.tool);
|
||||
@@ -925,8 +948,20 @@ export class EnvironmentStateWriteSimulation {
|
||||
),
|
||||
),
|
||||
) ?? [];
|
||||
// Serving moves only the entities an update writes, so the others must already be in the order tried.
|
||||
const kept = this.#state
|
||||
.members(view.type, line.collection)
|
||||
.filter((entity) => {
|
||||
const target = entityTarget(entity.type, entity.collection, entity.id);
|
||||
return ![...exercised].some((name) =>
|
||||
this.#wrote.get(name)?.has(target),
|
||||
);
|
||||
})
|
||||
.map((entity) => entity.fields);
|
||||
for (const rule of rules.filter(
|
||||
(candidate) => current === undefined || !same(candidate, current),
|
||||
(candidate) =>
|
||||
(current === undefined || !same(candidate, current)) &&
|
||||
keptInOrder(kept, candidate),
|
||||
)) {
|
||||
// A create placed by another sort shows the collection is not kept in this order.
|
||||
if (
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SHELF_SORTED_ROWS,
|
||||
TITLE_ORDER_ROWS,
|
||||
UNLOADED_ANSWER_ROWS,
|
||||
UNSORTED_SEED_ROWS,
|
||||
} from "../../../tests/fixtures/environmentStateFormWrites.js";
|
||||
import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js";
|
||||
import { mergedCreatePlan } from "./environmentStateWriteCreateCalls.js";
|
||||
@@ -162,6 +163,12 @@ describe("collections kept sorted", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("learns no sort from a rename that moving the renamed entity alone cannot reach", () => {
|
||||
const plan = planOf(UNSORTED_SEED_ROWS, "put_folders_by_id");
|
||||
expect(plan.served).toBe(false);
|
||||
expect(plan.insert).toBeNull();
|
||||
});
|
||||
|
||||
it("serves a bulk update whose recordings echo the empty values each call sent", () => {
|
||||
expect(planOf(BULK_EMPTY_TAG_ROWS, "patch_labels")).toMatchObject({
|
||||
served: true,
|
||||
|
||||
+17
@@ -422,3 +422,20 @@ export const SHELF_SORTED_ROWS: readonly TimedCaptureRow[] = (() => {
|
||||
["GET", "/api/books", [renamed, alpha, bravo]],
|
||||
];
|
||||
})();
|
||||
|
||||
/** Seeded folders listed out of title order, then a rename after which a list read happens to show title order. */
|
||||
export const UNSORTED_SEED_ROWS: readonly TimedCaptureRow[] = (() => {
|
||||
const [charlie, bravo, alpha] = [
|
||||
folder(1, "Charlie"),
|
||||
folder(2, "Bravo"),
|
||||
folder(3, "Alpha"),
|
||||
];
|
||||
const renamed = folder(1, "Zulu", 2);
|
||||
return [
|
||||
["GET", "/api/folders", [charlie, bravo, alpha]],
|
||||
["GET", "/api/folders/1", charlie],
|
||||
["PUT", "/api/folders/1", renamed, 200, { title: "Zulu", version: 1 }],
|
||||
["GET", "/api/folders/1", renamed],
|
||||
["GET", "/api/folders", [alpha, bravo, renamed]],
|
||||
];
|
||||
})();
|
||||
Reference in new issue
Block a user