refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/fs/fs-observation-policy/README.md
README.md: a126c7d0e3c806e3ce22eedfb05b21d8fd5aba60
README.zh.md: 829893028e388380e33b35ec9d5bf4c9d0a58757
@@ -0,0 +1,73 @@
# @deepseek-ai/dsh-fs-observation-policy
English | [中文](README.zh.md)
The **fs-observation-policy plugin**: it records observed presence or absence and adds read-before-edit plus guarded write/edit on top of the `ctx.fs` provider contract ([`@deepseek-ai/dsh-fs`](../fs)) — through the `fs/*` event gate, **NOT** through a method service. This plugin registers **no** `ctx.fsPolicy` service and has no public `read`/`write`/`edit`/`resolve` methods. It is the policy third of the filesystem stack: not a swappable seam, but the policy that does not belong on the `FileSystem` provider base class.
```ts
import type { Context } from '@deepseek-ai/cordis'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
declare const ctx: Context
// No service to inject — this plugin only registers the three fs/* listeners.
// Load it alongside a ctx.fs provider (e.g. @deepseek-ai/dsh-fs-local) and the
// @deepseek-ai/dsh-tool-fs tools; the tools dispatch the fs/* events this plugin
// decides. Order does not matter for resolution (no inject), but the policy
// listener should be the first decider registered for the fs/*-intent slots.
await ctx.plugin(FsPolicy)
```
## The four-layer split
| Layer | Package | Role |
|---|---|---|
| tool / executor | `@deepseek-ai/dsh-tool-fs` | model-facing schemas + read windowing + text rendering; reads/writes/edits via `ctx.fs`, dispatches the `fs/*` events |
| policy | `@deepseek-ai/dsh-fs-observation-policy` (this) | observed-state + read-before-edit + version-guarded write/edit, contributed through the `fs/*` event gate (no service) |
| provider contract | `@deepseek-ai/dsh-fs` | `ctx.fs`: text IO + atomic mutation primitives (optional version guard); owns the `fs/*` event vocabulary |
| provider | `@deepseek-ai/dsh-fs-local` | local implementation of `ctx.fs` |
## How the gate participates
Three `fs/*` events (declared by `@deepseek-ai/dsh-fs`, dispatched by `@deepseek-ai/dsh-tool-fs`):
| Event | This plugin's listener |
|---|---|
| `fs/write-intent` | Unseen or observed absent → `{ kind: 'createIfAbsent' }`; observed present → `{ kind: 'replaceIfVersion', version: vObserved }`. Single-slot decision; does NOT call `next()`. |
| `fs/edit-intent` | Unseen → `FS_NOT_OBSERVED`; observed absent → `FS_NOT_FOUND`; observed present → `{ version: vObserved }` as the CAS basis. Single-slot decision; does NOT call `next()`. |
| `fs/observed` | Records `{ kind: 'present', version }` or `{ kind: 'absent' }` for this owner+target. Synchronous, side-effect-only `WeakMap.set`. |
## Observed state is the prior-observation record; freshness is provider CAS
Observed state is a weak owner-to-target map with three logical states: unseen, confirmed absent, or present at a version. A successful file read or mutation records presence; a metadata miss from `read` or the `str_replace_editor` `view`, `str_replace`, or `insert` command records absence before returning `FS_NOT_FOUND`. The plugin performs no filesystem I/O: it converts that state into a provider guard. Presence supplies the observed version, while absence lets only a `createIfAbsent` write proceed; edit has no version basis and returns `FS_NOT_FOUND`. A windowed read observes the whole file version, so a later targeted edit is allowed only while that file remains unchanged. State is discarded on plugin disposal and is not persisted across sessions.
## Single-slot, first-wins
The `fs/write-intent`/`fs/edit-intent` slots hold exactly one decider — this plugin fully decides and does not call `next()`. The slot is first-wins by registration order; this plugin owning it is the default-deployment convention, not an event-enforced invariant (a decider registered before / `prepend`ed would win instead). This is not a composable authorization chain — layered permission/audit/sandbox interception belongs on `tools/execute`.
## No method coupling
Because the plugin influences the world only through events, removing it does not break `@deepseek-ai/dsh-tool-fs` at a service-injection boundary: the tool falls through to the bare `ctx.fs` provider (unconditional write/edit, no observed-state). Loading it back layers the policy on. That graceful add/remove is the whole point of the event gate over a mandatory method service.
## Model Experience
### Filesystem tool outcome
#### What the model sees
This plugin adds no prompt or schema. It rejects an edit without a prior observation with code `FS_NOT_OBSERVED` and exact message `edit requires reading "<path>" first`; editing a target just observed absent returns `FS_NOT_FOUND`. Guarded mutations whose positive observation is stale propagate the provider-owned `FS_STALE_VERSION` error. [`dsh-tool-fs`](../tool-fs/README.md) owns the model-facing error wrapper, which appends the recovery instruction to `FS_STALE_VERSION` (`— re-read the file, then retry`) and `FS_NOT_OBSERVED` (`— read the file, then retry`) messages while preserving the code. Following the stale remedy on an externally deleted target now records absence: the next guarded write may recreate it with `createIfAbsent`, while the provider atomically preserves any concurrent creator.
#### Token effect
Zero tokens on allowed operations beyond the ordinary tool result. A denial adds the small retained error result and avoids any success payload.
#### KV Cache effect
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
## Known Limitations and Deferred Work
- **Observed state does not survive a session resume** — persistence of the `WeakMap` record is deferred, so a resumed session must re-read files before guarded writes/edits.
- **Actors without an agent session can never satisfy the policy** — their edits throw `FS_NOT_OBSERVED` and their writes always resolve `createIfAbsent`, so a non-agent caller cannot overwrite an existing file through the gate.
- **Direct `ctx.fs` reads emit no `fs/observed`** — a file read outside the `read` tool stays unobserved, and a later guarded edit rejects with `FS_NOT_OBSERVED` until the tool reads it.
- **Authorization is version freshness, not view completeness** — any windowed read authorizes a full-file overwrite of an unchanged file, deliberately weaker than a full-view rule ([seam-split Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md)).
@@ -0,0 +1,73 @@
# @deepseek-ai/dsh-fs-observation-policy
[English](README.md) | 中文
**fs-observation-policy 插件**:它记录观测到的存在或缺失状态,并在 `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))之上增加编辑前读取和带防护的写入/编辑;它通过 `fs/*` 事件门禁参与,**不是**通过方法服务。该插件**不**注册 `ctx.fsPolicy` 服务,也没有公开的 `read`/`write`/`edit`/`resolve` 方法。它是文件系统栈的政策层:不是可替换 seam,而是不应位于 `FileSystem` 提供方基类上的政策。
```ts
import type { Context } from '@deepseek-ai/cordis'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
declare const ctx: Context
// No service to inject — this plugin only registers the three fs/* listeners.
// Load it alongside a ctx.fs provider (e.g. @deepseek-ai/dsh-fs-local) and the
// @deepseek-ai/dsh-tool-fs tools; the tools dispatch the fs/* events this plugin
// decides. Order does not matter for resolution (no inject), but the policy
// listener should be the first decider registered for the fs/*-intent slots.
await ctx.plugin(FsPolicy)
```
## 四层拆分
| 层 | 包 | 角色 |
|---|---|---|
| 工具/执行器 | `@deepseek-ai/dsh-tool-fs` | 面向模型的 schema、读取窗口和文本渲染;通过 `ctx.fs` 读取/写入/编辑,并分派 `fs/*` 事件 |
| 策略 | `@deepseek-ai/dsh-fs-observation-policy`(本包) | 通过 `fs/*` 事件门禁提供已观察状态、编辑前读取和版本防护的写入/编辑(无服务) |
| 提供方约定 | `@deepseek-ai/dsh-fs` | `ctx.fs`:文本 I/O 与原子变更原语(可选版本防护);拥有 `fs/*` 事件词汇 |
| 提供方 | `@deepseek-ai/dsh-fs-local` | `ctx.fs` 的本地实现 |
## 门禁的参与方式
三个 `fs/*` 事件(由 `@deepseek-ai/dsh-fs` 声明,`@deepseek-ai/dsh-tool-fs` 分派):
| 事件 | 本插件的监听器 |
|---|---|
| `fs/write-intent` | 未见或已观测为缺失 → `{ kind: 'createIfAbsent' }`;已观测为存在 → `{ kind: 'replaceIfVersion', version: vObserved }`。单 slot 决策;不调用 `next()`。 |
| `fs/edit-intent` | 未见 → `FS_NOT_OBSERVED`;已观测为缺失 → `FS_NOT_FOUND`;已观测为存在 → 返回 `{ version: vObserved }` 作为 CAS 基础。单 slot 决策;不调用 `next()`。 |
| `fs/observed` | 为该所有者与目标记录 `{ kind: 'present', version }` 或 `{ kind: 'absent' }`。同步、只有副作用的 `WeakMap.set`。 |
## 已观察状态是先前观察记录;新鲜度由提供方 CAS 保证
观测状态是一张以所有者为弱键、记录各目标的映射表,具有三种逻辑状态:未见、确认缺失、存在于某个版本。成功读取文件或变更会记录存在;`read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中时,都会在返回 `FS_NOT_FOUND` 前记录缺失。插件不执行文件系统 I/O:它把该状态转换为提供方防护。存在状态提供观测到的版本;缺失状态只允许 `createIfAbsent` 写入继续,edit 因没有版本基准而返回 `FS_NOT_FOUND`。窗口读取会观察整个文件的版本,因此只有文件保持不变时才允许后续的定向编辑。插件 dispose(资源释放)时会丢弃状态,并且不会跨会话持久化。
## 单 slot、先到者胜
`fs/write-intent`/`fs/edit-intent` slot 只容纳一个决策器;本插件会完整决策,不调用 `next()`。slot 按注册顺序先到者胜;由本插件拥有 slot 只是默认部署约定,不是事件强制的不变式(更早注册或通过 `prepend` 注册的决策器会胜出)。这不是可组合的授权链;分层权限/审计/沙箱拦截属于 `tools/execute`。
## 不与方法耦合
由于插件只通过事件影响外部世界,移除它不会在服务注入边界破坏 `@deepseek-ai/dsh-tool-fs`:工具会直接落到裸 `ctx.fs` 提供方(无条件写入/编辑,无已观察状态)。重新加载插件后,策略会重新生效。相比必需的方法服务,这种可平稳增删的性质正是事件门禁的全部目的。
## 模型体验
### 文件系统工具结果
#### 模型看到的内容
该插件不添加提示词或 schema。没有先前观测时,它会以代码 `FS_NOT_OBSERVED` 和精确消息 `edit requires reading "<path>" first` 拒绝编辑;编辑刚被观测为缺失的目标会返回 `FS_NOT_FOUND`。正向观测陈旧时,带防护的变更会传播由提供方拥有的 `FS_STALE_VERSION` 错误。[`dsh-tool-fs`](../tool-fs/README.md) 拥有面向模型的错误包装,会为 `FS_STALE_VERSION` 消息追加恢复指令(`— re-read the file, then retry`)、为 `FS_NOT_OBSERVED` 消息追加恢复指令(`— read the file, then retry`),同时保留错误码。外部删除目标后,遵循陈旧恢复指令会记录缺失:下一次带防护的写入可以通过 `createIfAbsent` 重新创建该目标,而提供方会以原子方式保留任何并发创建者写入的文件。
#### Token 影响
允许的操作除了普通工具结果外不增加 token。拒绝会添加少量保留的错误结果,并避免产生成功 payload。
#### KV Cache 影响
仅追加;新增可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
## 已知限制与暂缓事项
- **已观察状态无法在会话恢复后保留**:`WeakMap` 记录的持久化工作延期处理,因此恢复的会话必须重新读取文件,才能执行防护写入/编辑。
- **没有 agent(智能体)会话的参与者绝无法满足策略**:它们的编辑会抛出 `FS_NOT_OBSERVED`,写入总会解析为 `createIfAbsent`,因此非 agent 调用方无法通过门禁覆盖现有文件。
- **直接 `ctx.fs` 读取不会发出 `fs/observed`**:在 `read` 工具之外读取的文件仍未观察;后续防护编辑会以 `FS_NOT_OBSERVED` 拒绝,直到工具读取该文件。
- **授权依据是版本新鲜度,而非视图完整性**:任何窗口读取都会授权对未变文件执行全文件覆盖,这有意弱于完整视图规则(见 [seam 拆分 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md))。
@@ -0,0 +1,45 @@
{
"name": "@deepseek-ai/dsh-fs-observation-policy",
"description": "File-context policy plugin for the DeepSeek Harness — observed-state, read-before-edit, and version-guarded write/edit added over the ctx.fs provider seam through the fs/* event gate (no service API)",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/fs/fs-observation-policy"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}
@@ -0,0 +1,130 @@
/**
* Event-only filesystem observation policy; it registers no service. A weak owner/target map
* records every authoritative presence/absence observation, single-slot intent listeners derive
* guards from that state, and the provider performs the atomic freshness/no-clobber check. Without
* this plugin, tools retain the bare provider's unconditional mutation behavior. See the package
* README for composition rules.
* @module @deepseek-ai/dsh-fs-observation-policy
*/
import type { Context } from '@deepseek-ai/cordis'
import { FsError } from '@deepseek-ai/dsh-fs'
import type { FsObservation, FsTarget, FsVersion, FsWriteIntent } from '@deepseek-ai/dsh-fs'
import type { FsObservationActor } from './types.ts'
export type { FsObservationActor } from './types.ts'
/**
* Per-context observed-file state and the three `fs/*` decisions over it. One
* instance is created per `apply()` so disposal can drop all state for HMR.
*/
class ObservedStateGate {
/**
* Observed-file state, keyed first by the owner object (weakly held, so a
* collected session frees its state), then by {@link FsTarget.targetKey}. An
* entry's presence is the prior-observation record; its discriminant keeps
* confirmed absence distinct from an unseen target.
*/
private observed = new WeakMap<object, Map<string, FsObservation>>()
/**
* Derive the observed-state owner from the opaque event actor — normally the
* active agent session. `undefined` when no owner can be derived (e.g. a
* direct tool call with no agent); such calls read freely but cannot satisfy
* the write/edit prior-observation policy.
*/
private owner(actor: object | undefined): object | undefined {
// tsgolint treats object as assignable to weak FsObservationActor, while tsc still requires the structural cast for property access.
// See the analyzer-divergence consequence in .agents/notes/implemented/process/2026-07-29-oxlint-linter.md.
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- The analyzers disagree on this weak type.
return (actor as FsObservationActor | undefined)?.agent?.session
}
private get(owner: object, targetKey: string): FsObservation | undefined {
return this.observed.get(owner)?.get(targetKey)
}
private set(owner: object, targetKey: string, observation: FsObservation): void {
let byTarget = this.observed.get(owner)
if (!byTarget) {
byTarget = new Map()
this.observed.set(owner, byTarget)
}
byTarget.set(targetKey, observation)
}
/** Drop all recorded state (HMR safety / disposal). */
clear(): void {
this.observed = new WeakMap()
}
/**
* Decide the write intent: unseen or confirmed absent ⇒ `createIfAbsent`;
* confirmed present ⇒ `replaceIfVersion` at the observed version.
*/
writeIntent(target: FsTarget, actor: object | undefined): FsWriteIntent {
const owner = this.owner(actor)
const prior = owner ? this.get(owner, target.targetKey) : undefined
return prior?.kind === 'present'
? { kind: 'replaceIfVersion', version: prior.version }
: { kind: 'createIfAbsent' }
}
/**
* Decide the edit version guard: unseen rejects with `FS_NOT_OBSERVED`,
* confirmed absence rejects with `FS_NOT_FOUND`, and presence supplies the
* observed version as the CAS basis.
*/
editIntent(target: FsTarget, actor: object | undefined): { version: FsVersion } {
const owner = this.owner(actor)
const prior = owner ? this.get(owner, target.targetKey) : undefined
if (!owner || prior === undefined) {
throw new FsError(`edit requires reading "${target.displayPath}" first`, 'FS_NOT_OBSERVED')
}
if (prior.kind === 'absent') {
throw new FsError(`cannot edit "${target.displayPath}": not found`, 'FS_NOT_FOUND')
}
return { version: prior.version }
}
/** Record an authoritative present or absent observation for this owner and target. */
observe(target: FsTarget, observation: FsObservation, actor: object | undefined): void {
const owner = this.owner(actor)
if (owner) this.set(owner, target.targetKey, observation)
}
}
/** Cordis plugin name used by loader diagnostics. */
export const name = 'fs-observation-policy'
/**
* Register the three `fs/*` listeners. No `inject` — this plugin reads no
* services; it operates only on its own `WeakMap`. The waterfalls are unbound
* (the tool dispatches them with no `this`), so the listeners take the raw
* `(target, actor, next)` arguments.
*/
export function apply(ctx: Context): void {
const gate = new ObservedStateGate()
ctx.effect(() => () => {
// Drop all recorded state on disposal so a reloaded plugin starts clean
// (HMR safety). The WeakMap itself would be GC'd, but replacing it makes the
// release observable and immediate for tests.
gate.clear()
}, 'fs-observation-policy observed-state teardown')
// fs/write-intent: occupy the single decision slot — do NOT call next().
// Deferred through Promise.resolve().then so the declared Promise return type
// holds (a throw rejects, never escapes synchronously through the waterfall).
ctx.on('fs/write-intent', (target, actor) => Promise.resolve().then(() => gate.writeIntent(target, actor)))
// fs/edit-intent: occupy the single decision slot — do not call next().
ctx.on('fs/edit-intent', (target, actor) => Promise.resolve().then(() => gate.editIntent(target, actor)))
// fs/observed must remain synchronous and non-throwing: emit does not await
// promises, and successful mutations have already committed. WeakMap.set
// satisfies that contract for both presence and absence.
ctx.on('fs/observed', (target, observation, actor) => {
gate.observe(target, observation, actor)
})
}
@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-fs-observation-policy`.
* @module @deepseek-ai/dsh-fs-observation-policy/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-fs-observation-policy'
/** Cordis companion plugin name. */
export const name = 'fs-observation-policy-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
* beyond contracts enforced at its owning seam.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,29 @@
/**
* Vocabulary for the fs-observation-policy plugin: the minimal execution-context
* fields used to derive an observed-state owner by narrowing the opaque `object`
* actor the `fs/*` events carry.
*
* The provider vocabulary (`FsTarget`, `FsVersion`, write/edit request types) is
* re-used from `@deepseek-ai/dsh-fs`; this package owns only the observed-state
* owner structure on top of it.
*
* @module @deepseek-ai/dsh-fs-observation-policy/types
*/
/**
* Minimal structural view of a tool execution the policy plugin needs to derive
* an observed-state owner. `@deepseek-ai/dsh-tools`' `ToolExecution` contains
* these fields, so the tool passes its `exec` straight through as the opaque
* `object` actor on the `fs/*` events; this plugin narrows that actor to
* `FsObservationActor` without importing `dsh-tools`, `dsh-agent`, or `dsh-session`.
*
* The owner is `agent.session` when present. It is treated as an opaque object
* identity (a `WeakMap` key); this package never reads any of its fields.
*/
export interface FsObservationActor {
/** The agent on whose behalf the call runs, when there is one. */
agent?: {
/** The session that owns observed-file state, used as an opaque key. */
session?: object
}
}
@@ -0,0 +1,236 @@
/** Event-level policy tests; no filesystem provider is needed because the plugin performs no I/O. */
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type { FsObservation, FsTarget, FsWriteIntent } from '@deepseek-ai/dsh-fs'
import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy'
import type { FsObservationActor } from '@deepseek-ai/dsh-fs-observation-policy'
function target(path: string): FsTarget {
return { targetKey: FsTargetKey(path), displayPath: path }
}
const ownerExec = (session: object): FsObservationActor => ({ agent: { session } })
const present = (version: string): FsObservation => ({ kind: 'present', version: FsVersion(version) })
const absent: FsObservation = { kind: 'absent' }
/** Dispatch the write-intent waterfall with the bare default thunk. */
function writeIntent(ctx: Context, t: FsTarget, actor: object | undefined): Promise<FsWriteIntent | undefined> {
return ctx.waterfall('fs/write-intent', t, actor, () => undefined)
}
/** Dispatch the edit-intent waterfall with the bare default thunk. */
function editIntent(ctx: Context, t: FsTarget, actor: object | undefined): Promise<{ version: FsVersion } | undefined> {
return ctx.waterfall('fs/edit-intent', t, actor, () => undefined)
}
async function setup() {
const ctx = new Context()
const fiber = await ctx.plugin(FsPolicy)
return { ctx, fiber }
}
describe('registration / disposal', () => {
it('registers no service API (it is a plugin, not ctx.fsPolicy)', async () => {
const { ctx } = await setup()
expect((ctx as Context & { fsPolicy?: unknown }).fsPolicy).toBeUndefined()
})
it('mounts with no inject (reads no services)', async () => {
// It mounts immediately even with nothing else in the context.
const ctx = new Context()
await ctx.plugin(FsPolicy)
// The listener is live: an unobserved write decides createIfAbsent.
expect(await writeIntent(ctx, target('a.txt'), undefined)).toEqual({ kind: 'createIfAbsent' })
})
})
describe('write-intent decision', () => {
it('an unobserved target decides createIfAbsent', async () => {
const { ctx } = await setup()
expect(await writeIntent(ctx, target('a.txt'), ownerExec({}))).toEqual({ kind: 'createIfAbsent' })
})
it('a no-owner actor decides createIfAbsent', async () => {
const { ctx } = await setup()
expect(await writeIntent(ctx, target('a.txt'), undefined)).toEqual({ kind: 'createIfAbsent' })
expect(await writeIntent(ctx, target('a.txt'), {})).toEqual({ kind: 'createIfAbsent' })
})
it('an actor with an agent but no session has no owner (createIfAbsent)', async () => {
// The middle optional-chain rung: agent present, session undefined ⇒ owner
// undefined ⇒ unobservable, so a write can only be a blind create.
const { ctx } = await setup()
expect(await writeIntent(ctx, target('a.txt'), { agent: {} })).toEqual({ kind: 'createIfAbsent' })
})
it('an observed target decides replaceIfVersion at the observed version', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), present('v7'), exec)
expect(await writeIntent(ctx, target('a.txt'), exec)).toEqual({ kind: 'replaceIfVersion', version: 'v7' })
})
it('a target observed absent decides createIfAbsent', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), absent, exec)
expect(await writeIntent(ctx, target('a.txt'), exec)).toEqual({ kind: 'createIfAbsent' })
})
})
describe('edit-intent decision', () => {
it('rejects an unread edit with FS_NOT_OBSERVED', async () => {
const { ctx } = await setup()
await expect(editIntent(ctx, target('a.txt'), ownerExec({}))).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
})
it('rejects an edit with no owner (cannot prove prior observation)', async () => {
const { ctx } = await setup()
await expect(editIntent(ctx, target('a.txt'), undefined)).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
})
it('rejects an edit whose actor has an agent but no session (no owner)', async () => {
const { ctx } = await setup()
await expect(editIntent(ctx, target('a.txt'), { agent: {} })).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
})
it('returns the observed version as the CAS basis after an observation', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), present('v3'), exec)
expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v3' })
})
it('rejects editing a target observed absent with FS_NOT_FOUND', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), absent, exec)
await expect(editIntent(ctx, target('a.txt'), exec)).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
})
})
describe('observed-state is the prior-observation record', () => {
it('a read observation authorizes an in-place write at that version', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), present('v0'), exec) // a read
expect(await writeIntent(ctx, target('a.txt'), exec)).toEqual({ kind: 'replaceIfVersion', version: 'v0' })
})
it('a write/edit observation refreshes the basis, so the next edit needs no re-read', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
// A create records v1; the follow-up edit guards against v1 with no read.
ctx.emit('fs/observed', target('a.txt'), present('v1'), exec)
expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v1' })
// The edit records v2; a second edit guards against v2.
ctx.emit('fs/observed', target('a.txt'), present('v2'), exec)
expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v2' })
})
it('a no-owner observation records nothing', async () => {
const { ctx } = await setup()
ctx.emit('fs/observed', target('a.txt'), present('v0'), undefined)
// Still unobserved for any owner.
await expect(editIntent(ctx, target('a.txt'), ownerExec({}))).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
})
it('supports present → absent → present transitions for one owner', async () => {
const { ctx } = await setup()
const exec = ownerExec({})
const a = target('a.txt')
ctx.emit('fs/observed', a, present('v1'), exec)
expect(await writeIntent(ctx, a, exec)).toEqual({ kind: 'replaceIfVersion', version: 'v1' })
ctx.emit('fs/observed', a, absent, exec)
expect(await writeIntent(ctx, a, exec)).toEqual({ kind: 'createIfAbsent' })
await expect(editIntent(ctx, a, exec)).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
ctx.emit('fs/observed', a, present('v2'), exec)
expect(await editIntent(ctx, a, exec)).toEqual({ version: 'v2' })
})
})
describe('multi-owner isolation', () => {
it('owner A observing does not grant owner B edit authority', async () => {
const { ctx } = await setup()
const a = ownerExec({})
const b = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), present('v0'), a)
await expect(editIntent(ctx, target('a.txt'), b)).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
expect(await editIntent(ctx, target('a.txt'), a)).toEqual({ version: 'v0' })
})
it('each owner records its own observed version independently', async () => {
const { ctx } = await setup()
const a = ownerExec({})
const b = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), present('v0'), a) // A observed v0
// B never observed → createIfAbsent; A still holds v0 → replaceIfVersion.
expect(await writeIntent(ctx, target('a.txt'), b)).toEqual({ kind: 'createIfAbsent' })
expect(await writeIntent(ctx, target('a.txt'), a)).toEqual({ kind: 'replaceIfVersion', version: 'v0' })
})
})
describe('single-slot, first-wins', () => {
it('fully decides the slot without calling next() (the bare default is unreached)', async () => {
const { ctx } = await setup()
let defaultRan = false
const intent = await ctx.waterfall('fs/write-intent', target('a.txt'), ownerExec({}), () => {
defaultRan = true
return undefined
})
expect(intent).toEqual({ kind: 'createIfAbsent' })
expect(defaultRan).toBe(false)
})
it('a SECOND decider registered AFTER fs-observation-policy is not reached (first-wins short-circuit)', async () => {
const { ctx } = await setup()
let secondRan = false
// Registered after fs-observation-policy, so it dispatches second; fs-observation-policy does
// not call next(), so this never runs. (A decider registered BEFORE — or with
// prepend — would instead win: first-wins is by convention, not enforced.)
ctx.on('fs/edit-intent', () => {
secondRan = true
return Promise.resolve(undefined)
})
const exec = ownerExec({})
ctx.emit('fs/observed', target('a.txt'), present('v0'), exec)
await editIntent(ctx, target('a.txt'), exec)
expect(secondRan).toBe(false)
})
it('a SECOND write-intent decider registered AFTER fs-observation-policy is not reached', async () => {
const { ctx } = await setup()
let secondRan = false
ctx.on('fs/write-intent', () => {
secondRan = true
return Promise.resolve(undefined)
})
await writeIntent(ctx, target('a.txt'), ownerExec({}))
expect(secondRan).toBe(false)
})
})
describe('disposal releases recorded state (HMR safety)', () => {
it('a fresh plugin after disposal starts with no inherited state', async () => {
const ctx = new Context()
const exec = ownerExec({})
const fiber = await ctx.plugin(FsPolicy)
ctx.emit('fs/observed', target('a.txt'), present('v0'), exec)
expect(await editIntent(ctx, target('a.txt'), exec)).toEqual({ version: 'v0' })
await fiber.dispose()
await ctx.plugin(FsPolicy)
// Same owner object, but state was released on disposal.
await expect(editIntent(ctx, target('a.txt'), exec)).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
})
it('no listeners remain after disposal (the gate no longer decides)', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(FsPolicy)
await fiber.dispose()
// With no listener, the waterfall falls through to the bare default.
expect(await writeIntent(ctx, target('a.txt'), ownerExec({}))).toBeUndefined()
})
})
@@ -0,0 +1,25 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../llm/llm"
},
{
"path": "../fs"
},
{
"path": "../../runtime-diagnostics/invariants"
}
]
}