Conversation
b6aeb70 to
b53964b
Compare
b53964b to
f41b38f
Compare
f41b38f to
a6dd906
Compare
a6dd906 to
8104365
Compare
8104365 to
955445c
Compare
955445c to
9259552
Compare
9568f77 to
549dc26
Compare
d644394 to
e96f40c
Compare
e96f40c to
0c5b866
Compare
0c5b866 to
739efc7
Compare
739efc7 to
d097c95
Compare
d097c95 to
1328018
Compare
1328018 to
5f6e5eb
Compare
|
from CODEX: Findings:
|
5f6e5eb to
84cbd60
Compare
|
All three addressed.
On your eviction question: yes for the reference store, and the cap above is it. For the Drivine store, retention past that is the durable store's own policy, kept for as long as an operator decides, and a sweep on |
84cbd60 to
9828921
Compare
9828921 to
1060a1c
Compare
The state machine lives in one place: save accepts only RUNNING runs, and ExtractionRunTransition applied by the store under compare-and-set is the only way a run reaches a terminal status. Replaying the same terminal payload succeeds; an incompatible rewrite is rejected by insert-or-compare on a fingerprint of the transition payload alone, so a replay survives an interleaved invocation record while a materially different retry cannot pass as one. Every scoped read is bounded and scopes before it limits; lineage walks are bounded and cycle-safe, and all runs of a root resolve in one read off the denormalized ref. Each kind of state has exactly one owner. Header writes carry a version and take it under compare-and-set, and they write header fields only: whatever invocation list a caller hands to save is carried through untouched, and an insert starts with no invocation rows at all. recordInvocation is the only door onto invocation state, insert-or-compare on the invocation's own key. A header save therefore cannot create, update or delete an invocation row, and a stale header carrying an old snapshot leaves a newer stored record alone. Run state accumulates from independent writers, the way a lineage event stream does; one writer's row is never another writer's to rewrite. The cross-backend contract suite carries the discriminating cases, so the Drivine store inherits these semantics with nothing left to remember: interleaved replay, lineage disagreement, the terminal matrix, transition races that prove which write landed, ownership of invocation rows across both doors, and concurrent writers on separate attempts both landing. Every field of a run and of an invocation record is pinned through the door that writes it, including clearing a stored value back to null. Terminal records are locked alike whatever outcome they carry, through recordInvocation. Closed value sets are exercised by iterating their own entries, so a newly declared constant is covered the day it is added. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
The terminal fingerprint covered counts and failures, so any change to their shape would have changed a persisted format; it now hashes status and finishedAt alone under xrun-terminal:v2, and counts and failures ride as data. A typed product outcome in a later slice changes no stored byte. The store announces an applied transition through ExtractionRunTransitioned, emitted once where the transition is accepted: a replay announces nothing and a rejected write announces nothing, pinned by contract. The suite carries the reshaped failure vocabulary throughout. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
The absent-by-decision list still said no reference type exists, while the sections above it describe the shipped specification. The bullet now records both facts: the first cut was removed for having no runtime path, and the interface returned as a written contract the host implements. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
The KDoc on ExtractionRunStore.save said a first save returned the version the caller named plus one. It returns 0: the version the caller named, now confirmed by the store. An accepted update returns the stored version plus one, and a replay returns the stored version unchanged. The contract test now pins all three. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
InMemoryExtractionRunStore kept every run and every terminal fingerprint forever, which is unbounded retention for a store meant as a reference and sometimes a bridge before a host has a database. Add a maxRuns constructor parameter, defaulting to 10,000, as the last parameter so the existing Java descriptors survive. When an insert would push the store past the cap, evict the oldest ended runs by startedAt until it fits again, dropping each evicted run's terminal fingerprint with it. A run still RUNNING is never evicted, so a store where every run happens to be running can grow past the cap; that breach logs once at warn, not on every insert. Say plainly in the class KDoc, and in the ExtractionRunStore contract KDoc, that retention past the cap is the reference store's own policy, not something the contract promises, and that a host running it in production is accepting that runs older than the cap are gone. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
page() filtered and sorted runs.values while holding the monitor, so every scoped read was O(all tenants' runs), and a busy neighbour tenant paid the cost of every other tenant's page too. Keep a per-tenant index, contextId to that tenant's own run keys in insertion order, maintained in exactly two places: an insert adds a key, and eviction removes one. A run's value is always looked up fresh in the main map, so nothing has to touch the index when a header is saved again, a run transitions, or an invocation is recorded against it. runsInContext, childrenOf and runsOfRoot now iterate only their own tenant's candidates before filtering, ordering and limiting; the rule that scope comes before the limit is unchanged, only the candidate set a page starts from shrinks. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
transition announced the terminal run outside the monitor and let the listener's exception reach the caller after the write had landed. A caller retrying on that exception got REPLAYED and no event either time. The announcement is now caught: the failure is logged at error with the run's key and the result is returned as applied, because the store's job ended when the write did. The design note and the changelog say so. Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
1060a1c to
68c5f8a
Compare
PR 7 of the extraction-integrity train (refs #67), stacked on #95. The run lifecycle and store contract, in
dicecore. The state machine lives in one place:saveaccepts only RUNNING runs, andExtractionRunTransition.applyTounder store compare-and-set is the only way a run reaches COMPLETED/FAILED/CANCELLED — nothing can manufacture a terminal run without a store. Idempotency has teeth: replaying the same terminal payload succeeds; an incompatible rewrite is rejected by insert-or-compare on a SHA-256 fingerprint of the transition payload, deterministic and golden-literal-locked.ExtractionRunStoregives tenant-scoped bounded pages (scope before limit), bounded cycle-safe chain walks, whole-lineage lookup in one read off the denormalized root, and invocation persistence that survives header saves. The cross-backend contract suite carries the discriminating cases — interleaved replay, lineage disagreement, the terminal-by-terminal matrix, a transition race, invocation preservation — so the Drivine store inherits the semantics.Changed in this review round:
(status, finishedAt). Counts and failures travel as data on the transition and still participate in its equality, so Return typed product outcomes for batched and linked extraction passes #69's typed product outcomes change no persisted fingerprint format.TERMINAL_VERSIONmoved toxrun-terminal:v2once, before any release, and the design doc states the choice.ExtractionRunTransitionedis a newDiceEventcarrying the terminal run; the in-memory store takes aDiceEventListenercollaborator and emits after the write lands, gated on the result being applied. Nothing is wired automatically — the audit projection and host listeners have their subscription point.ProtectedContentRefis deferred out: DICE had no writer, reader, or retention behavior for it; it returns with the first runtime path that uses it. The doc states the reference as it now stands.Breaking changes: none. New types only; the header-version parameter's binary consequences are enumerated in the CHANGELOG (which descriptors it preserves, which it moves, and what happens to a caller that never passes one).
Opt-in and status: EXPERIMENTAL,
@ApiStatus.Experimentalon every type. Nothing calls the store yet; the coordinator is the first real caller and is sequenced next.stateDiagram-v2 [*] --> RUNNING: save accepts RUNNING only RUNNING --> RUNNING: save under versioned compare-and-set RUNNING --> SUCCEEDED: transition RUNNING --> FAILED: transition RUNNING --> CANCELLED: transition SUCCEEDED --> SUCCEEDED: identical replay succeeds FAILED --> FAILED: identical replay succeeds CANCELLED --> CANCELLED: identical replay succeedsTwo write doors, and what each promises:
flowchart TD A[save: whole header] --> V{version matches stored?} V -- no --> R[rejected, stored row untouched] V -- yes --> W[header replaced, invocations merged by identity] B[recordInvocation: one child row] --> T{record already terminal?} T -- yes --> F{payload fingerprint identical?} F -- yes --> K[accepted as replay, nothing moves] F -- no --> R2[rejected] T -- no --> M[child row inserted or compared] W --> E[applied terminal transition emits ExtractionRunTransitioned]Closed value sets are exercised by iterating their own
entries, so a newly declared constant is covered the day it is added.ExtractionRunStore.savestates the returned version for each kind of save. A first save returns version 0 (the version the caller named, now confirmed by the store), an accepted update the stored version plus one, and a replay the stored version unchanged. The KDoc used to say a first save returned the caller's version plus one. The contract test pins all three.