diff --git a/packages/session-query/session-query/src/index.ts b/packages/session-query/session-query/src/index.ts index c468f31696..e5659c554a 100644 --- a/packages/session-query/session-query/src/index.ts +++ b/packages/session-query/session-query/src/index.ts @@ -78,6 +78,7 @@ export class SessionQueryService extends Service { * Trace known ancestry and descendants from one corpus observation. * @param sessionId - logical session id to trace. * @returns a complete lineage or an explicit unresolved parent boundary. + * @throws when corpus resolution fails, the target is absent, or its known ancestry cycles. */ async traceSession(sessionId: SessionId): Promise { const records = await this._corpus.listSessions() @@ -88,6 +89,7 @@ export class SessionQueryService extends Service { * Trace one event's direct positional and provenance relationships. * @param request - target session id and event seq. * @returns direct links plus the target's positional replacement chain. + * @throws when source resolution fails, the target is absent, or surface/provenance validation fails. */ async traceEvent(request: SessionEventTraceRequest): Promise { const loaded = await this._corpus.load(request.sessionId) diff --git a/packages/session-query/session-query/src/tracing.ts b/packages/session-query/session-query/src/tracing.ts index d0c7f200cb..c7cfd89b9a 100644 --- a/packages/session-query/session-query/src/tracing.ts +++ b/packages/session-query/session-query/src/tracing.ts @@ -105,7 +105,12 @@ export function traceLineage( let unresolvedParentId: SessionId | undefined let parentId = target.header.parentSession while (parentId !== undefined) { - if (ancestrySeen.has(parentId)) lineageCycle(parentId) + if (ancestrySeen.has(parentId)) { + throw new SessionQueryError( + `session lineage contains a cycle at "${parentId}"`, + 'SESSION_QUERY_INVALID_LINEAGE', + ) + } ancestrySeen.add(parentId) const parent = byId.get(parentId) if (parent === undefined) { @@ -146,7 +151,17 @@ function analyzeEventLog( sessionId: SessionId, events: readonly SessionEvent[], ): EventLogAnalysis { - const folded = safeFold(events) + let folded: ReturnType + try { + folded = foldSurface(events) + } catch (error: unknown) { + throw new SessionQueryError( + /* v8 ignore next -- foldSurface throws Error instances */ + `invalid session surface: ${error instanceof Error ? error.message : 'unknown error'}`, + 'SESSION_QUERY_INVALID_SURFACE', + { cause: error }, + ) + } const current = new Set(folded.nodes.map(node => node.seq)) const shadowed = new Set() const replacedBy = new Map() @@ -182,15 +197,24 @@ function validateProvenance( const sources = rawEventSources(event) if (sources === undefined) continue if (!isSurfaceEligibleType(event.type)) { - invalidProvenance(`non-surface event at seq ${event.seq} carries sourceEventSeqs`) + 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) { - invalidProvenance(`event at seq ${event.seq} has an empty or invalid sourceEventSeqs`) + 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() for (const source of sources as unknown[]) { if (unique.has(source)) { - invalidProvenance(`event at seq ${event.seq} repeats source seq ${String(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 ( @@ -200,7 +224,10 @@ function validateProvenance( || source >= event.seq || events[source]?.seq !== source ) { - invalidProvenance(`event at seq ${event.seq} references unknown or non-earlier source seq ${String(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', + ) } } } @@ -212,12 +239,18 @@ function validateProvenance( const replacement = events[replacementSeq]! const sources = rawEventSources(replacement) if (!Array.isArray(sources)) { - invalidProvenance(`replacement at seq ${replacementSeq} omits its shadowed surface 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)) { - invalidProvenance(`replacement at seq ${replacementSeq} omits shadowed surface seq ${removedSeq}`) + throw new SessionQueryError( + `invalid session provenance: replacement at seq ${replacementSeq} omits shadowed surface seq ${removedSeq}`, + 'SESSION_QUERY_INVALID_PROVENANCE', + ) } } } @@ -232,19 +265,6 @@ function eventSources(event: SessionEvent): number[] { return Array.isArray(sources) ? sources as number[] : [] } -function safeFold(events: readonly SessionEvent[]): ReturnType { - try { - return foldSurface(events) - } catch (error: unknown) { - throw new SessionQueryError( - /* v8 ignore next -- foldSurface throws Error instances */ - `invalid session surface: ${error instanceof Error ? error.message : 'unknown error'}`, - 'SESSION_QUERY_INVALID_SURFACE', - { cause: error }, - ) - } -} - function buildDescendants( childrenByParent: ReadonlyMap, sessionId: SessionId, @@ -278,17 +298,3 @@ function compareSessionsAscending(a: SessionRecord, b: SessionRecord): number { function cloneRecord(record: SessionRecord): SessionRecord { return { ...record, header: structuredClone(record.header) } } - -function lineageCycle(id: SessionId): never { - throw new SessionQueryError( - `session lineage contains a cycle at "${id}"`, - 'SESSION_QUERY_INVALID_LINEAGE', - ) -} - -function invalidProvenance(message: string): never { - throw new SessionQueryError( - `invalid session provenance: ${message}`, - 'SESSION_QUERY_INVALID_PROVENANCE', - ) -}