delimit-cli 3.14.28 → 3.14.29
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/gateway/ai/backends/deploy_bridge.py +56 -2
- package/gateway/ai/backends/gateway_core.py +212 -1
- package/gateway/ai/backends/generate_bridge.py +84 -13
- package/gateway/ai/backends/governance_bridge.py +63 -16
- package/gateway/ai/backends/memory_bridge.py +77 -76
- package/gateway/ai/backends/ops_bridge.py +76 -6
- package/gateway/ai/backends/os_bridge.py +23 -3
- package/gateway/ai/backends/repo_bridge.py +156 -17
- package/gateway/ai/backends/tools_design.py +116 -9
- package/gateway/ai/backends/tools_infra.py +200 -72
- package/gateway/ai/backends/tools_real.py +8 -0
- package/gateway/ai/backends/ui_bridge.py +115 -5
- package/gateway/ai/backends/vault_bridge.py +69 -114
- package/gateway/ai/content_engine.py +1276 -0
- package/gateway/ai/context_fs.py +193 -0
- package/gateway/ai/daemon.py +500 -0
- package/gateway/ai/data_plane.py +291 -0
- package/gateway/ai/deliberation.py +1033 -6
- package/gateway/ai/events.py +39 -0
- package/gateway/ai/founding_users.py +162 -0
- package/gateway/ai/governance.py +698 -4
- package/gateway/ai/inbox_daemon.py +78 -17
- package/gateway/ai/integrations/__init__.py +1 -0
- package/gateway/ai/integrations/opensage_wrapper.py +288 -0
- package/gateway/ai/key_resolver.py +95 -0
- package/gateway/ai/ledger_manager.py +289 -1
- package/gateway/ai/license.py +62 -4
- package/gateway/ai/license_core.py +208 -7
- package/gateway/ai/local_server.py +215 -0
- package/gateway/ai/loop_engine.py +408 -0
- package/gateway/ai/mcp_bridge.py +178 -0
- package/gateway/ai/release_sync.py +2 -2
- package/gateway/ai/screen_record.py +374 -0
- package/gateway/ai/secrets_broker.py +235 -0
- package/gateway/ai/social.py +189 -27
- package/gateway/ai/social_target.py +1635 -0
- package/gateway/ai/supabase_sync.py +190 -0
- package/gateway/ai/tracing.py +195 -0
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/diff_engine_v2.py +272 -78
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/gateway/core/policy_engine.py +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Supabase sync -- writes gateway data to cloud for dashboard access.
|
|
2
|
+
|
|
3
|
+
Writes are fire-and-forget (never blocks tool execution).
|
|
4
|
+
If Supabase is unreachable, data stays in local files (always the source of truth).
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("delimit.supabase_sync")
|
|
14
|
+
|
|
15
|
+
_client = None
|
|
16
|
+
_init_attempted = False
|
|
17
|
+
SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
|
|
18
|
+
SUPABASE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
19
|
+
|
|
20
|
+
# Also check local secrets file
|
|
21
|
+
if not SUPABASE_URL:
|
|
22
|
+
secrets_file = Path.home() / ".delimit" / "secrets" / "supabase.json"
|
|
23
|
+
if secrets_file.exists():
|
|
24
|
+
try:
|
|
25
|
+
creds = json.loads(secrets_file.read_text())
|
|
26
|
+
SUPABASE_URL = creds.get("url", "")
|
|
27
|
+
SUPABASE_KEY = creds.get("service_role_key", "")
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_client():
|
|
33
|
+
"""Lazy-init Supabase client. Returns the SDK client, 'http' for fallback, or None."""
|
|
34
|
+
global _client, _init_attempted
|
|
35
|
+
if _client is not None:
|
|
36
|
+
return _client
|
|
37
|
+
if _init_attempted:
|
|
38
|
+
return _client # Already tried and failed, return cached result (may be None or "http")
|
|
39
|
+
_init_attempted = True
|
|
40
|
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
from supabase import create_client
|
|
44
|
+
_client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
45
|
+
return _client
|
|
46
|
+
except ImportError:
|
|
47
|
+
logger.debug("supabase-py not installed, using HTTP fallback")
|
|
48
|
+
_client = "http"
|
|
49
|
+
return _client
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(f"Supabase init failed: {e}")
|
|
52
|
+
_client = "http" # Fall back to HTTP rather than giving up entirely
|
|
53
|
+
return _client
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _http_post(table: str, data: dict, headers_extra: Optional[Dict] = None) -> bool:
|
|
57
|
+
"""POST to Supabase REST API without the SDK."""
|
|
58
|
+
import urllib.request
|
|
59
|
+
try:
|
|
60
|
+
url = f"{SUPABASE_URL}/rest/v1/{table}"
|
|
61
|
+
body = json.dumps(data).encode()
|
|
62
|
+
req = urllib.request.Request(url, data=body, method="POST")
|
|
63
|
+
req.add_header("Content-Type", "application/json")
|
|
64
|
+
req.add_header("apikey", SUPABASE_KEY)
|
|
65
|
+
req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
|
|
66
|
+
req.add_header("Prefer", "return=minimal")
|
|
67
|
+
if headers_extra:
|
|
68
|
+
for k, v in headers_extra.items():
|
|
69
|
+
req.add_header(k, v)
|
|
70
|
+
urllib.request.urlopen(req, timeout=5)
|
|
71
|
+
return True
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Supabase HTTP POST to {table} failed: {e}")
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _http_patch(table: str, query: str, data: dict) -> bool:
|
|
78
|
+
"""PATCH to Supabase REST API without the SDK."""
|
|
79
|
+
import urllib.request
|
|
80
|
+
try:
|
|
81
|
+
url = f"{SUPABASE_URL}/rest/v1/{table}?{query}"
|
|
82
|
+
body = json.dumps(data).encode()
|
|
83
|
+
req = urllib.request.Request(url, data=body, method="PATCH")
|
|
84
|
+
req.add_header("Content-Type", "application/json")
|
|
85
|
+
req.add_header("apikey", SUPABASE_KEY)
|
|
86
|
+
req.add_header("Authorization", f"Bearer {SUPABASE_KEY}")
|
|
87
|
+
req.add_header("Prefer", "return=minimal")
|
|
88
|
+
urllib.request.urlopen(req, timeout=5)
|
|
89
|
+
return True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.debug(f"Supabase HTTP PATCH to {table} failed: {e}")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sync_event(event: dict):
|
|
96
|
+
"""Sync an event to Supabase (fire-and-forget).
|
|
97
|
+
|
|
98
|
+
Maps the gateway event dict to the Supabase events table schema:
|
|
99
|
+
id (uuid, required), type (text, required), tool (text, required),
|
|
100
|
+
ts, model, status, venture, detail, user_id, session_id
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
client = _get_client()
|
|
104
|
+
if client is None:
|
|
105
|
+
return
|
|
106
|
+
row = {
|
|
107
|
+
"id": str(uuid.uuid4()),
|
|
108
|
+
"type": event.get("type", "tool_call"),
|
|
109
|
+
"tool": event.get("tool", "unknown"),
|
|
110
|
+
"ts": event.get("ts", ""),
|
|
111
|
+
"model": event.get("model", ""),
|
|
112
|
+
"status": event.get("status", "ok"),
|
|
113
|
+
"venture": event.get("venture", ""),
|
|
114
|
+
"session_id": event.get("session_id", ""),
|
|
115
|
+
"user_id": event.get("user_id", ""),
|
|
116
|
+
}
|
|
117
|
+
# Include risk_level and trace info in detail field
|
|
118
|
+
detail_parts = []
|
|
119
|
+
if event.get("risk_level"):
|
|
120
|
+
detail_parts.append(f"risk={event['risk_level']}")
|
|
121
|
+
if event.get("trace_id"):
|
|
122
|
+
detail_parts.append(f"trace={event['trace_id']}")
|
|
123
|
+
if event.get("span_id"):
|
|
124
|
+
detail_parts.append(f"span={event['span_id']}")
|
|
125
|
+
if detail_parts:
|
|
126
|
+
row["detail"] = " ".join(detail_parts)
|
|
127
|
+
|
|
128
|
+
if client == "http":
|
|
129
|
+
_http_post("events", row)
|
|
130
|
+
else:
|
|
131
|
+
client.table("events").insert(row).execute()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.debug(f"Event sync failed: {e}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def sync_ledger_item(item: dict):
|
|
137
|
+
"""Sync a ledger item to Supabase (upsert).
|
|
138
|
+
|
|
139
|
+
Maps the gateway ledger item to the Supabase ledger_items table schema:
|
|
140
|
+
id (text, required), title (text, required), priority, venture,
|
|
141
|
+
status, description, source, note, assignee
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
client = _get_client()
|
|
145
|
+
if client is None:
|
|
146
|
+
return
|
|
147
|
+
row = {
|
|
148
|
+
"id": item.get("id", ""),
|
|
149
|
+
"title": item.get("title", ""),
|
|
150
|
+
"priority": item.get("priority", "P1"),
|
|
151
|
+
"venture": item.get("venture", ""),
|
|
152
|
+
"status": item.get("status", "open"),
|
|
153
|
+
"description": item.get("description", ""),
|
|
154
|
+
"source": item.get("source", "mcp"),
|
|
155
|
+
}
|
|
156
|
+
if not row["id"] or not row["title"]:
|
|
157
|
+
return # Required fields missing
|
|
158
|
+
if client == "http":
|
|
159
|
+
_http_post(
|
|
160
|
+
"ledger_items",
|
|
161
|
+
row,
|
|
162
|
+
headers_extra={
|
|
163
|
+
"Prefer": "resolution=merge-duplicates,return=minimal",
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
client.table("ledger_items").upsert(row).execute()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.debug(f"Ledger item sync failed: {e}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def sync_ledger_update(item_id: str, status: str, note: str = ""):
|
|
173
|
+
"""Sync a ledger status update to Supabase."""
|
|
174
|
+
try:
|
|
175
|
+
client = _get_client()
|
|
176
|
+
if client is None:
|
|
177
|
+
return
|
|
178
|
+
update = {"status": status}
|
|
179
|
+
if note:
|
|
180
|
+
update["note"] = note
|
|
181
|
+
if status == "done":
|
|
182
|
+
from datetime import datetime, timezone
|
|
183
|
+
update["completed_at"] = datetime.now(timezone.utc).isoformat()
|
|
184
|
+
|
|
185
|
+
if client == "http":
|
|
186
|
+
_http_patch("ledger_items", f"id=eq.{item_id}", update)
|
|
187
|
+
else:
|
|
188
|
+
client.table("ledger_items").update(update).eq("id", item_id).execute()
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.debug(f"Ledger update sync failed: {e}")
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Distributed tracing for agent tool calls.
|
|
2
|
+
|
|
3
|
+
STR-053: Every tool call is part of a trace -- from prompt to tool to artifact to outcome.
|
|
4
|
+
Traces are stored as JSONL files in ~/.delimit/traces/ for local-first observability.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
TRACES_DIR = Path.home() / ".delimit" / "traces"
|
|
12
|
+
|
|
13
|
+
_span_seq = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def start_span(trace_id: str, tool: str, args: dict = None) -> dict:
|
|
17
|
+
"""Start a trace span."""
|
|
18
|
+
global _span_seq
|
|
19
|
+
_span_seq += 1
|
|
20
|
+
TRACES_DIR.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
span = {
|
|
22
|
+
"trace_id": trace_id,
|
|
23
|
+
"span_id": f"{trace_id}-{int(time.time()*1000) % 10000:04d}-{_span_seq}",
|
|
24
|
+
"tool": tool,
|
|
25
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
26
|
+
"status": "running",
|
|
27
|
+
"args_summary": str(args)[:200] if args else "",
|
|
28
|
+
}
|
|
29
|
+
trace_file = TRACES_DIR / f"trace-{trace_id}.jsonl"
|
|
30
|
+
with open(trace_file, "a") as f:
|
|
31
|
+
f.write(json.dumps(span) + "\n")
|
|
32
|
+
return span
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def end_span(trace_id: str, span_id: str, status: str = "ok", result_summary: str = ""):
|
|
36
|
+
"""End a trace span."""
|
|
37
|
+
span = {
|
|
38
|
+
"trace_id": trace_id,
|
|
39
|
+
"span_id": span_id,
|
|
40
|
+
"ended_at": datetime.now(timezone.utc).isoformat(),
|
|
41
|
+
"status": status,
|
|
42
|
+
"result_summary": result_summary[:200],
|
|
43
|
+
}
|
|
44
|
+
trace_file = TRACES_DIR / f"trace-{trace_id}.jsonl"
|
|
45
|
+
with open(trace_file, "a") as f:
|
|
46
|
+
f.write(json.dumps(span) + "\n")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_trace(trace_id: str) -> list:
|
|
50
|
+
"""Get all spans for a trace, merging start/end records by span_id."""
|
|
51
|
+
trace_file = TRACES_DIR / f"trace-{trace_id}.jsonl"
|
|
52
|
+
if not trace_file.exists():
|
|
53
|
+
return []
|
|
54
|
+
spans = {}
|
|
55
|
+
for line in trace_file.read_text().splitlines():
|
|
56
|
+
try:
|
|
57
|
+
s = json.loads(line)
|
|
58
|
+
sid = s.get("span_id", "")
|
|
59
|
+
if sid in spans:
|
|
60
|
+
spans[sid].update(s)
|
|
61
|
+
else:
|
|
62
|
+
spans[sid] = s
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
return sorted(spans.values(), key=lambda s: s.get("started_at", ""))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_traces(limit: int = 20) -> list:
|
|
69
|
+
"""List recent traces."""
|
|
70
|
+
if not TRACES_DIR.exists():
|
|
71
|
+
return []
|
|
72
|
+
traces = []
|
|
73
|
+
for f in sorted(TRACES_DIR.glob("trace-*.jsonl"), reverse=True)[:limit]:
|
|
74
|
+
try:
|
|
75
|
+
lines = f.read_text().splitlines()
|
|
76
|
+
first = json.loads(lines[0]) if lines else {}
|
|
77
|
+
span_count = len(set(
|
|
78
|
+
json.loads(l).get("span_id", "") for l in lines if l.strip()
|
|
79
|
+
))
|
|
80
|
+
traces.append({
|
|
81
|
+
"trace_id": first.get("trace_id", f.stem.replace("trace-", "")),
|
|
82
|
+
"started_at": first.get("started_at", ""),
|
|
83
|
+
"span_count": span_count,
|
|
84
|
+
"first_tool": first.get("tool", ""),
|
|
85
|
+
})
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
return traces
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_demo_traces() -> list:
|
|
92
|
+
"""Generate demo trace data for UI development. Returns trace IDs created."""
|
|
93
|
+
TRACES_DIR.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
now = time.time()
|
|
95
|
+
demo_traces = []
|
|
96
|
+
|
|
97
|
+
# Trace 1: Successful lint + diff + ledger workflow
|
|
98
|
+
t1 = "demo-a1b2"
|
|
99
|
+
spans_1 = [
|
|
100
|
+
{"tool": "delimit_lint", "duration_ms": 120, "status": "ok", "args": "old=api_v1.yaml new=api_v2.yaml", "result": "2 breaking changes found"},
|
|
101
|
+
{"tool": "delimit_diff", "duration_ms": 85, "status": "ok", "args": "old=api_v1.yaml new=api_v2.yaml", "result": "5 changes: 2 breaking, 1 deprecation, 2 additions"},
|
|
102
|
+
{"tool": "delimit_ledger_add", "duration_ms": 45, "status": "ok", "args": "title=Fix breaking changes priority=high", "result": "Created DLM-105"},
|
|
103
|
+
{"tool": "delimit_deliberate", "duration_ms": 3200, "status": "ok", "args": "question=Should we ship v2 with breaks?", "result": "Consensus: delay release, add migration guide"},
|
|
104
|
+
{"tool": "delimit_explain", "duration_ms": 210, "status": "ok", "args": "template=migration", "result": "Migration guide generated for 2 endpoints"},
|
|
105
|
+
]
|
|
106
|
+
trace_file = TRACES_DIR / f"trace-{t1}.jsonl"
|
|
107
|
+
with open(trace_file, "w") as f:
|
|
108
|
+
for i, s in enumerate(spans_1):
|
|
109
|
+
offset = sum(sp["duration_ms"] for sp in spans_1[:i]) / 1000
|
|
110
|
+
started = now - 300 + offset
|
|
111
|
+
span_id = f"{t1}-{i:04d}"
|
|
112
|
+
start_record = {
|
|
113
|
+
"trace_id": t1,
|
|
114
|
+
"span_id": span_id,
|
|
115
|
+
"tool": s["tool"],
|
|
116
|
+
"started_at": datetime.fromtimestamp(started, tz=timezone.utc).isoformat(),
|
|
117
|
+
"status": "running",
|
|
118
|
+
"args_summary": s["args"],
|
|
119
|
+
}
|
|
120
|
+
end_record = {
|
|
121
|
+
"trace_id": t1,
|
|
122
|
+
"span_id": span_id,
|
|
123
|
+
"ended_at": datetime.fromtimestamp(started + s["duration_ms"] / 1000, tz=timezone.utc).isoformat(),
|
|
124
|
+
"status": s["status"],
|
|
125
|
+
"result_summary": s["result"],
|
|
126
|
+
}
|
|
127
|
+
f.write(json.dumps(start_record) + "\n")
|
|
128
|
+
f.write(json.dumps(end_record) + "\n")
|
|
129
|
+
demo_traces.append(t1)
|
|
130
|
+
|
|
131
|
+
# Trace 2: Deploy workflow with a blocked step
|
|
132
|
+
t2 = "demo-c3d4"
|
|
133
|
+
spans_2 = [
|
|
134
|
+
{"tool": "delimit_deploy_plan", "duration_ms": 150, "status": "ok", "args": "service=gateway version=3.3.0", "result": "Plan: build, test, publish"},
|
|
135
|
+
{"tool": "delimit_deploy_build", "duration_ms": 890, "status": "warn", "args": "service=gateway", "result": "Built with 2 warnings (deprecated deps)"},
|
|
136
|
+
{"tool": "delimit_deploy_publish", "duration_ms": 12, "status": "blocked", "args": "service=gateway target=prod", "result": "Blocked: requires approval (high risk)"},
|
|
137
|
+
]
|
|
138
|
+
trace_file = TRACES_DIR / f"trace-{t2}.jsonl"
|
|
139
|
+
with open(trace_file, "w") as f:
|
|
140
|
+
for i, s in enumerate(spans_2):
|
|
141
|
+
offset = sum(sp["duration_ms"] for sp in spans_2[:i]) / 1000
|
|
142
|
+
started = now - 600 + offset
|
|
143
|
+
span_id = f"{t2}-{i:04d}"
|
|
144
|
+
start_record = {
|
|
145
|
+
"trace_id": t2,
|
|
146
|
+
"span_id": span_id,
|
|
147
|
+
"tool": s["tool"],
|
|
148
|
+
"started_at": datetime.fromtimestamp(started, tz=timezone.utc).isoformat(),
|
|
149
|
+
"status": "running",
|
|
150
|
+
"args_summary": s["args"],
|
|
151
|
+
}
|
|
152
|
+
end_record = {
|
|
153
|
+
"trace_id": t2,
|
|
154
|
+
"span_id": span_id,
|
|
155
|
+
"ended_at": datetime.fromtimestamp(started + s["duration_ms"] / 1000, tz=timezone.utc).isoformat(),
|
|
156
|
+
"status": s["status"],
|
|
157
|
+
"result_summary": s["result"],
|
|
158
|
+
}
|
|
159
|
+
f.write(json.dumps(start_record) + "\n")
|
|
160
|
+
f.write(json.dumps(end_record) + "\n")
|
|
161
|
+
demo_traces.append(t2)
|
|
162
|
+
|
|
163
|
+
# Trace 3: Security scan workflow
|
|
164
|
+
t3 = "demo-e5f6"
|
|
165
|
+
spans_3 = [
|
|
166
|
+
{"tool": "delimit_scan", "duration_ms": 340, "status": "ok", "args": "path=./project", "result": "Project scanned: FastAPI, 76 tools, OpenAPI spec found"},
|
|
167
|
+
{"tool": "delimit_security_scan", "duration_ms": 1200, "status": "warn", "args": "path=./project", "result": "1 high severity: outdated cryptography package"},
|
|
168
|
+
{"tool": "delimit_ledger_add", "duration_ms": 38, "status": "ok", "args": "title=Upgrade cryptography package priority=high", "result": "Created SEC-001"},
|
|
169
|
+
]
|
|
170
|
+
trace_file = TRACES_DIR / f"trace-{t3}.jsonl"
|
|
171
|
+
with open(trace_file, "w") as f:
|
|
172
|
+
for i, s in enumerate(spans_3):
|
|
173
|
+
offset = sum(sp["duration_ms"] for sp in spans_3[:i]) / 1000
|
|
174
|
+
started = now - 900 + offset
|
|
175
|
+
span_id = f"{t3}-{i:04d}"
|
|
176
|
+
start_record = {
|
|
177
|
+
"trace_id": t3,
|
|
178
|
+
"span_id": span_id,
|
|
179
|
+
"tool": s["tool"],
|
|
180
|
+
"started_at": datetime.fromtimestamp(started, tz=timezone.utc).isoformat(),
|
|
181
|
+
"status": "running",
|
|
182
|
+
"args_summary": s["args"],
|
|
183
|
+
}
|
|
184
|
+
end_record = {
|
|
185
|
+
"trace_id": t3,
|
|
186
|
+
"span_id": span_id,
|
|
187
|
+
"ended_at": datetime.fromtimestamp(started + s["duration_ms"] / 1000, tz=timezone.utc).isoformat(),
|
|
188
|
+
"status": s["status"],
|
|
189
|
+
"result_summary": s["result"],
|
|
190
|
+
}
|
|
191
|
+
f.write(json.dumps(start_record) + "\n")
|
|
192
|
+
f.write(json.dumps(end_record) + "\n")
|
|
193
|
+
demo_traces.append(t3)
|
|
194
|
+
|
|
195
|
+
return demo_traces
|
|
@@ -3,7 +3,7 @@ Delimit Contract Ledger
|
|
|
3
3
|
Reads, validates, and queries the append-only JSONL event ledger.
|
|
4
4
|
Optional SQLite index for fast lookups (never required for CI).
|
|
5
5
|
|
|
6
|
-
Per
|
|
6
|
+
Per Jamsons Doctrine:
|
|
7
7
|
- Deterministic outputs
|
|
8
8
|
- Append-only artifacts
|
|
9
9
|
- SQLite index is optional, not required for CI
|
|
@@ -5,7 +5,7 @@ Constructs a deterministic service dependency graph from manifests.
|
|
|
5
5
|
The graph maps each API/service to its downstream consumers,
|
|
6
6
|
enabling impact analysis when an API contract changes.
|
|
7
7
|
|
|
8
|
-
Per
|
|
8
|
+
Per Jamsons Doctrine:
|
|
9
9
|
- Deterministic outputs (sorted, reproducible)
|
|
10
10
|
- No telemetry
|
|
11
11
|
- Graceful degradation when manifests are missing
|