Preferences — a rule kind that Scribe keeps up to date (#3849 steps 1–3) #150
@@ -859,7 +859,13 @@ async def get_writepath_config(user_id: int) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _rule_hint_line(rule, *, where: str, seen: bool) -> str:
|
def _rule_hint_line(rule, *, where: str, seen: bool) -> str:
|
||||||
"""One rule hint line — both arms, both tails (#3750).
|
"""One rule hint line — both arms, both tails, both kinds (#3750, #3849).
|
||||||
|
|
||||||
|
TWO INDEPENDENT AXES. `kind` decides the head, `seen` decides the tail,
|
||||||
|
and neither reads the other. A preference and a rule differ in force; a
|
||||||
|
repeat and a first surfacing differ in whether the session already holds
|
||||||
|
the line. Those are unrelated facts, and keeping them unrelated in the
|
||||||
|
code is what stopped the second kind from reopening the repeat question.
|
||||||
|
|
||||||
ONE FUNCTION BECAUSE THE TAILS MUST NOT DRIFT. The two arms phrase their
|
ONE FUNCTION BECAUSE THE TAILS MUST NOT DRIFT. The two arms phrase their
|
||||||
heads differently ("may apply here" vs "may apply to this Bash call") and
|
heads differently ("may apply here" vs "may apply to this Bash call") and
|
||||||
@@ -891,15 +897,38 @@ def _rule_hint_line(rule, *, where: str, seen: bool) -> str:
|
|||||||
managing it.
|
managing it.
|
||||||
"""
|
"""
|
||||||
trigger = (rule.when_to_apply or "").strip()
|
trigger = (rule.when_to_apply or "").strip()
|
||||||
|
preference = rule.kind == "preference"
|
||||||
|
# KIND CHANGES THE HEAD; `seen` CHANGES THE TAIL. The two axes are
|
||||||
|
# independent and stay that way, which is what lets the repeat logic above
|
||||||
|
# survive a second kind without being reasoned about again: whether a
|
||||||
|
# record is already on the ledger has nothing to do with how much force it
|
||||||
|
# carries, so the seen-branch is shared verbatim.
|
||||||
|
#
|
||||||
|
# The noun is the whole of the visual difference, and that is deliberate.
|
||||||
|
# A reader skimming an injected block gets one word to place the register \u2014
|
||||||
|
# so it is the SECOND word that moves, and it is the word naming force.
|
||||||
|
# Everything structural after it is identical, so the three kinds read as
|
||||||
|
# one set rather than three formats (milestone 385's step 5 writes the
|
||||||
|
# lesson voice against these two; they are one paragraph, not three).
|
||||||
|
noun = "Preference" if preference else "Standing rule"
|
||||||
|
# The only other place force is asserted. A rule's line tells the reader
|
||||||
|
# not to dismiss it unread, because dismissing a rule unread is how the
|
||||||
|
# thing it prevents happens. A preference makes no such claim: it says
|
||||||
|
# where to find how this has been done, and following it is what keeps
|
||||||
|
# things consistent rather than what keeps them correct.
|
||||||
|
reason = (
|
||||||
|
"for how this has been done before" if preference
|
||||||
|
else "before deciding it does not apply"
|
||||||
|
)
|
||||||
tail = (
|
tail = (
|
||||||
f"You saw it earlier this session; pull it with get_rule({rule.id}) "
|
f"You saw it earlier this session; pull it with get_rule({rule.id}) "
|
||||||
"if you no longer hold it."
|
"if you no longer hold it."
|
||||||
if seen else
|
if seen else
|
||||||
f"Read it with get_rule({rule.id}) before deciding it does not "
|
f"Read it with get_rule({rule.id}) {reason}; it is not in this "
|
||||||
"apply; it is not in this session's loaded set."
|
"session's loaded set."
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f"Standing rule that may apply {where} \u2014 \u201c{rule.title}\u201d"
|
f"{noun} that may apply {where} \u2014 \u201c{rule.title}\u201d"
|
||||||
+ (f" ({trigger})" if trigger else "")
|
+ (f" ({trigger})" if trigger else "")
|
||||||
+ f". {tail}"
|
+ f". {tail}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1006,6 +1006,93 @@ async def test_the_two_tails_are_distinguishable_and_say_the_true_one(source, ru
|
|||||||
assert fresh_ctx != seen_ctx, "the two tails collapsed into one"
|
assert fresh_ctx != seen_ctx, "the two tails collapsed into one"
|
||||||
|
|
||||||
|
|
||||||
|
# ── the third axis: KIND (milestone 399) ────────────────────────────────
|
||||||
|
#
|
||||||
|
# A preference is a rule row that does not bind, so it rides the same arms and
|
||||||
|
# the same line. What must differ is the REGISTER: a rule's line tells the
|
||||||
|
# reader not to dismiss it unread, because dismissing a rule unread is how the
|
||||||
|
# thing it prevents happens. A preference makes no such claim — it says where
|
||||||
|
# to find how this has been done, and following it buys consistency rather
|
||||||
|
# than correctness.
|
||||||
|
#
|
||||||
|
# Rendered in the rule's voice, a preference becomes the thing milestone 399
|
||||||
|
# exists to avoid: a rule with a different column.
|
||||||
|
|
||||||
|
_PREF = fake_rule(
|
||||||
|
id=177,
|
||||||
|
kind="preference",
|
||||||
|
title="Pace hard debugging one step at a time",
|
||||||
|
statement="Advance one investigative step per turn.",
|
||||||
|
when_to_apply="during hard debugging",
|
||||||
|
)
|
||||||
|
_RULE_FORCE = "before deciding it does not apply"
|
||||||
|
_PREF_FORCE = "for how this has been done before"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_a_preference_does_not_speak_in_the_rules_voice(source, run):
|
||||||
|
"""The register, pinned on the two places force is actually asserted.
|
||||||
|
|
||||||
|
On BOTH the noun and the clause, because either alone is weak. A line
|
||||||
|
reading "Preference … before deciding it does not apply" has swapped the
|
||||||
|
label and kept the instruction, which is worse than not distinguishing
|
||||||
|
them at all: it looks handled.
|
||||||
|
"""
|
||||||
|
ctx = (await run([(0.81, _PREF)], MagicMock()))["context"]
|
||||||
|
|
||||||
|
assert "Preference that may apply" in ctx, (
|
||||||
|
f"{source} announced a preference as something else. The noun is the "
|
||||||
|
f"one word a skimming reader gets to place the register, so it is the "
|
||||||
|
f"word that has to move. Context was: {ctx!r}"
|
||||||
|
)
|
||||||
|
assert _PREF_FORCE in ctx and _RULE_FORCE not in ctx, (
|
||||||
|
f"{source} told the session to read a PREFERENCE before deciding it "
|
||||||
|
f"does not apply. That is a rule's claim: it is the sentence that "
|
||||||
|
f"makes a line bind, and a preference does not."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_kind_and_seen_do_not_read_each_other(source, run):
|
||||||
|
"""The structural claim the design rests on: two INDEPENDENT axes.
|
||||||
|
|
||||||
|
Whether a record is already on the exclusion ledger has nothing to do with
|
||||||
|
how much force it carries. Keeping the two unrelated in the code is what
|
||||||
|
let a second kind arrive without reopening #3750's repeat question — and
|
||||||
|
the way that silently breaks is a `seen` branch that grows a kind test,
|
||||||
|
or a kind branch that grows a `seen` test, leaving one of the four
|
||||||
|
combinations rendered by nobody's intention.
|
||||||
|
|
||||||
|
So: all four are exercised, and the seen tail must come out identical for
|
||||||
|
both kinds.
|
||||||
|
"""
|
||||||
|
pref_fresh = (await run([(0.81, _PREF)], MagicMock()))["context"]
|
||||||
|
pref_seen = (await run([(0.81, _PREF)], MagicMock(),
|
||||||
|
exclude_rule_ids=[177]))["context"]
|
||||||
|
rule_seen = (await run([(0.81, _HELD)], MagicMock(),
|
||||||
|
exclude_rule_ids=[156]))["context"]
|
||||||
|
|
||||||
|
assert _SEEN_TAIL in pref_seen and _FRESH_TAIL in pref_fresh, (
|
||||||
|
f"{source}: the seen/fresh split stopped working once kind was added — "
|
||||||
|
f"the tail axis is now reading the head axis"
|
||||||
|
)
|
||||||
|
# The tail is about the LEDGER, so it is shared verbatim. Compared as the
|
||||||
|
# tail alone rather than the whole line, since the heads differ by design.
|
||||||
|
assert _SEEN_TAIL in rule_seen, "the rule's seen tail changed"
|
||||||
|
assert pref_seen.count(_SEEN_TAIL) == rule_seen.count(_SEEN_TAIL) == 1, (
|
||||||
|
f"{source} rendered the repeat clause a different number of times for "
|
||||||
|
f"the two kinds; the tail is about the ledger and does not vary by force"
|
||||||
|
)
|
||||||
|
# And the head still differs in the seen case — a repeat of a preference
|
||||||
|
# is still a preference.
|
||||||
|
assert "Preference that may apply" in pref_seen, (
|
||||||
|
f"{source} lost the preference register on a repeat, so a preference "
|
||||||
|
f"seen twice reads as a rule the second time"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
|
@pytest.mark.parametrize(("source", "run"), _ARMS, ids=["write_path", "pre_tool"])
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_neither_tail_injects_the_rule_statement(source, run):
|
async def test_neither_tail_injects_the_rule_statement(source, run):
|
||||||
|
|||||||
Reference in New Issue
Block a user