mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(story-mode): a file a python or node script rewrites counts as changed
From review round 5: Claude rewrote textstats.py, the change the user asked for, through `python3 - <<EOF ... open(p, "w").write(s)`, and the page listed only the two files the same command wrote with `cat >`. A script fed to python or node by a here-document is now read for the files it opens for writing (`open(target, 'w'|'a'|...)`, `Path(...) .write_text`, `writeFileSync`), with the target a string literal or a name bound to one (`p = "textstats.py"`); anything computed is left out, never guessed, and scratch targets are skipped. They are edits without lines, like the files Claude Code says a command modified, since what the script changed is not in the command. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b220d6d6d7
commit
c36700a9c0
2 files changed
+84
-4
No files matched your search
+50
-2
@@ -566,8 +566,9 @@ function judgeBashCommand(command: string): { intent: StoryIntent; segment: stri
|
||||
* Files a command writes through here-documents: `cat > f <<'EOF'`, `cat <<EOF > f`, `tee f <<EOF`
|
||||
* (and `>>` / `tee -a`, appends). Each becomes a whole-file edit of the body's lines. A relative
|
||||
* target resolves against the transcript line's `cwd` unless the command changed directory first;
|
||||
* scratch targets (`/tmp/x`, `"$S/x"`) are not project files. A body piped into a program
|
||||
* (`python3 - <<EOF`) writes nothing.
|
||||
* scratch targets (`/tmp/x`, `"$S/x"`) are not project files. A script fed to python or node
|
||||
* (`python3 - <<EOF`) is read for the files it opens for writing; those are edits without lines,
|
||||
* since what the script changed is not in the command.
|
||||
*/
|
||||
export function heredocWrites(command: string, cwd?: string): StoryEdit[] {
|
||||
const out: StoryEdit[] = [...echoWrites(command, cwd)];
|
||||
@@ -600,6 +601,18 @@ export function heredocWrites(command: string, cwd?: string): StoryEdit[] {
|
||||
append = !!tee[1];
|
||||
}
|
||||
}
|
||||
if (!target && /\b(python3?|node)\b/.test(before)) {
|
||||
for (const written of scriptWrites(body)) {
|
||||
if (SCRATCH_TARGET.test(written) || out.some((d) => d.file.endsWith(written))) continue;
|
||||
const file =
|
||||
written.startsWith('/') || !cwd || moved
|
||||
? written
|
||||
: `${cwd.replace(/\/+$/, '')}/${written.replace(/^\.\//, '')}`;
|
||||
out.push({ file, added: 0, removed: 0, created: false, hunks: [], truncated: true });
|
||||
if (out.length >= 6) break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!target || SCRATCH_TARGET.test(target)) continue;
|
||||
const file =
|
||||
target.startsWith('/') || !cwd || moved ? target : `${cwd.replace(/\/+$/, '')}/${target.replace(/^\.\//, '')}`;
|
||||
@@ -622,6 +635,41 @@ export function heredocWrites(command: string, cwd?: string): StoryEdit[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files a python or node script opens for writing: `open('a.py', 'w')`, `open(p, "w")` after
|
||||
* `p = "a.py"`, `Path('a.py').write_text(...)`, `fs.writeFileSync('a.js', ...)`. Only string literals
|
||||
* and names bound to one; anything computed is left out rather than guessed.
|
||||
*/
|
||||
function scriptWrites(body: string[]): string[] {
|
||||
const names = new Map<string, string>();
|
||||
for (const line of body) {
|
||||
const m = /^\s*(?:(?:const|let|var)\s+)?([A-Za-z_]\w*)\s*=\s*(?:Path\(\s*)?(['"])([^'"\n]+)\2\s*\)?\s*;?\s*$/.exec(
|
||||
line
|
||||
);
|
||||
if (m) names.set(m[1], m[3]);
|
||||
}
|
||||
const target = (lit: string | undefined, name: string | undefined) => lit ?? (name ? names.get(name) : undefined);
|
||||
const found: Array<{ at: number; file: string }> = [];
|
||||
const text = body.join('\n');
|
||||
const patterns = [
|
||||
// open(target, 'w') / open(target, mode="a+")
|
||||
/\bopen\(\s*(?:(['"])([^'"\n]+)\1|([A-Za-z_]\w*))\s*,\s*(?:mode\s*=\s*)?(['"])([rwaxbt+]*)\4/g,
|
||||
// Path(target).write_text( / name.write_text(
|
||||
/(?:\bPath\(\s*(?:(['"])([^'"\n]+)\1|([A-Za-z_]\w*))\s*\)|\b([A-Za-z_]\w*))\.write_(?:text|bytes)\(/g,
|
||||
// fs.writeFileSync(target / appendFileSync(target
|
||||
/\b(?:write|append)File(?:Sync)?\(\s*(?:(['"])([^'"\n]+)\1|([A-Za-z_]\w*))/g,
|
||||
];
|
||||
for (const [k, re] of patterns.entries()) {
|
||||
for (const m of text.matchAll(re)) {
|
||||
if (k === 0 && !/[wax+]/.test(m[5])) continue;
|
||||
const file = k === 1 ? target(m[2], m[3] ?? m[4]) : target(m[2], m[3]);
|
||||
if (file && !/[*?$`{}]/.test(file)) found.push({ at: m.index ?? 0, file });
|
||||
}
|
||||
}
|
||||
// In the order the script writes them.
|
||||
return unique(found.sort((x, y) => x.at - y.at).map((f) => f.file)).slice(0, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a linter, formatter or type checker reported problems (`Found 3 errors.`, `1 file would be
|
||||
* reformatted`, `✖ 5 problems`, `error TS2322:`). Read when a trailing command (`; ls`) hid the exit code.
|
||||
|
||||
+34
-2
@@ -939,10 +939,42 @@ describe('buildStoryPage', () => {
|
||||
),
|
||||
end(1000),
|
||||
];
|
||||
(lines[1] as { cwd?: string }).cwd = '/w';
|
||||
(lines[2] as { cwd?: string }).cwd = '/w';
|
||||
const page = pageOf(lines);
|
||||
expect(page.events[1].edits?.map((d) => d.file)).toEqual(['/w/test_a.py']);
|
||||
expect(page.filesChanged).toBe(1);
|
||||
// The script's own write and the file Claude Code says changed: both, neither with lines.
|
||||
expect(page.events[1].edits?.map((d) => [d.file, d.hunks.length])).toEqual([
|
||||
['/w/t.py', 0],
|
||||
['/w/test_a.py', 0],
|
||||
]);
|
||||
expect(page.filesChanged).toBe(2);
|
||||
});
|
||||
|
||||
it('a python or node script fed by a here-document: the files it opens for writing, never guessed', () => {
|
||||
const files = (command: string) => heredocWrites(command, '/w').map((d) => d.file);
|
||||
expect(
|
||||
files(
|
||||
[
|
||||
"python3 - <<'EOF'",
|
||||
'p = "textstats.py"',
|
||||
's = open(p).read()',
|
||||
"s = s.replace('a', 'b')",
|
||||
'open(p, "w").write(s)',
|
||||
"Path('README.md').write_text('# x')",
|
||||
"with open('notes.txt', mode='a') as f: f.write('x')",
|
||||
"open('/tmp/scratch.txt', 'w').write('x')",
|
||||
"open(os.path.join(d, 'x.py'), 'w')",
|
||||
"open('data.json').read()",
|
||||
'EOF',
|
||||
].join('\n')
|
||||
)
|
||||
).toEqual(['/w/textstats.py', '/w/README.md', '/w/notes.txt']);
|
||||
expect(files("node <<'EOF'\nconst out = 'dist/app.js';\nfs.writeFileSync(out, code);\nEOF")).toEqual([
|
||||
'/w/dist/app.js',
|
||||
]);
|
||||
expect(files("python3 - <<'EOF'\nprint(open('a.py').read())\nEOF")).toEqual([]);
|
||||
const [edit] = heredocWrites("python3 - <<'EOF'\nopen('a.py', 'w').write('x')\nEOF", '/w');
|
||||
expect(edit).toMatchObject({ file: '/w/a.py', added: 0, removed: 0, hunks: [], truncated: true });
|
||||
});
|
||||
|
||||
it('a compaction says what it did; ToolSearch is looking; EnterPlanMode says plan mode', () => {
|
||||
|
||||
Reference in new issue
Block a user