【deepseek-harness】Cordis 时空可组合性编程范式 — 三段式精读笔记(四)

Cordis 时空可组合性编程范式 --- 三段式精读笔记(四)

第5章 实现与案例研究

本文档采用三段式结构:每节先给出英文原文,再给出中文翻译,最后给出详细解释说明。

本章把理论落地为 Cordis 框架,并用 Koishi 做案例验证。


5 Implementation and Case Study / 实现与案例研究

原文 (English)

This section presents Cordis, which realizes the formal models of Section 3 as a practical programming abstraction. Cordis is a meta-framework of spatiotemporal composability: unlike application frameworks that target a specific domain (e.g., web routing, ORM, UI rendering), it prescribes no concrete scenario; its sole responsibility is to supply universal dynamic composition semantics. The implementation is layered into three tiers: (1) the core library (Section 5.1) implements the effect and coeffect systems directly; (2) the component loader (Section 5.2) extends the core with configuration reconciliation and hot module replacement; and (3) application frameworks such as Koishi (Section 5.3) build domain-specific functionality on top of the former two tiers.

中文翻译

本节介绍 Cordis,它把第3章的形式化模型实现为一个实用的编程抽象。Cordis 是一个时空可组合性的元框架(meta-framework):与针对特定领域的应用框架(如 web 路由、ORM、UI 渲染)不同,它不预设任何具体场景,其唯一职责是提供通用的动态组合语义。实现分为三层:(1) 核心库(5.1节)直接实现效应系统与协效应系统;(2) 组件加载器(5.2节)在核心之上扩展配置协调(configuration reconciliation)与热模块替换(hot module replacement);(3) 应用框架如 Koishi(5.3节)在前两层之上构建领域特定功能。

详细解释

Cordis 把第3章的双系统(效应 effect / 协效应 coeffect)翻译成可运行代码,关键定位是"元框架"------它不解决业务,只规定"组件如何动态组装与拆卸"。这种分层与 DSH 自身的体系高度同构:DSH 宿主(dsh web 进程)扮演元框架角色,提供 ctx 上下文、ctx.effect、ctx.inject、ctx.use 等组合原语;而各类插件(梁神模式 dsh-liangshen、SSH dsh-ssh、任务看板 dsh-task-board)则是运行在其上的"应用框架/组件",只贡献领域词汇。三层划分(核心库→加载器→应用框架)也对应到 Cordis 仓库结构:@cordisjs/core 提供原语,@cordisjs/loader 提供声明式配置与 HMR,Koishi 等上层框架只复用前两者。这种"不预设场景"的设计让同一套组合语义既能跑在 Node.js 服务端做机器人,也能跑在浏览器做控制台,正是后文 5.3 案例要验证的通用性。


5.1 Core Library / 核心库

原文 (English)

Table 2 summarizes the correspondence between theoretical constructs and their runtime counterparts. In particular, we use the runtime names introduced below throughout this section, reserving the theoretical symbols for the formal correspondence. We also write @@name for a framework-internal symbol key, so the brackets in ctx@@store denote symbol-keyed access to an opaque slot on the context, rather than indexing into a string-keyed map.

Table 2 \| Theory-to-implementation correspondence --- 选录关键映射

  • Γ∞ → ctx, the first-class context
  • 𝛾 ∈ Γ → the context tree together with everything the running system has touched
  • 𝔈Γ, 𝔈iterΓ → Effect callback returning / yielding inverses
  • effectΓ(𝑒) → ctx.effect(callback)
  • Σ, Σiso, Σinter → ctx@@store, ctx@@isolate, ctx@@intercept
  • get(𝑘), set(𝑘, 𝑣) → ctx.get(key), ctx.set(key, value)
  • isolate(𝑘, 𝑟) → ctx.isolate(key, realm)
  • intercept(𝑘, 𝜈) → ctx.intercept(key, metadata)
  • ⟨𝑑, 𝑝, 𝑒, 𝜋, 𝜎, 𝜏, 𝜃⟩ → fiber, the instantiation of a component in ℭΓ
  • dom(𝐹𝛾) → enumerated through ctx.registry
  • 𝑛 : 𝔑 → fiber.uid
  • 𝑑 : 𝔇Γ → fiber.inject
  • 𝑝 : 𝔓Γ → the component's provide
  • 𝑒 : 𝔈∗Γ → fiber.apply
  • 𝜋 : 𝔑 → fiber.parent.fiber.uid, the fiber owning the context it was instantiated on
  • derived realization (Definition 27) → fiber.ctx, the child context the fiber runs in
  • 𝜃 (Definition 44) → fiber.state, the lifecycle state, whose LOADING is 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 and whose FAILED is 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜉)
  • recover, accumulator 𝑔 → fiber.dispose, the accumulator
  • 𝜔 (Definition 44) → fiber.committed, the committed view
  • provider𝑘(𝛾) → an Impl whose provider fiber is ACTIVE
  • target(𝛾, 𝑛) → fiber.target, recomputed by refresh (Algorithm 5), where ⊥ is INACTIVE
  • 𝖥𝗎𝗍𝗎𝗋𝖾, inertia (Section 4.3.3) → fiber.inertia, the handle of the transition in flight
  • O-Insert, O-Retire (Definition 47) → ctx.use and the inverse of its callback (Algorithm 4)
  • O-Remove → the fiber dropped from its runtime, with uid cleared
  • L-Begin, L-Iter, L-Finish → execute's iteration loop (Algorithm 1)
  • L-Divert → the guard failing at an iteration boundary (Algorithm 1), or reload chaining into unload
  • L-Leave → refresh marking the fiber UNLOADING (Line 10)
  • L-Unload → unload and its inertial chaining (Algorithm 5)
  • guard on L-Unload → unload awaiting the notified dependents (Line 25)
  • L-Raise → the error recorded on the fiber, with its target set to ⊥

The remainder of this section builds the core library from the bottom up. Section 5.1.1 realizes revertible effects, the sole primitive through which a context is mutated; Section 5.1.2 realizes reactive coeffects over it; Section 5.1.3 composes both into the component lifecycle; and Section 5.1.4 exposes the context-level operations built on them.

中文翻译

表2总结了理论构造与运行时对应物之间的对应关系。特别地,本节通篇使用下面引入的运行时名称,而把理论符号留给形式化对应。我们还把框架内部符号键写作 @@name,因此 ctx@@store 中的方括号表示对一个不透明槽位的符号键访问,而不是对字符串键映射的索引。

表2 理论到实现的对应关系------选录关键映射见原文表格,涵盖 Γ∞→ctx、effectΓ→ctx.effect、Σ/Σiso/Σinter→ctx\[@@store\]/ctx\[@@isolate\]/ctx\[@@intercept\]、fiber 各字段、target/inertia/committed 状态、以及 O-/L- 系列操作标签的运行时落点。

本节余下部分自底向上构建核心库:5.1.1实现可逆效应(revertible effects),这是变更上下文的唯一原语;5.1.2在其上实现响应式协效应(reactive coeffects);5.1.3把二者组合成组件生命周期;5.1.4暴露建立其上的上下文级操作。

详细解释

表2是整章的"翻译字典",把第3、4章的数学符号逐一绑定到 Cordis 源码里的运行时名字。最核心的几条:一阶上下文 Γ∞ 就是 ctx;效应 effectΓ 落地为 ctx.effect;协效应三层存储 Σ/Σiso/Σinter 落地为 ctx 上三个用 Symbol 键访问的内部槽 @@store/@@isolate/@@intercept------用 Symbol 而非字符串键,既避免与用户属性冲突,也对外保持不透明。一个组件实例就是一个 fiber,它把理论元组 ⟨𝑑,𝑝,𝑒,𝜋,𝜎,𝜏,𝜃⟩ 摊平为 fiber.inject / fiber.apply / fiber.parent / fiber.state / fiber.committed / fiber.target / fiber.inertia 等字段。在 DSH/Cordis 实际 API 中,开发者面对的就是这套运行时名:用 ctx.effect(() => { ... return () => cleanup }) 注册带逆的效应、用 ctx.inject/Service 声明协效应依赖、用 ctx.use(component) 实例化子组件。表里 O-/L- 前缀的标签(O-Insert/O-Retire/O-Remove、L-Begin/L-Iter/L-Finish/L-Divert/L-Leave/L-Unload/L-Raise)对应第4章的操作语义,分别映射到 ctx.use 的回调、execute 的迭代循环、refresh 标记 UNLOADING、unload 等待依赖者等具体代码行------这种"每条理论操作都能点到某一行算法"的对应,是论文可 mechanized 验证的基础。本节按"效应原语→协效应→生命周期→上下文访问"的顺序自底向上搭建,正是依赖关系所要求的次序:协效应的 set 本身是效应,生命周期复用 execute,上下文访问又建立在 committed view 之上。


5.1.1 Effect Tracking / 效应跟踪

原文 (English)

This section realizes revertible effects (Section 3.1). Every context mutation in Cordis flows through a single primitive, ctx.effect: coeffect provision, component instantiation, and every other context-mutating operation reduces to a ctx.effect call, so any operation performed through the context is automatically tracked and recovered upon component unloading. Operationally, ctx.effect is the realization of effectiterΓ (Definition 52): it takes a callback of type 𝔈iterΓ and lifts it to 𝔈iter𝜕Γ, yielding a dispose closure that, when invoked, recovers the effect. Cordis accepts both 𝔈Γ and 𝔈iterΓ through this one operation (ad-hoc polymorphism); we take the iterator form as representative, since a plain effect function is the degenerate iterator that yields a single inverse. What the operation does not check is the witness that 𝔈∗Γ carries: the callback supplies an inverse, and that the inverse recovers the effect it accompanies is an obligation on the component author rather than a property the runtime verifies. Theorem 61 is where the calculus appeals to it, and Section 6.1 is where the obligation is delimited.

