Fix Leo direct-claim KB proposal readback
Some checks are pending
CI / lint-and-test (push) Waiting to run

This commit is contained in:
twentyOne2x 2026-07-10 02:16:39 +02:00
parent 25057e29c2
commit dbb79b22bc
7 changed files with 442 additions and 1 deletions

View file

@ -124,10 +124,22 @@ def parse_args() -> argparse.Namespace:
list_proposals.add_argument("--limit", type=int, default=10) list_proposals.add_argument("--limit", type=int, default=10)
list_proposals.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS) list_proposals.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS)
search_proposals = sub.add_parser("search-proposals", help="Search staged KB proposals across statuses from Cloud SQL.")
search_proposals.add_argument("query")
search_proposals.add_argument("--status", default="all")
search_proposals.add_argument("--limit", type=int, default=10)
search_proposals.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS)
show_proposal = sub.add_parser("show-proposal", help="Show one staged KB proposal from Cloud SQL.") show_proposal = sub.add_parser("show-proposal", help="Show one staged KB proposal from Cloud SQL.")
show_proposal.add_argument("proposal_id") show_proposal.add_argument("proposal_id")
show_proposal.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS) show_proposal.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS)
decision_matrix_status = sub.add_parser(
"decision-matrix-status",
help="Read whether decision-matrix approval tables exist before claiming matrix approval.",
)
decision_matrix_status.add_argument("--format", choices=["markdown", "json"], default=argparse.SUPPRESS)
return parser.parse_args() return parser.parse_args()
@ -966,6 +978,55 @@ limit {max(1, min(args.limit, 100))};
} }
def search_proposals(args: argparse.Namespace) -> dict[str, Any]:
terms = terms_for(args.query)
patterns = sql_array([f"%{term}%" for term in terms], "text")
status_clause = ""
if args.status.lower() != "all":
status_clause = f"and status = {sql_literal(args.status)}"
sql = f"""
select jsonb_build_object(
'id', id::text,
'proposal_type', proposal_type,
'status', status,
'proposed_by_handle', proposed_by_handle,
'channel', channel,
'source_ref', source_ref,
'rationale', left(rationale, 700),
'payload', payload,
'created_at', created_at::text,
'reviewed_at', reviewed_at::text,
'applied_at', applied_at::text
)::text
from kb_stage.kb_proposals
where (
coalesce(rationale, '') ilike any({patterns})
or coalesce(source_ref, '') ilike any({patterns})
or coalesce(proposal_type, '') ilike any({patterns})
or coalesce(proposed_by_handle, '') ilike any({patterns})
or payload::text ilike any({patterns})
)
{status_clause}
order by
case status
when 'approved' then 0
when 'pending_review' then 1
when 'applied' then 2
else 3
end,
created_at desc
limit {max(1, min(args.limit, 100))};
"""
return {
"artifact": "teleo_cloudsql_kb_proposal_search",
"backend": canonical_backend(args),
"query": args.query,
"terms": terms,
"status_filter": args.status,
"proposals": psql_json_lines(args, sql, db=args.canonical_db),
}
def show_proposal(args: argparse.Namespace) -> dict[str, Any]: def show_proposal(args: argparse.Namespace) -> dict[str, Any]:
sql = f""" sql = f"""
select jsonb_build_object( select jsonb_build_object(
@ -997,6 +1058,54 @@ where id = {sql_literal(args.proposal_id)}::uuid;
return rows[0] return rows[0]
def decision_matrix_status(args: argparse.Namespace) -> dict[str, Any]:
sql = """
with table_status(schema_name, table_name, exists_in_cloudsql) as (
values
('public', 'matrix_voters', to_regclass('public.matrix_voters') is not null),
('public', 'proposal_votes', to_regclass('public.proposal_votes') is not null),
('public', 'proposal_decisions', to_regclass('public.proposal_decisions') is not null),
('kb_stage', 'matrix_voters', to_regclass('kb_stage.matrix_voters') is not null),
('kb_stage', 'proposal_votes', to_regclass('kb_stage.proposal_votes') is not null),
('kb_stage', 'proposal_decisions', to_regclass('kb_stage.proposal_decisions') is not null)
),
status_counts as (
select coalesce(jsonb_object_agg(status, proposal_count order by status), '{}'::jsonb) as counts
from (
select status, count(*) as proposal_count
from kb_stage.kb_proposals
group by status
) grouped
)
select jsonb_build_object(
'artifact', 'teleo_cloudsql_kb_decision_matrix_status',
'backend', current_database() || '|' || current_user,
'decision_matrix_tables', (
select jsonb_object_agg(schema_name || '.' || table_name, exists_in_cloudsql order by schema_name, table_name)
from table_status
),
'all_required_tables_present', (
select bool_and(exists_in_cloudsql) from table_status
),
'any_decision_matrix_table_present', (
select bool_or(exists_in_cloudsql) from table_status
),
'proposal_status_counts', (select counts from status_counts),
'guidance', case
when (select bool_and(exists_in_cloudsql) from table_status)
then 'Decision-matrix tables exist; inspect proposal_decisions/proposal_votes for the specific proposal before claiming approval.'
else 'Decision-matrix approval schema is absent or incomplete; do not infer matrix approval from kb_stage proposal rationale/status alone.'
end
)::text;
"""
rows = psql_json_lines(args, sql, db=args.canonical_db)
if not rows:
raise SystemExit("No decision matrix status returned.")
result = rows[0]
result["backend"] = canonical_backend(args)
return result
def status(args: argparse.Namespace) -> dict[str, Any]: def status(args: argparse.Namespace) -> dict[str, Any]:
canonical_sql = """ canonical_sql = """
select json_build_object( select json_build_object(
@ -1173,6 +1282,7 @@ def emit_markdown(value: dict[str, Any]) -> None:
if value["artifact"] == "teleo_cloudsql_kb_proposal_list": if value["artifact"] == "teleo_cloudsql_kb_proposal_list":
print("# Staged KB Proposals\n") print("# Staged KB Proposals\n")
print(f"- Status filter: `{value.get('status_filter')}`\n")
rows = value.get("proposals") or [] rows = value.get("proposals") or []
if not rows: if not rows:
print("No matching proposals.") print("No matching proposals.")
@ -1186,6 +1296,25 @@ def emit_markdown(value: dict[str, Any]) -> None:
print() print()
return return
if value["artifact"] == "teleo_cloudsql_kb_proposal_search":
print("# Staged KB Proposal Search\n")
print(f"- Query: `{value['query']}`")
print(f"- Terms: `{', '.join(value['terms'])}`")
print(f"- Status filter: `{value['status_filter']}`\n")
rows = value.get("proposals") or []
if not rows:
print("No matching proposals.")
for row in rows:
print(f"## {row['id']}\n")
print(f"- Type: `{row['proposal_type']}`")
print(f"- Status: `{row['status']}`")
print(f"- Source: `{row.get('source_ref') or '-'}`")
print(f"- Created: `{row.get('created_at')}`")
print(f"- Applied at: `{row.get('applied_at') or '-'}`")
print(f"- Rationale: {row.get('rationale') or ''}")
print()
return
if value["artifact"] == "teleo_cloudsql_kb_proposal": if value["artifact"] == "teleo_cloudsql_kb_proposal":
print("# Staged KB Proposal\n") print("# Staged KB Proposal\n")
print(f"- Proposal id: `{value['id']}`") print(f"- Proposal id: `{value['id']}`")
@ -1200,6 +1329,19 @@ def emit_markdown(value: dict[str, Any]) -> None:
print(json.dumps(value.get("payload") or {}, indent=2, sort_keys=True)) print(json.dumps(value.get("payload") or {}, indent=2, sort_keys=True))
return return
if value["artifact"] == "teleo_cloudsql_kb_decision_matrix_status":
print("# Decision Matrix Status\n")
print(f"- Backend: `{value['backend']}`")
print(f"- all required tables present: `{value['all_required_tables_present']}`")
print(f"- any decision-matrix table present: `{value['any_decision_matrix_table_present']}`")
print(f"- proposal status counts: `{json.dumps(value.get('proposal_status_counts') or {}, sort_keys=True)}`")
print(f"- guidance: {value['guidance']}")
print("\n## Tables\n")
for table, exists in sorted((value.get("decision_matrix_tables") or {}).items()):
print(f"- `{table}`: `{exists}`")
print()
return
print("# Teleo Cloud SQL Memory Context\n") print("# Teleo Cloud SQL Memory Context\n")
print(f"- Backend: `{value['backend']}`") print(f"- Backend: `{value['backend']}`")
print(f"- Query: `{value['query']}`") print(f"- Query: `{value['query']}`")
@ -1234,8 +1376,12 @@ def main() -> None:
result = propose_edge(args) result = propose_edge(args)
elif args.command == "list-proposals": elif args.command == "list-proposals":
result = list_proposals(args) result = list_proposals(args)
elif args.command == "search-proposals":
result = search_proposals(args)
elif args.command == "show-proposal": elif args.command == "show-proposal":
result = show_proposal(args) result = show_proposal(args)
elif args.command == "decision-matrix-status":
result = decision_matrix_status(args)
else: else:
result = query_rows(args, args.query, args.limit) result = query_rows(args, args.query, args.limit)
if args.format == "json": if args.format == "json":

View file

@ -163,10 +163,22 @@ def parse_args() -> argparse.Namespace:
list_proposals.add_argument("--limit", type=int, default=10) list_proposals.add_argument("--limit", type=int, default=10)
add_output_flag(list_proposals) add_output_flag(list_proposals)
search_proposals = sub.add_parser("search-proposals", help="Search KB mutation proposals across statuses.")
search_proposals.add_argument("query")
search_proposals.add_argument("--status", default="all")
search_proposals.add_argument("--limit", type=int, default=10)
add_output_flag(search_proposals)
show_proposal = sub.add_parser("show-proposal", help="Show one KB mutation proposal.") show_proposal = sub.add_parser("show-proposal", help="Show one KB mutation proposal.")
show_proposal.add_argument("proposal_id") show_proposal.add_argument("proposal_id")
add_output_flag(show_proposal) add_output_flag(show_proposal)
decision_matrix_status = sub.add_parser(
"decision-matrix-status",
help="Read whether decision-matrix approval tables exist before claiming matrix approval.",
)
add_output_flag(decision_matrix_status)
return parser.parse_args() return parser.parse_args()
@ -647,6 +659,53 @@ def list_proposals(args: argparse.Namespace) -> list[dict[str, Any]]:
return psql_json(args, sql) return psql_json(args, sql)
def search_proposals(args: argparse.Namespace) -> dict[str, Any]:
terms = query_terms(args.query)
patterns = sql_array([f"%{term}%" for term in terms], "text")
status_clause = ""
if args.status.lower() != "all":
status_clause = f"and status = {sql_literal(args.status)}"
sql = f"""
select jsonb_build_object(
'id', id::text,
'proposal_type', proposal_type,
'status', status,
'proposed_by_handle', proposed_by_handle,
'channel', channel,
'source_ref', source_ref,
'rationale', left(rationale, 700),
'payload', payload,
'created_at', created_at::text,
'reviewed_at', reviewed_at::text,
'applied_at', applied_at::text
)::text
from kb_stage.kb_proposals
where (
coalesce(rationale, '') ilike any({patterns})
or coalesce(source_ref, '') ilike any({patterns})
or coalesce(proposal_type, '') ilike any({patterns})
or coalesce(proposed_by_handle, '') ilike any({patterns})
or payload::text ilike any({patterns})
)
{status_clause}
order by
case status
when 'approved' then 0
when 'pending_review' then 1
when 'applied' then 2
else 3
end,
created_at desc
limit {max(1, min(args.limit, 100))};
"""
return {
"query": args.query,
"terms": terms,
"status_filter": args.status,
"proposals": psql_json(args, sql),
}
def show_proposal(args: argparse.Namespace) -> dict[str, Any] | None: def show_proposal(args: argparse.Namespace) -> dict[str, Any] | None:
sql = f""" sql = f"""
select jsonb_build_object( select jsonb_build_object(
@ -674,6 +733,51 @@ def show_proposal(args: argparse.Namespace) -> dict[str, Any] | None:
return rows[0] if rows else None return rows[0] if rows else None
def decision_matrix_status(args: argparse.Namespace) -> dict[str, Any]:
sql = """
with table_status(schema_name, table_name, exists_on_vps) as (
values
('public', 'matrix_voters', to_regclass('public.matrix_voters') is not null),
('public', 'proposal_votes', to_regclass('public.proposal_votes') is not null),
('public', 'proposal_decisions', to_regclass('public.proposal_decisions') is not null),
('kb_stage', 'matrix_voters', to_regclass('kb_stage.matrix_voters') is not null),
('kb_stage', 'proposal_votes', to_regclass('kb_stage.proposal_votes') is not null),
('kb_stage', 'proposal_decisions', to_regclass('kb_stage.proposal_decisions') is not null)
),
status_counts as (
select coalesce(jsonb_object_agg(status, proposal_count order by status), '{}'::jsonb) as counts
from (
select status, count(*) as proposal_count
from kb_stage.kb_proposals
group by status
) grouped
)
select jsonb_build_object(
'artifact', 'teleo_kb_decision_matrix_status',
'decision_matrix_tables', (
select jsonb_object_agg(schema_name || '.' || table_name, exists_on_vps order by schema_name, table_name)
from table_status
),
'all_required_tables_present', (
select bool_and(exists_on_vps) from table_status
),
'any_decision_matrix_table_present', (
select bool_or(exists_on_vps) from table_status
),
'proposal_status_counts', (select counts from status_counts),
'guidance', case
when (select bool_and(exists_on_vps) from table_status)
then 'Decision-matrix tables exist; inspect proposal_decisions/proposal_votes for the specific proposal before claiming approval.'
else 'Decision-matrix approval schema is absent or incomplete; do not infer matrix approval from kb_stage proposal rationale/status alone.'
end
)::text;
"""
rows = psql_json(args, sql)
if not rows:
raise SystemExit("No decision matrix status returned.")
return rows[0]
def load_evidence(args: argparse.Namespace, claim_ids: list[str], limit: int) -> dict[str, list[dict[str, Any]]]: def load_evidence(args: argparse.Namespace, claim_ids: list[str], limit: int) -> dict[str, list[dict[str, Any]]]:
if not claim_ids: if not claim_ids:
return {} return {}
@ -882,6 +986,27 @@ def print_proposal_list(rows: list[dict[str, Any]]) -> None:
print() print()
def print_proposal_search(data: dict[str, Any]) -> None:
print("# KB Proposal Search\n")
print(f"- query: `{data['query']}`")
print(f"- terms: `{', '.join(data['terms'])}`")
print(f"- status filter: `{data['status_filter']}`")
print()
print_proposal_list(data["proposals"])
def print_decision_matrix_status(data: dict[str, Any]) -> None:
print("# Decision Matrix Status\n")
print(f"- all required tables present: `{data['all_required_tables_present']}`")
print(f"- any decision-matrix table present: `{data['any_decision_matrix_table_present']}`")
print(f"- proposal status counts: `{json.dumps(data.get('proposal_status_counts') or {}, sort_keys=True)}`")
print(f"- guidance: {data['guidance']}")
print("\n## Tables\n")
for table, exists in sorted((data.get("decision_matrix_tables") or {}).items()):
print(f"- `{table}`: `{exists}`")
print()
def main() -> int: def main() -> int:
args = parse_args() args = parse_args()
if args.command == "search": if args.command == "search":
@ -920,11 +1045,15 @@ def main() -> int:
data = record_document_evaluation(args) data = record_document_evaluation(args)
elif args.command == "list-proposals": elif args.command == "list-proposals":
data = {"proposals": list_proposals(args)} data = {"proposals": list_proposals(args)}
elif args.command == "search-proposals":
data = search_proposals(args)
elif args.command == "show-proposal": elif args.command == "show-proposal":
proposal = show_proposal(args) proposal = show_proposal(args)
if not proposal: if not proposal:
raise SystemExit(f"proposal not found: {args.proposal_id}") raise SystemExit(f"proposal not found: {args.proposal_id}")
data = proposal data = proposal
elif args.command == "decision-matrix-status":
data = decision_matrix_status(args)
else: else:
raise AssertionError(args.command) raise AssertionError(args.command)
@ -946,8 +1075,12 @@ def main() -> int:
print_json(data) print_json(data)
elif args.command == "list-proposals": elif args.command == "list-proposals":
print_proposal_list(data["proposals"]) print_proposal_list(data["proposals"])
elif args.command == "search-proposals":
print_proposal_search(data)
elif args.command == "show-proposal": elif args.command == "show-proposal":
print_proposal(data) print_proposal(data)
elif args.command == "decision-matrix-status":
print_decision_matrix_status(data)
else: else:
print_json(data) print_json(data)
return 0 return 0

View file

@ -10,7 +10,7 @@ CLOUDSQL_STATUS_TIMEOUT=${TELEO_CLOUDSQL_STATUS_TIMEOUT_SECONDS:-30}
cloudsql_supported() { cloudsql_supported() {
case "$COMMAND" in case "$COMMAND" in
status|search|context|show|evidence|edges|propose-core-change|propose-edge|list-proposals|show-proposal) return 0 ;; status|search|context|show|evidence|edges|propose-core-change|propose-edge|list-proposals|search-proposals|show-proposal|decision-matrix-status) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }

View file

@ -35,7 +35,9 @@ Use narrower bridge commands when needed:
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb evidence <claim_id> /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb evidence <claim_id>
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb edges <claim_id> /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb edges <claim_id>
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb list-proposals /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb list-proposals
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb search-proposals "<terms>"
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb show-proposal <proposal_id> /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb show-proposal <proposal_id>
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb decision-matrix-status
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb propose-core-change --proposal-type revise_claim --target-kind claim --target-ref "<claim id or description>" --proposed "<replacement>" --source-ref "<telegram/chat/source ref>" --rationale "<why this should be reviewed>" /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb propose-core-change --proposal-type revise_claim --target-kind claim --target-ref "<claim id or description>" --proposed "<replacement>" --source-ref "<telegram/chat/source ref>" --rationale "<why this should be reviewed>"
``` ```
@ -50,6 +52,25 @@ good default is:
3. final answer with what is grounded, what is weak, and what evidence would 3. final answer with what is grounded, what is weak, and what evidence would
improve it. improve it.
For no-context direct claims such as "Is X in Leo now?", "did the DB change?",
"did the decision matrix approve this?", or "is it still just proposals?", do
not stop at `search` or default `list-proposals`. Run the status-specific
proposal and governance readbacks needed to avoid overclaiming:
```bash
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb search-proposals "<entity/framework/claim terms>" --status all --limit 20
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb decision-matrix-status
```
If `decision-matrix-status` says the matrix tables are absent or incomplete,
do not infer matrix approval from proposal rationale, reviewer notes, or
`kb_stage.kb_proposals.status`. Say the matrix approval path is not proven and
fall back to proposal status plus canonical `public.*` readback.
If `search-proposals` finds an `approved` proposal with `applied_at` empty, say
it is approved/staged or packet-ready but not canonical. Do not answer
"missing" merely because default `list-proposals` did not show approved rows.
## Telegram Rendering ## Telegram Rendering
Make KB answers easy to scan in Telegram: Make KB answers easy to scan in Telegram:

View file

@ -29,7 +29,9 @@ Use narrower bridge commands when needed:
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb evidence <claim_id> /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb evidence <claim_id>
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb edges <claim_id> /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb edges <claim_id>
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb list-proposals /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb list-proposals
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb search-proposals "<terms>"
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb show-proposal <proposal_id> /home/teleo/.hermes/profiles/leoclean/bin/teleo-kb show-proposal <proposal_id>
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb decision-matrix-status
``` ```
## Answer Discipline ## Answer Discipline
@ -42,6 +44,25 @@ For KB questions, prefer the bridge over raw database access. A good default is:
3. final answer with what is grounded, what is weak, and what evidence or 3. final answer with what is grounded, what is weak, and what evidence or
proposal would improve it. proposal would improve it.
For no-context direct claims such as "Is X in Leo now?", "did the DB change?",
"did the decision matrix approve this?", or "is it still just proposals?", do
not stop at `search` or default `list-proposals`. Run the status-specific
proposal and governance readbacks needed to avoid overclaiming:
```bash
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb search-proposals "<entity/framework/claim terms>" --status all --limit 20
/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb decision-matrix-status
```
If `decision-matrix-status` says the matrix tables are absent or incomplete,
do not infer matrix approval from proposal rationale, reviewer notes, or
`kb_stage.kb_proposals.status`. Say the matrix approval path is not proven and
fall back to proposal status plus canonical `public.*` readback.
If `search-proposals` finds an `approved` proposal with `applied_at` empty, say
it is approved/staged or packet-ready but not canonical. Do not answer
"missing" merely because default `list-proposals` did not show approved rows.
## Telegram Rendering ## Telegram Rendering
Make KB answers easy to scan in Telegram: Make KB answers easy to scan in Telegram:

View file

@ -9,6 +9,7 @@ import io
import re import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin" BRIDGE_DIR = ROOT / "hermes-agent" / "leoclean-bin"
@ -42,7 +43,9 @@ def test_cloudsql_wrapper_supports_expected_review_gated_commands() -> None:
"propose-core-change", "propose-core-change",
"propose-edge", "propose-edge",
"list-proposals", "list-proposals",
"search-proposals",
"show-proposal", "show-proposal",
"decision-matrix-status",
]: ]:
assert command in wrapper_text assert command in wrapper_text
assert command in cloudsql_text assert command in cloudsql_text
@ -85,6 +88,114 @@ def _load_module(path: Path):
return module return module
def test_vps_bridge_search_proposals_finds_approved_rows_by_payload(monkeypatch) -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
captured_sql: list[str] = []
def fake_psql_json(_args, sql):
captured_sql.append(sql)
return [{"id": "a64df080-8502-42e2-98f4-9bbdecb8da73", "status": "approved"}]
monkeypatch.setattr(module, "psql_json", fake_psql_json)
args = SimpleNamespace(query="Helmer 7 Powers", status="all", limit=20)
result = module.search_proposals(args)
assert result["proposals"][0]["status"] == "approved"
assert {"helmer", "powers"} <= set(result["terms"])
sql = captured_sql[0]
assert "payload::text ilike any" in sql
assert "coalesce(rationale, '') ilike any" in sql
assert "status =" not in sql
def test_vps_bridge_proposal_list_prints_rationale_for_non_edge_rows(capsys) -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
module.print_proposal_list(
[
{
"id": "a64df080-8502-42e2-98f4-9bbdecb8da73",
"proposal_type": "attach_evidence",
"status": "approved",
"proposed_by_handle": "leo",
"channel": "telegram",
"created_at": "2026-07-09 00:00:00+00",
"payload": {"title": "Helmer 7 Powers"},
"rationale": "Revised Helmer packet should remain visible as approved but unapplied.",
}
]
)
output = capsys.readouterr().out
assert "attach_evidence" in output
assert "approved" in output
assert "Revised Helmer packet" in output
def test_vps_bridge_decision_matrix_status_checks_schema_tables(monkeypatch) -> None:
module = _load_module(BRIDGE_DIR / "kb_tool.py")
captured_sql: list[str] = []
def fake_psql_json(_args, sql):
captured_sql.append(sql)
return [
{
"artifact": "teleo_kb_decision_matrix_status",
"all_required_tables_present": False,
"any_decision_matrix_table_present": False,
"decision_matrix_tables": {},
"proposal_status_counts": {},
"guidance": "Decision-matrix approval schema is absent or incomplete.",
}
]
monkeypatch.setattr(module, "psql_json", fake_psql_json)
result = module.decision_matrix_status(SimpleNamespace())
assert result["all_required_tables_present"] is False
sql = captured_sql[0]
for table in (
"public.matrix_voters",
"public.proposal_votes",
"public.proposal_decisions",
"kb_stage.matrix_voters",
"kb_stage.proposal_votes",
"kb_stage.proposal_decisions",
):
assert table in sql
def test_cloudsql_bridge_matches_direct_claim_readback_commands(monkeypatch) -> None:
module = _load_module(BRIDGE_DIR / "cloudsql_memory_tool.py")
captured_sql: list[str] = []
def fake_psql_json_lines(_args, sql, db=None):
captured_sql.append(sql)
if "matrix_voters" in sql:
return [
{
"artifact": "teleo_cloudsql_kb_decision_matrix_status",
"all_required_tables_present": False,
"any_decision_matrix_table_present": False,
"decision_matrix_tables": {},
"proposal_status_counts": {},
"guidance": "Decision-matrix approval schema is absent or incomplete.",
}
]
return [{"id": "a64df080-8502-42e2-98f4-9bbdecb8da73", "status": "approved"}]
monkeypatch.setattr(module, "psql_json_lines", fake_psql_json_lines)
args = SimpleNamespace(query="Helmer 7 Powers", status="all", limit=20, canonical_db="teleo")
proposal_result = module.search_proposals(args)
matrix_result = module.decision_matrix_status(SimpleNamespace(canonical_db="teleo"))
assert proposal_result["artifact"] == "teleo_cloudsql_kb_proposal_search"
assert proposal_result["proposals"][0]["status"] == "approved"
assert matrix_result["artifact"] == "teleo_cloudsql_kb_decision_matrix_status"
assert any("payload::text ilike any" in sql for sql in captured_sql)
assert any("public.proposal_decisions" in sql for sql in captured_sql)
def test_kb_bridges_emit_public_claim_links_for_telegram_rendering() -> None: def test_kb_bridges_emit_public_claim_links_for_telegram_rendering() -> None:
claim_id = "d3fb892b-3c5a-4700-9512-55e5c680eec1" claim_id = "d3fb892b-3c5a-4700-9512-55e5c680eec1"
expected = f"https://leo.livingip.xyz/kb/claims/{claim_id}" expected = f"https://leo.livingip.xyz/kb/claims/{claim_id}"

View file

@ -11,6 +11,9 @@ def test_gcp_kb_skill_uses_cloudsql_bridge_not_vps_docker() -> None:
assert "answer from the Cloud SQL KB after a bounded read" in text assert "answer from the Cloud SQL KB after a bounded read" in text
assert "Do not browse the public web just because the KB has evidence gaps" in text assert "Do not browse the public web just because the KB has evidence gaps" in text
assert "/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb" in text assert "/home/teleo/.hermes/profiles/leoclean/bin/teleo-kb" in text
assert "search-proposals" in text
assert "decision-matrix-status" in text
assert "Do not answer" in text or "do not infer matrix approval" in text
assert "docker exec" not in text assert "docker exec" not in text
assert "teleo-pg" not in text assert "teleo-pg" not in text
assert "on the VPS" not in text assert "on the VPS" not in text
@ -26,6 +29,10 @@ def test_vps_kb_skill_keeps_vps_scope_explicit() -> None:
assert "prefer the bridge over raw database access" in text assert "prefer the bridge over raw database access" in text
assert "Claim / Body / Concept Map Loop" in text assert "Claim / Body / Concept Map Loop" in text
assert "Status: applied | pending | missing | partially applied" in text assert "Status: applied | pending | missing | partially applied" in text
assert "search-proposals" in text
assert "decision-matrix-status" in text
assert "do not infer matrix approval" in text
assert "approved/staged or packet-ready but not canonical" in text
assert "Do not call an approved proposal \"implemented\"" in text assert "Do not call an approved proposal \"implemented\"" in text
assert "needs reviewer/operator apply tooling rather than inviting ad hoc SQL from chat" in squashed assert "needs reviewer/operator apply tooling rather than inviting ad hoc SQL from chat" in squashed
assert "Never end a normal Telegram answer by offering to run direct `INSERT`, `UPDATE`" in text assert "Never end a normal Telegram answer by offering to run direct `INSERT`, `UPDATE`" in text
@ -34,6 +41,8 @@ def test_vps_kb_skill_keeps_vps_scope_explicit() -> None:
assert "/kb/claims/<claim_id>" in text assert "/kb/claims/<claim_id>" in text
assert "wrap claim IDs, proposal IDs" in text assert "wrap claim IDs, proposal IDs" in text
assert "claim page: https://leo.livingip.xyz/kb/claims/<claim_id>" in text assert "claim page: https://leo.livingip.xyz/kb/claims/<claim_id>" in text
assert "search-proposals" in text
assert "decision-matrix-status" in text
def test_gcp_kb_skill_keeps_claim_links_and_backtick_rendering() -> None: def test_gcp_kb_skill_keeps_claim_links_and_backtick_rendering() -> None: