@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/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).
|
package/lib/compare.d.ts
ADDED
|
@@ -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
|