@pellux/goodvibes-tui 0.24.0 → 0.25.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +4 -5
  3. package/docs/foundation-artifacts/operator-contract.json +304 -230
  4. package/package.json +2 -2
  5. package/src/daemon/calendar/caldav-client.ts +657 -0
  6. package/src/daemon/calendar/ics.ts +556 -0
  7. package/src/daemon/calendar/index.ts +52 -0
  8. package/src/daemon/calendar/register.ts +527 -0
  9. package/src/daemon/channels/drafts/draft-store.ts +363 -0
  10. package/src/daemon/channels/drafts/index.ts +22 -0
  11. package/src/daemon/channels/drafts/register.ts +449 -0
  12. package/src/daemon/channels/inbox/cursor-store.ts +298 -0
  13. package/src/daemon/channels/inbox/index.ts +58 -0
  14. package/src/daemon/channels/inbox/mapping.ts +190 -0
  15. package/src/daemon/channels/inbox/poller.ts +155 -0
  16. package/src/daemon/channels/inbox/provider-adapter.ts +152 -0
  17. package/src/daemon/channels/inbox/providers/discord.ts +253 -0
  18. package/src/daemon/channels/inbox/providers/email.ts +151 -0
  19. package/src/daemon/channels/inbox/providers/imap-client.ts +300 -0
  20. package/src/daemon/channels/inbox/providers/route-util.ts +23 -0
  21. package/src/daemon/channels/inbox/providers/slack.ts +264 -0
  22. package/src/daemon/channels/inbox/register.ts +247 -0
  23. package/src/daemon/channels/routing/inbox-bridge.ts +58 -0
  24. package/src/daemon/channels/routing/index.ts +39 -0
  25. package/src/daemon/channels/routing/register.ts +296 -0
  26. package/src/daemon/channels/routing/route-store.ts +278 -0
  27. package/src/daemon/channels/routing/routing-resolver.ts +75 -0
  28. package/src/daemon/email/imap-connector.ts +441 -0
  29. package/src/daemon/email/imap-parsing.ts +499 -0
  30. package/src/daemon/email/index.ts +68 -0
  31. package/src/daemon/email/register.ts +715 -0
  32. package/src/daemon/email/smtp-connector.ts +557 -0
  33. package/src/daemon/operator/credential-store.ts +129 -0
  34. package/src/daemon/operator/index.ts +43 -0
  35. package/src/daemon/operator/register-helper.ts +150 -0
  36. package/src/daemon/operator/sqlite-store.ts +124 -0
  37. package/src/daemon/operator/surfaces.ts +137 -0
  38. package/src/daemon/operator/types.ts +207 -0
  39. package/src/daemon/remote/backends/cloud-terminal.ts +137 -0
  40. package/src/daemon/remote/backends/docker.ts +80 -0
  41. package/src/daemon/remote/backends/index.ts +34 -0
  42. package/src/daemon/remote/backends/local-process.ts +113 -0
  43. package/src/daemon/remote/backends/process-runner.ts +151 -0
  44. package/src/daemon/remote/backends/ssh.ts +120 -0
  45. package/src/daemon/remote/backends/types.ts +71 -0
  46. package/src/daemon/remote/dispatcher.ts +160 -0
  47. package/src/daemon/remote/index.ts +74 -0
  48. package/src/daemon/remote/peer-registry.ts +321 -0
  49. package/src/daemon/remote/register.ts +411 -0
  50. package/src/daemon/triage/index.ts +59 -0
  51. package/src/daemon/triage/integration.ts +179 -0
  52. package/src/daemon/triage/pipeline.ts +285 -0
  53. package/src/daemon/triage/register.ts +231 -0
  54. package/src/daemon/triage/scorer.ts +287 -0
  55. package/src/daemon/triage/tagger.ts +777 -0
  56. package/src/runtime/services.ts +35 -0
  57. package/src/version.ts +1 -1
@@ -0,0 +1,285 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Daemon-internal triage PIPELINE.
3
+ //
4
+ // runInboxTriage(items): scores each inbound item (scorer.ts) and writes the
5
+ // resulting triageScore/triageTags back into the inbox triage store so that a
6
+ // later channels.inbox.list response can surface pre-scored items.
7
+ //
8
+ // The inbox surface owns the authoritative cursor-store and exposes
9
+ // triageScore/triageTags columns. To stay strictly within src/daemon/triage/
10
+ // (per the hard rules), this pipeline persists into a dedicated, co-located
11
+ // OperatorSqliteStore ('inbox-triage.sqlite') keyed by item id. The inbox
12
+ // surface reads these rows by id when assembling its list response. The schema
13
+ // mirrors the agreed columns: triageScore REAL, triageTags TEXT (JSON array).
14
+ // ---------------------------------------------------------------------------
15
+
16
+ import {
17
+ OperatorSqliteStore,
18
+ type InboundChannelItem,
19
+ type OperatorContext,
20
+ } from '../operator/index.ts';
21
+ import {
22
+ labelToTag,
23
+ scoreInboundItem,
24
+ type TriageLabel,
25
+ type TriageScore,
26
+ type TriageScorerOptions,
27
+ } from './scorer.ts';
28
+
29
+ export const TRIAGE_STORE_FILE = 'inbox-triage.sqlite';
30
+
31
+ const SCHEMA: string[] = [
32
+ `CREATE TABLE IF NOT EXISTS inbox_triage (
33
+ id TEXT PRIMARY KEY,
34
+ surface TEXT NOT NULL,
35
+ triageScore REAL NOT NULL,
36
+ triageLabel TEXT NOT NULL,
37
+ triageTags TEXT NOT NULL,
38
+ spamSignal REAL NOT NULL,
39
+ prioritySignal REAL NOT NULL,
40
+ updatedAt TEXT NOT NULL
41
+ )`,
42
+ `CREATE INDEX IF NOT EXISTS idx_inbox_triage_label ON inbox_triage (triageLabel)`,
43
+ `CREATE INDEX IF NOT EXISTS idx_inbox_triage_surface ON inbox_triage (surface)`,
44
+ ];
45
+
46
+ export interface TriageMetadata {
47
+ triageScore: number;
48
+ triageLabel: TriageLabel;
49
+ triageTags: string[];
50
+ signals: TriageScore['signals'];
51
+ }
52
+
53
+ export interface TriagedItem extends InboundChannelItem {
54
+ triage: TriageMetadata;
55
+ }
56
+
57
+ export interface RunInboxTriageOptions {
58
+ scorer?: TriageScorerOptions;
59
+ /** Inject a store (tests). When omitted, a triage store is opened/closed. */
60
+ store?: OperatorSqliteStore;
61
+ /** When true, do not persist — only compute (used by inbox.triage.list). */
62
+ dryRun?: boolean;
63
+ /** Clock injection for deterministic updatedAt in tests. */
64
+ now?: () => Date;
65
+ }
66
+
67
+ export interface RunInboxTriageResult {
68
+ items: TriagedItem[];
69
+ scored: number;
70
+ persisted: number;
71
+ }
72
+
73
+ function toMetadata(score: TriageScore): TriageMetadata {
74
+ return {
75
+ triageScore: score.score,
76
+ triageLabel: score.label,
77
+ triageTags: [labelToTag(score.label)],
78
+ signals: score.signals,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Open the triage store for this working directory. Caller owns close()/save()
84
+ * when they pass their own store; otherwise runInboxTriage manages lifecycle.
85
+ */
86
+ export function createTriageStore(workingDirectory: string): OperatorSqliteStore {
87
+ return new OperatorSqliteStore({
88
+ workingDirectory,
89
+ fileName: TRIAGE_STORE_FILE,
90
+ schema: SCHEMA,
91
+ });
92
+ }
93
+
94
+ function persistRow(
95
+ store: OperatorSqliteStore,
96
+ item: InboundChannelItem,
97
+ meta: TriageMetadata,
98
+ updatedAt: string,
99
+ ): void {
100
+ store.run(
101
+ `INSERT INTO inbox_triage
102
+ (id, surface, triageScore, triageLabel, triageTags, spamSignal, prioritySignal, updatedAt)
103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
104
+ ON CONFLICT(id) DO UPDATE SET
105
+ surface = excluded.surface,
106
+ triageScore = excluded.triageScore,
107
+ triageLabel = excluded.triageLabel,
108
+ triageTags = excluded.triageTags,
109
+ spamSignal = excluded.spamSignal,
110
+ prioritySignal = excluded.prioritySignal,
111
+ updatedAt = excluded.updatedAt`,
112
+ [
113
+ item.id,
114
+ item.surface,
115
+ meta.triageScore,
116
+ meta.triageLabel,
117
+ JSON.stringify(meta.triageTags),
118
+ meta.signals.spam,
119
+ meta.signals.priority,
120
+ updatedAt,
121
+ ],
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Score every item and (unless dryRun) persist triageScore/triageTags back into
127
+ * the inbox triage store. Returns each item enriched with triage metadata so
128
+ * the caller can surface it without a second read.
129
+ *
130
+ * Called by the inbox poller (Responsibility 1) after each poll.
131
+ */
132
+ export async function runInboxTriage(
133
+ items: readonly InboundChannelItem[],
134
+ ctx: OperatorContext,
135
+ options: RunInboxTriageOptions = {},
136
+ ): Promise<RunInboxTriageResult> {
137
+ const now = options.now ?? (() => new Date());
138
+ const updatedAt = now().toISOString();
139
+
140
+ const enriched: TriagedItem[] = items.map((item) => {
141
+ const score = scoreInboundItem(item, options.scorer);
142
+ return { ...item, triage: toMetadata(score) };
143
+ });
144
+
145
+ if (options.dryRun || enriched.length === 0) {
146
+ return { items: enriched, scored: enriched.length, persisted: 0 };
147
+ }
148
+
149
+ const ownsStore = !options.store;
150
+ const store = options.store ?? createTriageStore(ctx.workingDirectory);
151
+ let persisted = 0;
152
+ try {
153
+ if (ownsStore) await store.init();
154
+ store.transaction(() => {
155
+ for (const item of enriched) {
156
+ persistRow(store, item, item.triage, updatedAt);
157
+ persisted += 1;
158
+ }
159
+ });
160
+ if (ownsStore) await store.save();
161
+ } catch (error) {
162
+ ctx.logger.error('triage: failed to persist scores', {
163
+ message: error instanceof Error ? error.message : String(error),
164
+ });
165
+ throw error;
166
+ } finally {
167
+ if (ownsStore) store.close();
168
+ }
169
+
170
+ return { items: enriched, scored: enriched.length, persisted };
171
+ }
172
+
173
+ /**
174
+ * Inbound item enriched with the persisted triage metadata that
175
+ * `channels.inbox.list` advertises (triageScore + triageTags). When no triage
176
+ * row exists yet for an item the original item is returned unchanged.
177
+ */
178
+ export interface TriageOverlay {
179
+ triageScore?: number;
180
+ triageTags?: string[];
181
+ triageLabel?: TriageLabel;
182
+ }
183
+
184
+ /** An inbound item overlaid with optional persisted triage metadata. */
185
+ export type TriageEnrichedItem = InboundChannelItem & TriageOverlay;
186
+
187
+ /**
188
+ * Merge persisted triage metadata onto a batch of inbound items by id. This is
189
+ * the exact glue `channels.inbox.list` invokes to surface pre-scored items: the
190
+ * inbox surface lists from its cursor store, then calls this to overlay the
191
+ * triageScore/triageTags columns the contract promises. Items without a stored
192
+ * triage row pass through untouched, so an un-scored feed degrades gracefully.
193
+ */
194
+ export function enrichItemsWithTriage<T extends { id: string }>(
195
+ store: OperatorSqliteStore,
196
+ items: readonly T[],
197
+ ): Array<T & TriageOverlay> {
198
+ if (items.length === 0) return [];
199
+ // Single batched read (`WHERE id IN (...)`) instead of one SELECT per item —
200
+ // this is a hot read path (every channels.inbox.list call), so the N+1 is
201
+ // collapsed to one query keyed by id.
202
+ const byId = readTriageMetadataBatch(
203
+ store,
204
+ items.map((item) => item.id),
205
+ );
206
+ return items.map((item) => {
207
+ const meta = byId.get(item.id);
208
+ if (!meta) return { ...item };
209
+ return {
210
+ ...item,
211
+ triageScore: meta.triageScore,
212
+ triageLabel: meta.triageLabel,
213
+ triageTags: meta.triageTags,
214
+ };
215
+ });
216
+ }
217
+
218
+ interface TriageRow {
219
+ id: string;
220
+ triageScore: number;
221
+ triageLabel: string;
222
+ triageTags: string;
223
+ spamSignal: number;
224
+ prioritySignal: number;
225
+ }
226
+
227
+ function rowToMetadata(row: Omit<TriageRow, 'id'>): TriageMetadata {
228
+ let tags: string[] = [];
229
+ try {
230
+ const parsed = JSON.parse(row.triageTags) as unknown;
231
+ if (Array.isArray(parsed)) tags = parsed.filter((t): t is string => typeof t === 'string');
232
+ } catch {
233
+ tags = [];
234
+ }
235
+ return {
236
+ triageScore: row.triageScore,
237
+ triageLabel: row.triageLabel as TriageLabel,
238
+ triageTags: tags,
239
+ signals: { spam: row.spamSignal, priority: row.prioritySignal },
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Read persisted triage rows for many item ids in a single `WHERE id IN (...)`
245
+ * query. Returns a Map keyed by item id; ids without a stored row are absent.
246
+ * De-duplicates ids so the placeholder list stays minimal.
247
+ */
248
+ export function readTriageMetadataBatch(
249
+ store: OperatorSqliteStore,
250
+ itemIds: readonly string[],
251
+ ): Map<string, TriageMetadata> {
252
+ const out = new Map<string, TriageMetadata>();
253
+ const uniqueIds = [...new Set(itemIds)];
254
+ if (uniqueIds.length === 0) return out;
255
+ const placeholders = uniqueIds.map(() => '?').join(', ');
256
+ const rows = store.all<TriageRow>(
257
+ `SELECT id, triageScore, triageLabel, triageTags, spamSignal, prioritySignal
258
+ FROM inbox_triage WHERE id IN (${placeholders})`,
259
+ uniqueIds,
260
+ );
261
+ for (const row of rows) {
262
+ out.set(row.id, rowToMetadata(row));
263
+ }
264
+ return out;
265
+ }
266
+
267
+ /** Read a single persisted triage row by item id (used by the inbox surface). */
268
+ export function readTriageMetadata(
269
+ store: OperatorSqliteStore,
270
+ itemId: string,
271
+ ): TriageMetadata | null {
272
+ const row = store.get<{
273
+ triageScore: number;
274
+ triageLabel: string;
275
+ triageTags: string;
276
+ spamSignal: number;
277
+ prioritySignal: number;
278
+ }>(
279
+ `SELECT triageScore, triageLabel, triageTags, spamSignal, prioritySignal
280
+ FROM inbox_triage WHERE id = ?`,
281
+ [itemId],
282
+ );
283
+ if (!row) return null;
284
+ return rowToMetadata(row);
285
+ }
@@ -0,0 +1,231 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Daemon-internal triage surface REGISTRATION.
3
+ //
4
+ // Registers the internal methods inbox.triage.list and inbox.triage.tag with
5
+ // transport ['internal'] ONLY — deliberately NOT 'ws'. This keeps them OFF the
6
+ // agent-facing WS method list while remaining invocable by the inbox poller
7
+ // internally. Access is 'operator'.
8
+ //
9
+ // If these are ever promoted to agent-facing methods, their IDs must move to a
10
+ // separate handoff (per the contract); they must not be added to 'ws' here.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import {
14
+ OperatorError,
15
+ declareOperatorMethods,
16
+ type InboundChannelItem,
17
+ type OperatorContext,
18
+ type SurfaceRegister,
19
+ type Unregister,
20
+ } from '../operator/index.ts';
21
+ import { runInboxTriage, type RunInboxTriageOptions } from './pipeline.ts';
22
+ import { createTriageTagger, type TriageTaggerOptions } from './tagger.ts';
23
+ import type { TriageLabel } from './scorer.ts';
24
+
25
+ export const TRIAGE_METHOD_IDS = {
26
+ // Internal step name per the handoff contract (line 612): the scoring step is
27
+ // 'inbox.triage.list' (it lists scored items), the mutation step is
28
+ // 'inbox.triage.tag'. These are non-published, transport ['internal'] only.
29
+ list: 'inbox.triage.list',
30
+ tag: 'inbox.triage.tag',
31
+ } as const;
32
+
33
+ const SCORE_INPUT_SCHEMA: Record<string, unknown> = {
34
+ type: 'object',
35
+ required: ['items'],
36
+ additionalProperties: false,
37
+ properties: {
38
+ items: {
39
+ type: 'array',
40
+ items: { type: 'object' },
41
+ description: 'InboundChannelItem[] to score.',
42
+ },
43
+ persist: {
44
+ type: 'boolean',
45
+ description:
46
+ 'Omit or set true to persist triageScore/triageTags to the inbox triage store; set false to dry-run (compute scores only, no write).',
47
+ },
48
+ },
49
+ };
50
+
51
+ const SCORE_OUTPUT_SCHEMA: Record<string, unknown> = {
52
+ type: 'object',
53
+ required: ['items', 'scored', 'persisted'],
54
+ properties: {
55
+ scored: { type: 'number' },
56
+ persisted: { type: 'number' },
57
+ items: {
58
+ type: 'array',
59
+ items: {
60
+ type: 'object',
61
+ properties: {
62
+ id: { type: 'string' },
63
+ triage: {
64
+ type: 'object',
65
+ properties: {
66
+ triageScore: { type: 'number' },
67
+ triageLabel: { type: 'string', enum: ['spam', 'priority', 'normal'] },
68
+ triageTags: { type: 'array', items: { type: 'string' } },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ };
76
+
77
+ // NOTE: `confirm` is deliberately ABSENT from this advertised schema. Per the
78
+ // operator-confirm-contract-fidelity pattern, confirmed methods must NOT
79
+ // advertise `confirm` in their inputSchema; enforcement is body-level via
80
+ // assertConfirmed (register-helper.ts), which reads body.confirm from the raw
81
+ // untyped invocation body. Keeping it out of the schema makes this an exact
82
+ // match to the handoff Input contract instead of a strict superset.
83
+ const TAG_INPUT_SCHEMA: Record<string, unknown> = {
84
+ type: 'object',
85
+ required: ['item'],
86
+ additionalProperties: false,
87
+ properties: {
88
+ item: { type: 'object', description: 'InboundChannelItem to tag provider-side.' },
89
+ tags: { type: 'array', items: { type: 'string' } },
90
+ label: { type: 'string', enum: ['spam', 'priority', 'normal'] },
91
+ },
92
+ };
93
+
94
+ const TAG_OUTPUT_SCHEMA: Record<string, unknown> = {
95
+ type: 'object',
96
+ required: ['surface', 'itemId', 'appliedTags', 'skipped'],
97
+ properties: {
98
+ surface: { type: 'string' },
99
+ itemId: { type: 'string' },
100
+ appliedTags: { type: 'array', items: { type: 'string' } },
101
+ skipped: { type: 'boolean' },
102
+ reason: { type: 'string' },
103
+ },
104
+ };
105
+
106
+ interface ScoreBody {
107
+ items?: unknown;
108
+ persist?: unknown;
109
+ }
110
+
111
+ interface TagBody {
112
+ item?: unknown;
113
+ tags?: unknown;
114
+ label?: unknown;
115
+ confirm?: unknown;
116
+ }
117
+
118
+ function asInboundItems(value: unknown): InboundChannelItem[] {
119
+ if (!Array.isArray(value)) {
120
+ throw new OperatorError('`items` must be an array of inbound items.', 'TRIAGE_INVALID_INPUT', 400);
121
+ }
122
+ return value.map((entry, index) => {
123
+ if (typeof entry !== 'object' || entry === null) {
124
+ throw new OperatorError(`items[${index}] is not an object.`, 'TRIAGE_INVALID_INPUT', 400);
125
+ }
126
+ const item = entry as Partial<InboundChannelItem>;
127
+ if (typeof item.id !== 'string' || typeof item.surface !== 'string') {
128
+ throw new OperatorError(
129
+ `items[${index}] requires string id and surface.`,
130
+ 'TRIAGE_INVALID_INPUT',
131
+ 400,
132
+ );
133
+ }
134
+ return item as InboundChannelItem;
135
+ });
136
+ }
137
+
138
+ function asInboundItem(value: unknown): InboundChannelItem {
139
+ const [item] = asInboundItems([value]);
140
+ return item!;
141
+ }
142
+
143
+ export interface RegisterTriageOptions {
144
+ pipeline?: RunInboxTriageOptions;
145
+ tagger?: TriageTaggerOptions;
146
+ }
147
+
148
+ /**
149
+ * SurfaceRegister for the daemon-internal triage surface.
150
+ *
151
+ * Wiring contract: the daemon integration layer (services.ts, the single
152
+ * allowed edit site there) calls `registerTriageMethods(ctx)` once with the
153
+ * OperatorContext and retains the returned Unregister for teardown. For the
154
+ * full poll -> score -> persist -> list enrichment loop the contract relies on,
155
+ * compose it via `registerTriagedInbox` (see ./integration.ts), which is
156
+ * directly exercised by the integration test.
157
+ */
158
+ export function createTriageRegister(options: RegisterTriageOptions = {}): SurfaceRegister {
159
+ return (ctx: OperatorContext): Unregister => {
160
+ const tagger = createTriageTagger(ctx, options.tagger);
161
+
162
+ return declareOperatorMethods(ctx, [
163
+ {
164
+ descriptor: {
165
+ id: TRIAGE_METHOD_IDS.list,
166
+ title: 'Inbox Triage — List (score)',
167
+ description:
168
+ 'Daemon-internal: score inbound items for spam/priority and optionally persist triageScore/triageTags.',
169
+ category: 'inbox',
170
+ source: 'daemon',
171
+ access: 'operator',
172
+ transport: ['internal'],
173
+ scopes: ['inbox:triage'],
174
+ effect: 'local-state-mutation',
175
+ inputSchema: SCORE_INPUT_SCHEMA,
176
+ outputSchema: SCORE_OUTPUT_SCHEMA,
177
+ },
178
+ handler: async (input) => {
179
+ const body = (input.body ?? {}) as ScoreBody;
180
+ const items = asInboundItems(body.items);
181
+ const dryRun = body.persist === false;
182
+ return runInboxTriage(items, ctx, { ...options.pipeline, dryRun });
183
+ },
184
+ },
185
+ {
186
+ descriptor: {
187
+ id: TRIAGE_METHOD_IDS.tag,
188
+ title: 'Inbox Triage — Tag',
189
+ description:
190
+ 'Daemon-internal: apply a triage label back on the provider side (IMAP flag / Slack or Discord reaction).',
191
+ category: 'inbox',
192
+ source: 'daemon',
193
+ access: 'operator',
194
+ transport: ['internal'],
195
+ scopes: ['inbox:triage', 'inbox:triage:write'],
196
+ effect: 'confirmed-connected-host-state',
197
+ confirm: true,
198
+ inputSchema: TAG_INPUT_SCHEMA,
199
+ outputSchema: TAG_OUTPUT_SCHEMA,
200
+ },
201
+ handler: async (input) => {
202
+ const body = (input.body ?? {}) as TagBody;
203
+ const item = asInboundItem(body.item);
204
+ const tags = Array.isArray(body.tags)
205
+ ? body.tags.filter((t): t is string => typeof t === 'string')
206
+ : undefined;
207
+ const label =
208
+ typeof body.label === 'string' ? (body.label as TriageLabel) : undefined;
209
+ return tagger.applyTags({
210
+ item,
211
+ tags,
212
+ label,
213
+ // confirm:true is enforced by the declareOperatorMethod guard before
214
+ // we reach here; mirror both flags into the tagger for defense-in-depth.
215
+ confirm: body.confirm === true,
216
+ explicitUserRequest: input.context.explicitUserRequest === true,
217
+ });
218
+ },
219
+ },
220
+ ]);
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Default SurfaceRegister export. Integration may import either this `register`
226
+ * or `registerTriageMethods` (alias) and call it with the OperatorContext.
227
+ */
228
+ export const register: SurfaceRegister = createTriageRegister();
229
+
230
+ /** Named alias matching the integration contract (registerTriageMethods). */
231
+ export const registerTriageMethods: SurfaceRegister = register;