fix(fs): address review — rename to dsh-fs-policy, fs/*-intent events, RFC currency, ENOTDIR
Rename per review naming decisions: - package dsh-file-context → dsh-fs-policy (dir, package name, plugin name, tsconfig refs, importers, type-equiv manifest, generated catalog + module-graph) - events fs/write-expectation → fs/write-intent, fs/edit-expectation → fs/edit-intent (fs/observed unchanged); type FsWriteExpectation → FsWriteIntent, "expectation" wording → "intent" throughout - exported FileContextExec → FsPolicyExec Make the implemented RFCs describe what shipped, not the superseded designs: the 2026-06-17 capability-seam + tool-schemas RFCs no longer place policy on ctx.fs or use full/partial-view authorization, and the fsspec RFC's ctx.fileContext service prose is rewritten to the fs/* event-gate reality (freshness-based auth). Sharpen docs/rfc/implemented/AGENTS.md: a rename is a fact to fix IN PLACE — the "new RFC" escape hatch is for macro decision reversals only, not renames. Code fixes from review: - fsio.ts resolveLocalTarget/probe translate ENOTDIR (a parent path segment is a file) into the structured FsError taxonomy instead of leaking a raw Node error; resolve reports FS_NOT_FOUND, probe reports absent. Regression tests proven to fail on the unfixed code. - tool-fs HMR test now asserts prompt sections (not just tool schemas) are withdrawn on disposal. - fs/observed is a plain (unguarded) ctx.emit: correct the fs-policy comment, filesystem.md, and tool-fs module doc that wrongly claimed the tool "contains" a throwing listener; a throw surfaces as the tool's isError result. - drop the false "loaded by the default product config" claim (no config wires the fs tools yet), the duplicate ctx.bash service-map row, the stale FileReadRequest catalog link-map entry, and the fs/fs README EOF blank line; correct the dsh-fs package.json description.
This commit is contained in:
@@ -7,7 +7,7 @@ This package is the provider-seam layer of the four-layer filesystem stack, spli
|
||||
| Layer | Package | Role |
|
||||
|---|---|---|
|
||||
| tool / executor | `@deepseek-ai/dsh-tool-fs` | model-facing `read`/`write`/`edit` schemas + read windowing + text rendering; reads/writes/edits via `ctx.fs`, dispatches the `fs/*` events |
|
||||
| policy | `@deepseek-ai/dsh-file-context` | observed-state + read-before-edit + version-guarded write/edit, contributed through the `fs/*` event gate (no service) |
|
||||
| policy | `@deepseek-ai/dsh-fs-policy` | observed-state + read-before-edit + version-guarded write/edit, contributed through the `fs/*` event gate (no service) |
|
||||
| provider seam | `@deepseek-ai/dsh-fs` (this) | `ctx.fs`: text IO + atomic mutation primitives (optional version guard); owns the `fs/*` event vocabulary |
|
||||
| provider | `@deepseek-ai/dsh-fs-local` | the host-filesystem implementation |
|
||||
|
||||
@@ -23,22 +23,21 @@ A backend subclasses `FileSystem` and implements six primitives.
|
||||
| `stat(target, signal?)` | Return `FsInfo` metadata (`version`, `type`, optional `size`), or `undefined` when the target is absent. Never content. |
|
||||
| `readText(target, signal?)` | Read the whole regular text file as one decoded string. Owns regular-file checks, UTF-8 decoding, binary/NUL rejection (`FS_NOT_TEXT`). |
|
||||
| `streamText(target, signal?)` | Stream the same text as decoded chunks for large files (cross-chunk UTF-8 decoding stays here). |
|
||||
| `writeText(target, content, expected?, signal?)` | Atomic create/replace. `expected` is OPTIONAL: omit ⇒ unconditional create-or-overwrite; supply an `FsWriteExpectation` (`createIfAbsent`/`replaceIfVersion`) to guard. |
|
||||
| `writeText(target, content, expected?, signal?)` | Atomic create/replace. `expected` is OPTIONAL: omit ⇒ unconditional create-or-overwrite; supply an `FsWriteIntent` (`createIfAbsent`/`replaceIfVersion`) to guard. |
|
||||
| `editText(target, edit, expected?, signal?)` | Literal edit. `expected` is OPTIONAL: omit ⇒ unconditional edit of the current content; supply `{ version }` to guard (verified BEFORE matching). A missing target reports `FS_STALE_VERSION` either way. Applies and writes atomically — one mutation critical section. |
|
||||
|
||||
The mutation runs inside the backend's per-target lock either way, so an unconditional write/edit is still atomic — "unconditional" drops the *version* precondition, not the atomicity.
|
||||
|
||||
## The `fs/*` policy events
|
||||
|
||||
This package declares three events (see the generated [catalog](../../../docs/cordis-catalog/events-and-services.md)) so the emitter (`@deepseek-ai/dsh-tool-fs`) and the policy listener (`@deepseek-ai/dsh-file-context`) share a vocabulary without the emitter depending on the policy plugin. `fs/write-expectation` and `fs/edit-expectation` are single-slot decision waterfalls (the listener fully decides, never calling `next()`); `fs/observed` is a fire-and-forget recording event. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
|
||||
This package declares three events (see the generated [catalog](../../../docs/cordis-catalog/events-and-services.md)) so the emitter (`@deepseek-ai/dsh-tool-fs`) and the policy listener (`@deepseek-ai/dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. `fs/write-intent` and `fs/edit-intent` are single-slot decision waterfalls (the listener fully decides, never calling `next()`); `fs/observed` is a fire-and-forget recording event. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
|
||||
|
||||
## A provider seam, not the policy layer
|
||||
|
||||
`ctx.fs` is deliberately close to fsspec-style storage primitives — half a level above byte-level `cat`/`open`, because it decodes text and rejects binaries so the policy layer never touches raw bytes. It owns UTF-8 decoding, binary rejection, atomic writes, and the literal-edit critical section. It does **not** own line windows, numbered lines, rendered footers, or observed-state. Observed-state, read-before-edit, and version-guarded write/edit are policy a plugin (`@deepseek-ai/dsh-file-context`) ADDS by supplying the optional guard — not provider behavior — so a sandboxed/remote backend inherits no model-facing observation policy.
|
||||
`ctx.fs` is deliberately close to fsspec-style storage primitives — half a level above byte-level `cat`/`open`, because it decodes text and rejects binaries so the policy layer never touches raw bytes. It owns UTF-8 decoding, binary rejection, atomic writes, and the literal-edit critical section. It does **not** own line windows, numbered lines, rendered footers, or observed-state. Observed-state, read-before-edit, and version-guarded write/edit are policy a plugin (`@deepseek-ai/dsh-fs-policy`) ADDS by supplying the optional guard — not provider behavior — so a sandboxed/remote backend inherits no model-facing observation policy.
|
||||
|
||||
`editText` stays on this seam (not composed in the policy layer from a read plus a write) because version guard + literal match + atomic rewrite must stay inside one critical section for correct error attribution and one-wins/one-stale concurrency, and a remote backend may implement it as a native compare-and-edit.
|
||||
|
||||
## Vocabulary
|
||||
|
||||
`FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids RFC](../../../docs/rfc/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsWriteExpectation` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy RFC](../../../docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry surfaces `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts.
|
||||
|
||||
`FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids RFC](../../../docs/rfc/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsWriteIntent` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy RFC](../../../docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry surfaces `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-fs",
|
||||
"description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service, and the read-before-write/edit file-state contract",
|
||||
"description": "Abstract filesystem capability seam (ctx.fs) for the DeepSeek Harness — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* The filesystem provider seam (`ctx.fs`): an abstract service defining the
|
||||
* text-storage primitives a backend provides — resolve a path into a stable
|
||||
* target, stat its metadata, read/stream its text, write it atomically with an
|
||||
* explicit expectation, and apply a guarded literal edit — without saying HOW.
|
||||
* explicit intent, and apply a guarded literal edit — without saying HOW.
|
||||
* Implementations subclass {@link FileSystem} and register themselves as the
|
||||
* `fs` service; `@deepseek-ai/dsh-fs-local` (the host filesystem) is the first.
|
||||
* Future implementations swap in sandboxed, remote, virtual, or project-scoped
|
||||
@@ -20,7 +20,7 @@
|
||||
* literal-edit critical section — but NOT line windows, numbered lines,
|
||||
* rendered footers, or observed-state. Read windowing lives in the model-facing
|
||||
* tool (`@deepseek-ai/dsh-tool-fs`); observed-state and read-before-write/edit
|
||||
* are policy a plugin (`@deepseek-ai/dsh-file-context`) adds through the `fs/*`
|
||||
* are policy a plugin (`@deepseek-ai/dsh-fs-policy`) adds through the `fs/*`
|
||||
* event gate. So a sandboxed/remote backend inherits no model-facing observation
|
||||
* policy it has no business carrying.
|
||||
*
|
||||
@@ -41,14 +41,14 @@
|
||||
* unconditional write/edit is still atomic; "unconditional" drops the *version*
|
||||
* precondition, not the atomicity. Observed-state, read-before-edit, and
|
||||
* version-guarded write/edit are NOT provider behavior — they are policy a
|
||||
* plugin (`@deepseek-ai/dsh-file-context`) adds on top by supplying the guard.
|
||||
* plugin (`@deepseek-ai/dsh-fs-policy`) adds on top by supplying the guard.
|
||||
*
|
||||
* ## The fs policy events live here, not in the policy plugin
|
||||
*
|
||||
* This package owns the `fs/write-expectation`, `fs/edit-expectation`, and
|
||||
* This package owns the `fs/write-intent`, `fs/edit-intent`, and
|
||||
* `fs/observed` event vocabulary (see {@link Events}). The emitter is
|
||||
* `@deepseek-ai/dsh-tool-fs` and the default listener is
|
||||
* `@deepseek-ai/dsh-file-context`; the events live in the one package both
|
||||
* `@deepseek-ai/dsh-fs-policy`; the events live in the one package both
|
||||
* already depend on, so the emitter shares a vocabulary with the policy listener
|
||||
* without depending on the policy plugin. The events carry only `dsh-fs`
|
||||
* vocabulary plus an opaque `object` actor — no model-facing concepts (line
|
||||
@@ -64,7 +64,7 @@ import type {
|
||||
FsInfo,
|
||||
FsTarget,
|
||||
FsVersion,
|
||||
FsWriteExpectation,
|
||||
FsWriteIntent,
|
||||
FsWriteOutcome,
|
||||
} from './types.ts'
|
||||
|
||||
@@ -79,7 +79,7 @@ export type {
|
||||
FsErrorCode,
|
||||
FsInfo,
|
||||
FsTarget,
|
||||
FsWriteExpectation,
|
||||
FsWriteIntent,
|
||||
FsWriteOutcome,
|
||||
} from './types.ts'
|
||||
|
||||
@@ -90,11 +90,11 @@ declare module 'cordis' {
|
||||
|
||||
interface Events {
|
||||
/**
|
||||
* Single-slot decision: produce the write expectation for the next
|
||||
* Single-slot decision: produce the write intent for the next
|
||||
* {@link FileSystem.writeText}. The tool dispatches this as an unbound
|
||||
* waterfall (no `this`) and supplies a default thunk returning `undefined`
|
||||
* (unconditional create-or-overwrite — the bare provider). The
|
||||
* `@deepseek-ai/dsh-file-context` policy listener returns `createIfAbsent`
|
||||
* `@deepseek-ai/dsh-fs-policy` policy listener returns `createIfAbsent`
|
||||
* (unobserved actor) or `{ kind: 'replaceIfVersion', version: vObserved }`
|
||||
* (observed) and does NOT call `next()` — one decision, not a composable
|
||||
* chain. The slot is first-wins: the first non-`next()` decider (registration
|
||||
@@ -102,23 +102,23 @@ declare module 'cordis' {
|
||||
* not layering. `actor` is the opaque tool-execution context, never read here.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'fs/write-expectation'(target: FsTarget, actor: object | undefined, next: () => FsWriteExpectation | undefined | Promise<FsWriteExpectation | undefined>): Promise<FsWriteExpectation | undefined>
|
||||
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
|
||||
/**
|
||||
* Single-slot decision: produce the optional version guard for the next
|
||||
* {@link FileSystem.editText}. The tool dispatches this as an unbound
|
||||
* waterfall and supplies a default thunk returning `undefined` (unconditional
|
||||
* edit of the current content — the bare provider; no `stat`). The
|
||||
* `@deepseek-ai/dsh-file-context` policy listener returns
|
||||
* `@deepseek-ai/dsh-fs-policy` policy listener returns
|
||||
* `{ version: vObserved }`, or throws `FS_NOT_OBSERVED` if the actor is unset
|
||||
* or has not observed the target. Does NOT call `next()`: one decision,
|
||||
* first-wins (see {@link Events.'fs/write-expectation'}).
|
||||
* first-wins (see {@link Events.'fs/write-intent'}).
|
||||
* @mode waterfall
|
||||
*/
|
||||
'fs/edit-expectation'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
||||
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
||||
/**
|
||||
* Record that an actor observed a target at a version, after a successful
|
||||
* read/write/edit. Fire-and-forget (plain `emit`). A listener MUST be a
|
||||
* synchronous, side-effect-only recorder (`@deepseek-ai/dsh-file-context`'s
|
||||
* synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s
|
||||
* is a `WeakMap.set`): the tool does not guard the emit, so a listener that
|
||||
* throws surfaces as the tool's `isError` result, and cordis `emit` does not
|
||||
* await listener promises — async or fallible audit/telemetry does not
|
||||
@@ -147,7 +147,7 @@ declare module 'cordis' {
|
||||
* binary/NUL rejection, and `FS_NOT_TEXT`.
|
||||
* - {@link writeText} is atomic temp-file + rename. `expected` is OPTIONAL:
|
||||
* omit it for an unconditional create-or-overwrite (the bare-provider default),
|
||||
* or supply a {@link FsWriteExpectation} to guard the write.
|
||||
* or supply a {@link FsWriteIntent} to guard the write.
|
||||
* - {@link editText} verifies `expected.version` BEFORE literal matching (so a
|
||||
* stale edit reports `FS_STALE_VERSION`, not `FS_EDIT_NOT_FOUND`/
|
||||
* `FS_AMBIGUOUS_EDIT` against newer content), then applies literal replacement
|
||||
@@ -188,7 +188,7 @@ export abstract class FileSystem extends Service {
|
||||
* unconditional create-or-overwrite (the bare provider — no version guard, no
|
||||
* read-first requirement). Atomic either way.
|
||||
*/
|
||||
abstract writeText(target: FsTarget, content: string, expected?: FsWriteExpectation, signal?: AbortSignal): Promise<FsWriteOutcome>
|
||||
abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome>
|
||||
|
||||
/**
|
||||
* Apply a literal edit to an existing UTF-8 text file. When `expected` is
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Vocabulary for the filesystem provider seam (`ctx.fs`): the opaque
|
||||
* target/version identities, the metadata `stat` returns, the write-expectation
|
||||
* target/version identities, the metadata `stat` returns, the write-intent
|
||||
* and outcome shapes, the literal-edit request/outcome, and the typed error
|
||||
* taxonomy.
|
||||
*
|
||||
* These types are shared by every backend (`@deepseek-ai/dsh-fs-local` and
|
||||
* future sandboxed/remote backends) and by the policy layer
|
||||
* (`@deepseek-ai/dsh-file-context`). They are deliberately a *text-storage*
|
||||
* (`@deepseek-ai/dsh-fs-policy`). They are deliberately a *text-storage*
|
||||
* vocabulary half a level above byte-level fsspec: `readText`/`streamText` hand
|
||||
* back decoded text, never raw bytes. Host-path assumptions stay out — `targetKey`
|
||||
* and `version` are opaque branded tokens, and `displayPath` is the only field a
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* Model-facing concepts (line windows, numbered lines, observed-state) do NOT
|
||||
* live here; they belong to the consumer tool and the policy plugin
|
||||
* (`@deepseek-ai/dsh-tool-fs` / `@deepseek-ai/dsh-file-context`).
|
||||
* (`@deepseek-ai/dsh-tool-fs` / `@deepseek-ai/dsh-fs-policy`).
|
||||
*
|
||||
* @module @deepseek-ai/dsh-fs/types
|
||||
*/
|
||||
@@ -91,7 +91,7 @@ export interface FsInfo {
|
||||
* is expressed by omission, so the write and edit mutations share one symmetric
|
||||
* shape (`expected?`: omit = unconditional, present = guarded).
|
||||
*/
|
||||
export type FsWriteExpectation =
|
||||
export type FsWriteIntent =
|
||||
| { kind: 'createIfAbsent' }
|
||||
| { kind: 'replaceIfVersion'; version: FsVersion }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tests for the filesystem provider seam itself: registration, duplicate-service
|
||||
* behavior, disposal, and the branded id factories. The provider primitives and
|
||||
* policy live in `dsh-fs-local` and `dsh-file-context`; this seam owns only the
|
||||
* policy live in `dsh-fs-local` and `dsh-fs-policy`; this seam owns only the
|
||||
* abstract service contract, so a minimal fake backend exercises it.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
FsEditRequest,
|
||||
FsInfo,
|
||||
FsTarget,
|
||||
FsWriteExpectation,
|
||||
FsWriteIntent,
|
||||
FsWriteOutcome,
|
||||
} from '@deepseek-ai/dsh-fs'
|
||||
|
||||
@@ -38,7 +38,7 @@ class FakeFileSystem extends FileSystem {
|
||||
const content = await this.readText(target)
|
||||
return (async function* () { yield content })()
|
||||
}
|
||||
override async writeText(target: FsTarget, content: string, _expected?: FsWriteExpectation): Promise<FsWriteOutcome> {
|
||||
override async writeText(target: FsTarget, content: string, _expected?: FsWriteIntent): Promise<FsWriteOutcome> {
|
||||
const existed = this.files.has(target.targetKey)
|
||||
this.files.set(target.targetKey, content)
|
||||
return { operation: existed ? 'update' : 'create', version: FsVersion('v2') }
|
||||
|
||||
Reference in New Issue
Block a user