Files
FabledGrimoire/src/scribe/routes/family.py
T
bvandeusenandClaude Opus 5.5 f6734291c9 docs(6206): the remaining models, routes, mcp/tools/repos and config comments drop citations
A shipped tool docstring (mcp/tools/repos bind_repo) cited this instance's
"rule 1: dev is home"; it now says what a dev-branch project should do.
routes/knowledge, routes/tasks, routes/systems and models/retrieval_log state
the property each guards rather than the bug that once broke it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:44:31 -04:00

147 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Family canon routes — the web door to the promotion engine.
The agent decides promotions and each project's answers through the MCP
tools; this door exists so a person can READ every decision and its reasons
— the ideas, the adoption matrix, the log — and retire an idea or undo a
decision when they disagree. The promote and propose endpoints are here for
parity with the agent's door, and are recorded as the operator's.
Every write is gated on the idea's note in the service.
"""
import logging
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import family as family_svc
from scribe.services import family_adoption as adoption_svc
logger = logging.getLogger(__name__)
family_bp = Blueprint("family", __name__, url_prefix="/api/family")
def _page() -> tuple[int, int]:
try:
limit = max(1, min(int(request.args.get("limit", 50)), 200))
offset = max(0, int(request.args.get("offset", 0)))
except ValueError:
limit, offset = 50, 0
return limit, offset
@family_bp.route("/ideas", methods=["GET"])
@login_required
async def list_ideas_route():
limit, offset = _page()
ideas = await family_svc.list_ideas(
get_current_user_id(),
status=request.args.get("status") or None,
platform=request.args.get("platform") or None,
limit=limit, offset=offset,
)
return jsonify({
"ideas": ideas, "criteria": list(family_svc.CRITERIA),
"conflict_order": list(adoption_svc.CONFLICT_ORDER),
})
@family_bp.route("/ideas/<int:note_id>", methods=["GET"])
@login_required
async def get_idea_route(note_id: int):
uid = get_current_user_id()
idea = await family_svc.get_idea(uid, note_id)
if idea is None:
return not_found("Family idea")
idea["precedents"] = await family_svc.precedents(uid, note_id)
return jsonify(idea)
@family_bp.route("/ideas/<int:note_id>/propose", methods=["POST"])
@login_required
async def propose_route(note_id: int):
data = await request.get_json() or {}
try:
idea, created = await family_svc.propose(
get_current_user_id(), note_id, reason=data.get("reason") or "",
applies_when=data.get("applies_when") or None, decided_via="operator",
)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
return jsonify({"idea": idea, "created": created}), (201 if created else 200)
@family_bp.route("/ideas/<int:note_id>/promote", methods=["POST"])
@login_required
async def promote_route(note_id: int):
data = await request.get_json() or {}
try:
result = await family_svc.promote(
get_current_user_id(), note_id,
applies_when=data.get("applies_when") or "",
platforms=data.get("platforms") or [],
criteria=data.get("criteria") or {},
evidence=data.get("evidence") or [],
reason=data.get("reason") or "",
precedent_ids=data.get("precedent_ids") or [],
decided_via="operator",
)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
return jsonify(result)
@family_bp.route("/ideas/<int:note_id>/retire", methods=["POST"])
@login_required
async def retire_route(note_id: int):
data = await request.get_json() or {}
try:
result = await family_svc.retire(
get_current_user_id(), note_id, reason=data.get("reason") or "",
decided_via="operator",
)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
return jsonify(result)
@family_bp.route("/matrix", methods=["GET"])
@login_required
async def matrix_route():
"""Projects × canon ideas — the adoption matrix. Read-only: the agent
answers through assess_family_adoption; a person reads the answers here
and undoes one from the decision log if they disagree."""
matrix = await adoption_svc.adoption_matrix(
get_current_user_id(),
platform=request.args.get("platform") or None,
project_id=request.args.get("project_id", type=int) or None,
)
return jsonify(matrix)
@family_bp.route("/decisions", methods=["GET"])
@login_required
async def list_decisions_route():
limit, offset = _page()
idea_id = request.args.get("idea_id", type=int)
project_id = request.args.get("project_id", type=int)
rows = await family_svc.list_decisions(
get_current_user_id(), idea_id=idea_id or None, project_id=project_id or None,
limit=limit, offset=offset,
)
return jsonify({"decisions": rows, "limit": limit, "offset": offset})
@family_bp.route("/decisions/<int:decision_id>/undo", methods=["POST"])
@login_required
async def undo_route(decision_id: int):
data = await request.get_json() or {}
try:
result = await family_svc.undo(
get_current_user_id(), decision_id, reason=data.get("reason") or "",
decided_via="operator",
)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
return jsonify(result)