@deepwatch/dsh-trajectory 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/lib/compare.d.ts +186 -0
- package/lib/compare.js +314 -0
- package/lib/definition.d.ts +91 -0
- package/lib/definition.js +176 -0
- package/lib/events.d.ts +172 -0
- package/lib/events.js +244 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/projection.d.ts +72 -0
- package/lib/projection.js +174 -0
- package/lib/selection-store.d.ts +63 -0
- package/lib/selection-store.js +108 -0
- package/lib/selection.d.ts +106 -0
- package/lib/selection.js +223 -0
- package/package.json +47 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Watch projection over DSH's session log, and its replay guarantee.
|
|
3
|
+
*
|
|
4
|
+
* Replay is the property that makes a receipt worth anything. If reopening a
|
|
5
|
+
* finished session could produce a different picture of it, then the picture
|
|
6
|
+
* is an opinion rather than a record, and a verdict shown in it proves nothing
|
|
7
|
+
* about what happened at the time.
|
|
8
|
+
*
|
|
9
|
+
* So this is a pure fold: the same events in, the same projection out, with no
|
|
10
|
+
* clock read, no network, no model, and nothing regenerated. `projectionHash`
|
|
11
|
+
* makes that assertable rather than assumed — two runs over the same log
|
|
12
|
+
* produce the same hash, and a change to what a record carries changes it.
|
|
13
|
+
*
|
|
14
|
+
* @module @deepwatch/dsh-trajectory/projection
|
|
15
|
+
*/
|
|
16
|
+
import type { SessionEventLike, WatchTrajectoryRecord } from './events.js';
|
|
17
|
+
/** The Watch view of one session. */
|
|
18
|
+
export interface WatchProjection {
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
/** Records in log order. Ordering is by DSH sequence, never by wall clock. */
|
|
21
|
+
readonly records: readonly WatchTrajectoryRecord[];
|
|
22
|
+
/** Evidence id to the record that produced it, for citation resolution. */
|
|
23
|
+
readonly byEvidence: ReadonlyMap<string, WatchTrajectoryRecord>;
|
|
24
|
+
/** Record id to record, for deep-link restoration. */
|
|
25
|
+
readonly byRecord: ReadonlyMap<string, WatchTrajectoryRecord>;
|
|
26
|
+
}
|
|
27
|
+
/** An empty projection, so a session with no Watch activity is still a value. */
|
|
28
|
+
export declare function emptyProjection(sessionId: string): WatchProjection;
|
|
29
|
+
/**
|
|
30
|
+
* Fold a session's events into the Watch projection.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately takes the events rather than a live session: the same function
|
|
33
|
+
* serves the live view and replay, so there is no second code path whose
|
|
34
|
+
* output could differ from the one people trust.
|
|
35
|
+
*
|
|
36
|
+
* @param events - DSH session events in ascending sequence order.
|
|
37
|
+
* @param sessionId - the session these belong to.
|
|
38
|
+
*/
|
|
39
|
+
export declare function project(events: readonly SessionEventLike[], sessionId: string): WatchProjection;
|
|
40
|
+
/**
|
|
41
|
+
* Merge externally-produced records into a projection.
|
|
42
|
+
*
|
|
43
|
+
* Memory records do not come from tool results — memory influences a turn
|
|
44
|
+
* without the agent calling anything — so they are contributed rather than
|
|
45
|
+
* derived. They are sorted into the same sequence order so the ledger stays
|
|
46
|
+
* one chronology rather than two lists shown together.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withRecords(projection: WatchProjection, extra: readonly WatchTrajectoryRecord[]): WatchProjection;
|
|
49
|
+
/**
|
|
50
|
+
* A stable hash of what a projection contains.
|
|
51
|
+
*
|
|
52
|
+
* Over the identifiers and the verdict, not over wall-clock times or free
|
|
53
|
+
* text: a replay that produced the same records at the same sequence numbers
|
|
54
|
+
* with the same verdicts *is* the same projection, and a hash that also
|
|
55
|
+
* covered the summary line would fail whenever someone improved the wording.
|
|
56
|
+
*
|
|
57
|
+
* Not cryptographic, and not trying to be. It exists to catch a projection
|
|
58
|
+
* that changed, not to resist someone forging one.
|
|
59
|
+
*/
|
|
60
|
+
export declare function projectionHash(projection: WatchProjection): string;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a deep-linked selection against a projection.
|
|
63
|
+
*
|
|
64
|
+
* Returns the record a link points at, preferring the record id and falling
|
|
65
|
+
* back to the evidence id — a link made before a record id changed still opens
|
|
66
|
+
* the right evidence, which is the part a person cared about.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveRecord(projection: WatchProjection, selection: {
|
|
69
|
+
readonly recordId: string | null;
|
|
70
|
+
readonly evidenceId: string | null;
|
|
71
|
+
}): WatchTrajectoryRecord | null;
|
|
72
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Watch projection over DSH's session log, and its replay guarantee.
|
|
3
|
+
*
|
|
4
|
+
* Replay is the property that makes a receipt worth anything. If reopening a
|
|
5
|
+
* finished session could produce a different picture of it, then the picture
|
|
6
|
+
* is an opinion rather than a record, and a verdict shown in it proves nothing
|
|
7
|
+
* about what happened at the time.
|
|
8
|
+
*
|
|
9
|
+
* So this is a pure fold: the same events in, the same projection out, with no
|
|
10
|
+
* clock read, no network, no model, and nothing regenerated. `projectionHash`
|
|
11
|
+
* makes that assertable rather than assumed — two runs over the same log
|
|
12
|
+
* produce the same hash, and a change to what a record carries changes it.
|
|
13
|
+
*
|
|
14
|
+
* @module @deepwatch/dsh-trajectory/projection
|
|
15
|
+
*/
|
|
16
|
+
import { isWatchTool, recordsFromToolResult, toolResultValue } from './events.js';
|
|
17
|
+
/** An empty projection, so a session with no Watch activity is still a value. */
|
|
18
|
+
export function emptyProjection(sessionId) {
|
|
19
|
+
return {
|
|
20
|
+
sessionId,
|
|
21
|
+
records: [],
|
|
22
|
+
byEvidence: new Map(),
|
|
23
|
+
byRecord: new Map(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function str(value) {
|
|
27
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
28
|
+
}
|
|
29
|
+
function num(value) {
|
|
30
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fold a session's events into the Watch projection.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately takes the events rather than a live session: the same function
|
|
36
|
+
* serves the live view and replay, so there is no second code path whose
|
|
37
|
+
* output could differ from the one people trust.
|
|
38
|
+
*
|
|
39
|
+
* @param events - DSH session events in ascending sequence order.
|
|
40
|
+
* @param sessionId - the session these belong to.
|
|
41
|
+
*/
|
|
42
|
+
export function project(events, sessionId) {
|
|
43
|
+
const pending = new Map();
|
|
44
|
+
const records = [];
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
if (event.type === 'tool/call') {
|
|
47
|
+
const name = str(event.data['name']);
|
|
48
|
+
if (!isWatchTool(name))
|
|
49
|
+
continue;
|
|
50
|
+
const callId = str(event.data['callId']);
|
|
51
|
+
if (callId === null)
|
|
52
|
+
continue;
|
|
53
|
+
pending.set(callId, {
|
|
54
|
+
name: name,
|
|
55
|
+
turn: num(event.data['turn']),
|
|
56
|
+
step: num(event.data['step']),
|
|
57
|
+
// Correlation travels in the tool arguments when the caller set one;
|
|
58
|
+
// it is what ties this row to Watch Core's own logs and receipts.
|
|
59
|
+
correlationId: str(event.data['arguments']?.['correlationId']),
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (event.type !== 'tool/result')
|
|
64
|
+
continue;
|
|
65
|
+
const message = event.data['message'];
|
|
66
|
+
const source = message?.['source'];
|
|
67
|
+
const callId = str(source?.['callId']);
|
|
68
|
+
if (callId === null)
|
|
69
|
+
continue;
|
|
70
|
+
const call = pending.get(callId);
|
|
71
|
+
if (call === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
pending.delete(callId);
|
|
74
|
+
const context = {
|
|
75
|
+
sessionId,
|
|
76
|
+
turn: call.turn,
|
|
77
|
+
step: call.step,
|
|
78
|
+
callId,
|
|
79
|
+
toolName: call.name,
|
|
80
|
+
correlationId: call.correlationId,
|
|
81
|
+
};
|
|
82
|
+
records.push(...recordsFromToolResult(event, context, toolResultValue(event)));
|
|
83
|
+
}
|
|
84
|
+
return index(sessionId, records);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge externally-produced records into a projection.
|
|
88
|
+
*
|
|
89
|
+
* Memory records do not come from tool results — memory influences a turn
|
|
90
|
+
* without the agent calling anything — so they are contributed rather than
|
|
91
|
+
* derived. They are sorted into the same sequence order so the ledger stays
|
|
92
|
+
* one chronology rather than two lists shown together.
|
|
93
|
+
*/
|
|
94
|
+
export function withRecords(projection, extra) {
|
|
95
|
+
if (extra.length === 0)
|
|
96
|
+
return projection;
|
|
97
|
+
const merged = [...projection.records, ...extra]
|
|
98
|
+
.sort((a, b) => {
|
|
99
|
+
const bySeq = a.seq - b.seq;
|
|
100
|
+
return bySeq !== 0 ? bySeq : a.recordId.localeCompare(b.recordId);
|
|
101
|
+
});
|
|
102
|
+
return index(projection.sessionId, merged);
|
|
103
|
+
}
|
|
104
|
+
/** Build the lookup maps once, so callers never scan. */
|
|
105
|
+
function index(sessionId, records) {
|
|
106
|
+
const byEvidence = new Map();
|
|
107
|
+
const byRecord = new Map();
|
|
108
|
+
for (const record of records) {
|
|
109
|
+
byRecord.set(record.recordId, record);
|
|
110
|
+
for (const evidenceId of record.refs.evidenceIds) {
|
|
111
|
+
// First writer wins: the record that produced a piece of evidence is the
|
|
112
|
+
// one a citation should select, not a later one that merely cites it.
|
|
113
|
+
if (!byEvidence.has(evidenceId))
|
|
114
|
+
byEvidence.set(evidenceId, record);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { sessionId, records, byEvidence, byRecord };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A stable hash of what a projection contains.
|
|
121
|
+
*
|
|
122
|
+
* Over the identifiers and the verdict, not over wall-clock times or free
|
|
123
|
+
* text: a replay that produced the same records at the same sequence numbers
|
|
124
|
+
* with the same verdicts *is* the same projection, and a hash that also
|
|
125
|
+
* covered the summary line would fail whenever someone improved the wording.
|
|
126
|
+
*
|
|
127
|
+
* Not cryptographic, and not trying to be. It exists to catch a projection
|
|
128
|
+
* that changed, not to resist someone forging one.
|
|
129
|
+
*/
|
|
130
|
+
export function projectionHash(projection) {
|
|
131
|
+
const canonical = projection.records.map(record => [
|
|
132
|
+
record.recordId,
|
|
133
|
+
record.type,
|
|
134
|
+
String(record.seq),
|
|
135
|
+
record.refs.sourceRevisionId ?? '',
|
|
136
|
+
record.refs.evidenceIds.join(','),
|
|
137
|
+
record.refs.verificationId ?? '',
|
|
138
|
+
record.refs.verdict ?? '',
|
|
139
|
+
record.refs.receiptId ?? '',
|
|
140
|
+
record.refs.memoryIds.join(','),
|
|
141
|
+
record.refs.temporalRange === null
|
|
142
|
+
? ''
|
|
143
|
+
: `${String(record.refs.temporalRange.startMs)}-${String(record.refs.temporalRange.endMs)}`,
|
|
144
|
+
record.redacted ? 'redacted' : '',
|
|
145
|
+
].join('|')).join('\n');
|
|
146
|
+
// FNV-1a, 64-bit, in BigInt. Chosen because it is short, dependency-free,
|
|
147
|
+
// and identical in every runtime this projection is compared across.
|
|
148
|
+
let hash = 0xcbf29ce484222325n;
|
|
149
|
+
const prime = 0x100000001b3n;
|
|
150
|
+
const mask = 0xffffffffffffffffn;
|
|
151
|
+
for (const byte of new TextEncoder().encode(canonical)) {
|
|
152
|
+
hash = ((hash ^ BigInt(byte)) * prime) & mask;
|
|
153
|
+
}
|
|
154
|
+
return `fnv1a64:${hash.toString(16).padStart(16, '0')}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve a deep-linked selection against a projection.
|
|
158
|
+
*
|
|
159
|
+
* Returns the record a link points at, preferring the record id and falling
|
|
160
|
+
* back to the evidence id — a link made before a record id changed still opens
|
|
161
|
+
* the right evidence, which is the part a person cared about.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveRecord(projection, selection) {
|
|
164
|
+
if (selection.recordId !== null) {
|
|
165
|
+
const byId = projection.byRecord.get(selection.recordId);
|
|
166
|
+
if (byId !== undefined)
|
|
167
|
+
return byId;
|
|
168
|
+
}
|
|
169
|
+
if (selection.evidenceId !== null) {
|
|
170
|
+
return projection.byEvidence.get(selection.evidenceId) ?? null;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one selection every Watch surface reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* DSH's Trajectory selection is documented as local to Trajectory, with no
|
|
5
|
+
* anchor deep links. This is the smallest additive extension around that: a
|
|
6
|
+
* Watch-owned service holding the canonical selection for Watch-related
|
|
7
|
+
* records, which surfaces subscribe to. Nothing upstream is replaced — DSH
|
|
8
|
+
* keeps its own local selection, and Watch drives it from here.
|
|
9
|
+
*
|
|
10
|
+
* A store rather than a value, because several panels need to observe the same
|
|
11
|
+
* change; but the value it holds is the pure `WatchSelection` from
|
|
12
|
+
* `@deepwatch/dsh-trajectory`, so every rule about what a selection means
|
|
13
|
+
* stays testable without a browser.
|
|
14
|
+
*
|
|
15
|
+
* @module @deepwatch/dsh-trajectory/selection-store
|
|
16
|
+
*/
|
|
17
|
+
import type { WatchSelection } from './selection.js';
|
|
18
|
+
import type { WatchProjection } from './projection.js';
|
|
19
|
+
/** Notified whenever the selection changes. */
|
|
20
|
+
export type SelectionListener = (selection: WatchSelection) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Holds the canonical Watch selection for one workspace.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately has no knowledge of panels. A panel subscribes and renders; it
|
|
25
|
+
* never asks another panel what is selected, which is what stops the panels
|
|
26
|
+
* from disagreeing.
|
|
27
|
+
*/
|
|
28
|
+
export declare class WatchSelectionStore {
|
|
29
|
+
private current;
|
|
30
|
+
private readonly listeners;
|
|
31
|
+
/** The latest projection, so a deep link can resolve without a round trip. */
|
|
32
|
+
private projection;
|
|
33
|
+
constructor(workspaceId: string, sessionId: string);
|
|
34
|
+
/** The current selection. Always a value; never undefined. */
|
|
35
|
+
get(): WatchSelection;
|
|
36
|
+
/**
|
|
37
|
+
* Replace the selection.
|
|
38
|
+
*
|
|
39
|
+
* A selection identical to the current one is dropped rather than
|
|
40
|
+
* broadcast. Panels both read and write this store, so an echo would loop:
|
|
41
|
+
* a panel reacts to its own change, re-selects, and notifies again.
|
|
42
|
+
*/
|
|
43
|
+
set(selection: WatchSelection): void;
|
|
44
|
+
/** Subscribe; returns an unsubscribe function. */
|
|
45
|
+
subscribe(listener: SelectionListener): () => void;
|
|
46
|
+
/** Publish the projection a deep link should resolve against. */
|
|
47
|
+
setProjection(projection: WatchProjection): void;
|
|
48
|
+
/** The current projection, or null before the session has loaded. */
|
|
49
|
+
getProjection(): WatchProjection | null;
|
|
50
|
+
/** A link that restores the current selection. */
|
|
51
|
+
link(): string;
|
|
52
|
+
/**
|
|
53
|
+
* Restore a selection from a link.
|
|
54
|
+
*
|
|
55
|
+
* @returns whether the fragment was a Watch link that named a session. A
|
|
56
|
+
* fragment that is not one leaves the selection alone rather than clearing
|
|
57
|
+
* it to a half-restored state.
|
|
58
|
+
*/
|
|
59
|
+
restore(fragment: string): boolean;
|
|
60
|
+
/** Release every subscriber. Called when the plugin unloads. */
|
|
61
|
+
dispose(): void;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=selection-store.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one selection every Watch surface reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* DSH's Trajectory selection is documented as local to Trajectory, with no
|
|
5
|
+
* anchor deep links. This is the smallest additive extension around that: a
|
|
6
|
+
* Watch-owned service holding the canonical selection for Watch-related
|
|
7
|
+
* records, which surfaces subscribe to. Nothing upstream is replaced — DSH
|
|
8
|
+
* keeps its own local selection, and Watch drives it from here.
|
|
9
|
+
*
|
|
10
|
+
* A store rather than a value, because several panels need to observe the same
|
|
11
|
+
* change; but the value it holds is the pure `WatchSelection` from
|
|
12
|
+
* `@deepwatch/dsh-trajectory`, so every rule about what a selection means
|
|
13
|
+
* stays testable without a browser.
|
|
14
|
+
*
|
|
15
|
+
* @module @deepwatch/dsh-trajectory/selection-store
|
|
16
|
+
*/
|
|
17
|
+
import { emptySelection, fromDeepLink, toDeepLink } from './selection.js';
|
|
18
|
+
/**
|
|
19
|
+
* Holds the canonical Watch selection for one workspace.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately has no knowledge of panels. A panel subscribes and renders; it
|
|
22
|
+
* never asks another panel what is selected, which is what stops the panels
|
|
23
|
+
* from disagreeing.
|
|
24
|
+
*/
|
|
25
|
+
export class WatchSelectionStore {
|
|
26
|
+
current;
|
|
27
|
+
listeners = new Set();
|
|
28
|
+
/** The latest projection, so a deep link can resolve without a round trip. */
|
|
29
|
+
projection = null;
|
|
30
|
+
constructor(workspaceId, sessionId) {
|
|
31
|
+
this.current = emptySelection(workspaceId, sessionId);
|
|
32
|
+
}
|
|
33
|
+
/** The current selection. Always a value; never undefined. */
|
|
34
|
+
get() {
|
|
35
|
+
return this.current;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Replace the selection.
|
|
39
|
+
*
|
|
40
|
+
* A selection identical to the current one is dropped rather than
|
|
41
|
+
* broadcast. Panels both read and write this store, so an echo would loop:
|
|
42
|
+
* a panel reacts to its own change, re-selects, and notifies again.
|
|
43
|
+
*/
|
|
44
|
+
set(selection) {
|
|
45
|
+
if (isSame(this.current, selection))
|
|
46
|
+
return;
|
|
47
|
+
this.current = selection;
|
|
48
|
+
for (const listener of this.listeners)
|
|
49
|
+
listener(selection);
|
|
50
|
+
}
|
|
51
|
+
/** Subscribe; returns an unsubscribe function. */
|
|
52
|
+
subscribe(listener) {
|
|
53
|
+
this.listeners.add(listener);
|
|
54
|
+
return () => { this.listeners.delete(listener); };
|
|
55
|
+
}
|
|
56
|
+
/** Publish the projection a deep link should resolve against. */
|
|
57
|
+
setProjection(projection) {
|
|
58
|
+
this.projection = projection;
|
|
59
|
+
}
|
|
60
|
+
/** The current projection, or null before the session has loaded. */
|
|
61
|
+
getProjection() {
|
|
62
|
+
return this.projection;
|
|
63
|
+
}
|
|
64
|
+
/** A link that restores the current selection. */
|
|
65
|
+
link() {
|
|
66
|
+
return toDeepLink(this.current);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Restore a selection from a link.
|
|
70
|
+
*
|
|
71
|
+
* @returns whether the fragment was a Watch link that named a session. A
|
|
72
|
+
* fragment that is not one leaves the selection alone rather than clearing
|
|
73
|
+
* it to a half-restored state.
|
|
74
|
+
*/
|
|
75
|
+
restore(fragment) {
|
|
76
|
+
const restored = fromDeepLink(fragment);
|
|
77
|
+
if (restored === null)
|
|
78
|
+
return false;
|
|
79
|
+
this.set(restored);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** Release every subscriber. Called when the plugin unloads. */
|
|
83
|
+
dispose() {
|
|
84
|
+
this.listeners.clear();
|
|
85
|
+
this.projection = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether two selections point at the same thing.
|
|
90
|
+
*
|
|
91
|
+
* `origin` is excluded on purpose: it records which surface moved, not what is
|
|
92
|
+
* selected, and including it would make every panel's echo look like a change.
|
|
93
|
+
*/
|
|
94
|
+
function isSame(left, right) {
|
|
95
|
+
return left.workspaceId === right.workspaceId
|
|
96
|
+
&& left.sessionId === right.sessionId
|
|
97
|
+
&& left.recordId === right.recordId
|
|
98
|
+
&& left.evidenceId === right.evidenceId
|
|
99
|
+
&& left.sourceId === right.sourceId
|
|
100
|
+
&& left.sourceRevisionId === right.sourceRevisionId
|
|
101
|
+
&& left.verificationId === right.verificationId
|
|
102
|
+
&& left.receiptId === right.receiptId
|
|
103
|
+
&& left.memoryId === right.memoryId
|
|
104
|
+
&& left.atMs === right.atMs
|
|
105
|
+
&& left.endMs === right.endMs
|
|
106
|
+
&& left.inspectorTab === right.inspectorTab;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=selection-store.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Unified Selection Model, and the deep links that serialize it.
|
|
3
|
+
*
|
|
4
|
+
* One canonical selection, not one per panel. That is the whole design: a
|
|
5
|
+
* citation in an answer, a row in Trajectory, a position in a player and the
|
|
6
|
+
* Evidence Inspector are four *projections* of the same value, so selecting in
|
|
7
|
+
* any of them moves all of them, and there is no state to reconcile between
|
|
8
|
+
* them because there is only one.
|
|
9
|
+
*
|
|
10
|
+
* DSH's own Trajectory selection is documented as local to Trajectory, with no
|
|
11
|
+
* anchor deep links. This is the smallest additive Watch-owned extension
|
|
12
|
+
* around it: Watch holds the canonical selection for Watch-related records and
|
|
13
|
+
* drives Trajectory's local selection from it, rather than replacing anything
|
|
14
|
+
* upstream owns.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is pure. A selection is a value, not a store.
|
|
17
|
+
*
|
|
18
|
+
* @module @deepwatch/dsh-trajectory/selection
|
|
19
|
+
*/
|
|
20
|
+
import type { WatchTrajectoryRecord } from './events.js';
|
|
21
|
+
/** Which inspector panel a selection opens. */
|
|
22
|
+
export type InspectorTab = 'evidence' | 'verification' | 'receipt' | 'memory' | 'source';
|
|
23
|
+
/**
|
|
24
|
+
* The one selection every Watch surface responds to.
|
|
25
|
+
*
|
|
26
|
+
* Identifiers only. A selection that carried evidence *content* would be a
|
|
27
|
+
* second copy of it, and the copy in a URL would be one nobody could
|
|
28
|
+
* invalidate.
|
|
29
|
+
*/
|
|
30
|
+
export interface WatchSelection {
|
|
31
|
+
readonly workspaceId: string;
|
|
32
|
+
readonly sessionId: string;
|
|
33
|
+
/** The Trajectory record, when the selection came from or points at one. */
|
|
34
|
+
readonly recordId: string | null;
|
|
35
|
+
readonly evidenceId: string | null;
|
|
36
|
+
readonly sourceId: string | null;
|
|
37
|
+
readonly sourceRevisionId: string | null;
|
|
38
|
+
readonly verificationId: string | null;
|
|
39
|
+
readonly receiptId: string | null;
|
|
40
|
+
readonly memoryId: string | null;
|
|
41
|
+
/** Where in the source, in milliseconds. */
|
|
42
|
+
readonly atMs: number | null;
|
|
43
|
+
readonly endMs: number | null;
|
|
44
|
+
readonly inspectorTab: InspectorTab | null;
|
|
45
|
+
/** Which surface initiated it, so a surface can skip echoing itself. */
|
|
46
|
+
readonly origin: string;
|
|
47
|
+
}
|
|
48
|
+
/** A selection with nothing selected, for a fresh session. */
|
|
49
|
+
export declare function emptySelection(workspaceId: string, sessionId: string): WatchSelection;
|
|
50
|
+
/**
|
|
51
|
+
* Which inspector panel a record should open.
|
|
52
|
+
*
|
|
53
|
+
* Derived rather than stored, so a record selected from Trajectory and the
|
|
54
|
+
* same record reached from a citation land on the same panel.
|
|
55
|
+
*/
|
|
56
|
+
export declare function tabForRecord(record: WatchTrajectoryRecord): InspectorTab;
|
|
57
|
+
/**
|
|
58
|
+
* Select one Trajectory record.
|
|
59
|
+
*
|
|
60
|
+
* This is the reverse direction of the round trip: a Watch row in Trajectory
|
|
61
|
+
* resolves to the same evidence, timestamp and receipt a citation would.
|
|
62
|
+
*/
|
|
63
|
+
export declare function selectRecord(base: WatchSelection, record: WatchTrajectoryRecord, origin?: string): WatchSelection;
|
|
64
|
+
/** One citation as an answer carries it. */
|
|
65
|
+
export interface CitationRef {
|
|
66
|
+
readonly evidenceId: string;
|
|
67
|
+
readonly sourceRevisionId: string | null;
|
|
68
|
+
readonly atMs: number | null;
|
|
69
|
+
readonly endMs?: number | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Select a citation from an agent answer.
|
|
73
|
+
*
|
|
74
|
+
* The forward direction: clicking a timestamp in an answer resolves the exact
|
|
75
|
+
* source revision and moment, and finds the Trajectory record that produced
|
|
76
|
+
* it. `records` is searched rather than trusted from the citation, because the
|
|
77
|
+
* record is the thing Trajectory can highlight and the citation only knows its
|
|
78
|
+
* own evidence id.
|
|
79
|
+
*/
|
|
80
|
+
export declare function selectCitation(base: WatchSelection, citation: CitationRef, records: readonly WatchTrajectoryRecord[], origin?: string): WatchSelection;
|
|
81
|
+
/** Whether two selections point at the same thing, ignoring which surface moved. */
|
|
82
|
+
export declare function sameSelection(left: WatchSelection, right: WatchSelection): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Serialize a selection as a deep link fragment.
|
|
85
|
+
*
|
|
86
|
+
* A fragment rather than a query string, and identifiers rather than content.
|
|
87
|
+
* A fragment is not sent to a server, which matters because these ids point at
|
|
88
|
+
* someone's private session; and a link that carried evidence *text* would be
|
|
89
|
+
* a copy nobody could invalidate when the underlying source changed.
|
|
90
|
+
*
|
|
91
|
+
* @returns the fragment including its leading `#`, so it can be appended
|
|
92
|
+
* directly to a workspace URL.
|
|
93
|
+
*/
|
|
94
|
+
export declare function toDeepLink(selection: WatchSelection): string;
|
|
95
|
+
/**
|
|
96
|
+
* Restore a selection from a deep link.
|
|
97
|
+
*
|
|
98
|
+
* Returns null when the fragment is not a Watch link or names no session — a
|
|
99
|
+
* link that cannot identify what it points at should leave the current
|
|
100
|
+
* selection alone rather than clearing it to a half-restored state.
|
|
101
|
+
*
|
|
102
|
+
* Unknown parameters are ignored rather than rejected, so a link produced by a
|
|
103
|
+
* newer build still opens the part this one understands.
|
|
104
|
+
*/
|
|
105
|
+
export declare function fromDeepLink(fragment: string): WatchSelection | null;
|
|
106
|
+
//# sourceMappingURL=selection.d.ts.map
|