@deepseek-ai/dsh-client-ui-slots 0.0.1-rc.2 → 0.0.1-rc.3

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/README.i18n.yaml CHANGED
@@ -3,4 +3,4 @@
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-slots/README.md
5
5
  README.md: 7ee2a3d22c2a41b0e5c356c52d383a047cf99029
6
- README.zh.md: 793bb2d1f77e02e34a41fab16b2c2c64b78744dc
6
+ README.zh.md: 0a61ec43d575429616ad5658228d1e9c93ef8462
package/README.zh.md CHANGED
@@ -4,22 +4,22 @@
4
4
 
5
5
  Slot 注册表纯核心、slot 终端设计:SlotMap 声明合并、SlotCore 上唯一的 `register` 组合 API、四 share 组件 props 类型家族、store seat 类型家族,以及 renderer 安装约定。只使用 React 类型;该包不依赖 React,也不依赖 Cordis。
6
6
 
7
- 一次 `register({ name, children?, store?, inject?, ...kind }, Component)` 调用会向已声明 slot 贡献一个组件,同时声明子 slot(声明 = 渲染授权 = 运行时规范,三者共用一张表)、store seat 以及注册方的业务表层。组件会在调用点依据 `ComposedProps` 接受检查;该类型是四个 share 的交集,每个 share 都从各自的唯一真源派生:
7
+ 一次 `register({ name, children?, store?, inject?, ...kind }, Component)` 调用会向已声明 slot 贡献一个组件,同时声明子 slot(声明 = 渲染授权 = 运行时规范,三者共用一张表)、store seat 以及注册方的业务表层。组件会在调用点依据 `ComposedProps` 接受类型检查;该类型是四个 share 的交集,每个 share 都从各自的唯一真源派生:
8
8
 
9
9
  | share | 类型 | 来源 |
10
10
  |---|---|---|
11
- | runtime | `PropsRuntime<K>` | SlotMap 条目:`owner`(父级 renderSlot 调用点)+ Session 标准工具包 + 全局 seat |
11
+ | 运行时 | `PropsRuntime<K>` | SlotMap 条目:`owner`(父级 renderSlot 调用点)+ 会话标准工具包 + 全局 seat |
12
12
  | child render | `PropsRenderSlots<S>` | register 调用的 `children` key 集合(静态缩窄的 `renderSlot`) |
13
- | store | `PropsStore<H>` | 已声明 handle:`useStore` selector hook + 移除 draft 的 `actions` |
13
+ | store | `PropsStore<H>` | 已声明 handle:`useStore` selector 钩子 + 移除 draft 的 `actions` |
14
14
  | business | `I` | 从 `inject` factory 返回值推断 |
15
15
 
16
16
  chain-kind slot 会反转键控路由:条目自行提名,而不是由分发点选择 `entryKey`。每次注册都携带一个纯 `ChainSelect` selector(另有可选的升序 `priority`,相同值按注册顺序处理);第一个非 null 返回值选中其条目,并成为组件的 `matched` prop;全部返回 null 时则使用 owner 的 `renderSlotChain` fallback(`ChainRenderOpts`)。
17
17
 
18
- 标准工具包接口(`SessionStandardProps`、`GlobalStandardProps`)在这里声明为空,由 runtime 包合并(与 SlotMap key 相同的 declare-merge 模式)。renderer 会把运行时 Session 和 Workspace observable source 绑定为 selector hook。Inject factory 参数从声明派生(`InjectParams`):Session slot 获得 `sessionId`;声明 store 时追加 baked `actions`;没有其他参数,数据访问位于 apply 闭包的 ctx 中。
18
+ 标准工具包接口(`SessionStandardProps`、`GlobalStandardProps`)在这里声明为空,由运行时包合并(与 SlotMap key 相同的 declare-merge 模式)。renderer 会把运行时会话和 Workspace observable source 绑定为 selector 钩子。Inject factory 参数从声明派生(`InjectParams`):会话 slot 获得 `sessionId`;声明 store 时追加 baked `actions`;没有其他参数,数据访问位于 apply 闭包的 ctx 中。
19
19
 
