@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
package/lib/selection.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
/** A selection with nothing selected, for a fresh session. */
|
|
21
|
+
export function emptySelection(workspaceId, sessionId) {
|
|
22
|
+
return {
|
|
23
|
+
workspaceId,
|
|
24
|
+
sessionId,
|
|
25
|
+
recordId: null,
|
|
26
|
+
evidenceId: null,
|
|
27
|
+
sourceId: null,
|
|
28
|
+
sourceRevisionId: null,
|
|
29
|
+
verificationId: null,
|
|
30
|
+
receiptId: null,
|
|
31
|
+
memoryId: null,
|
|
32
|
+
atMs: null,
|
|
33
|
+
endMs: null,
|
|
34
|
+
inspectorTab: null,
|
|
35
|
+
origin: 'none',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Which inspector panel a record should open.
|
|
40
|
+
*
|
|
41
|
+
* Derived rather than stored, so a record selected from Trajectory and the
|
|
42
|
+
* same record reached from a citation land on the same panel.
|
|
43
|
+
*/
|
|
44
|
+
export function tabForRecord(record) {
|
|
45
|
+
switch (record.type) {
|
|
46
|
+
case 'verification.requested':
|
|
47
|
+
case 'verification.completed':
|
|
48
|
+
return 'verification';
|
|
49
|
+
case 'browser.action.dispatched':
|
|
50
|
+
case 'browser.action.receipt':
|
|
51
|
+
return 'receipt';
|
|
52
|
+
case 'memory.context.injected':
|
|
53
|
+
case 'memory.record.corrected':
|
|
54
|
+
case 'memory.record.forgotten':
|
|
55
|
+
return 'memory';
|
|
56
|
+
case 'source.bound':
|
|
57
|
+
return 'source';
|
|
58
|
+
case 'evidence.created':
|
|
59
|
+
case 'observation.created':
|
|
60
|
+
return 'evidence';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Select one Trajectory record.
|
|
65
|
+
*
|
|
66
|
+
* This is the reverse direction of the round trip: a Watch row in Trajectory
|
|
67
|
+
* resolves to the same evidence, timestamp and receipt a citation would.
|
|
68
|
+
*/
|
|
69
|
+
export function selectRecord(base, record, origin = 'trajectory') {
|
|
70
|
+
return {
|
|
71
|
+
...base,
|
|
72
|
+
recordId: record.recordId,
|
|
73
|
+
evidenceId: record.refs.evidenceIds[0] ?? null,
|
|
74
|
+
sourceId: record.refs.sourceId,
|
|
75
|
+
sourceRevisionId: record.refs.sourceRevisionId,
|
|
76
|
+
verificationId: record.refs.verificationId,
|
|
77
|
+
receiptId: record.refs.receiptId,
|
|
78
|
+
memoryId: record.refs.memoryIds[0] ?? null,
|
|
79
|
+
atMs: record.refs.temporalRange?.startMs ?? null,
|
|
80
|
+
endMs: record.refs.temporalRange?.endMs ?? null,
|
|
81
|
+
inspectorTab: tabForRecord(record),
|
|
82
|
+
origin,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Select a citation from an agent answer.
|
|
87
|
+
*
|
|
88
|
+
* The forward direction: clicking a timestamp in an answer resolves the exact
|
|
89
|
+
* source revision and moment, and finds the Trajectory record that produced
|
|
90
|
+
* it. `records` is searched rather than trusted from the citation, because the
|
|
91
|
+
* record is the thing Trajectory can highlight and the citation only knows its
|
|
92
|
+
* own evidence id.
|
|
93
|
+
*/
|
|
94
|
+
export function selectCitation(base, citation, records, origin = 'conversation') {
|
|
95
|
+
const owning = records.find(record => record.refs.evidenceIds.includes(citation.evidenceId));
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
recordId: owning?.recordId ?? null,
|
|
99
|
+
evidenceId: citation.evidenceId,
|
|
100
|
+
// The citation's own revision wins when it has one: it is what was cited,
|
|
101
|
+
// and the record may aggregate several.
|
|
102
|
+
sourceId: citation.sourceRevisionId ?? owning?.refs.sourceId ?? null,
|
|
103
|
+
sourceRevisionId: citation.sourceRevisionId ?? owning?.refs.sourceRevisionId ?? null,
|
|
104
|
+
verificationId: owning?.refs.verificationId ?? null,
|
|
105
|
+
receiptId: owning?.refs.receiptId ?? null,
|
|
106
|
+
memoryId: null,
|
|
107
|
+
atMs: citation.atMs,
|
|
108
|
+
endMs: citation.endMs ?? citation.atMs,
|
|
109
|
+
inspectorTab: 'evidence',
|
|
110
|
+
origin,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Whether two selections point at the same thing, ignoring which surface moved. */
|
|
114
|
+
export function sameSelection(left, right) {
|
|
115
|
+
return left.workspaceId === right.workspaceId
|
|
116
|
+
&& left.sessionId === right.sessionId
|
|
117
|
+
&& left.recordId === right.recordId
|
|
118
|
+
&& left.evidenceId === right.evidenceId
|
|
119
|
+
&& left.sourceRevisionId === right.sourceRevisionId
|
|
120
|
+
&& left.verificationId === right.verificationId
|
|
121
|
+
&& left.receiptId === right.receiptId
|
|
122
|
+
&& left.memoryId === right.memoryId
|
|
123
|
+
&& left.atMs === right.atMs
|
|
124
|
+
&& left.inspectorTab === right.inspectorTab;
|
|
125
|
+
}
|
|
126
|
+
// ── deep links ──────────────────────────────────────────────────────────────
|
|
127
|
+
/** The parameter names a deep link uses. Stable; changing one breaks old links. */
|
|
128
|
+
const PARAM = {
|
|
129
|
+
workspaceId: 'w',
|
|
130
|
+
sessionId: 's',
|
|
131
|
+
recordId: 'r',
|
|
132
|
+
evidenceId: 'e',
|
|
133
|
+
sourceId: 'src',
|
|
134
|
+
sourceRevisionId: 'rev',
|
|
135
|
+
verificationId: 'v',
|
|
136
|
+
receiptId: 'rc',
|
|
137
|
+
memoryId: 'm',
|
|
138
|
+
atMs: 't',
|
|
139
|
+
endMs: 'te',
|
|
140
|
+
inspectorTab: 'tab',
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Serialize a selection as a deep link fragment.
|
|
144
|
+
*
|
|
145
|
+
* A fragment rather than a query string, and identifiers rather than content.
|
|
146
|
+
* A fragment is not sent to a server, which matters because these ids point at
|
|
147
|
+
* someone's private session; and a link that carried evidence *text* would be
|
|
148
|
+
* a copy nobody could invalidate when the underlying source changed.
|
|
149
|
+
*
|
|
150
|
+
* @returns the fragment including its leading `#`, so it can be appended
|
|
151
|
+
* directly to a workspace URL.
|
|
152
|
+
*/
|
|
153
|
+
export function toDeepLink(selection) {
|
|
154
|
+
const params = new URLSearchParams();
|
|
155
|
+
const put = (key, value) => {
|
|
156
|
+
if (value === null || value === '')
|
|
157
|
+
return;
|
|
158
|
+
params.set(key, String(value));
|
|
159
|
+
};
|
|
160
|
+
put(PARAM.workspaceId, selection.workspaceId);
|
|
161
|
+
put(PARAM.sessionId, selection.sessionId);
|
|
162
|
+
put(PARAM.recordId, selection.recordId);
|
|
163
|
+
put(PARAM.evidenceId, selection.evidenceId);
|
|
164
|
+
put(PARAM.sourceId, selection.sourceId);
|
|
165
|
+
put(PARAM.sourceRevisionId, selection.sourceRevisionId);
|
|
166
|
+
put(PARAM.verificationId, selection.verificationId);
|
|
167
|
+
put(PARAM.receiptId, selection.receiptId);
|
|
168
|
+
put(PARAM.memoryId, selection.memoryId);
|
|
169
|
+
put(PARAM.atMs, selection.atMs);
|
|
170
|
+
put(PARAM.endMs, selection.endMs);
|
|
171
|
+
put(PARAM.inspectorTab, selection.inspectorTab);
|
|
172
|
+
return `#watch=${encodeURIComponent(params.toString())}`;
|
|
173
|
+
}
|
|
174
|
+
/** Inspector tabs that may appear in a link, so a bad one cannot be injected. */
|
|
175
|
+
const TABS = new Set(['evidence', 'verification', 'receipt', 'memory', 'source']);
|
|
176
|
+
/**
|
|
177
|
+
* Restore a selection from a deep link.
|
|
178
|
+
*
|
|
179
|
+
* Returns null when the fragment is not a Watch link or names no session — a
|
|
180
|
+
* link that cannot identify what it points at should leave the current
|
|
181
|
+
* selection alone rather than clearing it to a half-restored state.
|
|
182
|
+
*
|
|
183
|
+
* Unknown parameters are ignored rather than rejected, so a link produced by a
|
|
184
|
+
* newer build still opens the part this one understands.
|
|
185
|
+
*/
|
|
186
|
+
export function fromDeepLink(fragment) {
|
|
187
|
+
const raw = fragment.startsWith('#') ? fragment.slice(1) : fragment;
|
|
188
|
+
const marker = 'watch=';
|
|
189
|
+
const at = raw.startsWith(marker) ? 0 : raw.indexOf(`&${marker}`);
|
|
190
|
+
if (at < 0)
|
|
191
|
+
return null;
|
|
192
|
+
const encoded = raw.slice(at === 0 ? marker.length : at + marker.length + 1);
|
|
193
|
+
const params = new URLSearchParams(decodeURIComponent(encoded));
|
|
194
|
+
const sessionId = params.get(PARAM.sessionId);
|
|
195
|
+
if (sessionId === null || sessionId === '')
|
|
196
|
+
return null;
|
|
197
|
+
const number = (key) => {
|
|
198
|
+
const value = params.get(key);
|
|
199
|
+
if (value === null)
|
|
200
|
+
return null;
|
|
201
|
+
const parsed = Number(value);
|
|
202
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
203
|
+
};
|
|
204
|
+
const tab = params.get(PARAM.inspectorTab);
|
|
205
|
+
return {
|
|
206
|
+
workspaceId: params.get(PARAM.workspaceId) ?? '',
|
|
207
|
+
sessionId,
|
|
208
|
+
recordId: params.get(PARAM.recordId),
|
|
209
|
+
evidenceId: params.get(PARAM.evidenceId),
|
|
210
|
+
sourceId: params.get(PARAM.sourceId),
|
|
211
|
+
sourceRevisionId: params.get(PARAM.sourceRevisionId),
|
|
212
|
+
verificationId: params.get(PARAM.verificationId),
|
|
213
|
+
receiptId: params.get(PARAM.receiptId),
|
|
214
|
+
memoryId: params.get(PARAM.memoryId),
|
|
215
|
+
atMs: number(PARAM.atMs),
|
|
216
|
+
endMs: number(PARAM.endMs),
|
|
217
|
+
inspectorTab: tab !== null && TABS.has(tab) ? tab : null,
|
|
218
|
+
// A restored selection did not come from a panel, and saying so stops the
|
|
219
|
+
// surfaces from treating it as an echo of their own change.
|
|
220
|
+
origin: 'deep-link',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=selection.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepwatch/dsh-trajectory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Watch records inside the DeepSeek Harness Trajectory, with unified selection, deep links and replay",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Sayed Allam",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/oxbshw/watch-skill.git",
|
|
11
|
+
"directory": "workspace/packages/watch/trajectory"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/oxbshw/watch-skill/tree/main/workspace#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/oxbshw/watch-skill/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"deepwatch",
|
|
19
|
+
"deepseek-harness",
|
|
20
|
+
"watch-skill",
|
|
21
|
+
"trajectory",
|
|
22
|
+
"replay"
|
|
23
|
+
],
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"types": "lib/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./lib/index.d.ts",
|
|
29
|
+
"default": "./lib/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/**/*.js",
|
|
35
|
+
"lib/**/*.d.ts"
|
|
36
|
+
],
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@deepwatch/dsh-contracts": "^0.1.0"
|
|
46
|
+
}
|
|
47
|
+
}
|