diff --git a/plugin/hooks/grimoire_after_write.sh b/plugin/hooks/grimoire_after_write.sh index ff043a4e..3da6f372 100644 --- a/plugin/hooks/grimoire_after_write.sh +++ b/plugin/hooks/grimoire_after_write.sh @@ -119,10 +119,11 @@ combined="" n_files=0 while IFS= read -r rel_path; do [ -n "${rel_path:-}" ] || continue - # A Bash call that rewrote many files is a refactor or a generator, not a - # shape being instantiated; four is enough to name what matters. + # Every changed file goes into the end-of-turn ledger, up to forty — a bound + # 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" -le 4 ] || break + [ "$n_files" -le 40 ] || break file_path="$repo_root/$rel_path" # 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" fi [ -n "$code" ] || continue - # `|| true` inside: an early-exiting `head` must not void its own output. - names=$(printf '%s' "$code" | grimoire_defs | sort -u | head -12 || true) - # What this write defined, for the end-of-turn question — + all_names=$(printf '%s' "$code" | grimoire_defs | sort -u || true) + # What this write defined, for the end-of-turn question: every definition, # 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 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_context="" diff --git a/plugin/hooks/grimoire_prior_art.sh b/plugin/hooks/grimoire_prior_art.sh index 979e91cc..da8c9f79 100755 --- a/plugin/hooks/grimoire_prior_art.sh +++ b/plugin/hooks/grimoire_prior_art.sh @@ -79,10 +79,16 @@ fi # 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.) +# +# `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="" 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. - names=$(printf '%s' "$code" | grimoire_defs | sort -u | head -12 || true) + names=$(printf '%s\n' "$all_names" | head -12 || true) fi 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 # 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 - # 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="" [ -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" syncfile="$state_dir/${safe_sid}.sync.ids" derivefile="$state_dir/${safe_sid}.derive.ids" diff --git a/tests/test_after_write_hook.py b/tests/test_after_write_hook.py index 67bafc7e..7d048a01 100644 --- a/tests/test_after_write_hook.py +++ b/tests/test_after_write_hook.py @@ -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 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): env = _env(tmp_path) repo = _repo(tmp_path, env) diff --git a/tests/test_shape_check_hook.py b/tests/test_shape_check_hook.py index 9f6971c3..de1084d4 100644 --- a/tests/test_shape_check_hook.py +++ b/tests/test_shape_check_hook.py @@ -103,6 +103,19 @@ def test_an_edit_to_an_existing_file_adds_no_file_line(tmp_path): 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): with http_sink(b'{"note_ids":[]}') as (port, _seen): env = _env(tmp_path, f"http://127.0.0.1:{port}")