mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
Leak bounce exempts only names kuhy typed; hide property meanings that name an entry
The fetched statement carries LeetCode's topic tags and site hints ("Hash
Table", "Two Pointers"), so exempting it pre-cleared the answer. Some real
toolbox meanings illustrate with the operation (self-inverse: "a ^ x ^ x"),
so the incomplete-list note shows those properties by name only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjvF5DZG9JduSBYKxqyysi
This commit is contained in:
parent
ebf7ca624e
commit
6343499ed1
@ -31,7 +31,8 @@ Both REPL paths run `phase.PhaseGate` on every block kuhy sends:
|
||||
event carries `req_incomplete: true`).
|
||||
- Until the first `code` event, a reply naming a toolbox entry or alias is
|
||||
bounced (`leak.py`: the Stop hook on the Claude path, `local_coach` on the
|
||||
local one), except names kuhy or the fetched statement already used.
|
||||
local one), except names kuhy already typed (not the statement: its tags
|
||||
and hints name the answer).
|
||||
- Each reply's `<!-- rung:Hn -->` marker is logged and stripped.
|
||||
|
||||
Every step lands in `~/.local/share/leetcode-coach/sessions/<date>-<slug>.jsonl`
|
||||
|
||||
@ -5,9 +5,11 @@ its name or any alias, e.g. "XOR", "^", "hash map" -- hands over step 3 of the
|
||||
method (searching the toolbox by properties). Shared by the Stop hook
|
||||
(`stop_gate`) and the offline coach (`local_coach`), so both judge alike.
|
||||
|
||||
An entry kuhy (or the problem statement) already named is free for the whole
|
||||
session, under every alias: after "is it XOR?" the honest answer has to be
|
||||
able to say XOR, or the rewrite can never pass and the gate loops.
|
||||
An entry kuhy already named is free for the whole session, under every
|
||||
alias: after "is it XOR?" the honest answer has to be able to say XOR, or
|
||||
the rewrite can never pass and the gate loops. Only kuhy's own words count:
|
||||
the fetched statement carries LeetCode's topic tags and hints ("Hash
|
||||
Table", "Sliding Window"), so exempting it would pre-clear the answer.
|
||||
|
||||
Matching is case-insensitive on whole tokens. ``\b`` does not work next to a
|
||||
symbol alias like ``^``, so the boundary is "no word character either side":
|
||||
|
||||
@ -81,6 +81,16 @@ def req_state(events: list[dict[str, Any]]) -> ReqState:
|
||||
return ReqState(accepted, not accepted and tries >= MAX_LISTS, tries)
|
||||
|
||||
|
||||
def _property_line(prop: str, box: toolbox.Toolbox) -> str:
|
||||
"""`prop: meaning`, or just `prop` when the meaning names an entry.
|
||||
|
||||
Some meanings illustrate with the operation itself (self-inverse:
|
||||
"a ^ x ^ x = a"); shown to kuhy that is the answer, not a property.
|
||||
"""
|
||||
meaning = box.properties[prop]
|
||||
return prop if leak.leaked_names(meaning, [], box) else f"{prop}: {meaning}"
|
||||
|
||||
|
||||
def _req_note(result: req_check.Check, state: ReqState, box: toolbox.Toolbox) -> str:
|
||||
if result.verdict == "pass":
|
||||
return "[req list accepted -- code is open]"
|
||||
@ -89,7 +99,7 @@ def _req_note(result: req_check.Check, state: ReqState, box: toolbox.Toolbox) ->
|
||||
if state.forced:
|
||||
missing = ", ".join(result.missing)
|
||||
return f"[req list still incomplete ({missing}) -- code accepted anyway]"
|
||||
props = "\n".join(f" - {p}: {box.properties[p]}" for p in result.missing)
|
||||
props = "\n".join(f" - {_property_line(p, box)}" for p in result.missing)
|
||||
return (
|
||||
f"[req list incomplete -- it does not yet say these properties:\n{props}\n"
|
||||
f" revise with req: ({state.tries} of {MAX_LISTS})]"
|
||||
@ -187,5 +197,4 @@ class PhaseGate:
|
||||
"""Toolbox entries `reply` names too early (the local path's bounce)."""
|
||||
if any(t.strip() == "reveal" for t in user_texts) or self.code_seen():
|
||||
return []
|
||||
allowed = [*user_texts, self.statement()]
|
||||
return leak.leaked_names(reply, allowed, self.toolbox)
|
||||
return leak.leaked_names(reply, user_texts, self.toolbox)
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
"""Stop hook: in a /leetcode session, refuse a reply that hands kuhy a line of code.
|
||||
|
||||
Before kuhy's first code it also bounces a reply that names a toolbox entry
|
||||
(`leak`), except an entry kuhy or the fetched problem already named.
|
||||
(`leak`), except an entry kuhy already named. Not the fetched problem:
|
||||
its topic tags and site hints say "Hash Table", "Two Pointers".
|
||||
|
||||
The prose rule ("never write a line for their code") held in one haiku
|
||||
role-play out of two, so it is adjudicated here instead. A reply is blocked
|
||||
@ -61,8 +62,7 @@ def verdict(raw: str, reply: str) -> str | None:
|
||||
if any(u.strip() == "reveal" for u in users):
|
||||
return None
|
||||
if not _code_seen(users):
|
||||
quoted = [gate_detector.allowed_text(entries)]
|
||||
named = leak.leaked_names(reply, quoted, toolbox.load_safe())
|
||||
named = leak.leaked_names(reply, users, toolbox.load_safe())
|
||||
if named:
|
||||
return local_prompt.GATE_REASON_LEAK.format(names=", ".join(named))
|
||||
idents = gate_detector.starter_identifiers(entries)
|
||||
|
||||
@ -203,13 +203,27 @@ def test_leaks_only_before_code_and_never_what_kuhy_typed(
|
||||
assert gate.leaks(reply, []) == []
|
||||
|
||||
|
||||
def test_a_statement_naming_an_entry_lets_the_coach_restate_it(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
def test_the_statement_does_not_free_a_name(tmp_path: Path) -> None:
|
||||
gate = phase.PhaseGate(
|
||||
tmp_path / "l.jsonl", toolbox.load_safe(), lambda: "Use XOR."
|
||||
tmp_path / "l.jsonl", toolbox.load_safe(), lambda: "Topics: XOR."
|
||||
)
|
||||
assert gate.leaks("XOR here", []) == []
|
||||
assert gate.leaks("XOR here", []) == ["XOR"]
|
||||
|
||||
|
||||
def test_a_meaning_that_names_an_entry_is_not_shown(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
box = toolbox.Toolbox(
|
||||
(toolbox.Entry("XOR", ("^",), ("self-inverse",)),),
|
||||
{"self-inverse": "a ^ x ^ x = a", "constant-space": "memory stays flat"},
|
||||
)
|
||||
missing = req_check.Check("incomplete", ("self-inverse", "constant-space"))
|
||||
monkeypatch.setattr(req_check, "check", _Checks(missing))
|
||||
gate = phase.PhaseGate(tmp_path / "l.jsonl", box, lambda: "s")
|
||||
_, note = gate.screen("req: one pass")
|
||||
assert " - self-inverse\n" in note
|
||||
assert " - constant-space: memory stays flat\n" in note
|
||||
assert "^" not in note
|
||||
|
||||
|
||||
def test_open_exports_the_log_path_for_the_hooks(
|
||||
|
||||
@ -48,14 +48,19 @@ def test_the_bounce_reason_does_not_whitelist_itself() -> None:
|
||||
assert stop_gate.verdict(_raw(SLASH_LOAD, feedback), LEAKY) is not None
|
||||
|
||||
|
||||
def test_names_in_the_fetched_statement_may_be_restated() -> None:
|
||||
def test_the_fetched_statement_whitelists_nothing() -> None:
|
||||
"""Its topic tags and site hints name the answer ("Topics: Hash Table")."""
|
||||
tool = {
|
||||
"type": "user",
|
||||
"message": {
|
||||
"content": [{"type": "tool_result", "content": "Solve it with XOR."}]
|
||||
"content": [
|
||||
{"type": "tool_result", "content": "Topics: Hash Table. Use XOR."}
|
||||
]
|
||||
},
|
||||
}
|
||||
assert stop_gate.verdict(_raw(SLASH_LOAD, tool), LEAKY) is None
|
||||
raw = _raw(SLASH_LOAD, tool)
|
||||
assert stop_gate.verdict(raw, LEAKY) is not None
|
||||
assert stop_gate.verdict(raw, "Would a hash map help?") is not None
|
||||
|
||||
|
||||
def test_the_session_log_lifts_the_bounce_once_code_exists(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user