nexo-brain 7.30.21 → 7.30.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/bin/nexo-brain.js +7 -1
- package/bin/nexo-managed-mcp.js +42 -4
- package/package.json +1 -1
- package/src/auto_update.py +7 -0
- package/src/cli.py +69 -0
- package/src/closure_plane.py +389 -8
- package/src/db/_schema.py +205 -2
- package/src/managed_mcp/catalog.py +6 -0
- package/src/managed_mcp/reconcile.py +198 -0
- package/src/opportunity_orchestrator.py +933 -0
- package/src/product_knowledge/catalog.json +53 -3
- package/src/scripts/nexo-email-monitor.py +71 -5
- package/src/scripts/nexo-send-reply.py +15 -2
- package/src/server.py +122 -2
- package/templates/core-prompts/email-monitor.md +6 -1
- package/tool-enforcement-map.json +120 -0
package/src/closure_plane.py
CHANGED
|
@@ -11,12 +11,34 @@ import datetime as _dt
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
|
+
import time as _time
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
OPEN_STATES = {"open", "waiting", "verified"}
|
|
19
20
|
FINAL_STATES = {"closed", "rejected", "stale"}
|
|
21
|
+
STATE_ALIASES = {
|
|
22
|
+
"ready": "open",
|
|
23
|
+
"waiting_user": "waiting",
|
|
24
|
+
"blocked": "waiting",
|
|
25
|
+
"done": "closed",
|
|
26
|
+
}
|
|
27
|
+
READINESS_STATES = {
|
|
28
|
+
"available",
|
|
29
|
+
"missing_tool",
|
|
30
|
+
"missing_credential",
|
|
31
|
+
"needs_user_permission",
|
|
32
|
+
"unsafe",
|
|
33
|
+
"external_blocker",
|
|
34
|
+
"unknown",
|
|
35
|
+
}
|
|
36
|
+
TRIAGE_STATES = OPEN_STATES | FINAL_STATES
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _canonical_state(value: Any) -> str:
|
|
40
|
+
clean = str(value or "").strip().lower()
|
|
41
|
+
return STATE_ALIASES.get(clean, clean)
|
|
20
42
|
|
|
21
43
|
|
|
22
44
|
def _now_iso() -> str:
|
|
@@ -236,7 +258,7 @@ def _upsert_candidate(conn, item: dict[str, Any]) -> bool:
|
|
|
236
258
|
|
|
237
259
|
|
|
238
260
|
def _record_event(conn, item_id: str, event_type: str, from_state: str, to_state: str, note: str, evidence: str = "") -> None:
|
|
239
|
-
event_id = _hash_id("CIE", f"{item_id}:{event_type}:{from_state}:{to_state}:{
|
|
261
|
+
event_id = _hash_id("CIE", f"{item_id}:{event_type}:{from_state}:{to_state}:{_time.time_ns()}:{note}", 24)
|
|
240
262
|
conn.execute(
|
|
241
263
|
"""
|
|
242
264
|
INSERT INTO closure_item_events (
|
|
@@ -247,6 +269,17 @@ def _record_event(conn, item_id: str, event_type: str, from_state: str, to_state
|
|
|
247
269
|
)
|
|
248
270
|
|
|
249
271
|
|
|
272
|
+
def _readiness_counts(conn) -> dict[str, int]:
|
|
273
|
+
if not _table_exists(conn, "closure_capability_readiness"):
|
|
274
|
+
return {}
|
|
275
|
+
return {
|
|
276
|
+
row["status"]: row["n"]
|
|
277
|
+
for row in conn.execute(
|
|
278
|
+
"SELECT status, COUNT(*) AS n FROM closure_capability_readiness GROUP BY status"
|
|
279
|
+
).fetchall()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
250
283
|
def _protocol_task_candidates(conn, limit: int) -> list[dict[str, Any]]:
|
|
251
284
|
if not _table_exists(conn, "protocol_tasks"):
|
|
252
285
|
return []
|
|
@@ -469,6 +502,7 @@ def refresh_closure_items(conn=None, *, limit_per_adapter: int = 250) -> dict[st
|
|
|
469
502
|
run_migrations(conn)
|
|
470
503
|
candidates: list[dict[str, Any]] = []
|
|
471
504
|
adapter_counts: dict[str, int] = {}
|
|
505
|
+
adapter_errors: dict[str, str] = {}
|
|
472
506
|
adapters = [
|
|
473
507
|
("protocol_tasks", lambda: _protocol_task_candidates(conn, limit_per_adapter)),
|
|
474
508
|
("followups", lambda: _followup_candidates(conn, limit_per_adapter)),
|
|
@@ -479,8 +513,9 @@ def refresh_closure_items(conn=None, *, limit_per_adapter: int = 250) -> dict[st
|
|
|
479
513
|
for name, adapter in adapters:
|
|
480
514
|
try:
|
|
481
515
|
produced = adapter()
|
|
482
|
-
except Exception:
|
|
516
|
+
except Exception as exc:
|
|
483
517
|
produced = []
|
|
518
|
+
adapter_errors[name] = f"{type(exc).__name__}: {exc}"
|
|
484
519
|
adapter_counts[name] = len(produced)
|
|
485
520
|
candidates.extend(produced)
|
|
486
521
|
|
|
@@ -493,17 +528,34 @@ def refresh_closure_items(conn=None, *, limit_per_adapter: int = 250) -> dict[st
|
|
|
493
528
|
return {
|
|
494
529
|
"ok": True,
|
|
495
530
|
"adapters": adapter_counts,
|
|
531
|
+
"adapter_errors": adapter_errors,
|
|
496
532
|
"observed": len(candidates),
|
|
497
533
|
"created": created,
|
|
498
534
|
}
|
|
499
535
|
|
|
500
536
|
|
|
501
|
-
def closure_next(
|
|
537
|
+
def closure_next(
|
|
538
|
+
conn=None,
|
|
539
|
+
*,
|
|
540
|
+
limit: int = 10,
|
|
541
|
+
include_waiting: bool = False,
|
|
542
|
+
source: str = "",
|
|
543
|
+
kind: str = "",
|
|
544
|
+
state: str = "",
|
|
545
|
+
max_risk: float | None = None,
|
|
546
|
+
area: str = "",
|
|
547
|
+
) -> list[dict[str, Any]]:
|
|
502
548
|
if conn is None:
|
|
503
549
|
from db import get_db
|
|
504
550
|
|
|
505
551
|
conn = get_db()
|
|
506
|
-
|
|
552
|
+
clean_state = _canonical_state(state)
|
|
553
|
+
if clean_state:
|
|
554
|
+
if clean_state not in OPEN_STATES | FINAL_STATES:
|
|
555
|
+
return []
|
|
556
|
+
states = (clean_state,)
|
|
557
|
+
else:
|
|
558
|
+
states = ("open", "verified", "waiting") if include_waiting else ("open", "verified")
|
|
507
559
|
clauses = [f"state IN ({','.join('?' for _ in states)})"]
|
|
508
560
|
params: list[Any] = list(states)
|
|
509
561
|
if source:
|
|
@@ -512,6 +564,24 @@ def closure_next(conn=None, *, limit: int = 10, include_waiting: bool = False, s
|
|
|
512
564
|
if kind:
|
|
513
565
|
clauses.append("kind = ?")
|
|
514
566
|
params.append(kind)
|
|
567
|
+
if max_risk is not None:
|
|
568
|
+
try:
|
|
569
|
+
risk_limit = float(max_risk)
|
|
570
|
+
except Exception:
|
|
571
|
+
risk_limit = 0.0
|
|
572
|
+
if risk_limit > 0:
|
|
573
|
+
clauses.append("risk_score <= ?")
|
|
574
|
+
params.append(max(0.0, min(risk_limit, 1.0)))
|
|
575
|
+
clean_area = str(area or "").strip()
|
|
576
|
+
if clean_area:
|
|
577
|
+
like_area = f"%{clean_area}%"
|
|
578
|
+
clauses.append(
|
|
579
|
+
"("
|
|
580
|
+
"source_primary = ? OR kind = ? OR owner = ? OR capability_required = ? "
|
|
581
|
+
"OR title LIKE ? OR summary LIKE ? OR source_payload_json LIKE ?"
|
|
582
|
+
")"
|
|
583
|
+
)
|
|
584
|
+
params.extend([clean_area, clean_area, clean_area, clean_area, like_area, like_area, like_area])
|
|
515
585
|
params.append(max(1, min(int(limit or 10), 100)))
|
|
516
586
|
rows = conn.execute(
|
|
517
587
|
f"""
|
|
@@ -547,6 +617,7 @@ def closure_status(conn=None, *, refresh: bool = True, limit: int = 10) -> dict[
|
|
|
547
617
|
"schema": "nexo.closure.status.v1",
|
|
548
618
|
"refreshed": refresh_result,
|
|
549
619
|
"counts": counts,
|
|
620
|
+
"capability_readiness": _readiness_counts(conn),
|
|
550
621
|
"open_total": sum(int(counts.get(state, 0)) for state in OPEN_STATES),
|
|
551
622
|
"by_kind": by_kind,
|
|
552
623
|
"next": closure_next(conn, limit=limit, include_waiting=True),
|
|
@@ -576,8 +647,13 @@ def closure_item_get(item_id: str, conn=None) -> dict[str, Any] | None:
|
|
|
576
647
|
"SELECT * FROM closure_item_events WHERE closure_item_id = ? ORDER BY created_at DESC LIMIT 50",
|
|
577
648
|
(payload["id"],),
|
|
578
649
|
).fetchall()
|
|
650
|
+
links = conn.execute(
|
|
651
|
+
"SELECT * FROM closure_item_links WHERE closure_item_id = ? ORDER BY created_at DESC, link_type, link_id",
|
|
652
|
+
(payload["id"],),
|
|
653
|
+
).fetchall() if _table_exists(conn, "closure_item_links") else []
|
|
579
654
|
payload["sources"] = [_as_dict(row) for row in sources]
|
|
580
655
|
payload["events"] = [_as_dict(row) for row in events]
|
|
656
|
+
payload["links"] = [_as_dict(row) for row in links]
|
|
581
657
|
return payload
|
|
582
658
|
|
|
583
659
|
|
|
@@ -641,12 +717,238 @@ def closure_close_item(item_id: str, *, reason: str = "completed", conn=None) ->
|
|
|
641
717
|
return {"ok": True, "id": item["id"], "state": final_state}
|
|
642
718
|
|
|
643
719
|
|
|
644
|
-
def
|
|
720
|
+
def closure_link_item(
|
|
721
|
+
item_id: str,
|
|
722
|
+
*,
|
|
723
|
+
link_type: str,
|
|
724
|
+
link_id: str,
|
|
725
|
+
relation: str = "related",
|
|
726
|
+
conn=None,
|
|
727
|
+
) -> dict[str, Any]:
|
|
728
|
+
if conn is None:
|
|
729
|
+
from db import get_db
|
|
730
|
+
|
|
731
|
+
conn = get_db()
|
|
732
|
+
item = closure_item_get(item_id, conn)
|
|
733
|
+
if not item:
|
|
734
|
+
return {"ok": False, "error": "closure item not found"}
|
|
735
|
+
clean_type = str(link_type or "").strip()
|
|
736
|
+
clean_id = str(link_id or "").strip()
|
|
737
|
+
clean_relation = str(relation or "related").strip() or "related"
|
|
738
|
+
if not clean_type or not clean_id:
|
|
739
|
+
return {"ok": False, "error": "link_type and link_id are required"}
|
|
740
|
+
now = _now_iso()
|
|
741
|
+
link_pk = _hash_id("CIL", f"{item['id']}:{clean_type}:{clean_id}:{clean_relation}", 24)
|
|
742
|
+
conn.execute(
|
|
743
|
+
"""
|
|
744
|
+
INSERT INTO closure_item_links (
|
|
745
|
+
id, closure_item_id, link_type, link_id, relation, created_at
|
|
746
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
747
|
+
ON CONFLICT(closure_item_id, link_type, link_id, relation) DO UPDATE SET
|
|
748
|
+
created_at = excluded.created_at
|
|
749
|
+
""",
|
|
750
|
+
(link_pk, item["id"], clean_type, clean_id, clean_relation, now),
|
|
751
|
+
)
|
|
752
|
+
_record_event(
|
|
753
|
+
conn,
|
|
754
|
+
item["id"],
|
|
755
|
+
"linked",
|
|
756
|
+
item["state"],
|
|
757
|
+
item["state"],
|
|
758
|
+
f"Linked {clean_type}:{clean_id} as {clean_relation}.",
|
|
759
|
+
)
|
|
760
|
+
conn.commit()
|
|
761
|
+
return {
|
|
762
|
+
"ok": True,
|
|
763
|
+
"id": link_pk,
|
|
764
|
+
"closure_item_id": item["id"],
|
|
765
|
+
"link_type": clean_type,
|
|
766
|
+
"link_id": clean_id,
|
|
767
|
+
"relation": clean_relation,
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def closure_set_capability_readiness(
|
|
772
|
+
capability: str,
|
|
773
|
+
*,
|
|
774
|
+
status: str = "unknown",
|
|
775
|
+
reason: str = "",
|
|
776
|
+
evidence: str = "",
|
|
777
|
+
expires_at: str = "",
|
|
778
|
+
conn=None,
|
|
779
|
+
) -> dict[str, Any]:
|
|
780
|
+
if conn is None:
|
|
781
|
+
from db import get_db
|
|
782
|
+
|
|
783
|
+
conn = get_db()
|
|
784
|
+
clean_capability = str(capability or "").strip()
|
|
785
|
+
clean_status = str(status or "unknown").strip()
|
|
786
|
+
if not clean_capability:
|
|
787
|
+
return {"ok": False, "error": "capability is required"}
|
|
788
|
+
if clean_status not in READINESS_STATES:
|
|
789
|
+
return {"ok": False, "error": f"invalid readiness status: {clean_status}"}
|
|
790
|
+
now = _now_iso()
|
|
791
|
+
row_id = _hash_id("CCR", clean_capability, 20)
|
|
792
|
+
conn.execute(
|
|
793
|
+
"""
|
|
794
|
+
INSERT INTO closure_capability_readiness (
|
|
795
|
+
id, capability, status, reason, verified_at, verification_evidence, expires_at
|
|
796
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
797
|
+
ON CONFLICT(capability) DO UPDATE SET
|
|
798
|
+
status = excluded.status,
|
|
799
|
+
reason = excluded.reason,
|
|
800
|
+
verified_at = excluded.verified_at,
|
|
801
|
+
verification_evidence = excluded.verification_evidence,
|
|
802
|
+
expires_at = excluded.expires_at
|
|
803
|
+
""",
|
|
804
|
+
(row_id, clean_capability, clean_status, str(reason or ""), now, str(evidence or ""), str(expires_at or "")),
|
|
805
|
+
)
|
|
806
|
+
conn.commit()
|
|
807
|
+
return {"ok": True, "id": row_id, "capability": clean_capability, "status": clean_status}
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def closure_triage_item(
|
|
811
|
+
item_id: str,
|
|
812
|
+
*,
|
|
813
|
+
state: str = "",
|
|
814
|
+
kind: str = "",
|
|
815
|
+
blocker_reason: str = "",
|
|
816
|
+
next_action: str = "",
|
|
817
|
+
evidence_required: str = "",
|
|
818
|
+
owner: str = "",
|
|
819
|
+
capability_required: str = "",
|
|
820
|
+
capability_status: str = "",
|
|
821
|
+
duplicate_of: str = "",
|
|
822
|
+
conn=None,
|
|
823
|
+
) -> dict[str, Any]:
|
|
824
|
+
if conn is None:
|
|
825
|
+
from db import get_db
|
|
826
|
+
|
|
827
|
+
conn = get_db()
|
|
828
|
+
item = closure_item_get(item_id, conn)
|
|
829
|
+
if not item:
|
|
830
|
+
return {"ok": False, "error": "closure item not found"}
|
|
831
|
+
updates: dict[str, Any] = {}
|
|
832
|
+
requested_state = str(state or "").strip()
|
|
833
|
+
clean_state = _canonical_state(requested_state)
|
|
834
|
+
if clean_state:
|
|
835
|
+
if clean_state not in TRIAGE_STATES:
|
|
836
|
+
return {"ok": False, "error": f"invalid state: {requested_state}"}
|
|
837
|
+
updates["state"] = clean_state
|
|
838
|
+
for column, value in (
|
|
839
|
+
("kind", kind),
|
|
840
|
+
("blocker_reason", blocker_reason),
|
|
841
|
+
("next_action", next_action),
|
|
842
|
+
("evidence_required", evidence_required),
|
|
843
|
+
("owner", owner),
|
|
844
|
+
("capability_required", capability_required),
|
|
845
|
+
("capability_status", capability_status),
|
|
846
|
+
):
|
|
847
|
+
clean_value = str(value or "").strip()
|
|
848
|
+
if clean_value:
|
|
849
|
+
updates[column] = clean_value
|
|
850
|
+
clean_duplicate = str(duplicate_of or "").strip()
|
|
851
|
+
if clean_duplicate:
|
|
852
|
+
target = closure_item_get(clean_duplicate, conn)
|
|
853
|
+
if not target:
|
|
854
|
+
return {"ok": False, "error": "duplicate target not found"}
|
|
855
|
+
closure_link_item(item["id"], link_type="closure_item", link_id=target["id"], relation="duplicate_of", conn=conn)
|
|
856
|
+
updates["state"] = "stale"
|
|
857
|
+
updates["close_reason"] = f"duplicate_of:{target['id']}"
|
|
858
|
+
updates["closed_at"] = _now_iso()
|
|
859
|
+
if not updates:
|
|
860
|
+
return {"ok": False, "error": "no triage changes supplied"}
|
|
861
|
+
now = _now_iso()
|
|
862
|
+
updates["updated_at"] = now
|
|
863
|
+
if set(updates) - {"updated_at"}:
|
|
864
|
+
updates["last_progress_at"] = now
|
|
865
|
+
assignments = ", ".join(f"{column} = ?" for column in updates)
|
|
866
|
+
params = list(updates.values()) + [item["id"]]
|
|
867
|
+
conn.execute(f"UPDATE closure_items SET {assignments} WHERE id = ?", params)
|
|
868
|
+
to_state = updates.get("state", item["state"])
|
|
869
|
+
_record_event(
|
|
870
|
+
conn,
|
|
871
|
+
item["id"],
|
|
872
|
+
"triaged",
|
|
873
|
+
item["state"],
|
|
874
|
+
to_state,
|
|
875
|
+
"Closure item triaged: " + ", ".join(sorted(column for column in updates if column != "updated_at")),
|
|
876
|
+
)
|
|
877
|
+
conn.commit()
|
|
878
|
+
result = {"ok": True, "id": item["id"], "state": to_state, "updated": sorted(updates)}
|
|
879
|
+
if requested_state and requested_state != clean_state:
|
|
880
|
+
result["requested_state"] = requested_state
|
|
881
|
+
return result
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def closure_snapshot(conn=None, *, refresh: bool = True, snapshot_date: str = "", limit: int = 10) -> dict[str, Any]:
|
|
885
|
+
if conn is None:
|
|
886
|
+
from db import get_db
|
|
887
|
+
from db._schema import run_migrations
|
|
888
|
+
|
|
889
|
+
conn = get_db()
|
|
890
|
+
run_migrations(conn)
|
|
891
|
+
if refresh:
|
|
892
|
+
refresh_closure_items(conn)
|
|
893
|
+
date_key = str(snapshot_date or _today()).strip()[:10]
|
|
894
|
+
if date_key != _today():
|
|
895
|
+
_write_daily_snapshot_for_date(conn, snapshot_date=date_key, limit=limit)
|
|
896
|
+
else:
|
|
897
|
+
_write_daily_snapshot(conn, limit=limit)
|
|
898
|
+
row = conn.execute(
|
|
899
|
+
"SELECT * FROM closure_daily_snapshots WHERE snapshot_date = ?",
|
|
900
|
+
(date_key,),
|
|
901
|
+
).fetchone()
|
|
902
|
+
payload = _as_dict(row) if row else {}
|
|
903
|
+
if payload.get("top_items_json"):
|
|
904
|
+
try:
|
|
905
|
+
payload["top_items"] = json.loads(payload["top_items_json"])
|
|
906
|
+
except Exception:
|
|
907
|
+
payload["top_items"] = []
|
|
908
|
+
payload["capability_readiness"] = _readiness_counts(conn)
|
|
909
|
+
return {"ok": bool(payload), "snapshot": payload}
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _write_daily_snapshot_for_date(conn, *, snapshot_date: str, limit: int = 10) -> None:
|
|
913
|
+
counts = {
|
|
914
|
+
row["state"]: row["n"]
|
|
915
|
+
for row in conn.execute("SELECT state, COUNT(*) AS n FROM closure_items GROUP BY state").fetchall()
|
|
916
|
+
}
|
|
917
|
+
top = closure_next(conn, limit=limit, include_waiting=True)
|
|
918
|
+
conn.execute(
|
|
919
|
+
"""
|
|
920
|
+
INSERT OR REPLACE INTO closure_daily_snapshots (
|
|
921
|
+
snapshot_date, total_open, total_verified, total_waiting, total_closed,
|
|
922
|
+
top_items_json, created_at
|
|
923
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
924
|
+
""",
|
|
925
|
+
(
|
|
926
|
+
snapshot_date,
|
|
927
|
+
int(counts.get("open", 0)),
|
|
928
|
+
int(counts.get("verified", 0)),
|
|
929
|
+
int(counts.get("waiting", 0)),
|
|
930
|
+
int(counts.get("closed", 0)),
|
|
931
|
+
_safe_json([
|
|
932
|
+
{
|
|
933
|
+
"id": item.get("id"),
|
|
934
|
+
"title": item.get("title"),
|
|
935
|
+
"priority_score": item.get("priority_score"),
|
|
936
|
+
"state": item.get("state"),
|
|
937
|
+
}
|
|
938
|
+
for item in top
|
|
939
|
+
]),
|
|
940
|
+
_now_iso(),
|
|
941
|
+
),
|
|
942
|
+
)
|
|
943
|
+
conn.commit()
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _write_daily_snapshot(conn, *, limit: int = 10) -> None:
|
|
645
947
|
counts = {
|
|
646
948
|
row["state"]: row["n"]
|
|
647
949
|
for row in conn.execute("SELECT state, COUNT(*) AS n FROM closure_items GROUP BY state").fetchall()
|
|
648
950
|
}
|
|
649
|
-
top = closure_next(conn, limit=
|
|
951
|
+
top = closure_next(conn, limit=limit, include_waiting=True)
|
|
650
952
|
conn.execute(
|
|
651
953
|
"""
|
|
652
954
|
INSERT OR REPLACE INTO closure_daily_snapshots (
|
|
@@ -679,7 +981,15 @@ def handle_closure_status(refresh: bool = True, limit: int = 10) -> str:
|
|
|
679
981
|
return json.dumps(closure_status(refresh=refresh, limit=limit), indent=2, ensure_ascii=False)
|
|
680
982
|
|
|
681
983
|
|
|
682
|
-
def handle_closure_next(
|
|
984
|
+
def handle_closure_next(
|
|
985
|
+
limit: int = 10,
|
|
986
|
+
include_waiting: bool = False,
|
|
987
|
+
source: str = "",
|
|
988
|
+
kind: str = "",
|
|
989
|
+
state: str = "",
|
|
990
|
+
max_risk: float | None = None,
|
|
991
|
+
area: str = "",
|
|
992
|
+
) -> str:
|
|
683
993
|
from db import get_db
|
|
684
994
|
from db._schema import run_migrations
|
|
685
995
|
|
|
@@ -688,7 +998,16 @@ def handle_closure_next(limit: int = 10, include_waiting: bool = False, source:
|
|
|
688
998
|
refresh_closure_items(conn)
|
|
689
999
|
return json.dumps({
|
|
690
1000
|
"ok": True,
|
|
691
|
-
"items": closure_next(
|
|
1001
|
+
"items": closure_next(
|
|
1002
|
+
conn,
|
|
1003
|
+
limit=limit,
|
|
1004
|
+
include_waiting=include_waiting,
|
|
1005
|
+
source=source,
|
|
1006
|
+
kind=kind,
|
|
1007
|
+
state=state,
|
|
1008
|
+
max_risk=max_risk,
|
|
1009
|
+
area=area,
|
|
1010
|
+
),
|
|
692
1011
|
}, indent=2, ensure_ascii=False)
|
|
693
1012
|
|
|
694
1013
|
|
|
@@ -702,6 +1021,68 @@ def handle_closure_item_get(item_id: str) -> str:
|
|
|
702
1021
|
return json.dumps({"ok": bool(item), "item": item}, indent=2, ensure_ascii=False)
|
|
703
1022
|
|
|
704
1023
|
|
|
1024
|
+
def handle_closure_triage(
|
|
1025
|
+
item_id: str,
|
|
1026
|
+
state: str = "",
|
|
1027
|
+
kind: str = "",
|
|
1028
|
+
blocker_reason: str = "",
|
|
1029
|
+
next_action: str = "",
|
|
1030
|
+
evidence_required: str = "",
|
|
1031
|
+
owner: str = "",
|
|
1032
|
+
capability_required: str = "",
|
|
1033
|
+
capability_status: str = "",
|
|
1034
|
+
duplicate_of: str = "",
|
|
1035
|
+
) -> str:
|
|
1036
|
+
from db import get_db
|
|
1037
|
+
from db._schema import run_migrations
|
|
1038
|
+
|
|
1039
|
+
conn = get_db()
|
|
1040
|
+
run_migrations(conn)
|
|
1041
|
+
return json.dumps(
|
|
1042
|
+
closure_triage_item(
|
|
1043
|
+
item_id,
|
|
1044
|
+
state=state,
|
|
1045
|
+
kind=kind,
|
|
1046
|
+
blocker_reason=blocker_reason,
|
|
1047
|
+
next_action=next_action,
|
|
1048
|
+
evidence_required=evidence_required,
|
|
1049
|
+
owner=owner,
|
|
1050
|
+
capability_required=capability_required,
|
|
1051
|
+
capability_status=capability_status,
|
|
1052
|
+
duplicate_of=duplicate_of,
|
|
1053
|
+
conn=conn,
|
|
1054
|
+
),
|
|
1055
|
+
indent=2,
|
|
1056
|
+
ensure_ascii=False,
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def handle_closure_link(item_id: str, link_type: str, link_id: str, relation: str = "related") -> str:
|
|
1061
|
+
from db import get_db
|
|
1062
|
+
from db._schema import run_migrations
|
|
1063
|
+
|
|
1064
|
+
conn = get_db()
|
|
1065
|
+
run_migrations(conn)
|
|
1066
|
+
return json.dumps(
|
|
1067
|
+
closure_link_item(item_id, link_type=link_type, link_id=link_id, relation=relation, conn=conn),
|
|
1068
|
+
indent=2,
|
|
1069
|
+
ensure_ascii=False,
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def handle_closure_snapshot(refresh: bool = True, snapshot_date: str = "", limit: int = 10) -> str:
|
|
1074
|
+
from db import get_db
|
|
1075
|
+
from db._schema import run_migrations
|
|
1076
|
+
|
|
1077
|
+
conn = get_db()
|
|
1078
|
+
run_migrations(conn)
|
|
1079
|
+
return json.dumps(
|
|
1080
|
+
closure_snapshot(conn, refresh=refresh, snapshot_date=snapshot_date, limit=limit),
|
|
1081
|
+
indent=2,
|
|
1082
|
+
ensure_ascii=False,
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
|
|
705
1086
|
def handle_closure_verify(item_id: str, evidence: str) -> str:
|
|
706
1087
|
from db import get_db
|
|
707
1088
|
from db._schema import run_migrations
|