diff --git a/src/scribe/services/rulebooks.py b/src/scribe/services/rulebooks.py index 48eed7d..57b0849 100644 --- a/src/scribe/services/rulebooks.py +++ b/src/scribe/services/rulebooks.py @@ -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 diff --git a/tests/test_milestone_summary_brief.py b/tests/test_milestone_summary_brief.py index 3c5fcf9..63309bf 100644 --- a/tests/test_milestone_summary_brief.py +++ b/tests/test_milestone_summary_brief.py @@ -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))