@khorsheed/dsh-reader 0.2.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 (58) hide show
  1. package/README.en.md +90 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +90 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +15119 -0
  6. package/lib/index.js +3660 -0
  7. package/lib/invariant.js +38 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +1738 -0
  10. package/lib/typert.remote-client.d.ts +82 -0
  11. package/lib/typert.remote-client.js +1115 -0
  12. package/lib/types/arxiv.d.ts +47 -0
  13. package/lib/types/arxiv.js +76 -0
  14. package/lib/types/client/ReaderPane.d.ts +71 -0
  15. package/lib/types/client/ReaderPane.js +3135 -0
  16. package/lib/types/client/contract.d.ts +254 -0
  17. package/lib/types/client/contract.js +1 -0
  18. package/lib/types/client/definition.d.ts +27 -0
  19. package/lib/types/client/definition.js +32 -0
  20. package/lib/types/client/extract-article.d.ts +102 -0
  21. package/lib/types/client/extract-article.js +1024 -0
  22. package/lib/types/client/icons.d.ts +27 -0
  23. package/lib/types/client/icons.js +11 -0
  24. package/lib/types/client/index.d.ts +34 -0
  25. package/lib/types/client/index.js +222 -0
  26. package/lib/types/client/locales.d.ts +391 -0
  27. package/lib/types/client/locales.js +382 -0
  28. package/lib/types/client/parse-rss.d.ts +69 -0
  29. package/lib/types/client/parse-rss.js +656 -0
  30. package/lib/types/client/quote.d.ts +89 -0
  31. package/lib/types/client/quote.js +119 -0
  32. package/lib/types/client/selectors.d.ts +167 -0
  33. package/lib/types/client/selectors.js +413 -0
  34. package/lib/types/client/session.d.ts +171 -0
  35. package/lib/types/client/session.js +223 -0
  36. package/lib/types/client/store.d.ts +181 -0
  37. package/lib/types/client/store.js +226 -0
  38. package/lib/types/client/translate.d.ts +379 -0
  39. package/lib/types/client/translate.js +873 -0
  40. package/lib/types/index.d.ts +48 -0
  41. package/lib/types/index.js +25 -0
  42. package/lib/types/invariant.d.ts +29 -0
  43. package/lib/types/invariant.js +38 -0
  44. package/lib/types/link-resolvers.d.ts +107 -0
  45. package/lib/types/link-resolvers.js +299 -0
  46. package/lib/types/remote.d.ts +224 -0
  47. package/lib/types/remote.js +275 -0
  48. package/lib/types/schedule.d.ts +78 -0
  49. package/lib/types/schedule.js +114 -0
  50. package/lib/types/service.d.ts +633 -0
  51. package/lib/types/service.js +1697 -0
  52. package/lib/types/store.d.ts +224 -0
  53. package/lib/types/store.js +916 -0
  54. package/lib/types/types.d.ts +710 -0
  55. package/lib/types/types.js +182 -0
  56. package/locale/en.json +6 -0
  57. package/locale/zh.json +6 -0
  58. package/package.json +128 -0
