@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oxbshw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @deepwatch/dsh-trajectory
2
+
3
+ Watch records inside the DeepSeek Harness Trajectory, with unified selection, deep links and replay
4
+
5
+ Part of **DeepWatch** — the agent workspace built on the official
6
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
7
+ and powered by [Watch Skill](https://github.com/oxbshw/watch-skill) for perception, evidence, memory and
8
+ independent verification.
9
+
10
+ > **Host plugin — runs beside the agent in the DSH process.**
11
+ > Composed by the bundle.
12
+
13
+ ## Exports
14
+
15
+ - `@deepwatch/dsh-trajectory`
16
+
17
+ ## Install
18
+
19
+ > **Not on npm yet.** Nothing exists under the `@deepwatch` scope. This
20
+ > package is published for the first time by the `deepwatch-v0.1.0`
21
+ > release; until then the command below resolves nothing, and
22
+ > [the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme) has the path
23
+ > that works from a checkout.
24
+
25
+ ```sh
26
+ npm install @deepwatch/dsh-trajectory
27
+ ```
28
+
29
+ Rarely on its own. [`@deepwatch/dsh-bundle`](https://github.com/oxbshw/watch-skill/tree/main/workspace/packages/watch/bundle#readme)
30
+ composes this package with the rest of DeepWatch and is what a profile
31
+ normally depends on; installing this one directly is for embedding a
32
+ single piece in a composition you control.
33
+
34
+ ## Requirements
35
+
36
+ - Node `^22.19.0 || >=24.0.0`
37
+
38
+ ## Stability
39
+
40
+ `0.1.0` — a stable release.
41
+
42
+ Stable means tested, documented and supported — not 1.0. This is a
43
+ pre-1.0 line, and semantic versioning gives `0.x` no compatibility
44
+ guarantee across minor versions: **a `0.MINOR` bump may change or remove
45
+ surface, and a patch will not.** Depend on it with a tilde range
46
+ (`~0.1.0`) if you want that difference enforced by your lockfile
47
+ rather than by a changelog. The usual major-version promise starts at 1.0.
48
+
49
+ ## Side effects
50
+
51
+ Importing a module from this package evaluates no side effects, so a
52
+ bundler may drop what a build does not use. Mounting it in a host is a
53
+ separate matter: what it then reads or writes is governed by the
54
+ workspace boundary and the host's permissions, not by this flag.
55
+
56
+ ## Where this fits
57
+
58
+ Puts Watch records inside the Harness Trajectory, so a receipt, a verdict and the turn that produced them share one selection, one deep link and one replay.
59
+
60
+ The twenty packages and how they compose:
61
+ [the package map](https://github.com/oxbshw/watch-skill/blob/main/workspace/docs/packages.md).
62
+ Running DeepWatch, and the gates a change has to pass:
63
+ [the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme).
64
+
65
+ ## Attribution
66
+
67
+ Built on DeepSeek Harness · Powered by Watch Skill
68
+
69
+ DeepWatch and Watch Skill are independent projects and are not affiliated
70
+ with or endorsed by DeepSeek. MIT licensed; third-party notices are in
71
+ [THIRD_PARTY_NOTICES.md](https://github.com/oxbshw/watch-skill/blob/main/workspace/THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Comparing two observations of the same thing.
3
+ *
4
+ * The question Compare answers is narrower than "what changed", and much more
5
+ * useful: **where did these first stop agreeing?** A diff of two long runs
6
+ * produces hundreds of differences, most of them consequences of the first
7
+ * one, and a person reading it has to work backwards to find the moment that
8
+ * mattered. So the first divergence is a first-class result rather than
9
+ * something to scroll for.
10
+ *
11
+ * Two rules carry over from the rest of the product.
12
+ *
13
+ * **A difference is not a failure.** Most changes between two runs are the
14
+ * change somebody asked for. Compare surfaces divergences; deciding whether
15
+ * one was expected is a verification contract, and Compare never issues a
16
+ * verdict of its own.
17
+ *
18
+ * **Comparison is over identifiers, not content.** Two evidence records are
19
+ * compared by revision, range and digest. Compare holds no evidence payloads,
20
+ * so it cannot become a second place where evidence lives.
21
+ *
22
+ * @module @deepwatch/dsh-trajectory/compare
23
+ */
24
+ import type { WatchProjection } from './projection.js';
25
+ /** What is being compared. */
26
+ export type CompareSubject =
27
+ /** Two runs of the same task. */
28
+ 'run'
29
+ /** Two revisions of the same source. */
30
+ | 'source_revision'
31
+ /** Two time ranges, in the same source or in two. */
32
+ | 'temporal_region'
33
+ /**
34
+ * The same thing before and after a change was made.
35
+ *
36
+ * Named separately from `temporal_region` because it carries an
37
+ * expectation: somebody did something in between, and the question is
38
+ * whether the difference is the one they intended. Compare still issues no
39
+ * verdict — that remains a verification contract — but the surface reads
40
+ * differently when it knows a change was deliberate.
41
+ */
42
+ | 'before_after';
43
+ /** Every subject, for enumerating the picker. */
44
+ export declare const COMPARE_SUBJECTS: readonly CompareSubject[];
45
+ /** Which signal a divergence was found in. */
46
+ export type DivergenceChannel = 'visual' | 'text' | 'ocr' | 'transcript' | 'dom' | 'network' | 'console' | 'verification' | 'receipt';
47
+ /** How the two sides differ. */
48
+ export type DivergenceKind =
49
+ /** Present on the left, absent on the right. */
50
+ 'removed'
51
+ /** Absent on the left, present on the right. */
52
+ | 'added'
53
+ /** Present on both, different. */
54
+ | 'changed'
55
+ /** Present on both, and the difference is only in timing. */
56
+ | 'retimed';
57
+ /** One place two sides stopped agreeing. */
58
+ export interface Divergence {
59
+ readonly channel: DivergenceChannel;
60
+ readonly kind: DivergenceKind;
61
+ /** Where in the source, so the player can go there. */
62
+ readonly atMs: number | null;
63
+ /** The record on each side, by id. Never the content. */
64
+ readonly leftRecordId: string | null;
65
+ readonly rightRecordId: string | null;
66
+ readonly leftEvidenceId: string | null;
67
+ readonly rightEvidenceId: string | null;
68
+ /** One line for a list. Presentation only; nothing resolves from it. */
69
+ readonly summary: string;
70
+ }
71
+ /** The result of comparing two sides. */
72
+ export interface Comparison {
73
+ readonly subject: CompareSubject;
74
+ readonly leftId: string;
75
+ readonly rightId: string;
76
+ readonly divergences: readonly Divergence[];
77
+ /**
78
+ * The earliest divergence, or null when the two agree.
79
+ *
80
+ * The answer people actually want. Everything after the first divergence is
81
+ * usually a consequence of it, and presenting a hundred differences equally
82
+ * makes the one that mattered harder to find, not easier.
83
+ */
84
+ readonly firstDivergence: Divergence | null;
85
+ /** Records present on both sides and identical. */
86
+ readonly agreements: number;
87
+ }
88
+ /**
89
+ * The minimum of an evidence record Compare needs to place it on a channel.
90
+ *
91
+ * A structural subset rather than an import of `EvidenceRecord`: the
92
+ * trajectory package holds no evidence payloads, and taking the full type here
93
+ * would invite one to be stored.
94
+ */
95
+ export interface ChannelHint {
96
+ readonly modality: 'visual' | 'text' | 'audio' | 'dom' | 'network' | 'filesystem';
97
+ }
98
+ /** Evidence ids to what sense produced them, when the caller has resolved any. */
99
+ export type ChannelHints = ReadonlyMap<string, ChannelHint>;
100
+ /**
101
+ * Compare two projections.
102
+ *
103
+ * Pure, and deterministic for the same inputs — which is what lets a
104
+ * comparison be deep-linked and replayed like anything else in the product.
105
+ *
106
+ * @param subject - what the two sides are: two runs, two revisions, two regions.
107
+ */
108
+ export declare function compareProjections(left: WatchProjection, right: WatchProjection, subject: CompareSubject, ids: {
109
+ readonly leftId: string;
110
+ readonly rightId: string;
111
+ }, hints?: ChannelHints): Comparison;
112
+ /**
113
+ * Whether a comparison found a change in what was *established*.
114
+ *
115
+ * Separate from "found any difference" on purpose. Two runs producing
116
+ * different evidence at a different moment is normal; two runs reaching
117
+ * different verdicts is the thing somebody needs to look at.
118
+ */
119
+ export declare function hasVerdictDivergence(comparison: Comparison): boolean;
120
+ /**
121
+ * A stable digest of a comparison.
122
+ *
123
+ * Same inputs, same digest — so a comparison can be deep-linked, replayed and
124
+ * checked for change the same way a projection can. Over identifiers and kinds
125
+ * only, never over the summary text.
126
+ */
127
+ export declare function comparisonDigest(comparison: Comparison): string;
128
+ /**
129
+ * The first divergence that changed what was *established*.
130
+ *
131
+ * Distinct from `firstDivergence`, and the distinction is the useful one. The
132
+ * earliest difference between two runs is frequently a timestamp or an extra
133
+ * frame; the earliest difference in a verdict or a receipt is the moment the
134
+ * two runs stopped being the same outcome. A surface that offered only the
135
+ * first would send people to the wrong second.
136
+ */
137
+ export declare function firstMeaningfulDivergence(comparison: Comparison): Divergence | null;
138
+ /**
139
+ * A deep link to one side of a divergence.
140
+ *
141
+ * Built from the same selection model everything else in the product uses, so
142
+ * a link out of Compare opens the same inspector a link out of Trajectory
143
+ * would. Returns null when the side has nothing to point at, rather than a
144
+ * link that resolves to an empty panel.
145
+ */
146
+ export declare function divergenceLink(comparison: Comparison, divergence: Divergence, side: 'left' | 'right', context: {
147
+ readonly workspaceId: string;
148
+ readonly sessionId: string;
149
+ }): string | null;
150
+ /**
151
+ * A portable comparison.
152
+ *
153
+ * Identifiers, kinds and links — never evidence content, for the same reason
154
+ * `Divergence` carries none. An exported bundle that inlined what it compared
155
+ * would be a second copy of the evidence, and a second copy is one nobody can
156
+ * invalidate.
157
+ */
158
+ export interface ComparisonBundle {
159
+ readonly digest: string;
160
+ readonly subject: CompareSubject;
161
+ readonly leftId: string;
162
+ readonly rightId: string;
163
+ readonly agreements: number;
164
+ readonly divergences: readonly Divergence[];
165
+ readonly firstDivergence: Divergence | null;
166
+ readonly firstMeaningfulDivergence: Divergence | null;
167
+ readonly links: readonly {
168
+ readonly side: 'left' | 'right';
169
+ readonly link: string;
170
+ }[];
171
+ }
172
+ /** Freeze a comparison into something that can be attached to a report. */
173
+ export declare function exportComparison(comparison: Comparison, context: {
174
+ readonly workspaceId: string;
175
+ readonly sessionId: string;
176
+ }): ComparisonBundle;
177
+ /**
178
+ * One line summarizing a comparison.
179
+ *
180
+ * Deliberately never says "passed" or "failed". Compare reports where two
181
+ * things stopped agreeing; whether that was the change somebody asked for is a
182
+ * verification contract, and a summary that editorialized would be Compare
183
+ * issuing the verdict it is not allowed to issue.
184
+ */
185
+ export declare function describeComparison(comparison: Comparison): string;
186
+ //# sourceMappingURL=compare.d.ts.map
package/lib/compare.js ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Comparing two observations of the same thing.
3
+ *
4
+ * The question Compare answers is narrower than "what changed", and much more
5
+ * useful: **where did these first stop agreeing?** A diff of two long runs
6
+ * produces hundreds of differences, most of them consequences of the first
7
+ * one, and a person reading it has to work backwards to find the moment that
8
+ * mattered. So the first divergence is a first-class result rather than
9
+ * something to scroll for.
10
+ *
11
+ * Two rules carry over from the rest of the product.
12
+ *
13
+ * **A difference is not a failure.** Most changes between two runs are the
14
+ * change somebody asked for. Compare surfaces divergences; deciding whether
15
+ * one was expected is a verification contract, and Compare never issues a
16
+ * verdict of its own.
17
+ *
18
+ * **Comparison is over identifiers, not content.** Two evidence records are
19
+ * compared by revision, range and digest. Compare holds no evidence payloads,
20
+ * so it cannot become a second place where evidence lives.
21
+ *
22
+ * @module @deepwatch/dsh-trajectory/compare
23
+ */
24
+ import { toDeepLink } from './selection.js';
25
+ /** Every subject, for enumerating the picker. */
26
+ export const COMPARE_SUBJECTS = [
27
+ 'run', 'source_revision', 'temporal_region', 'before_after',
28
+ ];
29
+ /** The channel one evidence modality belongs to. */
30
+ function channelForModality(modality) {
31
+ switch (modality) {
32
+ case 'visual':
33
+ return 'visual';
34
+ case 'audio':
35
+ return 'transcript';
36
+ case 'text':
37
+ return 'ocr';
38
+ case 'dom':
39
+ return 'dom';
40
+ case 'network':
41
+ return 'network';
42
+ case 'filesystem':
43
+ return 'text';
44
+ }
45
+ }
46
+ /**
47
+ * Which channel a record's evidence belongs to.
48
+ *
49
+ * Verdicts and receipts are decided by the record type, because that is what
50
+ * they are. Everything else needs the evidence, and when the caller has not
51
+ * resolved it the answer is `text` rather than a guess — filing an unresolved
52
+ * record under `visual` because most evidence is visual is how a transcript
53
+ * divergence ends up reported as a picture changing.
54
+ */
55
+ function channelFor(record, hints) {
56
+ switch (record.type) {
57
+ case 'verification.completed':
58
+ case 'verification.requested':
59
+ return 'verification';
60
+ case 'browser.action.receipt':
61
+ case 'browser.action.dispatched':
62
+ return 'receipt';
63
+ default: {
64
+ for (const evidenceId of record.refs.evidenceIds) {
65
+ const hint = hints.get(evidenceId);
66
+ if (hint !== undefined)
67
+ return channelForModality(hint.modality);
68
+ }
69
+ return 'text';
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * A key identifying "the same thing" across two sides.
75
+ *
76
+ * Deliberately not the record id: two runs produce different ids for the same
77
+ * step, and comparing by id would report every record as both added and
78
+ * removed. What makes two records comparable is what they are *about* — the
79
+ * channel and the moment.
80
+ */
81
+ function alignmentKey(record, hints) {
82
+ const at = record.refs.temporalRange?.startMs ?? null;
83
+ return `${channelFor(record, hints)}:${at === null ? 'untimed' : String(at)}`;
84
+ }
85
+ /** Whether two aligned records actually agree. */
86
+ function agrees(left, right) {
87
+ // Compared by identifier, never by content: the evidence itself lives at
88
+ // Watch Core, and a comparison that read payloads would need a copy of them.
89
+ return left.refs.sourceRevisionId === right.refs.sourceRevisionId
90
+ && left.refs.evidenceIds.join(',') === right.refs.evidenceIds.join(',')
91
+ && left.refs.verdict === right.refs.verdict
92
+ && left.refs.receiptId === right.refs.receiptId;
93
+ }
94
+ /** Describe how two aligned records differ. */
95
+ function describeChange(left, right) {
96
+ if (left.refs.verdict !== right.refs.verdict) {
97
+ // The most important difference there is: the same step reached a
98
+ // different conclusion. Named explicitly rather than folded into "changed".
99
+ return {
100
+ kind: 'changed',
101
+ summary: `verdict ${left.refs.verdict ?? 'none'} → ${right.refs.verdict ?? 'none'}`,
102
+ };
103
+ }
104
+ if (left.refs.sourceRevisionId !== right.refs.sourceRevisionId) {
105
+ return { kind: 'changed', summary: 'a different source revision' };
106
+ }
107
+ if (left.refs.evidenceIds.join(',') !== right.refs.evidenceIds.join(',')) {
108
+ return {
109
+ kind: 'changed',
110
+ summary: `${String(left.refs.evidenceIds.length)} → ${String(right.refs.evidenceIds.length)} evidence record(s)`,
111
+ };
112
+ }
113
+ return { kind: 'changed', summary: 'differs' };
114
+ }
115
+ /**
116
+ * Compare two projections.
117
+ *
118
+ * Pure, and deterministic for the same inputs — which is what lets a
119
+ * comparison be deep-linked and replayed like anything else in the product.
120
+ *
121
+ * @param subject - what the two sides are: two runs, two revisions, two regions.
122
+ */
123
+ export function compareProjections(left, right, subject, ids, hints = new Map()) {
124
+ const leftByKey = new Map();
125
+ const rightByKey = new Map();
126
+ for (const record of left.records)
127
+ leftByKey.set(alignmentKey(record, hints), record);
128
+ for (const record of right.records)
129
+ rightByKey.set(alignmentKey(record, hints), record);
130
+ const divergences = [];
131
+ let agreements = 0;
132
+ const keys = [...new Set([...leftByKey.keys(), ...rightByKey.keys()])].sort();
133
+ for (const key of keys) {
134
+ const leftRecord = leftByKey.get(key);
135
+ const rightRecord = rightByKey.get(key);
136
+ if (leftRecord !== undefined && rightRecord !== undefined) {
137
+ if (agrees(leftRecord, rightRecord)) {
138
+ agreements += 1;
139
+ continue;
140
+ }
141
+ const change = describeChange(leftRecord, rightRecord);
142
+ divergences.push({
143
+ channel: channelFor(leftRecord, hints),
144
+ kind: change.kind,
145
+ atMs: leftRecord.refs.temporalRange?.startMs ?? null,
146
+ leftRecordId: leftRecord.recordId,
147
+ rightRecordId: rightRecord.recordId,
148
+ leftEvidenceId: leftRecord.refs.evidenceIds[0] ?? null,
149
+ rightEvidenceId: rightRecord.refs.evidenceIds[0] ?? null,
150
+ summary: change.summary,
151
+ });
152
+ continue;
153
+ }
154
+ const present = leftRecord ?? rightRecord;
155
+ if (present === undefined)
156
+ continue;
157
+ const removed = rightRecord === undefined;
158
+ divergences.push({
159
+ channel: channelFor(present, hints),
160
+ kind: removed ? 'removed' : 'added',
161
+ atMs: present.refs.temporalRange?.startMs ?? null,
162
+ leftRecordId: removed ? present.recordId : null,
163
+ rightRecordId: removed ? null : present.recordId,
164
+ leftEvidenceId: removed ? present.refs.evidenceIds[0] ?? null : null,
165
+ rightEvidenceId: removed ? null : present.refs.evidenceIds[0] ?? null,
166
+ summary: removed ? 'only in the first' : 'only in the second',
167
+ });
168
+ }
169
+ // Ordered by when they happened, so "first" means first in the source rather
170
+ // than first in an arbitrary map iteration. Untimed divergences sort last:
171
+ // they cannot be placed on a timeline, and putting them first would hide a
172
+ // real early divergence behind them.
173
+ const ordered = [...divergences].sort((a, b) => {
174
+ if (a.atMs === b.atMs)
175
+ return a.channel.localeCompare(b.channel);
176
+ if (a.atMs === null)
177
+ return 1;
178
+ if (b.atMs === null)
179
+ return -1;
180
+ return a.atMs - b.atMs;
181
+ });
182
+ return {
183
+ subject,
184
+ leftId: ids.leftId,
185
+ rightId: ids.rightId,
186
+ divergences: ordered,
187
+ firstDivergence: ordered[0] ?? null,
188
+ agreements,
189
+ };
190
+ }
191
+ /**
192
+ * Whether a comparison found a change in what was *established*.
193
+ *
194
+ * Separate from "found any difference" on purpose. Two runs producing
195
+ * different evidence at a different moment is normal; two runs reaching
196
+ * different verdicts is the thing somebody needs to look at.
197
+ */
198
+ export function hasVerdictDivergence(comparison) {
199
+ return comparison.divergences.some(divergence => divergence.channel === 'verification');
200
+ }
201
+ /**
202
+ * A stable digest of a comparison.
203
+ *
204
+ * Same inputs, same digest — so a comparison can be deep-linked, replayed and
205
+ * checked for change the same way a projection can. Over identifiers and kinds
206
+ * only, never over the summary text.
207
+ */
208
+ export function comparisonDigest(comparison) {
209
+ const canonical = [
210
+ comparison.subject,
211
+ comparison.leftId,
212
+ comparison.rightId,
213
+ String(comparison.agreements),
214
+ ...comparison.divergences.map(divergence => [
215
+ divergence.channel,
216
+ divergence.kind,
217
+ divergence.atMs === null ? '' : String(divergence.atMs),
218
+ divergence.leftRecordId ?? '',
219
+ divergence.rightRecordId ?? '',
220
+ ].join('|')),
221
+ ].join('\n');
222
+ let hash = 0xcbf29ce484222325n;
223
+ const prime = 0x100000001b3n;
224
+ const mask = 0xffffffffffffffffn;
225
+ for (const byte of new TextEncoder().encode(canonical)) {
226
+ hash = ((hash ^ BigInt(byte)) * prime) & mask;
227
+ }
228
+ return `fnv1a64:${hash.toString(16).padStart(16, '0')}`;
229
+ }
230
+ /**
231
+ * The first divergence that changed what was *established*.
232
+ *
233
+ * Distinct from `firstDivergence`, and the distinction is the useful one. The
234
+ * earliest difference between two runs is frequently a timestamp or an extra
235
+ * frame; the earliest difference in a verdict or a receipt is the moment the
236
+ * two runs stopped being the same outcome. A surface that offered only the
237
+ * first would send people to the wrong second.
238
+ */
239
+ export function firstMeaningfulDivergence(comparison) {
240
+ return comparison.divergences.find(divergence => divergence.channel === 'verification' || divergence.channel === 'receipt') ?? comparison.firstDivergence;
241
+ }
242
+ /**
243
+ * A deep link to one side of a divergence.
244
+ *
245
+ * Built from the same selection model everything else in the product uses, so
246
+ * a link out of Compare opens the same inspector a link out of Trajectory
247
+ * would. Returns null when the side has nothing to point at, rather than a
248
+ * link that resolves to an empty panel.
249
+ */
250
+ export function divergenceLink(comparison, divergence, side, context) {
251
+ const recordId = side === 'left' ? divergence.leftRecordId : divergence.rightRecordId;
252
+ const evidenceId = side === 'left' ? divergence.leftEvidenceId : divergence.rightEvidenceId;
253
+ if (recordId === null && evidenceId === null)
254
+ return null;
255
+ return toDeepLink({
256
+ workspaceId: context.workspaceId,
257
+ sessionId: side === 'left' ? comparison.leftId : comparison.rightId,
258
+ recordId,
259
+ evidenceId,
260
+ sourceId: null,
261
+ sourceRevisionId: null,
262
+ verificationId: null,
263
+ receiptId: null,
264
+ memoryId: null,
265
+ atMs: divergence.atMs,
266
+ endMs: null,
267
+ inspectorTab: divergence.channel === 'verification' ? 'verification' : 'evidence',
268
+ origin: 'compare',
269
+ });
270
+ }
271
+ /** Freeze a comparison into something that can be attached to a report. */
272
+ export function exportComparison(comparison, context) {
273
+ const meaningful = firstMeaningfulDivergence(comparison);
274
+ const links = [];
275
+ if (meaningful !== null) {
276
+ for (const side of ['left', 'right']) {
277
+ const link = divergenceLink(comparison, meaningful, side, context);
278
+ if (link !== null)
279
+ links.push({ side, link });
280
+ }
281
+ }
282
+ return {
283
+ digest: comparisonDigest(comparison),
284
+ subject: comparison.subject,
285
+ leftId: comparison.leftId,
286
+ rightId: comparison.rightId,
287
+ agreements: comparison.agreements,
288
+ divergences: comparison.divergences,
289
+ firstDivergence: comparison.firstDivergence,
290
+ firstMeaningfulDivergence: meaningful,
291
+ links,
292
+ };
293
+ }
294
+ /**
295
+ * One line summarizing a comparison.
296
+ *
297
+ * Deliberately never says "passed" or "failed". Compare reports where two
298
+ * things stopped agreeing; whether that was the change somebody asked for is a
299
+ * verification contract, and a summary that editorialized would be Compare
300
+ * issuing the verdict it is not allowed to issue.
301
+ */
302
+ export function describeComparison(comparison) {
303
+ if (comparison.divergences.length === 0) {
304
+ return `No divergence across ${String(comparison.agreements)} aligned record(s).`;
305
+ }
306
+ const meaningful = firstMeaningfulDivergence(comparison);
307
+ const where = meaningful?.atMs === null || meaningful === null
308
+ ? 'at an untimed record'
309
+ : `at ${String(meaningful.atMs)}ms`;
310
+ return `${String(comparison.divergences.length)} divergence(s), `
311
+ + `${String(comparison.agreements)} agreement(s). `
312
+ + `First meaningful: ${meaningful?.channel ?? 'none'} ${where}.`;
313
+ }
314
+ //# sourceMappingURL=compare.js.map
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Watch records registered into DeepSeek Harness's own event system.
3
+ *
4
+ * This is the seam the plan insists on. Watch does **not** build a second
5
+ * tracker: it registers a `ConversationNodeDefinition` into
6
+ * `ctx.conversationEvents` — the same registry Trajectory itself uses — and a
7
+ * view target into `ctx.conversationViews`. Both read the single session event
8
+ * log DSH already owns.
9
+ *
10
+ * What that buys is not tidiness. It means a Watch record and the Tool record
11
+ * it came from cannot disagree about when something happened, because they are
12
+ * two projections of the same event at the same sequence number. A separate
13
+ * Watch ledger would eventually drift, and the drift would be invisible.
14
+ *
15
+ * The Trajectory *contribution* union is closed in upstream's own package, so
16
+ * Watch rows cannot be injected into the existing Trajectory ledger without an
17
+ * upstream patch. Rather than take one, Watch registers its own view target
18
+ * over the same events — the arrangement upstream already uses for `chat` and
19
+ * `trajectory`, and additive by construction.
20
+ *
21
+ * @module @deepwatch/dsh-trajectory/definition
22
+ */
23
+ import type { WatchProjection } from './projection.js';
24
+ import type { WatchTrajectoryRecord } from './events.js';
25
+ /**
26
+ * The two DSH registries this module needs.
27
+ *
28
+ * Structural rather than imported from Cordis: these are two method
29
+ * signatures, and taking a framework dependency for them would tie a pure
30
+ * module to a runtime it never uses. The browser half passes its real context.
31
+ */
32
+ export interface WatchRegistries {
33
+ readonly conversationEvents: {
34
+ register(definition: unknown): void;
35
+ };
36
+ readonly conversationViews: {
37
+ register(definition: unknown): void;
38
+ };
39
+ }
40
+ /** The view target Watch publishes. Distinct from `trajectory` and `chat`. */
41
+ export declare const WATCH_TARGET = "watchEvidence";
42
+ /**
43
+ * The Watch event Definition.
44
+ *
45
+ * Typed structurally rather than against upstream's exported generics: this
46
+ * package must build outside the DSH monorepo, where those types are reachable
47
+ * only through the packages the browser bundle is allowed to import. The shape
48
+ * is checked against the real contract by the round-trip tests, which run the
49
+ * same extraction over real event shapes.
50
+ */
51
+ export declare function watchTrajectoryDefinition(sessionId: string): unknown;
52
+ /** The node shape the view builder folds. */
53
+ interface WatchViewNode {
54
+ readonly key: string;
55
+ readonly anchorSeq: number;
56
+ readonly data: {
57
+ readonly records: readonly WatchTrajectoryRecord[];
58
+ };
59
+ }
60
+ /**
61
+ * The Watch view target's incremental builder.
62
+ *
63
+ * Rebuilds the whole projection on change rather than patching it. The record
64
+ * set for one session is small — tens of rows, not thousands — and a full fold
65
+ * is the same code path replay uses, so the live view and a reopened one
66
+ * cannot diverge. Patching would be faster and would introduce exactly the
67
+ * class of bug this whole design exists to rule out.
68
+ */
69
+ declare class WatchViewBuilder {
70
+ private readonly sessionId;
71
+ readonly empty: WatchProjection;
72
+ private nodes;
73
+ constructor(sessionId: string);
74
+ replace(input: {
75
+ nodes: readonly WatchViewNode[];
76
+ }): WatchProjection;
77
+ patch(input: {
78
+ nodes: readonly WatchViewNode[];
79
+ }): WatchProjection;
80
+ private build;
81
+ }
82
+ /**
83
+ * Register the Watch definition and view target.
84
+ *
85
+ * Both registrations ride Cordis effects, so unloading the plugin removes them
86
+ * and the workspace returns to stock DSH with no Watch rows and no leftover
87
+ * target — which is the uninstall path the bundle promises.
88
+ */
89
+ export declare function registerWatchTrajectory(ctx: WatchRegistries, sessionId: string): void;
90
+ export { WatchViewBuilder };
91
+ //# sourceMappingURL=definition.d.ts.map