Algorithm 1 shows the construction of ctx.effect. We write 𝑓 ∘ 𝑔 for the disposer that runs 𝑓 after 𝑔, and id for the no-op; prepending each new inverse therefore yields LIFO recovery.

Algorithm 1 Effect tracking

复制代码
1  async function execute(callback, guard)
2    iter ← callback()
3    inverse ← id
4    while guard()
5      (value, done) ← await iter.next()
6      if value then inverse ← value ∘ inverse
7      if done then break
8    return inverse
9  function effect(ctx, callback)
10   armed ← true
11   task ← execute(callback, () ↦ armed)
12   async function dispose()
13     if not armed then return
14     armed ← false
15     recover ← await task
16     recover()
17   ctx.dispose ← dispose ∘ ctx.dispose
18   return dispose

The engine execute drives the callback as an effect iterator (𝔈iterΓ, Definition 51) and folds the inverse yielded at each step into a single composite. Before each step it consults a caller-supplied guard; once the guard trips, iteration stops and only the inverses accumulated so far remain. This is the step-boundary interruption of Section 4.3.2: the 𝖬𝖺𝗒𝖻𝖾(𝔈iter) continuation is realized by the iterator's done flag together with guard.

ctx.effect is a thin wrapper over execute that adds two things. First, self-disposal: the guard reports the armed flag, and the returned dispose flips armed to false, which simultaneously halts any in-flight iteration and makes recovery fire at most once. Firing twice would apply an inverse at a state no application of the effect produced, where nothing holds it to reverting anything. Second, parent composition: dispose is prepended to the enclosing context's accumulated inverse ctx.dispose, so a child effect's inverse is itself an effect on the parent, which is the recursive structure of 𝜕2Γ. The component level (Section 5.1.3) reuses the same execute with a guard that tests the stability of fiber.target instead of armed.

中文翻译

本节实现可逆效应(revertible effects,3.1节)。Cordis 中每一次上下文变更都流经唯一原语 ctx.effect:协效应的提供、组件实例化,以及所有其他变更上下文的操作,都归约为一次 ctx.effect 调用,因此任何经由上下文执行的操作都会被自动跟踪,并在组件卸载时自动恢复。在操作层面,ctx.effect 是 effectiterΓ(定义52)的实现:它接受一个类型为 𝔈iterΓ 的回调,将其提升为 𝔈iter𝜕Γ,产出一个 dispose 闭包,调用它即可恢复该效应。Cordis 通过这一个操作同时接受 𝔈Γ 与 𝔈iterΓ(临时多态,ad-hoc polymorphism);我们以迭代器形式为代表,因为普通效应函数就是只产出一个逆(inverse)的退化迭代器。该操作不检查 𝔈∗Γ 所携带的见证(witness):回调提供逆,而"逆确实恢复了它所伴随的效应"是组件作者的义务,而非运行时验证的性质。定理61是演算援引它的地方,6.1节则是该义务被界定之处。

算法1展示 ctx.effect 的构造。我们用 𝑓 ∘ 𝑔 表示先运行 𝑔 再运行 𝑓 的销毁器,用 id 表示空操作;因此每产出一个新逆就前缀拼接,得到 LIFO(后进先出)恢复。

算法1 效应跟踪------见原文伪代码

引擎 execute 把回调当作效应迭代器(𝔈iterΓ,定义51)驱动,并把每步产出的逆折叠成一个复合逆。每步之前它查询调用方提供的 guard;一旦 guard 触发,迭代停止,只保留此前累积的逆。这就是4.3.2节的步边界中断(step-boundary interruption):𝖬𝖺𝗒𝖻𝖾(𝔈iter) 延续由迭代器的 done 标志加上 guard 共同实现。

ctx.effect 是 execute 之上的薄封装,增加两件事。第一,自销毁(self-disposal):guard 汇报 armed 标志,返回的 dispose 把 armed 翻为 false,这同时中止任何进行中的迭代,并使恢复至多触发一次。触发两次会把逆应用到一个没有任何效应产生过的状态,那时没有任何东西保证它还能还原什么。第二,父组合(parent composition):dispose 被前缀拼接到外层上下文累积的逆 ctx.dispose 上,于是子效应的逆本身也是父上下文上的一个效应,这正是 𝜕2Γ 的递归结构。组件层(5.1.3节)复用同一个 execute,只是把 guard 换成测试 fiber.target 的稳定性,而非 armed。

详细解释

这一节是整个 Cordis 的"原语层":所有变更都收口到 ctx.effect,因此"跟踪+恢复"是自动的、不可绕过的。映射到 DSH 实际用法,最常见的形态是 ctx.effect(() => { const timer = setInterval(...); return () => clearInterval(timer) })------回调里干副作用,return 出来的函数就是 inverse,组件卸载时框架自动调用它清理定时器。算法1的 execute 把回调当 async generator 驱动:每 yield 一个清理函数就 LIFO 拼进 composite inverse,guard 一旦为假就停止迭代,只回滚已累积部分------这正是4.3.2节"步边界中断"的落地,使得"长事务中途被卸载"也能干净部分回滚,而不是非要跑完。两个工程细节值得记:armed 标志保证 dispose 至多触发一次(避免重复应用逆到错误状态),dispose 被 prepend 到 ctx.dispose 形成父子链------所以卸载父 ctx 会级联触发所有子效应的逆,递归结构对应 𝜕²Γ。一个微妙点是论文坦承的"义务":框架不验证你 return 的函数真的还原了副作用,写错了 inverse 是作者的责任(6.1节会界定这个义务边界)。这种"唯一原语+自动跟踪"的设计直接对应到 Koishi/DSH 里"插件作者无需手写 uninstall 路径"的体验------5.3会专门强调这一点。


5.1.2 Coeffect Operations / 协效应操作

原文 (English)

This section realizes reactive coeffects (Section 3.2). All coeffect operations act on three symbol-keyed slots that each context carries:

• @@store: the value store 𝜎 : (𝑟 : 𝑅) ⇀ 𝒱︀𝑟 from realm symbols to typed values;

• @@isolate: the realm table 𝜌 : Map(𝐾, 𝑅) from coeffect keys to realm symbols;

• @@intercept: the interception table 𝜄 : (𝑘 : 𝐾) → ℳ︀𝑘 assigning each key its metadata.

The first two compose into the two-layer resolution 𝑘 → 𝜌(𝑘) → 𝜎(𝜌(𝑘)): ctx.get(key) (Algorithm 2) reads the realm symbol 𝜌(𝑘) from @@isolate, then the bound value 𝜎(𝜌(𝑘)) from @@store. The 𝜌 indirection lets isolation redirect a key to an independent binding, whereas @@intercept is consulted only when a binding is accessed, adjusting how it is used rather than what it resolves to. We realize these operations in two parts: (1) provision and notification, which install or retract bindings and propagate the change to dependents; and (2) isolation and interception, which reshape how a key resolves.

Provision and notification. Since set(𝑘, 𝑣) has type 𝔈Σ (Section 3.1), coeffect provision is a ctx.effect call and inherits its automatic tracking and recovery. Algorithm 2 implements ctx.set(key, value), the concrete set(𝑘, 𝑣): the callback binds a value into the store under the realm symbol 𝜌(𝑘), and the returned dispose function removes it. Both installation and removal invoke notify to propagate the change to dependent components.

Algorithm 2 Coeffect operations

复制代码
1  function get(ctx, key)
2    realm ← ctx[@@isolate][key]   ▷ 𝜌(𝑘)
3    return ctx[@@store][realm]    ▷ 𝜎(𝜌(𝑘))
4  function set(ctx, key, value)
5    function callback()
6      realm ← ctx[@@isolate][key] ▷ 𝜌(𝑘)
7      ctx[@@store][realm] ← value ▷ 𝜎[𝜌(𝑘) ↦ 𝑣]
8      notify(ctx, [key])
9      return function()
10       delete ctx[@@store][realm] ▷ 𝜎 ∖ 𝜌(𝑘)
11       notify(ctx, [key])
12   return ctx.effect(callback)

Algorithm 3 propagates each binding change to dependents by testing, for each live fiber, whether a changed key appears in its fiber.inject and resolves to the same realm; if so, it calls refresh (Section 5.1.3) to re-evaluate that fiber against the new state, and it returns the fibers it re-evaluated so that a caller can wait for them. This is the reactive classification of Definition 26: a change that flips satisfaction activates or deactivates the fiber, and refresh's idempotence renders a neutral change harmless. The interaction of this re-evaluation with diverse control flows is developed in Section 5.1.3.

Algorithm 3 Reactive notification

复制代码
1  function notify(ctx, keys)
2    affected ← ⌀
3    for fiber in all_fibers do
4      for key in keys do
5        if key ∈ fiber.inject and fiber.ctx[@@isolate][key] = ctx[@@isolate][key] then
6          refresh(fiber)
7          affected ← affected ∪ {fiber}
8          break
9    return affected

A binding counts as available to a dependent only while the fiber that installed it is ACTIVE, so refresh resolves each declared key against an active provider rather than against the store alone. This is the provided by relation of Definition 46, and it is what makes a withdrawal visible to dependents one step before it happens: a provider that has entered UNLOADING has stopped providing, so its dependents recompute an unsatisfied target view and begin their own teardown while its bindings are all still in place.

Isolation and interception. The two operations do structurally the same thing: each derives a child context that adjusts one inherited table for key, leaving the parent untouched, so recovery is implicit: discarding the child context suffices, with no explicit inverse to run. ctx.isolate(key, realm) overrides the realm mapping 𝜌 with realm, or a freshly generated symbol by default (realizing isolate, Definition 29), so two contexts that assign different symbols to the same key resolve to independent bindings. ctx.intercept(key, metadata) merges metadata into the interception table 𝜄 (realizing intercept, Definition 31): following that definition, the new metadata is combined with whatever the context already carries for key and takes priority over it.

中文翻译

本节实现响应式协效应(reactive coeffects,3.2节)。所有协效应操作都作用于每个上下文携带的三个符号键槽:

• @@store:值存储 𝜎 : (𝑟 : 𝑅) ⇀ 𝒱︀𝑟,从领域符号(realm symbol)到类型化值;

• @@isolate:领域表 𝜌 : Map(𝐾, 𝑅),从协效应键到领域符号;

• @@intercept:拦截表 𝜄 : (𝑘 : 𝐾) → ℳ︀𝑘,为每个键赋予其元数据。

前两者组合成两层解析 𝑘 → 𝜌(𝑘) → 𝜎(𝜌(𝑘)):ctx.get(key)(算法2)先从 @@isolate 读出领域符号 𝜌(𝑘),再从 @@store 读出绑定值 𝜎(𝜌(𝑘))。𝜌 这层间接使得隔离(isolation)能把一个键重定向到独立绑定,而 @@intercept 仅在访问绑定时被咨询,调整的是"如何使用"而非"解析到什么"。我们分两部分实现这些操作:(1) 提供与通知(provision and notification),安装或撤回绑定并把变更传播给依赖者;(2) 隔离与拦截(isolation and interception),重塑一个键如何解析。

提供与通知。由于 set(𝑘,𝑣) 的类型是 𝔈Σ(3.1节),协效应提供就是一次 ctx.effect 调用,继承其自动跟踪与恢复。算法2实现 ctx.set(key, value),即具体的 set(𝑘,𝑣):回调把一个值绑定到领域符号 𝜌(𝑘) 下的存储里,返回的 dispose 函数移除它。安装与移除都调用 notify 把变更传播给依赖组件。

算法2 协效应操作------见原文伪代码

算法3把每次绑定变更传播给依赖者:对每个活跃 fiber,测试变更键是否出现在其 fiber.inject 中且解析到同一领域;若是,则调用 refresh(5.1.3节)针对新状态重新求值该 fiber,并返回被重新求值的 fiber 集合,以便调用方可以等待它们。这就是定义26的响应式分类(reactive classification):翻转满足性的变更会激活或停用 fiber,而 refresh 的幂等性使中性变更无害。这种重新求值与多样控制流的交互在5.1.3节展开。

算法3 响应式通知------见原文伪代码

一个绑定仅当安装它的 fiber 处于 ACTIVE 时才对依赖者算作可用,因此 refresh 把每个声明键解析为活跃提供者,而非仅解析存储。这就是定义46的 provided by 关系,它使得一次撤回在发生前一步就对依赖者可见:已进入 UNLOADING 的提供者已停止提供,于是其依赖者重新算出一个未满足的 target 视图,并趁其绑定都还在时开始自己的拆卸。

隔离与拦截。两个操作在结构上做同一件事:各自派生一个子上下文,为 key 调整一张继承来的表,而保持父上下文不动,所以恢复是隐式的------丢弃子上下文即可,无需运行显式逆。ctx.isolate(key, realm) 用 realm 覆盖领域映射 𝜌,缺省时用新生成的符号(实现 isolate,定义29),于是两个对同一键赋予不同符号的上下文会解析到独立绑定。ctx.intercept(key, metadata) 把元数据合并进拦截表 𝜄(实现 intercept,定义31):按该定义,新元数据与上下文已为 key 携带的合并,并优先于后者。

详细解释

这一节把第3.2节的协效应系统落地为 ctx 上三个 Symbol 槽:@@store(实际存值)、@@isolate(键→领域符号的映射表)、@@intercept(键→元数据的拦截表)。两层解析 𝑘→𝜌(𝑘)→𝜎(𝜌(𝑘)) 是关键巧思:把"键"和"实际存储槽"解耦,隔离就是改 𝜌 这层映射,让同一键名在不同子树里指向不同领域符号,从而互不干扰。映射到 DSH/Cordis 实际 API:ctx.set('database', myDb) 提供一个服务,ctx.inject(['database'])ctx.inject({ database: ['required'] }) 声明依赖;Koishi 里 ctx.set('database', new DatabaseService(ctx)) 注册数据库服务,其他插件通过 ctx.get('database') 或直接 ctx.database 取用。算法2最深的点是:set 本身是一次 ctx.effect,所以它"自动被跟踪、自动可恢复"------服务下线时框架自动 delete 掉 @@store 里那条并 notify 依赖者,作者不用写 teardown。算法3的 notify 对每个 fiber 测"变更键 ∈ 它的 inject 且解析到同一 realm",命中就 refresh------这就是"切换数据库后端时只有真依赖该后端的插件才被重新激活"的机制基础。一个精妙的时间点设计:提供者进入 UNLOADING 就立刻算"已不提供",依赖者据此提前一步开始自己拆卸,而此时旧绑定值还物理存在于 @@store 里------这就是定理63"拆卸期间依赖仍可读"得以成立的运行时依据。isolate/intercept 则对应 ctx.isolate('database')(让某子树拥有独立数据库实例)和 ctx.intercept('database', { ... })(给某子树的数据库访问加一层元数据,如权限拦截),二者都靠派生子上下文实现,恢复即"丢弃子 ctx",零显式逆。


5.1.3 Component Lifecycle / 组件生命周期

原文 (English)

A component is instantiated as a fiber by ctx.use. This section gives the fiber (introduced in Section 5.1) operational meaning as the inertial state machine of Section 4.3.3. Two fields drive the algorithm below: fiber.parent, the parent context of fiber.ctx that forms the component hierarchy (the recursive structure of Γ∞, Section 3.3.1), and fiber.inertia, a handle to the in-flight asynchronous transition (or null if idle).

Algorithm 4 shows component instantiation. A component pairs a coeffect specification component.inject (𝑑) with an effect function component.apply; instantiation binds the component's config into fiber.apply (Line 9), the config-applied effect function (𝑒) that the lifecycle then runs. The callback function (Line 2) is the effect tracked in the parent fiber: when executed, it initiates the child's lifecycle by calling refresh (Algorithm 5); when recovered, it forces the child's target to ⊥ and triggers unload. This is the registration primitive of Definition 47, with callback as its O-Insert and the closure callback returns as its O-Retire: an instantiation is an ordinary tracked effect of the parent, so unloading a parent cascades to its children.

Algorithm 4 Component instantiation

复制代码
1  function use(ctx, component, config)
2    function callback()
3      refresh(fiber)
4      return function()
5        fiber.target ← ⊥
6        unload(fiber)
7    fiber ← Fiber(parent: ctx, inject: component.inject)
8    fiber.ctx ← ctx[fiber ↦ fiber]
9    fiber.apply ← () ↦ component.apply(fiber.ctx, config)
10   ctx.effect(callback)
11   return fiber

Algorithm 5 realizes the inertial state machine of Section 4.3.3, in which reload and unload are inertial: once entered, a transition runs to completion before the system responds to a target-state change. It uses two auxiliary lookups over the coeffect store: resolve(inject) returns the bindings the declared keys currently resolve to, and provided(fiber) returns the keys whose binding this fiber installed. The refresh function recomputes fiber.target from the coeffect store and, if the fiber is not already in a transition, initiates either a reload or unload task². The reload function records the current target and executes the component's effect function apply. Upon completion, it checks whether the target still matches: if so, the fiber enters ACTIVE; if not (regardless of whether the new target is ⊥ or a different set of providers), it chains into unload. Symmetrically, unload recovers all tracked effects in LIFO order and then either enters INACTIVE or chains into reload. This mutual recursion implements the inertial property: once a transition begins, it completes before any new transition can start.

²create_task schedules an async function to run concurrently and returns a handle to it (stored in fiber.inertia). We write it explicitly for language independence: with eager scheduling (e.g., TypeScript promises), the call is implicit and the returned promise is the handle, whereas with lazy scheduling (e.g., Python coroutines, Rust futures) the host must spawn the task for it to progress.

Algorithm 5 Component lifecycle

复制代码
1  function refresh(fiber)
2    target ← target(𝛾, 𝑛)
3    if target = fiber.target then return
4    fiber.target ← target
5    if fiber.inertia then return
6    if target ≠ ⊥ then
7      fiber.state ← LOADING
8      fiber.inertia ← create_task(reload(fiber))
9    else
10     fiber.state ← UNLOADING   ▷ out of service before any inverse is scheduled
11     fiber.inertia ← create_task(unload(fiber))
12 async function reload(fiber)
13   target0 ← fiber.target
14   fiber.committed ← resolve(fiber.inject)   ▷ commit the view
15   recover ← await execute(fiber.apply, () ↦ fiber.target = target0)
16   fiber.dispose ← recover ∘ fiber.dispose
17   if fiber.target = target0 then
18     fiber.state ← ACTIVE
19     notify(fiber.ctx, provided(fiber))
20     fiber.inertia ← null
21   else
22     fiber.state ← UNLOADING
23     fiber.inertia ← create_task(unload(fiber))
24 async function unload(fiber)
25   await all(notify(fiber.ctx, provided(fiber)).map(f ↦ f.await()))  ▷ drain dependents
26   await fiber.dispose()
27   fiber.dispose ← id
28   fiber.committed ← ⊥
29   if fiber.target = ⊥ then
30     fiber.state ← INACTIVE
31     fiber.inertia ← null
32   else
33     fiber.state ← LOADING
34     fiber.inertia ← create_task(reload(fiber))

fiber.target is computed by resolving each declared key against the current coeffect store and tupling the uid of the fiber that provides it, so it is a digest of target(𝛾, 𝑛) (Definition 46). Identifying a binding by its provider rather than by its value is what makes a single comparison against the recorded target sufficient: a uid is drawn fresh and never reused, so a provider that is replaced cannot be mistaken for the one it replaced, even when the two provide equal values. Since notify (Section 5.1.2) recomputes the target on every coeffect change, a fiber reloads precisely when one of its declared keys comes to be provided by a different fiber. A provider that overwrites its own binding in place is therefore not observed; a component that wants its replacement to propagate withdraws the binding and installs it afresh.

The algorithm operates at two complementary levels. At the transition level, reload and unload check the target at completion, enabling inertial chaining across transitions. At the iteration level within each transition, the effect execution (Algorithm 1) checks the target at each iteration boundary, enabling partial rollback within a single transition. These two mechanisms correspond to the inter-transition chaining of Section 4.3.3 and the intra-transition staleness check that Theorem 64 rests on.

Three lines carry the coeffect ordering of Theorem 63, and where each of them sits is what makes the ordering hold. reload commits the resolved view at Line 14 and unload discards it only after every inverse has run, so a fiber reads the same bindings for as long as it is loaded, its own teardown included. refresh marks the fiber UNLOADING at Line 10 before the transition task is created, which is the L-Leave step: the fiber stops providing, and the dependents recompute against that before any of its inverses is scheduled. unload then waits at Line 25 for each notified dependent to reach INACTIVE, which is the guard on L-Unload; notify admits a dependent only when its declared key resolves to the same realm symbol as the provider's, which is the runtime form of the guard's demand that the dependent see the key from this fiber rather than merely declare it. The wait sits ahead of the whole recovery rather than inside one of the inverses being waited on, since fiber.dispose initiates a fiber's effects concurrently and a wait placed within one of them would leave the rest unordered. Termination follows Theorem 66: a fiber only ever waits on dependents that have already stopped being satisfiable, and a dependent that is itself a provider waits the same way for its own, so the provider graph is traversed on demand rather than analyzed in advance.

中文翻译

组件通过 ctx.use 被实例化为一个 fiber。本节赋予 fiber(5.1节引入)以4.3.3节惯性状态机(inertial state machine)的操作含义。两个字段驱动下面的算法:fiber.parent,即 fiber.ctx 的父上下文,构成组件层级(Γ∞ 的递归结构,3.3.1节);fiber.inertia,进行中异步迁移的句柄(空闲时为 null)。

算法4展示组件实例化。一个组件把协效应规格 component.inject(𝑑)与一个效应函数 component.apply 配对;实例化把组件的 config 绑进 fiber.apply(第9行),即生命周期随后运行的"应用过配置的效应函数"(𝑒)。回调函数(第2行)是父 fiber 中被跟踪的效应:执行时通过调用 refresh(算法5)发起子组件生命周期;恢复时把子组件的 target 强制为 ⊥ 并触发 unload。这就是定义47的注册原语,callback 是其 O-Insert,callback 返回的闭包是其 O-Retire:一次实例化是父上下文上的一次普通被跟踪效应,所以卸载父组件会级联到子组件。

算法4 组件实例化------见原文伪代码

算法5实现4.3.3节的惯性状态机,其中 reload 与 unload 是惯性的:一旦进入,一次迁移会运行到完成,系统才响应 target 状态变更。它用两个对协效应存储的辅助查询:resolve(inject) 返回声明键当前解析到的绑定,provided(fiber) 返回本 fiber 安装了绑定的键。refresh 函数从协效应存储重算 fiber.target,若 fiber 不在一次迁移中,则发起一个 reload 或 unload 任务²。reload 函数记录当前 target 并执行组件的效应函数 apply。完成后检查 target 是否仍匹配:若匹配,fiber 进入 ACTIVE;若不匹配(无论新 target 是 ⊥ 还是另一组提供者),则链入 unload。对称地,unload 以 LIFO 顺序恢复所有被跟踪效应,然后要么进入 INACTIVE,要么链入 reload。这种互递归实现了惯性性质:一旦迁移开始,它会先完成,任何新迁移才能开始。

²create_task 调度一个异步函数并发运行并返回其句柄(存于 fiber.inertia)。我们显式写出它以保持语言无关:在急切调度下(如 TypeScript promise)调用是隐式的、返回的 promise 即句柄;在惰性调度下(如 Python 协程、Rust future)宿主必须显式 spawn 任务才能推进。

算法5 组件生命周期------见原文伪代码

fiber.target 通过对每个声明键解析当前协效应存储、并把提供它的 fiber 的 uid 元组化得到,因此它是 target(𝛾,𝑛)(定义46)的一个摘要。以提供者而非值来标识绑定,使得与已记录 target 的单次比较就足够:uid 是新鲜抽取且永不复用的,所以被替换的提供者不会被误认为其替代者,即使两者提供相等的值。由于 notify(5.1.2节)在每次协效应变更时重算 target,一个 fiber 恰在其某个声明键改由不同 fiber 提供时重载。原地覆盖自身绑定的提供者因此不被观测;想让替换传播的组件需要先撤回绑定再重新安装。

算法在两个互补层面运作。在迁移层面,reload 与 unload 在完成时检查 target,实现跨迁移的惯性链。在每次迁移内部的迭代层面,效应执行(算法1)在每个迭代边界检查 target,实现单次迁移内的部分回滚。这两个机制分别对应4.3.3节的迁移间链与定理64所依赖的迁移内过期检查。

有三行承载定理63的协效应顺序,它们各自的位置正是顺序成立的原因。reload 在第14行提交已解析视图,unload 只在所有逆跑完后才丢弃它,所以一个 fiber 在它被加载期间(含自身拆卸)始终读取同一组绑定。refresh 在第10行、即迁移任务创建之前就把 fiber 标为 UNLOADING,这是 L-Leave 步:fiber 停止提供,依赖者据此在它的任何逆被调度之前重算。unload 随后第25行等待每个被通知的依赖者到达 INACTIVE,这是 L-Unload 上的 guard;notify 仅在依赖者声明键解析到与提供者相同的领域符号时才接纳它,这是 guard"要求依赖者从本 fiber 看到该键而非仅仅声明它"的运行时形式。这个等待放在整个恢复之前,而非放进某个正被等待的逆内部,因为 fiber.dispose 并发地发起一个 fiber 的各效应,把等待放进其中一个会让其余失序。终止性遵循定理66:一个 fiber 只等待已不再可满足的依赖者,而本身是提供者的依赖者以同样方式等待自己的依赖者,所以提供者图是按需遍历的,而非预先分析。

详细解释

这是全章最核心、也最精巧的算法,把第4章的惯性状态机翻译成 refresh/reload/unload 三函数互递归。映射到 DSH/Cordis 实际用法:ctx.plugin(MyComponent) / ctx.use(MyComponent, config) 就是算法4的 use------它把实例化本身做成父 ctx 上的一次 ctx.effect,所以"卸载父会级联卸载子"是免费的。Cordis 的 Service 类(如 Koishi 的 DatabaseService)通常在 start()ctx.effect(() => ...) 注册副作用、在 dispose() 里被框架自动调用清理,正是 reload 跑 apply、unload 跑 dispose 的对应。算法5的精髓是"惯性":reload/unload 一旦启动就跑完才响应新 target------这避免了"边加载边被要求卸载"导致的撕裂。两层过期检查对应两层时间语义:迁移间(reload/unload 完成时查 target)和迁移内(execute 每个迭代边界查 target),后者使长事务中途被卸载能干净部分回滚,正是定理64的基础。一个反直觉但关键的工程决策:fiber.target 用"提供者 uid 元组"做摘要而非用值------所以"用相等的值换一个提供者"会触发 reload,而"同一提供者原地改值"不触发;想传播就得先撤回再重装。定理63的协效应顺序靠三行代码的"位置"保证:第14行提交 committed view 使组件在其整个生命周期(含自己拆卸时)读同一组绑定;第10行提前标 UNLOADING 让依赖者早一步开始拆卸;第25行等待依赖者到 INACTIVE 后才真正回收。最实际的体验结论:组件作者完全不必关心"我的依赖走了我该怎么办"------框架自动检测、自动 reload/卸载、自动按依赖图顺序等待,这正是5.3强调的"无认知负担的时间可组合性"。


5.1.4 Context Access / 上下文访问

原文 (English)

The coeffect operations of Section 5.1.2 form a reflective API: a coeffect is written with ctx.set(key, value) and read with ctx.get(key), both keyed by name. Cordis layers a second, more native way to extend and consume the context on top of this reflective API: property access. A component can access a coeffect as the property ctxkey, as if it were native structure of the context, rather than through a method call. In TypeScript, Cordis realizes this with a Proxy whose get trap mediates every property access. Algorithm 6 shows how a context resolves such an access to a coeffect, atop the primitive get of Section 5.1.2.

Algorithm 6 Proxy-mediated context access

复制代码
1  function resolve(ctx, key)
2    fiber ← ctx.fiber
3    repeat
4      if key ∈ fiber.committed then return fiber.committed[key]
5      if key ∈ fiber.inject then throw INACTIVE_ACCESS
6      if fiber = root then throw UNDECLARED_ACCESS
7      fiber ← fiber.parent.fiber

Algorithm 6 walks the fiber chain upward from the accessing context: at the first fiber whose committed view binds key, the access is authorized and that binding is returned; if the walk reaches a fiber that declares key without having committed it, the fiber is not loaded and the access fails; and if it reaches the root without any declaration, the access is rejected as undeclared. This is where the proxy differs from the bare ctx.get: ctx.get(key) is a lookup against the store that returns the bound value or nothing and never fails, whereas the proxy resolves against the accessing fiber's own view and enforces the coeffect specification 𝑑 at the point of use. Reading the view rather than the store is also what Theorem 63 rests on, since it is what keeps a dependency readable to a component whose teardown was triggered by that dependency going away.

