Skip to content

Commit 9ccd43c

Browse files
committed
Refuse an evicted-read replay of a later passage that may have become a twin
Review of the previous commit found that, without the Planner's read, a replay of a multi-passage card was checked against where the card stands for its first passage only. A later passage was judged by its anchor, which can have been recovered by digest onto an identical copy of a rewritten paragraph, so the replay was reported as captured while the same input with the read retained was refused. Without the read, a replay is now accepted only for a card standing beside the one passage it was opened on. A card with several passages is refused, with the cause that the server no longer has that read, until the Planner reads the document again; a retry at a fresh revision is judged normally and stays idempotent. The decisions guide states this. Assistant-model: Claude Sonnet 5.5 Assistant-workflow: goal (run 29d6269b-6026-4c1f-8a50-399c757f21c0) Assistant-duration: unmeasured converged, estimated unmeasured Assistant-verification: red before fix passed: with the one-line change reverted, the new capture-decisions test (a card on two passages, the second rewritten beside an identical copy, the read evicted by nine later outlines, then a replay) failed and all 35 pass with it Assistant-verification: bun test unit passed: GIT_CONFIG_GLOBAL=/dev/null bun test on this head (6813 pass, 0 fail, 22 skipped for TEST_DATABASE_URL) Assistant-verification: bun run types passed: whole workspace and e2e, on this head Assistant-verification: bun run ci passed: dprint, oxlint (72 existing warnings, none new), tokens and design checks, on this head Assistant-verification: bun run e2e passed: 43 tests across harness-decisions-deck, planner-decisions, run-card, planner-attachment, open-ended-decisions, decision-prose and inline-decision-deck on this head Assistant-verification: bun run test:postgres unavailable: not run, storage is unchanged Co-authored-by: Alex Lavaee <lavaman131@github.com>
1 parent fe6bef9 commit 9ccd43c

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎apps/server/src/questions/capture-decisions.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,28 @@ test("a stale address whose index disagrees with its digest is refused as the fr
643643
await edited(context, [{ op: "insert_root", source: "A late note." }]);
644644
await expectNothingCreated(context, input, 0, "does not match the block you read");
645645
});
646+
647+
test("a repeated key whose read was evicted is refused when a later passage was rewritten beside an identical copy", async () => {
648+
let context = await setup(
649+
"# Draft\n\nProvisional onboarding.\n\nProvisional collaboration.\n\nProvisional pricing.",
650+
);
651+
let single = question(context.plan, "decision-one", 1);
652+
let digests = Room.digests(context.plan.document);
653+
let input = {
654+
revision: 0,
655+
questions: [{ ...single, blocks: [1, 2].map(index => ({ index, digest: digests[index]! })) }],
656+
};
657+
await Capture.captureDecisions(context.plan, context.server, context.plan.id, input);
658+
await edited(context, [{
659+
op: "replace",
660+
index: 3,
661+
source: "Provisional collaboration, rewritten.",
662+
}]);
663+
await edited(context, [{ op: "insert_root", source: "Provisional collaboration." }]);
664+
for (let note = 0; note < 9; note++) {
665+
await edited(context, [{ op: "insert_root", source: `Note ${note}.` }]);
666+
Edit.outline(context.plan);
667+
}
668+
expect(Edit.read(context.plan, 0)).toBeUndefined();
669+
await expectNothingCreated(context, input, 1, "the server no longer has that read");
670+
});

‎apps/server/src/questions/service-publish.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,9 @@ function repeats(
256256
let located = locate(plan, readAt, block);
257257
return !("cause" in located) && located.index === index;
258258
}
259-
// Without the read, only where the card stands says which paragraph it was opened beside.
260-
return position > 0 || standing === index;
259+
// Without the read, only where the card stands says which paragraph it was opened beside,
260+
// and it stands beside the first passage alone; a later one cannot be told from a twin.
261+
return position === 0 && standing === index;
261262
});
262263
}
263264

‎docs/decisions.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ must still carry it, now alone, wherever it stands. A paragraph rewritten since,
3939
copy now repeats, one that was already repeated when it was read, and a read the server no longer
4040
retains each refuse the batch with a message saying the document changed and to read it again,
4141
rather than anchoring a card beside a twin. Nothing is created for a refused batch.
42+
Replaying a key whose card exists is judged against the same read. Where the server no longer
43+
holds it (after a restart, or once more than eight revisions have been read since), a replay is
44+
accepted only for a card standing beside the single passage it was opened on; a card with several
45+
passages is refused until the Planner reads again, since a later passage cannot then be told from
46+
an identical copy.
4247
Stop Planner does not withdraw committed cards.
4348

4449
Each Save queues an update to the related prose. Decisions sharing a passage share its write

0 commit comments

Comments
 (0)