20
- store 家族(输入 `defineStore` 规范/输出 `StoreHandle<T, A>`)为 store seat 建模:`init` 推断状态 schema;`actions` 是完整的 draft-transform 写入集合;`BakedActions` 移除 draft 参数,成为组件和 inject factory 收到的回调。`defineStore` 值实现位于 runtime 包(引擎所属位置),并满足这里导出的 `DefineStore` 约定。引擎产物与 renderer host 约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React hook;hook 绑定属于渲染机制,只有 props 约定 hook 类型(`SnapshotSelectorHook`)位于这里。
20
+ store 家族(输入 `defineStore` 规范/输出 `StoreHandle<T, A>`)为 store seat 建模:`init` 推断状态 schema;`actions` 是完整的 draft-transform 写入集合;`BakedActions` 移除 draft 参数,成为组件和 inject factory 收到的回调。`defineStore` 值实现位于运行时包(引擎所属位置),并满足这里导出的 `DefineStore` 约定。引擎产物与 renderer host 约定携带裸快照 source(`getSnapshot`/`subscribe`),绝不携带 React 钩子;钩子绑定属于渲染机制,只有 props 约定钩子类型(`SnapshotSelectorHook`)位于这里。
21
21
 
22
- `SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证(注册未声明 slot、重复声明子项、在两个 scope 下使用同一个共享 handle、chain 注册缺少 `select`,这些情况都在 register 时抛出)。条目的 disposer 会递归移除其声明的子 slot:账本行、贡献和 store 挂载都会随同一生命周期结束而移除。每个 key 还携带一个 declaration epoch(声明代次),它只在声明与折叠时递增;运行时将其用于 [`ctx.slots.inject`](../runtime/README.md#slot-declaration-injection),且与普通条目版本相互独立。`renderer.ts` 携带安装约定(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;实现在 web-react 中,安装则在外壳启动中完成。
22
+ `SlotCore` 在构造时预置 `'root'` slot,并强制执行加载时验证(注册未声明 slot、重复声明子项、在两个 scope 下使用同一个共享 handle、chain 注册缺少 `select`,这些情况都在 register 时抛出)。条目的 disposer 会递归移除其声明的子 slot:账本行、贡献和 store 挂载都会随同一生命周期结束而移除。每个 key 还携带一个 declaration epoch(声明代次),它只在声明与移除时递增;运行时将其用于 [`ctx.slots.inject`](../runtime/README.md#slot-declaration-injection),且与普通条目版本相互独立。`renderer.ts` 携带安装约定(`SlotRenderer`、`SlotRendererHost`)以及 `StaleAuthorizationError`/`SlotOwnershipError`;实现在 web-react 中,安装则在外壳启动中完成。
23
23
 
24
24
  ## 模型体验
25
25
 
package/lib/index.js CHANGED
@@ -32,7 +32,9 @@ const NO_ENTRIES = Object.freeze([]);
32
32
  * fire); {@link SlotCore.subscribeDeclaration} fires synchronously for each
33
33
  * declaration lifetime boundary; {@link SlotCore.subscribe} notifications
34
34
  * batch per microtask, so N same-tick mutations produce one notification per
35
- * touched key.
35
+ * touched key. Entry crash reports ({@link SlotCore.reportEntryError}) ride
36
+ * the same mutation channel when they abdicate, then notify
37
+ * {@link SlotCore.onEntryError} synchronously.
36
38
  */
37
39
  var SlotCore = class {
38
40
  records = /* @__PURE__ */ new Map();
@@ -41,6 +43,15 @@ var SlotCore = class {
41
43
  handleScopes = /* @__PURE__ */ new Map();
42
44
  dirty = /* @__PURE__ */ new Set();
43
45
  flushScheduled = false;
46
+ /**
47
+ * Entries retired by an abdicating crash report
48
+ * ({@link SlotCore.reportEntryError}): excluded from
49
+ * {@link SlotCore.entriesOfSlot} projections for the rest of their
50
+ * registration's life, while the registration itself stays on the ledger
51
+ * (disposal authority remains with the registrant).
52
+ */
53
+ abdicated = /* @__PURE__ */ new WeakSet();
54
+ entryErrorListeners = /* @__PURE__ */ new Set();
44
55
  constructor() {
45
56
  const root = this.record("root");
46
57
  root.spec = {
@@ -54,18 +65,26 @@ var SlotCore = class {
54
65
  const rec = this.records.get(options.name);
55
66
  if (!rec?.spec) throw new Error(`slot "${options.name}" is not declared (a parent entry's children table must declare it)`);
56
67
  const spec = rec.spec;
68
+ const priority = options.priority ?? 0;
69
+ const occupantHint = (occupant) => `at priority ${priority}${occupant.registrant !== void 0 ? ` (registered by ${occupant.registrant})` : ""} — register at a different priority to shadow it (lowest renders)`;
57
70
  switch (spec.kind) {
58
- case "single":
59
- if (rec.entries.length > 0) throw new Error(`single slot "${options.name}" already has a registration`);
71
+ case "single": {
72
+ const occupant = rec.entries.find((e) => (e.options.priority ?? 0) === priority);
73
+ if (occupant) throw new Error(`single slot "${options.name}" already has a registration ${occupantHint(occupant)}`);
60
74
  break;
61
- case "keyed":
75
+ }
76
+ case "keyed": {
62
77
  if (options.key === void 0) throw new Error(`keyed slot "${options.name}" requires options.key`);
63
- if (rec.entries.some((e) => e.options.key === options.key)) throw new Error(`keyed slot "${options.name}" already has an entry for key "${options.key}"`);
78
+ const occupant = rec.entries.find((e) => e.options.key === options.key && (e.options.priority ?? 0) === priority);
79
+ if (occupant) throw new Error(`keyed slot "${options.name}" already has an entry for key "${options.key}" ${occupantHint(occupant)}`);
64
80
  break;
65
- case "list":
81
+ }
82
+ case "list": {
66
83
  if (options.id === void 0) throw new Error(`list slot "${options.name}" requires options.id`);
67
- if (rec.entries.some((e) => e.options.id === options.id)) throw new Error(`list slot "${options.name}" already has an entry with id "${options.id}"`);
84
+ const occupant = rec.entries.find((e) => e.options.id === options.id && (e.options.priority ?? 0) === priority);
85
+ if (occupant) throw new Error(`list slot "${options.name}" already has an entry with id "${options.id}" ${occupantHint(occupant)}`);
68
86
  break;
87
+ }
69
88
  case "chain":
70
89
  if (options.select === void 0) throw new Error(`chain slot "${options.name}" requires options.select`);
71
90
  break;
@@ -100,8 +119,7 @@ var SlotCore = class {
100
119
  ...options.registrant !== void 0 ? { registrant: options.registrant } : {}
101
120
  };
102
121
  const next = [...rec.entries, entry];
103
- if (spec.kind === "list") next.sort((a, b) => (a.options.order ?? 0) - (b.options.order ?? 0));
104
- if (spec.kind === "chain") next.sort((a, b) => (a.options.priority ?? 0) - (b.options.priority ?? 0));
122
+ next.sort(spec.kind === "list" ? (a, b) => (a.options.priority ?? 0) - (b.options.priority ?? 0) || (a.options.order ?? 0) - (b.options.order ?? 0) : (a, b) => (a.options.priority ?? 0) - (b.options.priority ?? 0));
105
123
  rec.entries = next;
106
124
  this.markDirty(options.name, rec);
107
125
  if (options.children) {
@@ -110,6 +128,7 @@ var SlotCore = class {
110
128
  const childRec = this.record(childKey);
111
129
  childRec.spec = childSpec;
112
130
  childRec.declaredBy = `an entry in "${options.name}"${options.registrant ? ` (${options.registrant})` : ""}`;
131
+ childRec.parent = options.name;
113
132
  childRec.declarationEpoch += 1;
114
133
  declarations.push([childKey, childRec]);
115
134
  }
@@ -146,6 +165,34 @@ var SlotCore = class {
146
165
  return this.records.get(key)?.entries ?? NO_ENTRIES;
147
166
  }
148
167
  /**
168
+ * Project a key's entries to its shadowing winners: the first live
169
+ * (non-abdicated) entry of each cell in priority order — single: the slot
170
+ * is one cell; keyed: one cell per `key`; list: one cell per `id` (winners
171
+ * keep ledger sequence; list renderers still refine display by `order`).
172
+ * Chain keys return the raw entries unchanged: election consumes every
173
+ * entry, shadowing does not apply. The raw {@link SlotCore.entries} view
174
+ * stays the inspection surface. Builds a fresh array per call — a render
175
+ * body read, not a uSES getSnapshot source.
176
+ * @param key - slot key (dynamic: the render machinery holds keys as strings).
177
+ * @returns the winning entry per occupied cell (empty while undeclared).
178
+ */
179
+ entriesOfSlot(key) {
180
+ const rec = this.records.get(key);
181
+ if (!rec?.spec) return NO_ENTRIES;
182
+ const kind = rec.spec.kind;
183
+ if (kind === "chain") return rec.entries;
184
+ const heads = [];
185
+ const seenCells = /* @__PURE__ */ new Set();
186
+ for (const entry of rec.entries) {
187
+ if (this.abdicated.has(entry)) continue;
188
+ const cell = kind === "keyed" ? entry.options.key : kind === "list" ? entry.options.id : void 0;
189
+ if (seenCells.has(cell)) continue;
190
+ seenCells.add(cell);
191
+ heads.push(entry);
192
+ }
193
+ return heads;
194
+ }
195
+ /**
149
196
  * Look up a slot's declared spec, narrowed by the SlotMap key.
150
197
  * @param key - SlotMap key.
151
198
  * @returns the spec, or undefined while undeclared.
@@ -164,6 +211,47 @@ var SlotCore = class {
164
211
  return this.records.get(key)?.spec;
165
212
  }
166
213
  /**
214
+ * Export the current declaration topology without components or executable hooks.
215
+ * @param root - exact Slot key to select; omitted returns every live root.
216
+ * @returns selected live Slot trees, or an empty array when `root` is unavailable.
217
+ */
218
+ snapshot(root) {
219
+ const build = (name, seen) => {
220
+ const record = this.records.get(name);
221
+ if (record?.spec === void 0 || seen.has(name)) return void 0;
222
+ const branch = new Set(seen);
223
+ branch.add(name);
224
+ const active = new Set(this.entriesOfSlot(name));
225
+ const children = [...this.records.entries()].filter(([, candidate]) => candidate.spec !== void 0 && candidate.parent === name).flatMap(([child]) => {
226
+ const node = build(child, branch);
227
+ return node === void 0 ? [] : [node];
228
+ });
229
+ return {
230
+ name,
231
+ kind: record.spec.kind,
232
+ scope: record.spec.scope,
233
+ ...record.declaredBy === void 0 ? {} : { declaredBy: record.declaredBy },
234
+ occupants: record.entries.map((entry) => ({
235
+ ...entry.registrant === void 0 ? {} : { registrant: entry.registrant },
236
+ ...entry.options.key === void 0 ? {} : { key: entry.options.key },
237
+ ...entry.options.id === void 0 ? {} : { id: entry.options.id },
238
+ ...entry.options.order === void 0 ? {} : { order: entry.options.order },
239
+ priority: entry.options.priority ?? 0,
240
+ active: active.has(entry)
241
+ })),
242
+ children
243
+ };
244
+ };
245
+ if (root !== void 0) {
246
+ const node = build(root, /* @__PURE__ */ new Set());
247
+ return node === void 0 ? [] : [node];
248
+ }
249
+ return [...this.records.entries()].filter(([, record]) => record.spec !== void 0 && (record.parent === void 0 || this.records.get(record.parent)?.spec === void 0)).flatMap(([name]) => {
250
+ const node = build(name, /* @__PURE__ */ new Set());
251
+ return node === void 0 ? [] : [node];
252
+ });
253
+ }
254
+ /**
167
255
  * Read the declaration lifetime of a key. Entry additions and removals do
168
256
  * not change it; declaration creation and collapse each advance it.
169
257
  * @param key - slot key.
@@ -226,6 +314,47 @@ var SlotCore = class {
226
314
  };
227
315
  }
228
316
  /**
317
+ * Renderer crash report from an entry boundary. Always notifies
318
+ * {@link SlotCore.onEntryError} listeners; with `info.abdicate` set (the
319
+ * shadowing kinds — single/keyed/list) it first retires the entry from its
320
+ * cell, one-shot: the record's version bumps through the ordinary mutation
321
+ * channel so outlets re-project onto the cell's next survivor, and a
322
+ * repeat abdicating report no-ops entirely. Chain crashes report with
323
+ * `abdicate: false` — election alternatives resolve at select time, so the
324
+ * entry keeps its cell and only the notification fires. The registration
325
+ * itself stays on the ledger either way — raw {@link SlotCore.entries}
326
+ * still lists the entry and its disposer keeps working.
327
+ * @param key - slot key the entry rendered under.
328
+ * @param entry - the crashed entry.
329
+ * @param error - the crash cause, forwarded to listeners verbatim.
330
+ * @param info - `abdicate`: whether the crash retires the entry from its cell.
331
+ */
332
+ reportEntryError(key, entry, error, info) {
333
+ if (info.abdicate) {
334
+ if (this.abdicated.has(entry)) return;
335
+ this.abdicated.add(entry);
336
+ const rec = this.records.get(key);
337
+ if (rec !== void 0) this.markDirty(key, rec);
338
+ }
339
+ for (const fn of [...this.entryErrorListeners]) fn(key, entry, error, { abdicated: info.abdicate });
340
+ }
341
+ /**
342
+ * Observe entry boundary crashes (every render-time entry failure the
343
+ * boundaries contain, abdicating or not) — the supervision seam for hosts
344
+ * mirroring contribution health. Fires synchronously per report, after the
345
+ * registry mutated for abdicating crashes (same listener discipline as
346
+ * {@link SlotCore.onMutate}).
347
+ * @param fn - called with the slot key, the crashed entry, the crash
348
+ * cause, and `abdicated`: whether the crash retired the entry from its cell.
349
+ * @returns unsubscribe.
350
+ */
351
+ onEntryError(fn) {
352
+ this.entryErrorListeners.add(fn);
353
+ return () => {
354
+ this.entryErrorListeners.delete(fn);
355
+ };
356
+ }
357
+ /**
229
358
  * Cascade for a removed entry: release its store mount and collapse every
230
359
  * child slot it declared — specs clear, contributions empty (their stale
231
360
  * disposers no-op), recursively down the declaration tree. One lifecycle
@@ -244,6 +373,7 @@ var SlotCore = class {
244
373
  const doomed = childRec.entries;
245
374
  childRec.spec = void 0;
246
375
  childRec.declaredBy = void 0;
376
+ childRec.parent = void 0;
247
377
  childRec.declarationEpoch += 1;
248
378
  childRec.entries = NO_ENTRIES;
249
379
  this.markDirty(childKey, childRec);
@@ -257,6 +387,7 @@ var SlotCore = class {
257
387
  rec = {
258
388
  spec: void 0,
259
389
  declaredBy: void 0,
390
+ parent: void 0,
260
391
  declarationEpoch: 0,
261
392
  entries: NO_ENTRIES,
262
393
  version: 0,
package/lib/invariant.js CHANGED
@@ -10,7 +10,7 @@ const name = "client-ui-slots-invariant";
10
10
  const inject = ["invariants"];
11
11
  /**
12
12
  * No runtime invariant: a zero-dependency pure registry core — it emits no
13
- * cordis events itself (the runtime SlotsService wrapper owns the event
13
+ * cordis events itself (the runtime SlotRegistry wrapper owns the event
14
14
  * bridge and its invariants); define/register/dispose sequencing is asserted
15
15
  * directly by this package's behavior specs.
16
16
  */
@@ -371,13 +371,20 @@ export type InjectParams<K extends keyof SlotMap & string, H> = ScopeOf<K> exten
371
371
  * without re-registration. Owners resolve through {@link resolveSlotLabel}.
372
372
  */
373
373
  export type SlotLabel = string | (() => string);
374
- /** Kind shape fields carried in register options (keyed dispatch key; list id/order/label; chain select/priority). */
374
+ /**
375
+ * Kind shape fields carried in register options (keyed dispatch key; list
376
+ * id/order/label; chain select/priority; non-chain priority = cell shadowing rank).
377
+ */
375
378
  export type KindOptions<K extends keyof SlotMap & string, EntryKey extends EntryKeyOf<K>, M = never> = SlotMap[K]['kind'] extends 'keyed' ? {
376
379
  key: EntryKey;
380
+ /** Cell shadowing rank (ascending, default 0, lowest renders; same key + same priority throws — see {@link SlotCore.register}). */
381
+ priority?: number;
377
382
  } : SlotMap[K]['kind'] extends 'list' ? {
378
383
  id: string;
379
384
  order?: number;
380
385
  label?: SlotLabel;
386
+ /** Cell shadowing rank (ascending, default 0, lowest renders; same id + same priority throws — see {@link SlotCore.register}). */
387
+ priority?: number;
381
388
  } : SlotMap[K]['kind'] extends 'chain' ? {
382
389
  /** Routing selector, mandatory on chain entries; `M` (the component's `matched` prop) infers from its return. */
383
390
  select: ChainSelect<SlotMap[K] extends {
@@ -385,7 +392,13 @@ export type KindOptions<K extends keyof SlotMap & string, EntryKey extends Entry
385
392
  } ? O : object, M>;
386
393
  /** Explicit chain position (ascending, default 0, lower tries first); ties keep registration = assembly order. */
387
394
  priority?: number;
388
- } : object;
395
+ } : {
396
+ /**
397
+ * Cell shadowing rank (ascending, default 0, lowest renders; a
398
+ * same-priority second registration throws — see {@link SlotCore.register}).
399
+ */
400
+ priority?: number;
401
+ };
389
402
  /**
390
403
  * Compile-time presence check: an entry declaring children MUST consume
391
404
  * `renderSlot` (or `renderSlotChain` when its only children are chain slots)
@@ -451,6 +464,36 @@ export interface StoredEntry {
451
464
  * @returns the display string, or undefined when the entry declared none.
452
465
  */
453
466
  export declare function resolveSlotLabel(label: SlotLabel | undefined): string | undefined;
467
+ /** JSON-safe live occupant returned by slot inspection. */
468
+ export interface LiveSlotOccupant {
469
+ /** Plugin or package that registered the entry, when known. */
470
+ registrant?: string;
471
+ /** Keyed-slot cell. */
472
+ key?: string;
473
+ /** List-slot cell. */
474
+ id?: string;
475
+ /** List display order. */
476
+ order?: number;
477
+ /** Shadowing or chain priority. */
478
+ priority: number;
479
+ /** Whether the renderer currently selects this entry. */
480
+ active: boolean;
481
+ }
482
+ /** JSON-safe live slot declaration tree. */
483
+ export interface LiveSlotNode {
484
+ /** Exact SlotMap key. */
485
+ name: string;
486
+ /** Slot cardinality. */
487
+ kind: SlotKind;
488
+ /** Runtime data scope. */
489
+ scope: SlotScope;
490
+ /** Diagnostic owner of this declaration. */
491
+ declaredBy?: string;
492
+ /** Current registrations in ledger order. */
493
+ occupants: LiveSlotOccupant[];
494
+ /** Slots declared by entries mounted in this slot. */
495
+ children: LiveSlotNode[];
496
+ }
454
497
  /**
455
498
  * Pure slot registry (no cordis; event emission and the renderer installation contract
456
499
  * live in the runtime Service wrapper).
@@ -463,7 +506,9 @@ export declare function resolveSlotLabel(label: SlotLabel | undefined): string |
463
506
  * fire); {@link SlotCore.subscribeDeclaration} fires synchronously for each
464
507
  * declaration lifetime boundary; {@link SlotCore.subscribe} notifications
465
508
  * batch per microtask, so N same-tick mutations produce one notification per
466
- * touched key.
509
+ * touched key. Entry crash reports ({@link SlotCore.reportEntryError}) ride
510
+ * the same mutation channel when they abdicate, then notify
511
+ * {@link SlotCore.onEntryError} synchronously.
467
512
  */
468
513
  export declare class SlotCore {
469
514
  private records;
@@ -472,6 +517,15 @@ export declare class SlotCore {
472
517
  private handleScopes;
473
518
  private dirty;
474
519
  private flushScheduled;
520
+ /**
521
+ * Entries retired by an abdicating crash report
522
+ * ({@link SlotCore.reportEntryError}): excluded from
523
+ * {@link SlotCore.entriesOfSlot} projections for the rest of their
524
+ * registration's life, while the registration itself stays on the ledger
525
+ * (disposal authority remains with the registrant).
526
+ */
527
+ private abdicated;
528
+ private entryErrorListeners;
475
529
  constructor();
476
530
  /**
477
531
  * Contribute a component to a declared slot and (optionally) declare child
@@ -481,11 +535,18 @@ export declare class SlotCore {
481
535
  * re-checks nothing): registering into an undeclared slot throws; declaring
482
536
  * an already-declared child key throws (one declarer per slot — the message
483
537
  * names the first declarer); mounting one shared store handle under slots
484
- * of different scopes throws. Kind constraints: single — duplicate
485
- * registration throws; keyed — missing/duplicate `key` throws; list —
486
- * missing/duplicate `id` throws; chain — missing `select` throws (the
538
+ * of different scopes throws. Kind constraints: keyed — missing `key`
539
+ * throws; list — missing `id` throws; chain — missing `select` throws (the
487
540
  * selector is the entry's routing seat, see {@link ChainSelect}).
488
541
  *
542
+ * Shadowing (single/keyed/list): entries sharing one cell (single — the
543
+ * slot itself; keyed — same `key`; list — same `id`) coexist at distinct
544
+ * priorities, sorted ascending with ties keeping registration order; the
545
+ * cell's lowest live entry renders ({@link SlotCore.entriesOfSlot}). A
546
+ * second registration at an occupied cell's exact priority (default 0)
547
+ * throws naming the occupant, so priority-less composition keeps the
548
+ * historical one-occupant-per-cell fail-loud.
549
+ *
489
550
  * Lifecycle: the disposer removes the contribution AND collapses every
490
551
  * declared child slot (child entries clear recursively; their stale
491
552
  * disposers become no-ops) — one lifecycle axis, no dangling state.
@@ -531,6 +592,19 @@ export declare class SlotCore {
531
592
  * @returns entries in registration (list: order) sequence.
532
593
  */
533
594
  entries(key: string): readonly StoredEntry[];
595
+ /**
596
+ * Project a key's entries to its shadowing winners: the first live
597
+ * (non-abdicated) entry of each cell in priority order — single: the slot
598
+ * is one cell; keyed: one cell per `key`; list: one cell per `id` (winners
599
+ * keep ledger sequence; list renderers still refine display by `order`).
600
+ * Chain keys return the raw entries unchanged: election consumes every
601
+ * entry, shadowing does not apply. The raw {@link SlotCore.entries} view
602
+ * stays the inspection surface. Builds a fresh array per call — a render
603
+ * body read, not a uSES getSnapshot source.
604
+ * @param key - slot key (dynamic: the render machinery holds keys as strings).
605
+ * @returns the winning entry per occupied cell (empty while undeclared).
606
+ */
607
+ entriesOfSlot(key: string): readonly StoredEntry[];
534
608
  /**
535
609
  * Look up a slot's declared spec, narrowed by the SlotMap key.
536
610
  * @param key - SlotMap key.
@@ -545,6 +619,12 @@ export declare class SlotCore {
545
619
  * @returns the wide-typed spec, or undefined while undeclared.
546
620
  */
547
621
  specDynamic(key: string): SlotSpec<SlotEntryDef> | undefined;
622
+ /**
623
+ * Export the current declaration topology without components or executable hooks.
624
+ * @param root - exact Slot key to select; omitted returns every live root.
625
+ * @returns selected live Slot trees, or an empty array when `root` is unavailable.
626
+ */
627
+ snapshot(root?: string): LiveSlotNode[];
548
628
  /**
549
629
  * Read the declaration lifetime of a key. Entry additions and removals do
550
630
  * not change it; declaration creation and collapse each advance it.
@@ -586,6 +666,38 @@ export declare class SlotCore {
586
666
  * @returns unsubscribe.
587
667
  */
588
668
  onMutate(fn: (key: string) => void): () => void;
669
+ /**
670
+ * Renderer crash report from an entry boundary. Always notifies
671
+ * {@link SlotCore.onEntryError} listeners; with `info.abdicate` set (the
672
+ * shadowing kinds — single/keyed/list) it first retires the entry from its
673
+ * cell, one-shot: the record's version bumps through the ordinary mutation
674
+ * channel so outlets re-project onto the cell's next survivor, and a
675
+ * repeat abdicating report no-ops entirely. Chain crashes report with
676
+ * `abdicate: false` — election alternatives resolve at select time, so the
677
+ * entry keeps its cell and only the notification fires. The registration
678
+ * itself stays on the ledger either way — raw {@link SlotCore.entries}
679
+ * still lists the entry and its disposer keeps working.
680
+ * @param key - slot key the entry rendered under.
681
+ * @param entry - the crashed entry.
682
+ * @param error - the crash cause, forwarded to listeners verbatim.
683
+ * @param info - `abdicate`: whether the crash retires the entry from its cell.
684
+ */
685
+ reportEntryError(key: string, entry: StoredEntry, error: unknown, info: {
686
+ abdicate: boolean;
687
+ }): void;
688
+ /**
689
+ * Observe entry boundary crashes (every render-time entry failure the
690
+ * boundaries contain, abdicating or not) — the supervision seam for hosts
691
+ * mirroring contribution health. Fires synchronously per report, after the
692
+ * registry mutated for abdicating crashes (same listener discipline as
693
+ * {@link SlotCore.onMutate}).
694
+ * @param fn - called with the slot key, the crashed entry, the crash
695
+ * cause, and `abdicated`: whether the crash retired the entry from its cell.
696
+ * @returns unsubscribe.
697
+ */
698
+ onEntryError(fn: (key: string, entry: StoredEntry, error: unknown, info: {
699
+ abdicated: boolean;
700
+ }) => void): () => void;
589
701
  /**
590
702
  * Cascade for a removed entry: release its store mount and collapse every
591
703
  * child slot it declared — specs clear, contributions empty (their stale
@@ -8,7 +8,7 @@ import type { SlotEntryDef, SlotSpec, StoredEntry, Translate } from './index.ts'
8
8
  * active-locale or registry change; the renderer re-derives each entry's `t`
9
9
  * from (namespace, revision), so a locale switch hands out NEW function
10
10
  * references and memoized components re-render naturally. Implemented by the
11
- * locale plugin, installed through the runtime SlotsService (installLocale).
11
+ * locale plugin, installed through the runtime SlotRegistry (installLocale).
12
12
  * Install before the first render that needs the seat: outlets bind their
13
13
  * revision subscription at mount, and a face appearing later has no channel
14
14
  * to notify already-mounted outlets (the locale plugin is immediately-tier
@@ -94,7 +94,7 @@ export interface RenderOpts {
94
94
  /** Opaque occurrence context consumed only by function-valued injected Hooks. */
95
95
  hookContext?: unknown;
96
96
  }
97
- /** Host API the runtime SlotsService presents to the installed renderer. */
97
+ /** Host API the runtime SlotRegistry presents to the installed renderer. */
98
98
  export interface SlotRendererHost {
99
99
  /**
100
100
  * Subscribe to a key's registration changes (microtask-batched).
@@ -115,6 +115,29 @@ export interface SlotRendererHost {
115
115
  * @returns entries in registration (list: order) sequence.
116
116
  */
117
117
  entriesOf(key: string): readonly StoredEntry[];
118
+ /**
119
+ * Shadowing winners per cell for a key — the render read for single/keyed/
120
+ * list dispatch: the first live (non-abdicated) entry of each cell in
121
+ * priority order; chain keys pass through unchanged (election consumes
122
+ * every entry). Fresh array per call — a render-body read, not a uSES
123
+ * getSnapshot source.
124
+ * @param key - slot key.
125
+ * @returns the winning entry per occupied cell.
126
+ */
127
+ entriesOfSlot(key: string): readonly StoredEntry[];
128
+ /**
129
+ * Report an entry boundary crash. With `info.abdicate` (shadowing kinds)
130
+ * the entry retires from its cell, one-shot, so the next survivor renders;
131
+ * chain crashes report without abdicating. The registration stays on the
132
+ * ledger either way.
133
+ * @param key - slot key the entry rendered under.
134
+ * @param entry - the crashed entry.
135
+ * @param error - the crash cause.
136
+ * @param info - `abdicate`: whether the crash retires the entry from its cell.
137
+ */
138
+ reportEntryError(key: string, entry: StoredEntry, error: unknown, info: {
139
+ abdicate: boolean;
140
+ }): void;
118
141
  /**
119
142
  * Declared runtime spec from the declarations ledger.
120
143
  * @param key - slot key.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-slots",
3
3
  "description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam",
4
- "version": "0.0.1-rc.2",
4
+ "version": "0.0.1-rc.3",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },
@@ -28,7 +28,7 @@
28
28
  "license": "BSD-3-Clause",
29
29
  "devDependencies": {
30
30
  "@types/react": "~18.3.1",
31
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
31
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
32
32
  "@deepseek-ai/cordis": "^4.0.1-rc.1"
33
33
  },
34
34
  "files": [
@@ -37,7 +37,7 @@
37
37
  "lib/types/**/*.d.ts"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
40
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
41
41
  "@deepseek-ai/cordis": "^4.0.1-rc.1"
42
42
  }
43
43
  }