This rejection is a runtime check performed at the point of access. Because a component's coeffect specification 𝑑 is declared statically, the same violation is in principle detectable at compile time, by resolving each ctxkey against the declared 𝑑 before execution; Section 6.4 discusses how a host language's type-level dependency declarations and compile-time metaprogramming can carry out exactly this mediation.

中文翻译

5.1.2节的协效应操作构成一个反射式 API(reflective API):用 ctx.set(key, value) 写协效应,用 ctx.get(key) 读协效应,二者都按键名。Cordis 在这个反射式 API 之上叠加了第二种更原生的扩展与消费上下文的方式:属性访问(property access)。一个组件可以把协效应当作属性 ctxkey 访问,仿佛它是上下文的原生结构,而非通过方法调用。在 TypeScript 中,Cordis 用 Proxy 实现这一点,其 get trap 介导每一次属性访问。算法6展示上下文如何在5.1.2节原语 get 之上把这种访问解析为协效应。

算法6 代理介导的上下文访问------见原文伪代码

算法6从访问上下文沿 fiber 链向上走:在第一个其 committed 视图绑定了 key 的 fiber 处,访问被授权并返回该绑定;若走到一个声明了 key 但未提交它的 fiber,则该 fiber 未加载,访问失败;若走到根仍无任何声明,则访问作为未声明被拒绝。这就是代理与裸 ctx.get 的差别:ctx.get(key) 是对存储的查询,返回绑定值或空、永不失败;而代理针对访问 fiber 自己的视图解析,并在使用点强制协效应规格 𝑑。读视图而非读存储也是定理63所依赖的,因为正是它使得一个依赖对其拆卸由该依赖消失所触发的组件仍保持可读。

这种拒绝是在访问点执行的运行时检查。由于组件的协效应规格 𝑑 是静态声明的,同一违规原则上可在编译期检测:在执行前把每个 ctxkey 对已声明的 𝑑 解析;6.4节讨论宿主语言的类型级依赖声明与编译期元编程如何精确地执行这种介导。

详细解释

这一节给出协效应的"双 API"设计:底层反射式(ctx.get/ctx.set 按名存取、永不抛错),上层原生式(ctxkey 属性访问、会抛错)。在 DSH/Cordis 实际代码里,ctx.databasectx.httpctx.config 这种"像原生属性一样用服务"的写法就是算法6的 Proxy get trap 在工作------它沿 fiber 链向上找第一个 committed 了该 key 的 fiber,找到就返回绑定值。三种失败语义对应三种 bug:INACTIVE_ACCESS(声明了依赖但提供者还没起来,相当于"用了 inject 但服务未就绪")、UNDECLARED_ACCESS(根本没声明就访问,相当于"用了没 inject 的服务")。这些在运行时是抛错,但因为 inject 是静态声明的,论文指出原则上可前移到编译期------6.4节会用宿主语言类型系统(如 TypeScript 的 declare module / 类型级 inject 声明)做这件事,这正是 Cordis 类型化 ctx 的理论基础。一个与5.1.3紧密相连的点:代理读的是 fiber.committed(视图)而非 @@store(存储)------这保证了"组件自己被触发拆卸时,它的依赖在 teardown 代码里仍可读",因为视图在 unload 所有逆跑完前都不丢弃,这是定理63在访问层的最后一道保险。工程上这意味着:在 Service 的 dispose 回调里访问 ctx.database 仍是安全的,框架保证此时旧视图还在,不会读到一个已半回收的存储槽。


5.2 Component Loader / 组件加载器

原文 (English)

The core library equips component developers with imperative primitives for dynamic composition, such as ctx.effect, ctx.use, and ctx.set. A separate concern arises for application orchestrators, who assemble pre-existing components into a running system and adjust the composition over its lifetime. The component loader addresses this concern by introducing a declarative configuration layer: the orchestrator specifies the desired composition as a persistent data structure, and the loader translates changes to this specification into the corresponding imperative fiber operations.

中文翻译

核心库为组件开发者提供命令式动态组合原语,如 ctx.effect、ctx.use、ctx.set。对应用编排者(application orchestrators)则产生另一个关切:他们把既有组件组装进一个运行中的系统,并在其生命周期内调整这个组合。组件加载器处理这个关切,方法是引入一个声明式配置层(declarative configuration layer):编排者把期望的组合指定为一个持久数据结构,加载器把对该规格的变更翻译成对应的命令式 fiber 操作。

详细解释

这一节是"视角切换":5.1 服务于写组件的开发者(命令式原语),5.2 服务于组装系统的编排者(声明式配置)。核心思想是"配置即持久数据结构,加载器负责把它 diff 成 fiber 操作"------这与 DSH 的配置分层思想完全一致。DSH 体系里,cordis.patch.yml(包级补丁配置)+ profile(profile 级配置)+ bundle(bundle 级配置)+ 各组件 package.yml 的 config 字段,构成一棵配置树;加载器把这棵树协调成实际运行的 fiber 集合,并负责把"配置变更"翻译成"最小破坏性的 fiber 操作"(只 reload 真正变了的条目,而非整体重启)。这种"声明式配置 + 增量协调"是把5.1的命令式原语包装成编排友好的高层 API,让运维者改 YAML 就能动态加减组件,而不必手写 ctx.use 调用序列。后两节分别讲配置协调(5.2.1)与热模块替换(5.2.2),前者解决"改配置如何不重启",后者解决"改源码如何不重启"。


5.2.1 Declarative Configuration / 声明式配置

原文 (English)

Section 4 decomposes a running system into fibers, each an instantiation of one component. Everything an instantiation needs can be declared, so an orchestrator can describe a whole system as a declarative configuration: a persistent record that the loader realizes as fibers and keeps in step with them.

Entries. A configuration consists of entries. Each entry specifies a fiber and manages it, and the binding runs in both directions: the loader responds to a change in an entry's fields by adjusting the fiber, and a component that revises its own configuration or disables itself has the change written back to its entry.

Definition 74. An entry declares a single fiber, recording:

• id --- a stable identifier, used as the reconciliation key when its group's child list changes;

• url --- the URL of the component module to instantiate;

• isolate --- an isolation annotation applied to the entry's context;

• intercept --- an interception annotation applied to the entry's context;

• config --- the configuration bound into the component to form its effect function apply;

• disabled --- whether the entry is administratively turned off.

An entry can serve as a faithful specification because what supports a fiber is exactly what an entry records. The support set of Definition 67 reads 𝜏, 𝜋, 𝑑, and 𝑝 and nothing else, and an entry gives all four: disabled gives 𝜏, the entry's parent in the tree gives 𝜋, and url selects the component which declares 𝑑 and 𝑝. The fields the support set leaves unread are the fiber's runtime state, which an instantiation does not need either, and Lemma 70 identifies the support set with the 𝖠𝖼𝗍𝗂𝗏𝖾 fibers of a quiescent state (Definition 49) as far as each component installs every key it declares (Definition 69).

These entries form a configuration tree that is the authoritative record of what the system loads. An entry may be a leaf mapping to a single fiber, or its component may in turn load further components, making the entry a branch node. Cordis provides components for such grouped and nested loading: @cordisjs/group takes a list of child entries as its configuration and loads them as a subgroup, and @cordisjs/include loads an external configuration file (YAML or JSON) and grafts its entries in as a nested subtree. Both are ordinary components resting on the registration primitive of Definition 47 (Algorithm 4), so a nested tree stays within the calculus and the results below hold of it.

Reconciliation. When an entry's record changes, the loader reconciles incrementally rather than tearing the fiber down and rebuilding it wholesale. Reconciling this way is sound for reasons the metatheory supplies.

• Theorem 73 makes the quiescent state a function of the final configuration alone: whatever instantiations and retirements the loader performs on the way, and in whatever order, the system quiesces where a load of the final configuration from scratch would have left it. Which components end up loaded is read off the declarations only as far as each of them installs every key it declares (Definition 69); a component that declares a key and installs it under some configurations alone is one the loader can still reconcile, but the set of loaded components then answers to those configurations as well.

• Theorem 66 proves that the system does quiesce, so a reconciliation is complete once its instantiations and retirements have been issued.

• Corollary 62 puts a departing fiber's contribution to the state at nothing, so rebuilding one entry withdraws what its fiber installed and leaves the fibers around it as they were.

• Theorem 63 lets the entries be instantiated together, with no load order for the orchestrator to arrange: a fiber whose declared keys are not yet provided waits at its L-Begin, and one whose provider leaves is deactivated ahead of it. A dependency therefore constrains when a fiber activates rather than when its module is fetched and evaluated, so the loader loads modules concurrently, where bringing up a large configuration spends its time.

On top of the fiber that an entry declares, the loader dispatches on which of the entry's fields changed and applies the least disruptive operation for each.

• id, url --- rebuilds the entry, since its identity or its component has changed;

• isolate --- reassigns the entry's realms (Algorithm 7);

• intercept --- updated in place, as interception metadata is consulted at read time and needs no reload;

• config --- handed to the component, which decides how to apply the new payload, typically by diffing it against the previous one and reloading only on a material change. In particular, an @cordisjs/group entry's config is its list of child entries, so it applies the update as a keyed diff over child ids, creating, removing, or updating each child; since updating a surviving child re-enters this same per-field dispatch, group reconciliation and entry update recurse together down the tree;

• disabled --- unloads the fiber when set and reloads it when cleared.

