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:
David Snelling 2026-08-17 16:21:25 -07:00
parent f4653e47c9
commit 9ac9e70686
4 changed files with 308 additions and 7 deletions

View file

@ -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

View file

@ -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
}
// ============================================================================