@@ -0,0 +1,710 @@
1
+ /**
2
+ * Shared vocabulary of `@khorsheed/dsh-reader`: the persisted state document,
3
+ * the Remote wire payloads, and the pure helpers both faces share.
4
+ *
5
+ * Two responsibilities are deliberately split across the process boundary:
6
+ *
7
+ * - the HOST half owns the network (the sanctioned `ctx.web` fetch seam) and
8
+ * the disk (one version-guarded JSON document over `ctx.fs`). It stores the
9
+ * LAST RAW PAYLOAD per source — feed XML or article HTML — plus fetch
10
+ * metadata, and never parses either.
11
+ * - the BROWSER half owns parsing. The host runtime has no XML/DOM parser at
12
+ * all (Node's `globalThis.DOMParser` is absent), so the single parser lives
13
+ * in the browser where `DOMParser` is native, and its output never needs to
14
+ * be persisted: entries and extracted article bodies are session state.
15
+ *
16
+ * @module @khorsheed/dsh-reader/types
17
+ */
18
+ /** What a source is: a syndication feed, or one manually saved article link. */
19
+ export type ReaderSourceKind = 'rss' | 'link';
20
+ /** Fetch state of one source's most recent attempt. */
21
+ export type ReaderFetchStatus = 'ok' | 'fetching' | 'error';
22
+ /**
23
+ * Why a source has no previewable body.
24
+ *
25
+ * A saved link is a link the reader OWNS even when its body cannot be read
26
+ * here, so these are not add failures: they are the reason the card says
27
+ * "link only" and the detail view offers the original instead of a blank
28
+ * page. The set is deliberately small and each member has its own sentence —
29
+ * "collected but not previewable" with no cause is a worse answer than a
30
+ * wrong one, and the three real causes (a bot wall, a login wall, a
31
+ * non-web file) send the reader to different actions.
32
+ */
33
+ export type ReaderPreviewFailureCode =
34
+ /** The site refuses non-browser requests, or answered with a bot challenge. */
35
+ 'blocked'
36
+ /** The target sits behind an authentication wall (institutional proxy, SSO). */
37
+ | 'login'
38
+ /** The response is not a web page at all (a PDF or another file type). */
39
+ | 'unsupported-type'
40
+ /** The address hops to a different site, which the host seam refuses to follow. */
41
+ | 'redirected'
42
+ /** The page yielded nothing to read (an interstitial, an empty shell). */
43
+ | 'empty'
44
+ /** The request never completed: network, timeout, or a 5xx. The retryable one. */
45
+ | 'unreachable'
46
+ /** The address answered with an HTTP error status (404 and friends). */
47
+ | 'http'
48
+ /**
49
+ * The host is KNOWN to be unreadable server-side without a request (a
50
+ * script-rendered app behind an anti-bot wall — OpenReview, measured). The
51
+ * sentence for it carries the way out (paste the arXiv version / open in a
52
+ * browser), so it is not folded into `blocked`, which means "we tried and
53
+ * the site answered a wall".
54
+ */
55
+ | 'unreadable';
56
+ /**
57
+ * Whether an automatic retry is worth the request.
58
+ *
59
+ * Only a transport failure is: a bot wall, a login wall, a PDF and a 404 all
60
+ * answer the same way forever, so retrying them on a timer would be a crawler
61
+ * with a grudge rather than a reader. The manual refresh stays available.
62
+ *
63
+ * @param code - the recorded failure.
64
+ * @returns true when the entry may be retried automatically.
65
+ */
66
+ export declare function isRetryablePreviewFailure(code: ReaderPreviewFailureCode): boolean;
67
+ /** A recorded reason that a source has no previewable body. */
68
+ export interface ReaderPreviewFailure {
69
+ readonly code: ReaderPreviewFailureCode;
70
+ /** The seam's or the classifier's own words, kept for diagnosis. */
71
+ readonly message: string;
72
+ /** When the failure was recorded (ISO-8601). */
73
+ readonly at: string;
74
+ }
75
+ /**
76
+ * One source of entries.
77
+ *
78
+ * A `link` source always holds exactly one entry (the article itself) and is
79
+ * created by pasting a URL; an `rss` source holds however many items its feed
80
+ * declares.
81
+ */
82
+ export interface ReaderSource {
83
+ readonly id: string;
84
+ readonly kind: ReaderSourceKind;
85
+ /** Feed URL (normalized after redirects), or the article URL for a link. */
86
+ readonly url: string;
87
+ /** User-facing name; falls back to the URL's host plus last path segment. */
88
+ readonly label?: string;
89
+ readonly enabled: boolean;
90
+ /** ISO-8601 timestamp of when the user added it. */
91
+ readonly addedAt: string;
92
+ /** ISO-8601 timestamp of the last completed fetch attempt. */
93
+ readonly fetchedAt?: string;
94
+ readonly status?: ReaderFetchStatus;
95
+ /** Human-readable failure of the last attempt, when `status` is `error`. */
96
+ readonly error?: string;
97
+ /**
98
+ * True when the host's fetch seam cut the payload at its size limit
99
+ * (`maxBodyChars`, 100 000 by default). A truncated XML document cannot be
100
+ * parsed and a truncated HTML page yields a partial article, so every
101
+ * consumer treats this as "incomplete" and says so rather than guessing.
102
+ */
103
+ readonly truncated?: boolean;
104
+ /**
105
+ * Why this source has no previewable body, when it has none.
106
+ *
107
+ * A saved link keeps its identity and its URL even when the body cannot be
108
+ * read, so this is recorded instead of the source being dropped: losing a
109
+ * link the reader deliberately saved is a worse outcome than a card that
110
+ * says it can only be opened in a browser.
111
+ */
112
+ readonly failure?: ReaderPreviewFailure;
113
+ /**
114
+ * The URL the reader pasted, when this source was resolved to a canonical
115
+ * version of the same work (a DOI that gated into its arXiv version). The
116
+ * source's own `url` is the resolved one — it is what refreshes fetch — and
117
+ * this keeps the provenance visible.
118
+ */
119
+ readonly resolvedFrom?: string;
120
+ /** The last raw payload: feed XML for `rss`, article HTML for `link`. */
121
+ readonly raw?: string;
122
+ }
123
+ /** The whole persisted document: one JSON file under the plugin's state root. */
124
+ export interface ReaderStateDoc {
125
+ readonly version: 1;
126
+ readonly sources: readonly ReaderSource[];
127
+ readonly refresh: ReaderRefreshConfig;
128
+ /** How long fetched article bodies are served, and how many are kept. */
129
+ readonly cache?: ReaderCachePolicy;
130
+ /** The reader's tag vocabulary, keyed by tag id. */
131
+ readonly tags?: Readonly<Record<string, ReaderTag>>;
132
+ /** ISO-8601 timestamp of the last completed refresh run. */
133
+ readonly lastRefreshAt?: string;
134
+ /**
135
+ * The reader's own annotations, keyed by ENTRY id.
136
+ *
137
+ * Deliberately a separate table from the sources: an entry belongs to a feed
138
+ * that may drop it tomorrow, while a tag the reader put on it is theirs and
139
+ * must outlive the feed's window. It is also the only place a fetched article
140
+ * body can live — the sources hold raw payloads, not per-entry markup.
141
+ */
142
+ readonly annotations?: Readonly<Record<string, ReaderEntryAnnotation>>;
143
+ /**
144
+ * The global sentence memory's manifest; the TABLE lives in its own
145
+ * `bodies/` file.
146
+ *
147
+ * Kept out of the document body on purpose: the table is bounded at 50 000
148
+ * sentences (several MB of JSON), and this document is read and rewritten
149
+ * whole on every commit — including one per article opened (`recordRead`).
150
+ * The manifest is all the commit path needs.
151
+ */
152
+ readonly translationMemory?: ReaderTranslationMemoryManifest;
153
+ /**
154
+ * What the reader opened, newest first (the 「最近阅读」 page).
155
+ *
156
+ * Persisted, unlike the pane's session memory: "what was I reading" is a fact
157
+ * about the reader's own behaviour that has to outlive a reload and a restart,
158
+ * and it holds no third-party text — a title and a URL the reader already
159
+ * asked for. It is NOT the read cursor (that stays per session, in the pane).
160
+ */
161
+ readonly recent?: readonly ReaderRecentEntry[];
162
+ }
163
+ /**
164
+ * One entry the reader opened, as the 「最近阅读」 page lists it.
165
+ *
166
+ * The title and URL are stored rather than looked up because the feed that
167
+ * published the entry may have rolled it out of its window: a recent list that
168
+ * silently drops yesterday's article is not a record of what was read.
169
+ */
170
+ export interface ReaderRecentEntry {
171
+ readonly entryId: string;
172
+ readonly sourceId: string;
173
+ readonly title: string;
174
+ /** The article URL, for reopening an entry the feed no longer publishes. */
175
+ readonly url?: string;
176
+ /** When it was opened (ISO-8601). */
177
+ readonly readAt: string;
178
+ }
179
+ /**
180
+ * What the plugin holds for one entry: a body, a stored raw payload, or why not.
181
+ *
182
+ * `raw` is the state that makes a fetch resumable: the payload is on disk and the
183
+ * browser half has not extracted it yet, so closing the page costs nothing.
184
+ */
185
+ export type ReaderEntryFetchState = {
186
+ readonly state: 'none';
187
+ } | {
188
+ readonly state: 'fetching';
189
+ readonly at: string;
190
+ } | {
191
+ readonly state: 'raw';
192
+ readonly at: string;
193
+ } | {
194
+ readonly state: 'ready';
195
+ readonly at?: string;
196
+ } | {
197
+ readonly state: 'failed';
198
+ readonly at: string;
199
+ readonly message: string;
200
+ readonly code?: ReaderPreviewFailureCode;
201
+ };
202
+ /** The persisted twin of the in-flight/raw half of {@link ReaderEntryFetchState}. */
203
+ export interface ReaderEntryFetchRecord {
204
+ readonly state: 'fetching' | 'raw';
205
+ readonly at: string;
206
+ /** The `bodies/` file holding the raw payload, once it has been written. */
207
+ readonly rawFile?: string;
208
+ /** The payload's character count (the document itself stays out of `state.json`). */
209
+ readonly chars?: number;
210
+ /** Where the payload came from, so an edited URL is detectable. */
211
+ readonly url?: string;
212
+ /** True when the seam capped the payload. */
213
+ readonly truncated?: boolean;
214
+ }
215
+ /** One entry's reader-authored state (see the annotations section below). */
216
+ export interface ReaderEntryAnnotation {
217
+ /** The fetched full text, when one was asked for and arrived. */
218
+ readonly body?: ReaderEntryBody;
219
+ /** Tag ids on this entry, in the order they were applied. */
220
+ readonly tagIds?: readonly string[];
221
+ /** A fetch in flight, or a payload fetched and still waiting to be extracted. */
222
+ readonly fetch?: ReaderEntryFetchRecord;
223
+ /** Why the last fetch failed, so a retry is a decision and not a loop. */
224
+ readonly error?: string;
225
+ /** The classified reason, so the wall can show the same sentence as the detail. */
226
+ readonly failureCode?: ReaderPreviewFailureCode;
227
+ /** When that failure was recorded (ISO-8601). */
228
+ readonly failedAt?: string;
229
+ /**
230
+ * The article's own title, captured at extraction.
231
+ *
232
+ * A saved link's entry is synthesized from the source's URL-derived label;
233
+ * once the body has been extracted the paper's real name lives here, and the
234
+ * wall card prefers it. Deliberately survives the body: past eviction it is
235
+ * the card's memory of what the paper was. Only ever written for a link
236
+ * source's single entry — a feed entry's title is already the publisher's.
237
+ */
238
+ readonly title?: string;
239
+ /** A short excerpt (the abstract / first paragraph), captured with the title. */
240
+ readonly excerpt?: string;
241
+ /**
242
+ * This entry's translated segment map (a hash→translation table, never
243
+ * markup). No TTL: a translation costs a gesture plus per-sentence model work
244
+ * to rebuild, so only the shared translation budget evicts it.
245
+ */
246
+ readonly translation?: ReaderEntryTranslation;
247
+ }
248
+ /** How long a fetched article body is served before the reader is offered a refetch. */
249
+ export interface ReaderCachePolicy {
250
+ /** 0 means "never expires" (the body is kept until the budget evicts it). */
251
+ readonly ttlHours: number;
252
+ /** How many entry bodies may be retained at once (oldest evicted first). */
253
+ readonly maxEntries: number;
254
+ /**
255
+ * How large the two translation tiers may grow together, in characters.
256
+ *
257
+ * Absent means {@link DEFAULT_TRANSLATION_BUDGET_CHARS}. Translations carry no
258
+ * TTL (rebuilding one costs a user gesture plus per-sentence model work, so
259
+ * they must not inherit the body TTL) — this budget, LRU by last use across
260
+ * both tiers, is the only eviction.
261
+ */
262
+ readonly translationBudgetChars?: number;
263
+ }
264
+ /** The default cache policy: a day, and a few hundred articles. */
265
+ export declare const DEFAULT_CACHE_POLICY: ReaderCachePolicy;
266
+ /**
267
+ * The schema version of the persisted translation tiers (the global sentence
268
+ * memory's table, and each entry's segment map). A browser Translator model
269
+ * upgrade quietly changes what the same sentence translates to, and a
270
+ * segmentation change here changes what a "sentence" is — bumping this turns
271
+ * every old record into a MISS. Records are evicted LAZILY (the next write or
272
+ * budget pass drops them), never wiped eagerly.
273
+ */
274
+ export declare const TRANSLATION_STORAGE_VERSION = 1;
275
+ /**
276
+ * The default total budget for both translation tiers, in characters — 64 MB
277
+ * worth of text. Counted in characters, like every other budget in this
278
+ * document (a CJK sentence costs more bytes than that reads; the approximation
279
+ * is the store's existing convention).
280
+ */
281
+ export declare const DEFAULT_TRANSLATION_BUDGET_CHARS: number;
282
+ /** How many sentences the global memory keeps before its own LRU end is evicted. */
283
+ export declare const MAX_TRANSLATION_MEMORY_ENTRIES = 50000;
284
+ /** One remembered sentence translation — the global tier's value. */
285
+ export interface ReaderTranslationMemoryEntry {
286
+ /** The source sentence, verbatim (the hash in the entry's key is over this). */
287
+ readonly source: string;
288
+ /** What the translator answered. */
289
+ readonly target: string;
290
+ /** The LRU clock: when the entry last served a translation (or was written). */
291
+ readonly lastUsedAt: string;
292
+ }
293
+ /** The global memory file's content; the table lives in `bodies/`, not inline. */
294
+ export interface ReaderTranslationMemoryTable {
295
+ readonly version: number;
296
+ readonly entries: Readonly<Record<string, ReaderTranslationMemoryEntry>>;
297
+ }
298
+ /** The document's pointer at the global memory file. */
299
+ export interface ReaderTranslationMemoryManifest {
300
+ readonly version: number;
301
+ /** The `bodies/` file holding the table (a fixed, safe name). */
302
+ readonly file: string;
303
+ readonly entries: number;
304
+ readonly chars: number;
305
+ readonly updatedAt: string;
306
+ }
307
+ /**
308
+ * One entry's translation of its body: a map of sentence hash to translation —
309
+ * never translated markup, so a body re-fetched with minor edits still reuses
310
+ * every unchanged sentence through the global tier, while a `bodyHash`
311
+ * mismatch invalidates only this exact-fit record.
312
+ */
313
+ export interface ReaderEntryTranslation {
314
+ readonly version: number;
315
+ /** The `<src>→<tgt>` label the map was built under. */
316
+ readonly pair: string;
317
+ /** Hash of the normalized body the segments were cut from. */
318
+ readonly bodyHash: string;
319
+ /** sentenceHash → translation, inline while the map is small. */
320
+ readonly segments?: Readonly<Record<string, string>>;
321
+ /** The `bodies/` file holding the segment map once it is large. */
322
+ readonly file?: string;
323
+ /** The map's serialized size, for the shared budget. */
324
+ readonly chars?: number;
325
+ readonly translatedAt: string;
326
+ /** The shared-budget LRU clock. */
327
+ readonly lastUsedAt: string;
328
+ }
329
+ /** One sentence the browser half learned (or was served), as it reports it. */
330
+ export interface ReaderSentenceLearn {
331
+ /** The client's `translationHash(source)`, stored opaquely. */
332
+ readonly hash: string;
333
+ readonly source: string;
334
+ readonly target: string;
335
+ }
336
+ /** An entry translation as the wire serves it: the segment map resolved. */
337
+ export interface ReaderEntryTranslationView {
338
+ readonly pair: string;
339
+ readonly bodyHash: string;
340
+ readonly segments: Record<string, string>;
341
+ }
342
+ /**
343
+ * How much the two caches currently hold, aggregated on the host.
344
+ *
345
+ * Counts and characters only — the tables themselves never cross the wire for
346
+ * a settings readout. `chars` is the store's accounting unit (characters, the
347
+ * same unit the budgets are written in).
348
+ */
349
+ export interface ReaderStorageStats {
350
+ /** The cached article bodies (what TTL + maxEntries govern). */
351
+ readonly bodies: {
352
+ readonly entries: number;
353
+ readonly chars: number;
354
+ };
355
+ /** The two translation tiers: per-entry maps, and the global sentence memory. */
356
+ readonly translations: {
357
+ /** How many entries carry a segment map. */
358
+ readonly entries: number;
359
+ /** The maps' combined size. */
360
+ readonly chars: number;
361
+ /** The global memory's sentence count (0 when no table exists). */
362
+ readonly memoryEntries: number;
363
+ /** The global memory's size (0 when no table exists). */
364
+ readonly memoryChars: number;
365
+ };
366
+ }
367
+ export interface ReaderRefreshConfig {
368
+ readonly enabled: boolean;
369
+ /** Local time of day as `HH:MM`; defaults to `10:00`. */
370
+ readonly timeOfDay: string;
371
+ }
372
+ /**
373
+ * One parsed entry. Produced by the browser half and never persisted.
374
+ *
375
+ * `contentHtml` is the best body the reader could assemble: a feed's
376
+ * `<content:encoded>` / `<content>` when it ships full text, otherwise the
377
+ * article body extracted from the fetched page. It is always the output of the
378
+ * whitelist normalizer, never raw third-party markup.
379
+ */
380
+ export interface ReaderEntry {
381
+ /** Stable id: the feed's guid/id, else the link, else a title+link hash. */
382
+ readonly id: string;
383
+ readonly sourceId: string;
384
+ readonly title: string;
385
+ readonly link?: string;
386
+ readonly author?: string;
387
+ /** ISO-8601 publication timestamp, when the feed declares one. */
388
+ readonly publishedAt?: string;
389
+ readonly tags?: readonly string[];
390
+ /** Short plain-text excerpt for the card. */
391
+ readonly summary?: string;
392
+ /** Normalized article body for the detail view. */
393
+ readonly contentHtml?: string;
394
+ /**
395
+ * True when {@link contentHtml} is only the FEED's own summary.
396
+ *
397
+ * A feed that publishes no `content:encoded` / `<content>` still gets its
398
+ * description rendered in the detail view (an empty page would be worse), so
399
+ * the browser half has to be able to tell "the feed gave me its summary" from
400
+ * "this is the article" — otherwise the entry looks complete and nothing ever
401
+ * fetches the page behind it. The automatic backfill and opening an entry
402
+ * both treat this as a body still owed.
403
+ */
404
+ readonly summaryOnly?: boolean;
405
+ /** True when the body is known to be incomplete (see {@link ReaderSource.truncated}). */
406
+ readonly truncated?: boolean;
407
+ /**
408
+ * Figures the page draws with its own scripts, counted at extraction. Saved
409
+ * links extract at load time, so the count rides the entry; the detail view
410
+ * shows the notice and the 「渲染抓取」 action from it.
411
+ */
412
+ readonly scriptFigures?: number;
413
+ /**
414
+ * True when the ENTRY itself is a salvage of a payload the host capped: its
415
+ * text is real but stops where the cap landed. The detail view says so.
416
+ */
417
+ readonly partial?: boolean;
418
+ }
419
+ /** One source as the browser sees it (no payload: bodies are fetched on demand). */
420
+ export interface ReaderSourceSummary {
421
+ readonly id: string;
422
+ readonly kind: ReaderSourceKind;
423
+ readonly url: string;
424
+ readonly label: string;
425
+ readonly enabled: boolean;
426
+ /**
427
+ * When the reader added it (ISO-8601).
428
+ *
429
+ * The browser needs this to order the wall and the management list by
430
+ * "when did this arrive": a saved link carries no publication date at all,
431
+ * so without this field it sorts as the oldest thing on the wall — which is
432
+ * exactly where a reader who just added it will not look.
433
+ */
434
+ readonly addedAt: string;
435
+ readonly fetchedAt?: string;
436
+ readonly status?: ReaderFetchStatus;
437
+ readonly error?: string;
438
+ readonly truncated?: boolean;
439
+ readonly hasBody: boolean;
440
+ /** Present when this source has no previewable body (see {@link ReaderPreviewFailure}). */
441
+ readonly failure?: {
442
+ readonly code: ReaderPreviewFailureCode;
443
+ readonly message: string;
444
+ };
445
+ /** The URL the reader pasted, when the source was resolved to a canonical version (see {@link ReaderSource.resolvedFrom}). */
446
+ readonly resolvedFrom?: string;
447
+ }
448
+ /** Capability handshake: what the browser may rely on in this composition. */
449
+ export interface ReaderCapabilities {
450
+ readonly protocolVersion: 1;
451
+ /** False when no filesystem is mounted: state lives in memory and is lost on restart. */
452
+ readonly hasFs: boolean;
453
+ /** False when no side-chat service is mounted: the side-chat gesture must hide. */
454
+ readonly hasSideChat: boolean;
455
+ /** ISO-8601 timestamp of the next scheduled refresh, when one is armed. */
456
+ readonly nextRefreshAt?: string;
457
+ /**
458
+ * ISO-8601 timestamp of the last completed refresh run, when there was one.
459
+ * The list shows it so a reader can decide whether the snapshot is stale
460
+ * enough to be worth a fetch.
461
+ */
462
+ readonly lastRefreshAt?: string;
463
+ }
464
+ /** Outcome of a mutating source verb. A domain refusal, never an exception. */
465
+ export type ReaderMutationOutcome = 'ok' | 'not-found' | 'invalid-url' | 'invalid-time' | 'duplicate' | 'unavailable';
466
+ /** What `addSource` decided a pasted URL was. */
467
+ export interface ReaderAddOutcome {
468
+ readonly outcome: 'subscribed' | 'saved-link';
469
+ readonly kind: ReaderSourceKind;
470
+ readonly id: string;
471
+ readonly label: string;
472
+ /**
473
+ * Present when the source was saved but its body cannot be previewed.
474
+ *
475
+ * The add still succeeded: the URL is a link the reader now owns. This is
476
+ * the verdict's reason, not a failure — which is why it rides the outcome
477
+ * instead of becoming one more refusal that loses the link.
478
+ */
479
+ readonly failure?: {
480
+ readonly code: ReaderPreviewFailureCode;
481
+ readonly message: string;
482
+ };
483
+ }
484
+ /**
485
+ * A refusal from `addSource`: the mutation outcomes that make sense for "turn
486
+ * this URL into a source", plus the two fetch-stage refusals that have no
487
+ * meaning for the other verbs. Kept as its own union so the wire type states
488
+ * exactly what this verb can answer.
489
+ */
490
+ export type ReaderAddRefusal = Extract<ReaderMutationOutcome, 'invalid-url' | 'duplicate' | 'unavailable'> | 'unsupported-content' | 'fetch-failed';
491
+ /**
492
+ * The fetch itself failed, with the seam's own reason attached.
493
+ *
494
+ * A separate shape rather than one more string in {@link ReaderAddRefusal}:
495
+ * "the request did not complete" is a different conversation from "that URL
496
+ * cannot be a source", and only the first one has something to show the reader
497
+ * beyond the verdict. Without the reason the UI can only say "failed", which
498
+ * is not a diagnosis.
499
+ */
500
+ export interface ReaderAddFailure {
501
+ readonly outcome: 'fetch-failed';
502
+ /** The fetch seam's message, verbatim — a refused connection, a timeout, a byte cap. */
503
+ readonly reason: string;
504
+ }
505
+ /**
506
+ * One entry the host would like full text for.
507
+ *
508
+ * The host decides WHICH entries need it (it owns the cache, the failures and
509
+ * the policy); the browser decides what the page says (it owns the DOM). This
510
+ * is the split that makes automatic backfill possible at all.
511
+ */
512
+ export interface ReaderBackfillCandidate {
513
+ readonly entryId: string;
514
+ readonly url: string;
515
+ readonly label: string;
516
+ /** Why a previous attempt failed, when one did (and when it is worth retrying). */
517
+ readonly lastError?: string;
518
+ }
519
+ /** Per-source result of a refresh run. */
520
+ export interface ReaderRefreshResult {
521
+ readonly id: string;
522
+ readonly status: 'ok' | 'fetch-failed' | 'truncated' | 'unavailable';
523
+ readonly message?: string;
524
+ }
525
+ /** One raw payload, requested on demand for parsing in the browser. */
526
+ export interface ReaderBody {
527
+ readonly id: string;
528
+ readonly raw?: string;
529
+ readonly truncated?: boolean;
530
+ /** Present when the payload could not be produced (fetch failed, no fs, …). */
531
+ readonly error?: string;
532
+ /**
533
+ * The captured article title for a link source's single entry, when a past
534
+ * extraction stored it (see {@link ReaderEntryAnnotation.title}). The wall
535
+ * prefers it over the URL-derived label.
536
+ */
537
+ readonly title?: string;
538
+ /** The captured excerpt, with the title. */
539
+ readonly excerpt?: string;
540
+ }
541
+ /** Default state root segment under `$DSH_HOME/state`. */
542
+ export declare const STATE_ROOT_SEGMENT = "dsh-reader";
543
+ /** Default daily refresh time. */
544
+ export declare const DEFAULT_REFRESH_TIME = "10:00";
545
+ /**
546
+ * Per-source raw-payload budget in characters.
547
+ *
548
+ * Sized to the HOST's egress cap rather than below it: a deployment can raise
549
+ * `web-fetch-http`'s `maxBodyChars` (the acceptance instance runs it at
550
+ * 2,000,000), and a per-source budget smaller than what the seam is willing to
551
+ * deliver would silently re-truncate every large feed here — the reader would
552
+ * pay for the fetch and still get half a document, with the note blaming the
553
+ * fetch. The fetch cap is the outer bound; this one must not be the inner one.
554
+ */
555
+ export declare const MAX_BODY_CHARS_PER_SOURCE: number;
556
+ /** Total raw-payload budget in characters across all sources (newest kept). */
557
+ export declare const MAX_TOTAL_BODY_CHARS: number;
558
+ /**
559
+ * How large an extracted body may be before it lives in its own file.
560
+ *
561
+ * `state.json` is read and rewritten as a whole on every mutation, so a research
562
+ * paper whose figures are inlined as base64 (transformer-circuits.pub's emotions
563
+ * paper is a 41.8 MB HTML document and extracts to tens of megabytes) would make
564
+ * every reader operation pay a multi-megabyte JSON round trip. Above this
565
+ * threshold the body is written to one file under `bodies/` and the document
566
+ * keeps only its metadata — so the cache policy (`ttlHours`, `maxEntries`) still
567
+ * governs it exactly like an inline body, and eviction deletes the file.
568
+ */
569
+ export declare const INLINE_BODY_MAX_CHARS: number;
570
+ /**
571
+ * Build a display label for a source from its URL: the registrable-ish host
572
+ * plus the last meaningful path segment, which is what distinguishes several
573
+ * feeds from one publisher.
574
+ *
575
+ * @param url - the source URL (any string; a malformed one degrades to its own head).
576
+ * @returns a short human label.
577
+ */
578
+ export declare function defaultSourceLabel(url: string): string;
579
+ /**
580
+ * Build a stable entry id. Feeds are inconsistent about identifiers, so the
581
+ * order is: explicit guid/id, then the entry link, then a hash over title and
582
+ * link together (a feed with neither must still deduplicate across refreshes).
583
+ *
584
+ * @param entry - the identifying fields an entry may carry.
585
+ * @returns an id stable across refreshes of the same feed content.
586
+ */
587
+ export declare function stableEntryId(entry: {
588
+ title: string;
589
+ link?: string;
590
+ guid?: string;
591
+ }): string;
592
+ /**
593
+ * The entry id a saved link's single entry carries.
594
+ *
595
+ * A `link` source is exactly one entry, so the host must be able to name that
596
+ * entry when it records a per-entry annotation (the fetch failure the detail
597
+ * view reads). Both halves use this one function: a magic string duplicated
598
+ * across the process boundary is a bug waiting for a rename.
599
+ *
600
+ * @param sourceId - the link source's id.
601
+ * @returns the entry id the browser will build for it.
602
+ */
603
+ export declare function linkEntryId(sourceId: string): string;
604
+ /**
605
+ * The query that selects every entry of one source KIND.
606
+ *
607
+ * The same local-predicate mechanism as a source or tag filter, so a kind
608
+ * narrowing is visible in the search box and can be cleared there. The prefix
609
+ * cannot collide with a real source id: ids are always `<kind>-<hash>`.
610
+ *
611
+ * @param kind - the source kind to select.
612
+ * @returns the query string.
613
+ */
614
+ export declare function kindQuery(kind: ReaderSourceKind): string;
615
+ /** The source kinds a kind query can name, for the predicate that reads one. */
616
+ export declare const READER_SOURCE_KINDS: readonly ReaderSourceKind[];
617
+ /**
618
+ * Shorten a summary for a card, collapsing whitespace so multi-line feed
619
+ * descriptions do not blow up the layout.
620
+ *
621
+ * @param text - the raw summary or content text.
622
+ * @param maxLength - the maximum returned length, ellipsis included.
623
+ * @returns the collapsed, shortened text, or `undefined` when there is none.
624
+ */
625
+ export declare function summarize(text: string | undefined, maxLength?: number): string | undefined;
626
+ /** Coerce an unknown thrown value into a message string. */
627
+ export declare function errorMessage(error: unknown): string;
628
+ /**
629
+ * One entry's cached article body, keyed by the entry's stable id.
630
+ *
631
+ * A feed may publish full text for some entries and only a summary for others
632
+ * (measured: the OpenAI alignment feed runs 155 to 62,044 characters entry by
633
+ * entry), so this is where the full text of a summary-only entry lives once the
634
+ * reader has asked for it. Keyed by entry id — NOT by the source — because the
635
+ * id is derived from the entry's guid/link and therefore survives every
636
+ * refresh; keying by "position in the feed" would mix entries up on the next
637
+ * fetch.
638
+ */
639
+ export interface ReaderEntryBody {
640
+ /** Whitelist-normalized article markup, when it is small enough to inline. */
641
+ readonly html?: string;
642
+ /**
643
+ * The `bodies/` file holding this body, when it is too large to inline.
644
+ *
645
+ * A bare file name, never a path: the store owns the directory, and a document
646
+ * that carried an absolute path would break the moment the state root moves.
647
+ */
648
+ readonly file?: string;
649
+ /** The body's character count, so the document can report its size without it. */
650
+ readonly chars?: number;
651
+ /**
652
+ * True when this body came from the capture package's rendered fetch rather
653
+ * than the plain fetch. The detail view's 「重新抓取」 routes on it: a
654
+ * rendered entry re-renders (a plain fetch would clobber the rendered body
655
+ * with the page's pre-JS shell).
656
+ */
657
+ readonly rendered?: boolean;
658
+ /** When it was fetched (ISO-8601), for the "fetched just now" line and pruning. */
659
+ readonly fetchedAt: string;
660
+ /**
661
+ * When it stops being served without asking again (ISO-8601).
662
+ *
663
+ * A cached body is an optimization, not an archive: the acceptance decision
664
+ * was 24h, because a reader re-opens an article within the day or not at all.
665
+ * Past the deadline the detail view offers the fetch again rather than
666
+ * silently serving yesterday's copy.
667
+ */
668
+ readonly expiresAt: string;
669
+ /** The URL it was fetched from, so a source edit is detectable. */
670
+ readonly url: string;
671
+ /** True when the fetch hit the seam's cap (the note then says so honestly). */
672
+ readonly truncated?: boolean;
673
+ /** Figures the page draws with its own scripts (dropped; the note counts them). */
674
+ readonly scriptFigures?: number;
675
+ }
676
+ /** One user-defined tag. */
677
+ export interface ReaderTag {
678
+ /** Stable id — renaming a tag must not rewrite every entry that carries it. */
679
+ readonly id: string;
680
+ /** The reader-facing name, unique case-insensitively. */
681
+ readonly name: string;
682
+ readonly createdAt: string;
683
+ }
684
+ /** A mutation on the annotation face, as a bare domain value. */
685
+ export type ReaderAnnotationOutcome = 'ok' | 'not-found' | 'invalid' | 'duplicate' | 'unavailable' | 'empty';
686
+ /**
687
+ * What the pane asks for when it opens one entry.
688
+ *
689
+ * `html` present means the detail view can render immediately. Absent means
690
+ * there is nothing beyond the feed summary and the view should offer the fetch.
691
+ */
692
+ export interface ReaderEntryBodyView {
693
+ readonly entryId: string;
694
+ /** True when this body is a cached fetch rather than the feed's own payload. */
695
+ readonly cached: boolean;
696
+ /** False when the cache's deadline has passed: the view offers the fetch. */
697
+ readonly fresh: boolean;
698
+ /** True when the feed's payload already carries full text for this entry. */
699
+ readonly fromFeed: boolean;
700
+ readonly html?: string;
701
+ readonly fetchedAt?: string;
702
+ readonly truncated?: boolean;
703
+ /** Figures this page draws with scripts — the detail view says so. */
704
+ readonly scriptFigures?: number;
705
+ /** True when this body came from a rendered fetch (the「重新抓取」route reads it). */
706
+ readonly rendered?: boolean;
707
+ /** Why a fetch could not produce a body, when one was attempted. */
708
+ readonly error?: string;
709
+ }
710
+ //# sourceMappingURL=types.d.ts.map