fix(6471): the end-of-turn shape check hears every definition a write made
The write hooks recorded at most 12 definitions per write (alphabetical, so test functions were the ones dropped) and the post-Bash hook stopped after 4 files, so the Stop hook never asked about the rest. The caps bound the prior-art query; the written ledger now gets the full list, up to 40 files per Bash call, while the query stays at 4 files x 12 names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -119,10 +119,11 @@ combined=""
|
|||||||
n_files=0
|
n_files=0
|
||||||
while IFS= read -r rel_path; do
|
while IFS= read -r rel_path; do
|
||||||
[ -n "${rel_path:-}" ] || continue
|
[ -n "${rel_path:-}" ] || continue
|
||||||
# A Bash call that rewrote many files is a refactor or a generator, not a
|
# Every changed file goes into the end-of-turn ledger, up to forty — a bound
|
||||||
# shape being instantiated; four is enough to name what matters.
|
# on this hook's run time, past which the call was a generator. The arms
|
||||||
|
# below speak for the first four only (see the check after the ledger).
|
||||||
n_files=$((n_files + 1))
|
n_files=$((n_files + 1))
|
||||||
[ "$n_files" -le 4 ] || break
|
[ "$n_files" -le 40 ] || break
|
||||||
file_path="$repo_root/$rel_path"
|
file_path="$repo_root/$rel_path"
|
||||||
|
|
||||||
# The code just written: the ADDED lines of the uncommitted diff for a
|
# The code just written: the ADDED lines of the uncommitted diff for a
|
||||||
@@ -136,14 +137,19 @@ while IFS= read -r rel_path; do
|
|||||||
fresh="new"
|
fresh="new"
|
||||||
fi
|
fi
|
||||||
[ -n "$code" ] || continue
|
[ -n "$code" ] || continue
|
||||||
# `|| true` inside: an early-exiting `head` must not void its own output.
|
all_names=$(printf '%s' "$code" | grimoire_defs | sort -u || true)
|
||||||
names=$(printf '%s' "$code" | grimoire_defs | sort -u | head -12 || true)
|
# What this write defined, for the end-of-turn question: every definition,
|
||||||
# What this write defined, for the end-of-turn question —
|
|
||||||
# recorded before the arms below, which skip a write that defines nothing.
|
# recorded before the arms below, which skip a write that defines nothing.
|
||||||
printf '%s\n' "$names" | grimoire_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
printf '%s\n' "$all_names" | grimoire_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
||||||
# Nothing DEFINED in what was written (prose, data, a call-site edit) →
|
# Nothing DEFINED in what was written (prose, data, a call-site edit) →
|
||||||
# nothing to say; the arms are about shapes.
|
# nothing to say; the arms are about shapes.
|
||||||
[ -n "$names" ] || continue
|
[ -n "$all_names" ] || continue
|
||||||
|
# A Bash call that rewrote many files is a refactor or a generator, not a
|
||||||
|
# shape being instantiated; four files and twelve names per file are enough
|
||||||
|
# for the arms to name what matters, and bound their git grep and URL.
|
||||||
|
[ "$n_files" -le 4 ] || continue
|
||||||
|
# `|| true` inside: an early-exiting `head` must not void its own output.
|
||||||
|
names=$(printf '%s\n' "$all_names" | head -12 || true)
|
||||||
|
|
||||||
local_lines=$(grimoire_local_dups "$repo_root" "$rel_path" <<< "$names") || local_lines=""
|
local_lines=$(grimoire_local_dups "$repo_root" "$rel_path" <<< "$names") || local_lines=""
|
||||||
local_context=""
|
local_context=""
|
||||||
|
|||||||
@@ -79,10 +79,16 @@ fi
|
|||||||
|
|
||||||
# ARM 1 — BY NAME, LOCALLY: does a definition of this already exist
|
# ARM 1 — BY NAME, LOCALLY: does a definition of this already exist
|
||||||
# in the repo? (grimoire_local_dups in grimoire_defs.sh carries the why.)
|
# in the repo? (grimoire_local_dups in grimoire_defs.sh carries the why.)
|
||||||
|
#
|
||||||
|
# `all_names` is every definition the write carries; it feeds the end-of-turn
|
||||||
|
# ledger, which must hear about all of them. `names` is the first twelve, and
|
||||||
|
# bounds the queries below — a git grep and a URL per name.
|
||||||
|
all_names=""
|
||||||
names=""
|
names=""
|
||||||
if [ -n "$code" ]; then
|
if [ -n "$code" ]; then
|
||||||
|
all_names=$(printf '%s' "$code" | grimoire_defs | sort -u || true)
|
||||||
# `|| true` inside: an early-exiting `head` must not void its own output.
|
# `|| true` inside: an early-exiting `head` must not void its own output.
|
||||||
names=$(printf '%s' "$code" | grimoire_defs | sort -u | head -12 || true)
|
names=$(printf '%s\n' "$all_names" | head -12 || true)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local_lines=""
|
local_lines=""
|
||||||
@@ -204,10 +210,11 @@ if [ -n "$session_id" ]; then
|
|||||||
# Write onto a path that does not exist yet creates a file, which may be a
|
# Write onto a path that does not exist yet creates a file, which may be a
|
||||||
# unit in its own right (see grimoire_written_append). Written before any
|
# unit in its own right (see grimoire_written_append). Written before any
|
||||||
# server call: the ask at the end of the turn must not depend on this
|
# server call: the ask at the end of the turn must not depend on this
|
||||||
# write's hint having been answered.
|
# write's hint having been answered. Every definition, not the capped list
|
||||||
|
# the queries use; an Edit that names none records its enclosing one.
|
||||||
fresh=""
|
fresh=""
|
||||||
[ -e "$file_path" ] || fresh="new"
|
[ -e "$file_path" ] || fresh="new"
|
||||||
printf '%s\n' "$shapes" | grimoire_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
printf '%s\n' "${all_names:-$shapes}" | grimoire_written_append "$safe_sid" "$rel_path" "$fresh" "$code"
|
||||||
idfile="$state_dir/${safe_sid}.ids"
|
idfile="$state_dir/${safe_sid}.ids"
|
||||||
syncfile="$state_dir/${safe_sid}.sync.ids"
|
syncfile="$state_dir/${safe_sid}.sync.ids"
|
||||||
derivefile="$state_dir/${safe_sid}.derive.ids"
|
derivefile="$state_dir/${safe_sid}.derive.ids"
|
||||||
|
|||||||
@@ -90,6 +90,23 @@ def test_after_write_names_what_bash_just_wrote_then_stays_quiet_until_the_next_
|
|||||||
assert seen[0]["exclude_derive"] == ["dup:483a"]
|
assert seen[0]["exclude_derive"] == ["dup:483a"]
|
||||||
assert set(seen[0]["shapes"][0].split(",")) == {"css:log-empty", "css:other"}
|
assert set(seen[0]["shapes"][0].split(",")) == {"css:log-empty", "css:other"}
|
||||||
|
|
||||||
|
def test_after_write_records_every_definition_for_the_turn_but_queries_only_the_first_four_files(tmp_path):
|
||||||
|
"""The end-of-turn ledger hears every file and every definition a Bash call
|
||||||
|
wrote; the prior-art arms stay bounded to four files and twelve names."""
|
||||||
|
with http_sink(SINK_REPLY) as (port, seen):
|
||||||
|
env = _env(tmp_path, url=f"http://127.0.0.1:{port}")
|
||||||
|
repo = _repo(tmp_path, env)
|
||||||
|
for n in range(6):
|
||||||
|
body = "".join(f"def f{n}_{i:02d}():\n return {i}\n\n" for i in range(15))
|
||||||
|
(repo / f"gen{n}.py").write_text(body)
|
||||||
|
_run(repo, env, session="s-many")
|
||||||
|
ledger = (tmp_path / "grimoire-priorart" / "s-many.written.ids").read_text().splitlines()
|
||||||
|
assert len(ledger) == 6 * 15
|
||||||
|
assert "gen5.py\tsym\tf5_14" in ledger
|
||||||
|
assert len(seen) == 4
|
||||||
|
assert all(len(q["shapes"][0].split(",")) == 12 for q in seen)
|
||||||
|
|
||||||
|
|
||||||
def test_after_write_is_silent_where_it_has_nothing_to_say(tmp_path):
|
def test_after_write_is_silent_where_it_has_nothing_to_say(tmp_path):
|
||||||
env = _env(tmp_path)
|
env = _env(tmp_path)
|
||||||
repo = _repo(tmp_path, env)
|
repo = _repo(tmp_path, env)
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ def test_an_edit_to_an_existing_file_adds_no_file_line(tmp_path):
|
|||||||
assert lines == ["lib.py\tsym\thelper"]
|
assert lines == ["lib.py\tsym\thelper"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_write_with_many_definitions_notes_every_one(tmp_path):
|
||||||
|
"""The ledger is not bounded by the twelve names the prior-art query sends:
|
||||||
|
a new test file with twenty functions is asked about in full."""
|
||||||
|
with http_sink(b'{"note_ids":[]}') as (port, _seen):
|
||||||
|
env = _env(tmp_path, f"http://127.0.0.1:{port}")
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
body = "".join(f"def test_case_{i:02d}():\n assert {i}\n\n" for i in range(20))
|
||||||
|
_pre_write(env, repo, "test_many.py", body)
|
||||||
|
lines = _ledger(tmp_path).read_text().splitlines()
|
||||||
|
assert len(lines) == 20
|
||||||
|
assert "test_many.py\tsym\ttest_case_19" in lines
|
||||||
|
|
||||||
|
|
||||||
def test_a_skipped_path_notes_nothing(tmp_path):
|
def test_a_skipped_path_notes_nothing(tmp_path):
|
||||||
with http_sink(b'{"note_ids":[]}') as (port, _seen):
|
with http_sink(b'{"note_ids":[]}') as (port, _seen):
|
||||||
env = _env(tmp_path, f"http://127.0.0.1:{port}")
|
env = _env(tmp_path, f"http://127.0.0.1:{port}")
|
||||||
|
|||||||
Reference in New Issue
Block a user