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
@@ -3,7 +3,7 @@
* file/streamed text reads, atomic guarded writes (createIfAbsent /
* replaceIfVersion), version-guarded literal edits, concurrency races, symlink
* identity, and HMR/disposal. Read WINDOWING is policy and lives in
* `dsh-file-context`, so it is not exercised here.
* `dsh-fs-policy`, so it is not exercised here.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+17 -2
View File
@@ -2,7 +2,7 @@
* Cordis-free tests for the raw local-filesystem I/O: path resolution, probe,
* whole-file/streamed text reads, binary/UTF-8 rejection, atomic-write temp
* safety, literal edit matching, and line-ending handling. Line WINDOWING is
* policy and lives in `dsh-file-context`, so it is not tested here.
* policy and lives in `dsh-fs-policy`, so it is not tested here.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
@@ -21,7 +21,7 @@ import {
writeFileAtomic,
} from '@deepseek-ai/dsh-fs-local'
import type { LocalTarget } from '@deepseek-ai/dsh-fs-local'
import { FsTargetKey } from '@deepseek-ai/dsh-fs'
import { FsError, FsTargetKey } from '@deepseek-ai/dsh-fs'
let dir: string
beforeEach(async () => {
@@ -88,6 +88,16 @@ describe('resolveLocalTarget', () => {
it('rejects a blank path', async () => {
await expect(resolveLocalTarget(dir, ' ')).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
})
it('rejects a path whose ancestor is a file with a structured FsError (ENOTDIR)', async () => {
// "afile" is a regular file, so "afile/child.txt" hits ENOTDIR on realpath;
// the raw Node error must be translated into the FsError taxonomy so the tool
// result keeps its { name, code } metadata.
await writeFile(join(dir, 'afile'), 'i am a file')
const err = await resolveLocalTarget(dir, 'afile/child.txt').then(() => undefined, (e: unknown) => e)
expect(err).toBeInstanceOf(FsError)
expect(err).toMatchObject({ code: 'FS_NOT_FOUND' })
})
})
describe('probe', () => {
@@ -128,6 +138,11 @@ describe('probe', () => {
await new Promise<void>((resolve) => { server.close(() => { resolve() }) })
}
})
it('returns null when an ancestor path segment is a file (ENOTDIR), not a raw throw', async () => {
await writeFile(join(dir, 'afile'), 'i am a file')
expect(await probe(join(dir, 'afile', 'child.txt'))).toBeNull()
})
})
describe('readWholeText', () => {