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).
4.1 KiB
RFC: Prune write-only fields and a dead routing knob from the fs seam
Status: implemented (proposed and accepted 2026-07-04)
Problem
The fs seam split moved read routing and policy out of the backend into dsh-tool-fs and dsh-fs-policy. Four pieces of surface kept the pre-split shape — populated on every call, read by nobody:
STREAM_MIN_SIZE+FsIoInternals.streamMinSizeindsh-fs-local(packages/fs/fs-local/src/fsio.ts, re-exported frompackages/fs/fs-local/src/index.ts): zero readers anywhere, including fs-local's own source and tests. The backend has no read routing —readWholeText/streamWholeTextare separate primitives the caller chooses between — and the real routing constant lives in the consumer (packages/fs/tool-fs/src/read.ts, compared againstinfo.size). Two mirrors of the 10 MiB fact; the backend's was dead, and the knob's JSDoc claimed a "read routing" override that did not exist.FsTarget.inputPath(packages/fs/fs/src/types.ts): every backend and every test fake had to fabricate a "diagnostics only" value with zero production readers — the policy plugin and every error message usetargetKey/displayPath. ThelistDirproducer exposed the semantic wobble: directory children got the bare entry name, which was nobody's "input".FsEditOutcome.replacements+.replaceAll(packages/fs/fs/src/types.ts):replacementshad zero production readers (the single-match policy itself stays — it is enforced by theFS_AMBIGUOUS_EDIT/FS_EDIT_NOT_FOUNDthrows inside the backend, whose error message keeps the internal count);replaceAllwas read only byformatEditOutputinpackages/fs/tool-fs/src/edit.ts— as an echo of thereplace_allargument the tool already holds. Shrunk,FsEditOutcomeis{ version, before, after }, parallel toFsWriteOutcome's genuinely backend-discovered fields.FileReadOutcome.limit+.version(packages/fs/tool-fs/src/read-render.ts): populated by the read tool, butformatReadOutputrendersoffset/lines/totalLines/truncatedByBytesonly, and thefs/observedemit usesinfo.versiondirectly rather than an outcome copy.
Decision
Delete the fs-local constant, its re-export, and the streamMinSize knob (the remaining FsIoInternals knobs are genuinely used by the atomic-write tests); drop inputPath from FsTarget; shrink FsEditOutcome to { version, before, after } and pass replaceAll to formatEditOutput from the parsed args; drop limit/version from FileReadOutcome. The filesystem.md pastes, packages/fs/fs/README.md, and the test fakes that had to fabricate the removed fields shrink with the types.
Why not keep them?
A future permission/containment layer might want the pre-resolution path for error text — but it would want the request, which every call site still holds. "N occurrences replaced" might become model-facing text — a behavior change to design when wanted, and the backend-internal count survives for its error message. A read footer might display limit — everything the footer shows already derives from lines/totalLines. Meanwhile every current and future backend (remote, native) would have to fabricate wire fields nobody consumes, and every test fake would have to satisfy them.
Acceptance criteria
- The removed surfaces are gone —
STREAM_MIN_SIZE/streamMinSizeindsh-fs-local,FsTarget.inputPath,FsEditOutcome.replacements/.replaceAll, andFileReadOutcome.limit/.version— while the request-sidereplaceAll(FsEditRequest) and the version fields on the other outcome types are untouched; doc pastes and the manifest in sync; the suite is green with the shrunk fakes. formatEditOutput's emitted text is unchanged for bothreplace_allbranches, so no snapshot golden churns.
Risks
The in-flight fs discovery work (glob/grep tools) touches the same dsh-fs type files — a textual, not design, conflict; land in either order and reconcile mechanically. Backends gain no new obligations; they shed four.