This commit is contained in:
parent
5e1d7a5b8c
commit
1e0d747ae4
2 changed files with 285 additions and 1 deletions
|
|
@ -89,6 +89,7 @@ def _load_packets(request: web.Request, filters: dict[str, Any]) -> list[dict[st
|
||||||
for proposal in proposals:
|
for proposal in proposals:
|
||||||
packet = proposal_review.classify_proposal(proposal)
|
packet = proposal_review.classify_proposal(proposal)
|
||||||
packet["normalization_preview"] = proposal_normalize.normalize_proposal(proposal)
|
packet["normalization_preview"] = proposal_normalize.normalize_proposal(proposal)
|
||||||
|
packet["apply_preview"] = build_apply_preview(proposal, packet)
|
||||||
packets.append(packet)
|
packets.append(packet)
|
||||||
return packets
|
return packets
|
||||||
|
|
||||||
|
|
@ -135,11 +136,243 @@ def _code(value: Any) -> str:
|
||||||
return f"<code>{escape(str(value))}</code>"
|
return f"<code>{escape(str(value))}</code>"
|
||||||
|
|
||||||
|
|
||||||
|
def _payload_dict(value: Any) -> dict[str, Any]:
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _payload_list(value: Any) -> list[Any]:
|
||||||
|
return value if isinstance(value, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_preview_row(
|
||||||
|
*,
|
||||||
|
action: str,
|
||||||
|
table: str,
|
||||||
|
target: str,
|
||||||
|
claim_ids: list[str] | None = None,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
source: str = "proposal",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"action": action,
|
||||||
|
"table": table,
|
||||||
|
"target": target,
|
||||||
|
"claim_ids": claim_ids or [],
|
||||||
|
"details": details or {},
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _strict_apply_preview_rows(
|
||||||
|
proposal_type: str | None,
|
||||||
|
apply_payload: dict[str, Any],
|
||||||
|
*,
|
||||||
|
source: str = "proposal.apply_payload",
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if proposal_type == "add_edge":
|
||||||
|
from_claim = str(apply_payload.get("from_claim") or "")
|
||||||
|
to_claim = str(apply_payload.get("to_claim") or "")
|
||||||
|
edge_type = str(apply_payload.get("edge_type") or "")
|
||||||
|
return [
|
||||||
|
_apply_preview_row(
|
||||||
|
action="insert_if_missing",
|
||||||
|
table="public.claim_edges",
|
||||||
|
target=f"{from_claim} -> {to_claim} ({edge_type or 'edge_type missing'})",
|
||||||
|
claim_ids=[claim_id for claim_id in [from_claim, to_claim] if claim_id],
|
||||||
|
details={
|
||||||
|
"from_claim": from_claim,
|
||||||
|
"to_claim": to_claim,
|
||||||
|
"edge_type": edge_type,
|
||||||
|
"weight": apply_payload.get("weight"),
|
||||||
|
},
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if proposal_type == "attach_evidence":
|
||||||
|
rows = []
|
||||||
|
for evidence in _payload_list(apply_payload.get("evidence")):
|
||||||
|
ev = _payload_dict(evidence)
|
||||||
|
claim_id = str(ev.get("claim_id") or "")
|
||||||
|
source_id = str(ev.get("source_id") or "")
|
||||||
|
role = str(ev.get("role") or "grounds")
|
||||||
|
rows.append(
|
||||||
|
_apply_preview_row(
|
||||||
|
action="insert_if_missing",
|
||||||
|
table="public.claim_evidence",
|
||||||
|
target=f"{claim_id} <= {source_id} ({role})",
|
||||||
|
claim_ids=[claim_id] if claim_id else [],
|
||||||
|
details={
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"source_id": source_id,
|
||||||
|
"role": role,
|
||||||
|
"weight": ev.get("weight"),
|
||||||
|
},
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
if proposal_type == "revise_strategy":
|
||||||
|
agent_id = str(apply_payload.get("agent_id") or "")
|
||||||
|
strategy_nodes = _payload_list(apply_payload.get("strategy_nodes"))
|
||||||
|
return [
|
||||||
|
_apply_preview_row(
|
||||||
|
action="update_then_insert",
|
||||||
|
table="public.strategies",
|
||||||
|
target=f"new active strategy for agent {agent_id or 'unknown'}",
|
||||||
|
details={
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"strategy_keys": sorted(_payload_dict(apply_payload.get("strategy")).keys()),
|
||||||
|
},
|
||||||
|
source=source,
|
||||||
|
),
|
||||||
|
_apply_preview_row(
|
||||||
|
action="retire_then_insert",
|
||||||
|
table="public.strategy_nodes",
|
||||||
|
target=f"{len(strategy_nodes)} replacement strategy node(s)",
|
||||||
|
details={"agent_id": agent_id, "strategy_node_count": len(strategy_nodes)},
|
||||||
|
source=source,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return [
|
||||||
|
_apply_preview_row(
|
||||||
|
action="blocked",
|
||||||
|
table="none",
|
||||||
|
target=f"unsupported proposal_type {proposal_type or 'unknown'}",
|
||||||
|
details={"proposal_type": proposal_type},
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_apply_preview(proposal: dict[str, Any], packet: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Build a read-only, row-level preview of what an apply step would touch."""
|
||||||
|
payload = _payload_dict(proposal.get("payload"))
|
||||||
|
apply_payload = _payload_dict(payload.get("apply_payload"))
|
||||||
|
proposal_type = proposal.get("proposal_type")
|
||||||
|
normalization = _payload_dict(packet.get("normalization_preview"))
|
||||||
|
|
||||||
|
if proposal.get("status") == "applied":
|
||||||
|
return {
|
||||||
|
"state": "applied",
|
||||||
|
"rows": [],
|
||||||
|
"blocked_fragments": [],
|
||||||
|
"note": "Proposal is already applied; use the canonical claim/edge/evidence readback.",
|
||||||
|
}
|
||||||
|
|
||||||
|
if apply_payload and proposal_type in proposal_review.ap.APPLYABLE_TYPES:
|
||||||
|
return {
|
||||||
|
"state": "strict_apply_payload_ready",
|
||||||
|
"rows": _strict_apply_preview_rows(proposal_type, apply_payload),
|
||||||
|
"blocked_fragments": [],
|
||||||
|
"note": "Preview only; the page does not execute canonical writes.",
|
||||||
|
}
|
||||||
|
|
||||||
|
child_rows = []
|
||||||
|
for index, child in enumerate(_payload_list(normalization.get("strict_child_proposals"))):
|
||||||
|
child_payload = _payload_dict(_payload_dict(child).get("payload"))
|
||||||
|
child_apply_payload = _payload_dict(child_payload.get("apply_payload"))
|
||||||
|
child_type = str(_payload_dict(child).get("proposal_type") or "")
|
||||||
|
child_rows.extend(
|
||||||
|
_strict_apply_preview_rows(
|
||||||
|
child_type,
|
||||||
|
child_apply_payload,
|
||||||
|
source=f"normalization.strict_child_proposals[{index}]",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
blocked = _payload_list(normalization.get("blocked_fragments"))
|
||||||
|
if child_rows and not blocked:
|
||||||
|
state = "strict_child_proposals_ready"
|
||||||
|
note = "Preview of strict child proposals; reviewer still needs to stage those children before apply."
|
||||||
|
elif child_rows:
|
||||||
|
state = "partial_preview_blocked_fragments"
|
||||||
|
note = "Some strict child rows can be previewed; blocked fragments still need canonical IDs or schema decisions."
|
||||||
|
else:
|
||||||
|
state = "not_applyable_yet"
|
||||||
|
note = "No canonical write preview is safe until the proposal has a strict apply_payload or strict child proposals."
|
||||||
|
|
||||||
|
return {
|
||||||
|
"state": state,
|
||||||
|
"rows": child_rows,
|
||||||
|
"blocked_fragments": blocked,
|
||||||
|
"note": note,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _status_link(label: str, status: str, filters: dict[str, Any]) -> str:
|
def _status_link(label: str, status: str, filters: dict[str, Any]) -> str:
|
||||||
query = urlencode({"status": status, "limit": filters["limit"]})
|
query = urlencode({"status": status, "limit": filters["limit"]})
|
||||||
return f'<a class="filter-link" href="/kb-proposals?{query}">{escape(label)}</a>'
|
return f'<a class="filter-link" href="/kb-proposals?{query}">{escape(label)}</a>'
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_preview_html(preview: dict[str, Any]) -> str:
|
||||||
|
state = _badge(preview.get("state"))
|
||||||
|
rows = _payload_list(preview.get("rows"))
|
||||||
|
blocked = _payload_list(preview.get("blocked_fragments"))
|
||||||
|
note = escape(str(preview.get("note") or ""))
|
||||||
|
row_html = ""
|
||||||
|
if rows:
|
||||||
|
rendered_rows = []
|
||||||
|
for row in rows:
|
||||||
|
details = _payload_dict(_payload_dict(row).get("details"))
|
||||||
|
detail_text = ", ".join(
|
||||||
|
f"{escape(str(key))}={escape(str(value))}"
|
||||||
|
for key, value in details.items()
|
||||||
|
if value not in (None, "")
|
||||||
|
)
|
||||||
|
claim_links = " ".join(
|
||||||
|
claim_link_html(claim_id, label=str(claim_id)[:8])
|
||||||
|
for claim_id in _payload_list(_payload_dict(row).get("claim_ids"))
|
||||||
|
) or '<span class="muted">none</span>'
|
||||||
|
detail_html = escape(detail_text) if detail_text else '<span class="muted">none</span>'
|
||||||
|
rendered_rows.append(
|
||||||
|
"<tr>"
|
||||||
|
f"<td>{_code(_payload_dict(row).get('action') or 'unknown')}</td>"
|
||||||
|
f"<td>{_code(_payload_dict(row).get('table') or 'unknown')}</td>"
|
||||||
|
f"<td>{escape(str(_payload_dict(row).get('target') or ''))}</td>"
|
||||||
|
f"<td>{claim_links}</td>"
|
||||||
|
f"<td>{detail_html}</td>"
|
||||||
|
"</tr>"
|
||||||
|
)
|
||||||
|
row_html = f"""
|
||||||
|
<table class="proposal-detail apply-preview-table">
|
||||||
|
<thead><tr><th>Action</th><th>Table</th><th>Target</th><th>Claims</th><th>Details</th></tr></thead>
|
||||||
|
<tbody>{''.join(rendered_rows)}</tbody>
|
||||||
|
</table>"""
|
||||||
|
else:
|
||||||
|
row_html = '<p class="muted">No canonical row preview is executable yet.</p>'
|
||||||
|
|
||||||
|
blocked_html = ""
|
||||||
|
if blocked:
|
||||||
|
blocked_rows = []
|
||||||
|
for fragment in blocked:
|
||||||
|
frag = _payload_dict(fragment)
|
||||||
|
missing = ", ".join(escape(str(value)) for value in _payload_list(frag.get("missing"))) or "unknown"
|
||||||
|
blocked_rows.append(
|
||||||
|
"<tr>"
|
||||||
|
f"<td>{_code(frag.get('kind') or 'fragment')}</td>"
|
||||||
|
f"<td>{escape(str(frag.get('reason') or 'blocked'))}</td>"
|
||||||
|
f"<td>{missing}</td>"
|
||||||
|
"</tr>"
|
||||||
|
)
|
||||||
|
blocked_html = f"""
|
||||||
|
<div class="label preview-label">Blocked fragments</div>
|
||||||
|
<table class="proposal-detail apply-preview-table">
|
||||||
|
<thead><tr><th>Kind</th><th>Reason</th><th>Missing</th></tr></thead>
|
||||||
|
<tbody>{''.join(blocked_rows)}</tbody>
|
||||||
|
</table>"""
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<div class="next-action apply-preview">
|
||||||
|
<div class="label">Apply preview</div>
|
||||||
|
<p>{state} <span class="muted">{note}</span></p>
|
||||||
|
{row_html}
|
||||||
|
{blocked_html}
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
|
||||||
def _packet_card(packet: dict[str, Any]) -> str:
|
def _packet_card(packet: dict[str, Any]) -> str:
|
||||||
auth = packet.get("identity_and_authorization") or {}
|
auth = packet.get("identity_and_authorization") or {}
|
||||||
summary = packet.get("payload_summary") or {}
|
summary = packet.get("payload_summary") or {}
|
||||||
|
|
@ -186,6 +419,7 @@ def _packet_card(packet: dict[str, Any]) -> str:
|
||||||
<div class="label">Normalization action</div>
|
<div class="label">Normalization action</div>
|
||||||
<p>{escape(str(normalization.get("next_normalization_action") or "No normalization action recorded."))}</p>
|
<p>{escape(str(normalization.get("next_normalization_action") or "No normalization action recorded."))}</p>
|
||||||
</div>"""
|
</div>"""
|
||||||
|
apply_preview_html = _apply_preview_html(packet.get("apply_preview") or {})
|
||||||
return f"""
|
return f"""
|
||||||
<article class="proposal-card">
|
<article class="proposal-card">
|
||||||
<div class="proposal-card-header">
|
<div class="proposal-card-header">
|
||||||
|
|
@ -199,6 +433,7 @@ def _packet_card(packet: dict[str, Any]) -> str:
|
||||||
<p>{next_action}</p>
|
<p>{next_action}</p>
|
||||||
</div>
|
</div>
|
||||||
{normalization_html}
|
{normalization_html}
|
||||||
|
{apply_preview_html}
|
||||||
</article>"""
|
</article>"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,37 @@ def test_api_kb_proposals_returns_review_packets_and_counts():
|
||||||
assert data["packets"][0]["identity_and_authorization"]["source_ref"] == "telegram:leo:m3taversal:claim-review"
|
assert data["packets"][0]["identity_and_authorization"]["source_ref"] == "telegram:leo:m3taversal:claim-review"
|
||||||
assert data["packets"][0]["normalization_preview"]["normalization_state"] == "blocked_missing_canonical_ids"
|
assert data["packets"][0]["normalization_preview"]["normalization_state"] == "blocked_missing_canonical_ids"
|
||||||
assert data["packets"][1]["normalization_preview"]["normalization_state"] == "already_strict_apply_payload"
|
assert data["packets"][1]["normalization_preview"]["normalization_state"] == "already_strict_apply_payload"
|
||||||
|
assert data["packets"][0]["apply_preview"]["state"] == "not_applyable_yet"
|
||||||
|
assert data["packets"][0]["apply_preview"]["blocked_fragments"][0]["missing"] == [
|
||||||
|
"canonical from_claim",
|
||||||
|
"canonical to_claim",
|
||||||
|
]
|
||||||
|
assert data["packets"][1]["apply_preview"]["state"] == "strict_apply_payload_ready"
|
||||||
|
assert data["packets"][1]["apply_preview"]["rows"] == [
|
||||||
|
{
|
||||||
|
"action": "insert_if_missing",
|
||||||
|
"table": "public.claim_edges",
|
||||||
|
"target": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb -> cccccccc-cccc-cccc-cccc-cccccccccccc (supports)",
|
||||||
|
"claim_ids": [
|
||||||
|
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||||
|
"cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||||
|
],
|
||||||
|
"details": {
|
||||||
|
"from_claim": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||||
|
"to_claim": "cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||||
|
"edge_type": "supports",
|
||||||
|
"weight": None,
|
||||||
|
},
|
||||||
|
"source": "proposal.apply_payload",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_page_renders_applyability_and_next_admin_action():
|
def test_page_renders_applyability_and_next_admin_action():
|
||||||
|
proposal = _approved_freeform_proposal()
|
||||||
packet = routes.proposal_review.classify_proposal(_approved_freeform_proposal())
|
packet = routes.proposal_review.classify_proposal(_approved_freeform_proposal())
|
||||||
packet["normalization_preview"] = routes.proposal_normalize.normalize_proposal(_approved_freeform_proposal())
|
packet["normalization_preview"] = routes.proposal_normalize.normalize_proposal(proposal)
|
||||||
|
packet["apply_preview"] = routes.build_apply_preview(proposal, packet)
|
||||||
data = routes.build_kb_proposal_response(
|
data = routes.build_kb_proposal_response(
|
||||||
[packet],
|
[packet],
|
||||||
{"status": "approved", "proposal_id": "", "limit": 20},
|
{"status": "approved", "proposal_id": "", "limit": 20},
|
||||||
|
|
@ -105,6 +131,29 @@ def test_page_renders_applyability_and_next_admin_action():
|
||||||
assert "blocked_missing_canonical_ids" in html
|
assert "blocked_missing_canonical_ids" in html
|
||||||
assert 'href="/kb/claims/d0000000-0000-0000-0000-000000000005"' in html
|
assert 'href="/kb/claims/d0000000-0000-0000-0000-000000000005"' in html
|
||||||
assert "<code>d0000000-0000-0000-0000-000000000005</code>" in html
|
assert "<code>d0000000-0000-0000-0000-000000000005</code>" in html
|
||||||
|
assert "Apply preview" in html
|
||||||
|
assert "not_applyable_yet" in html
|
||||||
|
assert "No canonical row preview is executable yet." in html
|
||||||
|
assert "canonical from_claim" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_page_renders_strict_apply_payload_claim_links():
|
||||||
|
proposal = _approved_applyable_proposal()
|
||||||
|
packet = routes.proposal_review.classify_proposal(proposal)
|
||||||
|
packet["normalization_preview"] = routes.proposal_normalize.normalize_proposal(proposal)
|
||||||
|
packet["apply_preview"] = routes.build_apply_preview(proposal, packet)
|
||||||
|
data = routes.build_kb_proposal_response(
|
||||||
|
[packet],
|
||||||
|
{"status": "approved", "proposal_id": "", "limit": 20},
|
||||||
|
)
|
||||||
|
|
||||||
|
html = routes.render_kb_proposals_page(data)
|
||||||
|
|
||||||
|
assert "strict_apply_payload_ready" in html
|
||||||
|
assert "public.claim_edges" in html
|
||||||
|
assert "insert_if_missing" in html
|
||||||
|
assert 'href="/kb/claims/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"' in html
|
||||||
|
assert 'href="/kb/claims/cccccccc-cccc-cccc-cccc-cccccccccccc"' in html
|
||||||
|
|
||||||
|
|
||||||
def test_api_kb_proposals_supports_proposal_id_filter():
|
def test_api_kb_proposals_supports_proposal_id_filter():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue