@deepwatch/dsh-contracts 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/lib/query.d.ts ADDED
@@ -0,0 +1,362 @@
1
+ /**
2
+ * The read plane: how a Watch mode asks the host what it should render.
3
+ *
4
+ * Watch contributes tools, and a tool result is how evidence reaches the
5
+ * *conversation*. It is not how a surface populates itself. A
6
+ * `conversation.view` entry is handed `{ inspect, onInspectDone }` and nothing
7
+ * else, so Live, Memory, Library and Compare had no way to obtain their own
8
+ * data and each defaulted to an empty array. Four of the seven modes rendered
9
+ * an honest empty state, which is a truthful surface and not a working one.
10
+ *
11
+ * The seam this uses is DSH's own. `packages/typert` defines Remote services
12
+ * dispatched through a Gateway that already owns request correlation, abort
13
+ * signals and structured failure, and `ctx.remote` is the client face of it.
14
+ * An earlier note in this distribution described `ctx.remote` as an event bus
15
+ * rather than a query client. That is true of `$on`/`$dispatch` and misses the
16
+ * typed invocation path beside them, and the conclusion drawn from it -- that
17
+ * populating these modes would mean building a second data path -- was the
18
+ * wrong one. Nothing here is a second path.
19
+ *
20
+ * Every operation is enumerated. There is no open `operation: string` with a
21
+ * free-form `params` object: a request is one member of a discriminated union
22
+ * or it is refused, so a surface cannot ask for something the host does not
23
+ * implement and the host cannot receive a shape it did not expect.
24
+ *
25
+ * Four properties this exists to guarantee.
26
+ *
27
+ * **Reads cannot express a write.** Every operation answers a question. A
28
+ * request that changes something is not in the union, so a surface cannot
29
+ * acquire a side effect by accident and captured or model-generated content
30
+ * reaching these fields cannot become an action.
31
+ *
32
+ * **Nothing here names a location.** Identifiers are drawn from a charset with
33
+ * no separator, no colon and no dot-dot, so a parameter cannot carry a
34
+ * filesystem path, a UNC share, an executable name or a storage URL. The host
35
+ * decides where it reads; the caller only says which record.
36
+ *
37
+ * **Every answer carries a revision, and a stale one is dropped.** Two reads
38
+ * issued in order can return out of order, and a surface that renders whichever
39
+ * arrived last shows older data than it had a moment ago -- intermittently, and
40
+ * under load, which is where that bug survives review.
41
+ *
42
+ * **Everything is bounded.** Request size, string length, array length, nesting
43
+ * depth, identifier length and cursor length all have limits, because the cost
44
+ * of a malformed request must not be a function of how malformed it is.
45
+ *
46
+ * Cancellation is deliberately absent from this module. Typert dispatches with
47
+ * an `AbortSignal`, and a second cancellation protocol carried in the payload
48
+ * would be a way for the two to disagree about whether a call is still live.
49
+ *
50
+ * Browser-safe like the rest of this package: no Node imports, no runtime
51
+ * identity, nothing a client bundle would have to deduplicate.
52
+ *
53
+ * @module @deepwatch/dsh-contracts/query
54
+ */
55
+ import type { WatchResult } from './index.js';
56
+ /** The read-plane contract version this build speaks. */
57
+ export declare const WATCH_QUERY_PROTOCOL_VERSION = 1;
58
+ /** The oldest read-plane contract this build still answers. */
59
+ export declare const WATCH_QUERY_PROTOCOL_MIN = 1;
60
+ /**
61
+ * Every bound the read plane enforces.
62
+ *
63
+ * Stated in one place so a reviewer can see the whole budget at once, and so a
64
+ * test can assert against the same numbers the parser uses.
65
+ */
66
+ export declare const QUERY_LIMITS: {
67
+ /** A whole request, serialised. Generous for a query, useless as a channel. */
68
+ readonly requestBytes: 8192;
69
+ /** Any single string the caller supplies. */
70
+ readonly stringLength: 2048;
71
+ /** A free-text search term. Shorter than a general string on purpose. */
72
+ readonly queryLength: 512;
73
+ /** Any array the caller supplies. */
74
+ readonly arrayLength: 64;
75
+ /** How deep a params object may nest before it is refused. */
76
+ readonly depth: 6;
77
+ /** An identifier: record ids, session ids, scope names. */
78
+ readonly identifierLength: 128;
79
+ /** A request id. */
80
+ readonly requestIdLength: 64;
81
+ /** A cursor, which the host issued and the caller returns unchanged. */
82
+ readonly cursorLength: 512;
83
+ /** The most records one page may carry, whatever the caller asked for. */
84
+ readonly limit: 200;
85
+ /** The longest a surface may wait before it must show something. */
86
+ readonly deadlineMs: 30000;
87
+ };
88
+ /** The namespaces a mode may read from. Reads only; there is no write here. */
89
+ export declare const QUERY_NAMESPACES: readonly ["library", "memory", "compare", "live"];
90
+ /** One readable namespace. */
91
+ export type QueryNamespace = (typeof QUERY_NAMESPACES)[number];
92
+ /**
93
+ * Every operation the host implements, by namespace.
94
+ *
95
+ * The parser rejects anything absent from this table, so adding an operation
96
+ * is one edit and forgetting to implement one is a refusal rather than an
97
+ * undefined call.
98
+ */
99
+ export declare const QUERY_OPERATIONS: {
100
+ readonly library: readonly ["search", "get"];
101
+ readonly memory: readonly ["list", "get"];
102
+ readonly compare: readonly ["pair"];
103
+ readonly live: readonly ["state"];
104
+ };
105
+ /** Why a read was refused. */
106
+ export type QueryErrorCode =
107
+ /** The host speaks a contract this client cannot read, or the reverse. */
108
+ 'protocol_mismatch'
109
+ /** The namespace is not one this host serves. */
110
+ | 'unknown_namespace'
111
+ /** The namespace exists; this operation in it does not. */
112
+ | 'unknown_operation'
113
+ /** The request did not satisfy the contract; nothing was executed. */
114
+ | 'malformed_request'
115
+ /** The request exceeded a size, depth or length bound. */
116
+ | 'request_too_large'
117
+ /** The deadline passed before an answer existed. */
118
+ | 'deadline_exceeded'
119
+ /** The caller aborted, or the surface was disposed. */
120
+ | 'cancelled'
121
+ /** The host is present but this capability is not available here. */
122
+ | 'unavailable'
123
+ /** The cursor is not one this host issued for this scope and snapshot. */
124
+ | 'cursor_expired'
125
+ /** The host answered, and the answer did not satisfy the contract. */
126
+ | 'malformed_response';
127
+ /**
128
+ * A monotonic revision.
129
+ *
130
+ * Compared, never interpreted. Deliberately not a timestamp: two hosts with
131
+ * unsynchronised clocks would produce an ordering that is wrong rather than
132
+ * merely coarse.
133
+ */
134
+ export type Revision = number;
135
+ /** What a Library search asks for. */
136
+ export interface LibrarySearchParams {
137
+ /** Free text. Every term must match; the host decides how it tokenises. */
138
+ readonly query: string;
139
+ readonly limit: number;
140
+ /** Restrict to these modalities, or all of them when empty. */
141
+ readonly modalities: readonly string[];
142
+ }
143
+ /** One Library record, by id. */
144
+ export interface LibraryGetParams {
145
+ readonly recordId: string;
146
+ }
147
+ /** What a Memory listing asks for. */
148
+ export interface MemoryListParams {
149
+ /** Which memory scope to read. The host maps this to a store. */
150
+ readonly scope: string;
151
+ readonly limit: number;
152
+ }
153
+ /** One memory card, by id. */
154
+ export interface MemoryGetParams {
155
+ readonly cardId: string;
156
+ }
157
+ /** The two records a comparison is over. */
158
+ export interface ComparePairParams {
159
+ readonly leftId: string;
160
+ readonly rightId: string;
161
+ }
162
+ /** Live has no parameters: it reports the session the host already has. */
163
+ export type LiveStateParams = Record<string, never>;
164
+ /** What every read carries, whatever it asks for. */
165
+ export interface QueryEnvelope {
166
+ readonly protocol: number;
167
+ /** Correlates the answer. Typert owns cancellation; this is for logs. */
168
+ readonly requestId: string;
169
+ /** How long the caller will wait. Clamped to the limit above. */
170
+ readonly deadlineMs: number;
171
+ /** Continues an earlier snapshot, when the host issued one. */
172
+ readonly cursor: string | null;
173
+ }
174
+ /** One fully-typed read. */
175
+ export type QueryRequest = (QueryEnvelope & {
176
+ readonly namespace: 'library';
177
+ readonly operation: 'search';
178
+ readonly params: LibrarySearchParams;
179
+ }) | (QueryEnvelope & {
180
+ readonly namespace: 'library';
181
+ readonly operation: 'get';
182
+ readonly params: LibraryGetParams;
183
+ }) | (QueryEnvelope & {
184
+ readonly namespace: 'memory';
185
+ readonly operation: 'list';
186
+ readonly params: MemoryListParams;
187
+ }) | (QueryEnvelope & {
188
+ readonly namespace: 'memory';
189
+ readonly operation: 'get';
190
+ readonly params: MemoryGetParams;
191
+ }) | (QueryEnvelope & {
192
+ readonly namespace: 'compare';
193
+ readonly operation: 'pair';
194
+ readonly params: ComparePairParams;
195
+ }) | (QueryEnvelope & {
196
+ readonly namespace: 'live';
197
+ readonly operation: 'state';
198
+ readonly params: LiveStateParams;
199
+ });
200
+ /** A Library record as the surface renders it. */
201
+ export interface LibraryRecordView {
202
+ readonly recordId: string;
203
+ readonly title: string;
204
+ readonly modality: string;
205
+ readonly capturedAt: string | null;
206
+ /** Where this came from, stated rather than implied. */
207
+ readonly provenance: string;
208
+ /** The evidence this record points at. Never a filesystem path. */
209
+ readonly evidenceIds: readonly string[];
210
+ }
211
+ /** A memory card as the surface renders it. */
212
+ export interface MemoryCardView {
213
+ readonly cardId: string;
214
+ readonly scope: string;
215
+ readonly text: string;
216
+ readonly writtenAt: string | null;
217
+ /** Which earlier card this one corrects, when it corrects one. */
218
+ readonly correctsCardId: string | null;
219
+ readonly forgotten: boolean;
220
+ readonly provenance: string;
221
+ }
222
+ /** The result of comparing two records. */
223
+ export interface CompareResultView {
224
+ readonly leftId: string;
225
+ readonly rightId: string;
226
+ /** Differences in what the two produced. */
227
+ readonly outputDifferences: readonly string[];
228
+ /**
229
+ * Differences in what was verified about them.
230
+ *
231
+ * Separate from output on purpose: two records can agree on their output and
232
+ * disagree on whether anybody checked it, and collapsing those is how an
233
+ * unverified result inherits a verified one's credibility.
234
+ */
235
+ readonly verificationDifferences: readonly string[];
236
+ /** Whether the same inputs would produce this same comparison again. */
237
+ readonly reproducible: boolean;
238
+ }
239
+ /** The Live session, flattened for the wire. */
240
+ export interface LiveStateView {
241
+ readonly sessionId: string | null;
242
+ readonly sourceId: string | null;
243
+ readonly state: string;
244
+ readonly permission: string;
245
+ readonly startedAt: string | null;
246
+ readonly reason: string;
247
+ readonly observationCount: number;
248
+ }
249
+ /** What a receipt records about a session that ended. */
250
+ export interface LiveReceiptView {
251
+ readonly sessionId: string;
252
+ readonly sourceId: string;
253
+ readonly endedAt: string | null;
254
+ readonly outcome: string;
255
+ readonly observationCount: number;
256
+ }
257
+ /** The item type each operation returns. */
258
+ export interface QueryResultMap {
259
+ 'library/search': LibraryRecordView;
260
+ 'library/get': LibraryRecordView;
261
+ 'memory/list': MemoryCardView;
262
+ 'memory/get': MemoryCardView;
263
+ 'compare/pair': CompareResultView;
264
+ 'live/state': LiveStateView | LiveReceiptView;
265
+ }
266
+ /** What every read returns. */
267
+ export interface QuerySnapshot<Item> {
268
+ readonly protocol: number;
269
+ readonly requestId: string;
270
+ /** The host revision at the moment this answer was produced. */
271
+ readonly revision: Revision;
272
+ readonly items: readonly Item[];
273
+ /** Non-null when more remains; pass it back as `cursor`. */
274
+ readonly nextCursor: string | null;
275
+ /**
276
+ * Whether the host answered from complete data.
277
+ *
278
+ * A rebuilding index answers `false` and still returns what it has. The
279
+ * surface has to say so rather than presenting a partial answer as the whole.
280
+ */
281
+ readonly complete: boolean;
282
+ }
283
+ /** A read outcome. */
284
+ export type QueryResult<Item> = WatchResult<QuerySnapshot<Item>>;
285
+ /**
286
+ * What a cursor is bound to.
287
+ *
288
+ * A cursor is meaningless outside the snapshot it was issued against, and
289
+ * dangerous outside its scope: replaying one from another session would page
290
+ * through data the caller never asked for. So the binding travels in the
291
+ * cursor and the host checks it rather than trusting the caller to.
292
+ */
293
+ export interface CursorScope {
294
+ readonly namespace: QueryNamespace;
295
+ readonly operation: string;
296
+ /** The workspace, profile or session the snapshot belongs to. */
297
+ readonly scope: string;
298
+ /** The revision the snapshot was taken at. */
299
+ readonly revision: Revision;
300
+ }
301
+ /** A decoded cursor: its binding, and where it resumes. */
302
+ export interface DecodedCursor extends CursorScope {
303
+ readonly offset: number;
304
+ }
305
+ /** Encode a cursor. Opaque to the caller, checkable by the host. */
306
+ export declare function encodeCursor(cursor: DecodedCursor): string;
307
+ /**
308
+ * Decode a cursor and confirm it belongs here.
309
+ *
310
+ * Returns null for anything that does not decode or does not match the scope
311
+ * it is being replayed into. The caller turns that into `cursor_expired`,
312
+ * which is the honest description: the host cannot serve it, and saying why in
313
+ * more detail would describe another session's state.
314
+ */
315
+ export declare function decodeCursor(value: string, expected: CursorScope): DecodedCursor | null;
316
+ /** A non-negative safe integer. Protocol numbers and revisions must be one. */
317
+ export declare function isSafeCount(value: unknown): value is number;
318
+ /** Whether a value is an identifier of an acceptable shape and length. */
319
+ export declare function isIdentifier(value: unknown): value is string;
320
+ /** Whether a value is a namespace this host serves. */
321
+ export declare function isQueryNamespace(value: unknown): value is QueryNamespace;
322
+ /** Whether an operation exists in a namespace. */
323
+ export declare function isQueryOperation(namespace: QueryNamespace, operation: unknown): boolean;
324
+ /** Negotiate the read-plane contract, or null when there is no overlap. */
325
+ export declare function negotiateQueryProtocol(peerMin: number, peerMax: number): number | null;
326
+ /**
327
+ * Whether an arriving snapshot is newer than what a surface already shows.
328
+ *
329
+ * The equal case is deliberately false. Re-rendering an identical revision
330
+ * costs a frame and gains nothing, and treating equal as newer would let two
331
+ * in-flight answers to the same revision fight.
332
+ */
333
+ export declare function isNewerRevision(arriving: Revision, showing: Revision | null): boolean;
334
+ /** Bring a caller deadline inside what the host will honour. */
335
+ export declare function clampDeadline(requested: unknown): number;
336
+ /** Bring a caller page size inside what the host will return. */
337
+ export declare function clampLimit(requested: unknown): number;
338
+ /** Build the refusal a host returns when a read could not be produced. */
339
+ export declare function queryRefusal(code: QueryErrorCode, message: string, fix: string, options?: {
340
+ readonly requestId?: string;
341
+ readonly retryable?: boolean;
342
+ }): WatchResult<never>;
343
+ /**
344
+ * Validate a request at the boundary.
345
+ *
346
+ * Everything crossing into the host is parsed here, including requests this
347
+ * distribution's own client produced: a surface is reachable by anything that
348
+ * can reach the page, and "our own code sent it" is an assumption rather than
349
+ * a guarantee. Returns the normalised request, or the refusal to send back.
350
+ */
351
+ export declare function parseQueryRequest(value: unknown): WatchResult<QueryRequest>;
352
+ /**
353
+ * Validate a snapshot before a surface renders it.
354
+ *
355
+ * The host is trusted to be the host and not trusted to be correct. A response
356
+ * that does not satisfy this contract is a defect somewhere, and rendering it
357
+ * anyway turns a defect into a wrong answer displayed confidently.
358
+ */
359
+ export declare function parseQuerySnapshot<Item>(value: unknown, parseItem: (item: unknown) => Item | null): WatchResult<QuerySnapshot<Item>>;
360
+ /** Read a Library record off the wire, or null when it is not one. */
361
+ export declare function parseLibraryRecord(value: unknown): LibraryRecordView | null;
362
+ //# sourceMappingURL=query.d.ts.map