feat(log): system commits carry their origin; the attested per-id reconcile door
Two consumer-driven cures sharing one stamp. (1) TX-LOG ORIGIN: engine-
originated commits stamp an optional origin on their tx-log entry AND the
commit fact's meta — 'system:embed-landing' (the deferred vector landing),
'system:adoption-backfill' (baseline re-commits), 'system:reconcile'. A
downstream activity feed showed a double tick because the landing commit was
indistinguishable from a user save, and the consumer rightly refused a
time-window collapse as a quiet loss; feeds now filter on fact. User writes
stay unstamped — absent origin is the user shape, every existing consumer
unchanged. (2) reconcileLogDivergence(id, {attest}): the human's door for
log-live-canonical-absent, the one class adoption refuses by design because
a lost-tombstone deletion is indistinguishable from canonical loss.
'deleted' mints the missing tombstone (history keeps the earlier live
record); 'restore' folds the log's only copy back into canonical; wrong-
class calls refuse typed with nothing written. Loud, narrated, single-row,
origin-stamped. From a production adoption's one surviving divergence.
This commit is contained in:
parent
f4653e47c9
commit
9ac9e70686
4 changed files with 308 additions and 7 deletions
|
|
@ -428,7 +428,13 @@ export class GenerationStore {
|
|||
private pendingGens: number[] = []
|
||||
private readonly pendingBuffer = new Map<
|
||||
number,
|
||||
{ nouns: Map<string, GenerationRecord>; verbs: Map<string, GenerationRecord>; timestamp: number }
|
||||
{
|
||||
nouns: Map<string, GenerationRecord>
|
||||
verbs: Map<string, GenerationRecord>
|
||||
timestamp: number
|
||||
/** Engine-origin stamp for the tx-log entry (absent = user write). */
|
||||
origin?: string
|
||||
}
|
||||
>()
|
||||
/** Pending timer-coalesced flush handle (cleared on flush/close). */
|
||||
private pendingFlushTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
|
@ -1642,6 +1648,12 @@ export class GenerationStore {
|
|||
* surfacing that honestly.
|
||||
*/
|
||||
records?: FactMarkerRecord[]
|
||||
/**
|
||||
* Engine-origin stamp (`'system:embed-landing'`, `'system:adoption-backfill'`,
|
||||
* `'system:reconcile'`). Rides the tx-log entry AND the commit fact's meta,
|
||||
* so both records agree about WHO committed. Absent = user write.
|
||||
*/
|
||||
origin?: string
|
||||
}): Promise<{ generation: number; timestamp: number; degraded?: string[] }> {
|
||||
return this.withMutex(async () => {
|
||||
// Refuse to accept a write whose history we cannot make durable: if the
|
||||
|
|
@ -1710,7 +1722,7 @@ export class GenerationStore {
|
|||
// incomplete for these ids until the next rebuild/repairIndex (the
|
||||
// egress guard prevents wrong results meanwhile). Loud, honest,
|
||||
// no double-write.
|
||||
this.pendingBuffer.set(gen, { nouns: nounBefore, verbs: verbBefore, timestamp })
|
||||
this.pendingBuffer.set(gen, { nouns: nounBefore, verbs: verbBefore, timestamp, ...(args.origin ? { origin: args.origin } : {}) })
|
||||
this.pendingGens.push(gen)
|
||||
this.extendChains(gen, nouns, verbs)
|
||||
// The adopted generation is committed — it gets its fact like any
|
||||
|
|
@ -1723,6 +1735,7 @@ export class GenerationStore {
|
|||
timestamp,
|
||||
nouns,
|
||||
verbs,
|
||||
...(args.origin ? { meta: { origin: args.origin } } : {}),
|
||||
...(args.records && args.records.length > 0 ? { records: args.records } : {})
|
||||
})
|
||||
)
|
||||
|
|
@ -1763,7 +1776,7 @@ export class GenerationStore {
|
|||
if (this.commitFaultInjector) this.commitFaultInjector('singleop-after-execute')
|
||||
|
||||
// Buffer the pending generation + make it instantly visible to reads.
|
||||
this.pendingBuffer.set(gen, { nouns: nounBefore, verbs: verbBefore, timestamp })
|
||||
this.pendingBuffer.set(gen, { nouns: nounBefore, verbs: verbBefore, timestamp, ...(args.origin ? { origin: args.origin } : {}) })
|
||||
this.pendingGens.push(gen)
|
||||
this.extendChains(gen, nouns, verbs)
|
||||
// Fact log (dual-write): the acked write's AFTER-IMAGE fact, appended
|
||||
|
|
@ -1802,6 +1815,7 @@ export class GenerationStore {
|
|||
timestamp,
|
||||
nouns,
|
||||
verbs,
|
||||
...(args.origin ? { meta: { origin: args.origin } } : {}),
|
||||
...(args.records && args.records.length > 0 ? { records: args.records } : {})
|
||||
})
|
||||
)
|
||||
|
|
@ -1958,7 +1972,7 @@ export class GenerationStore {
|
|||
const deltaPath = `${dir}/tx.json`
|
||||
await this.storage.writeRawObject(deltaPath, delta)
|
||||
stagedPaths.push(deltaPath)
|
||||
logEntries.push({ generation: gen, timestamp: buf.timestamp })
|
||||
logEntries.push({ generation: gen, timestamp: buf.timestamp, ...(buf.origin ? { origin: buf.origin } : {}) })
|
||||
}
|
||||
|
||||
// Test-only crash simulation. A crash here must cost only the window's
|
||||
|
|
|
|||
|
|
@ -412,6 +412,17 @@ export interface TxLogEntry {
|
|||
timestamp: number
|
||||
/** Transaction metadata, when supplied to `transact()`. */
|
||||
meta?: Record<string, unknown>
|
||||
/**
|
||||
* WHO committed. Absent = a user write (every pre-existing consumer's
|
||||
* reading stays exact). Engine-originated commits stamp themselves —
|
||||
* `'system:embed-landing'` (the deferred vector landing),
|
||||
* `'system:adoption-backfill'` (baseline re-commits), `'system:reconcile'`
|
||||
* (the attested per-id divergence door) — so activity feeds can filter on
|
||||
* fact instead of collapsing near-in-time entries (a consumer refused that
|
||||
* heuristic as a quiet loss, correctly; this field is the honest cure).
|
||||
* The same stamp rides the commit fact's meta, so log and tx-log agree.
|
||||
*/
|
||||
origin?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue