Files
ten31-database/backend/thesis_review.py
T
Keysat 3e199fd8d5 Phase 1 Workstream A+E: thesis substrate + dual-approval gate
- migration 0002_phase1_architect: thesis_lines (core spine + per-segment lines),
  thesis_nodes (+ append-only revisions), thesis_versions (one-canonical-per-line
  DB invariant), thesis_reviews (dual approval + feedback), segments. Reversible.
- backend/mcp/architect_tools.py: agent draft tools (node tree, versions,
  segments, get_canonical fails-closed) — NO self-approval path. MCP-exposed.
- backend/thesis_review.py + server.py routes: human-gated approval. Dual sign-off
  via thesis_required_approvals; atomic supersede; every action logged.
- docs/PHASE_1.md (kickoff brief); docs/OPERATIONS.md (partner guide);
  start9/0.4 "Resolve duplicate names" fuzzy action.

Verified on synthetic data: dual approval promotes correctly, exactly one
canonical survives supersede, get_canonical fails closed, full interaction_log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:20:00 -05:00

154 lines
6.5 KiB
Python

"""Human-gated thesis approval (Phase 1 Workstream E).
NOT an agent tool — called only by authenticated CRM routes in server.py. The
Architect can draft and submit; only a human partner can promote a version to
canonical. Supports Grant's dual-sign-off + collaborative feedback: both partners
can leave reviews (approve / request_changes / comment with free-text the
Architect reads to iterate); a version promotes to canonical once distinct
'approve' reviewers reach `thesis_required_approvals` (app_settings, default 1 —
set to 2 for dual sign-off). Promotion atomically supersedes the prior canonical,
honoring the one-canonical-per-line DB invariant. Everything is logged.
"""
import json
import sqlite3
import uuid
from datetime import datetime, timezone
_VALID = ("approve", "request_changes", "comment")
def _now():
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat() + "Z"
def _conn(db):
c = sqlite3.connect(db)
c.row_factory = sqlite3.Row
c.execute("PRAGMA foreign_keys=ON")
return c
def _log(c, actor_id, action, target_id, payload):
c.execute("""INSERT INTO interaction_log
(id, ts, actor_type, actor_id, action, target_type, target_id, payload, source, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(str(uuid.uuid4()), _now(), "human", actor_id, action, "thesis", target_id,
json.dumps(payload) if payload is not None else None, "crm_ui", _now()))
def required_approvals(c):
r = c.execute("SELECT value_json FROM app_settings WHERE key='thesis_required_approvals'").fetchone()
try:
return max(1, int(json.loads(r[0]))) if r else 1
except Exception:
return 1
def _approver_count(c, version_id):
return len({r[0] for r in c.execute(
"SELECT DISTINCT reviewer_user_id FROM thesis_reviews WHERE version_id=? AND decision='approve'",
(version_id,))})
def _promote(c, version_row, approver_user_id):
"""Atomically supersede the prior canonical (if any) for this line, then make
this version canonical. Supersede-first keeps the one-canonical unique index satisfied."""
line_id = version_row["line_id"]
prior = c.execute("SELECT id FROM thesis_versions WHERE line_id=? AND status='canonical'", (line_id,)).fetchone()
if prior:
c.execute("UPDATE thesis_versions SET status='superseded', superseded_by=? WHERE id=?",
(version_row["id"], prior["id"]))
c.execute("UPDATE thesis_versions SET status='canonical', approved_at=? WHERE id=?", (_now(), version_row["id"]))
_log(c, approver_user_id, "thesis.approved", version_row["id"],
{"line_id": line_id, "superseded": prior["id"] if prior else None})
def record_review(db, version_id, reviewer_user_id, decision, feedback=None, target_node_id=None):
"""Record a partner's review. Promotes to canonical when approve-threshold met.
`reviewer_user_id` MUST be a real authenticated human (enforced by the route)."""
if decision not in _VALID:
return {"error": "bad_decision", "allowed": list(_VALID)}
c = _conn(db)
v = c.execute("SELECT * FROM thesis_versions WHERE id=?", (version_id,)).fetchone()
if not v:
c.close()
return {"error": "not_found", "version_id": version_id}
c.execute("""INSERT INTO thesis_reviews (id, version_id, reviewer_user_id, decision, feedback, target_node_id, created_at)
VALUES (?,?,?,?,?,?,?)""",
(str(uuid.uuid4()), version_id, reviewer_user_id, decision, feedback, target_node_id, _now()))
_log(c, reviewer_user_id, f"thesis.review.{decision}", version_id,
{"feedback": feedback, "target_node_id": target_node_id})
promoted = False
approvals = _approver_count(c, version_id)
need = required_approvals(c)
if decision == "approve" and v["status"] in ("draft", "in_review") and approvals >= need:
_promote(c, v, reviewer_user_id)
promoted = True
c.commit()
c.close()
return {"version_id": version_id, "decision": decision, "approvals": approvals,
"required": need, "promoted_to_canonical": promoted}
# ── reads for the review UI ───────────────────────────────────────────────────
def list_lines(db):
c = _conn(db)
rows = [dict(r) for r in c.execute(
"SELECT id, line_key, name, segment_key, is_core, status FROM thesis_lines WHERE deleted_at IS NULL ORDER BY is_core DESC, name")]
c.close()
return {"lines": rows}
def list_versions_for_review(db):
c = _conn(db)
rows = []
for v in c.execute("""SELECT v.id, v.line_id, l.line_key, l.name, v.version_no, v.status, v.created_at, v.rationale
FROM thesis_versions v JOIN thesis_lines l ON l.id=v.line_id
WHERE v.status='in_review' ORDER BY v.created_at DESC"""):
d = dict(v)
d["approvals"] = _approver_count(c, v["id"])
d["required"] = required_approvals(c)
rows.append(d)
c.close()
return {"versions": rows}
def get_canonical(db, line_key):
"""The one canonical version's frozen body_json; fails closed if none."""
c = _conn(db)
line = c.execute("SELECT id FROM thesis_lines WHERE line_key=? AND deleted_at IS NULL", (line_key,)).fetchone()
if not line:
c.close()
return {"status": "no_such_line", "line_key": line_key}
r = c.execute("SELECT * FROM thesis_versions WHERE line_id=? AND status='canonical'", (line["id"],)).fetchone()
c.close()
if not r:
return {"status": "no_canonical_thesis", "line_key": line_key}
return {"status": "ok", "line_key": line_key, "version_id": r["id"], "version_no": r["version_no"],
"approved_at": r["approved_at"], "thesis": json.loads(r["body_json"])}
def get_version(db, version_id):
c = _conn(db)
v = c.execute("SELECT * FROM thesis_versions WHERE id=?", (version_id,)).fetchone()
if not v:
c.close()
return {"error": "not_found", "version_id": version_id}
out = dict(v)
try:
out["body"] = json.loads(v["body_json"])
except Exception:
out["body"] = None
out.pop("body_json", None)
out["reviews"] = [dict(r) for r in c.execute(
"SELECT reviewer_user_id, decision, feedback, target_node_id, created_at FROM thesis_reviews WHERE version_id=? ORDER BY created_at",
(version_id,))]
out["approvals"] = _approver_count(c, version_id)
out["required"] = required_approvals(c)
c.close()
return out