refactor(fs): prune write-only fields and the dead routing knob from the seam

The fs seam split left four pieces of pre-split surface populated on
every call and read by nobody:

- STREAM_MIN_SIZE + FsIoInternals.streamMinSize in dsh-fs-local: the
  backend has no read routing (readWholeText/streamWholeText are
  separate primitives the caller picks), and the real 10 MiB routing
  constant lives in dsh-tool-fs's read tool. Delete the dead mirror and
  the knob whose JSDoc claimed an override that did not exist; the
  remaining FsIoInternals knobs stay (the atomic-write tests use them).
- FsTarget.inputPath: a "diagnostics only" field every backend and test
  fake had to fabricate, with zero production readers (policy and error
  messages use targetKey/displayPath). listDir gave children the bare
  entry name, which was nobody's input.
- FsEditOutcome.replacements/.replaceAll: replacements had no reader
  (the single-match policy is enforced by the FS_AMBIGUOUS_EDIT /
  FS_EDIT_NOT_FOUND throws, whose message keeps the internal count);
  replaceAll only echoed the replace_all argument back to
  formatEditOutput, which now takes it from the parsed args. The
  outcome shrinks to { version, before, after }, parallel to
  FsWriteOutcome's backend-discovered fields. Emitted text is unchanged
  for both branches (no snapshot churn).
- FileReadOutcome.limit/.version: formatReadOutput renders
  offset/lines/totalLines/truncatedByBytes only, and the fs/observed
  emit uses info.version directly.

Backends shed four fabrication obligations and gain none. Doc pastes
(core-data-structures/filesystem.md), the dsh-fs README resolve row,
and the test fakes shrink with the types. RFC moved to
implemented/simplification and amended to the shipped shape
(FsEditSpec -> FsEditRequest name fix; manifest rows needed no change).
This commit is contained in:
Tianyi Cui
2026-07-04 15:37:43 +08:00
parent 226a8b5e4c
commit e64623ebfd
14 changed files with 34 additions and 67 deletions

View File

@@ -19,7 +19,7 @@ A backend subclasses `FileSystem` and implements seven primitives.
| Member | Semantics |
|---|---|
| `resolve(path, opts?)` | Resolve a path into a stable `FsTarget` (`inputPath`, opaque `targetKey`, `displayPath`). `opts.cwd` is the base a relative `path` resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same `targetKey`. |
| `resolve(path, opts?)` | Resolve a path into a stable `FsTarget` (opaque `targetKey`, `displayPath`). `opts.cwd` is the base a relative `path` resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same `targetKey`. |
| `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). |

View File

@@ -52,8 +52,6 @@ export function FsVersion(v: string): FsVersion {
* this; every other operation takes it.
*/
export interface FsTarget {
/** The original model/plugin-supplied path, for diagnostics only. */
inputPath: string
/** Opaque key for stale guards and target lookup. */
targetKey: FsTargetKey
/**
@@ -142,10 +140,6 @@ export interface FsEditRequest {
/** Outcome of a literal edit. */
export interface FsEditOutcome {
/** Number of literal replacements applied. */
replacements: number
/** Whether every match was replaced. */
replaceAll: boolean
/** Opaque version of the file after the edit. */
version: FsVersion
/**

View File

@@ -23,7 +23,7 @@ class FakeFileSystem extends FileSystem {
files = new Map<string, string>()
override async resolve(path: string): Promise<FsTarget> {
return { inputPath: path, targetKey: FsTargetKey(path), displayPath: path }
return { targetKey: FsTargetKey(path), displayPath: path }
}
override async stat(target: FsTarget): Promise<FsInfo | undefined> {
const content = this.files.get(target.targetKey)
@@ -45,7 +45,7 @@ class FakeFileSystem extends FileSystem {
{
name: 'alpha.md',
type: 'file',
target: { inputPath: 'skills/alpha.md', targetKey: FsTargetKey('skills/alpha.md'), displayPath: 'skills/alpha.md' },
target: { targetKey: FsTargetKey('skills/alpha.md'), displayPath: 'skills/alpha.md' },
size: 2,
version: FsVersion('v1'),
},
@@ -60,7 +60,7 @@ class FakeFileSystem extends FileSystem {
const content = this.files.get(target.targetKey) ?? ''
const after = content.split(edit.oldString).join(edit.newString)
this.files.set(target.targetKey, after)
return { replacements: 1, replaceAll: edit.replaceAll, version: FsVersion('v3'), before: content, after }
return { version: FsVersion('v3'), before: content, after }
}
}
@@ -108,7 +108,7 @@ describe('FileSystem provider seam', () => {
expect(entries).toEqual([{
name: 'alpha.md',
type: 'file',
target: { inputPath: 'skills/alpha.md', targetKey: 'skills/alpha.md', displayPath: 'skills/alpha.md' },
target: { targetKey: 'skills/alpha.md', displayPath: 'skills/alpha.md' },
size: 2,
version: 'v1',
}])