refactor(session): centralize surface provenance validation

This commit is contained in:
Hypatia May
2026-07-13 14:46:43 +08:00
parent 7351f07995
commit 75de01f06d
10 changed files with 171 additions and 131 deletions

View File

@@ -14,7 +14,7 @@ This is trusted context-wide infrastructure. It performs no caller authorization
Persistence is optional and may mount or unmount dynamically. Cross-corpus listing and lineage tracing fail with `SESSION_QUERY_PERSISTENCE_FAILED` while mounted persistence is unreadable. An event read or trace targeting a known live session does not consult persistence, so durable backend health cannot make current in-memory history unreadable. Persisted event operations list before loading and reject a metadata mismatch rather than combining inconsistent observations.
`traceEvent()` validates the whole loaded log before returning relationships: provenance arrays are nonempty and duplicate-free, references name known earlier events, only surface event types carry sources, and each positional replacement names every surface node it removed. Provenance violations fail with `SESSION_QUERY_INVALID_PROVENANCE`; positional fold failures remain `SESSION_QUERY_INVALID_SURFACE`. `listEvents()` only needs surface classification and deliberately does not enforce the trace-specific provenance contract.
`traceEvent()` validates the whole loaded log with `dsh-session`'s shared provenance checker before returning relationships: provenance arrays are nonempty and duplicate-free, references name known earlier events, only surface event types carry sources, and each positional replacement names every surface node it removed. Provenance violations fail with `SESSION_QUERY_INVALID_PROVENANCE`; positional fold failures remain `SESSION_QUERY_INVALID_SURFACE`. `listEvents()` only needs surface classification and deliberately does not enforce the trace-specific provenance contract.
`SessionQueryError.code` is a closed union: `SESSION_QUERY_EVENT_NOT_FOUND`, `SESSION_QUERY_INVALID_CONFIG`, `SESSION_QUERY_INVALID_LINEAGE`, `SESSION_QUERY_INVALID_PROVENANCE`, `SESSION_QUERY_INVALID_SURFACE`, `SESSION_QUERY_INVALID_WINDOW`, `SESSION_QUERY_PERSISTENCE_FAILED`, `SESSION_QUERY_SESSION_NOT_FOUND`, and `SESSION_QUERY_SOURCE_CONFLICT`.

View File

@@ -1,6 +1,6 @@
/** One-shot session-lineage and event-relationship tracing helpers. */
import { foldSurface, isSurfaceEligibleType } from '@deepseek-ai/dsh-session'
import { foldSurface, validateSurfaceProvenance } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { SessionQueryError } from './config.ts'
import type {
@@ -51,7 +51,21 @@ export function traceEventLog(
}
const analysis = analyzeEventLog(sessionId, events)
validateProvenance(events, analysis.replacedEventSeqs)
const knownSeqs = new Set<number>()
for (const event of events) {
const violation = validateSurfaceProvenance(
event,
knownSeqs,
analysis.replacedEventSeqs.get(event.seq),
)
if (violation !== undefined) {
throw new SessionQueryError(
`invalid session provenance: ${violation}`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
knownSeqs.add(event.seq)
}
const replacementChain: number[] = []
let replacement = analysis.replacedBy.get(seq)
@@ -189,73 +203,6 @@ function analyzeEventLog(
}
}
function validateProvenance(
events: readonly SessionEvent[],
replacedEventSeqs: ReadonlyMap<number, readonly number[]>,
): void {
for (const event of events) {
const sources = rawEventSources(event)
if (sources === undefined) continue
if (!isSurfaceEligibleType(event.type)) {
throw new SessionQueryError(
`invalid session provenance: non-surface event at seq ${event.seq} carries sourceEventSeqs`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
if (!Array.isArray(sources) || sources.length === 0) {
throw new SessionQueryError(
`invalid session provenance: event at seq ${event.seq} has an empty or invalid sourceEventSeqs`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
const unique = new Set<unknown>()
for (const source of sources as unknown[]) {
if (unique.has(source)) {
throw new SessionQueryError(
`invalid session provenance: event at seq ${event.seq} repeats source seq ${String(source)}`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
unique.add(source)
if (
typeof source !== 'number'
|| !Number.isInteger(source)
|| source < 0
|| source >= event.seq
|| events[source]?.seq !== source
) {
throw new SessionQueryError(
`invalid session provenance: event at seq ${event.seq} references unknown or non-earlier source seq ${String(source)}`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
}
}
for (const [replacementSeq, removedSeqs] of replacedEventSeqs) {
// Canonical logs guarantee events[i].seq === i, and the fold reports only
// replacement events from this input log.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const replacement = events[replacementSeq]!
const sources = rawEventSources(replacement)
if (!Array.isArray(sources)) {
throw new SessionQueryError(
`invalid session provenance: replacement at seq ${replacementSeq} omits its shadowed surface sources`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
const sourceSet = new Set(sources as unknown[])
for (const removedSeq of removedSeqs) {
if (!sourceSet.has(removedSeq)) {
throw new SessionQueryError(
`invalid session provenance: replacement at seq ${replacementSeq} omits shadowed surface seq ${removedSeq}`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}
}
}
}
function rawEventSources(event: SessionEvent): unknown {
return (event as SessionEvent & { sourceEventSeqs?: unknown }).sourceEventSeqs
}