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>
This commit is contained in:
Keysat
2026-06-05 10:20:00 -05:00
parent 6be2e40f54
commit 3e199fd8d5
10 changed files with 993 additions and 0 deletions
+55
View File
@@ -37,6 +37,12 @@ except Exception:
jwt = None
JWT_AVAILABLE = False
# Phase-1 Architect: human-gated thesis approval logic (pure stdlib; guarded).
try:
import thesis_review # type: ignore
except Exception:
thesis_review = None
# ─── Configuration ────────────────────────────────────────────────────────────
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -1731,6 +1737,16 @@ class CRMHandler(BaseHTTPRequestHandler):
if path == '/api/audit-log':
return self.handle_list_audit_log(user, params)
# ─── Architect thesis (Phase 1) ───
if path == '/api/thesis/lines':
return self.handle_list_thesis_lines(user)
if path == '/api/thesis/versions':
return self.handle_list_thesis_review_queue(user)
if re.match(r'^/api/thesis/versions/[^/]+$', path):
return self.handle_get_thesis_version(user, path.split('/')[-1])
if re.match(r'^/api/thesis/[^/]+/canonical$', path):
return self.handle_get_canonical_thesis(user, path.split('/')[-2])
self.send_error_json("Not found", 404)
def do_POST(self):
@@ -1795,6 +1811,10 @@ class CRMHandler(BaseHTTPRequestHandler):
if path == '/api/fundraising/backup-verify':
return self.handle_verify_fundraising_backups(user)
# ─── Architect thesis review (Phase 1, human approval gate) ───
if re.match(r'^/api/thesis/versions/[^/]+/review$', path):
return self.handle_thesis_review(user, path.split('/')[-2], body)
self.send_error_json("Not found", 404)
def do_PUT(self):
@@ -3408,6 +3428,41 @@ class CRMHandler(BaseHTTPRequestHandler):
conn.close()
return self.send_json({"message": "Tag deleted"})
# ─── Architect thesis (Phase 1) ───
def handle_list_thesis_lines(self, user):
if thesis_review is None:
return self.send_error_json("Thesis module unavailable", 503)
return self.send_json(thesis_review.list_lines(DB_PATH))
def handle_list_thesis_review_queue(self, user):
if thesis_review is None:
return self.send_error_json("Thesis module unavailable", 503)
return self.send_json(thesis_review.list_versions_for_review(DB_PATH))
def handle_get_thesis_version(self, user, version_id):
if thesis_review is None:
return self.send_error_json("Thesis module unavailable", 503)
return self.send_json(thesis_review.get_version(DB_PATH, version_id))
def handle_get_canonical_thesis(self, user, line_key):
if thesis_review is None:
return self.send_error_json("Thesis module unavailable", 503)
return self.send_json(thesis_review.get_canonical(DB_PATH, line_key))
def handle_thesis_review(self, user, version_id, body):
# Promotion to canonical is a human partner action (guardrail #4).
if not require_admin(user):
return self.send_error_json("Admin required", 403)
if thesis_review is None:
return self.send_error_json("Thesis module unavailable", 503)
body = body or {}
res = thesis_review.record_review(DB_PATH, version_id, user['user_id'],
body.get('decision'), body.get('feedback'),
body.get('target_node_id'))
if res.get('error'):
return self.send_error_json(res['error'], 400)
return self.send_json({"data": res})
def handle_list_users(self, user):
conn = get_db()
users = rows_to_list(conn.execute(