rcs-context 0.0.0-stage → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1331 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ function encodeCanonical(value, ancestors) {
4
+ if (value === null) return 'null'
5
+
6
+ switch (typeof value) {
7
+ case 'string':
8
+ return JSON.stringify(value.normalize('NFC'))
9
+ case 'boolean':
10
+ return value ? 'true' : 'false'
11
+ case 'number':
12
+ if (!Number.isFinite(value)) throw new TypeError('canonical JSON rejects non-finite numbers')
13
+ return Object.is(value, -0) ? '0' : JSON.stringify(value)
14
+ case 'undefined':
15
+ case 'bigint':
16
+ case 'function':
17
+ case 'symbol':
18
+ throw new TypeError(`canonical JSON rejects ${typeof value}`)
19
+ case 'object':
20
+ break
21
+ default:
22
+ throw new TypeError('canonical JSON rejects unknown value type')
23
+ }
24
+
25
+ if (ancestors.has(value)) throw new TypeError('canonical JSON rejects cyclic objects')
26
+ ancestors.add(value)
27
+ try {
28
+ if (Array.isArray(value)) {
29
+ return `[${Array.from(value, (item) => encodeCanonical(item, ancestors)).join(',')}]`
30
+ }
31
+
32
+ const prototype = Object.getPrototypeOf(value)
33
+ if (prototype !== Object.prototype && prototype !== null) {
34
+ throw new TypeError('canonical JSON accepts only plain objects and arrays')
35
+ }
36
+ if (Object.getOwnPropertySymbols(value).length > 0) {
37
+ throw new TypeError('canonical JSON rejects symbol keys')
38
+ }
39
+
40
+ const keys = Object.keys(value)
41
+ if (keys.length === 1 && keys[0] === '$set') {
42
+ if (!Array.isArray(value.$set)) throw new TypeError('canonical set wrapper requires an array')
43
+ const elements = Array.from(value.$set, (item) => encodeCanonical(item, ancestors)).sort()
44
+ return `{"$set":[${elements.join(',')}]}`
45
+ }
46
+
47
+ const entries = keys.map((key) => [key.normalize('NFC'), value[key]])
48
+ entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
49
+ for (let index = 1; index < entries.length; index++) {
50
+ if (entries[index - 1][0] === entries[index][0]) {
51
+ throw new TypeError('canonical JSON rejects key collisions after NFC normalization')
52
+ }
53
+ }
54
+ return `{${entries.map(([key, item]) =>
55
+ `${JSON.stringify(key)}:${encodeCanonical(item, ancestors)}`).join(',')}}`
56
+ } finally {
57
+ ancestors.delete(value)
58
+ }
59
+ }
60
+
61
+ export function canonicalJson(value) {
62
+ return encodeCanonical(value, new Set())
63
+ }
64
+
65
+ export function sha256Canonical(value) {
66
+ return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex')
67
+ }
68
+
69
+ const fileContract = {
70
+ allowedSources: ['observation', 'probe'],
71
+ cardinality: 'single',
72
+ mutable: true,
73
+ entityBound: true,
74
+ invalidationRule: 'observation-or-entity-generation',
75
+ conflictRule: 'causal-order',
76
+ injectable: true,
77
+ // Deterministic facts keep their historical observation/probe ordering. Their
78
+ // conflict rule is causal order, so this list is documentation of the static
79
+ // rank rather than an input to a value conflict.
80
+ sourceAuthorityOrder: ['observation', 'probe'],
81
+ }
82
+
83
+ const projectContract = {
84
+ // `llm` is the only non-user source a project fact may carry, and only through
85
+ // the proposal engine: deterministic quote validation derives the value from an
86
+ // archived user message and stores `verification: 'derived'`. File and synthetic
87
+ // predicates still refuse it, and rendering keeps refusing non-injectable
88
+ // verification, so a hand-written unverified llm assertion stays queryable but
89
+ // never enters injected context.
90
+ allowedSources: ['user', 'llm'],
91
+ // Project rules are independent facts, not alternative values of one setting:
92
+ // "金额用整数分" and "测试用 node:test" must coexist. Each rule gets a stable
93
+ // valueKey (the exact rule value for an assert, the target's key for a
94
+ // correction/retraction), so equal-authority conflicts are scoped to the same
95
+ // rule rather than to the whole predicate. The slot is the exact rule value, not
96
+ // a semantic topic: changing a setting's value must use correct/retract; two
97
+ // plain asserts of the same topic remain separate rules until a semantic slot
98
+ // extractor is added.
99
+ cardinality: 'multi',
100
+ mutable: true,
101
+ entityBound: false,
102
+ invalidationRule: 'explicit-correction-or-retraction',
103
+ conflictRule: 'equal-authority-conflict',
104
+ // Static, registry-declared authority. The model can never declare it. A user
105
+ // statement outranks an LLM proposal that merely restates the same archived
106
+ // message, so a lower-authority duplicate can neither conflict out the user fact
107
+ // nor render a second copy of it.
108
+ sourceAuthorityOrder: ['user', 'llm'],
109
+ injectable: true,
110
+ valueSchema: {
111
+ type: 'object',
112
+ required: ['text'],
113
+ properties: { text: { type: 'string', minLength: 1, nonBlank: true } },
114
+ },
115
+ }
116
+
117
+ export const ASSERTION_OPERATIONS = new Set(['assert', 'correct', 'retract'])
118
+ export const VERIFICATION_VALUES = new Set([
119
+ 'observed', 'user-confirmed', 'derived', 'unverified', 'conflicted',
120
+ ])
121
+ // Only evidence-backed verification may enter automatic shadow injection in Cycle 1.
122
+ // `unverified` and `conflicted` stay queryable for explicit audit but never render.
123
+ export const INJECTABLE_VERIFICATIONS = new Set(['observed', 'user-confirmed', 'derived'])
124
+
125
+ // LLM facts may only describe project semantics, and only through the proposal
126
+ // engine's exact-quote evidence object. The ledger stores no wall clock or model
127
+ // text in that evidence: proposalId/eventKey/start/end/quote/model/schemaVersion are
128
+ // the complete, closed set.
129
+ export const LLM_PREDICATES = new Set([
130
+ 'project.decision', 'project.constraint', 'project.preference',
131
+ ])
132
+ export const LLM_EVIDENCE_FIELDS = new Set([
133
+ 'proposalId', 'eventKey', 'start', 'end', 'quote', 'model', 'modelDigest', 'schemaVersion',
134
+ ])
135
+
136
+ // Route capability is never a stored flag: enablement is recomputed from the
137
+ // evidence counters on every read, so a hand-edited row cannot turn a route on.
138
+ // It lives in the core because both the journal and the ledger read gate depend on
139
+ // the exact same thresholds, and because the counters themselves are bound to the
140
+ // evaluation evidence that produced them.
141
+ export const ROUTE_SAMPLE_FLOOR = 20
142
+ export const ROUTE_PRECISION_FLOOR = 0.95
143
+ export const ROUTE_RECALL_FLOOR = 0.5
144
+
145
+ export function routeEnabled({ sampleCount, precision, recall, safetyFailures } = {}) {
146
+ return Number.isInteger(sampleCount) && sampleCount >= ROUTE_SAMPLE_FLOOR &&
147
+ typeof precision === 'number' && Number.isFinite(precision) &&
148
+ precision >= ROUTE_PRECISION_FLOOR &&
149
+ typeof recall === 'number' && Number.isFinite(recall) &&
150
+ recall >= ROUTE_RECALL_FLOOR &&
151
+ safetyFailures === 0
152
+ }
153
+
154
+ function hexSha256(value) {
155
+ return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
156
+ }
157
+
158
+ function nonNegativeInteger(value) {
159
+ return Number.isInteger(value) && value >= 0
160
+ }
161
+
162
+ // A route row is only executable when its stored counters are the exact arithmetic
163
+ // result of the evaluation evidence that accompanies it. Nothing here trusts a
164
+ // caller-supplied precision/recall pair: `sampleCount` must equal the number of
165
+ // positive labels, precision and recall are recomputed from tp/fp/fn, and the
166
+ // safety counter must agree with the evidence.
167
+ export function routeEvidenceValid(evidence) {
168
+ if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) return false
169
+ if (!hexSha256(evidence.corpusSha256) || !hexSha256(evidence.evaluatorSha256)) return false
170
+ if (typeof evidence.modelDigest !== 'string' || evidence.modelDigest.trim() === '') return false
171
+ if (!nonNegativeInteger(evidence.tp) || !nonNegativeInteger(evidence.fp) ||
172
+ !nonNegativeInteger(evidence.fn) || !nonNegativeInteger(evidence.positiveLabels) ||
173
+ !nonNegativeInteger(evidence.safetyFailures)) {
174
+ return false
175
+ }
176
+ if (!Number.isFinite(evidence.measuredAt)) return false
177
+ if (evidence.sampleCount !== undefined && evidence.sampleCount !== evidence.positiveLabels) {
178
+ return false
179
+ }
180
+ if (evidence.tp + evidence.fn !== evidence.positiveLabels) return false
181
+ if (evidence.tp + evidence.fp === 0) return false
182
+ const precision = evidence.tp / (evidence.tp + evidence.fp)
183
+ const recall = evidence.tp / (evidence.tp + evidence.fn)
184
+ if (evidence.precision !== undefined && evidence.precision !== precision) return false
185
+ if (evidence.recall !== recall) return false
186
+ return true
187
+ }
188
+
189
+ export function routeRowValid({
190
+ sampleCount, precision, recall, safetyFailures, evidence, modelDigest,
191
+ } = {}) {
192
+ if (typeof modelDigest !== 'string' || modelDigest.trim() === '') return false
193
+ if (!routeEvidenceValid(evidence)) return false
194
+ if (evidence.modelDigest !== modelDigest) return false
195
+ if (sampleCount !== evidence.positiveLabels) return false
196
+ if (safetyFailures !== evidence.safetyFailures) return false
197
+ const derivedPrecision = evidence.tp / (evidence.tp + evidence.fp)
198
+ if (precision !== derivedPrecision || recall !== evidence.recall) return false
199
+ return routeEnabled({ sampleCount, precision, recall, safetyFailures })
200
+ }
201
+
202
+ // Static authority rank from the predicate registry. Lower rank is higher
203
+ // authority; an unknown source is lower than every declared source, and a
204
+ // predicate with no declared order returns null (no static ordering).
205
+ export function sourceAuthorityRank(predicate, sourceKind) {
206
+ const contract = typeof predicate === 'string' ? PREDICATES[predicate] : predicate
207
+ const order = contract?.sourceAuthorityOrder
208
+ if (!Array.isArray(order) || order.length === 0) return null
209
+ const index = order.indexOf(sourceKind)
210
+ return index === -1 ? order.length : index
211
+ }
212
+
213
+ // A project fact is duplicated by a lower-authority claim when the claim's exact
214
+ // text either equals or contains the higher-authority text. Containment covers the
215
+ // common model behaviour of quoting the whole sentence around an existing
216
+ // constraint; both forms add no information and must not render twice.
217
+ export function duplicatesHigherAuthorityValue(predicate, incumbent, candidate) {
218
+ const incumbentRank = sourceAuthorityRank(predicate, incumbent?.sourceKind)
219
+ const candidateRank = sourceAuthorityRank(predicate, candidate?.sourceKind)
220
+ if (incumbentRank === null || candidateRank === null) return false
221
+ if (candidateRank <= incumbentRank) return false
222
+ const incumbentText = incumbent?.value?.text
223
+ const candidateText = candidate?.value?.text
224
+ return typeof incumbentText === 'string' && incumbentText !== '' &&
225
+ typeof candidateText === 'string' &&
226
+ (candidateText === incumbentText || candidateText.includes(incumbentText))
227
+ }
228
+
229
+ export function matchesValueSchema(schema, value) {
230
+ if (!schema || typeof schema !== 'object') return false
231
+ switch (schema.type) {
232
+ case 'boolean':
233
+ return typeof value === 'boolean'
234
+ case 'number':
235
+ return typeof value === 'number' && Number.isFinite(value) &&
236
+ (schema.minimum === undefined || value >= schema.minimum)
237
+ case 'string':
238
+ if (typeof value !== 'string') return false
239
+ if (schema.minLength !== undefined && value.length < schema.minLength) return false
240
+ if (schema.nonBlank === true && value.trim() === '') return false
241
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) return false
242
+ return true
243
+ case 'object': {
244
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
245
+ for (const key of schema.required ?? []) {
246
+ if (!Object.hasOwn(value, key)) return false
247
+ }
248
+ for (const [key, propertySchema] of Object.entries(schema.properties ?? {})) {
249
+ if (Object.hasOwn(value, key) && !matchesValueSchema(propertySchema, value[key])) {
250
+ return false
251
+ }
252
+ }
253
+ return true
254
+ }
255
+ default:
256
+ return false
257
+ }
258
+ }
259
+
260
+ export function registryEntry(predicate) {
261
+ return typeof predicate === 'string' && Object.hasOwn(PREDICATES, predicate)
262
+ ? PREDICATES[predicate]
263
+ : null
264
+ }
265
+
266
+ function nonEmptyString(value) {
267
+ return typeof value === 'string' && value.trim() !== ''
268
+ }
269
+
270
+ function isRecord(value) {
271
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
272
+ }
273
+
274
+ // A `sourceKind:'llm'` assertion is not a free-form claim: it must reproduce the
275
+ // engine's exact-quote provenance or it is refused before archiving. Evidence-less,
276
+ // unverified, observed or non-project LLM facts are therefore unrepresentable, not
277
+ // merely non-injectable.
278
+ function validateLlmAssertion(assertion, errors) {
279
+ if (!LLM_PREDICATES.has(assertion.predicate)) errors.push('llm-predicate-not-allowed')
280
+ if (assertion.sourceAuthority !== 'user-evidence') {
281
+ errors.push('llm-authority-not-user-evidence')
282
+ }
283
+ if (assertion.verification !== 'derived') errors.push('llm-verification-not-derived')
284
+
285
+ const evidence = assertion.evidence
286
+ if (!isRecord(evidence)) {
287
+ errors.push('llm-evidence-missing')
288
+ return
289
+ }
290
+ for (const key of Object.keys(evidence)) {
291
+ if (!LLM_EVIDENCE_FIELDS.has(key)) errors.push(`llm-evidence-unknown:${key}`)
292
+ }
293
+ for (const field of ['proposalId', 'eventKey', 'quote', 'model', 'modelDigest', 'schemaVersion']) {
294
+ if (!nonEmptyString(evidence[field])) errors.push(`llm-evidence-invalid:${field}`)
295
+ }
296
+ if (!Number.isInteger(evidence.start) || evidence.start < 0) {
297
+ errors.push('llm-evidence-invalid:start')
298
+ }
299
+ if (!Number.isInteger(evidence.end)) errors.push('llm-evidence-invalid:end')
300
+ if (Number.isInteger(evidence.start) && Number.isInteger(evidence.end)) {
301
+ if (evidence.end <= evidence.start) errors.push('llm-evidence-invalid:span')
302
+ else if (nonEmptyString(evidence.quote) &&
303
+ evidence.end - evidence.start !== evidence.quote.length) {
304
+ errors.push('llm-evidence-span-mismatch')
305
+ }
306
+ }
307
+ // A retraction carries no value; its quote is only the retraction evidence. Every
308
+ // value-bearing LLM fact must quote exactly the text it asserts.
309
+ if (assertion.op !== 'retract' &&
310
+ (!isRecord(assertion.value) || assertion.value.text !== evidence.quote)) {
311
+ errors.push('llm-evidence-quote-mismatch')
312
+ }
313
+ }
314
+
315
+ export function validateAssertionInput(assertion) {
316
+ const errors = []
317
+ if (!assertion || typeof assertion !== 'object' || Array.isArray(assertion)) {
318
+ return { valid: false, errors: ['not-an-object'] }
319
+ }
320
+ for (const field of ['assertionId', 'scope', 'subjectEntityId']) {
321
+ if (!nonEmptyString(assertion[field])) errors.push(`missing-field:${field}`)
322
+ }
323
+ if (!nonEmptyString(assertion.sid)) errors.push('missing-field:sid')
324
+ if (!Number.isInteger(assertion.seq)) errors.push('invalid-field:seq')
325
+ if (!Number.isFinite(assertion.validAt)) errors.push('invalid-field:validAt')
326
+ if (!Number.isFinite(assertion.recordedAt)) errors.push('invalid-field:recordedAt')
327
+ if (assertion.qualifiers != null &&
328
+ (typeof assertion.qualifiers !== 'object' || Array.isArray(assertion.qualifiers))) {
329
+ errors.push('invalid-field:qualifiers')
330
+ }
331
+
332
+ const contract = registryEntry(assertion.predicate)
333
+ if (!contract) errors.push('unknown-predicate')
334
+ if (!ASSERTION_OPERATIONS.has(assertion.op)) errors.push('unsupported-operation')
335
+ if (!VERIFICATION_VALUES.has(assertion.verification)) errors.push('invalid-verification')
336
+ if (contract && !contract.allowedSources.includes(assertion.sourceKind)) {
337
+ errors.push('source-not-allowed')
338
+ }
339
+ if (assertion.op === 'assert' && assertion.targetAssertionId != null) {
340
+ errors.push('unexpected-target-assertion')
341
+ }
342
+ if ((assertion.op === 'correct' || assertion.op === 'retract') &&
343
+ !nonEmptyString(assertion.targetAssertionId)) {
344
+ errors.push('missing-target-assertion')
345
+ }
346
+ if (contract && assertion.op !== 'retract' &&
347
+ !matchesValueSchema(contract.valueSchema, assertion.value)) {
348
+ errors.push('value-schema-invalid')
349
+ }
350
+ if (assertion.sourceKind === 'llm') validateLlmAssertion(assertion, errors)
351
+ return { valid: errors.length === 0, errors }
352
+ }
353
+
354
+ export const PREDICATES = {
355
+ 'file.exists': {
356
+ ...fileContract,
357
+ valueSchema: { type: 'boolean' },
358
+ // Auto-injection renders only positive existence. `exists=false` remains a
359
+ // legitimate current fact for explicit audit, but absence markers are not
360
+ // injected into the model context: a renderer returning null contributes no line.
361
+ renderer: (fact) => (fact.value === true ? `文件路径为 ${fact.subjectDisplay},当前存在` : null),
362
+ },
363
+ 'file.size': {
364
+ ...fileContract,
365
+ valueSchema: { type: 'number', minimum: 0 },
366
+ renderer: (fact) => `文件 ${fact.subjectDisplay} 的大小为 ${fact.value} 字节`,
367
+ },
368
+ 'file.mtime': {
369
+ ...fileContract,
370
+ valueSchema: { type: 'number', minimum: 0 },
371
+ renderer: (fact) => `文件 ${fact.subjectDisplay} 的修改时间为 ${fact.value}`,
372
+ },
373
+ 'file.sha256': {
374
+ ...fileContract,
375
+ valueSchema: { type: 'string', pattern: '^[0-9a-f]{64}$' },
376
+ renderer: (fact) => `文件 ${fact.subjectDisplay} 的 SHA-256 为 ${fact.value}`,
377
+ },
378
+ 'file.lines': {
379
+ ...fileContract,
380
+ injectable: false,
381
+ invalidationRule: 'disabled-pending-encoding-and-newline-contract',
382
+ valueSchema: { type: 'number', minimum: 0 },
383
+ renderer: (fact) => `文件 ${fact.subjectDisplay} 的行数为 ${fact.value}`,
384
+ },
385
+ 'project.decision': {
386
+ ...projectContract,
387
+ renderer: (fact) => `项目决策:${fact.value.text}`,
388
+ },
389
+ 'project.constraint': {
390
+ ...projectContract,
391
+ renderer: (fact) => `项目约束:${fact.value.text}`,
392
+ },
393
+ 'project.preference': {
394
+ ...projectContract,
395
+ renderer: (fact) => `项目偏好:${fact.value.text}`,
396
+ },
397
+ 'synthetic.set': {
398
+ allowedSources: [],
399
+ cardinality: 'multi',
400
+ mutable: true,
401
+ entityBound: false,
402
+ invalidationRule: 'test-only',
403
+ conflictRule: 'test-only',
404
+ injectable: false,
405
+ valueSchema: { type: 'object' },
406
+ renderer: (fact) => `测试集合元素:${canonicalJson(fact.value)}`,
407
+ },
408
+ }
409
+
410
+ export function factKey(fact) {
411
+ const predicate = registryEntry(fact.predicate)
412
+ if (!predicate) throw new TypeError(`unknown predicate: ${fact.predicate}`)
413
+
414
+ const qualifiers = fact.qualifiers === undefined ? null : fact.qualifiers
415
+ const parts = [fact.scope, fact.subjectEntityId, fact.generation, fact.predicate, canonicalJson(qualifiers)]
416
+ if (predicate.cardinality === 'multi') {
417
+ // A multi fact must carry its rule slot. The reducer derives it from the value
418
+ // for asserts and inherits it from the target for corrections/retractions; the
419
+ // value fallback keeps direct factKey() callers (tests, audits) well-defined.
420
+ const valueKey = fact.valueKey === undefined ? fact.value : fact.valueKey
421
+ parts.push(canonicalJson(valueKey))
422
+ }
423
+ return parts.join('/')
424
+ }
425
+
426
+ export function renderFacts(facts, budget) {
427
+ const lines = []
428
+ const selected = []
429
+ let text = ''
430
+ let overflowCount = 0
431
+ let suppressedCount = 0
432
+ let invalidCount = 0
433
+
434
+ for (const fact of facts) {
435
+ const predicate = registryEntry(fact.predicate)
436
+ // Unknown predicates and schema-invalid values are reported, never thrown: a
437
+ // corrupt archived fact must not take down the whole rendering path.
438
+ if (!predicate) {
439
+ invalidCount++
440
+ continue
441
+ }
442
+ if (!predicate.injectable || (fact.status !== undefined && fact.status !== 'active')) {
443
+ overflowCount++
444
+ continue
445
+ }
446
+ // Evidence-backed LLM facts (`derived`) may render after the project source
447
+ // contract was extended; an unverified or conflicted value still never does.
448
+ // Legacy snapshots remain suppressed regardless of their verification claim.
449
+ if (fact.conflict != null || fact.sourceKind === 'legacy' ||
450
+ (fact.verification !== undefined && !INJECTABLE_VERIFICATIONS.has(fact.verification))) {
451
+ suppressedCount++
452
+ continue
453
+ }
454
+ if (!matchesValueSchema(predicate.valueSchema, fact.value)) {
455
+ invalidCount++
456
+ continue
457
+ }
458
+
459
+ let line
460
+ try {
461
+ line = predicate.renderer(fact)
462
+ } catch {
463
+ invalidCount++
464
+ continue
465
+ }
466
+ // A null line means the predicate is a legitimate current fact that must not be
467
+ // auto-injected (for example `file.exists=false`). It is suppressed, not overflow.
468
+ if (line === null || line === undefined) {
469
+ suppressedCount++
470
+ continue
471
+ }
472
+
473
+ const candidate = lines.length === 0 ? line : `${text}\n${line}`
474
+ if (candidate.length <= budget) {
475
+ lines.push(line)
476
+ selected.push(fact)
477
+ text = candidate
478
+ } else {
479
+ overflowCount++
480
+ }
481
+ }
482
+
483
+ return { text, selected, overflowCount, suppressedCount, invalidCount }
484
+ }
485
+
486
+ const OBSERVATION_FIELDS = {
487
+ exists: 'file.exists',
488
+ size: 'file.size',
489
+ mtime: 'file.mtime',
490
+ sha256: 'file.sha256',
491
+ }
492
+ const WRITE_INVALIDATED_FIELDS = ['size', 'mtime', 'sha256']
493
+
494
+ function lexicalCompare(a, b) {
495
+ return a < b ? -1 : a > b ? 1 : 0
496
+ }
497
+
498
+ function causalCompare(a, b) {
499
+ return (a[0] - b[0]) || lexicalCompare(a[1], b[1]) || (a[2] - b[2]) || lexicalCompare(a[3], b[3])
500
+ }
501
+
502
+ function observationKind(observation) {
503
+ if (observation.op !== 'probe') return observation.op
504
+ return observation.value?.exists === false ? 'probe-missing' : 'probe-present'
505
+ }
506
+
507
+ function observationSource(observation) {
508
+ return {
509
+ sourceKind: observation.source?.kind ?? observation.sourceKind ??
510
+ (observation.op === 'probe' ? 'probe' : 'observation'),
511
+ sourceAuthority: observation.source?.authority ?? observation.sourceAuthority ?? null,
512
+ }
513
+ }
514
+
515
+ export function projectLedger({ observations, assertions, reducerVersion }) {
516
+ const entityStates = new Map()
517
+ const observationPlacements = []
518
+ const assertionPlacements = []
519
+ const assertionPlacementById = new Map()
520
+ const assertionById = new Map()
521
+ const factVersions = []
522
+ const pendingFacts = []
523
+ const dead = []
524
+
525
+ // Archived input is untrusted: a single malformed assertion must be dead-lettered
526
+ // with a stable reason, never thrown, and never allowed to poison an otherwise valid
527
+ // scope. Structural validation runs before any placement or causal ordering.
528
+ const assertionInputs = Array.isArray(assertions) ? assertions : []
529
+ const validAssertions = []
530
+ for (let index = 0; index < assertionInputs.length; index++) {
531
+ const assertion = assertionInputs[index]
532
+ const validation = validateAssertionInput(assertion)
533
+ if (!validation.valid) {
534
+ dead.push({
535
+ assertionId: nonEmptyString(assertion?.assertionId)
536
+ ? assertion.assertionId
537
+ : `assertion:index:${index}`,
538
+ reason: validation.errors[0],
539
+ errors: validation.errors,
540
+ })
541
+ continue
542
+ }
543
+ validAssertions.push(assertion)
544
+ }
545
+
546
+ const terminalCounts = new Map()
547
+ for (const assertion of validAssertions) {
548
+ if (assertion.op !== 'correct' && assertion.op !== 'retract') continue
549
+ terminalCounts.set(assertion.targetAssertionId, (terminalCounts.get(assertion.targetAssertionId) ?? 0) + 1)
550
+ }
551
+ const duplicateTerminalIds = new Set(validAssertions.filter((assertion) =>
552
+ (assertion.op === 'correct' || assertion.op === 'retract') &&
553
+ terminalCounts.get(assertion.targetAssertionId) > 1).map((assertion) => assertion.assertionId))
554
+ for (const assertion of validAssertions) {
555
+ if (!duplicateTerminalIds.has(assertion.assertionId)) continue
556
+ dead.push({
557
+ assertionId: assertion.assertionId,
558
+ targetAssertionId: assertion.targetAssertionId,
559
+ reason: 'duplicate-terminal-operation',
560
+ })
561
+ }
562
+
563
+ const changes = [
564
+ ...observations.map((observation) => ({
565
+ kind: 'observation', value: observation,
566
+ causalOrder: [observation.validAt, observation.sid, observation.seq, `observation:${observation.eventKey}`],
567
+ })),
568
+ ...validAssertions.map((assertion) => ({
569
+ kind: 'assertion', value: assertion,
570
+ causalOrder: [assertion.validAt, assertion.sid, assertion.seq, `assertion:${assertion.assertionId}`],
571
+ })),
572
+ ].sort((a, b) => causalCompare(a.causalOrder, b.causalOrder))
573
+
574
+ function entityState(scope, entityId) {
575
+ const key = `${scope}\u0000${entityId}`
576
+ if (!entityStates.has(key)) {
577
+ entityStates.set(key, { scope, entityId, generations: [], lastValidAt: null, lastRecordedAt: null })
578
+ }
579
+ return entityStates.get(key)
580
+ }
581
+
582
+ function currentGeneration(state) {
583
+ return state.generations[state.generations.length - 1] ?? null
584
+ }
585
+
586
+ function openGeneration(state) {
587
+ return state.generations.find((generation) => generation.closedBy === null) ?? null
588
+ }
589
+
590
+ function createGeneration(state, openedBy, validAt) {
591
+ const previous = currentGeneration(state)
592
+ if (previous) previous.replacedAt = validAt
593
+ const generation = {
594
+ number: (previous?.number ?? 0) + 1,
595
+ openedBy,
596
+ closedBy: null,
597
+ presence: true,
598
+ replacedAt: null,
599
+ }
600
+ state.generations.push(generation)
601
+ return generation
602
+ }
603
+
604
+ function closeGeneration(state, causalOrder) {
605
+ let generation = openGeneration(state)
606
+ if (!generation && state.generations.length === 0) {
607
+ generation = createGeneration(state, causalOrder, causalOrder[0])
608
+ }
609
+ if (!generation) return currentGeneration(state)
610
+ generation.closedBy = causalOrder
611
+ generation.presence = false
612
+ return generation
613
+ }
614
+
615
+ function placeObservation(observation, role, entityId, generation, reason = null) {
616
+ observationPlacements.push({
617
+ eventKey: observation.eventKey,
618
+ scope: observation.scope,
619
+ entityId,
620
+ role,
621
+ generation: generation?.number ?? null,
622
+ unplaced: generation === null,
623
+ reason,
624
+ causalOrder: [observation.validAt, observation.sid, observation.seq, `observation:${observation.eventKey}`],
625
+ })
626
+ }
627
+
628
+ function placeAssertion(assertionId, observation, generation, reason = null) {
629
+ const placement = {
630
+ assertionId,
631
+ scope: observation.scope,
632
+ entityId: observation.subjectEntityId,
633
+ generation: generation?.number ?? null,
634
+ unplaced: generation === null,
635
+ reason,
636
+ causalOrder: [observation.validAt, observation.sid, observation.seq, assertionId],
637
+ }
638
+ assertionPlacements.push(placement)
639
+ assertionPlacementById.set(assertionId, placement)
640
+ }
641
+
642
+ function deriveAssertion(observation, role, generation, predicate, op, value) {
643
+ const assertionId = `observation:${observation.eventKey}:${role}:${predicate}:${op}`
644
+ const subjectEntityId = role === 'target' ? observation.targetEntityId : observation.subjectEntityId
645
+ const source = observationSource(observation)
646
+ return {
647
+ fact: {
648
+ scope: observation.scope,
649
+ subjectEntityId,
650
+ subjectDisplay: subjectEntityId,
651
+ generation: generation.number,
652
+ predicate,
653
+ qualifiers: null,
654
+ value: op === 'invalidate' ? null : value,
655
+ assertionId,
656
+ evidence: { eventKey: observation.eventKey, rawRef: observation.rawRef ?? null },
657
+ causalOrder: [observation.validAt, observation.sid, observation.seq, assertionId],
658
+ validFrom: observation.validAt,
659
+ validTo: null,
660
+ recordedAt: observation.recordedAt,
661
+ retiredAt: null,
662
+ status: op === 'invalidate' ? 'invalidated' : 'active',
663
+ verification: 'observed',
664
+ conflict: null,
665
+ op,
666
+ sid: observation.sid,
667
+ seq: observation.seq,
668
+ reducerVersion,
669
+ ...source,
670
+ },
671
+ generation,
672
+ subjectEntityId,
673
+ }
674
+ }
675
+
676
+ function derivedForObservation(observation, role, generation) {
677
+ const kind = observationKind(observation)
678
+ const value = observation.value ?? {}
679
+ const derived = []
680
+ const assert = (predicate, factValue) => {
681
+ // Observation adapters should never mint a value that contradicts the registry
682
+ // schema. If archived input does, the measurement is unknown, not a poison fact.
683
+ const contract = registryEntry(predicate)
684
+ if (!contract || !matchesValueSchema(contract.valueSchema, factValue)) return
685
+ derived.push(deriveAssertion(observation, role, generation, predicate, 'assert', factValue))
686
+ }
687
+ const invalidate = (predicate) => {
688
+ derived.push(deriveAssertion(observation, role, generation, predicate, 'invalidate', null))
689
+ }
690
+
691
+ if (kind === 'scope_init') return derived
692
+ if (kind === 'create' || kind === 'write' || kind === 'probe-present') {
693
+ assert('file.exists', true)
694
+ if (kind === 'create' || kind === 'write') {
695
+ for (const field of WRITE_INVALIDATED_FIELDS) {
696
+ if (!Object.hasOwn(value, field)) invalidate(OBSERVATION_FIELDS[field])
697
+ }
698
+ }
699
+ for (const field of WRITE_INVALIDATED_FIELDS) {
700
+ if (Object.hasOwn(value, field)) assert(OBSERVATION_FIELDS[field], value[field])
701
+ }
702
+ } else if (kind === 'read' || kind === 'stat') {
703
+ for (const field of Object.keys(OBSERVATION_FIELDS)) {
704
+ if (Object.hasOwn(value, field)) assert(OBSERVATION_FIELDS[field], value[field])
705
+ }
706
+ } else if (kind === 'delete' || kind === 'probe-missing' || kind === 'rename-source') {
707
+ assert('file.exists', false)
708
+ for (const field of WRITE_INVALIDATED_FIELDS) invalidate(OBSERVATION_FIELDS[field])
709
+ } else if (kind === 'rename-target') {
710
+ assert('file.exists', true)
711
+ }
712
+
713
+ return derived.sort((a, b) => causalCompare(a.fact.causalOrder, b.fact.causalOrder))
714
+ }
715
+
716
+ function applyDerived(observation, role, generation, kindOverride = null) {
717
+ const effectiveObservation = kindOverride === null ? observation : { ...observation, op: kindOverride }
718
+ const derived = derivedForObservation(effectiveObservation, role, generation)
719
+ for (const item of derived) {
720
+ pendingFacts.push(item)
721
+ assertionById.set(item.fact.assertionId, item.fact)
722
+ placeAssertion(item.fact.assertionId, {
723
+ ...observation,
724
+ subjectEntityId: item.subjectEntityId,
725
+ }, generation)
726
+ }
727
+ }
728
+
729
+ function processObservation(observation) {
730
+ const successful = observation.outcome === 'success' || observation.outcome === 'observed'
731
+ const state = entityState(observation.scope, observation.subjectEntityId)
732
+ if (!successful) {
733
+ const role = observation.op === 'rename' ? 'source' : 'subject'
734
+ placeObservation(observation, role, observation.subjectEntityId, null, `outcome-${observation.outcome}`)
735
+ if (observation.op === 'rename' && observation.targetEntityId != null) {
736
+ placeObservation(observation, 'target', observation.targetEntityId, null, `outcome-${observation.outcome}`)
737
+ }
738
+ return
739
+ }
740
+
741
+ const kind = observationKind(observation)
742
+ if (kind === 'scope_init') {
743
+ const generation = openGeneration(state) ?? createGeneration(state,
744
+ [observation.validAt, observation.sid, observation.seq, `observation:${observation.eventKey}`], observation.validAt)
745
+ state.lastValidAt = observation.validAt
746
+ state.lastRecordedAt = observation.recordedAt
747
+ placeObservation(observation, 'subject', observation.subjectEntityId, generation)
748
+ applyDerived(observation, 'subject', generation)
749
+ return
750
+ }
751
+
752
+ // A success report that itself says the entity is absent is contradictory evidence.
753
+ // It degrades to unknown instead of minting exists=true or asserting stale metadata.
754
+ // delete/probe-missing are the legitimate absence transitions and bypass this guard.
755
+ if ((kind === 'create' || kind === 'write' || kind === 'probe-present' ||
756
+ kind === 'read' || kind === 'stat') && observation.value?.exists === false) {
757
+ placeObservation(observation, 'subject', observation.subjectEntityId, null,
758
+ 'reported-exists-false')
759
+ return
760
+ }
761
+
762
+ if (kind === 'create' || kind === 'write' || kind === 'probe-present') {
763
+ const generation = openGeneration(state) ?? createGeneration(state,
764
+ [observation.validAt, observation.sid, observation.seq, `observation:${observation.eventKey}`], observation.validAt)
765
+ state.lastValidAt = observation.validAt
766
+ state.lastRecordedAt = observation.recordedAt
767
+ placeObservation(observation, 'subject', observation.subjectEntityId, generation)
768
+ applyDerived(observation, 'subject', generation)
769
+ return
770
+ }
771
+
772
+ if (kind === 'read' || kind === 'stat') {
773
+ const generation = openGeneration(state)
774
+ placeObservation(observation, 'subject', observation.subjectEntityId, generation,
775
+ generation ? null : 'no-matching-generation')
776
+ if (generation) {
777
+ state.lastValidAt = observation.validAt
778
+ state.lastRecordedAt = observation.recordedAt
779
+ applyDerived(observation, 'subject', generation)
780
+ }
781
+ return
782
+ }
783
+
784
+ if (kind === 'delete' || kind === 'probe-missing') {
785
+ const generation = closeGeneration(state,
786
+ [observation.validAt, observation.sid, observation.seq, `observation:${observation.eventKey}`])
787
+ state.lastValidAt = observation.validAt
788
+ state.lastRecordedAt = observation.recordedAt
789
+ placeObservation(observation, 'subject', observation.subjectEntityId, generation)
790
+ applyDerived(observation, 'subject', generation)
791
+ return
792
+ }
793
+
794
+ if (kind === 'rename') {
795
+ const targetState = entityState(observation.scope, observation.targetEntityId)
796
+ const sourceOpen = openGeneration(state)
797
+ const targetOpen = openGeneration(targetState)
798
+ if (observation.subjectEntityId === observation.targetEntityId && sourceOpen) {
799
+ placeObservation(observation, 'source', observation.subjectEntityId, sourceOpen)
800
+ placeObservation(observation, 'target', observation.targetEntityId, sourceOpen)
801
+ return
802
+ }
803
+ if (!sourceOpen || targetOpen) {
804
+ const reason = !sourceOpen ? 'source-not-open' : 'target-exists'
805
+ placeObservation(observation, 'source', observation.subjectEntityId, null, reason)
806
+ placeObservation(observation, 'target', observation.targetEntityId, null, reason)
807
+ return
808
+ }
809
+ const causalOrder = [observation.validAt, observation.sid, observation.seq, `observation:${observation.eventKey}`]
810
+ const sourceGeneration = closeGeneration(state, causalOrder)
811
+ const targetGeneration = createGeneration(targetState, causalOrder, observation.validAt)
812
+ state.lastValidAt = observation.validAt
813
+ state.lastRecordedAt = observation.recordedAt
814
+ targetState.lastValidAt = observation.validAt
815
+ targetState.lastRecordedAt = observation.recordedAt
816
+ placeObservation(observation, 'source', observation.subjectEntityId, sourceGeneration)
817
+ placeObservation(observation, 'target', observation.targetEntityId, targetGeneration)
818
+ applyDerived(observation, 'source', sourceGeneration, 'rename-source')
819
+ applyDerived(observation, 'target', targetGeneration, 'rename-target')
820
+ return
821
+ }
822
+
823
+ placeObservation(observation, 'subject', observation.subjectEntityId, null, 'unsupported-operation')
824
+ }
825
+
826
+ // Resolve the rule slot for every multi assertion before any fact is built. An
827
+ // assert owns its value; a correction/retraction inherits the target's slot so it
828
+ // closes exactly that rule rather than opening a new one. The fixed-point loop
829
+ // also handles a correction whose target is itself a correction.
830
+ function multiValueKeysFor(assertions) {
831
+ const keys = new Map()
832
+ for (const assertion of assertions) {
833
+ const contract = registryEntry(assertion.predicate)
834
+ if (!contract || contract.cardinality !== 'multi') continue
835
+ if (assertion.valueKey !== undefined) {
836
+ keys.set(assertion.assertionId, assertion.valueKey)
837
+ } else if (assertion.op === 'assert' && assertion.value !== undefined && assertion.value !== null) {
838
+ keys.set(assertion.assertionId, assertion.value)
839
+ }
840
+ }
841
+ let changed = true
842
+ while (changed) {
843
+ changed = false
844
+ for (const assertion of assertions) {
845
+ if (keys.has(assertion.assertionId) ||
846
+ (assertion.op !== 'correct' && assertion.op !== 'retract')) continue
847
+ const targetKey = keys.get(assertion.targetAssertionId)
848
+ if (targetKey !== undefined) {
849
+ keys.set(assertion.assertionId, targetKey)
850
+ changed = true
851
+ }
852
+ }
853
+ }
854
+ return keys
855
+ }
856
+ const multiValueKeyById = multiValueKeysFor(validAssertions)
857
+
858
+ function processAssertion(assertion) {
859
+ let generation = null
860
+ let reason = null
861
+ if (assertion.op === 'correct' || assertion.op === 'retract') {
862
+ const target = assertionPlacementById.get(assertion.targetAssertionId)
863
+ if (target && !target.unplaced) generation = { number: target.generation }
864
+ else reason = 'target-unplaced'
865
+ } else {
866
+ const state = entityStates.get(`${assertion.scope}\u0000${assertion.subjectEntityId}`)
867
+ generation = state ? openGeneration(state) : null
868
+ if (!generation) reason = 'no-matching-generation'
869
+ }
870
+ const placement = {
871
+ assertionId: assertion.assertionId,
872
+ scope: assertion.scope,
873
+ entityId: assertion.subjectEntityId,
874
+ generation: generation?.number ?? null,
875
+ unplaced: generation === null,
876
+ reason,
877
+ causalOrder: [assertion.validAt, assertion.sid, assertion.seq, `assertion:${assertion.assertionId}`],
878
+ }
879
+ assertionPlacements.push(placement)
880
+ assertionPlacementById.set(assertion.assertionId, placement)
881
+ if (!duplicateTerminalIds.has(assertion.assertionId) &&
882
+ (generation || assertion.op === 'correct' || assertion.op === 'retract')) {
883
+ const contract = registryEntry(assertion.predicate)
884
+ const multiValueKey = contract?.cardinality === 'multi'
885
+ ? multiValueKeyById.get(assertion.assertionId) ??
886
+ (assertion.op === 'assert' ? assertion.value : { unresolved: assertion.assertionId })
887
+ : undefined
888
+ const fact = {
889
+ scope: assertion.scope,
890
+ subjectEntityId: assertion.subjectEntityId,
891
+ subjectDisplay: assertion.subjectEntityId,
892
+ generation: generation?.number ?? null,
893
+ predicate: assertion.predicate,
894
+ qualifiers: assertion.qualifiers ?? null,
895
+ ...(multiValueKey !== undefined ? { valueKey: multiValueKey } : {}),
896
+ value: assertion.op === 'retract' || assertion.op === 'invalidate' ? null : assertion.value,
897
+ assertionId: assertion.assertionId,
898
+ evidence: assertion.evidence ?? null,
899
+ causalOrder: [assertion.validAt, assertion.sid, assertion.seq, `assertion:${assertion.assertionId}`],
900
+ validFrom: assertion.validAt,
901
+ validTo: null,
902
+ recordedAt: assertion.recordedAt,
903
+ retiredAt: null,
904
+ status: assertion.op === 'invalidate' ? 'invalidated' : 'active',
905
+ verification: assertion.verification,
906
+ conflict: null,
907
+ op: assertion.op,
908
+ targetAssertionId: assertion.targetAssertionId ?? null,
909
+ sid: assertion.sid,
910
+ seq: assertion.seq,
911
+ reducerVersion,
912
+ sourceKind: assertion.sourceKind,
913
+ sourceAuthority: assertion.sourceAuthority ?? null,
914
+ }
915
+ pendingFacts.push({ fact, generation, subjectEntityId: assertion.subjectEntityId })
916
+ assertionById.set(assertion.assertionId, fact)
917
+ }
918
+ }
919
+
920
+ const deferredAssertions = []
921
+ for (const change of changes) {
922
+ if (change.kind === 'observation') processObservation(change.value)
923
+ else if ((change.value.op === 'correct' || change.value.op === 'retract') &&
924
+ !assertionPlacementById.has(change.value.targetAssertionId)) {
925
+ deferredAssertions.push(change.value)
926
+ } else processAssertion(change.value)
927
+ }
928
+ while (deferredAssertions.length) {
929
+ const waiting = deferredAssertions.splice(0)
930
+ let placedTarget = false
931
+ for (const assertion of waiting) {
932
+ if (assertionPlacementById.has(assertion.targetAssertionId)) {
933
+ processAssertion(assertion)
934
+ placedTarget = true
935
+ } else deferredAssertions.push(assertion)
936
+ }
937
+ if (!placedTarget) {
938
+ for (const assertion of deferredAssertions.splice(0)) processAssertion(assertion)
939
+ }
940
+ }
941
+
942
+ function generationFor(fact) {
943
+ const state = entityStates.get(`${fact.scope}\u0000${fact.subjectEntityId}`)
944
+ return state?.generations.find((generation) => generation.number === fact.generation) ?? null
945
+ }
946
+
947
+ function sameRowValue(a, b) {
948
+ return canonicalJson(a.value) === canonicalJson(b.value) &&
949
+ canonicalJson(a.conflict) === canonicalJson(b.conflict)
950
+ }
951
+
952
+ function systemRowKey(fact) {
953
+ return `${fact.factKey}\u0000${fact.assertionId}\u0000${fact.validFrom}`
954
+ }
955
+
956
+ // Per-fact key cache: canonicalizing qualifiers once per fact removes the repeated
957
+ // full-row canonicalization that dominated the old whole-prefix snapshot loop.
958
+ const factKeyCache = new WeakMap()
959
+ function cachedFactKey(fact) {
960
+ let key = factKeyCache.get(fact)
961
+ if (key === undefined) {
962
+ key = factKey(fact)
963
+ factKeyCache.set(fact, key)
964
+ }
965
+ return key
966
+ }
967
+
968
+ // System-version rows are immutable except for retiredAt (excluded from the
969
+ // signature), so their canonical signature is safe to cache across events.
970
+ const systemSignatureCache = new WeakMap()
971
+ function rowSignature(row) {
972
+ return canonicalJson({ ...row, recordedAt: null, retiredAt: null })
973
+ }
974
+ function cachedRowSignature(row) {
975
+ let signature = systemSignatureCache.get(row)
976
+ if (signature === undefined) {
977
+ signature = rowSignature(row)
978
+ systemSignatureCache.set(row, signature)
979
+ }
980
+ return signature
981
+ }
982
+
983
+ function removeAliveClaim(group, assertionId) {
984
+ const index = group.alive.findIndex((fact) => fact.assertionId === assertionId)
985
+ if (index !== -1) group.alive.splice(index, 1)
986
+ }
987
+
988
+ function applyClaimState(group, fact) {
989
+ if (fact.op === 'invalidate') {
990
+ group.latestInvalidation = fact.causalOrder
991
+ group.alive.length = 0
992
+ return
993
+ }
994
+ if (fact.op === 'correct' || fact.op === 'retract') {
995
+ group.terminated.add(fact.targetAssertionId)
996
+ removeAliveClaim(group, fact.targetAssertionId)
997
+ }
998
+ if ((fact.op === 'assert' || fact.op === 'correct') &&
999
+ !group.terminated.has(fact.assertionId)) {
1000
+ group.alive.push(fact)
1001
+ }
1002
+ }
1003
+
1004
+ function rebuildClaimState(group) {
1005
+ group.alive = []
1006
+ group.terminated = new Set()
1007
+ group.latestInvalidation = null
1008
+ for (const fact of group.items) applyClaimState(group, fact)
1009
+ }
1010
+
1011
+ function claimsForAlive(group, contract) {
1012
+ if (contract.conflictRule === 'causal-order') {
1013
+ const last = group.alive[group.alive.length - 1]
1014
+ return last ? [last] : []
1015
+ }
1016
+ if (contract.conflictRule === 'equal-authority-conflict' && group.alive.length) {
1017
+ return conflictClaimsForAlive(group.alive, contract)
1018
+ }
1019
+ return group.alive.slice()
1020
+ }
1021
+
1022
+ // Authority-aware conflict grouping for semantic predicates. Only claims at the
1023
+ // highest static authority present can become current. A lower-authority claim is
1024
+ // carried as `shadowed`, rendered as conflicted for audit, and can never force the
1025
+ // higher fact into conflict or duplicate its text. Among equal-authority claims the
1026
+ // original rule still applies: distinct values conflict instead of "newest wins".
1027
+ function conflictClaimsForAlive(alive, contract) {
1028
+ const order = Array.isArray(contract.sourceAuthorityOrder)
1029
+ ? contract.sourceAuthorityOrder
1030
+ : null
1031
+ const latestByValue = (claims) => {
1032
+ const latest = new Map()
1033
+ for (const fact of claims) latest.set(canonicalJson(fact.value), fact)
1034
+ return [...latest.values()]
1035
+ }
1036
+ if (!order || order.length === 0) return latestByValue(alive)
1037
+
1038
+ const rankOf = (fact) => {
1039
+ const index = order.indexOf(fact.sourceKind)
1040
+ return index === -1 ? order.length : index
1041
+ }
1042
+ let best = Infinity
1043
+ for (const fact of alive) best = Math.min(best, rankOf(fact))
1044
+ const top = alive.filter((fact) => rankOf(fact) === best)
1045
+ const lower = alive.filter((fact) => rankOf(fact) > best)
1046
+ const topClaims = latestByValue(top)
1047
+ const higherAuthorityIds = topClaims.map((fact) => fact.assertionId).sort(lexicalCompare)
1048
+ const claims = [...topClaims]
1049
+ for (const fact of lower) {
1050
+ claims.push({ ...fact, shadowed: true, higherAuthorityIds })
1051
+ }
1052
+ return claims
1053
+ }
1054
+
1055
+ function clampRow(row, nextValidAt) {
1056
+ row.validTo = nextValidAt
1057
+ const generation = generationFor(row)
1058
+ const closedAt = generation?.closedBy?.[0] ?? null
1059
+ if (generation?.replacedAt !== null && generation?.replacedAt !== undefined &&
1060
+ (row.validTo === null || generation.replacedAt < row.validTo)) {
1061
+ row.validTo = generation.replacedAt
1062
+ }
1063
+ if (registryEntry(row.predicate)?.entityBound && closedAt !== null && row.validFrom < closedAt &&
1064
+ (row.validTo === null || closedAt < row.validTo)) {
1065
+ row.validTo = closedAt
1066
+ }
1067
+ return row
1068
+ }
1069
+
1070
+ function rowsForCurrent(key, claims, current) {
1071
+ const primary = claims.filter((fact) => fact.shadowed !== true)
1072
+ const conflicted = primary.length > 1
1073
+ const conflictIds = conflicted
1074
+ ? primary.map((fact) => fact.assertionId).sort(lexicalCompare)
1075
+ : null
1076
+ const nextRows = claims.map((fact) => {
1077
+ const { shadowed, higherAuthorityIds, ...base } = fact
1078
+ if (shadowed === true) {
1079
+ // A lower-authority claim is preserved for audit but is never active and
1080
+ // never renders. Its conflict record points at the authority that shadows it.
1081
+ const assertionIds = [...new Set([
1082
+ ...(Array.isArray(higherAuthorityIds) ? higherAuthorityIds : []),
1083
+ fact.assertionId,
1084
+ ])].sort(lexicalCompare)
1085
+ return {
1086
+ ...base,
1087
+ factKey: key,
1088
+ validFrom: current.validFrom,
1089
+ validTo: null,
1090
+ status: 'conflicted',
1091
+ conflict: { assertionIds, reason: 'lower-authority-shadowed' },
1092
+ }
1093
+ }
1094
+ return {
1095
+ ...base,
1096
+ factKey: key,
1097
+ validFrom: current.validFrom,
1098
+ validTo: null,
1099
+ status: conflicted ? 'conflicted' : 'active',
1100
+ conflict: conflicted
1101
+ ? { assertionIds: conflictIds, reason: 'equal-authority-overlap' }
1102
+ : null,
1103
+ }
1104
+ })
1105
+ if (current.op === 'invalidate' && nextRows.length === 0) {
1106
+ nextRows.push({ ...current, factKey: key, validFrom: current.validFrom, validTo: null })
1107
+ }
1108
+ return nextRows
1109
+ }
1110
+
1111
+ function mergeOrPush(rows, row) {
1112
+ const previous = rows[rows.length - 1]
1113
+ if (previous && previous.validTo === row.validFrom && previous.assertionId === row.assertionId &&
1114
+ previous.factKey === row.factKey && previous.status === row.status &&
1115
+ sameRowValue(previous, row)) {
1116
+ previous.validTo = row.validTo
1117
+ } else {
1118
+ rows.push(row)
1119
+ }
1120
+ }
1121
+
1122
+ // Bounded fallback used when a late fact lands in the middle of a group's causal
1123
+ // order. It mirrors the original whole-prefix semantics for that group only.
1124
+ function computeGroupRows(key, items) {
1125
+ const rows = []
1126
+ for (let index = 0; index < items.length; index++) {
1127
+ const terminated = new Set()
1128
+ let latestInvalidation = null
1129
+ for (let prior = 0; prior <= index; prior++) {
1130
+ const fact = items[prior]
1131
+ if (fact.op === 'correct' || fact.op === 'retract') terminated.add(fact.targetAssertionId)
1132
+ if (fact.op === 'invalidate') latestInvalidation = fact.causalOrder
1133
+ }
1134
+
1135
+ let claims = items.slice(0, index + 1).filter((fact) =>
1136
+ (fact.op === 'assert' || fact.op === 'correct') && !terminated.has(fact.assertionId) &&
1137
+ (!latestInvalidation || causalCompare(fact.causalOrder, latestInvalidation) > 0))
1138
+ const contract = registryEntry(items[index].predicate)
1139
+ if (!contract) continue
1140
+ if (contract.conflictRule === 'causal-order') {
1141
+ claims = claims.length ? [claims[claims.length - 1]] : []
1142
+ } else if (contract.conflictRule === 'equal-authority-conflict' && claims.length) {
1143
+ claims = conflictClaimsForAlive(claims, contract)
1144
+ }
1145
+
1146
+ const current = items[index]
1147
+ const nextRows = rowsForCurrent(key, claims, current)
1148
+ const nextValidAt = items[index + 1]?.validFrom ?? null
1149
+ for (const row of nextRows) mergeOrPush(rows, clampRow(row, nextValidAt))
1150
+ }
1151
+ for (const row of rows) {
1152
+ if (row.status === 'active' && row.validTo !== null) row.status = 'superseded'
1153
+ }
1154
+ return rows
1155
+ }
1156
+
1157
+ // Monotonic append fast path: only the current group's final interval is closed,
1158
+ // new claims are derived from maintained alive-claim state, and one interval is
1159
+ // emitted. Total work for a group becomes linear instead of quadratic.
1160
+ function appendGroupIncremental(key, group, fact) {
1161
+ for (const row of group.rows) {
1162
+ if (row.validTo === null) clampRow(row, fact.validFrom)
1163
+ }
1164
+ applyClaimState(group, fact)
1165
+ const claims = claimsForAlive(group, registryEntry(fact.predicate))
1166
+ for (const row of rowsForCurrent(key, claims, fact)) {
1167
+ mergeOrPush(group.rows, clampRow(row, null))
1168
+ }
1169
+ for (const row of group.rows) {
1170
+ if (row.status === 'active' && row.validTo !== null) row.status = 'superseded'
1171
+ }
1172
+ return group.rows
1173
+ }
1174
+
1175
+ function updateGroupRows(key, group, fact) {
1176
+ const last = group.items[group.items.length - 1]
1177
+ if (!last || causalCompare(last.causalOrder, fact.causalOrder) <= 0) {
1178
+ group.items.push(fact)
1179
+ return appendGroupIncremental(key, group, fact)
1180
+ }
1181
+ let low = 0
1182
+ let high = group.items.length
1183
+ while (low < high) {
1184
+ const middle = (low + high) >> 1
1185
+ if (causalCompare(group.items[middle].causalOrder, fact.causalOrder) <= 0) low = middle + 1
1186
+ else high = middle
1187
+ }
1188
+ group.items.splice(low, 0, fact)
1189
+ group.rows = computeGroupRows(key, group.items)
1190
+ rebuildClaimState(group)
1191
+ return group.rows
1192
+ }
1193
+
1194
+ const systemFacts = pendingFacts.slice().sort((a, b) =>
1195
+ (a.fact.recordedAt - b.fact.recordedAt) || lexicalCompare(a.fact.sourceKind, b.fact.sourceKind) ||
1196
+ lexicalCompare(a.fact.assertionId, b.fact.assertionId))
1197
+ const applied = []
1198
+ const appliedById = new Map()
1199
+ const terminatedTargets = new Set()
1200
+ const groupSnapshots = new Map()
1201
+ const systemRowsByGroup = new Map()
1202
+
1203
+ for (const item of systemFacts) {
1204
+ const fact = item.fact
1205
+ if (fact.op === 'correct' || fact.op === 'retract') {
1206
+ const target = appliedById.get(fact.targetAssertionId)
1207
+ let reason = null
1208
+ if (!target) reason = 'target-unresolved'
1209
+ else if (terminatedTargets.has(target.assertionId)) reason = 'duplicate-terminal-operation'
1210
+ else if (target.scope !== fact.scope || target.subjectEntityId !== fact.subjectEntityId ||
1211
+ target.predicate !== fact.predicate ||
1212
+ canonicalJson(target.qualifiers) !== canonicalJson(fact.qualifiers)) reason = 'target-contract-mismatch'
1213
+ else if (fact.validFrom < target.validFrom) reason = 'terminal-valid-at-before-target'
1214
+ if (reason) {
1215
+ dead.push({ assertionId: fact.assertionId, targetAssertionId: fact.targetAssertionId, reason })
1216
+ continue
1217
+ }
1218
+ terminatedTargets.add(target.assertionId)
1219
+ }
1220
+
1221
+ applied.push(item)
1222
+ appliedById.set(fact.assertionId, fact)
1223
+
1224
+ const key = cachedFactKey(fact)
1225
+ let group = groupSnapshots.get(key)
1226
+ if (!group) {
1227
+ group = {
1228
+ key, items: [], rows: [], alive: [], terminated: new Set(), latestInvalidation: null,
1229
+ }
1230
+ groupSnapshots.set(key, group)
1231
+ }
1232
+ const nextSnapshot = updateGroupRows(key, group, fact)
1233
+ const previousRows = systemRowsByGroup.get(key) ?? []
1234
+ const previousByKey = new Map(previousRows.map((row) => [systemRowKey(row), row]))
1235
+ const nextByKey = new Map(nextSnapshot.map((row) => [systemRowKey(row), row]))
1236
+ const nextSystemRows = []
1237
+
1238
+ for (const [rowKey, previous] of previousByKey) {
1239
+ const nextRow = nextByKey.get(rowKey)
1240
+ if (!nextRow || cachedRowSignature(previous) !== rowSignature(nextRow)) {
1241
+ previous.retiredAt = fact.recordedAt
1242
+ }
1243
+ }
1244
+ for (const [rowKey, row] of nextByKey) {
1245
+ const previous = previousByKey.get(rowKey)
1246
+ if (previous && previous.retiredAt === null) {
1247
+ nextSystemRows.push(previous)
1248
+ } else {
1249
+ const version = { ...row, recordedAt: fact.recordedAt, retiredAt: null }
1250
+ systemSignatureCache.set(version, rowSignature(version))
1251
+ factVersions.push(version)
1252
+ nextSystemRows.push(version)
1253
+ }
1254
+ }
1255
+ systemRowsByGroup.set(key, nextSystemRows)
1256
+ }
1257
+
1258
+ const systemRows = [...systemRowsByGroup.values()].flat()
1259
+
1260
+ const entities = []
1261
+ const currentGenerationKeys = new Set()
1262
+ for (const state of entityStates.values()) {
1263
+ const generation = currentGeneration(state)
1264
+ if (!generation) continue
1265
+ entities.push({
1266
+ scope: state.scope,
1267
+ entityId: state.entityId,
1268
+ generation: generation.number,
1269
+ presence: generation.presence,
1270
+ openedBy: generation.openedBy,
1271
+ closedBy: generation.closedBy,
1272
+ lastValidAt: state.lastValidAt,
1273
+ lastRecordedAt: state.lastRecordedAt,
1274
+ })
1275
+ currentGenerationKeys.add(`${state.scope}\u0000${state.entityId}\u0000${generation.number}`)
1276
+ }
1277
+
1278
+ const currentFacts = systemRows.filter((fact) =>
1279
+ currentGenerationKeys.has(`${fact.scope}\u0000${fact.subjectEntityId}\u0000${fact.generation}`) &&
1280
+ (fact.op === 'assert' || fact.op === 'correct') &&
1281
+ (fact.status === 'active' || fact.status === 'conflicted') &&
1282
+ fact.validTo === null && fact.retiredAt === null)
1283
+
1284
+ entities.sort((a, b) => lexicalCompare(a.scope, b.scope) || lexicalCompare(a.entityId, b.entityId))
1285
+ factVersions.sort((a, b) => causalCompare(a.causalOrder, b.causalOrder) ||
1286
+ (a.recordedAt - b.recordedAt) || lexicalCompare(a.factKey, b.factKey) || (a.validFrom - b.validFrom))
1287
+ currentFacts.sort((a, b) => lexicalCompare(a.factKey, b.factKey) || lexicalCompare(a.assertionId, b.assertionId))
1288
+
1289
+ return {
1290
+ reducerVersion,
1291
+ entities,
1292
+ observationPlacements,
1293
+ assertionPlacements,
1294
+ factVersions,
1295
+ currentFacts,
1296
+ dead,
1297
+ }
1298
+ }
1299
+
1300
+ export function factsAt(projection, { validAt, recordedAt }) {
1301
+ return projection.factVersions.filter((fact) =>
1302
+ fact.recordedAt <= recordedAt && (fact.retiredAt === null || recordedAt < fact.retiredAt) &&
1303
+ fact.validFrom <= validAt && (fact.validTo === null || validAt < fact.validTo) &&
1304
+ (fact.status === 'active' || fact.status === 'superseded' || fact.status === 'conflicted'))
1305
+ .sort((a, b) => lexicalCompare(a.factKey, b.factKey) || lexicalCompare(a.assertionId, b.assertionId))
1306
+ }
1307
+
1308
+ export function projectionHash(projection) {
1309
+ const facts = projection.currentFacts
1310
+ .filter((fact) => (fact.status === 'active' || fact.status === 'conflicted') &&
1311
+ fact.validTo === null && fact.retiredAt === null)
1312
+ .map((fact) => ({
1313
+ fact_key: fact.factKey,
1314
+ subject_entity_id: fact.subjectEntityId,
1315
+ generation: fact.generation,
1316
+ predicate: fact.predicate,
1317
+ value_json: fact.value ?? null,
1318
+ qualifiers_json: fact.qualifiers ?? null,
1319
+ assertion_id: fact.assertionId,
1320
+ evidence_json: fact.evidence ?? null,
1321
+ causal_order: fact.causalOrder,
1322
+ valid_from: fact.validFrom,
1323
+ valid_to: fact.validTo ?? null,
1324
+ recorded_at: fact.recordedAt,
1325
+ status: fact.status,
1326
+ verification: fact.verification,
1327
+ conflict_json: fact.conflict ?? null,
1328
+ }))
1329
+ .sort((a, b) => lexicalCompare(a.fact_key, b.fact_key) || lexicalCompare(a.assertion_id, b.assertion_id))
1330
+ return sha256Canonical({ reducer_version: projection.reducerVersion, facts })
1331
+ }