Ver Fonte

refactor(surface): use nodeBySeq map for lookup in _replace, drop dead params

Hypatia May há 3 meses atrás
pai
commit
1ec8c40d0d

+ 45 - 1
docs/core-data-structures/session.md

@@ -66,7 +66,51 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
 
 `SessionEventType = keyof SessionEventMap`. Because `SessionEventMap` is merge-extensible, switches over `SessionEvent` must NOT use `assertNever` — a plugin-added variant is a valid unknown value; handle the known cases and fall through `default`.
 
-The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) additionally carry two optional surface fields: `surfaceOp` (how the event enters the derived surface linked list — `'append'` or a `{ op: 'replace', start, end }` shadow) and `sourceEventSeqs` (provenance). See the [session surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md).
+## Surface types
+
+The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) carry surface metadata declaring how they join the derived surface linked list. See the [session surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md).
+
+### `SurfaceEventType` — the message-producing subset of event types
+
+```ts type-equiv
+export type SurfaceEventType =
+  | 'user/message'
+  | 'assistant/message'
+  | 'tool/result'
+  | 'context/message'
+  | 'steering/message'
+```
+
+### `SurfaceOp` — how an event entered the surface
+
+```ts type-equiv
+export type SurfaceOp =
+  | 'append'
+  | { op: 'replace'; start: number; end: number }
+```
+
+`'append'` is the normal tail-append path. `replace` shadows surface nodes from `start` through `end` inclusive (both must be valid surface node seqs; `start === end` replaces a single node) and inserts the new node in their place.
+
+### `SurfaceIntent` — the parameter to `session.append()`
+
+```ts type-equiv
+export interface SurfaceIntent {
+  surfaceOp: SurfaceOp
+  sourceEventSeqs?: number[]
+}
+```
+
+Required for `SurfaceEventType` events — every message-producing event must declare how it joins the surface, the sole source of derived history. Non-surface types reject it at compile time.
+
+### `SurfaceNode` — a node in the surface linked list
+
+```ts type-equiv
+export interface SurfaceNode {
+  seq: number
+  prev: number | null
+  next: number | null
+}
+```
 
 ## Derived history: `deriveMessages()`
 

+ 14 - 14
packages/core/session/src/surface.ts

@@ -55,7 +55,7 @@ export interface SurfaceNode {
 export class SurfaceManager {
   /** Surface nodes in linked-list order (head to tail). Empty until first access. */
   private _nodes: SurfaceNode[] = []
-  /** Map from event seq → node for O(1) lookup during replacements. */
+  /** Map from event seq → node. */
   private _nodeBySeq = new Map<number, SurfaceNode>()
   /** The last processed seq. -1 forces a full rebuild on first access. */
   private _lastProcessedSeq = -1
@@ -100,7 +100,7 @@ export class SurfaceManager {
         this._nodes.push(node)
         this._nodeBySeq.set(event.seq, node)
       } else {
-        this._replace(this._nodes, this._nodeBySeq, event.seq, event.surfaceOp)
+        this._replace(event.seq, event.surfaceOp)
       }
     }
     this._lastProcessedSeq = this.log.length - 1
@@ -108,31 +108,31 @@ export class SurfaceManager {
 
   /** Apply a replace operation to the in-progress surface. */
   private _replace(
-    nodes: SurfaceNode[],
-    nodeBySeq: Map<number, SurfaceNode>,
     newSeq: number,
     op: Extract<SurfaceOp, { op: 'replace' }>,
   ): void {
-    const startIdx = nodes.findIndex(n => n.seq === op.start)
-    if (startIdx === -1) {
+    const startNode = this._nodeBySeq.get(op.start)
+    if (!startNode) {
       throw new Error(`surface replace: start seq ${op.start} not found in surface`)
     }
-    const endIdx = nodes.findIndex(n => n.seq === op.end)
-    if (endIdx === -1) {
+    const endNode = this._nodeBySeq.get(op.end)
+    if (!endNode) {
       throw new Error(`surface replace: end seq ${op.end} not found in surface`)
     }
+    const startIdx = this._nodes.indexOf(startNode)
+    const endIdx = this._nodes.indexOf(endNode)
     if (startIdx > endIdx) {
       throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`)
     }
 
     // Remove shadowed nodes from `[startIdx, endIdx]` inclusive.
     const count = endIdx - startIdx + 1
-    const removed = nodes.splice(startIdx, count)
-    for (const r of removed) nodeBySeq.delete(r.seq)
+    const removed = this._nodes.splice(startIdx, count)
+    for (const r of removed) this._nodeBySeq.delete(r.seq)
 
     // Insert the new node where the removed range was.
-    const prevNode = startIdx > 0 ? nodes[startIdx - 1] : undefined
-    const nextNode = startIdx < nodes.length ? nodes[startIdx] : undefined
+    const prevNode = startIdx > 0 ? this._nodes[startIdx - 1] : undefined
+    const nextNode = startIdx < this._nodes.length ? this._nodes[startIdx] : undefined
 
     const newNode: SurfaceNode = {
       seq: newSeq,
@@ -141,7 +141,7 @@ export class SurfaceManager {
     }
     if (prevNode) prevNode.next = newSeq
     if (nextNode) nextNode.prev = newSeq
-    nodes.splice(startIdx, 0, newNode)
-    nodeBySeq.set(newSeq, newNode)
+    this._nodes.splice(startIdx, 0, newNode)
+    this._nodeBySeq.set(newSeq, newNode)
   }
 }

+ 4 - 0
scripts/type-equiv.manifest.json

@@ -19,6 +19,10 @@
     { "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" },
     { "doc": "docs/core-data-structures/session.md", "symbol": "TurnTriggerMap", "source": "packages/core/session/src/types.ts" },
     { "doc": "docs/core-data-structures/session.md", "symbol": "TurnEndReasonMap", "source": "packages/core/session/src/types.ts" },
+    { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceEventType", "source": "packages/core/session/src/types.ts" },
+    { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceOp", "source": "packages/core/session/src/types.ts" },
+    { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceIntent", "source": "packages/core/session/src/types.ts" },
+    { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceNode", "source": "packages/core/session/src/surface.ts" },
 
     { "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" },
     { "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" },