feat(rulebook): augment get_project with applicable_rules + MCP instructions
This commit is contained in:
@@ -20,6 +20,16 @@ Hierarchy: Project -> Milestone -> Task/Note.
|
||||
unchanged on updates.
|
||||
- For optional integer FKs (project_id, milestone_id, parent_id), use 0 to mean
|
||||
"not set".
|
||||
|
||||
Scribe maintains a Rulebook system (Rulebook -> Topic -> Rule). Rules carry
|
||||
an actionable statement plus optional Why and How-to-apply context. When you
|
||||
start work on a project, get_project(id) returns applicable_rules (the rules
|
||||
from rulebooks the project subscribes to) and subscribed_rulebooks. Consult
|
||||
these before making decisions about workflow, conventions, or scope. Full
|
||||
text (Why / How-to-apply) is available via get_rule(id). You may create new
|
||||
rules via create_rule when you notice a pattern worth codifying — coordinate
|
||||
with the operator on whether it belongs in an existing rulebook+topic or a
|
||||
new one.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
from fabledassistant.mcp._context import current_user_id
|
||||
from fabledassistant.services import milestones as milestones_svc
|
||||
from fabledassistant.services import projects as projects_svc
|
||||
from fabledassistant.services import rulebooks as rulebooks_svc
|
||||
|
||||
|
||||
async def list_projects() -> dict:
|
||||
@@ -33,10 +34,11 @@ async def list_projects() -> dict:
|
||||
|
||||
|
||||
async def get_project(project_id: int) -> dict:
|
||||
"""Fetch a Scribe project by ID, including its milestone summary.
|
||||
"""Fetch a Scribe project by ID.
|
||||
|
||||
Returns full project fields plus a milestone_summary list with each
|
||||
milestone's id, title, status, and task counts.
|
||||
Returns full project fields, a milestone_summary list, and the
|
||||
rulebook-applicable_rules / subscribed_rulebooks pair the assistant
|
||||
should consult when working on this project.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
project = await projects_svc.get_project(uid, project_id)
|
||||
@@ -46,6 +48,12 @@ async def get_project(project_id: int) -> dict:
|
||||
data["milestone_summary"] = await milestones_svc.get_project_milestone_summary(
|
||||
uid, project_id,
|
||||
)
|
||||
applicable = await rulebooks_svc.get_applicable_rules(
|
||||
project_id=project_id, user_id=uid,
|
||||
)
|
||||
data["applicable_rules"] = applicable["rules"]
|
||||
data["applicable_rules_truncated"] = applicable["truncated"]
|
||||
data["subscribed_rulebooks"] = applicable["subscribed_rulebooks"]
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -41,18 +41,57 @@ async def test_list_projects_wraps_in_dict():
|
||||
async def test_get_project_enriches_with_milestone_summary():
|
||||
p = _fake_project(id=5, title="found")
|
||||
milestone_summary = [{"id": 10, "title": "MS", "task_count": 3}]
|
||||
applicable_payload = {
|
||||
"rules": [], "truncated": False, "subscribed_rulebooks": [],
|
||||
}
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
):
|
||||
out = await get_project(project_id=5)
|
||||
assert out["id"] == 5
|
||||
assert out["milestone_summary"] == milestone_summary
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_includes_applicable_rules_and_subscribed_rulebooks():
|
||||
"""The augmented get_project response includes applicable_rules and
|
||||
subscribed_rulebooks pulled from services/rulebooks.get_applicable_rules.
|
||||
"""
|
||||
p = _fake_project(id=3, title="Fabled Assistant")
|
||||
milestone_summary = []
|
||||
applicable_payload = {
|
||||
"rules": [
|
||||
{"id": 1, "title": "dev is home",
|
||||
"statement": "Work directly on dev",
|
||||
"topic_title": "git-workflow",
|
||||
"rulebook_title": "FabledSword family"},
|
||||
],
|
||||
"truncated": False,
|
||||
"subscribed_rulebooks": [{"id": 1, "title": "FabledSword family"}],
|
||||
}
|
||||
with patch(
|
||||
"fabledassistant.mcp.tools.projects.projects_svc.get_project",
|
||||
AsyncMock(return_value=p),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary",
|
||||
AsyncMock(return_value=milestone_summary),
|
||||
), patch(
|
||||
"fabledassistant.mcp.tools.projects.rulebooks_svc.get_applicable_rules",
|
||||
AsyncMock(return_value=applicable_payload),
|
||||
):
|
||||
out = await get_project(project_id=3)
|
||||
assert out["applicable_rules"][0]["title"] == "dev is home"
|
||||
assert out["subscribed_rulebooks"] == [{"id": 1, "title": "FabledSword family"}]
|
||||
assert out["applicable_rules_truncated"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_raises_when_not_found():
|
||||
with patch(
|
||||
|
||||
Reference in New Issue
Block a user