fix(rules): planning reads list rules by id and title instead of restating them (#4081)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 53s
CI & Build / TypeScript typecheck (push) Successful in 1m3s
CI & Build / Python tests (push) Failing after 1m4s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 53s
CI & Build / TypeScript typecheck (push) Successful in 1m3s
CI & Build / Python tests (push) Failing after 1m4s
CI & Build / Build & push image (push) Skipped
start_planning on project 2 replied with 92,645 characters, 65k of them applicable_rules. Milestone 414 made a project's listing every global rule tagged to an area it works in (before, the rules of subscribed rulebooks, and project 2 subscribed to none), and the non-brief rules_payload sent each as a full rule_brief. get_milestone, get_project and get_task carried the same. Every rules_payload form now lists: id, title, the topic a global rule sits in, and `via` for a co_surfaces partner. get_rule reads one in full, and retrieval delivers them in full when work matches — the reasoning #4045 applied to the handshake. A test pins 81 full-length rules under 6k characters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -1112,36 +1112,46 @@ def rules_payload(
|
||||
(`plugin_context`) — computes a marker and shows nobody anything, and
|
||||
counting it would put rules in the denominator that no agent ever saw.
|
||||
|
||||
EVERY FORM LISTS, NONE RESTATES. Rules reach a session in full by
|
||||
retrieval, so these payloads say which constraints exist — id and title,
|
||||
the topic a global rule sits in, `via` for a co_surfaces partner — and
|
||||
get_rule reads one. Planning reads carried the full rule_brief until a
|
||||
project's listing grew to every global rule tagged to its areas
|
||||
(milestone 414) and start_planning replied with 92k characters (#4081),
|
||||
the shape #4045 had just removed from the handshake.
|
||||
|
||||
`brief` is the session handshake's form (#4045): the project's own rules
|
||||
as id and title, nothing else. Rules reach a session in full by
|
||||
retrieval, so the handshake lists which of the project's constraints exist
|
||||
rather than restating them; get_rule reads one. Only what is shown is
|
||||
recorded as surfaced.
|
||||
only. Only what is shown is recorded as surfaced.
|
||||
"""
|
||||
project_rules = [_rule_line(r) for r in applicable.get("project_rules", [])]
|
||||
if brief:
|
||||
project_rules = [
|
||||
{"id": r["id"], "title": r["title"]}
|
||||
for r in applicable.get("project_rules", [])
|
||||
]
|
||||
record_rule_surfaced(
|
||||
user_id=user_id, rule_ids=[r["id"] for r in project_rules], source=source,
|
||||
)
|
||||
return {"project_rules": project_rules}
|
||||
rules = [_rule_line(r) for r in applicable.get("rules", [])]
|
||||
record_rule_surfaced(
|
||||
user_id=user_id,
|
||||
rule_ids=(
|
||||
[r["id"] for r in applicable.get("rules", [])]
|
||||
+ [r["id"] for r in applicable.get("project_rules", [])]
|
||||
),
|
||||
rule_ids=[r["id"] for r in rules] + [r["id"] for r in project_rules],
|
||||
source=source,
|
||||
)
|
||||
return {
|
||||
"applicable_rules": applicable["rules"],
|
||||
"applicable_rules": rules,
|
||||
"applicable_rules_truncated": applicable["truncated"],
|
||||
"project_rules": applicable.get("project_rules", []),
|
||||
"project_rules": project_rules,
|
||||
}
|
||||
|
||||
|
||||
def _rule_line(brief: dict) -> dict:
|
||||
"""One rule as a listing names it: enough to recognise it and fetch it.
|
||||
Keys a row does not carry are left out rather than sent empty (#2483)."""
|
||||
line = {"id": brief["id"], "title": brief["title"]}
|
||||
for key in ("topic_title", "via"):
|
||||
if brief.get(key):
|
||||
line[key] = brief[key]
|
||||
return line
|
||||
|
||||
|
||||
# ── The staleness marker (milestone 323 step 5) ────────────────────────
|
||||
#
|
||||
# WHAT THIS CAN AND CANNOT SEE. An etag catches a rule that MOVED after a
|
||||
|
||||
@@ -193,3 +193,28 @@ async def test_list_milestones_lists_every_milestone_without_plans():
|
||||
out = await list_milestones(project_id=5)
|
||||
assert len(out["milestones"]) == 30
|
||||
assert all("body" not in m for m in out["milestones"])
|
||||
|
||||
|
||||
def test_a_planning_read_lists_rules_without_restating_them():
|
||||
"""#4081: a project's listing is every global rule tagged to its areas, so
|
||||
the full rule_brief of each put start_planning at 92k characters. Planning
|
||||
reads name the rules; get_rule reads one."""
|
||||
from scribe.services.rulebooks import rules_payload
|
||||
|
||||
applicable = {
|
||||
"rules": [{"id": i, "title": f"r{i}", "statement": PLAN, "when_to_apply": PLAN,
|
||||
"topic_title": "git", "relations": [{"note": PLAN}]} for i in range(50)]
|
||||
+ [{"id": 900, "title": "partner", "statement": PLAN, "via": "co_surfaces"}],
|
||||
"project_rules": [{"id": 100 + i, "title": f"pr{i}", "statement": PLAN}
|
||||
for i in range(30)],
|
||||
"truncated": True,
|
||||
}
|
||||
with patch("scribe.services.rulebooks.record_rule_surfaced") as surfaced:
|
||||
out = rules_payload(applicable, user_id=7, source="start_planning")
|
||||
|
||||
assert out["applicable_rules"][0] == {"id": 0, "title": "r0", "topic_title": "git"}
|
||||
assert out["applicable_rules"][-1] == {"id": 900, "title": "partner", "via": "co_surfaces"}
|
||||
assert out["project_rules"][0] == {"id": 100, "title": "pr0"}
|
||||
assert out["applicable_rules_truncated"] is True
|
||||
assert len(surfaced.call_args.kwargs["rule_ids"]) == 81
|
||||
assert len(json.dumps(out)) < 6_000, len(json.dumps(out))
|
||||
|
||||
Reference in New Issue
Block a user