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:
Tianyi Cui
2026-07-02 03:12:38 +08:00
parent e4b4eaaf38
commit 30c1863755
42 changed files with 360 additions and 323 deletions

View File

@@ -1,10 +1,10 @@
/**
* The model-facing `edit` tool: update an existing UTF-8 text file by replacing
* literal text, requiring a unique match by default. The tool is the executor:
* it dispatches the `fs/edit-expectation` waterfall to obtain the optional
* it dispatches the `fs/edit-intent` waterfall to obtain the optional
* version guard, calls `ctx.fs.editText` directly, and emits `fs/observed`. The
* default thunk returns `undefined` (unconditional edit of the current content
* — the bare provider); a policy plugin (`@deepseek-ai/dsh-file-context`)
* — the bare provider); a policy plugin (`@deepseek-ai/dsh-fs-policy`)
* occupies the single decision slot, returning `{ version: vObserved }` or
* throwing `FS_NOT_OBSERVED` for an unread file. The tool stats ZERO times
* either way; a missing target is reported by the provider as `FS_STALE_VERSION`.
@@ -52,7 +52,7 @@ export function applyEditTool(ctx: Context): void {
ctx.systemPrompt.section({
name: 'tool:edit',
order: 102,
text: 'Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default file-context policy requires it), unless you just created or edited it in this session.',
text: 'Use the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.',
})
ctx.tools.register(defineTool({
@@ -70,11 +70,11 @@ export function applyEditTool(ctx: Context): void {
// Single-slot decision: the policy plugin returns { version: vObserved } or
// throws FS_NOT_OBSERVED; the bare default is undefined (unconditional edit).
// No stat — the bare default never manufactures a version basis.
const expectation = await ctx.waterfall('fs/edit-expectation', target, exec, () => undefined)
const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)
const outcome = await ctx.fs.editText(
target,
{ oldString: input.oldString, newString: input.newString, replaceAll: input.replaceAll },
expectation,
intent,
exec.signal,
)
// Record the observed version (a no-op when no policy plugin listens).

View File

@@ -8,15 +8,16 @@
* concerns only — tool names, JSON schemas, argument validation, prompt
* sections, read windowing, result formatting. It does NOT inject a policy
* service. Instead, on each write/edit it dispatches a single-slot waterfall
* (`fs/write-expectation`/`fs/edit-expectation`) to obtain the OPTIONAL version
* guard, and after every read/write/edit it emits a contained `fs/observed`. A
* policy plugin (`@deepseek-ai/dsh-file-context`, loaded by the default product
* config) occupies the decision slot and listens for `fs/observed` to add
* observed-state + read-before-edit + version-guarded write/edit. With no policy
* plugin the waterfalls fall through to their `undefined` default (the
* unconstrained bare provider) and `fs/observed` is unheard — the tool still
* functions. This package never imports `node:fs`, `node:path`, or an
* `@deepseek-ai/dsh-fs-local` implementation.
* (`fs/write-intent`/`fs/edit-intent`) to obtain the OPTIONAL version guard, and
* after every read/write/edit it emits `fs/observed` with a plain (unguarded)
* `ctx.emit`. A policy plugin (`@deepseek-ai/dsh-fs-policy`) occupies the
* decision slot and listens for `fs/observed` to add observed-state +
* read-before-edit + version-guarded write/edit; a deployment that loads these
* tools is expected to also load it. With no policy plugin the waterfalls fall
* through to their `undefined` default (the unconstrained bare provider) and
* `fs/observed` is unheard — the tool still functions. This package never
* imports `node:fs`, `node:path`, or an `@deepseek-ai/dsh-fs-local`
* implementation.
*
* @module @deepseek-ai/dsh-tool-fs
*/

View File

@@ -3,7 +3,7 @@
* line-numbered content with pagination guidance. The tool is the executor — it
* stats and reads through `ctx.fs` directly, builds the line window
* ({@link module:@deepseek-ai/dsh-tool-fs/read-render}), and emits `fs/observed`
* so a policy plugin (`@deepseek-ai/dsh-file-context`) can record the read. With
* so a policy plugin (`@deepseek-ai/dsh-fs-policy`) can record the read. With
* no policy plugin the emit is simply unheard. This module owns the
* model-facing schema, argument validation, and the read I/O; the rendering
* (windowing + formatting) lives in `read-render.ts` and the

View File

@@ -1,10 +1,10 @@
/**
* The model-facing `write` tool: create or fully replace a UTF-8 text file. The
* tool is the executor: it dispatches the `fs/write-expectation` waterfall to
* tool is the executor: it dispatches the `fs/write-intent` waterfall to
* obtain the optional version guard, calls `ctx.fs.writeText` directly, and
* emits `fs/observed`. The default thunk returns `undefined` (unconditional
* create-or-overwrite — the bare provider); a policy plugin
* (`@deepseek-ai/dsh-file-context`) occupies the single decision slot and
* (`@deepseek-ai/dsh-fs-policy`) occupies the single decision slot and
* returns `createIfAbsent`/`replaceIfVersion` instead. The tool stats ZERO
* times either way.
*
@@ -39,7 +39,7 @@ export function applyWriteTool(ctx: Context): void {
ctx.systemPrompt.section({
name: 'tool:write',
order: 101,
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default file-context policy requires it) and prefer edit for targeted changes.',
text: 'Use the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.',
})
ctx.tools.register(defineTool({
@@ -54,8 +54,8 @@ export function applyWriteTool(ctx: Context): void {
const target = await ctx.fs.resolve(input.filePath)
// Single-slot decision: the policy plugin produces createIfAbsent/
// replaceIfVersion; the bare default is undefined (unconditional). No stat.
const expectation = await ctx.waterfall('fs/write-expectation', target, exec, () => undefined)
const outcome = await ctx.fs.writeText(target, input.content, expectation, exec.signal)
const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)
const outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal)
// Record the observed version (a no-op when no policy plugin listens).
ctx.emit('fs/observed', target, outcome.version, exec)
return [{ type: 'text', text: formatWriteOutput(target.displayPath, outcome) }]