dsh-plugin-shop 0.8.3 → 0.8.4-beta.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.
@@ -6,6 +6,10 @@ export declare const zh: {
6
6
  error: string;
7
7
  installedError: string;
8
8
  retry: string;
9
+ /** The shop's own error boundary caught a render error; the error's message
10
+ * follows on its own line, and Retry remounts the tab (design
11
+ * 2026-09-26-market-borrowings §4). */
12
+ tabCrashed: string;
9
13
  search: string;
10
14
  all: string;
11
15
  catalog: string;
@@ -35,12 +39,29 @@ export declare const zh: {
35
39
  installedRestartNotice: string;
36
40
  incompatibleBadge: string;
37
41
  incompatibleDetail: string;
42
+ /** The author's own `dsh.compatibility`, unmet on this machine (design
43
+ * 2026-09-01 §8.2). Each names BOTH sides — what the author declared and
44
+ * what this dsh runs — because the host sends both, and a bare
45
+ * "unsupported" leaves the reader guessing which side is off. */
46
+ harnessRangeDetail: string;
47
+ harnessProfileDetail: string;
48
+ /** dsh's OWN refusal (0.1.7 on), not the author's claim: the install button
49
+ * is disabled beside it (design 2026-09-26-dsh-017-readiness, B1). `{peers}`
50
+ * is each refused peer with the range the package declared. The remedy
51
+ * leads into the exemption command, rendered on its own line after it, and
52
+ * names the Refresh button by its label. */
53
+ harnessPeersDetail: string;
54
+ harnessPeersRemedy: string;
38
55
  hotNoPatchNotice: string;
39
56
  hotNotSimpleNotice: string;
40
57
  hotHostUnsupportedNotice: string;
41
58
  hotTimeoutNotice: string;
42
59
  hotMountFailedNotice: string;
43
- hotClientHalfNotice: string;
60
+ /** No mount was tried: this dsh already loaded the package once, and Node
61
+ * will not load a second copy from the same place (design
62
+ * 2026-09-26-market-borrowings §1). Covers an update and a reinstall alike,
63
+ * so it names the new files rather than a "new version". */
64
+ hotAlreadyLoadedNotice: string;
44
65
  installedNoRestartNotice: string;
45
66
  installedReloadNotice: string;
46
67
  reload: string;
@@ -65,6 +86,9 @@ export declare const zh: {
65
86
  /** The incompatible filter's label. FIXED — the switch beside it carries the
66
87
  * state, so the words name what the switch does and never flip with it. */
67
88
  hideIncompatible: string;
89
+ /** What the filter takes away is every card whose badge reads 不兼容 —
90
+ * missing components OR an author's declaration this dsh does not meet —
91
+ * so these three name the badge, not one of its reasons. */
68
92
  incompatibleFilterTitle: string;
69
93
  showIncompatibleTitle: string;
70
94
  emptyIncompatibleFiltered: string;
@@ -79,6 +103,7 @@ export declare const zh: {
79
103
  tarballIntegrityCode: string;
80
104
  ambiguousIdentityCode: string;
81
105
  nameTakenCode: string;
106
+ desktopProfileCode: string;
82
107
  nameTakenBadge: string;
83
108
  nameTakenDetail: string;
84
109
  npmSource: string;
@@ -101,7 +126,14 @@ export declare const zh: {
101
126
  restartConfirm: string;
102
127
  restarting: string;
103
128
  restartFailedNotice: string;
129
+ /** The same failure, when the host said where the new process writes —
130
+ * which is where a boot that died explains itself (design
131
+ * 2026-09-26-market-borrowings §3). "Or did not stay up": the page now
132
+ * waits for the new server to keep answering, so this also covers a boot
133
+ * that answered and then died. */
134
+ restartFailedLogNotice: string;
104
135
  restartTransportFailed: string;
136
+ restartBlockedDesktopNotice: string;
105
137
  restartBlockedWindowsNotice: string;
106
138
  restartBlockedSystemdNotice: string;
107
139
  restartBlockedPortZeroNotice: string;
@@ -124,6 +156,7 @@ export declare const en: {
124
156
  error: string;
125
157
  installedError: string;
126
158
  retry: string;
159
+ tabCrashed: string;
127
160
  search: string;
128
161
  all: string;
129
162
  catalog: string;
@@ -153,12 +186,16 @@ export declare const en: {
153
186
  installedRestartNotice: string;
154
187
  incompatibleBadge: string;
155
188
  incompatibleDetail: string;
189
+ harnessRangeDetail: string;
190
+ harnessProfileDetail: string;
191
+ harnessPeersDetail: string;
192
+ harnessPeersRemedy: string;
156
193
  hotNoPatchNotice: string;
157
194
  hotNotSimpleNotice: string;
158
195
  hotHostUnsupportedNotice: string;
159
196
  hotTimeoutNotice: string;
160
197
  hotMountFailedNotice: string;
161
- hotClientHalfNotice: string;
198
+ hotAlreadyLoadedNotice: string;
162
199
  installedNoRestartNotice: string;
163
200
  installedReloadNotice: string;
164
201
  reload: string;
@@ -189,6 +226,7 @@ export declare const en: {
189
226
  tarballIntegrityCode: string;
190
227
  ambiguousIdentityCode: string;
191
228
  nameTakenCode: string;
229
+ desktopProfileCode: string;
192
230
  nameTakenBadge: string;
193
231
  nameTakenDetail: string;
194
232
  npmSource: string;
@@ -211,7 +249,9 @@ export declare const en: {
211
249
  restartConfirm: string;
212
250
  restarting: string;
213
251
  restartFailedNotice: string;
252
+ restartFailedLogNotice: string;
214
253
  restartTransportFailed: string;
254
+ restartBlockedDesktopNotice: string;
215
255
  restartBlockedWindowsNotice: string;
216
256
  restartBlockedSystemdNotice: string;
217
257
  restartBlockedPortZeroNotice: string;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The browser half of the peer verdict (design 2026-09-01-harness-compatibility
3
+ * §9): a declared peer is missing only if NEITHER the host NOR this page's
4
+ * module table provides it.
5
+ *
6
+ * The host judges each entry's peers by node resolution from the profile
7
+ * (`host/peers.ts`), and that answers only half of how a client plugin's
8
+ * `require(x)` is served. The module table in `@deepseek-ai/dsh-client-modules`
9
+ * answers in this order: a platform SEED word; an already materialized module
10
+ * (`loadCache`); a graph row (`manifest.modules` — a `dsh.client` package in
11
+ * this profile's boot graph); a registered factory; otherwise it throws. Seed
12
+ * words never touch node resolution, and of the nine the 0.1.5-rc.3 web shell
13
+ * seeds only `@deepseek-ai/cordis` exists as a package on disk — so the host
14
+ * reports the other eight missing for every plugin that declares them, which
15
+ * on the live catalog made most of the host's badges false — `react` among
16
+ * them, the very module this shop's own client runs with. Design
17
+ * 2026-09-01-harness-compatibility §9.1 owns the measurement.
18
+ *
19
+ * So the host's list is never rendered as it arrives: every name the live
20
+ * table provides is removed first (`index.ts`, at the moment a result is
21
+ * handed to the tab). A pure core, `refineIncompatible`, driven by any oracle;
22
+ * and a thin shell, `moduleTableOracle`, over the harness's live object.
23
+ */
24
+ /** What the module table says about one specifier: `true` provided, `false`
25
+ * absent, `null` unknown. Injected, so fixtures drive every verdict and only
26
+ * one oracle ever touches the harness. */
27
+ export type ModuleOracle = (spec: string) => Promise<boolean | null>;
28
+ /**
29
+ * How long one probe may take before its answer counts as unknown.
30
+ *
31
+ * The work a probe can actually reach is a microtask: a seed-word lookup, or a
32
+ * factory this page already registered. So this bounds a harness that never
33
+ * answers, not work — and it exists because the cost of the alternative is the
34
+ * whole shelf. `refineAgainstModuleTable` promises that a table in any state
35
+ * cannot cost the reader the catalog, and an `import()` that never settles
36
+ * would have made that promise false by leaving the tab with nothing to render
37
+ * (§9's degradation rule: an unavailable fact is silence, never an accusation).
38
+ * A probe per name, not one deadline for the whole refinement: every other
39
+ * name is still judged, and the tab waits at most this long however many names
40
+ * there are.
41
+ */
42
+ export declare const MODULE_PROBE_TIMEOUT_MS = 2000;
43
+ /**
44
+ * The host's verdict less every name the oracle provides.
45
+ *
46
+ * An entry whose list empties is dropped, and an entry ANY of whose names
47
+ * comes back unknown gets no verdict at all — the rule `incompatibilityMap`
48
+ * follows on the host, for the host's reason: an unavailable fact must never
49
+ * read as an accusation, and keeping the rest of a list whose one unknown name
50
+ * was dropped would publish a partial answer as the whole one. Each distinct
51
+ * name is asked once, however many entries declare it. Never rejects: any
52
+ * failure of the oracle is an unknown.
53
+ */
54
+ export declare function refineIncompatible(incompatible: Readonly<Record<string, readonly string[]>>, provides: ModuleOracle, timeoutMs?: number): Promise<Record<string, string[]>>;
55
+ /**
56
+ * An oracle over the live `modules` service, or null when the page offers no
57
+ * usable table.
58
+ *
59
+ * The service is the harness's `ClientModuleLoader`, published to client
60
+ * plugins as `ctx.reflect.provide('modules', …)`. It arrives as unknown-typed
61
+ * foreign state, so only its PUBLIC contract is read, and only after
62
+ * narrowing: `manifest.modules` must be an array, `loadCache` a Map and
63
+ * `import` a function. `seed`, `factories` and `graphRows` exist on the
64
+ * instance too, but they are TypeScript-`private` in the harness's own
65
+ * declarations, and nothing here names them.
66
+ *
67
+ * Per spec, in the table's own order but with every side effect kept out:
68
+ * 1. A graph row provides it, and `import` is NOT called — for a row it
69
+ * fetches the bundle and runs the module body, and a probe must never be
70
+ * what loads a plugin. `manifest.modules` is the row index, but it is not
71
+ * fixed for the page's whole life on every harness build. On 0.1.5-rc.3
72
+ * `this.manifest = options.manifest` is the constructor's only write, so a
73
+ * row placed at boot stands unchanged for as long as the page lives. On
74
+ * 0.1.7-rc.2 (`next`) `updateManifest` REPLACES `this.manifest` outright
75
+ * with a new manifest that no longer lists an uninstalled package, and
76
+ * `reconcile` removes the plugin from the loader — so UNTIL this page's own
77
+ * uninstall reconciles, the OLD row still stands in `manifest.modules` and
78
+ * this step clears the entry falsely; AFTER it reconciles, this step can no
79
+ * longer be the false clear, because `manifest.modules` has already dropped
80
+ * the package. The false clear then comes from `prune`, which keeps the
81
+ * removed id only in the PRIVATE `graphRows` and in `loadCache`, and only
82
+ * while some retained module still references it — that is step 2 below,
83
+ * or the side effect of `import` in step 3 (a private graph row this
84
+ * function cannot see).
85
+ * 2. A `loadCache` record provides it, again without `import`.
86
+ * 3. Otherwise `import(spec, '', {})`, with the spec as written — the seed
87
+ * holds `react-dom/client` itself, not its stripped form. Resolving means a
88
+ * seed word, whose lookup has no side effect; rejecting with the
89
+ * "cannot resolve" message means absent; any other rejection is unknown.
90
+ *
91
+ * A kept-but-unmaterialized row is this function's own remaining blind spot:
92
+ * asked about here, it still reaches step 3's `import` probe, which fetches
93
+ * and runs it — the one side effect this module promises never to cause on
94
+ * its own account. `refineAgainstModuleTable`'s page-removed set is the fix
95
+ * for the one case a page can know about on its own — a name uninstalled
96
+ * through THIS shop, on THIS page, since it loaded — by bypassing this oracle
97
+ * for that name entirely rather than trusting a row that may only be kept. An
98
+ * uninstall made from another tab, another window, or the CLI is not in that
99
+ * set and stays unknown to this page until it reloads.
100
+ *
101
+ * One side-effect path remains: a registered page-local factory that is
102
+ * neither a graph row nor yet materialized, which `import` materializes. That
103
+ * is reachable only for a name the host could not resolve AND the page
104
+ * registered without a row — the shell's own page-local modules are
105
+ * node-resolvable, so the host never lists them. And what it does is what the
106
+ * first real `require` of that name does anyway: run a factory this page
107
+ * registered precisely so it would be materialized, once, memoized in
108
+ * `loadCache`, fetching nothing. It moves that moment earlier; it runs nothing
109
+ * the page did not already hold.
110
+ */
111
+ export declare function moduleTableOracle(service: unknown): ModuleOracle | null;
112
+ /**
113
+ * The host's verdict as this page can stand behind it: refined against the
114
+ * live table, or `{}` — no verdicts at all — when there is no usable table.
115
+ *
116
+ * Never the host's list unrefined. Measured, that list is majority-false on
117
+ * this harness line (see the header), so showing it whenever the table cannot
118
+ * be read would hand the reader a verdict already known to be unreliable;
119
+ * silence is this project's documented degradation — an unavailable fact must
120
+ * never read as an accusation. Never rejects, so a table in any state cannot
121
+ * cost the reader the catalog.
122
+ *
123
+ * `removed` is the page-removed set (design 2026-09-01-harness-compatibility
124
+ * section 9.1): package names this page has uninstalled since it loaded.
125
+ * Every name in it is reported missing exactly
126
+ * as the host said, WITHOUT ever reaching the table — not the rows check, not
127
+ * `loadCache`, not `import` — because a row or a cache record can outlive a
128
+ * restart-free uninstall (`moduleTableOracle`'s header), and the table's own
129
+ * "provided" would be precisely the false clear this set exists to prevent.
130
+ */
131
+ export declare function refineAgainstModuleTable(incompatible: Readonly<Record<string, readonly string[]>>, service: unknown, removed?: ReadonlySet<string>, timeoutMs?: number): Promise<Record<string, string[]>>;
@@ -5,7 +5,7 @@ export { isShopLike } from '../shared/shop-like.ts';
5
5
  export { identityKey, type EntryIdentity } from '../shared/identity.ts';
6
6
  import { type EntryIdentity } from '../shared/identity.ts';
7
7
  import type { ShopLocaleKey } from './locales.ts';
8
- import type { CatalogEntry, HotRestartReason, InstallRejectionCode, RestartBlockedReason } from '../host/index.ts';
8
+ import type { CatalogEntry, HarnessVerdict, HotRestartReason, InstallRejectionCode, RestartBlockedReason } from '../host/index.ts';
9
9
  import type { Activation } from '../host/activation.ts';
10
10
  import { type InstallState } from '../shared/install-state.ts';
11
11
  export type { Activation };
@@ -50,6 +50,105 @@ export declare function tierKey(tier: CatalogEntry['tier']): ShopLocaleKey;
50
50
  * both rendered as no warning at all. The map is keyed by install identity,
51
51
  * never by name, because two entries can share a name. */
52
52
  export declare function missingPeersOf(incompatible: Record<string, string[]>, key: string): string[];
53
+ /** The author's own `dsh.compatibility` that this installation does not meet
54
+ * (design 2026-09-01 §8.2), or undefined when the host gave no verdict. Keyed
55
+ * by install identity, never by name, for the reason `missingPeersOf` is.
56
+ *
57
+ * The map itself may be absent, whatever the result type says: a tab can
58
+ * outlive a shop self-update by one reload and then be talking to the OLD
59
+ * in-process host until dsh restarts, which answers the shape from before the
60
+ * key existed. That is "no verdict", never a crash. */
61
+ export declare function harnessVerdictOf(verdicts: Readonly<Record<string, HarnessVerdict>> | undefined, key: string): HarnessVerdict | undefined;
62
+ /**
63
+ * One thing standing between an entry and a working install, carrying the
64
+ * facts its copy names — never the copy, which the tab localizes (§4: no copy
65
+ * crosses the RPC).
66
+ *
67
+ * Refusals and warnings, each with its own remedy. Two are refusals, and
68
+ * disable the button (`refusesInstall`): a taken name, which the host WILL
69
+ * refuse and the reader fixes by uninstalling a plugin; and harness peers the
70
+ * running dsh WILL refuse (0.1.7 on), fixed by dsh's exact-version exemption,
71
+ * whose command the host sends when dsh would accept one. Everything else is
72
+ * about this harness and is advisory — the host installs anyway: components
73
+ * node resolution and the page's module table both lack, and an author's
74
+ * declared range or profile list this dsh falls outside of.
75
+ */
76
+ export type Blocker = {
77
+ kind: 'name-taken';
78
+ holder: string;
79
+ } | {
80
+ kind: 'harness-peers';
81
+ refused: Readonly<Record<string, string>>;
82
+ running: string;
83
+ allowCommand: string | null;
84
+ } | {
85
+ kind: 'missing-peers';
86
+ modules: readonly string[];
87
+ } | {
88
+ kind: 'harness-range';
89
+ range: string;
90
+ running: string;
91
+ } | {
92
+ kind: 'harness-profile';
93
+ declared: readonly string[];
94
+ running: string;
95
+ };
96
+ export type BlockerKind = Blocker['kind'];
97
+ /**
98
+ * Everything standing in one entry's way, in the order every surface renders
99
+ * it — the card's detail lines, the badge's accessible name, the outdated
100
+ * row's refusal — and the one list the incompatible filter reads, through
101
+ * `readsIncompatible`, and the install button, through `refusesInstall`.
102
+ *
103
+ * The refusals read first. The name conflict leads: it is about a plugin the
104
+ * reader chose and would lose. Then dsh's own refusal, then what this machine
105
+ * is measured to lack, then what the author declared, range before profiles —
106
+ * the order the host's verdict carries its halves.
107
+ */
108
+ export declare function blockersOf(missing: readonly string[], harness: HarnessVerdict | undefined, nameTakenBy: string | undefined): Blocker[];
109
+ /**
110
+ * The badge's visible word for these blockers, or null when there are none.
111
+ *
112
+ * "Incompatible" for anything about this harness, whichever reason put it
113
+ * there. A taken name is the more serious condition and names a different
114
+ * remedy, so it decides the word whenever it holds — the card is perfectly
115
+ * compatible, and a different plugin is in the way.
116
+ */
117
+ export declare function blockerBadgeKey(blockers: readonly {
118
+ kind: BlockerKind;
119
+ }[]): ShopLocaleKey | null;
120
+ /** Whether the badge reads "Incompatible" — exactly the set the incompatible
121
+ * filter counts and takes away. Derived from the badge's own rule, never
122
+ * restated beside it: the filter used to test the peer list by hand, which
123
+ * held only while missing peers were the one reason for the word — a card
124
+ * badged for its declared range would have read "Incompatible" and survived
125
+ * the filter that names it. */
126
+ export declare function readsIncompatible(blockers: readonly {
127
+ kind: BlockerKind;
128
+ }[]): boolean;
129
+ /**
130
+ * Whether this install is refused before it starts, so its button is
131
+ * disabled rather than opening the acknowledgement gate for an install that
132
+ * cannot proceed: by the host, for a taken name, or by the running dsh, for
133
+ * its harness peers.
134
+ *
135
+ * The second is the one harness reason that blocks (design
136
+ * 2026-09-26-dsh-017-readiness, B1, amending "warn, never block"): dsh 0.1.7
137
+ * refuses the install itself, before pnpm runs, and the shop cannot install
138
+ * past it. The verdict is dsh's own rule on the profile's own exemptions, so
139
+ * a disabled button is dsh's answer given early, never the shop's opinion —
140
+ * and recording the exemption the card shows is what enables it again.
141
+ */
142
+ export declare function refusesInstall(blockers: readonly {
143
+ kind: BlockerKind;
144
+ }[]): boolean;
145
+ /**
146
+ * The refused peers as the refusal's copy names them: each peer and the range
147
+ * the package declared, in the order dsh reported them. A blank range — which
148
+ * dsh refuses — is quoted, or the sentence would name the peer and nothing
149
+ * it asked for.
150
+ */
151
+ export declare function refusedPeersText(refused: Readonly<Record<string, string>>): string;
53
152
  /**
54
153
  * The plugin already holding this entry's bundle name, if a DIFFERENT one is,
55
154
  * named the way the host's refusal names it.
@@ -160,8 +259,33 @@ export declare const INSTALL_POLL_MS = 1000;
160
259
  * the NEW server, never the dying old one. */
161
260
  export declare const RESTART_GRACE_MS = 3000;
162
261
  /** §8 restart handoff: how long the client waits for the new server to
163
- * answer before reporting the manual restart command. */
262
+ * START answering before reporting the manual restart command. A run of good
263
+ * answers that began inside it may finish after it (see
264
+ * `restartMonitorVerdict`). */
164
265
  export declare const RESTART_WAIT_MS = 30000;
266
+ /** §8 restart handoff: how long the new server must keep answering before the
267
+ * page reloads into it (design 2026-09-26-market-borrowings §3). The Loader
268
+ * mounts entries concurrently, so a boot that is about to fail can bind the
269
+ * port and answer before the settled tree is audited and the process exits;
270
+ * dsh-market measured that window inside 8 s. */
271
+ export declare const RESTART_STABLE_MS = 8000;
272
+ /**
273
+ * The restart monitor's decision after a probe of the origin.
274
+ *
275
+ * `stableSinceMs` is when the current unbroken run of good answers began —
276
+ * null when the last probe failed, or none has succeeded yet — and both times
277
+ * count from the moment the restart was committed. One answer is not enough:
278
+ * reloading on it is what landed a reader on a boot that died a moment later,
279
+ * a blank page in place of the notice naming the log. So the page reloads
280
+ * only once the run has lasted `RESTART_STABLE_MS`, and the monitor gives up
281
+ * only when no run is under way past `RESTART_WAIT_MS` — which bounds the
282
+ * whole wait at the sum of the two, since a run that began in time either
283
+ * completes or breaks after the deadline.
284
+ */
285
+ export declare function restartMonitorVerdict(input: {
286
+ elapsedMs: number;
287
+ stableSinceMs: number | null;
288
+ }): 'wait' | 'reload' | 'failed';
165
289
  /** How long the check-update button reports "up to date" after a re-check
166
290
  * finds no newer release, before reverting to its idle label. */
167
291
  export declare const CHECK_UP_TO_DATE_MS = 3000;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The restart handoff's browser half (design §8), held by the page rather
3
+ * than by the tab that confirmed the restart.
4
+ *
5
+ * It lived in a RestartPanel effect until 2026-09-27, and a self-update
6
+ * killed it. dsh's client HMR (`@deepseek-ai/dsh-client-hmr`, mounted by the
7
+ * web bundle unconditionally) stat-polls every client bundle and hot-swaps
8
+ * one whose file changed; a self-update rewrites this package's own
9
+ * `lib/client.js`, so the swap disposes the tab's fiber and mounts a fresh
10
+ * instance of a freshly imported module. The effect's cleanup then stopped
11
+ * the probe, and once the restart finished nothing reloaded the page: it sat
12
+ * on the old process's answers while dsh's socket quietly reconnected to the
13
+ * new one. Measured on dsh 0.1.5-rc.3 updating 0.8.2 to 0.8.3; with the
14
+ * `/plugins/events` channel blocked the same flow reloaded.
15
+ *
16
+ * So the state sits on `globalThis` under a registry symbol — the one thing a
17
+ * swapped-in module instance shares with the one it replaced — and the probe
18
+ * loop is plain timers owned by no fiber. Any tab reads the state, so the
19
+ * replacement renders the restart under way instead of offering another.
20
+ */
21
+ /** A restart this page committed: still waiting for the new server, or
22
+ * given up on it. `logFile` is where the host said the new process writes;
23
+ * a host older than that field sends none. */
24
+ export type RestartMonitorState = {
25
+ kind: 'restarting';
26
+ logFile?: string;
27
+ } | {
28
+ kind: 'failed';
29
+ logFile?: string;
30
+ };
31
+ /** The page's restart, or null when it has committed none. The same object
32
+ * until the state changes, as `useSyncExternalStore` requires. */
33
+ export declare function readRestartMonitor(): RestartMonitorState | null;
34
+ /** Be told when the page's restart changes state. */
35
+ export declare function subscribeRestartMonitor(listener: () => void): () => void;
36
+ /**
37
+ * Watch the origin for the restarted server and reload into it: probe once
38
+ * the grace period is over (the host exits within it, so an answer is the NEW
39
+ * server), and reload only after it has KEPT answering for the stable window.
40
+ * A boot that is about to fail can bind the port and answer before its plugin
41
+ * tree is audited and it exits; reloading on that one answer left the reader
42
+ * on a blank page instead of the notice naming the log (design
43
+ * 2026-09-26-market-borrowings §3). `restartMonitorVerdict` decides; this
44
+ * loop only probes. Probes never overlap: each is scheduled one poll interval
45
+ * after the previous one settled.
46
+ *
47
+ * A restart already being watched is left alone — every offer on the page
48
+ * renders this state, so a second press can only come from a panel that has
49
+ * not re-rendered yet, and a second loop would probe the same origin twice.
50
+ */
51
+ export declare function startRestartMonitor(options: {
52
+ reload: () => void;
53
+ logFile?: string;
54
+ }): void;
55
+ /** Stop the probe loop and forget the page's restart. Nothing in the page
56
+ * calls this — a committed restart ends in a reload or a failure — so its
57
+ * caller is a test isolating one case from the next. */
58
+ export declare function resetRestartMonitor(): void;
@@ -35,18 +35,21 @@ export type Activation = 'live' | 'reload' | 'restart';
35
35
  * - A change to the BOOT COMPOSITION — a toggle, an uninstall — moves an
36
36
  * entry the registry already enumerates, and the served graph follows it
37
37
  * within seconds (§2, measured 2026-09-11). `true`.
38
- * - A HOT MOUNT — an install or an update — adds to the live loader entries
39
- * without entering that composition, so the graph a tab reloads into does
40
- * not contain the package. Measured 2026-09-14 against dsh 0.1.5-rc.1 in
41
- * `web-full-flow.e2e.ts`: across a reload following a hot mount the graph
42
- * is byte-identical, same `rev`, while the package's host half is live the
43
- * whole time. `false` — and a `restart` is then the only honest answer,
44
- * because there is nothing a reload could fetch.
38
+ * - A HOT MOUNT — an install — adds entries under the shop's own loader
39
+ * entry, which the registry enumerates like any other: across a reload
40
+ * following the mount the served graph gains the package and the page runs
41
+ * its browser half (measured 2026-09-26 on 0.1.5-rc.3 and 0.1.7-rc.2, in
42
+ * `web-full-flow.e2e.ts`). `true`.
45
43
  *
46
44
  * The asymmetry that decides every unknown still runs the same way: offering
47
45
  * a step that was not needed costs the reader one action, withholding one
48
- * that was needed is the defect this module exists to fix. What changed on
49
- * 2026-09-14 is which step is the needed one after a hot mount.
46
+ * that was needed is the defect this module exists to fix. Which step a hot
47
+ * mount needs has been measured twice. On 2026-09-14 the graph came back
48
+ * byte-identical across the reload, and installs moved to `restart` — but the
49
+ * shop then registered the tree from whichever context made the RPC call, the
50
+ * typert gateway's, and the registry never composed a tree hung there. Once
51
+ * the tree hung off the shop's own entry (2026-09-26), the same measurement
52
+ * found the package in the graph, and installs moved back to `reload`.
50
53
  */
51
54
  export declare function activationOf(input: {
52
55
  hostLive: boolean;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The patch files a bundle declares, read the way dsh reads them (design
3
+ * 2026-09-26-dsh-017-readiness, B4).
4
+ *
5
+ * dsh 0.1.7's app-boot (`bundlePatchFiles`) accepts `dsh.bundle.patch` as one
6
+ * package-relative file or as a list of them, applied in order, and throws on
7
+ * anything else. dsh 0.1.5 reads only the string form: it joins the
8
+ * declaration onto the package directory, so a list makes the profile fail to
9
+ * start. What the RUNNING dsh accepts is `RunningHarness.patchLists`; this
10
+ * reads the declaration itself, for the shop's own readers of a package's
11
+ * patch — the entries it owns, the rows a hot mount replicates.
12
+ */
13
+ /**
14
+ * The declared patch files in application order: one for a string, the
15
+ * listed ones for a list of strings. Null for a declaration dsh refuses —
16
+ * neither a path nor a list of paths — which is distinct from an absent one:
17
+ * only absence means the conventional `cordis.patch.yml`, and a caller that
18
+ * fell back to it for a refused declaration would act on a patch dsh never
19
+ * applies. Pure, and the list is a copy.
20
+ */
21
+ export declare function bundlePatchFiles(declared: unknown): string[] | null;
22
+ /** Why a bundle patch declaration would stop the profile from starting:
23
+ * `list-unsupported` is a list on a dsh that reads one file, `malformed` a
24
+ * declaration no dsh loads. */
25
+ export type PatchHazard = 'list-unsupported' | 'malformed';
26
+ /**
27
+ * Whether an installed package's `dsh.bundle.patch` would stop the profile
28
+ * from starting at the next boot, given whether the running dsh reads a list
29
+ * (`RunningHarness.patchLists`). An absent declaration is no hazard: dsh adds
30
+ * a package to the profile's bundles only when it declares a patch, so
31
+ * nothing reads the field. An unknown harness (null) forms no verdict on a
32
+ * list — the rule for every harness read — but a malformed declaration is
33
+ * refused by every dsh, so it is named whatever is known. Pure.
34
+ */
35
+ export declare function patchDeclarationHazard(declared: unknown, patchLists: boolean | null): PatchHazard | null;
@@ -5,9 +5,15 @@ import type { CatalogEntry, DeniedEntry } from './types.ts';
5
5
  * 3 adds `source` and repo entries (github install channel); 4 adds `subdir`
6
6
  * for monorepo-subpackage entries (2026-08-31 hub-borrowings A); 5 adds
7
7
  * `added`, `tarball` (release rescue), the `theme` category, and
8
- * `denied[].replacement` (2026-08-31 market borrowings);
9
- * 6 adds `peers`, the package's declared peer dependency names
10
- * (2026-09-01 harness compatibility). */
8
+ * `denied[].replacement` (2026-08-31 market borrowings).
9
+ *
10
+ * 6 adds nothing. It was reserved for `peers` (2026-09-01 harness
11
+ * compatibility), and that gate came off on 2026-09-03 without ever being
12
+ * opened: `peers` rides 5, as every additive optional key does, because this
13
+ * schema strips a key it does not know while a higher version NUMBER is what
14
+ * a capped client refuses outright. Every shop built since this constant
15
+ * became 6 accepts a 6 without knowing what one would mean, so a change those
16
+ * shops must refuse has to be 7. */
11
17
  export declare const SUPPORTED_SCHEMA_VERSION = 6;
12
18
  export interface CatalogSnapshot {
13
19
  schemaVersion: number;