refactor(session): centralize surface provenance validation
This commit is contained in:
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user