Managed realms. Isolation in the core derives a child context overriding the realm table 𝜌 at one key (Section 5.1.2), which suffices while the context tree stands still. An entry may be moved between groups at runtime, so the loader manages realms of its own, and the isolate field selects between two scoping rules per key. A value of true asks for a local realm, private to the entry and tagged by its id, which the entry carries with it wherever it moves; a string asks for a global realm shared by every entry naming that string, so moving such an entry changes which entries it shares a binding with rather than which realm it belongs to. A realm is discarded once no entry names it.

Reassigning an entry's realms turns on which keys changed realm, whether the entry is itself the provider at a changed key, and which dependents to notify. The middle question is the hard one, since a realm symbol may be shared by several fibers of which only one is the provider. The loader answers it with delimiters: one symbol 𝛿𝑘 per key, under which each context stores a tag of its own. A delimiter is written on a context and inherited by its descendants, so the entry's tag and the provider's agree exactly when the two were derived within one isolate scope for 𝑘, which is the case in which the binding at 𝑘 is the entry's own and has to move with it.

Algorithm 7 Isolation realm reassignment

复制代码
1  function patch_isolation(entry, 𝜌′)
2    𝜌 ← entry.ctx[@@isolate]
3    store ← entry.ctx[@@store]
4    Δ ← {𝑘 | 𝜌(𝑘) ≠ 𝜌′(𝑘)}    ▷ keys whose realm changes
5    for 𝑘 in Δ do
6      entry.ctx[𝛿𝑘] ← fresh tag
7      diff[𝑘] ← (𝜌(𝑘), 𝜌′(𝑘), entry.ctx[𝛿𝑘], store[𝜌(𝑘)].fiber.ctx[𝛿𝑘])
8    entry.ctx[@@isolate] ← 𝜌′
9    reload(entry.fiber)
10   for 𝑘 in Δ do
11     (𝑠1, 𝑠2, 𝑑1, 𝑑2) ← diff[𝑘]
12     if 𝑑1 = 𝑑2 and store[𝑠1] and not store[𝑠2] then  ▷ the binding is the entry's own
13       store[𝑠2] ← store[𝑠1]
14       delete store[𝑠1]
15   function affected(fiber, 𝑘)
16     (𝑠1, 𝑠2, 𝑑1, 𝑑2) ← diff[𝑘]
17     return fiber.ctx[@@isolate][𝑘] ∈ {𝑠1, 𝑠2} and (fiber.ctx[𝛿𝑘] = 𝑑1) ≠ (𝑑2 = 𝑑1)
18   notify(entry.ctx, Δ, affected)  ▷ in place of the realm test of Algorithm 3

The test turns on one property of delimiters. The tag under 𝛿𝑘 is written on the entry's context and inherited by every context derived from it, and it is drawn afresh at each reassignment, so for a context 𝛾′

𝛾′𝛿𝑘 = 𝑑1 ⟺ 𝛾′ is derived from the entry's context (65)

Write own(𝛾′) for that condition, of which 𝑑2 = 𝑑1 is the instance at the provider. The reassignment moves the contexts satisfying own from 𝑠1 to 𝑠2 and leaves the others where they are, and by the loop above it moves the binding to 𝑠2 exactly when the provider satisfies own. A dependent sees the binding while its own realm at 𝑘 is the realm the binding sits in. Where own agrees on the dependent and the provider, both move or neither does, so the dependent sees the binding afterwards exactly when it saw it before. Where own separates them, one side moves and the other stays, so the dependent gains or loses the binding. The inequality is that separation, and the membership test drops the dependents resolving 𝑘 in neither realm, which no part of the move reaches.

中文翻译

第4章把一个运行中的系统分解为 fiber,每个是一次组件实例化。一次实例化所需的一切都可声明,于是编排者可把整个系统描述为一个声明式配置:一个持久记录,加载器把它实现为 fiber 并与之保持同步。

条目(Entries)。一个配置由若干条目组成。每个条目指定一个 fiber 并管理它,绑定是双向的:加载器响应条目字段变更而调整 fiber;一个修订自身配置或禁用自身的组件,其变更被写回它的条目。

定义74。一个条目声明单个 fiber,记录:

• id --- 稳定标识符,当其组的子列表变更时用作协调键;

• url --- 要实例化的组件模块的 URL;

• isolate --- 应用于条目上下文的隔离注解;

• intercept --- 应用于条目上下文的拦截注解;

• config --- 绑进组件以构成其效应函数 apply 的配置;

• disabled --- 该条目是否被行政性关闭。

一个条目能充当忠实规格,是因为支撑一个 fiber 的恰好就是条目记录的内容。定义67的支撑集(support set)只读 𝜏、𝜋、𝑑、𝑝 而无其他,条目给出全部四者:disabled 给 𝜏,树中条目的父节点给 𝜋,url 选定声明 𝑑 与 𝑝 的组件。支撑集不读的字段是 fiber 的运行时状态,实例化也用不到,而引理70把支撑集等同于静默状态(定义49)的 𝖠𝖼𝗍𝗂𝗏𝖾 fiber(在每组件安装其声明的每个键的意义下,定义69)。

这些条目构成一棵配置树,是系统加载内容的权威记录。一个条目可以是映射到单个 fiber 的叶节点,其组件也可再加载更多组件,使该条目成为分支节点。Cordis 为这种分组与嵌套加载提供组件:@cordisjs/group 取子条目列表为配置并把它们加载为一个子组,@cordisjs/include 加载一个外部配置文件(YAML 或 JSON)并将其条目嫁接为嵌套子树。二者都是基于定义47注册原语(算法4)的普通组件,所以嵌套树仍在演算之内,下面的结论对它成立。

协调(Reconciliation)。当一个条目的记录变更时,加载器增量协调,而非把 fiber 整体拆掉重建。这样做是可靠的,理由由元理论提供:

• 定理73使静默状态仅是最终配置的函数:无论加载器途中做哪些实例化与退役、以何种顺序,系统都静默到"从零加载最终配置"会到达之处。哪些组件最终被加载只从声明读出(在每组件安装其声明的每个键的意义下,定义69);声明某键却只在某些配置下安装它的组件仍可被协调,但被加载组件集合也便受那些配置影响。

• 定理66证明系统确实会静默,所以一旦协调的实例化与退役发出,协调就完成。

• 推论62把一个离去 fiber 对状态的贡献置为零,所以重建一个条目会撤回其 fiber 安装的内容,而让其周围 fiber 维持原样。

• 定理63允许条目一起实例化,编排者无需安排加载顺序:声明键尚未被提供的 fiber 在其 L-Begin 等待,提供者离开的则提前被停用。因此依赖约束的是 fiber 何时激活,而非其模块何时被拉取与求值,于是加载器并发加载模块------而那正是启动大配置花时间之处。

在条目声明的 fiber 之上,加载器按条目哪个字段变了分派,并对每个字段应用破坏性最小的操作:

• id、url --- 重建条目,因其身份或其组件变了;

• isolate --- 重新分配条目的领域(算法7);

• intercept --- 原地更新,因拦截元数据在读取时被咨询,无需重载;

• config --- 交给组件,由组件决定如何应用新载荷,通常对前值做 diff、仅在有实质变更时重载。特别地,@cordisjs/group 条目的 config 是其子条目列表,所以它把更新作为对子 id 的 keyed diff 应用,创建、删除或更新每个子项;由于更新存活子项会重新进入同一个逐字段分派,组协调与条目更新沿树一起递归;

• disabled --- 置位时卸载 fiber,清除时重载。

受管领域(Managed realms)。核心中的隔离派生一个对键 𝑘 覆盖领域表 𝜌 的子上下文(5.1.2节),这在上下文树静止时足够。一个条目可在运行时在组间移动,所以加载器自管领域,isolate 字段在每个键上在两种作用域规则间选择。值 true 请求一个局部领域,为条目私有并以 id 标记,随条目移动;一个字符串请求一个全局领域,被每个命名该字符串的条目共享,所以移动这样一个条目改变的是它共享绑定的条目集,而非它属于哪个领域。一旦无条目命名某领域,该领域被丢弃。

重新分配条目领域取决于哪些键换了领域、条目自身是否是变更键的提供者、以及要通知哪些依赖者。中间的问题最难,因为一个领域符号可能被多个 fiber 共享,其中只有一个是提供者。加载器用定界符(delimiters)回答它:每个键一个符号 𝛿𝑘,每个上下文在其下存自己的标签。定界符写在某上下文上并被其后代继承,所以条目的标签与提供者的标签恰在二者在同一 𝑘 隔离作用域内派生时一致------即 𝑘 处的绑定是条目自己的、必须随它移动的情形。

算法7 隔离领域重新分配------见原文伪代码

该测试取决于定界符的一条性质。𝛿𝑘 下的标签写在条目上下文上并被每个派生自它的上下文继承,且每次重新分配时新鲜抽取,所以对上下文 𝛾′

𝛾′𝛿𝑘 = 𝑑1 ⟺ 𝛾′ 派生自条目的上下文 (65)

记该条件为 own(𝛾′),其中 𝑑2 = 𝑑1 是其在提供者处的实例。重新分配把满足 own 的上下文从 𝑠1 移到 𝑠2,其余留在原处;按上面循环,恰在提供者满足 own 时把绑定移到 𝑠2。一个依赖者当其 𝑘 处的自身领域是绑定所在领域时看到该绑定。在 own 对依赖者与提供者一致之处,要么都移要么都不移,所以依赖者之后看到绑定当且仅当之前看到。在 own 分离二者之处,一侧移一侧留,所以依赖者获得或失去绑定。该不等式就是那种分离,而成员测试丢弃在两个领域都不解析 𝑘 的依赖者------移动哪部分都到不了它们。

详细解释

