@deepwatch/dsh-live 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.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Triggers: a rule that notices something, and cannot do anything about it.
3
+ *
4
+ * A live observation runs for a long time and the interesting second is
5
+ * usually not the one anybody is watching. So a trigger says "tell me when the
6
+ * deploy log says failed", or "pin the moment the stream drops", and the
7
+ * session keeps an eye out.
8
+ *
9
+ * The whole design question is what a trigger is allowed to *do*, and the
10
+ * answer here is deliberately narrow: it may pin a moment, raise a
11
+ * notification, or ask for a snapshot. It may not act. That is not a
12
+ * limitation waiting to be lifted — {@link TriggerEffect} has no member that
13
+ * touches the world, because a trigger fires on observed content, and a
14
+ * trigger that could act would be a page deciding to click something by
15
+ * putting the right words on screen.
16
+ *
17
+ * The second rule is a cooldown, which sounds like ergonomics and is not. A
18
+ * text trigger on a scrolling log fires on every line, and a thousand
19
+ * notifications is the same as none — except that it also buries the one that
20
+ * mattered.
21
+ *
22
+ * @module @deepwatch/dsh-live/triggers
23
+ */
24
+ import type { LiveEvent, LiveEventKind, LiveSessionState } from './session.js';
25
+ /**
26
+ * What firing does.
27
+ *
28
+ * Three members, all of them observation. There is no `act`, and adding one
29
+ * would mean observed content could reach the operator loop without a person
30
+ * in between.
31
+ */
32
+ export type TriggerEffect =
33
+ /** Keep the moment, so it survives the buffer bound. */
34
+ 'pin'
35
+ /** Tell the person watching. */
36
+ | 'notify'
37
+ /** Ask for a fresh snapshot, for a trigger about continuity. */
38
+ | 'snapshot';
39
+ /** Every effect, so a UI can enumerate them and a test can check the set. */
40
+ export declare const TRIGGER_EFFECTS: readonly TriggerEffect[];
41
+ /** When a trigger fires. */
42
+ export type TriggerCondition =
43
+ /** Any event of this kind. */
44
+ {
45
+ readonly kind: 'event_kind';
46
+ readonly eventKind: LiveEventKind;
47
+ }
48
+ /** Observed text containing a phrase. */
49
+ | {
50
+ readonly kind: 'text_contains';
51
+ readonly phrase: string;
52
+ readonly caseSensitive: boolean;
53
+ }
54
+ /** A capture gap longer than a threshold. */
55
+ | {
56
+ readonly kind: 'gap_longer_than';
57
+ readonly ms: number;
58
+ }
59
+ /** Nothing observed for a while, which is itself information. */
60
+ | {
61
+ readonly kind: 'silence_for';
62
+ readonly ms: number;
63
+ };
64
+ /** One rule. */
65
+ export interface LiveTrigger {
66
+ readonly triggerId: string;
67
+ /** What the person called it. */
68
+ readonly label: string;
69
+ readonly when: TriggerCondition;
70
+ readonly effect: TriggerEffect;
71
+ /** How long after firing before it may fire again. */
72
+ readonly cooldownMs: number;
73
+ readonly enabled: boolean;
74
+ }
75
+ /** One firing. */
76
+ export interface TriggerFiring {
77
+ readonly triggerId: string;
78
+ readonly effect: TriggerEffect;
79
+ /** The event that caused it, when one did. */
80
+ readonly eventSeq: number | null;
81
+ readonly atMs: number;
82
+ /** One line for the notification. Presentation only. */
83
+ readonly reason: string;
84
+ }
85
+ /** When each trigger last fired, so a cooldown can be applied. */
86
+ export type TriggerCooldowns = ReadonlyMap<string, number>;
87
+ /**
88
+ * Evaluate triggers against newly arrived events.
89
+ *
90
+ * Pure, and takes the cooldown map rather than holding one, so the same
91
+ * function serves the live session and a replay of it. A trigger evaluator
92
+ * with internal state would produce different firings on replay than it did
93
+ * live, which would make a pinned moment unreproducible.
94
+ */
95
+ export declare function evaluateTriggers(state: LiveSessionState, triggers: readonly LiveTrigger[], newEvents: readonly LiveEvent[], nowMs: number, cooldowns?: TriggerCooldowns): {
96
+ readonly firings: readonly TriggerFiring[];
97
+ readonly cooldowns: TriggerCooldowns;
98
+ };
99
+ /**
100
+ * Whether a firing may cause anything outside the session.
101
+ *
102
+ * Always false. Present as a function rather than as a comment so a caller
103
+ * that is about to route a firing somewhere has something to check, and so a
104
+ * test can assert the answer for every effect the type allows.
105
+ */
106
+ export declare function mayAct(_firing: TriggerFiring): false;
107
+ /**
108
+ * One line describing what a trigger will do, for the panel that creates it.
109
+ *
110
+ * Says the effect in the words of what it does rather than its name, because
111
+ * "pin" is a word somebody could read as "act on".
112
+ */
113
+ export declare function describeTrigger(trigger: LiveTrigger): string;
114
+ //# sourceMappingURL=triggers.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Triggers: a rule that notices something, and cannot do anything about it.
3
+ *
4
+ * A live observation runs for a long time and the interesting second is
5
+ * usually not the one anybody is watching. So a trigger says "tell me when the
6
+ * deploy log says failed", or "pin the moment the stream drops", and the
7
+ * session keeps an eye out.
8
+ *
9
+ * The whole design question is what a trigger is allowed to *do*, and the
10
+ * answer here is deliberately narrow: it may pin a moment, raise a
11
+ * notification, or ask for a snapshot. It may not act. That is not a
12
+ * limitation waiting to be lifted — {@link TriggerEffect} has no member that
13
+ * touches the world, because a trigger fires on observed content, and a
14
+ * trigger that could act would be a page deciding to click something by
15
+ * putting the right words on screen.
16
+ *
17
+ * The second rule is a cooldown, which sounds like ergonomics and is not. A
18
+ * text trigger on a scrolling log fires on every line, and a thousand
19
+ * notifications is the same as none — except that it also buries the one that
20
+ * mattered.
21
+ *
22
+ * @module @deepwatch/dsh-live/triggers
23
+ */
24
+ /** Every effect, so a UI can enumerate them and a test can check the set. */
25
+ export const TRIGGER_EFFECTS = ['pin', 'notify', 'snapshot'];
26
+ /** Whether one event satisfies one condition. */
27
+ function matches(condition, event) {
28
+ switch (condition.kind) {
29
+ case 'event_kind':
30
+ return event.kind === condition.eventKind
31
+ ? { hit: true, reason: `a ${condition.eventKind} event` }
32
+ : { hit: false, reason: '' };
33
+ case 'text_contains': {
34
+ const haystack = condition.caseSensitive ? event.text : event.text.toLowerCase();
35
+ const needle = condition.caseSensitive ? condition.phrase : condition.phrase.toLowerCase();
36
+ return haystack.includes(needle)
37
+ ? { hit: true, reason: `observed text contains "${condition.phrase}"` }
38
+ : { hit: false, reason: '' };
39
+ }
40
+ case 'gap_longer_than': {
41
+ if (event.kind !== 'gap' || event.range === null)
42
+ return { hit: false, reason: '' };
43
+ const length = event.range.endMs - event.range.startMs;
44
+ return length > condition.ms
45
+ ? { hit: true, reason: `a ${String(length)}ms capture gap` }
46
+ : { hit: false, reason: '' };
47
+ }
48
+ case 'silence_for':
49
+ // Silence is not a property of an event. Handled separately below.
50
+ return { hit: false, reason: '' };
51
+ }
52
+ }
53
+ /**
54
+ * Evaluate triggers against newly arrived events.
55
+ *
56
+ * Pure, and takes the cooldown map rather than holding one, so the same
57
+ * function serves the live session and a replay of it. A trigger evaluator
58
+ * with internal state would produce different firings on replay than it did
59
+ * live, which would make a pinned moment unreproducible.
60
+ */
61
+ export function evaluateTriggers(state, triggers, newEvents, nowMs, cooldowns = new Map()) {
62
+ const firings = [];
63
+ const updated = new Map(cooldowns);
64
+ /** Whether a trigger is allowed to fire right now. */
65
+ const ready = (trigger, atMs) => {
66
+ if (!trigger.enabled)
67
+ return false;
68
+ const last = updated.get(trigger.triggerId);
69
+ return last === undefined || atMs - last >= trigger.cooldownMs;
70
+ };
71
+ for (const trigger of triggers) {
72
+ if (trigger.when.kind === 'silence_for') {
73
+ // A silence trigger is about the absence of events, so it is evaluated
74
+ // against the clock rather than against a batch.
75
+ const newest = state.events[state.events.length - 1];
76
+ const since = newest === undefined ? nowMs - state.startedAtMs : nowMs - newest.at;
77
+ if (newEvents.length === 0 && since >= trigger.when.ms && ready(trigger, nowMs)) {
78
+ updated.set(trigger.triggerId, nowMs);
79
+ firings.push({
80
+ triggerId: trigger.triggerId,
81
+ effect: trigger.effect,
82
+ eventSeq: null,
83
+ atMs: nowMs,
84
+ reason: `nothing observed for ${String(since)}ms`,
85
+ });
86
+ }
87
+ continue;
88
+ }
89
+ for (const event of newEvents) {
90
+ if (!ready(trigger, event.at))
91
+ continue;
92
+ const result = matches(trigger.when, event);
93
+ if (!result.hit)
94
+ continue;
95
+ updated.set(trigger.triggerId, event.at);
96
+ firings.push({
97
+ triggerId: trigger.triggerId,
98
+ effect: trigger.effect,
99
+ eventSeq: event.seq,
100
+ atMs: event.at,
101
+ reason: result.reason,
102
+ });
103
+ }
104
+ }
105
+ return { firings, cooldowns: updated };
106
+ }
107
+ /**
108
+ * Whether a firing may cause anything outside the session.
109
+ *
110
+ * Always false. Present as a function rather than as a comment so a caller
111
+ * that is about to route a firing somewhere has something to check, and so a
112
+ * test can assert the answer for every effect the type allows.
113
+ */
114
+ export function mayAct(_firing) {
115
+ return false;
116
+ }
117
+ /**
118
+ * One line describing what a trigger will do, for the panel that creates it.
119
+ *
120
+ * Says the effect in the words of what it does rather than its name, because
121
+ * "pin" is a word somebody could read as "act on".
122
+ */
123
+ export function describeTrigger(trigger) {
124
+ const when = trigger.when.kind === 'event_kind'
125
+ ? `on any ${trigger.when.eventKind} event`
126
+ : trigger.when.kind === 'text_contains'
127
+ ? `when observed text contains "${trigger.when.phrase}"`
128
+ : trigger.when.kind === 'gap_longer_than'
129
+ ? `when capture drops for more than ${String(trigger.when.ms)}ms`
130
+ : `when nothing is observed for ${String(trigger.when.ms)}ms`;
131
+ const does = trigger.effect === 'pin'
132
+ ? 'keep the moment'
133
+ : trigger.effect === 'notify'
134
+ ? 'tell you'
135
+ : 'ask for a fresh snapshot';
136
+ return `${when}, ${does}. A trigger never acts on what it sees.`;
137
+ }
138
+ //# sourceMappingURL=triggers.js.map
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@deepwatch/dsh-live",
3
+ "version": "0.1.0",
4
+ "description": "Live mode — cursors, gaps, clocks, reconnect and a bounded buffer",
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/live"
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
+ "live",
22
+ "capture"
23
+ ],
24
+ "dsh": {
25
+ "client": {
26
+ "inject": [
27
+ "@deepseek-ai/dsh-client-ui-slots"
28
+ ],
29
+ "platform": "web"
30
+ }
31
+ },
32
+ "main": "lib/index.js",
33
+ "types": "lib/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./lib/index.d.ts",
37
+ "default": "./lib/index.js"
38
+ },
39
+ "./capture": {
40
+ "types": "./lib/capture.d.ts",
41
+ "default": "./lib/capture.js"
42
+ },
43
+ "./client": {
44
+ "types": "./lib/client/index.d.ts",
45
+ "default": "./lib/client.js"
46
+ },
47
+ "./components": {
48
+ "types": "./lib/client/components.d.ts",
49
+ "default": "./lib/client/components.js"
50
+ },
51
+ "./live-mode": {
52
+ "types": "./lib/client/live-mode.d.ts",
53
+ "default": "./lib/client/live-mode.js"
54
+ },
55
+ "./package.json": "./package.json",
56
+ "./sources-catalogue": {
57
+ "types": "./lib/sources-catalogue.d.ts",
58
+ "default": "./lib/sources-catalogue.js"
59
+ },
60
+ "./synthetic-source": {
61
+ "types": "./lib/synthetic-source.d.ts",
62
+ "default": "./lib/synthetic-source.js"
63
+ }
64
+ },
65
+ "files": [
66
+ "lib/index.js",
67
+ "lib/client.js",
68
+ "lib/client.js.map",
69
+ "lib/**/*.js",
70
+ "lib/**/*.d.ts"
71
+ ],
72
+ "sideEffects": false,
73
+ "engines": {
74
+ "node": "^22.19.0 || >=24.0.0"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ },
79
+ "dependencies": {
80
+ "@deepwatch/dsh-client-brand": "^0.1.0",
81
+ "@deepwatch/dsh-contracts": "^0.1.0",
82
+ "@deepwatch/dsh-workspace": "^0.1.0"
83
+ },
84
+ "peerDependencies": {
85
+ "@deepseek-ai/cordis": "4.0.2",
86
+ "react": "^18.2.0"
87
+ },
88
+ "devDependencies": {
89
+ "@deepseek-ai/cordis": "4.0.2",
90
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
91
+ "@types/react": "~18.3.1",
92
+ "react": "^18.2.0"
93
+ },
94
+ "scripts": {
95
+ "bundle": "tsdown"
96
+ }
97
+ }