Thesis Workshop redesign: edit/choose/delete + approve-as-current (v0.1.0:56)
Addresses Grant's feedback that the Workshop was confusing and underbuilt (no delete,
no approve, redundant generate-vs-feedback panels, and a stray "0" on segment lines).
Backend (architect_tools.py + server.py routes/handlers):
- retire_node: soft-delete a node + its subtree (reversible). DELETE /api/thesis/nodes/{id}.
- choose_variant: 'Use this' — keep this option, soft-delete the others in its group,
mark it approved. POST /api/thesis/nodes/{id}/choose.
- upsert_thesis_node gains actor_type so a manual human edit is recorded as 'human'.
PUT /api/thesis/nodes/{id} edits a part's text directly.
- handle_approve_line: one-click 'approve as current' — records this admin's approval on
the line's in-review version (creating + submitting one from the live tree if none),
promoting to canonical at the required distinct-approval count. POST /api/thesis/lines/{key}/approve.
Frontend (ThesisWorkshop redesign):
- Merged the redundant "Generate options" + "Give feedback" panels into one "Ask the
Architect for options" box (revise was just generate-with-guidance).
- Per option: Use this / Edit (inline) / Delete. Per part: edit + delete via the same.
- "Approve as current" bar with dual-sign-off state + a "Current ✓" badge, and a one-line
"how it works" hint. Refreshes the tree after every action.
- Fixed the stray "0": `{line.is_core && <badge>}` rendered 0 for non-core lines (SQLite
integer 0); now `{!!line.is_core && ...}`.
Verified: backend test_thesis_actions.py (choose/edit/retire-subtree/dual-approval->canonical),
and a live in-browser smoke test (JSX compiles, Workshop renders, options show Use/Edit/Delete,
approve returns 1-of-2, no runtime errors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -192,7 +192,7 @@ def create_thesis_line(line_key, name, segment_key=None, is_core=False, descript
|
||||
|
||||
def upsert_thesis_node(line_id, node_type, body, title=None, parent_id=None, ord=None,
|
||||
variant_group=None, node_id=None, change_reason=None, change_summary=None,
|
||||
actor_id="architect", claude_session_id=None, meta=None, db=None):
|
||||
actor_id="architect", actor_type="agent", claude_session_id=None, meta=None, db=None):
|
||||
"""Create or edit a node. On edit, the prior state is written to
|
||||
thesis_node_revisions before the live row changes (full provenance)."""
|
||||
c = _conn(db)
|
||||
@@ -209,7 +209,7 @@ def upsert_thesis_node(line_id, node_type, body, title=None, parent_id=None, ord
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(str(uuid.uuid4()), node_id, prev["line_id"], rev_no, prev["node_type"], prev["title"], prev["body"],
|
||||
prev["status"], prev["ord"], prev["variant_group"], prev["meta"], change_summary, change_reason,
|
||||
"agent", actor_id, claude_session_id, _now()))
|
||||
actor_type, actor_id, claude_session_id, _now()))
|
||||
c.execute("""UPDATE thesis_nodes SET node_type=?, title=COALESCE(?,title), body=?, ord=COALESCE(?,ord),
|
||||
variant_group=?, meta=COALESCE(?,meta), updated_at=? WHERE id=?""",
|
||||
(node_type, title, body, ord, variant_group, json.dumps(meta) if meta else None, _now(), node_id))
|
||||
@@ -280,6 +280,50 @@ def submit_version_for_review(version_id, db=None):
|
||||
return {"version_id": version_id, "status": "in_review"}
|
||||
|
||||
|
||||
def retire_node(node_id, actor_id="human", db=None):
|
||||
"""Soft-delete a node and its whole subtree (deleted_at). Reversible (guardrail #3);
|
||||
the node + descendants drop out of the tree but the rows + revision history remain."""
|
||||
c = _conn(db)
|
||||
row = c.execute("SELECT id FROM thesis_nodes WHERE id=? AND deleted_at IS NULL", (node_id,)).fetchone()
|
||||
if not row:
|
||||
c.close()
|
||||
return {"error": "not_found", "node_id": node_id}
|
||||
ids, frontier = [node_id], [node_id]
|
||||
while frontier:
|
||||
nxt = []
|
||||
for pid in frontier:
|
||||
for r in c.execute("SELECT id FROM thesis_nodes WHERE parent_id=? AND deleted_at IS NULL", (pid,)):
|
||||
ids.append(r["id"]); nxt.append(r["id"])
|
||||
frontier = nxt
|
||||
for nid in ids:
|
||||
c.execute("UPDATE thesis_nodes SET deleted_at=?, updated_at=? WHERE id=?", (_now(), _now(), nid))
|
||||
_log(c, "thesis.node_retired", node_id, {"count": len(ids)}, actor_id=actor_id, actor_type="human")
|
||||
c.commit()
|
||||
c.close()
|
||||
return {"retired": node_id, "count": len(ids)}
|
||||
|
||||
|
||||
def choose_variant(node_id, actor_id="human", db=None):
|
||||
"""'Use this option': keep this variant, soft-delete the OTHER variants in its group,
|
||||
and mark it approved. Collapses a node's competing options down to the chosen one."""
|
||||
c = _conn(db)
|
||||
node = c.execute("SELECT id, variant_group FROM thesis_nodes WHERE id=? AND deleted_at IS NULL", (node_id,)).fetchone()
|
||||
if not node:
|
||||
c.close()
|
||||
return {"error": "not_found", "node_id": node_id}
|
||||
retired = []
|
||||
if node["variant_group"]:
|
||||
for r in c.execute("SELECT id FROM thesis_nodes WHERE variant_group=? AND id<>? AND deleted_at IS NULL",
|
||||
(node["variant_group"], node_id)):
|
||||
c.execute("UPDATE thesis_nodes SET deleted_at=?, updated_at=? WHERE id=?", (_now(), _now(), r["id"]))
|
||||
retired.append(r["id"])
|
||||
c.execute("UPDATE thesis_nodes SET status='approved', updated_at=? WHERE id=?", (_now(), node_id))
|
||||
_log(c, "thesis.variant_chosen", node_id, {"retired_siblings": len(retired)}, actor_id=actor_id, actor_type="human")
|
||||
c.commit()
|
||||
c.close()
|
||||
return {"chosen": node_id, "retired_siblings": len(retired)}
|
||||
|
||||
|
||||
def upsert_segment(segment_key, name, definition=None, needs_to_hear=None, avoid=None, db=None):
|
||||
"""Create/replace a segment's active definition (retire the prior active row)."""
|
||||
c = _conn(db)
|
||||
|
||||
Reference in New Issue
Block a user