这一节是把5.1的命令式原语包装成"运维友好"的声明式配置层。条目的六个字段(id/url/isolate/intercept/config/disabled)正好覆盖定义67支撑集所需的 𝜏/𝜋/𝑑/𝑝 四要素------这是"配置能忠实描述系统"的形式化保证。映射到 DSH 配置分层:bundle/profile/cordis.patch.yml/package.yml 共同构成一棵配置树,每个条目对应一个要实例化的 fiber;cordis.patch.yml 做包级配置补丁,profile 做环境/场景级覆盖,bundle 做整包集合,加载器按优先级合并后协调成 fiber 集合。@cordisjs/include 直接对应"配置文件嵌套引用"(一个 YAML include 另一个 YAML),@cordisjs/group 对应"分组条目"------二者都是普通组件,所以嵌套配置树仍在演算之内,定理照用。协调的可靠性由四条定理支撑,其中最有工程价值的是定理73(静默状态只取决于最终配置,与途中操作顺序无关)和定理63(无需编排加载顺序,依赖未就绪的 fiber 自动在 L-Begin 等待)------这让加载器可以并发拉取模块,启动大配置的瓶颈是网络/磁盘而非串行依赖。逐字段分派是"最小破坏"的体现:改 intercept 不重载(元数据读取时才咨询)、改 config 交给组件自己 diff(组件可只 reload 有实质变更的部分)、只有改 id/url 才整体重建。算法7的"受管领域"解决一个微妙问题:条目在组间移动时,它的领域符号该跟着走还是留?用 true(局部,随 id 走)或字符串(全局,按名共享)二选一;用定界符 𝛿𝑘 判定"某绑定是不是本条目自己提供的"(标签一致即派生自同一隔离作用域),从而精确决定绑定该不该跟着条目移动、哪些依赖者该被通知。这套机制让"运行时把一个插件从一个分组拖到另一个分组"在 Cordis/DSH 里是可靠操作,而非会撕裂状态的危险动作。


5.2.2 Hot Module Replacement / 热模块替换

原文 (English)

Hot module replacement (HMR) applies the revertible-effect pattern at the module level: when source files change, typically during development, the system replaces the affected modules in-place without restarting the process. Because a fiber already bounds all of its component's effects and coeffects, a module that is itself a component can be replaced through fiber operations alone: disposing the old fiber recovers everything the component installed, and a new fiber instantiated from the reloaded module reinstalls it. HMR therefore needs no developer-annotated acceptance boundaries, as opposed to Webpack 46 or Vite 47 HMR.

The @cordisjs/hmr component provides the HMR engine, which operates in three phases.

Phase 1: Module classification. The engine takes two inputs: the stashed set (file URLs whose contents have changed since the last reload) and the externals set (modules that cannot be hot-replaced and instead trigger a full restart). Writing get_imports(url) for the modules that url directly imports, it classifies the changes' dependency subgraph, marking each module accepted or declined:

Algorithm 8 Module classification

复制代码
1  function classify(stashed, externals)
2    accepted ← stashed
3    declined ← externals
4    pending ← ⌀
5    for url in stashed do
6      pending ← pending ∪ (get_imports(url) ∖ (accepted ∪ declined))
7    repeat
8      progress ← false
9      for url in pending do
10       if get_imports(url) ∩ accepted ≠ ⌀ then
11         accepted ← accepted ∪ {url}
12         pending ← pending ∖ {url}
13         progress ← true
14       else if get_imports(url) ⊆ declined then
15         declined ← declined ∪ {url}
15       declined ← declined ∪ {url}
16         pending ← pending ∖ {url}
17         progress ← true
18       else
19         pending ← pending ∪ (get_imports(url) ∖ (accepted ∪ declined))
20     until not progress
21   declined ← declined ∪ pending
22   return (accepted, declined)

Seeded with the imports of the stashed files, the fixed point accepts a module once one of its imports is accepted and declines one once all of its imports are declined; any module left undecided, caught in an import cycle, defaults to declined.

Phase 2: Stale-entry detection. Using accepted and declined, the engine then filters the component entries down to the stale ones, whose dependency tree reaches a changed module. It walks each entry's tree with get_dependencies, which collects the transitive imports of a module while respecting declined as a boundary:

Algorithm 9 Stale-entry detection

复制代码
1  function get_dependencies(root, declined)
2    deps ← ⌀
3    function traverse(url)
4      if url ∈ deps or url ∈ declined then return
5      deps ← deps ∪ {url}
6      for child in get_imports(url) do traverse(child)
7    traverse(root)
8    return deps
9  function detect(entries, accepted, declined)
10   stale_entries ← ⌀
11   for entry in entries do
12     tree ← get_dependencies(entry.url, declined)
13     if tree ∩ accepted ≠ ⌀ then
14       accepted ← accepted ∆ tree
15       stale_entries ← stale_entries ∪ {entry}
16   return stale_entries

An entry is stale exactly when its tree intersects accepted; that tree is then folded into accepted, so every stale module along it is invalidated in the next phase.

Phase 3: Transactional reload. Finally, the engine reloads the stale entries. It invalidates the accepted modules' caches³, backing up each removed module to enable rollback, then re-imports each stale entry's component module by its url and swaps in a fresh fiber:

Algorithm 10 Transactional module reload

复制代码
1  function reload(ctx, accepted, stale_entries)
2    backup ← invalidate_caches(accepted)
3    try
4      for entry in stale_entries do
5        entry.fiber.dispose()
6        entry.fiber ← ctx.use(import(entry.url), entry.config)
7    catch error
8      restore_caches(backup)
9      for entry in stale_entries do
10       entry.fiber.dispose()
11       entry.fiber ← ctx.use(backup[entry.url], entry.config)
12     throw error

The transactional guarantee ensures that the system never enters a half-reloaded state: if any module fails to import (e.g., due to a syntax error), the caches are restored and every stale entry is rebuilt from backupentry.url, the previous component whose cache was just restored, undoing the swaps already made.

³On Node.js, this means clearing the caches of both the ES module and CommonJS module systems, since a module imported through the ES loader can appear in both.

中文翻译

热模块替换(Hot Module Replacement,HMR)把可逆效应模式应用到模块层:当源文件变更时(通常在开发期间),系统原地替换受影响模块而不重启进程。因为 fiber 已经界定其组件的全部效应与协效应,一个本身是组件的模块可仅通过 fiber 操作被替换:dispose 旧 fiber 恢复该组件安装的一切,而从重载模块实例化的新 fiber 重新安装它。因此 HMR 不需要开发者标注的接受边界(acceptance boundaries),这与 Webpack46 或 Vite47 的 HMR 不同。

@cordisjs/hmr 组件提供 HMR 引擎,分三阶段运作。

阶段1:模块分类(Module classification)。引擎取两个输入:暂存集(stashed set,自上次重载以来内容变更的文件 URL)与外部集(externals set,无法热替换、转而触发完整重启的模块)。记 get_imports(url) 为 url 直接导入的模块,它对变更的依赖子图分类,把每个模块标记为 accepted 或 declined:

算法8 模块分类------见原文伪代码

以暂存文件的导入为种子,不动点在一个模块的某个导入被 accepted 时接受它、在其全部导入被 declined 时拒绝它;任何仍悬而决、困在导入环里的模块默认 declined。

阶段2:过期条目检测(Stale-entry detection)。用 accepted 与 declined,引擎把组件条目过滤到过期者------其依赖树触及变更模块者。它用 get_dependencies 走每个条目的树,后者在尊重 declined 为边界的同时收集一个模块的传递导入:

算法9 过期条目检测------见原文伪代码

一个条目恰在其树与 accepted 相交时过期;该树随后被折叠进 accepted,所以下一阶段沿它的每个过期模块都被失效。

阶段3:事务性重载(Transactional reload)。最后,引擎重载过期条目。它失效 accepted 模块的缓存³,备份每个被移除的模块以支持回滚,然后按 url 重新导入每个过期条目的组件模块并换入新 fiber:

算法10 事务性模块重载------见原文伪代码

事务性保证确保系统永不进入半重载状态:若任何模块导入失败(如语法错误),缓存被恢复,每个过期条目从 backupentry.url------刚恢复缓存的先前组件------重建,撤销已做的交换。

³在 Node.js 上,这意味着同时清除 ES 模块与 CommonJS 模块系统的缓存,因为通过 ES loader 导入的模块可能出现在两者中。

详细解释

HMR 是把5.1.1的可逆效应模式"放大到模块层":因为每个组件的全部效应/协效应都被 fiber 界定,dispose 旧 fiber = 自动回收它安装的一切,new fiber 从重载模块实例化 = 重新安装------所以无需像 Webpack/Vite 那样要开发者手写 if (import.meta.hot) { ... } 接受边界。这与 DSH 开发体验直接相关:开发 dsh-liangshen / dsh-ssh 等插件时,改源码保存即可热重载,连中的会话/连接/缓存状态在非热替换部分保持不变(本机说明里"client-plugin HMR receiver 激活"正是这一机制的客户端侧体现)。三阶段设计很清晰:阶段8分类用不动点把"变更文件 + 其导入子图"分成可热替换(accepted)与不可(declined,含导入环里悬而未决者,保守判 declined);阶段9用 declined 作边界遍历每个条目的依赖树,找出树触及 accepted 的过期条目;阶段10做事务性重载------先 invalidate 缓存并备份,逐个 dispose 旧 fiber + 重新 import + ctx.use 新 fiber,任一导入失败就 restore_caches 并用 backup 重建,保证"永不全重载"。工程细节里那条脚注③值得注意:Node.js 上要同时清 ESM 与 CJS 两套缓存,因为同一模块可能经 ES loader 出现在两者中------这是 Cordis 在 Node 混合模块现实下的具体适配。整体上 HMR 复用了5.1全部机制(fiber.dispose 自动回收、ctx.use 自动重装),没有引入新原语,是"可逆效应"在开发时序维度的自然外推------也正因如此它能在不要求作者额外标注的前提下保证正确性,这正是5.3要强调的"时间可组合性无认知负担"的典型例证。


