@liusiqi-dev/dsh-scope 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.i18n.yaml +52 -0
- package/README.md +112 -2
- package/README.zh.md +113 -0
- package/lib/index.js +357 -0
- package/lib/invariant.js +73 -0
- package/lib/types/index.d.ts +110 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/scoped-events.generated.d.ts +18 -0
- package/lib/types/store.d.ts +147 -0
- package/package.json +39 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Based on DeepSeek Harness, Copyright © 2026 DeepSeek Harness, MIT License. Modified by Liusiqi.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/core/scope/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 47f2816e2d2cecb9
|
|
7
|
+
zh: e051427327761450
|
|
8
|
+
/deepseek-ai-dsh-scope:
|
|
9
|
+
en: a493a184041d2c95
|
|
10
|
+
zh: af060f6404de90d8
|
|
11
|
+
/deepseek-ai-dsh-scope/summary:
|
|
12
|
+
en: f645dd571d3c8ca7
|
|
13
|
+
zh: 781b83572498adf1
|
|
14
|
+
/deepseek-ai-dsh-scope/table-of-contents:
|
|
15
|
+
en: 80b31a934611cb4a
|
|
16
|
+
zh: 05f3f7f9cb2fd881
|
|
17
|
+
/deepseek-ai-dsh-scope/use-this-package:
|
|
18
|
+
en: 14e53a4df086a0de
|
|
19
|
+
zh: 17ea1f39cfbe007e
|
|
20
|
+
/deepseek-ai-dsh-scope/use-this-package/mint-a-scope:
|
|
21
|
+
en: e7bfa7a59f61fd72
|
|
22
|
+
zh: 852d6d4b113babd1
|
|
23
|
+
/deepseek-ai-dsh-scope/use-this-package/route-scoped-events:
|
|
24
|
+
en: 7439a6c862956a63
|
|
25
|
+
zh: bfeda5194bb8cdc9
|
|
26
|
+
/deepseek-ai-dsh-scope/use-this-package/build-a-scoped-registry-layer:
|
|
27
|
+
en: 187aa0e904782395
|
|
28
|
+
zh: 14fa36d3716eaff2
|
|
29
|
+
/deepseek-ai-dsh-scope/understand-the-implementation:
|
|
30
|
+
en: ce7e776e9d516bdb
|
|
31
|
+
zh: 59d2751743db7795
|
|
32
|
+
/deepseek-ai-dsh-scope/understand-the-implementation/design-concept:
|
|
33
|
+
en: 1d19d421fa6f3546
|
|
34
|
+
zh: ed6679cfc9166f14
|
|
35
|
+
/deepseek-ai-dsh-scope/understand-the-implementation/source-map:
|
|
36
|
+
en: 52dd18180452c174
|
|
37
|
+
zh: 47d0198266697794
|
|
38
|
+
/deepseek-ai-dsh-scope/understand-the-implementation/the-parent-chain:
|
|
39
|
+
en: f0c433a9b0091de4
|
|
40
|
+
zh: 21cecb4194aff7d0
|
|
41
|
+
/deepseek-ai-dsh-scope/understand-the-implementation/event-filtering:
|
|
42
|
+
en: 9ec9b8d491c7dfe7
|
|
43
|
+
zh: da82e63bac268535
|
|
44
|
+
/deepseek-ai-dsh-scope/further-exploration:
|
|
45
|
+
en: f6f4e46a89785446
|
|
46
|
+
zh: ea130e3ffefe1139
|
|
47
|
+
/deepseek-ai-dsh-scope/known-limitations-and-deferred-work:
|
|
48
|
+
en: 87eea2029792dc14
|
|
49
|
+
zh: ba020413273d3235
|
|
50
|
+
/deepseek-ai-dsh-scope/known-limitations-and-deferred-work/dev-note:
|
|
51
|
+
en: e67f633dc6fe7773
|
|
52
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,113 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The scoped-registration library for plugin authors and maintainers building registries or event surfaces that isolate contributions per agent or per group."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @liusiqi-dev/dsh-scope
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-scope` lets plugin authors give each agent or group an isolated contribution set with a shared lifetime. Child scopes inherit ancestor contributions, with the nearest definition taking precedence, while ancestor scopes can observe descendant activity; neither relationship works in reverse. Disposing a scope removes everything owned by it. Use this dependency-free library when per-agent or per-group isolation must work without depending on the agent loop or presets.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
20
|
+
- [Dev Note](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="use-this-package"></a>
|
|
25
|
+
## Use this package
|
|
26
|
+
|
|
27
|
+
Plugin authors use `dsh-scope` to give one agent (or one group) its own registration world. The registries in the core group build on it — a tool registered through `agent.ctx` is visible only to that agent — and the same primitive serves any custom registry or scope-filtered event.
|
|
28
|
+
|
|
29
|
+
### Mint a scope
|
|
30
|
+
|
|
31
|
+
`createScope(ctx, key)` creates a scope under `ctx`'s fiber: its `ctx` carries the scope tag, and everything registered through it is both scope-visible and scope-lifetime. `dispose()` unwinds every registration through the scope; `rawDispose` is the exact Cordis disposer for nesting the teardown in an ordered composite effect.
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
const scope = createScope(ctx, agent)
|
|
35
|
+
scope.ctx.on('agent/status', ({ agent, status }) => track(agent, status))
|
|
36
|
+
// later:
|
|
37
|
+
await scope.dispose() // unwinds every registration made through scope.ctx
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Route scoped events
|
|
41
|
+
|
|
42
|
+
`scopeTarget(base, key)` builds the opaque carrier a scope-filtered event dispatches with. Untagged listeners stay global; a listener tagged with `key` receives events for that key and its descendants. The carrier carries routing state only — the real subject travels in the event arguments.
|
|
43
|
+
|
|
44
|
+
### Build a scoped registry layer
|
|
45
|
+
|
|
46
|
+
Registry authors use `ScopedLayers`, `NamedEntries`, and `AnonymousEntries` to hold one eager global layer plus lazily created exact-scope layers: reads never create layers, `merge()` materializes insertion-ordered named shadows along the scope chain, and `effect()` derives visibility and ownership from the same context. A scoped layer is reclaimed only when its whole aggregate is empty.
|
|
47
|
+
|
|
48
|
+
-----
|
|
49
|
+
|
|
50
|
+
<a id="understand-the-implementation"></a>
|
|
51
|
+
## Understand the implementation
|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary>Implementation internals — click to expand</summary>
|
|
55
|
+
|
|
56
|
+
This section explains how the package realizes the behavior above; the observable contract is covered in [Use this package](#use-this-package).
|
|
57
|
+
|
|
58
|
+
### Design concept
|
|
59
|
+
|
|
60
|
+
The registration context determines both visibility and ownership: a registration made through a scoped context is visible in that scope and disposed with it, preventing a contribution from being visible in one scope but torn down with another. The primitive routes trusted same-process plugins; it is not a sandbox or an authority boundary. Handing out a scoped context also hands out the minting plugin's service-resolution API (resolution walks the minting fiber's dependency chain), so a scope is minted from the plugin whose dependencies the scoped registrations need.
|
|
61
|
+
|
|
62
|
+
### Source map
|
|
63
|
+
|
|
64
|
+
| File | Role |
|
|
65
|
+
|---|---|
|
|
66
|
+
| [`src/index.ts`](src/index.ts) | `createScope`, `scopeOf`, `scopeTarget`, `bindScopeParent`/`scopeParentOf`/`scopeChainOf`, carrier marks |
|
|
67
|
+
| [`src/store.ts`](src/store.ts) | `ScopedLayers`, `NamedEntries`, `AnonymousEntries`, `ScopeLayer` |
|
|
68
|
+
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion over the generated scoped-event map |
|
|
69
|
+
| [`src/scoped-events.generated.ts`](src/scoped-events.generated.ts) | Generated resolver map of declared scoped events |
|
|
70
|
+
|
|
71
|
+
### The parent chain
|
|
72
|
+
|
|
73
|
+
One relation powers both directions: registration views inherit DOWN the chain (a child scope sees its ancestors' layers), while event admission extends UP it (a listener tagged with an ancestor receives events dispatched to a descendant key). Binding is once — a key that already has a parent throws, and only the returned binding may re-link it — and every link rejects a cycle. `scopeChainOf` returns `[key, parent, …]` nearest-first.
|
|
74
|
+
|
|
75
|
+
### Event filtering
|
|
76
|
+
|
|
77
|
+
`scopeTarget` composes the base's existing `Context.filter` with the scope predicate: an untagged listener is admitted; a tagged listener is admitted iff its tag is the dispatch key or an ancestor of it; `key === undefined` admits untagged listeners only. `{ global: true }` listeners bypass filtering. The `Scoped<T>` brand demands the carrier as the `this` type of a scope-filtered event, so dispatching with a bare subject is a compile error.
|
|
78
|
+
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
-----
|
|
82
|
+
|
|
83
|
+
<a id="further-exploration"></a>
|
|
84
|
+
## Further Exploration
|
|
85
|
+
|
|
86
|
+
The package-level contract is enough for most consumers; read these when you need the surrounding domain and the design rationale.
|
|
87
|
+
|
|
88
|
+
- [Scoped registration subsystem](../../../docs/subsystems/scope.md) — the identity, carrier, and layer types.
|
|
89
|
+
- [Agent-scope contexts Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md) — the security non-goals and context design.
|
|
90
|
+
- [Agent-scope runtime design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md) — how the loop builds per-agent scopes.
|
|
91
|
+
- [Core group map](../README.md) — how the core packages compose.
|
|
92
|
+
|
|
93
|
+
-----
|
|
94
|
+
|
|
95
|
+
## Known Limitations and Deferred Work
|
|
96
|
+
|
|
97
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
98
|
+
|
|
99
|
+
These limits define when the primitive needs special care. They are current package constraints, not a task backlog.
|
|
100
|
+
|
|
101
|
+
- **Only scope-aware APIs isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context.
|
|
102
|
+
- **A context carries one nearest scope key** — the hierarchy lives in the key-level parent relation, not in context tags; nested scope contexts still shadow to a single tag, and multi-membership policy sets remain unsupported.
|
|
103
|
+
- **Service reachability comes from the scope minter** — handing out `Scope.ctx` also hands out the minting plugin's injected services, so a broader minter cannot later be narrowed by the holder.
|
|
104
|
+
|
|
105
|
+
<a id="dev-note"></a>
|
|
106
|
+
### Dev Note
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
110
|
+
|
|
111
|
+
None.
|
|
112
|
+
|
|
113
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向插件作者与维护者的作用域注册库,用于构建按 agent(智能体)或按分组隔离贡献的注册表或事件接口。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @liusiqi-dev/dsh-scope
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-scope` 让插件作者能够为每个 agent 或分组提供隔离的贡献集合与统一生命周期。子作用域继承祖先贡献,且较近的定义优先;祖先作用域可以观察后代活动,这两种关系均不反向成立。释放作用域会移除它拥有的一切。按 agent 或分组隔离必须脱离 agent loop(智能体循环)与 preset 工作时,请使用这个零依赖库。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
20
|
+
- [开发备注](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="use-this-package"></a>
|
|
25
|
+
## 使用本包
|
|
26
|
+
|
|
27
|
+
插件作者使用 `dsh-scope` 为单个 agent(或单个分组)提供独立的注册世界。core 分组中的注册表都构建在它之上——通过 `agent.ctx` 注册的工具只对该 agent 可见——同样的原语也服务于任何自定义注册表或带作用域的事件。
|
|
28
|
+
|
|
29
|
+
### 创建作用域
|
|
30
|
+
|
|
31
|
+
`createScope(ctx, key)` 在 `ctx` 的 fiber 下创建作用域:其 `ctx` 携带作用域标签,通过它进行的每项注册既具备作用域可见性,也服从作用域生命周期。`dispose()` 撤销通过该作用域进行的每项注册;`rawDispose` 是确切 Cordis disposer,用于把 teardown 嵌套进有序组合 effect。
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
const scope = createScope(ctx, agent)
|
|
35
|
+
scope.ctx.on('agent/status', ({ agent, status }) => track(agent, status))
|
|
36
|
+
// later:
|
|
37
|
+
await scope.dispose() // unwinds every registration made through scope.ctx
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 路由带作用域的事件
|
|
41
|
+
|
|
42
|
+
`scopeTarget(base, key)` 构造带作用域事件分发所用的不透明载体。无标签监听器保持全局;标签为 `key` 的监听器接收该键及其后代的事件。载体只携带路由状态——真实主体由事件参数携带。
|
|
43
|
+
|
|
44
|
+
### 构建带作用域的注册表层
|
|
45
|
+
|
|
46
|
+
注册表作者使用 `ScopedLayers`、`NamedEntries` 与 `AnonymousEntries` 持有一个立即构造的全局层加惰性创建的精确作用域层:读取从不创建层,`merge()` 沿作用域链物化按插入序的具名遮蔽,`effect()` 从同一上下文推导可见性与所有权。只有当整个聚合为空时才回收作用域层。
|
|
47
|
+
|
|
48
|
+
-----
|
|
49
|
+
|
|
50
|
+
<a id="understand-the-implementation"></a>
|
|
51
|
+
## 理解实现
|
|
52
|
+
|
|
53
|
+
<details>
|
|
54
|
+
<summary>实现细节——点击展开</summary>
|
|
55
|
+
|
|
56
|
+
本节解释该包如何实现上述行为;可观察约定已在[使用本包](#use-this-package)中完整说明。
|
|
57
|
+
|
|
58
|
+
### 设计理念
|
|
59
|
+
|
|
60
|
+
注册上下文同时决定可见性与所有权:通过带作用域上下文进行的注册在该作用域内可见、并随其 dispose(资源释放),从而防止贡献在一个作用域中可见、却随另一个作用域拆除。该原语用于路由受信任的同进程插件;它不是沙箱或权限边界。交出带作用域的上下文,也会交出创建该上下文的插件的服务解析范围(解析沿创建者 fiber 的依赖链行进),因此作用域应由具备这些带作用域注册所需依赖的插件来创建。
|
|
61
|
+
|
|
62
|
+
### 源码地图
|
|
63
|
+
|
|
64
|
+
| 文件 | 职责 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| [`src/index.ts`](src/index.ts) | `createScope`、`scopeOf`、`scopeTarget`、`bindScopeParent`/`scopeParentOf`/`scopeChainOf`、载体标记 |
|
|
67
|
+
| [`src/store.ts`](src/store.ts) | `ScopedLayers`、`NamedEntries`、`AnonymousEntries`、`ScopeLayer` |
|
|
68
|
+
| [`src/invariant.ts`](src/invariant.ts) | 基于生成的作用域事件映射的不变式配套 |
|
|
69
|
+
| [`src/scoped-events.generated.ts`](src/scoped-events.generated.ts) | 已声明带作用域事件的生成解析器映射 |
|
|
70
|
+
|
|
71
|
+
### 父链
|
|
72
|
+
|
|
73
|
+
一个关系支撑两个方向:注册视图沿链**向下**继承(子作用域看得见祖先的各层),事件放行沿链**向上**扩展(标签为祖先的监听器收到分发到后代键的事件)。绑定仅此一次——已有父级的键直接抛错,只有返回的绑定句柄才能重新绑定——且每次链接都拒绝闭环。`scopeChainOf` 返回 `[key, parent, …]`,最近者在前。
|
|
74
|
+
|
|
75
|
+
### 事件筛选
|
|
76
|
+
|
|
77
|
+
`scopeTarget` 把基对象的现有 `Context.filter` 与作用域谓词组合起来:无标签监听器放行;有标签监听器仅当标签为分发键或其祖先时放行;`key === undefined` 只放行无标签监听器。带 `{ global: true }` 的监听器绕过筛选。`Scoped<T>` brand 要求带作用域事件以载体作为 `this` 类型,因此用裸主体分发会产生编译错误。
|
|
78
|
+
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
-----
|
|
82
|
+
|
|
83
|
+
<a id="further-exploration"></a>
|
|
84
|
+
## 进一步探索
|
|
85
|
+
|
|
86
|
+
包级约定对大多数消费方已经足够;需要周边领域与设计原理时再阅读以下页面。
|
|
87
|
+
|
|
88
|
+
- [作用域注册子系统](../../../docs/subsystems/scope.zh.md)——身份、载体与层类型。
|
|
89
|
+
- [agent 作用域上下文 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md)——安全非目标与上下文设计。
|
|
90
|
+
- [agent 作用域运行时设计 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md)——循环如何构建按 agent 的作用域。
|
|
91
|
+
- [core 分组地图](../README.zh.md)——core 各包如何组合。
|
|
92
|
+
|
|
93
|
+
-----
|
|
94
|
+
|
|
95
|
+
## 已知限制与延期工作
|
|
96
|
+
|
|
97
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
98
|
+
|
|
99
|
+
这些限制说明该原语何时需要特别留意。它们是当前包约束,不是任务积压。
|
|
100
|
+
|
|
101
|
+
- **只有感知作用域的 API 才会隔离状态**:注册表必须按 `scopeOf()` 归档,事件必须通过 `scopeTarget()` 分发;仅仅通过带作用域的上下文调用任意 Cordis 服务,并不会改变该服务仍为上下文全局这一事实。
|
|
102
|
+
- **一个上下文只携带一个最近的作用域键**:层级关系存在于键级父关系中而非上下文标签里;嵌套作用域上下文仍遮蔽为单一标签,多成员策略集仍不受支持。
|
|
103
|
+
- **服务可达性来自作用域创建者**:交出 `Scope.ctx` 也会交出创建插件注入的服务范围,因此,若作用域创建者提供的服务范围较宽,持有者之后也无法将其收窄。
|
|
104
|
+
|
|
105
|
+
<a id="dev-note"></a>
|
|
106
|
+
### 开发备注
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
110
|
+
|
|
111
|
+
无。
|
|
112
|
+
|
|
113
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { Context } from "@liusiqi-dev/cordis";
|
|
2
|
+
//#region lib/types/store.js
|
|
3
|
+
/**
|
|
4
|
+
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
|
|
5
|
+
*
|
|
6
|
+
* @module @liusiqi-dev/dsh-scope
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Insertion-ordered named entries with caller-owned duplicate diagnostics.
|
|
10
|
+
*
|
|
11
|
+
* Values are borrowed. Iterators are live within one nonempty table
|
|
12
|
+
* generation; draining the table detaches them from later insertions. Each
|
|
13
|
+
* successful insertion returns an idempotent undo for that exact entry.
|
|
14
|
+
*/
|
|
15
|
+
var NamedEntries = class {
|
|
16
|
+
duplicateError;
|
|
17
|
+
data = /* @__PURE__ */ new Map();
|
|
18
|
+
constructor(duplicateError) {
|
|
19
|
+
this.duplicateError = duplicateError;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Insert one unique name.
|
|
23
|
+
* @param name - name unique within this table.
|
|
24
|
+
* @param value - borrowed value to retain.
|
|
25
|
+
* @returns an idempotent undo that removes only this insertion.
|
|
26
|
+
*/
|
|
27
|
+
insert(name, value) {
|
|
28
|
+
const data = this.data;
|
|
29
|
+
if (data.has(name)) throw this.duplicateError(name);
|
|
30
|
+
data.set(name, value);
|
|
31
|
+
let active = true;
|
|
32
|
+
return () => {
|
|
33
|
+
if (!active) return;
|
|
34
|
+
active = false;
|
|
35
|
+
data.delete(name);
|
|
36
|
+
if (data.size === 0 && this.data === data) this.data = /* @__PURE__ */ new Map();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read one named value.
|
|
41
|
+
* @param name - name to resolve.
|
|
42
|
+
* @returns the retained value, or `undefined` when absent.
|
|
43
|
+
*/
|
|
44
|
+
get(name) {
|
|
45
|
+
return this.data.get(name);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Test one name for membership.
|
|
49
|
+
* @param name - name to test.
|
|
50
|
+
* @returns whether the table contains that name.
|
|
51
|
+
*/
|
|
52
|
+
has(name) {
|
|
53
|
+
return this.data.has(name);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Iterate live names in insertion order.
|
|
57
|
+
* @returns the native live key iterator.
|
|
58
|
+
*/
|
|
59
|
+
keys() {
|
|
60
|
+
return this.data.keys();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Iterate live entries in insertion order.
|
|
64
|
+
* @returns the native live entry iterator.
|
|
65
|
+
*/
|
|
66
|
+
entries() {
|
|
67
|
+
return this.data.entries();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Iterate live values in insertion order.
|
|
71
|
+
* @returns the native live value iterator.
|
|
72
|
+
*/
|
|
73
|
+
values() {
|
|
74
|
+
return this.data.values();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Test whether this table has no entries.
|
|
78
|
+
* @returns whether the table is empty.
|
|
79
|
+
*/
|
|
80
|
+
isEmpty() {
|
|
81
|
+
return this.data.size === 0;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Insertion-ordered anonymous entries with independent registration identity.
|
|
86
|
+
*
|
|
87
|
+
* Equal values remain separate registrations. Values are borrowed, and
|
|
88
|
+
* iterators are live within one nonempty table generation; draining the table
|
|
89
|
+
* detaches them from later appends.
|
|
90
|
+
*/
|
|
91
|
+
var AnonymousEntries = class {
|
|
92
|
+
data = /* @__PURE__ */ new Map();
|
|
93
|
+
/**
|
|
94
|
+
* Append one independently owned value.
|
|
95
|
+
* @param value - borrowed value to retain.
|
|
96
|
+
* @returns an idempotent undo for this exact append.
|
|
97
|
+
*/
|
|
98
|
+
append(value) {
|
|
99
|
+
const data = this.data;
|
|
100
|
+
const key = Symbol();
|
|
101
|
+
data.set(key, value);
|
|
102
|
+
let active = true;
|
|
103
|
+
return () => {
|
|
104
|
+
if (!active) return;
|
|
105
|
+
active = false;
|
|
106
|
+
data.delete(key);
|
|
107
|
+
if (data.size === 0 && this.data === data) this.data = /* @__PURE__ */ new Map();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Iterate live values in insertion order.
|
|
112
|
+
* @returns the native live value iterator.
|
|
113
|
+
*/
|
|
114
|
+
values() {
|
|
115
|
+
return this.data.values();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Test whether this table has no entries.
|
|
119
|
+
* @returns whether the table is empty.
|
|
120
|
+
*/
|
|
121
|
+
isEmpty() {
|
|
122
|
+
return this.data.size === 0;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Own the global and exact-scope layers for one registry.
|
|
127
|
+
*
|
|
128
|
+
* Reads never create scoped layers. Registrations derive both visibility and
|
|
129
|
+
* effect ownership from the supplied Cordis context, collect undo before
|
|
130
|
+
* notification, and reclaim only a completely empty aggregate layer.
|
|
131
|
+
*/
|
|
132
|
+
var ScopedLayers = class {
|
|
133
|
+
createLayer;
|
|
134
|
+
onChange;
|
|
135
|
+
/** The eagerly constructed context-global layer. */
|
|
136
|
+
global;
|
|
137
|
+
scoped = /* @__PURE__ */ new Map();
|
|
138
|
+
constructor(createLayer, onChange) {
|
|
139
|
+
this.createLayer = createLayer;
|
|
140
|
+
this.onChange = onChange;
|
|
141
|
+
this.global = createLayer(void 0);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Read an existing exact-scope overlay. Deliberately chain-blind: callers
|
|
145
|
+
* addressing one scope's OWN contributions (its restrictions, its guards)
|
|
146
|
+
* must not silently pick up an ancestor's — use {@link chainLayers} where
|
|
147
|
+
* inheritance is the point.
|
|
148
|
+
* @param scope - exact scope key; `undefined` denotes no overlay.
|
|
149
|
+
* @returns the existing scoped layer, or `undefined` without creating one.
|
|
150
|
+
*/
|
|
151
|
+
peek(scope) {
|
|
152
|
+
if (scope === void 0) return void 0;
|
|
153
|
+
return this.scoped.get(scope);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Existing overlays along the scope's parent chain ({@link scopeChainOf}),
|
|
157
|
+
* farthest ancestor first and the exact scope last, so a caller layering
|
|
158
|
+
* them in order gives the nearest scope the final word.
|
|
159
|
+
* @param scope - viewing scope, or `undefined` for no overlays.
|
|
160
|
+
* @returns the existing layers, nearest last; absent overlays are skipped.
|
|
161
|
+
*/
|
|
162
|
+
chainLayers(scope) {
|
|
163
|
+
const layers = [];
|
|
164
|
+
for (const key of scopeChainOf(scope).reverse()) {
|
|
165
|
+
const layer = this.scoped.get(key);
|
|
166
|
+
if (layer !== void 0) layers.push(layer);
|
|
167
|
+
}
|
|
168
|
+
return layers;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Materialize global named entries followed by scope-chain shadows,
|
|
172
|
+
* farthest ancestor first, so the nearest scope's entry wins a name.
|
|
173
|
+
* @param scope - viewing scope, or `undefined` for the global view.
|
|
174
|
+
* @param pick - select the named table from a layer.
|
|
175
|
+
* @returns an insertion-ordered effective map.
|
|
176
|
+
*/
|
|
177
|
+
merge(scope, pick) {
|
|
178
|
+
const merged = new Map(pick(this.global).entries());
|
|
179
|
+
for (const layer of this.chainLayers(scope)) for (const [name, value] of pick(layer).entries()) merged.set(name, value);
|
|
180
|
+
return merged;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Attach one synchronous layer mutation to its registration context.
|
|
184
|
+
* @param ctx - context that determines both scope visibility and effect ownership.
|
|
185
|
+
* @param action - atomic mutation returning its synchronous undo.
|
|
186
|
+
* @param options - Cordis effect label and optional change notification.
|
|
187
|
+
* @returns the exact disposer returned by `ctx.effect()`.
|
|
188
|
+
*/
|
|
189
|
+
effect(ctx, action, options) {
|
|
190
|
+
const scope = scopeOf(ctx);
|
|
191
|
+
const notify = options.notify ?? true;
|
|
192
|
+
return ctx.effect(function* () {
|
|
193
|
+
let layer;
|
|
194
|
+
let created = false;
|
|
195
|
+
if (scope === void 0) layer = this.global;
|
|
196
|
+
else {
|
|
197
|
+
const existing = this.scoped.get(scope);
|
|
198
|
+
if (existing === void 0) {
|
|
199
|
+
layer = this.createLayer(scope);
|
|
200
|
+
this.scoped.set(scope, layer);
|
|
201
|
+
created = true;
|
|
202
|
+
} else layer = existing;
|
|
203
|
+
}
|
|
204
|
+
let undo;
|
|
205
|
+
try {
|
|
206
|
+
undo = action(layer);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (scope !== void 0 && created && layer.isEmpty()) this.scoped.delete(scope);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
yield () => {
|
|
212
|
+
undo();
|
|
213
|
+
if (scope !== void 0 && layer.isEmpty()) this.scoped.delete(scope);
|
|
214
|
+
if (notify) this.onChange();
|
|
215
|
+
};
|
|
216
|
+
if (notify) this.onChange();
|
|
217
|
+
}.bind(this), options.label);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/types/index.js
|
|
222
|
+
/**
|
|
223
|
+
* Scoped-context primitive: mint a Cordis context that tags registrations with
|
|
224
|
+
* an opaque identity and build routing-only event carriers for that identity.
|
|
225
|
+
*
|
|
226
|
+
* @module @liusiqi-dev/dsh-scope
|
|
227
|
+
*/
|
|
228
|
+
/** Context tag written by {@link createScope}. */
|
|
229
|
+
const kScope = Symbol("dsh.scope");
|
|
230
|
+
/** The key associated with each carrier. Presence distinguishes an unkeyed carrier from a non-carrier. */
|
|
231
|
+
const carrierKeys = /* @__PURE__ */ new WeakMap();
|
|
232
|
+
/**
|
|
233
|
+
* The enclosing scope of each key. One relation powers both directions of
|
|
234
|
+
* scope nesting: registration views inherit DOWN the chain (a child scope
|
|
235
|
+
* sees its ancestors' layers — {@link ScopedLayers}), and event admission
|
|
236
|
+
* extends UP it (a listener tagged with an ancestor receives events dispatched
|
|
237
|
+
* to a descendant key — {@link scopeTarget}).
|
|
238
|
+
*/
|
|
239
|
+
const scopeParents = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
/** Cycle-checked write shared by the bind and every rebind. */
|
|
241
|
+
function linkScopeParent(key, parent) {
|
|
242
|
+
for (let cursor = parent; cursor !== void 0; cursor = scopeParents.get(cursor)) if (cursor === key) throw new Error("dsh-scope: scope parent link would form a cycle");
|
|
243
|
+
scopeParents.set(key, parent);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Bind `parent` as `key`'s enclosing scope, once.
|
|
247
|
+
*
|
|
248
|
+
* A key that already has a parent throws: there is no open re-link path, so a
|
|
249
|
+
* scope's ancestry cannot be moved by anyone but the original binder, who
|
|
250
|
+
* alone receives the {@link ScopeParentBinding}. A link that would close a
|
|
251
|
+
* cycle is rejected, because every chain consumer walks parents to the root.
|
|
252
|
+
* @param key - the child scope key.
|
|
253
|
+
* @param parent - its enclosing scope key.
|
|
254
|
+
* @returns the binding that alone may re-link this key.
|
|
255
|
+
*/
|
|
256
|
+
function bindScopeParent(key, parent) {
|
|
257
|
+
if (scopeParents.has(key)) throw new Error("dsh-scope: scope key is already bound to a parent; re-linking requires the binding returned by the original bind");
|
|
258
|
+
linkScopeParent(key, parent);
|
|
259
|
+
return { rebind(next) {
|
|
260
|
+
linkScopeParent(key, next);
|
|
261
|
+
} };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Read one key's enclosing scope.
|
|
265
|
+
* @param key - the scope key to inspect.
|
|
266
|
+
* @returns its parent key, or `undefined` for a root scope.
|
|
267
|
+
*/
|
|
268
|
+
function scopeParentOf(key) {
|
|
269
|
+
return scopeParents.get(key);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The chain from a key to its root ancestor.
|
|
273
|
+
* @param key - the starting key, or `undefined` for the empty chain.
|
|
274
|
+
* @returns keys nearest-first: `[key, parent, grandparent, …]`.
|
|
275
|
+
*/
|
|
276
|
+
function scopeChainOf(key) {
|
|
277
|
+
const chain = [];
|
|
278
|
+
for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) chain.push(cursor);
|
|
279
|
+
return chain;
|
|
280
|
+
}
|
|
281
|
+
/** Follow a Cordis fiber through asynchronous teardown even if its raw disposer was already claimed. */
|
|
282
|
+
async function quiesceFiber(fiber) {
|
|
283
|
+
await Promise.resolve(fiber.dispose());
|
|
284
|
+
while (fiber.inertia !== void 0) await fiber.inertia;
|
|
285
|
+
}
|
|
286
|
+
/** Shared no-op plugin used as the backing scope fiber. */
|
|
287
|
+
function scope() {}
|
|
288
|
+
/**
|
|
289
|
+
* Mint a scope under `ctx`. The scoped context inherits the minting plugin's
|
|
290
|
+
* dependency API and owns every registration made through it.
|
|
291
|
+
* @param ctx - active context whose dependency API the scope inherits.
|
|
292
|
+
* @param key - opaque identity used for listener routing.
|
|
293
|
+
* @param options - optional scope-chain placement.
|
|
294
|
+
* @returns the scoped context and exact/shared disposal boundaries.
|
|
295
|
+
*/
|
|
296
|
+
function createScope(ctx, key, options) {
|
|
297
|
+
if (options?.parent !== void 0) bindScopeParent(key, options.parent);
|
|
298
|
+
const fiber = ctx.plugin(scope);
|
|
299
|
+
const scoped = fiber.ctx.extend({ [kScope]: key });
|
|
300
|
+
let disposing;
|
|
301
|
+
return {
|
|
302
|
+
ctx: scoped,
|
|
303
|
+
rawDispose: fiber.dispose,
|
|
304
|
+
dispose: () => disposing ??= quiesceFiber(fiber)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Read the nearest scope tag inherited by a context.
|
|
309
|
+
* @param ctx - context to inspect.
|
|
310
|
+
* @returns its scope key, or `undefined` for an unscoped context.
|
|
311
|
+
*/
|
|
312
|
+
function scopeOf(ctx) {
|
|
313
|
+
return ctx[kScope];
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Build an opaque receiver that preserves the base filter, admits untagged
|
|
317
|
+
* listeners globally, and admits tagged listeners for a matching key or any
|
|
318
|
+
* of its ancestors ({@link bindScopeParent}): a listener owned by an enclosing
|
|
319
|
+
* scope receives every descendant scope's events, which is what lets one
|
|
320
|
+
* standing composition observe each of the agents composed under it. A tag
|
|
321
|
+
* BELOW the dispatch key stays excluded — events flow up the chain, never
|
|
322
|
+
* down.
|
|
323
|
+
* @param base - subject or service whose existing Cordis filter is preserved.
|
|
324
|
+
* @param key - routed scope identity, or `undefined` for an unscoped subject.
|
|
325
|
+
* @returns a carrier whose subject remains available only through event arguments.
|
|
326
|
+
*/
|
|
327
|
+
function scopeTarget(base, key) {
|
|
328
|
+
const baseFilter = base[Context.filter];
|
|
329
|
+
const carrier = { [Context.filter](ctx) {
|
|
330
|
+
if (baseFilter !== void 0 && !baseFilter.call(base, ctx)) return false;
|
|
331
|
+
const tag = scopeOf(ctx);
|
|
332
|
+
if (tag === void 0) return true;
|
|
333
|
+
for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) if (cursor === tag) return true;
|
|
334
|
+
return false;
|
|
335
|
+
} };
|
|
336
|
+
carrierKeys.set(carrier, key);
|
|
337
|
+
return carrier;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Test whether a value is a scope carrier.
|
|
341
|
+
* @param value - dispatch receiver to inspect.
|
|
342
|
+
* @returns whether {@link scopeTarget} created it.
|
|
343
|
+
*/
|
|
344
|
+
function isScopeCarrier(value) {
|
|
345
|
+
return typeof value === "object" && value !== null && carrierKeys.has(value);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Read a carrier's routing key.
|
|
349
|
+
* @param value - dispatch receiver to inspect.
|
|
350
|
+
* @returns the carrier key, or `undefined` for an unkeyed/non-carrier value.
|
|
351
|
+
*/
|
|
352
|
+
function carrierKeyOf(value) {
|
|
353
|
+
if (!isScopeCarrier(value)) return void 0;
|
|
354
|
+
return carrierKeys.get(value);
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
export { AnonymousEntries, NamedEntries, ScopedLayers, bindScopeParent, carrierKeyOf, createScope, isScopeCarrier, scopeChainOf, scopeOf, scopeParentOf, scopeTarget };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { carrierKeyOf, isScopeCarrier } from "@liusiqi-dev/dsh-scope";
|
|
2
|
+
//#region lib/types/scoped-events.generated.js
|
|
3
|
+
/**
|
|
4
|
+
* Generated scoped-event routing-subject resolvers for dsh-scope invariants.
|
|
5
|
+
* Do not edit by hand; run `pnpm run gen-scoped-events`.
|
|
6
|
+
*
|
|
7
|
+
* @module @liusiqi-dev/dsh-scope/scoped-events.generated
|
|
8
|
+
*/
|
|
9
|
+
const scopedSubjectResolvers = Object.freeze({
|
|
10
|
+
"agent/assistant-stream": (args) => args[0]["agent"],
|
|
11
|
+
"agent/created": (args) => args[0]["agent"],
|
|
12
|
+
"agent/disposed": (args) => args[0]["agent"],
|
|
13
|
+
"agent/error": (args) => args[0]["agent"],
|
|
14
|
+
"agent/inbox/claimed": (args) => args[0]["agent"],
|
|
15
|
+
"agent/inbox/discarded": (args) => args[0]["agent"],
|
|
16
|
+
"agent/inbox/inserted": (args) => args[0]["agent"],
|
|
17
|
+
"agent/pre-step": (args) => args[0]["agent"],
|
|
18
|
+
"agent/request": (args) => args[0]["agent"],
|
|
19
|
+
"agent/request-error": (args) => args[0]["agent"],
|
|
20
|
+
"agent/status": (args) => args[0]["agent"],
|
|
21
|
+
"agent/turn-stopping": (args) => args[0]["agent"],
|
|
22
|
+
"approval/request": (args) => args[0]["agent"],
|
|
23
|
+
"goal/changed": (args) => args[0]["agent"],
|
|
24
|
+
"session/created": null,
|
|
25
|
+
"session/disposed": null,
|
|
26
|
+
"session/event": null,
|
|
27
|
+
"session/flush": null,
|
|
28
|
+
"subagent/end": null,
|
|
29
|
+
"subagent/start": null,
|
|
30
|
+
"system-prompt/assemble": (args) => args[1]["scope"],
|
|
31
|
+
"tools/execute": (args) => args[0]["agent"],
|
|
32
|
+
"tools/post-execute": (args) => args[0]["agent"],
|
|
33
|
+
"tools/pre-execute": (args) => args[0]["agent"],
|
|
34
|
+
"tools/ptc-dispatch-log": (args) => args[0]["agent"],
|
|
35
|
+
"tools/result": (args) => args[0]["agent"],
|
|
36
|
+
"user-questions/request": (args) => args[0]["agent"]
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the routing key named by one scoped event payload. A null
|
|
40
|
+
* resolver means the payload cannot expose its external routing key, so the
|
|
41
|
+
* invariant checks carrier presence only.
|
|
42
|
+
* @param event - runtime Cordis event name.
|
|
43
|
+
* @returns the generated subject resolver, null for presence-only,
|
|
44
|
+
* or undefined when the event is not scope-filtered.
|
|
45
|
+
*/
|
|
46
|
+
function scopedSubjectResolverFor(event) {
|
|
47
|
+
return scopedSubjectResolvers[event];
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region lib/types/invariant.js
|
|
51
|
+
/** Package-owned scoped-dispatch invariants. @module @liusiqi-dev/dsh-scope/invariant */
|
|
52
|
+
const PACKAGE_NAME = "@liusiqi-dev/dsh-scope";
|
|
53
|
+
/** Cordis companion plugin name. */
|
|
54
|
+
const name = "scope-invariant";
|
|
55
|
+
/** Services required before the companion can register. */
|
|
56
|
+
const inject = ["invariants"];
|
|
57
|
+
/** Install the scoped-dispatch contribution into its child registration fiber. */
|
|
58
|
+
const install = (ctx, fail) => {
|
|
59
|
+
ctx.on("internal/dispatch", (_mode, eventName, args, thisArg) => {
|
|
60
|
+
const subjectOf = scopedSubjectResolverFor(eventName);
|
|
61
|
+
if (subjectOf === void 0) return;
|
|
62
|
+
if (!isScopeCarrier(thisArg)) fail(`"${eventName}" is a scope-filtered event but was dispatched without a scope carrier — pass scopeTarget(base, subject) as the dispatch thisArg (agent events: use agentEvents(ctx, agent))`);
|
|
63
|
+
if (subjectOf !== null && carrierKeyOf(thisArg) !== subjectOf(args)) fail(`"${eventName}" was dispatched with a scope carrier keyed to a DIFFERENT subject than its arguments name — the carrier key and the event's subject must be the same object (use agentEvents(ctx, agent))`);
|
|
64
|
+
}, { global: true });
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Register the scope invariant companion.
|
|
68
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
69
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
70
|
+
*/
|
|
71
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
72
|
+
//#endregion
|
|
73
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped-context primitive: mint a Cordis context that tags registrations with
|
|
3
|
+
* an opaque identity and build routing-only event carriers for that identity.
|
|
4
|
+
*
|
|
5
|
+
* @module @liusiqi-dev/dsh-scope
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@liusiqi-dev/cordis';
|
|
8
|
+
export { AnonymousEntries, NamedEntries, ScopedLayers } from './store.ts';
|
|
9
|
+
export type { ScopeLayer } from './store.ts';
|
|
10
|
+
/** An opaque, identity-compared scope key. */
|
|
11
|
+
export type ScopeKey = object;
|
|
12
|
+
declare const ScopedBrand: unique symbol;
|
|
13
|
+
/**
|
|
14
|
+
* A routing-only event receiver built by {@link scopeTarget}. The type
|
|
15
|
+
* parameter records the subject type for dispatch checking; the carrier does
|
|
16
|
+
* not expose the subject's properties. Event payloads carry the real subject.
|
|
17
|
+
*/
|
|
18
|
+
export type Scoped<T extends object> = object & {
|
|
19
|
+
readonly [ScopedBrand]: T;
|
|
20
|
+
};
|
|
21
|
+
/** The privileged handle to move one scope key's parent link. */
|
|
22
|
+
export interface ScopeParentBinding {
|
|
23
|
+
/**
|
|
24
|
+
* Re-link the bound key to a different parent, with the same cycle check as
|
|
25
|
+
* the bind. Valid only while nothing produced under the old parent is
|
|
26
|
+
* retained — the blank-session recompose contract, which the holder upholds
|
|
27
|
+
* because this relation cannot see what a session logged.
|
|
28
|
+
* @param parent - the new enclosing scope key.
|
|
29
|
+
*/
|
|
30
|
+
rebind(parent: ScopeKey): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Bind `parent` as `key`'s enclosing scope, once.
|
|
34
|
+
*
|
|
35
|
+
* A key that already has a parent throws: there is no open re-link path, so a
|
|
36
|
+
* scope's ancestry cannot be moved by anyone but the original binder, who
|
|
37
|
+
* alone receives the {@link ScopeParentBinding}. A link that would close a
|
|
38
|
+
* cycle is rejected, because every chain consumer walks parents to the root.
|
|
39
|
+
* @param key - the child scope key.
|
|
40
|
+
* @param parent - its enclosing scope key.
|
|
41
|
+
* @returns the binding that alone may re-link this key.
|
|
42
|
+
*/
|
|
43
|
+
export declare function bindScopeParent(key: ScopeKey, parent: ScopeKey): ScopeParentBinding;
|
|
44
|
+
/**
|
|
45
|
+
* Read one key's enclosing scope.
|
|
46
|
+
* @param key - the scope key to inspect.
|
|
47
|
+
* @returns its parent key, or `undefined` for a root scope.
|
|
48
|
+
*/
|
|
49
|
+
export declare function scopeParentOf(key: ScopeKey): ScopeKey | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The chain from a key to its root ancestor.
|
|
52
|
+
* @param key - the starting key, or `undefined` for the empty chain.
|
|
53
|
+
* @returns keys nearest-first: `[key, parent, grandparent, …]`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function scopeChainOf(key: ScopeKey | undefined): ScopeKey[];
|
|
56
|
+
/** A minted registration scope and its quiescent disposal boundaries. */
|
|
57
|
+
export interface Scope {
|
|
58
|
+
/** Context through which scope-owned registrations are made. */
|
|
59
|
+
ctx: Context;
|
|
60
|
+
/** Exact Cordis disposer, used when nesting this scope in an ordered composite effect. */
|
|
61
|
+
rawDispose: () => Promise<void> | void;
|
|
62
|
+
/** Dispose every scope-owned registration; racing calls await the same completion. */
|
|
63
|
+
dispose(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/** Options accepted by {@link createScope}. */
|
|
66
|
+
export interface CreateScopeOptions {
|
|
67
|
+
/** Enclosing scope bound via {@link bindScopeParent} before the scope is usable; the binding stays internal. */
|
|
68
|
+
parent?: ScopeKey;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Mint a scope under `ctx`. The scoped context inherits the minting plugin's
|
|
72
|
+
* dependency API and owns every registration made through it.
|
|
73
|
+
* @param ctx - active context whose dependency API the scope inherits.
|
|
74
|
+
* @param key - opaque identity used for listener routing.
|
|
75
|
+
* @param options - optional scope-chain placement.
|
|
76
|
+
* @returns the scoped context and exact/shared disposal boundaries.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createScope(ctx: Context, key: ScopeKey, options?: CreateScopeOptions): Scope;
|
|
79
|
+
/**
|
|
80
|
+
* Read the nearest scope tag inherited by a context.
|
|
81
|
+
* @param ctx - context to inspect.
|
|
82
|
+
* @returns its scope key, or `undefined` for an unscoped context.
|
|
83
|
+
*/
|
|
84
|
+
export declare function scopeOf(ctx: Context): ScopeKey | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Build an opaque receiver that preserves the base filter, admits untagged
|
|
87
|
+
* listeners globally, and admits tagged listeners for a matching key or any
|
|
88
|
+
* of its ancestors ({@link bindScopeParent}): a listener owned by an enclosing
|
|
89
|
+
* scope receives every descendant scope's events, which is what lets one
|
|
90
|
+
* standing composition observe each of the agents composed under it. A tag
|
|
91
|
+
* BELOW the dispatch key stays excluded — events flow up the chain, never
|
|
92
|
+
* down.
|
|
93
|
+
* @param base - subject or service whose existing Cordis filter is preserved.
|
|
94
|
+
* @param key - routed scope identity, or `undefined` for an unscoped subject.
|
|
95
|
+
* @returns a carrier whose subject remains available only through event arguments.
|
|
96
|
+
*/
|
|
97
|
+
export declare function scopeTarget<T extends object>(base: T, key: ScopeKey | undefined): Scoped<T>;
|
|
98
|
+
/**
|
|
99
|
+
* Test whether a value is a scope carrier.
|
|
100
|
+
* @param value - dispatch receiver to inspect.
|
|
101
|
+
* @returns whether {@link scopeTarget} created it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function isScopeCarrier(value: unknown): value is Scoped<object>;
|
|
104
|
+
/**
|
|
105
|
+
* Read a carrier's routing key.
|
|
106
|
+
* @param value - dispatch receiver to inspect.
|
|
107
|
+
* @returns the carrier key, or `undefined` for an unkeyed/non-carrier value.
|
|
108
|
+
*/
|
|
109
|
+
export declare function carrierKeyOf(value: unknown): ScopeKey | undefined;
|
|
110
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned scoped-dispatch invariants. @module @liusiqi-dev/dsh-scope/invariant */
|
|
2
|
+
import type { Context } from '@liusiqi-dev/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "scope-invariant";
|
|
5
|
+
/** Services required before the companion can register. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the scope invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated scoped-event routing-subject resolvers for dsh-scope invariants.
|
|
3
|
+
* Do not edit by hand; run `pnpm run gen-scoped-events`.
|
|
4
|
+
*
|
|
5
|
+
* @module @liusiqi-dev/dsh-scope/scoped-events.generated
|
|
6
|
+
*/
|
|
7
|
+
type ScopedSubjectResolver = (args: readonly unknown[]) => unknown;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the routing key named by one scoped event payload. A null
|
|
10
|
+
* resolver means the payload cannot expose its external routing key, so the
|
|
11
|
+
* invariant checks carrier presence only.
|
|
12
|
+
* @param event - runtime Cordis event name.
|
|
13
|
+
* @returns the generated subject resolver, null for presence-only,
|
|
14
|
+
* or undefined when the event is not scope-filtered.
|
|
15
|
+
*/
|
|
16
|
+
export declare function scopedSubjectResolverFor(event: string): ScopedSubjectResolver | null | undefined;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=scoped-events.generated.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared insertion-ordered storage and effect ownership for scope-aware registries.
|
|
3
|
+
*
|
|
4
|
+
* @module @liusiqi-dev/dsh-scope
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@liusiqi-dev/cordis';
|
|
7
|
+
import type { ScopeKey } from './index.ts';
|
|
8
|
+
/** One scope's aggregate contribution to a registry. */
|
|
9
|
+
export interface ScopeLayer {
|
|
10
|
+
/** Whether every table in this layer is empty. */
|
|
11
|
+
isEmpty(): boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Internal common read contract for the two entry-table implementations. */
|
|
14
|
+
interface EntryValues<V> {
|
|
15
|
+
values(): IterableIterator<V>;
|
|
16
|
+
isEmpty(): boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Insertion-ordered named entries with caller-owned duplicate diagnostics.
|
|
20
|
+
*
|
|
21
|
+
* Values are borrowed. Iterators are live within one nonempty table
|
|
22
|
+
* generation; draining the table detaches them from later insertions. Each
|
|
23
|
+
* successful insertion returns an idempotent undo for that exact entry.
|
|
24
|
+
*/
|
|
25
|
+
export declare class NamedEntries<V> implements EntryValues<V> {
|
|
26
|
+
private readonly duplicateError;
|
|
27
|
+
private data;
|
|
28
|
+
constructor(duplicateError: (name: string) => Error);
|
|
29
|
+
/**
|
|
30
|
+
* Insert one unique name.
|
|
31
|
+
* @param name - name unique within this table.
|
|
32
|
+
* @param value - borrowed value to retain.
|
|
33
|
+
* @returns an idempotent undo that removes only this insertion.
|
|
34
|
+
*/
|
|
35
|
+
insert(name: string, value: V): () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Read one named value.
|
|
38
|
+
* @param name - name to resolve.
|
|
39
|
+
* @returns the retained value, or `undefined` when absent.
|
|
40
|
+
*/
|
|
41
|
+
get(name: string): V | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Test one name for membership.
|
|
44
|
+
* @param name - name to test.
|
|
45
|
+
* @returns whether the table contains that name.
|
|
46
|
+
*/
|
|
47
|
+
has(name: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Iterate live names in insertion order.
|
|
50
|
+
* @returns the native live key iterator.
|
|
51
|
+
*/
|
|
52
|
+
keys(): IterableIterator<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Iterate live entries in insertion order.
|
|
55
|
+
* @returns the native live entry iterator.
|
|
56
|
+
*/
|
|
57
|
+
entries(): IterableIterator<[string, V]>;
|
|
58
|
+
/**
|
|
59
|
+
* Iterate live values in insertion order.
|
|
60
|
+
* @returns the native live value iterator.
|
|
61
|
+
*/
|
|
62
|
+
values(): IterableIterator<V>;
|
|
63
|
+
/**
|
|
64
|
+
* Test whether this table has no entries.
|
|
65
|
+
* @returns whether the table is empty.
|
|
66
|
+
*/
|
|
67
|
+
isEmpty(): boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Insertion-ordered anonymous entries with independent registration identity.
|
|
71
|
+
*
|
|
72
|
+
* Equal values remain separate registrations. Values are borrowed, and
|
|
73
|
+
* iterators are live within one nonempty table generation; draining the table
|
|
74
|
+
* detaches them from later appends.
|
|
75
|
+
*/
|
|
76
|
+
export declare class AnonymousEntries<V> implements EntryValues<V> {
|
|
77
|
+
private data;
|
|
78
|
+
/**
|
|
79
|
+
* Append one independently owned value.
|
|
80
|
+
* @param value - borrowed value to retain.
|
|
81
|
+
* @returns an idempotent undo for this exact append.
|
|
82
|
+
*/
|
|
83
|
+
append(value: V): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Iterate live values in insertion order.
|
|
86
|
+
* @returns the native live value iterator.
|
|
87
|
+
*/
|
|
88
|
+
values(): IterableIterator<V>;
|
|
89
|
+
/**
|
|
90
|
+
* Test whether this table has no entries.
|
|
91
|
+
* @returns whether the table is empty.
|
|
92
|
+
*/
|
|
93
|
+
isEmpty(): boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Own the global and exact-scope layers for one registry.
|
|
97
|
+
*
|
|
98
|
+
* Reads never create scoped layers. Registrations derive both visibility and
|
|
99
|
+
* effect ownership from the supplied Cordis context, collect undo before
|
|
100
|
+
* notification, and reclaim only a completely empty aggregate layer.
|
|
101
|
+
*/
|
|
102
|
+
export declare class ScopedLayers<L extends ScopeLayer> {
|
|
103
|
+
private readonly createLayer;
|
|
104
|
+
private readonly onChange;
|
|
105
|
+
/** The eagerly constructed context-global layer. */
|
|
106
|
+
readonly global: L;
|
|
107
|
+
private readonly scoped;
|
|
108
|
+
constructor(createLayer: (scope: ScopeKey | undefined) => L, onChange: () => void);
|
|
109
|
+
/**
|
|
110
|
+
* Read an existing exact-scope overlay. Deliberately chain-blind: callers
|
|
111
|
+
* addressing one scope's OWN contributions (its restrictions, its guards)
|
|
112
|
+
* must not silently pick up an ancestor's — use {@link chainLayers} where
|
|
113
|
+
* inheritance is the point.
|
|
114
|
+
* @param scope - exact scope key; `undefined` denotes no overlay.
|
|
115
|
+
* @returns the existing scoped layer, or `undefined` without creating one.
|
|
116
|
+
*/
|
|
117
|
+
peek(scope: ScopeKey | undefined): L | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Existing overlays along the scope's parent chain ({@link scopeChainOf}),
|
|
120
|
+
* farthest ancestor first and the exact scope last, so a caller layering
|
|
121
|
+
* them in order gives the nearest scope the final word.
|
|
122
|
+
* @param scope - viewing scope, or `undefined` for no overlays.
|
|
123
|
+
* @returns the existing layers, nearest last; absent overlays are skipped.
|
|
124
|
+
*/
|
|
125
|
+
chainLayers(scope: ScopeKey | undefined): L[];
|
|
126
|
+
/**
|
|
127
|
+
* Materialize global named entries followed by scope-chain shadows,
|
|
128
|
+
* farthest ancestor first, so the nearest scope's entry wins a name.
|
|
129
|
+
* @param scope - viewing scope, or `undefined` for the global view.
|
|
130
|
+
* @param pick - select the named table from a layer.
|
|
131
|
+
* @returns an insertion-ordered effective map.
|
|
132
|
+
*/
|
|
133
|
+
merge<V>(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries<V>): Map<string, V>;
|
|
134
|
+
/**
|
|
135
|
+
* Attach one synchronous layer mutation to its registration context.
|
|
136
|
+
* @param ctx - context that determines both scope visibility and effect ownership.
|
|
137
|
+
* @param action - atomic mutation returning its synchronous undo.
|
|
138
|
+
* @param options - Cordis effect label and optional change notification.
|
|
139
|
+
* @returns the exact disposer returned by `ctx.effect()`.
|
|
140
|
+
*/
|
|
141
|
+
effect(ctx: Context, action: (layer: L) => () => void, options: {
|
|
142
|
+
label: string;
|
|
143
|
+
notify?: boolean;
|
|
144
|
+
}): () => void;
|
|
145
|
+
}
|
|
146
|
+
export {};
|
|
147
|
+
//# sourceMappingURL=store.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liusiqi-dev/dsh-scope",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/core/scope"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@liusiqi-dev/dsh-invariants": "1.0.0",
|
|
36
|
+
"@liusiqi-dev/cordis": "~4.0.4"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@liusiqi-dev/dsh-invariants": "1.0.0",
|
|
40
|
+
"@liusiqi-dev/cordis": "~4.0.4"
|
|
41
|
+
}
|
|
6
42
|
}
|