docs: the alert shows size and age, not the old line count

The code never reads a file it is about to replace just to count its lines: stat
gives the size and the age, and the new content's line count is known without
reading anything. The spec overpromised the old count.
This commit is contained in:
2026-10-07 17:03:29 +08:00
parent adfd78e4eb
commit 01352ab3da
2 changed files with 7 additions and 5 deletions
@@ -1134,7 +1134,7 @@ describe("the mutating operations", () => {
allow, allow,
); );
expect(asked[0].operation).toBe("overwrite"); expect(asked[0].operation).toBe("overwrite");
expect(asked[0].details.join(" ")).toContain("Replaces 14 bytes, 4 lines"); expect(asked[0].details.join(" ")).toContain("Replaces 14 bytes");
}); });
it("creates missing folders, and says so before it does", async () => { it("creates missing folders, and says so before it does", async () => {
@@ -115,8 +115,10 @@ ones that follow from them are recorded here.
a move that silently destroys the file it lands on. The model can delete the a move that silently destroys the file it lands on. The model can delete the
destination first, which asks again under A2. destination first, which asks again under A2.
- **A6. `write_file` overwrites without a separate flag, and discloses which it is.** - **A6. `write_file` overwrites without a separate flag, and discloses which it is.**
The approval alert says whether the file exists, how big it is, and how many lines The approval alert says whether the file exists, how big it is and when it last
it has, so an overwrite is never a surprise. A `mustNotExist` flag would add a changed, so an overwrite is never a surprise. The old line count is deliberately not
among them: counting it would mean reading a file Lam is about to replace, and stat
gives the size and the age for free. A `mustNotExist` flag would add a
round trip and a second approval to the common case to guard against something the round trip and a second approval to the common case to guard against something the
alert already shows. alert already shows.
- **A7. Relative paths resolve against `$HOME`, not the working directory.** A launcher - **A7. Relative paths resolve against `$HOME`, not the working directory.** A launcher
@@ -221,7 +223,7 @@ ask rather than retry in the second.
| `list_directory` | `path`, `hidden?` | Directories first, then files, each sorted. Capped at `MAX_LIST_ENTRIES` (200), with a count of what was left out. | | `list_directory` | `path`, `hidden?` | Directories first, then files, each sorted. Capped at `MAX_LIST_ENTRIES` (200), with a count of what was left out. |
| `search_files` | `path`, `pattern`, `contains?` | `pattern` is a glob (`*`, `?`, `**`) matched against the basename; `contains` is a case-insensitive substring test on the file's first `MAX_SEARCH_BYTES` (64 KB). Capped at `MAX_SEARCH_RESULTS` (50) and `MAX_SEARCH_DEPTH` (8). A path that fails the read check is skipped, contents and all, so the denylist holds for search results as well as for direct reads. | | `search_files` | `path`, `pattern`, `contains?` | `pattern` is a glob (`*`, `?`, `**`) matched against the basename; `contains` is a case-insensitive substring test on the file's first `MAX_SEARCH_BYTES` (64 KB). Capped at `MAX_SEARCH_RESULTS` (50) and `MAX_SEARCH_DEPTH` (8). A path that fails the read check is skipped, contents and all, so the denylist holds for search results as well as for direct reads. |
| `read_file` | `path`, `offset?`, `limit?` | Numbered lines, stopping at whichever comes first of the default 200 lines and `MAX_READ_CHARS` (8000) characters, with the notice naming that cap so it is clear which one stopped it. Binary content (a NUL byte in the first 8 KB) is refused with its size and detected type rather than returned as mojibake. | | `read_file` | `path`, `offset?`, `limit?` | Numbered lines, stopping at whichever comes first of the default 200 lines and `MAX_READ_CHARS` (8000) characters, with the notice naming that cap so it is clear which one stopped it. Binary content (a NUL byte in the first 8 KB) is refused with its size and detected type rather than returned as mojibake. |
| `write_file` | `path`, `content` | Creates the parent directory chain when it is missing, disclosed in the alert, since `~/Downloads/reports/may.md` is a normal thing to ask for and there is no separate mkdir tool. Reports `Created` or `Overwrote (was 1,240 bytes, 40 lines)`. | | `write_file` | `path`, `content` | Creates the parent directory chain when it is missing, disclosed in the alert, since `~/Downloads/reports/may.md` is a normal thing to ask for and there is no separate mkdir tool. Reports `Created` or `Overwrote (...)` with the size and line count of what it wrote. |
| `edit_file` | `path`, `old_string`, `new_string`, `all?` | Exact substitution. Refuses when `old_string` occurs zero times or more than once without `all`, naming the count so the model can add context instead of guessing. | | `edit_file` | `path`, `old_string`, `new_string`, `all?` | Exact substitution. Refuses when `old_string` occurs zero times or more than once without `all`, naming the count so the model can add context instead of guessing. |
| `delete_path` | `path` | One file, or an empty directory. A non-empty directory is refused with its entry count (A4). | | `delete_path` | `path` | One file, or an empty directory. A non-empty directory is refused with its entry count (A4). |
| `move_path` | `from`, `to` | Both the source and the destination are policy checked and both are approved. Refuses an existing destination (A5). | | `move_path` | `from`, `to` | Both the source and the destination are policy checked and both are approved. Refuses an existing destination (A5). |
@@ -256,7 +258,7 @@ left to the implementation:
``` ```
Title: Overwrite report.md? Title: Overwrite report.md?
Body: /Users/me/Downloads/report.md Body: /Users/me/Downloads/report.md
Replaces 1,240 bytes, 40 lines, last changed 3 days ago. Replaces 1,240 bytes, last changed 3 days ago.
New content is 2,180 bytes, 61 lines. New content is 2,180 bytes, 61 lines.
[Allow] [Decline] [Allow] [Decline]
``` ```