5.3 Case Study: Koishi / 案例研究:Koishi

原文 (English)

Koishi is an open-source chatbot application framework built on Cordis 4. Over four years of development, it has accumulated over 4000 community-contributed plugins⁵, ranging from instant-messaging (IM) adapters and database drivers to administrative consoles and end-user features. Its scale and diversity make it a representative validation of Cordis's dynamic composability in a production setting.

Expressiveness and generality of the meta-framework. Koishi runs as a server-side bot whose every feature is realized as a plugin over the context primitives of Section 5.1; Koishi itself contributes only the chatbot-domain vocabulary. The same model reappears in a wholly different runtime: Koishi's web console is a second, independent Cordis application whose plugins compose the primitives of the browser and its user interface rather than those of the server. The disparate settings above establish two properties of the model of Section 3. (1) It is expressive: its primitives suffice to carry a complete production system, the host framework supplying only domain vocabulary. (2) It is general: it fixes how effects and coeffects compose while leaving their meaning to each application, and so presupposes neither a particular domain nor a particular runtime.

Temporal composability without cognitive overhead. The plugin systems surveyed in Section 1.2.1 cannot unload an individual extension's effects without restarting the extension host. Koishi routinely performs this operation: an orchestrator disables a plugin from the console and its effects are withdrawn in place; during development, the HMR engine re-applies edited plugins on save while preserving cache state and live connections elsewhere in the system. Cordis makes such removal not merely possible but effortless for the plugin author. Because effects performed through the context are tracked and their inverses composed automatically (Section 3.1), even an inexperienced author obtains ordered cleanup for a plugin's context-mediated effects without writing an uninstall path. This achieves the locality of concern whose absence Section 1.2.1 identifies: correctness that would otherwise rest on each author's diligence is instead discharged once, by the abstraction.

Spatial composability across an open ecosystem. In contrast to the plugin systems of Section 1.2.1, where inter-plugin dependencies are largely absent, Koishi's ecosystem exhibits a genuine dependency topology: IM adapters provide access to each messaging platform, database drivers provide persistent storage, and functional plugins declare these as coeffects and access them. Reconfiguring a provider at runtime, such as switching the storage backend or reconnecting an adapter, reactivates only the dependents whose resolved dependency changed (Section 3.2); a plugin whose dependency is unavailable stays inactive until it appears, without erroring. What the case study substantiates is that this composition holds across independently authored code: a plugin and its dependencies are typically written by different authors who coordinate on nothing beyond the coeffect that connects them, so reactive coeffects keep the assembly consistent across an open ecosystem of independent contributors.

Threats to validity. The evidence here is drawn from a single ecosystem in a single host language, so it cannot separate the merits of the paradigm from those of its TypeScript realization or of Koishi's particular domain, and it is observational rather than a controlled comparison against an alternative architecture. What the case study establishes is thus an existence-and-adoption result rather than a quantitative one; measuring the abstraction's overhead and its ...

⁴Koishi currently uses Cordis v3. This paper presents Cordis v4, which refines the effect and coeffect semantics and redesigns the loader; the core compositional model is shared across both versions.

⁵Koishi uses the term plugin for the concept this paper formalizes as component.

中文翻译

Koishi 是一个建立在 Cordis 4 上的开源聊天机器人应用框架。四年开发中,它积累了超过 4000 个社区贡献插件⁵,从即时通讯(IM)适配器、数据库驱动到管理控制台与终端用户功能。其规模与多样性使其成为 Cordis 动态可组合性在生产环境中的代表性验证。

元框架的表达力与通用性(Expressiveness and generality)。Koishi 作为服务端机器人运行,每个功能都实现为基于5.1节上下文原语的插件;Koishi 自身只贡献聊天机器人领域的词汇。同一模型在完全不同的运行时重现:Koishi 的 web 控制台是第二个独立的 Cordis 应用,其插件组合的是浏览器及其 UI 的原语,而非服务器的原语。上述迥异场景确立了3章模型的两个性质。(1) 它有表达力:其原语足以承载一个完整的生产系统,宿主框架只供应领域词汇。(2) 它通用:它固定效应与协效应如何组合,而把其含义留给各应用,因此既不预设特定领域,也不预设特定运行时。

无认知负担的时间可组合性(Temporal composability without cognitive overhead)。1.2.1节调研的插件系统无法在不重启扩展宿主的情况下卸载单个扩展的效应。Koishi 常规执行此操作:编排者从控制台禁用一个插件,其效应就地撤回;开发期间,HMR 引擎在保存时重新应用编辑过的插件,同时保留系统中别处的缓存状态与活跃连接。Cordis 使这种移除不仅可能,而且对插件作者毫不费力。因为通过上下文执行的效应被跟踪、其逆被自动组合(3.1节),即便缺乏经验的作者也为插件经上下文介导的效应获得有序清理,而无需写卸载路径。这实现了1.2.1节指出所缺失的关注点局部性(locality of concern):原本依赖每位作者勤勉的正确性,现在由抽象一次性履行。

跨开放生态系统的空间可组合性(Spatial composability across an open ecosystem)。与1.2.1节插件系统中插件间依赖基本缺失不同,Koishi 生态呈现出真实的依赖拓扑:IM 适配器提供对各消息平台的访问,数据库驱动提供持久存储,功能插件把这些声明为协效应并访问它们。运行时重新配置提供者(如切换存储后端、重连适配器)只重新激活其已解析依赖发生变化的依赖者(3.2节);依赖不可用的插件保持非激活直到它出现,而不报错。本案例研究所证实的是,这种组合在独立编写的代码间成立:一个插件及其依赖通常由不同作者编写,他们除了连接它们的协效应外不协调任何事,所以响应式协效应在独立贡献者的开放生态中保持组装一致。

效度威胁(Threats to validity)。此处的证据来自单一生态、单一宿主语言,所以无法把范式的优点与其 TypeScript 实现或 Koishi 特定领域的优点分开,且是观察性的而非对替代架构的受控比较。因此本案例研究建立的是一个"存在且被采用"的结果,而非定量结果;衡量抽象的开销及其......

⁴Koishi 当前使用 Cordis v3。本文呈现 Cordis v4,它细化了效应与协效应语义并重新设计了加载器;核心组合模型在两版本间共享。

⁵Koishi 用 plugin 一词指代本文形式化为 component 的概念。

详细解释

Koishi 案例是整篇论文的"存在性证明":用四年、4000+ 插件的真实生态证明第3章的双系统模型既能撑起生产系统(表达力),又不绑定领域/运行时(通用性------服务端机器人和浏览器控制台是同一套原语的两个独立 Cordis 应用)。三个维度恰好对应论文标题的"时空可组合性":时间维度上,Koishi 能"控制台一键禁用插件→效应就地撤回"和"HMR 保存即重应用而保留别处连接",而1.2.1调研的传统插件系统必须重启宿主才能卸载扩展------这正是5.1.1自动逆+5.1.3惯性生命周期的工程红利,连新手作者都"无需写 uninstall 路径"就能获得有序清理,关注点局部性由抽象一次性履行。空间维度上,Koishi 生态有真实依赖拓扑(IM 适配器/数据库驱动/功能插件通过协效应连接),且这些插件由不同作者独立编写、只对"连接它们的那个 coeffect"达成共识------运行时切换存储后端只重新激活真正依赖该后端的插件,依赖缺失的插件安静等待而非报错,这正是5.1.2响应式协效应+定义46 provided by 关系在开放生态中的兑现。映射到 DSH 体系:DSH 宿主本身就是一个 Cordis 元框架实例,梁神模式/SSH/任务看板/桌面启动器等插件由不同作者开发、通过 ctx 上的服务(如 SSH 主机配置、任务看板账本)相互组合,正是 Koishi 模式的同构复现。脚注④点出一个现实:Koishi 生产用 Cordis v3,论文讲 v4(细化语义、重设计加载器),核心组合模型两版共享------说明范式本身是稳定的,v4 是工程精炼而非范式推翻。效度威胁一节诚实承认局限:单一生态、单一语言(TypeScript)、观察性而非受控对比,所以只能算"存在+被采用"证据,不能定量分离范式优点与实现优点------这把案例研究的定位限定在"存在性证明"而非"性能证明",是负责任的学术表述。

相关推荐
阿里云云原生25 分钟前
从手工回测到全自动化:AgentLoop 在 Agent Engineering 中的落地与实践总结
云原生·agent
不会写代码的女程序猿39 分钟前
养生馆采购 AI 四诊仪,5000 元预算怎么选?
大数据·人工智能·科技·ai·健康医疗
NingBo41 分钟前
你还在用浏览器使用 DeepSeek Harness 吗?为你的 DSH 打开一个客户端吧
deepseek
俊哥V1 小时前
每日 AI 研究简报 · 2026-08-30
人工智能·ai
plainGeekDev1 小时前
Agent调试、错误处理与成本优化
agent·ai编程·claude
番茄不是西红柿kk1 小时前
GLM-5.3-Flash 20分钟复刻《我的世界》实录
人工智能·ai·aigc·agent·我的世界
阿里云大数据AI技术1 小时前
DataWorks Data Agent 实战课堂(六):数据集成定时任务巡检
人工智能·agent
认真就输DBA2 小时前
一个DBA,把用了16年的两款软件换了
数据库·ai·dba·tabby·笔记软件·obsidian
张忠琳2 小时前
【deepseek-harness】Cordis 时空可组合性编程范式 — 三段式精读笔记(五)
ai·agent·deepseek·harness·cordis·dsh