Bitsage commited on
Commit
5c04a53
·
1 Parent(s): ae1bf55

feat: Evidence Machine v2 — full rewrite, forensic console with NEC# constellation, citation engine, peer context, outreach tracking

Browse files
Files changed (2) hide show
  1. README.md +37 -176
  2. app.py +1330 -1196
README.md CHANGED
@@ -1,205 +1,66 @@
1
  ---
2
- title: CROVIA · CEP Terminal
3
- emoji: 🧾
4
- colorFrom: indigo
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 4.36.1
8
  python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
- short_description: Audit-first inspector for Crovia CEP evidence capsules
12
  tags:
13
  - audit
14
  - provenance
15
  - ai-act
16
  - compliance
17
- - receipts
18
- - datasets
 
19
  ---
20
 
 
21
 
22
- # CROVIA · CEP Terminal
23
- Public Evidence Inspector for AI Training Datasets
24
 
25
- Crovia CEP Terminal is a public, audit-first interface to inspect AI evidence capsules
26
- (CEP.v1 reference capsules) published as open datasets.
27
 
28
- This Space does not simulate compliance.
29
- It shows exactly what exists — and what does not.
30
 
31
- ---
32
-
33
- ## What this Space is
34
-
35
- This is not a demo with fabricated green checks.
36
-
37
- This Space is a live evidence terminal that:
38
-
39
- - loads real CEP capsules from Hugging Face datasets
40
- - performs lightweight but real structural checks
41
- - exposes missing provenance, signatures, and hashchain anchors
42
- - visualizes the actual audit surface of a dataset
43
-
44
- If something is missing, Crovia shows it.
45
-
46
- ---
47
-
48
- ## What this Space is NOT
49
-
50
- - Not a marketing dashboard
51
- - Not a compliance simulator
52
- - Not a “trust me” interface
53
- - Not a blockchain demo
54
-
55
- There are no fake receipts and no cosmetic green lights.
56
-
57
- ---
58
-
59
- ## How to read the results
60
-
61
- ### Trust Signal
62
-
63
- The Trust Signal (GREEN / AMBER / RED) is derived, not declared.
64
-
65
- A RED signal means:
66
- - missing cryptographic anchors
67
- - incomplete provenance
68
- - unverifiable payouts or receipts
69
-
70
- This is not an error.
71
- It is the correct audit outcome.
72
-
73
- ---
74
-
75
- ### Evidence Graph
76
-
77
- The graph shows what is actually bound to the capsule:
78
-
79
- - receipts
80
- - payouts
81
- - signatures
82
- - hashchain roots
83
-
84
- Unbound elements are shown explicitly.
85
-
86
- Nothing is hidden.
87
-
88
- ---
89
-
90
- ### Inspector Panel
91
-
92
- The Inspector explains why a dataset fails or passes:
93
-
94
- - missing schema
95
- - missing model metadata
96
- - missing period
97
- - missing signature
98
- - missing hashchain
99
-
100
- This is meant to be readable by:
101
- - engineers
102
- - legal teams
103
- - auditors
104
- - journalists
105
-
106
- ---
107
-
108
- ## OPEN EVIDENCE MODE (Important)
109
 
110
- This Hugging Face Space runs in OPEN EVIDENCE MODE (read-only):
111
 
112
- - **Inspect**: loads and inspects real CEP reference capsules from `Crovia/cep-capsules`
113
- - **Generate**: produces a `crovia_evidence_snapshot.v1` evidence snapshot from a target ID (HuggingFace + TPR temporal data)
 
114
 
115
- Deep cryptographic verification (DSSE, full hashchain replay, payout proofs)
116
- is intentionally delegated to the Crovia CLI / Core Engine.
117
 
118
- This keeps the Space:
119
- - fast
120
- - transparent
121
- - non-deceptive
 
 
122
 
123
- ---
124
-
125
- ## Why Crovia exists
126
-
127
- For years, the AI industry has said:
128
-
129
- “Your data was used to train the model.”
130
-
131
- Without receipts.
132
- Without numbers.
133
- Without proof.
134
-
135
- Crovia exists to enforce one rule:
136
-
137
- If an AI system makes value from data, there must be evidence.
138
-
139
- And that evidence must be:
140
- - inspectable
141
- - auditable
142
- - verifiable by anyone
143
-
144
- ---
145
-
146
- ## The uncomfortable truth
147
-
148
- Most open datasets used to train modern AI models:
149
-
150
- - do not expose provenance
151
- - do not bind payouts
152
- - do not anchor receipts
153
- - do not provide hashchains
154
-
155
- Crovia does not hide this.
156
-
157
- Crovia makes it visible.
158
-
159
- ---
160
 
161
- ## Why this Space matters
162
 
163
- This is likely the first public terminal that:
164
 
165
- - inspects real training datasets
166
- - shows compliance gaps in public
167
- - does not normalize missing evidence
168
-
169
- This Space is not here to look good.
170
- It is here to be correct.
171
-
172
- ---
173
-
174
- ## Next steps (outside this Space)
175
-
176
- To perform full verification:
177
-
178
- pip install crovia
179
-
180
- crovia explain capsule.json
181
- crovia trace receipts.ndjson --out proofs/hashchain.txt
182
- crovia verify bundle.json
183
-
184
- This Space shows what exists.
185
- The CLI proves what holds.
186
-
187
- ---
188
-
189
- ## Final note
190
-
191
- If a dataset fails here,
192
- it does not mean it is “bad”.
193
-
194
- It means:
195
-
196
- Evidence is missing.
197
-
198
- And missing evidence is exactly what Crovia was built to expose.
199
-
200
- Crovia.
201
- Evidence, not promises.
202
 
 
203
 
204
  ## License
 
205
  Apache-2.0
 
1
  ---
2
+ title: CROVIA · Evidence Machine
3
+ emoji: 🔬
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 4.36.1
8
  python_version: "3.11"
9
  app_file: app.py
10
  pinned: false
11
+ short_description: AI forensic evidence console with live data
12
  tags:
13
  - audit
14
  - provenance
15
  - ai-act
16
  - compliance
17
+ - forensics
18
+ - cryptography
19
+ - evidence
20
  ---
21
 
22
+ # CROVIA · Evidence Machine
23
 
24
+ **The world's first AI forensic evidence console.**
 
25
 
26
+ Type any AI model. Get a complete, cryptographically anchored, temporally verified evidence package showing exactly what training data documentation exists — and what doesn't.
 
27
 
28
+ ## What this does
 
29
 
30
+ - **200+ models monitored** with 20 NEC# documentation requirements each
31
+ - **Pedersen commitments** anchor every observation cryptographically
32
+ - **Chain height 3,000+** — continuous, append-only temporal proof
33
+ - **Regulatory mapping** — jurisdictions affected by each documentation gap
34
+ - **Citation engine** one-click, legal-grade evidence statements
35
+ - **Outreach tracking** — was the organization contacted? Did they respond?
36
+ - **Peer comparison** how does this model compare to its organization and industry?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ ## How to use
39
 
40
+ 1. Type a model ID (e.g. `google/gemma-3-12b-it`) in the command bar
41
+ 2. Click **INVESTIGATE**
42
+ 3. Read the signal strip, NEC# constellation, forensic report, and citation
43
 
44
+ ## What you see
 
45
 
46
+ - **Signal Strip** — Trust level, evidence strength, chain height, NEC# gaps, jurisdictions
47
+ - **NEC# Constellation** — Interactive SVG showing all 20 documentation requirements as nodes
48
+ - **NEC# Grid** — Color-coded grid of present/absent/critical elements
49
+ - **Forensic Report** — Full monospace report with cryptographic anchors
50
+ - **Jurisdictions** — Which regulations apply to the observed gaps
51
+ - **Citation Engine** — Copy-paste evidence statement for legal or journalistic use
52
 
53
+ ## CEP Capsules tab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ Backward-compatible inspector for reference CEP capsules from `Crovia/cep-capsules`.
56
 
57
+ ## Important
58
 
59
+ This is observation, not judgment. All data derived from publicly observable artifacts.
60
+ No model audit. No legal claim. Presence/absence only.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ Source: [registry.croviatrust.com](https://registry.croviatrust.com)
63
 
64
  ## License
65
+
66
  Apache-2.0
app.py CHANGED
@@ -11,59 +11,68 @@ import requests
11
  from huggingface_hub import HfApi, hf_hub_download
12
 
13
  # -----------------------------------------------------------------------------
14
- # CROVIA — CEP Terminal (UI v1: PRISM CONSOLE)
15
- # Custom HTML/CSS/JS frontend, Gradio backend + events
 
16
  # -----------------------------------------------------------------------------
17
 
18
  CEP_DATASET_ID = "Crovia/cep-capsules"
19
- TPR_API_URL = "https://registry.croviatrust.com"
20
- OPEN_EVIDENCE_MODE = True # Read-only evidence inspection, no settlement
21
-
22
- _CAPSULE_LIST_CACHE = {"ts": 0.0, "items": []}
23
- _CAPSULE_LIST_TTL_SEC = 300 # 5 min
24
-
25
- _TPR_TARGETS_CACHE = {"ts": 0.0, "items": []}
26
- _TPR_TARGETS_TTL_SEC = 300 # 5 min
27
-
28
- def _fetch_tpr_targets() -> List[Dict]:
29
- """Fetch monitored targets from TPR Registry API with caching."""
30
- now = time.time()
31
- if now - _TPR_TARGETS_CACHE["ts"] < _TPR_TARGETS_TTL_SEC and _TPR_TARGETS_CACHE["items"]:
32
- return _TPR_TARGETS_CACHE["items"]
33
- try:
34
- resp = requests.get(f"{TPR_API_URL}/api/targets/summary", timeout=5)
35
- targets = resp.json().get("targets", [])
36
- _TPR_TARGETS_CACHE["ts"] = now
37
- _TPR_TARGETS_CACHE["items"] = targets
38
- return targets
39
- except:
40
- return _TPR_TARGETS_CACHE["items"] or []
41
 
 
 
42
 
43
- def _nowz() -> str:
44
  return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
45
 
 
 
 
 
 
46
 
47
- def _canonical_json_bytes(obj: dict) -> bytes:
48
- return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
49
 
 
50
 
51
- def _sha256_hex(b: bytes) -> str:
52
- return hashlib.sha256(b).hexdigest()
 
 
 
 
 
 
 
 
 
 
53
 
 
 
54
 
55
- def _short(s: str, n: int = 16) -> str:
56
- if not isinstance(s, str) or not s:
57
- return ""
58
- return s[:n] + "…"
59
 
 
 
60
 
61
  def _list_capsules() -> List[str]:
62
- now = time.time()
63
- if (now - _CAPSULE_LIST_CACHE["ts"]) < _CAPSULE_LIST_TTL_SEC and _CAPSULE_LIST_CACHE["items"]:
64
- return _CAPSULE_LIST_CACHE["items"]
65
-
66
- items: List[str] = []
67
  try:
68
  files = HfApi().list_repo_files(repo_id=CEP_DATASET_ID, repo_type="dataset")
69
  for f in files:
@@ -72,1243 +81,1368 @@ def _list_capsules() -> List[str]:
72
  items = sorted(set(items))[:350]
73
  except Exception:
74
  items = []
75
-
76
- _CAPSULE_LIST_CACHE["ts"] = now
77
- _CAPSULE_LIST_CACHE["items"] = items
78
  return items
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- def fetch_capsule(cep_id: str) -> Dict[str, Any]:
82
- path = hf_hub_download(
83
- repo_id=CEP_DATASET_ID,
84
- filename=f"{cep_id}.json",
85
- repo_type="dataset",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  )
87
- with open(path, "r", encoding="utf-8") as f:
88
- return json.load(f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- def compute_trust(checks: Dict[str, bool]) -> Tuple[str, str, float]:
92
- missing = [k for k, v in checks.items() if not v]
93
 
94
- if not missing:
95
- return ("GREEN", "Fully anchored: evidence + signature + hashchain bound.", 1.0)
 
 
96
 
97
- if missing == ["hashchain_root"]:
98
- return ("YELLOW", "Evidence present, but not bound to a verifiable training run (missing hashchain).", 0.62)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- if missing == ["signature"]:
101
- return ("YELLOW", "Evidence present, but missing signature metadata (publisher not authenticated).", 0.58)
102
 
103
- if "evidence" in missing:
104
- return ("RED", "No evidence nodes inside capsule (cannot inspect annex/payout anchors).", 0.18)
 
105
 
106
- return ("RED", "Incomplete evidence: missing critical verification anchors.", 0.28)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
 
 
 
108
 
109
- def make_terminal_and_inspector(capsule: Dict[str, Any], cep_id: str) -> Dict[str, Any]:
110
- terminal: List[str] = []
111
- inspector: List[str] = []
 
 
 
 
112
 
113
- schema = capsule.get("schema", "unknown")
114
- period = capsule.get("period", "unknown")
115
- model = capsule.get("model", {})
116
- model_id = model.get("model_id", "unknown-model") if isinstance(model, dict) else "unknown-model"
117
 
118
- evidence = capsule.get("evidence", {})
119
- if not isinstance(evidence, dict):
120
- evidence = {}
 
 
 
121
 
122
- meta = capsule.get("meta", {}) if isinstance(capsule.get("meta"), dict) else {}
123
- hashchain_root = meta.get("hashchain_sha256", "")
124
- hashchain_present = isinstance(hashchain_root, str) and bool(hashchain_root.strip())
125
- sig_present = "signature" in capsule
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- cap_sha = _sha256_hex(_canonical_json_bytes(capsule))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- checks = {
130
- "schema": isinstance(schema, str) and schema != "unknown",
131
- "period": isinstance(period, str) and len(period) >= 4,
132
- "model_id": isinstance(model_id, str) and model_id != "unknown-model",
133
- "evidence": isinstance(evidence, dict) and len(evidence) > 0,
134
- "signature": bool(sig_present),
135
- "hashchain_root": bool(hashchain_present),
136
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- trust_level, trust_reason, trust_score = compute_trust(checks)
139
-
140
- terminal.append("CROVIA // PRISM CONSOLE v1")
141
- terminal.append(f"capsule {cep_id}")
142
- terminal.append(f"timestamp {_nowz()}")
143
- terminal.append(f"mode {'OPEN EVIDENCE (read-only)' if OPEN_EVIDENCE_MODE else 'FULL'}")
144
- terminal.append("")
145
- terminal.append("[STRUCTURE]")
146
- terminal.append(f"schema {schema}")
147
- terminal.append(f"model {model_id}")
148
- terminal.append(f"period {period}")
149
- terminal.append(f"evidence {len(evidence)} nodes")
150
- terminal.append("")
151
- terminal.append("[TRUST]")
152
- terminal.append(f"level {trust_level}")
153
- terminal.append(f"reason {trust_reason}")
154
- terminal.append("")
155
- terminal.append("[INTEGRITY]")
156
- terminal.append(f"capsule_sha256 {cap_sha}")
157
- terminal.append(f"signature {'present' if sig_present else 'missing'}")
158
- terminal.append(f"hashchain {('sha256:' + _short(hashchain_root, 16)) if hashchain_present else 'missing'}")
159
- terminal.append("")
160
-
161
- proof = f"crovia:v1;m={model_id};p={period};h={cap_sha};u=hf://{CEP_DATASET_ID}/{cep_id}.json"
162
- terminal.append("[PROOF STRING]")
163
- terminal.append(proof)
164
- terminal.append("")
165
- if trust_level != "GREEN":
166
- terminal.append("[WHY THIS MATTERS]")
167
- if not hashchain_present:
168
- terminal.append("- Not bound to a verifiable training run (no hashchain anchor).")
169
- if not sig_present:
170
- terminal.append("- Publisher not authenticated (no signature metadata).")
171
- if len(evidence) == 0:
172
- terminal.append("- No inspectable annex/payout nodes in evidence.")
173
- terminal.append("")
174
-
175
- inspector.append("INSPECTOR (real checks)")
176
- for k in ["schema", "period", "model_id", "evidence", "signature", "hashchain_root"]:
177
- inspector.append(f"- {k:13s} : {'OK' if checks[k] else 'FAIL'}")
178
- inspector.append("")
179
- inspector.append("TRUST SIGNAL")
180
- inspector.append(f"- level : {trust_level}")
181
- inspector.append(f"- score : {trust_score:.2f}")
182
- inspector.append(f"- note : {trust_reason}")
183
- inspector.append("")
184
- inspector.append("EVIDENCE NODES (first 18)")
185
- if evidence:
186
- for ek, ev in list(evidence.items())[:18]:
187
- if isinstance(ev, dict):
188
- sha = ev.get("sha256", "")
189
- url = ev.get("url", "")
190
- path = ev.get("path", "")
191
- line = f"- {ek}"
192
- if sha:
193
- line += f" | sha256:{_short(str(sha), 16)}"
194
- if url:
195
- line += f" | url:{str(url)[:88]}"
196
- if path:
197
- line += f" | path:{str(path)[:88]}"
198
- inspector.append(line)
199
- else:
200
- inspector.append(f"- {ek} | (non-object)")
201
- else:
202
- inspector.append("- (none)")
203
 
204
- nodes = list(evidence.keys())
205
- anchors = ["receipts", "payouts", "signature", "hashchain"]
206
- for a in anchors:
207
- if a not in nodes:
208
- nodes.append(a)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
- return {
211
- "cep_id": cep_id,
212
- "schema": schema,
213
- "period": period,
214
- "model_id": model_id,
215
- "trust_level": trust_level,
216
- "trust_reason": trust_reason,
217
- "trust_score": trust_score,
218
- "capsule_sha256": cap_sha,
219
- "signature_present": sig_present,
220
- "hashchain_present": hashchain_present,
221
- "hashchain_sha256": hashchain_root if hashchain_present else "",
222
- "proof": proof,
223
- "terminal": "\n".join(terminal),
224
- "inspector": "\n".join(inspector),
225
- "nodes": nodes,
226
- }
 
 
 
 
 
 
 
 
 
 
227
 
 
 
 
 
 
 
 
 
 
228
 
229
- def inspect_payload(cep_id: str) -> str:
230
- cep_id = (cep_id or "").strip()
231
- if not cep_id:
232
- return json.dumps({"error": "empty"}, ensure_ascii=False)
 
 
 
 
 
 
 
 
 
233
 
234
- try:
235
- cap = fetch_capsule(cep_id)
236
- except Exception as e:
237
- return json.dumps(
238
- {
239
- "error": "fetch_failed",
240
- "cep_id": cep_id,
241
- "detail": f"{type(e).__name__}: {e}",
242
- },
243
- ensure_ascii=False,
244
- )
245
-
246
- payload = make_terminal_and_inspector(cap, cep_id)
247
- return json.dumps(payload, ensure_ascii=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
- CSS = r"""
251
- <style>
252
- :root{
253
- --bg0:#05060a;
254
- --bg1:#070a12;
255
- --panel:#0b1020;
256
- --ink:#e7eaf1;
257
- --ink-strong:#f8fafc;
258
- --mut:#cbd5e1;
259
- --line:rgba(255,255,255,.10);
260
- --glow:rgba(56,189,248,.35);
261
- --good:#22c55e;
262
- --warn:#f59e0b;
263
- --bad:#ef4444;
264
- --cyan:#38bdf8;
265
- --shadow: 0 18px 55px rgba(0,0,0,.55);
266
- --radius:16px;
267
- --p:0.62;
268
- }
269
-
270
- html,body,#root,.gradio-container{
271
- background:var(--bg0) !important;
272
- }
273
-
274
- .gradio-container{
275
- max-width:1320px !important;
276
- margin:0 auto !important;
277
- padding:26px 18px 70px !important;
278
- font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Arial;
279
- color:var(--ink) !important;
280
- }
281
-
282
- footer{display:none !important;}
283
- .gradio-container .wrap{border:0 !important;}
284
- .gradio-container .prose{max-width:none !important;}
285
-
286
- /* Enable generator inputs */
287
- .gradio-container input[type="text"],
288
- .gradio-container textarea {
289
- pointer-events:auto !important;
290
- background:#1a1a2e !important;
291
- color:#f8fafc !important;
292
- border:1px solid rgba(139,92,246,0.3) !important;
293
- border-radius:8px !important;
294
- padding:12px !important;
295
- }
296
- .gradio-container button {
297
- pointer-events:auto !important;
298
- }
299
-
300
- /* HARD VISIBILITY (HF dimming killer) */
301
- .gradio-container, .gradio-container *{
302
- opacity:1 !important;
303
- filter:none !important;
304
- mix-blend-mode:normal !important;
305
- visibility:visible !important;
306
- }
307
-
308
- /* HERO */
309
- .crovia-hero{
310
- position:relative;
311
- border-radius:var(--radius);
312
- border:1px solid var(--line);
313
- background:
314
- radial-gradient(1000px 320px at 22% 10%, rgba(56,189,248,.18), transparent 60%),
315
- radial-gradient(800px 320px at 70% 0%, rgba(34,197,94,.14), transparent 55%),
316
- linear-gradient(180deg,#070a12,#05060a);
317
- box-shadow:var(--shadow);
318
- padding:22px 22px 18px;
319
- overflow:hidden;
320
- isolation:isolate;
321
- }
322
-
323
- .hero-top{
324
- display:flex;
325
- align-items:center;
326
- gap:14px;
327
- }
328
-
329
- .hero-logo{
330
- width:64px;height:64px;
331
- border-radius:16px;
332
- background:#070a12;
333
- border:1px solid rgba(255,255,255,.14);
334
- box-shadow:0 0 34px rgba(56,189,248,.28);
335
- display:flex;
336
- align-items:center;
337
- justify-content:center;
338
- overflow:hidden;
339
- flex:0 0 auto;
340
- }
341
-
342
- .hero-logo img{width:100%;height:100%;object-fit:cover;display:block;}
343
-
344
- .hero-title{
345
- margin:0;
346
- display:inline-block;
347
- padding:6px 10px;
348
- border-radius:12px;
349
- font-size:34px;
350
- font-weight:900;
351
- letter-spacing:.22em;
352
- text-transform:uppercase;
353
- color:#ffffff !important;
354
- background:rgba(0,0,0,.28);
355
- border:1px solid rgba(255,255,255,.08);
356
- text-shadow:
357
- 0 0 26px rgba(56,189,248,.55),
358
- 0 2px 14px rgba(0,0,0,.85);
359
- }
360
-
361
- .hero-sub{
362
- margin:10px 0 0;
363
- color:#e5e7eb !important;
364
- font-size:15px;
365
- line-height:1.7;
366
- max-width:900px;
367
- }
368
-
369
- .hero-sub strong{color:var(--ink-strong) !important;font-weight:700;}
370
-
371
- .hero-row{
372
- margin-top:14px;
373
- display:flex;
374
- gap:10px;
375
- flex-wrap:wrap;
376
- align-items:center;
377
- }
378
-
379
- .pill{
380
- display:inline-flex;
381
- align-items:center;
382
- gap:8px;
383
- padding:6px 12px;
384
- border-radius:999px;
385
- border:1px solid rgba(255,255,255,.14);
386
- background:rgba(255,255,255,.05);
387
- color:var(--ink-strong) !important;
388
- font-size:11px;
389
- letter-spacing:.14em;
390
- text-transform:uppercase;
391
- }
392
-
393
- .dot{
394
- width:8px;height:8px;
395
- border-radius:999px;
396
- background:var(--cyan);
397
- box-shadow:0 0 16px var(--glow);
398
- }
399
-
400
- .pill.good .dot{background:var(--good);box-shadow:0 0 16px rgba(34,197,94,.40);}
401
- .pill.warn .dot{background:var(--warn);box-shadow:0 0 16px rgba(245,158,11,.40);}
402
- .pill.bad .dot{background:var(--bad); box-shadow:0 0 16px rgba(239,68,68,.40);}
403
-
404
- .mini{
405
- font-size:12.5px;
406
- color:#f8fafc !important;
407
- opacity:.85;
408
- }
409
-
410
- .status-line{
411
- margin-top:14px;
412
- padding:8px 14px;
413
- border-radius:10px;
414
- border:1px solid rgba(255,255,255,.14);
415
- background:rgba(0,0,0,.35);
416
- font-size:12px;
417
- letter-spacing:.14em;
418
- text-transform:uppercase;
419
- color:#e5e7eb !important;
420
- box-shadow: inset 0 0 18px rgba(56,189,248,.12);
421
- }
422
-
423
- /* === CEP PICKER (HTML select) — NO GRADIO MOVE === */
424
- #cep_picker{
425
- margin-top:10px;
426
- width:620px;
427
- max-width:100%;
428
- }
429
-
430
- #cep_select{
431
- width:100%;
432
- height:44px;
433
- border-radius:12px;
434
- background:#05060a;
435
- color:#f8fafc;
436
- border:1px solid rgba(255,255,255,.16);
437
- padding:0 42px 0 14px;
438
- outline:none;
439
- appearance:none;
440
- -webkit-appearance:none;
441
- -moz-appearance:none;
442
- cursor:pointer;
443
- }
444
-
445
- #cep_picker{
446
- position:relative;
447
- }
448
- #cep_picker::after{
449
- content:"⌄";
450
- position:absolute;
451
- right:16px;
452
- top:50%;
453
- transform:translateY(-50%);
454
- pointer-events:none;
455
- color:#38bdf8;
456
- font-size:18px;
457
- opacity:.85;
458
- }
459
-
460
- /* GRID */
461
- .grid{
462
- margin-top:18px;
463
- display:grid;
464
- grid-template-columns:1fr 1fr;
465
- gap:16px;
466
- }
467
- @media (max-width:980px){ .grid{grid-template-columns:1fr;} }
468
-
469
- .card{
470
- border-radius:var(--radius);
471
- border:1px solid var(--line);
472
- background:
473
- radial-gradient(600px 240px at 10% 10%, rgba(56,189,248,.10), transparent 55%),
474
- linear-gradient(180deg,#070a12,#05060a);
475
- box-shadow:var(--shadow);
476
- overflow:hidden;
477
- }
478
-
479
- .card-h{
480
- padding:10px 14px;
481
- border-bottom:1px solid var(--line);
482
- letter-spacing:.18em;
483
- text-transform:uppercase;
484
- font-weight:800;
485
- font-size:12px;
486
- color:var(--ink-strong) !important;
487
- background:linear-gradient(90deg,rgba(255,255,255,.05),transparent 55%);
488
- }
489
-
490
- .card-b{padding:12px 14px;}
491
-
492
- pre.term{
493
- margin:0;
494
- padding:12px;
495
- border-radius:12px;
496
- border:1px solid rgba(255,255,255,.10);
497
- background:
498
- radial-gradient(500px 220px at 20% 10%, rgba(56,189,248,.10), transparent 55%),
499
- #05060a;
500
- color:#e5e7eb !important;
501
- font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;
502
- font-size:12.6px;
503
- line-height:1.55;
504
- min-height:330px;
505
- white-space:pre-wrap;
506
- }
507
-
508
- svg#constellation{
509
- width:100%;
510
- min-height:330px;
511
- border-radius:12px;
512
- border:1px solid rgba(255,255,255,.10);
513
- background:
514
- radial-gradient(520px 240px at 25% 15%, rgba(56,189,248,.12), transparent 60%),
515
- radial-gradient(420px 240px at 75% 80%, rgba(34,197,94,.10), transparent 55%),
516
- #05060a;
517
- }
518
-
519
- /* TRUST pill — make state readable (container, not only dot) */
520
- .pill.good{
521
- background: rgba(34,197,94,.14) !important;
522
- border-color: rgba(34,197,94,.45) !important;
523
- }
524
- .pill.warn{
525
- background: rgba(245,158,11,.14) !important;
526
- border-color: rgba(245,158,11,.45) !important;
527
- }
528
- .pill.bad{
529
- background: rgba(239,68,68,.14) !important;
530
- border-color: rgba(239,68,68,.45) !important;
531
- }
532
- #trust_text{
533
- color:#f8fafc !important;
534
- text-shadow:0 1px 12px rgba(0,0,0,.6);
535
- }
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  </style>
 
538
  """
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
- UI_HTML = r"""
542
- <div class="crovia-hero">
543
- <div class="hero-top">
544
- <div class="hero-logo">
545
- <img src="https://huggingface.co/spaces/Crovia/cep-terminal/resolve/main/crovia-logo.png" alt="CROVIA logo"/>
 
 
 
 
546
  </div>
547
- <div>
548
- <h1 class="hero-title">CROVIA · CEP TERMINAL</h1>
549
- <p class="hero-sub">
550
- Public inspection of AI training evidence capsules.
551
- <strong>No simulations. No assumptions.</strong> Only what exists.
552
- </p>
553
- <div class="hero-row">
554
- <div id="pill_trust" class="pill warn"><span class="dot"></span><span id="trust_text">TRUST: —</span></div>
555
- <div class="pill"><span class="dot"></span><span>DATASET: Crovia/cep-capsules</span></div>
556
- <div class="pill" title="Read-only evidence inspection. No settlement, no payout claims."><span class="dot"></span><span>MODE: OPEN EVIDENCE</span></div>
557
- </div>
558
- <div class="mini" id="mini_line">
559
- ▶ Select a <strong>CEP fingerprint</strong> to inspect real training evidence.
560
- </div>
561
-
562
- <div class="status-line" id="status_line">
563
- STATUS: — awaiting capsule selection
564
- </div>
565
-
566
- <div id="cep_picker"></div>
567
  </div>
568
- </div>
569
  </div>
570
 
571
- <div class="grid">
572
- <div class="card">
573
- <div class="card-h">C-LINE OUTPUT</div>
574
- <div class="card-b">
575
- <pre id="crovia_terminal" class="term">Loading…</pre>
576
- <div class="mini" id="copy_hint">—</div>
577
  </div>
578
- </div>
 
579
 
580
- <div class="card">
581
- <div class="card-h">PRISM MAP</div>
582
- <div class="card-b">
583
- <svg id="constellation" viewBox="0 0 820 360" preserveAspectRatio="xMidYMid meet"></svg>
 
 
 
 
 
 
 
584
  </div>
585
- </div>
586
 
587
- <div class="card" style="grid-column: 1 / -1;">
588
- <div class="card-h">INSPECTOR</div>
589
- <div class="card-b">
590
- <pre id="crovia_inspector" class="term" style="min-height:240px;"></pre>
 
 
 
 
 
591
  </div>
592
- </div>
593
  </div>
594
 
595
- <div class="card" style="margin-top:24px;background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.3);">
596
- <div class="card-h" style="color:#a78bfa;">📦 EVIDENCE SNAPSHOT GENERATOR</div>
597
- <div class="card-b">
598
- <p style="color:#a1a1aa;font-size:13px;margin-bottom:12px;">Generate evidence snapshot from any HuggingFace model with TPR temporal data</p>
599
- <div id="tpr_targets_container" style="margin-bottom:14px;">
600
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
601
- <span style="color:#64748b;font-size:11px;">⚡ TPR Monitored:</span>
602
- <span id="tpr_count" style="color:#a78bfa;font-size:11px;">Loading...</span>
603
- </div>
604
- <div id="tpr_quick_btns" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;"></div>
605
- <div style="display:flex;gap:8px;align-items:center;">
606
- <select id="tpr_select" style="flex:1;max-width:300px;background:#1e1b4b;border:1px solid #4c1d95;border-radius:6px;padding:8px 12px;color:#a78bfa;font-size:12px;cursor:pointer;">
607
- <option value="">— Select from all targets —</option>
608
- </select>
609
- <button id="tpr_go_btn" style="background:#4c1d95;border:none;border-radius:6px;padding:8px 16px;color:#a78bfa;font-size:12px;cursor:pointer;">Go</button>
610
- </div>
611
  </div>
612
- <div style="display:flex;gap:12px;margin-bottom:16px;">
613
- <input type="text" id="gen_input" placeholder="meta-llama/Llama-3.1-8B" style="flex:1;background:#0d1117;border:1px solid rgba(139,92,246,0.4);border-radius:8px;padding:12px 16px;color:#f8fafc;font-size:14px;outline:none;"/>
614
- <button id="gen_btn" style="background:linear-gradient(135deg,#8b5cf6,#6366f1);border:none;border-radius:8px;padding:12px 24px;color:#fff;font-weight:600;cursor:pointer;">🔨 Generate</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615
  </div>
616
- <pre id="gen_output" class="term" style="min-height:200px;max-height:400px;overflow:auto;font-size:12px;">Enter a HuggingFace model ID and click Generate</pre>
617
- <div style="display:flex;gap:12px;margin-top:16px;">
618
- <button id="dl_btn" style="background:#1e293b;border:1px solid #334155;border-radius:8px;padding:10px 20px;color:#94a3b8;font-weight:500;cursor:pointer;" disabled>💾 Download JSON</button>
619
- <button id="inspect_btn" style="background:#1e293b;border:1px solid #334155;border-radius:8px;padding:10px 20px;color:#94a3b8;font-weight:500;cursor:pointer;" disabled>🔍 Inspect</button>
620
- <button id="diff_btn" style="background:#1e293b;border:1px solid #334155;border-radius:8px;padding:10px 20px;color:#94a3b8;font-weight:500;cursor:pointer;" disabled>⚖️ Diff Against Reality</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  </div>
622
- <pre id="diff_output" class="term" style="min-height:100px;max-height:300px;overflow:auto;font-size:11px;margin-top:16px;display:none;background:#0f172a;border:1px solid #334155;"></pre>
623
- </div>
 
 
 
 
 
 
624
  </div>
625
  """
626
 
 
 
 
627
 
628
  JS = r"""
629
  () => {
630
- const $ = (q) => document.querySelector(q);
631
-
632
- function setPill(level){
633
- const pill = $("#pill_trust");
634
- if(!pill) return;
635
- pill.classList.remove("good","warn","bad");
636
- if(level==="GREEN") pill.classList.add("good");
637
- else if(level==="YELLOW") pill.classList.add("warn");
638
- else pill.classList.add("bad");
639
- }
640
-
641
- function drawConstellation(payload){
642
- const svg = $("#constellation");
643
- if(!svg) return;
644
- while(svg.firstChild) svg.removeChild(svg.firstChild);
645
-
646
- const W = 820, H = 360;
647
- const cx = W/2, cy = H/2 + 6;
648
- const r = 130;
649
-
650
- const nodes = (payload.nodes || []).slice(0, 28);
651
- const anchors = new Set(["receipts","payouts","signature","hashchain"]);
652
- const sigOn = !!payload.signature_present;
653
- const hcOn = !!payload.hashchain_present;
654
-
655
- function colorOf(name){
656
- if(name==="signature") return sigOn ? "#22c55e" : "rgba(255,255,255,.35)";
657
- if(name==="hashchain") return hcOn ? "#22c55e" : "rgba(255,255,255,.35)";
658
- if(name==="receipts" || name==="payouts") return "#38bdf8";
659
- return anchors.has(name) ? "#38bdf8" : "rgba(231,234,241,.72)";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  }
661
 
662
- const bg = document.createElementNS("http://www.w3.org/2000/svg","circle");
663
- bg.setAttribute("cx", cx); bg.setAttribute("cy", cy);
664
- bg.setAttribute("r", 150);
665
- bg.setAttribute("fill", "rgba(56,189,248,.06)");
666
- svg.appendChild(bg);
667
-
668
- nodes.forEach((name, i) => {
669
- const a = (Math.PI * 2) * (i / Math.max(1, nodes.length));
670
- const x = cx + r * Math.cos(a);
671
- const y = cy + r * Math.sin(a);
672
-
673
- const line = document.createElementNS("http://www.w3.org/2000/svg","line");
674
- line.setAttribute("x1", cx); line.setAttribute("y1", cy);
675
- line.setAttribute("x2", x); line.setAttribute("y2", y);
676
- line.setAttribute("stroke", "rgba(255,255,255,.16)");
677
- line.setAttribute("stroke-width", "1");
678
- svg.appendChild(line);
679
- });
680
-
681
- const center = document.createElementNS("http://www.w3.org/2000/svg","circle");
682
- center.setAttribute("cx", cx); center.setAttribute("cy", cy);
683
- center.setAttribute("r", 10);
684
- center.setAttribute("fill", "#38bdf8");
685
- svg.appendChild(center);
686
-
687
- const ctext = document.createElementNS("http://www.w3.org/2000/svg","text");
688
- ctext.setAttribute("x", cx);
689
- ctext.setAttribute("y", cy + 28);
690
- ctext.setAttribute("text-anchor", "middle");
691
- ctext.setAttribute("fill", "rgba(231,234,241,.92)");
692
- ctext.setAttribute("font-size", "12");
693
- ctext.textContent = payload.cep_id || "CEP";
694
- svg.appendChild(ctext);
695
-
696
- nodes.forEach((name, i) => {
697
- const a = (Math.PI * 2) * (i / Math.max(1, nodes.length));
698
- const x = cx + r * Math.cos(a);
699
- const y = cy + r * Math.sin(a);
700
-
701
- const dot = document.createElementNS("http://www.w3.org/2000/svg","circle");
702
- dot.setAttribute("cx", x); dot.setAttribute("cy", y);
703
- dot.setAttribute("r", anchors.has(name) ? 7 : 5);
704
- dot.setAttribute("fill", colorOf(name));
705
- svg.appendChild(dot);
706
-
707
- const t = document.createElementNS("http://www.w3.org/2000/svg","text");
708
- t.setAttribute("x", x);
709
- t.setAttribute("y", y - 10);
710
- t.setAttribute("text-anchor", "middle");
711
- t.setAttribute("fill", "rgba(231,234,241,.78)");
712
- t.setAttribute("font-size", "11");
713
- t.textContent = name;
714
- svg.appendChild(t);
715
- });
716
- }
717
-
718
- function updateUI(payload){
719
- if(!payload || payload.error){
720
- $("#crovia_terminal").textContent =
721
- "CROVIA // PRISM CONSOLE v1\n\n[ERROR]\n" + (payload?.detail || payload?.error || "unknown");
722
- $("#crovia_inspector").textContent = "—";
723
- $("#trust_text").textContent = "TRUST: ERROR";
724
- setPill("RED");
725
- drawConstellation({
726
- cep_id: payload?.cep_id || "CEP",
727
- nodes:["receipts","payouts","signature","hashchain"],
728
- signature_present:false,
729
- hashchain_present:false
730
- });
731
- return;
732
  }
733
 
734
- $("#crovia_terminal").textContent = payload.terminal || "—";
735
- $("#crovia_inspector").textContent = payload.inspector || "—";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
 
737
- const level = payload.trust_level || "YELLOW";
738
- $("#trust_text").textContent = "TRUST: " + level;
739
- setPill(level);
 
 
 
740
 
741
- $("#mini_line").textContent = payload.trust_reason || "Select a CEP fingerprint to light up the Prism.";
742
- drawConstellation(payload);
743
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
 
745
- function attachPayloadWatcher(){
746
- const root = document.querySelector("#crovia_payload");
747
- if(!root) return false;
 
 
 
 
 
 
 
 
 
 
 
748
 
749
- const input = root.querySelector("textarea, input");
750
- if(!input) return false;
 
 
 
 
 
751
 
752
- let last = "";
753
- const tick = () => {
754
- const val = input.value || "";
755
- if(val && val !== last){
756
- last = val;
757
- try{ updateUI(JSON.parse(val)); }catch(e){}
758
- }
759
- };
760
 
761
- input.addEventListener("input", tick);
762
- setInterval(tick, 250);
763
- return true;
764
- }
765
-
766
- // Build a REAL HTML <select> inside #cep_picker (no Gradio DOM move)
767
- function mountCepSelect(){
768
- const host = $("#cep_picker");
769
- if(!host) return false;
770
-
771
- // already mounted
772
- if(host.querySelector("#cep_select")) return true;
773
-
774
- const capsRoot = document.querySelector("#capsules_json");
775
- const capsInput = capsRoot ? capsRoot.querySelector("textarea, input") : null;
776
- if(!capsInput) return false;
777
-
778
- let caps = [];
779
- try{ caps = JSON.parse(capsInput.value || "[]"); }catch(e){ caps = []; }
780
-
781
- const cepRoot = document.querySelector("#cep_in");
782
- const cepInput = cepRoot ? cepRoot.querySelector("textarea, input") : null;
783
- if(!cepInput) return false;
784
-
785
- const sel = document.createElement("select");
786
- sel.id = "cep_select";
787
-
788
- caps.forEach((c) => {
789
- const opt = document.createElement("option");
790
- opt.value = c;
791
- opt.textContent = c;
792
- sel.appendChild(opt);
793
- });
794
-
795
- if(cepInput.value){
796
- sel.value = cepInput.value;
797
- }else if(caps.length){
798
- sel.value = caps[0];
799
- cepInput.value = caps[0];
800
- cepInput.dispatchEvent(new Event("input", { bubbles:true }));
801
  }
802
 
803
- sel.addEventListener("change", () => {
804
- cepInput.value = sel.value;
805
- // Trigger Gradio
806
- cepInput.dispatchEvent(new Event("input", { bubbles:true }));
807
- cepInput.dispatchEvent(new Event("change", { bubbles:true }));
808
- });
809
-
810
- host.appendChild(sel);
811
- return true;
812
- }
813
-
814
- function mountGenerator(){
815
- const btn = $("#gen_btn");
816
- const inp = $("#gen_input");
817
- const out = $("#gen_output");
818
- const dlBtn = $("#dl_btn");
819
- const inspBtn = $("#inspect_btn");
820
- const diffBtn = $("#diff_btn");
821
- const diffOut = $("#diff_output");
822
- if(!btn || !inp || !out) return false;
823
- if(btn.dataset.mounted) return true;
824
- btn.dataset.mounted = "1";
825
-
826
- const genRoot = document.querySelector("#gen_model_in");
827
- const genInput = genRoot ? genRoot.querySelector("textarea, input") : null;
828
- if(!genInput) return false;
829
-
830
- const resRoot = document.querySelector("#gen_result");
831
- const resInput = resRoot ? resRoot.querySelector("textarea, input") : null;
832
- if(!resInput) return false;
833
-
834
- const diffRoot = document.querySelector("#diff_result");
835
- const diffInput = diffRoot ? diffRoot.querySelector("textarea, input") : null;
836
-
837
- let currentCep = null;
838
- let currentModel = "";
839
-
840
- const enableButtons = (enable) => {
841
- if(dlBtn) dlBtn.disabled = !enable;
842
- if(inspBtn) inspBtn.disabled = !enable;
843
- if(diffBtn) diffBtn.disabled = !enable;
844
- if(enable){
845
- dlBtn.style.color = "#a78bfa";
846
- dlBtn.style.borderColor = "#6366f1";
847
- inspBtn.style.color = "#a78bfa";
848
- inspBtn.style.borderColor = "#6366f1";
849
- diffBtn.style.color = "#a78bfa";
850
- diffBtn.style.borderColor = "#6366f1";
851
- }
852
- };
853
-
854
- const doGen = () => {
855
- const model = inp.value.trim();
856
- if(!model){ out.textContent = '{"error": "Model ID required"}'; return; }
857
- out.textContent = 'Generating CEP for ' + model + '...';
858
- btn.disabled = true;
859
- btn.textContent = '⏳ Generating...';
860
- enableButtons(false);
861
- currentModel = model;
862
- genInput.value = model;
863
- genInput.dispatchEvent(new Event("input", {bubbles:true}));
864
- };
865
-
866
- btn.addEventListener("click", doGen);
867
- inp.addEventListener("keydown", (e) => { if(e.key==="Enter") doGen(); });
868
-
869
- // TASK 5: Download JSON
870
- if(dlBtn){
871
- dlBtn.addEventListener("click", () => {
872
- if(!currentCep) return;
873
- const blob = new Blob([currentCep], {type: "application/json"});
874
- const url = URL.createObjectURL(blob);
875
- const a = document.createElement("a");
876
- a.href = url;
877
- a.download = "CEP-" + currentModel.replace(/\//g, "_") + "-" + new Date().toISOString().slice(0,10) + ".json";
878
- a.click();
879
- URL.revokeObjectURL(url);
880
- });
881
  }
882
-
883
- // TASK 6: Inspect - scroll to inspector
884
- if(inspBtn){
885
- inspBtn.addEventListener("click", () => {
886
- const inspector = $("#crovia_inspector");
887
- if(inspector && currentCep){
888
- try{
889
- const parsed = JSON.parse(currentCep);
890
- const ev = parsed.crovia_evidence || parsed;
891
- inspector.textContent = "=== GENERATED CEP INSPECTION ===\\n\\n" + JSON.stringify(ev, null, 2);
892
- inspector.scrollIntoView({behavior: "smooth"});
893
- }catch(e){}
894
- }
895
- });
 
 
 
 
 
 
 
 
 
 
896
  }
897
-
898
- // TASK 6: Diff Against Reality
899
- if(diffBtn && diffOut){
900
- diffBtn.addEventListener("click", () => {
901
- if(!currentCep || !currentModel) return;
902
- diffOut.style.display = "block";
903
- diffOut.textContent = "Computing diff against reality for " + currentModel + "...";
904
- // Trigger backend diff
905
- const diffInRoot = document.querySelector("#diff_model_in");
906
- const diffInInput = diffInRoot ? diffInRoot.querySelector("textarea, input") : null;
907
- if(diffInInput){
908
- diffInInput.value = currentModel + "|||" + currentCep;
909
- diffInInput.dispatchEvent(new Event("input", {bubbles:true}));
910
- }
911
- });
 
 
 
 
 
 
 
 
 
 
912
  }
913
-
914
- let lastRes = "";
915
- setInterval(() => {
916
- const val = resInput.value || "";
917
- if(val && val !== lastRes){
918
- lastRes = val;
919
- out.textContent = val;
920
- btn.disabled = false;
921
- btn.textContent = '🔨 Generate';
922
- try{
923
- JSON.parse(val);
924
- currentCep = val;
925
- enableButtons(true);
926
- }catch(e){
927
- currentCep = null;
928
- enableButtons(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
  }
930
- }
931
- // Watch diff result
932
- if(diffInput && diffOut){
933
- const dval = diffInput.value || "";
934
- if(dval && diffOut.textContent.includes("Computing diff")){
935
- diffOut.textContent = dval;
 
 
 
 
 
 
 
 
 
936
  }
937
- }
938
  }, 200);
939
-
940
- return true;
941
- }
942
-
943
- function mountTprButtons(){
944
- const container = $("#tpr_targets_container");
945
- const quickBtns = $("#tpr_quick_btns");
946
- const countEl = $("#tpr_count");
947
- const selectEl = $("#tpr_select");
948
- const goBtn = $("#tpr_go_btn");
949
- const inp = $("#gen_input");
950
- const genBtn = $("#gen_btn");
951
- if(!container || !quickBtns || !selectEl || !inp || !genBtn) return false;
952
- if(container.dataset.mounted) return true;
953
-
954
- const tprRoot = document.querySelector("#tpr_targets_json");
955
- const tprInput = tprRoot ? tprRoot.querySelector("textarea, input") : null;
956
- if(!tprInput || !tprInput.value) return false;
957
-
958
- let targets = [];
959
- try { targets = JSON.parse(tprInput.value); } catch(e) { return false; }
960
- if(!targets.length) return false;
961
-
962
- container.dataset.mounted = "1";
963
-
964
- // Sort by days_monitored descending
965
- targets.sort((a,b) => (b.days_monitored || 0) - (a.days_monitored || 0));
966
-
967
- // Update count
968
- if(countEl) countEl.textContent = targets.length + " targets live";
969
-
970
- const icons = {"model":"🤖","dataset":"📊","repo":"📦"};
971
-
972
- // Top 6 quick buttons
973
- const topTargets = targets.slice(0, 6);
974
- topTargets.forEach(t => {
975
- const btn = document.createElement("button");
976
- btn.className = "tpr-btn";
977
- btn.style.cssText = "background:#1e1b4b;border:1px solid #4c1d95;border-radius:6px;padding:5px 10px;color:#a78bfa;font-size:11px;cursor:pointer;";
978
- const icon = icons[t.tipo_target] || "📌";
979
- const name = t.target_id.split("/").pop().slice(0,12);
980
- btn.textContent = icon + " " + name;
981
- btn.title = t.target_id + " (" + Math.round(t.days_monitored) + "d)";
982
- btn.addEventListener("click", () => {
983
- inp.value = t.target_id;
984
- genBtn.click();
985
- });
986
- quickBtns.appendChild(btn);
987
- });
988
-
989
- // Populate dropdown with all targets
990
- targets.forEach(t => {
991
- const opt = document.createElement("option");
992
- opt.value = t.target_id;
993
- const icon = icons[t.tipo_target] || "📌";
994
- opt.textContent = icon + " " + t.target_id + " (" + Math.round(t.days_monitored) + "d)";
995
- selectEl.appendChild(opt);
996
- });
997
-
998
- // Go button handler
999
- if(goBtn){
1000
- goBtn.addEventListener("click", () => {
1001
- if(selectEl.value){
1002
- inp.value = selectEl.value;
1003
- genBtn.click();
1004
- }
1005
- });
1006
- }
1007
-
1008
- // Double-click on select also triggers
1009
- selectEl.addEventListener("dblclick", () => {
1010
- if(selectEl.value){
1011
- inp.value = selectEl.value;
1012
- genBtn.click();
1013
- }
1014
- });
1015
-
1016
- return true;
1017
- }
1018
-
1019
- function checkUrlParam(){
1020
- const params = new URLSearchParams(window.location.search);
1021
- const model = params.get("model");
1022
- if(model){
1023
- const inp = $("#gen_input");
1024
- const genBtn = $("#gen_btn");
1025
- if(inp && genBtn){
1026
- inp.value = model;
1027
- setTimeout(() => genBtn.click(), 500);
1028
- }
1029
- }
1030
- }
1031
-
1032
- const boot = setInterval(() => {
1033
- const ok1 = mountCepSelect();
1034
- const ok2 = attachPayloadWatcher();
1035
- const ok3 = mountGenerator();
1036
- const ok4 = mountTprButtons();
1037
- if(ok1 && ok2 && ok3 && ok4){
1038
- clearInterval(boot);
1039
- checkUrlParam();
1040
- }
1041
- }, 200);
1042
 
1043
- return [];
1044
  }
1045
  """
1046
 
1047
- capsules = _list_capsules()
1048
- default_cep = capsules[0] if capsules else "CEP-2511-K4I7X2"
1049
-
1050
- def _canonical_json(obj):
1051
- """Canonical JSON: UTF-8, sort_keys=True, separators=(',',':'), no spaces."""
1052
- return json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
1053
 
1054
- def _canonical_hash(obj):
1055
- """SHA-256 of canonical JSON."""
1056
- return hashlib.sha256(_canonical_json(obj).encode('utf-8')).hexdigest()
1057
-
1058
- def _normalize_ts(ts):
1059
- """Normalize timestamp to ISO 8601 UTC with Z suffix."""
1060
- if not ts:
1061
- return None
1062
- ts = str(ts).strip()
1063
- if ts.endswith('Z'):
1064
- return ts
1065
- if '+00:00' in ts:
1066
- return ts.replace('+00:00', 'Z')
1067
- if len(ts) == 19: # No timezone info
1068
- return ts + 'Z'
1069
- return ts
1070
-
1071
- def _utc_now_z():
1072
- """Current UTC timestamp with Z suffix."""
1073
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1074
-
1075
- # Canonical empty structures for hashing
1076
- EMPTY_RECEIPTS_CANONICAL = {"items": [], "schema": "royalty_receipts.v1"}
1077
- EMPTY_PAYOUTS_CANONICAL = {"items": [], "schema": "payouts.v1"}
1078
- EMPTY_ROOT_HASH = hashlib.sha256(b"CROVIA_EMPTY_ROOT").hexdigest()
1079
-
1080
- def generate_cep(model_id):
1081
- """Generate CEP.v1 from HF model + TPR temporal data. CROVIA-GRADE spec compliant."""
1082
- if not model_id or not model_id.strip():
1083
- return '{"error": "Model ID required"}'
1084
- try:
1085
- model_id = model_id.strip()
1086
-
1087
- # Fetch TPR temporal data first (works for all targets)
1088
- tpr = None
1089
- try:
1090
- resp = requests.get(f"{TPR_API_URL}/api/targets/summary", timeout=5)
1091
- tpr = next((x for x in resp.json().get('targets', []) if x['target_id'] == model_id), None)
1092
- except:
1093
- pass
1094
-
1095
- # Try HF API (may fail for non-HF models like openai/*)
1096
- hf_info = None
1097
- try:
1098
- hf_api = HfApi()
1099
- hf_info = hf_api.model_info(model_id, securityStatus=False)
1100
- except Exception:
1101
- pass # Model not on HuggingFace - proceed with TPR data only
1102
-
1103
- # If neither HF nor TPR has data, we can't generate
1104
- if not hf_info and not tpr:
1105
- return json.dumps({"error": f"Target '{model_id}' not found on HuggingFace and not monitored by TPR."}, indent=2)
1106
-
1107
- period = datetime.now(timezone.utc).strftime("%Y-%m")
1108
- now_z = _utc_now_z()
1109
-
1110
- # Build metadata from available sources
1111
- if hf_info:
1112
- datasets = hf_info.card_data.get("datasets", []) if hf_info.card_data else []
1113
- if datasets is None:
1114
- datasets = []
1115
- meta = {
1116
- "model_id": model_id,
1117
- "author": hf_info.author or "unknown",
1118
- "license": hf_info.card_data.get("license") if hf_info.card_data else "unspecified",
1119
- "datasets_declared": datasets,
1120
- "tags": hf_info.tags[:10] if hf_info.tags else [],
1121
- "source": "huggingface"
1122
- }
1123
- else:
1124
- # TPR-only target (e.g., openai/gpt-4-turbo)
1125
- meta = {
1126
- "model_id": model_id,
1127
- "author": model_id.split("/")[0] if "/" in model_id else "unknown",
1128
- "license": "unspecified",
1129
- "datasets_declared": [],
1130
- "tags": [],
1131
- "source": "tpr_only",
1132
- "note": "Model not hosted on HuggingFace. Metadata derived from TPR monitoring."
1133
- }
1134
-
1135
- # TASK 1: ISO 8601 UTC with Z for temporal_evidence
1136
- if tpr:
1137
- meta["temporal_evidence"] = {
1138
- "days_monitored": int(tpr.get('days_monitored', 0)),
1139
- "observation_count": int(tpr.get('observation_count', 0)),
1140
- "absence_streak_days": int(tpr.get('absence_streak_days', 0)),
1141
- "first_seen": _normalize_ts(tpr.get('first_seen')),
1142
- "last_seen": _normalize_ts(tpr.get('last_seen')),
1143
- "source": "TPR Registry"
1144
- }
1145
-
1146
- # TASK 3 & 4: Canonical hashing with pluralized schemas
1147
- trust_bundle_obj = {"schema": "trust_bundle.v1", "period": period}
1148
- tb_hash = _canonical_hash(trust_bundle_obj)
1149
-
1150
- receipts_obj = EMPTY_RECEIPTS_CANONICAL.copy()
1151
- rec_hash = _canonical_hash(receipts_obj)
1152
-
1153
- payouts_obj = {**EMPTY_PAYOUTS_CANONICAL, "period": period}
1154
- pay_hash = _canonical_hash(payouts_obj)
1155
-
1156
- payload_hash = _canonical_hash(meta)
1157
-
1158
- # Generate fingerprint: ES-YYMM-NAME-HASH4
1159
- model_short = model_id.strip().split("/")[-1][:8].upper().replace("-", "")
1160
- hash_short = payload_hash[:4].upper()
1161
- fingerprint = f"ES-{period.replace('-', '')}-{model_short}-{hash_short}"
1162
-
1163
- # TASK 4: Include hashing spec in output
1164
- hashing_spec = {
1165
- "json_canonical": "utf-8, sort_keys=true, separators=(',',':'), no trailing spaces",
1166
- "empty_list_representation": "[]",
1167
- "empty_root_definition": "sha256(utf8('CROVIA_EMPTY_ROOT'))",
1168
- "empty_root_value": EMPTY_ROOT_HASH
1169
- }
1170
-
1171
- capsule = {
1172
- "schema": "crovia_evidence_snapshot.v1",
1173
- "fingerprint": fingerprint,
1174
- "crovia_evidence": {
1175
- "protocol": "CEP.v1",
1176
- "generated_at": now_z, # TASK 1
1177
- "model_metadata": meta,
1178
- "trust_bundle": {
1179
- "schema": "trust_bundle.v1",
1180
- "sha256": tb_hash,
1181
- "period": period
1182
- },
1183
- "receipts": {
1184
- "count": 0,
1185
- "sha256": rec_hash,
1186
- "schema": "royalty_receipts.v1" # TASK 3: pluralized
1187
- },
1188
- "payouts": {
1189
- "sha256": pay_hash,
1190
- "schema": "payouts.v1",
1191
- "period": period
1192
- },
1193
- "hash_chain": {
1194
- "root": EMPTY_ROOT_HASH,
1195
- "root_note": "Deterministic root. EMPTY_ROOT when no receipts are present.",
1196
- "verified": False,
1197
- "verified_note": "User-generated capsule: structure+hashes are reproducible; external attestation not provided.",
1198
- "source": "user_generated",
1199
- "algorithm": "sha256(utf8('CROVIA_EMPTY_ROOT'))"
1200
- },
1201
- "payload_hash": payload_hash,
1202
- "hashing": hashing_spec, # TASK 4
1203
- "generated_by": {
1204
- "engine": "CEP Terminal v3.1.0",
1205
- "version": "3.1.0",
1206
- "timestamp": now_z # TASK 1
1207
- }
1208
- }
1209
- }
1210
-
1211
- return json.dumps(capsule, indent=2, ensure_ascii=False)
1212
- except Exception as e:
1213
- return json.dumps({"error": f"{type(e).__name__}: {str(e)}"}, indent=2)
1214
 
1215
- with gr.Blocks(
1216
- title="CROVIA · CEP Terminal",
1217
- css=CSS,
1218
- js=JS
1219
- ) as demo:
1220
-
1221
  gr.HTML(UI_HTML)
1222
 
1223
- # Hidden components for Inspector (JS writes/reads)
1224
- capsules_json = gr.Textbox(value=json.dumps(capsules, ensure_ascii=False), visible=False, elem_id="capsules_json")
1225
- cep_in = gr.Textbox(value=default_cep, visible=False, elem_id="cep_in")
1226
- payload = gr.Textbox(value="", visible=False, elem_id="crovia_payload")
1227
-
1228
- # Hidden component for TPR targets (dynamic buttons)
1229
- tpr_targets = _fetch_tpr_targets()
1230
- tpr_targets_json = gr.Textbox(value=json.dumps(tpr_targets, ensure_ascii=False), visible=False, elem_id="tpr_targets_json")
1231
 
1232
- # Hidden components for Generator (JS writes/reads)
1233
- gen_model_in = gr.Textbox(value="", visible=False, elem_id="gen_model_in")
1234
- gen_result = gr.Textbox(value="", visible=False, elem_id="gen_result")
1235
-
1236
- # Hidden components for Diff (JS writes/reads)
1237
- diff_model_in = gr.Textbox(value="", visible=False, elem_id="diff_model_in")
1238
- diff_result = gr.Textbox(value="", visible=False, elem_id="diff_result")
1239
 
1240
- def _run(cep_id: str) -> str:
1241
- return inspect_payload(cep_id)
1242
-
1243
- def diff_against_reality(input_str: str) -> str:
1244
- """TASK 6: Compare generated CEP against current HuggingFace reality."""
1245
- if not input_str or "|||" not in input_str:
1246
- return '{"error": "Invalid input"}'
1247
- try:
1248
- model_id, cep_json = input_str.split("|||", 1)
1249
- model_id = model_id.strip()
1250
-
1251
- # Parse generated CEP
1252
- generated = json.loads(cep_json)
1253
- gen_meta = generated.get("crovia_evidence", {}).get("model_metadata", {})
1254
-
1255
- # Fetch current reality from HuggingFace
1256
- hf_api = HfApi()
1257
- info = hf_api.model_info(model_id, securityStatus=False)
1258
-
1259
- reality = {
1260
- "model_id": model_id,
1261
- "author": info.author or "unknown",
1262
- "license": info.card_data.get("license") if info.card_data else "unspecified",
1263
- "datasets_declared": info.card_data.get("datasets", []) if info.card_data else [],
1264
- "tags": info.tags[:10] if info.tags else []
1265
- }
1266
-
1267
- # Compute diff
1268
- diff_fields = []
1269
- for key in set(list(gen_meta.keys()) + list(reality.keys())):
1270
- if key == "temporal_evidence":
1271
- continue # Skip TPR data in diff
1272
- gen_val = gen_meta.get(key)
1273
- real_val = reality.get(key)
1274
- if gen_val != real_val:
1275
- diff_fields.append({
1276
- "field": key,
1277
- "generated": gen_val,
1278
- "current_reality": real_val,
1279
- "status": "MISMATCH" if gen_val and real_val else ("MISSING_IN_GENERATED" if real_val else "MISSING_IN_REALITY")
1280
- })
1281
-
1282
- # Compute difference fingerprint
1283
- diff_canonical = _canonical_json({"model_id": model_id, "diff_fields": diff_fields, "computed_at": _utc_now_z()})
1284
- diff_fingerprint = hashlib.sha256(diff_canonical.encode('utf-8')).hexdigest()
1285
-
1286
- result = {
1287
- "diff_against_reality": {
1288
- "model_id": model_id,
1289
- "computed_at": _utc_now_z(),
1290
- "fields_compared": len(set(list(gen_meta.keys()) + list(reality.keys()))) - 1,
1291
- "mismatches_found": len(diff_fields),
1292
- "diff_fields": diff_fields if diff_fields else "NO_DIFFERENCES",
1293
- "difference_fingerprint": diff_fingerprint,
1294
- "note": "This is an objective diff. No judgment, only facts."
1295
- }
1296
- }
1297
-
1298
- return json.dumps(result, indent=2, ensure_ascii=False)
1299
- except Exception as e:
1300
- return json.dumps({"error": f"{type(e).__name__}: {str(e)}"}, indent=2)
1301
-
1302
- # Inspector: cep_in changes -> compute payload
1303
- cep_in.change(_run, inputs=cep_in, outputs=payload)
1304
- demo.load(_run, inputs=cep_in, outputs=payload)
1305
-
1306
- # Generator: gen_model_in changes -> compute gen_result
1307
- gen_model_in.change(generate_cep, inputs=gen_model_in, outputs=gen_result)
1308
-
1309
- # Diff: diff_model_in changes -> compute diff_result
1310
- diff_model_in.change(diff_against_reality, inputs=diff_model_in, outputs=diff_result)
1311
 
1312
  demo.queue()
1313
  demo.launch()
1314
-
 
11
  from huggingface_hub import HfApi, hf_hub_download
12
 
13
  # -----------------------------------------------------------------------------
14
+ # CROVIA — CEP TERMINAL v2: EVIDENCE MACHINE
15
+ # World's first AI forensic evidence console
16
+ # Temporal proof + cryptographic anchoring + regulatory mapping + citation
17
  # -----------------------------------------------------------------------------
18
 
19
  CEP_DATASET_ID = "Crovia/cep-capsules"
20
+ REGISTRY_URL = "https://registry.croviatrust.com"
21
+ OPEN_EVIDENCE_MODE = True
22
+
23
+ # --- Caches ---
24
+ _CACHE = {
25
+ "tpa": {"ts": 0.0, "data": None},
26
+ "lineage": {"ts": 0.0, "data": None},
27
+ "outreach": {"ts": 0.0, "data": None},
28
+ "capsules": {"ts": 0.0, "data": []},
29
+ }
30
+ _TTL = 300 # 5 min
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ def _now():
33
+ return time.time()
34
 
35
+ def _nowz():
36
  return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
37
 
38
+ def _sha256_hex(b: bytes) -> str:
39
+ return hashlib.sha256(b).hexdigest()
40
+
41
+ def _canonical_json(obj):
42
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
43
 
44
+ def _canonical_hash(obj):
45
+ return _sha256_hex(_canonical_json(obj).encode("utf-8"))
46
 
47
+ # --- Registry Data Fetchers ---
48
 
49
+ def _fetch_cached(key, url):
50
+ now = _now()
51
+ if now - _CACHE[key]["ts"] < _TTL and _CACHE[key]["data"] is not None:
52
+ return _CACHE[key]["data"]
53
+ try:
54
+ resp = requests.get(url, timeout=8)
55
+ data = resp.json()
56
+ _CACHE[key]["ts"] = now
57
+ _CACHE[key]["data"] = data
58
+ return data
59
+ except Exception:
60
+ return _CACHE[key]["data"] or {}
61
 
62
+ def fetch_tpa():
63
+ return _fetch_cached("tpa", f"{REGISTRY_URL}/registry/data/tpa_latest.json")
64
 
65
+ def fetch_lineage():
66
+ return _fetch_cached("lineage", f"{REGISTRY_URL}/registry/data/lineage_graph.json")
 
 
67
 
68
+ def fetch_outreach():
69
+ return _fetch_cached("outreach", f"{REGISTRY_URL}/registry/data/outreach_status.json")
70
 
71
  def _list_capsules() -> List[str]:
72
+ now = _now()
73
+ if (now - _CACHE["capsules"]["ts"]) < _TTL and _CACHE["capsules"]["data"]:
74
+ return _CACHE["capsules"]["data"]
75
+ items = []
 
76
  try:
77
  files = HfApi().list_repo_files(repo_id=CEP_DATASET_ID, repo_type="dataset")
78
  for f in files:
 
81
  items = sorted(set(items))[:350]
82
  except Exception:
83
  items = []
84
+ _CACHE["capsules"]["ts"] = now
85
+ _CACHE["capsules"]["data"] = items
 
86
  return items
87
 
88
+ # --- Evidence Computation ---
89
+
90
+ def get_model_tpa(model_id: str) -> dict:
91
+ """Get TPA data for a specific model."""
92
+ tpa = fetch_tpa()
93
+ tpas = tpa.get("tpas", [])
94
+ for t in tpas:
95
+ if t.get("model_id", "").lower() == model_id.lower():
96
+ return t
97
+ return {}
98
+
99
+ def get_model_lineage(model_id: str) -> dict:
100
+ """Get lineage node for a model."""
101
+ lg = fetch_lineage()
102
+ for node in lg.get("nodes", []):
103
+ if node.get("id", "").lower() == model_id.lower():
104
+ return node
105
+ return {}
106
+
107
+ def get_model_outreach(model_id: str) -> dict:
108
+ """Get outreach status for a model's org."""
109
+ org = model_id.split("/")[0] if "/" in model_id else ""
110
+ if not org:
111
+ return {}
112
+ outreach = fetch_outreach()
113
+ entries = outreach if isinstance(outreach, list) else outreach.get("entries", outreach.get("organizations", []))
114
+ if isinstance(entries, list):
115
+ for e in entries:
116
+ oid = e.get("org", e.get("organization", ""))
117
+ if oid.lower() == org.lower():
118
+ return e
119
+ return {}
120
+
121
+ def compute_evidence_strength(tpa_entry: dict) -> dict:
122
+ """Compute evidence strength from NEC# observations."""
123
+ obs = tpa_entry.get("observations", [])
124
+ if not obs:
125
+ return {"score": 0, "total": 0, "present": 0, "absent": 0, "critical_gaps": 0}
126
+
127
+ total = len(obs)
128
+ present = sum(1 for o in obs if o.get("is_present"))
129
+ absent = total - present
130
+ critical = sum(1 for o in obs if not o.get("is_present") and o.get("severity_label") == "CRITICAL")
131
+ score = round((present / total) * 100, 1) if total > 0 else 0
132
+
133
+ return {
134
+ "score": score,
135
+ "total": total,
136
+ "present": present,
137
+ "absent": absent,
138
+ "critical_gaps": critical,
139
+ }
140
 
141
+ def compute_peer_context(model_id: str) -> dict:
142
+ """Compute peer comparison context."""
143
+ tpa = fetch_tpa()
144
+ tpas = tpa.get("tpas", [])
145
+
146
+ org = model_id.split("/")[0] if "/" in model_id else ""
147
+
148
+ all_scores = []
149
+ org_scores = []
150
+ for t in tpas:
151
+ obs = t.get("observations", [])
152
+ if not obs:
153
+ continue
154
+ s = sum(1 for o in obs if o.get("is_present")) / len(obs) * 100
155
+ all_scores.append(s)
156
+ tid = t.get("model_id", "")
157
+ if "/" in tid and tid.split("/")[0].lower() == org.lower():
158
+ org_scores.append(s)
159
+
160
+ return {
161
+ "industry_avg": round(sum(all_scores) / len(all_scores), 1) if all_scores else 0,
162
+ "org_avg": round(sum(org_scores) / len(org_scores), 1) if org_scores else 0,
163
+ "total_models": len(all_scores),
164
+ "org_models": len(org_scores),
165
+ }
166
+
167
+ # --- Main Evidence Function ---
168
+
169
+ def generate_evidence(model_id: str) -> str:
170
+ """Generate complete forensic evidence package for a model."""
171
+ model_id = (model_id or "").strip()
172
+ if not model_id:
173
+ return json.dumps({"error": "empty"})
174
+
175
+ tpa_entry = get_model_tpa(model_id)
176
+ lineage = get_model_lineage(model_id)
177
+ outreach = get_model_outreach(model_id)
178
+ strength = compute_evidence_strength(tpa_entry)
179
+ peer = compute_peer_context(model_id)
180
+
181
+ tpa_data = fetch_tpa()
182
+ chain_height = tpa_data.get("chain_height", 0)
183
+
184
+ # Build observations detail
185
+ obs_detail = []
186
+ jurisdictions = set()
187
+ for o in tpa_entry.get("observations", []):
188
+ obs_detail.append({
189
+ "nec_id": o.get("necessity_id", ""),
190
+ "name": o.get("necessity_name", ""),
191
+ "present": o.get("is_present", False),
192
+ "severity": o.get("severity_label", ""),
193
+ "jurisdictions": o.get("jurisdictions_affected", 0),
194
+ "jurisdiction_hints": o.get("jurisdictions_hint", []),
195
+ "commitment_x": o.get("commitment_x", ""),
196
+ "commitment_y": o.get("commitment_y", ""),
197
+ })
198
+ for j in o.get("jurisdictions_hint", []):
199
+ jurisdictions.add(j)
200
+
201
+ # Trust level
202
+ if strength["score"] >= 80:
203
+ trust = "GREEN"
204
+ elif strength["score"] >= 40:
205
+ trust = "YELLOW"
206
+ else:
207
+ trust = "RED"
208
+
209
+ org = model_id.split("/")[0] if "/" in model_id else "unknown"
210
+
211
+ # Citation text
212
+ citation = (
213
+ f"As of {_nowz()}, model {model_id} published by {org} "
214
+ f"has been monitored by the Crovia Temporal Proof Registry. "
215
+ f"{strength['absent']}/{strength['total']} NEC# documentation requirements "
216
+ f"remain absent ({strength['critical_gaps']} critical). "
217
+ f"Cryptographic anchor: chain height {chain_height}, "
218
+ f"TPA-ID {tpa_entry.get('tpa_id', 'N/A')}. "
219
+ f"Source: registry.croviatrust.com"
220
  )
221
+
222
+ payload = {
223
+ "model_id": model_id,
224
+ "org": org,
225
+ "timestamp": _nowz(),
226
+ "found": bool(tpa_entry),
227
+ "trust_level": trust if tpa_entry else "UNKNOWN",
228
+ "tpa_id": tpa_entry.get("tpa_id", ""),
229
+ "chain_height": chain_height,
230
+ "strength": strength,
231
+ "peer": peer,
232
+ "observations": obs_detail,
233
+ "jurisdictions": sorted(jurisdictions),
234
+ "lineage": {
235
+ "compliance_score": lineage.get("compliance_score"),
236
+ "severity": lineage.get("severity"),
237
+ "nec_absent": lineage.get("nec_absent"),
238
+ "card_length": lineage.get("card_length"),
239
+ } if lineage else None,
240
+ "outreach": {
241
+ "status": outreach.get("status", outreach.get("outreach_status", "unknown")),
242
+ "contacted": outreach.get("contacted", outreach.get("discussion_sent", False)),
243
+ "response": outreach.get("response", outreach.get("response_received", False)),
244
+ } if outreach else None,
245
+ "citation": citation,
246
+ }
247
+
248
+ return json.dumps(payload, ensure_ascii=False)
249
 
250
+ # --- Startup data ---
251
+
252
+ def get_targets_list() -> str:
253
+ """Get list of all monitored targets for autocomplete."""
254
+ tpa = fetch_tpa()
255
+ tpas = tpa.get("tpas", [])
256
+ targets = []
257
+ for t in tpas:
258
+ mid = t.get("model_id", "")
259
+ if mid:
260
+ obs = t.get("observations", [])
261
+ absent = sum(1 for o in obs if not o.get("is_present"))
262
+ targets.append({"id": mid, "gaps": absent})
263
+ targets.sort(key=lambda x: -x["gaps"])
264
+ return json.dumps(targets, ensure_ascii=False)
265
+
266
+ def get_registry_stats() -> str:
267
+ """Get registry-wide stats for the header."""
268
+ tpa = fetch_tpa()
269
+ tpas = tpa.get("tpas", [])
270
+ lg = fetch_lineage()
271
+
272
+ models = set()
273
+ orgs = set()
274
+ total_gaps = 0
275
+ for t in tpas:
276
+ mid = t.get("model_id", "")
277
+ models.add(mid)
278
+ if "/" in mid:
279
+ orgs.add(mid.split("/")[0])
280
+ for o in t.get("observations", []):
281
+ if not o.get("is_present"):
282
+ total_gaps += 1
283
+
284
+ return json.dumps({
285
+ "models": len(models),
286
+ "orgs": len(orgs),
287
+ "chain_height": tpa.get("chain_height", 0),
288
+ "total_gaps": total_gaps,
289
+ "lineage_nodes": len(lg.get("nodes", [])),
290
+ }, ensure_ascii=False)
291
 
292
+ # --- Capsule Inspector (backward compat) ---
 
293
 
294
+ def fetch_capsule(cep_id: str) -> Dict[str, Any]:
295
+ path = hf_hub_download(repo_id=CEP_DATASET_ID, filename=f"{cep_id}.json", repo_type="dataset")
296
+ with open(path, "r", encoding="utf-8") as f:
297
+ return json.load(f)
298
 
299
+ def inspect_capsule(cep_id: str) -> str:
300
+ cep_id = (cep_id or "").strip()
301
+ if not cep_id:
302
+ return json.dumps({"error": "empty"})
303
+ try:
304
+ cap = fetch_capsule(cep_id)
305
+ schema = cap.get("schema", "unknown")
306
+ model = cap.get("model", {})
307
+ model_id = model.get("model_id", "unknown") if isinstance(model, dict) else "unknown"
308
+ evidence = cap.get("evidence", {}) if isinstance(cap.get("evidence"), dict) else {}
309
+ meta = cap.get("meta", {}) if isinstance(cap.get("meta"), dict) else {}
310
+ hashchain_root = meta.get("hashchain_sha256", "")
311
+ sig_present = "signature" in cap
312
+ cap_sha = _sha256_hex(_canonical_json(cap).encode("utf-8"))
313
+
314
+ return json.dumps({
315
+ "type": "capsule",
316
+ "cep_id": cep_id,
317
+ "schema": schema,
318
+ "model_id": model_id,
319
+ "evidence_nodes": len(evidence),
320
+ "signature": sig_present,
321
+ "hashchain": bool(hashchain_root),
322
+ "hashchain_short": hashchain_root[:16] if hashchain_root else "",
323
+ "capsule_sha256": cap_sha,
324
+ "evidence_keys": list(evidence.keys())[:20],
325
+ }, ensure_ascii=False)
326
+ except Exception as e:
327
+ return json.dumps({"error": f"{type(e).__name__}: {e}"})
328
 
 
 
329
 
330
+ # =============================================================================
331
+ # CSS
332
+ # =============================================================================
333
 
334
+ CSS = """
335
+ <style>
336
+ :root {
337
+ --bg: #030712;
338
+ --bg1: #0a0f1a;
339
+ --bg2: #111827;
340
+ --surface: #1f2937;
341
+ --border: rgba(255,255,255,0.08);
342
+ --border-h: rgba(255,255,255,0.16);
343
+ --text: #f9fafb;
344
+ --text2: #d1d5db;
345
+ --text3: #9ca3af;
346
+ --cyan: #22d3ee;
347
+ --blue: #3b82f6;
348
+ --violet: #8b5cf6;
349
+ --green: #22c55e;
350
+ --amber: #f59e0b;
351
+ --red: #ef4444;
352
+ --rose: #f43f5e;
353
+ --glow-cyan: 0 0 30px rgba(34,211,238,0.3);
354
+ --glow-violet: 0 0 30px rgba(139,92,246,0.3);
355
+ --radius: 16px;
356
+ }
357
 
358
+ html, body, #root, .gradio-container {
359
+ background: var(--bg) !important;
360
+ }
361
 
362
+ .gradio-container {
363
+ max-width: 1400px !important;
364
+ margin: 0 auto !important;
365
+ padding: 0 16px 60px !important;
366
+ font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
367
+ color: var(--text) !important;
368
+ }
369
 
370
+ footer { display: none !important; }
371
+ .gradio-container .wrap { border: 0 !important; }
372
+ .gradio-container .prose { max-width: none !important; }
 
373
 
374
+ .gradio-container, .gradio-container * {
375
+ opacity: 1 !important;
376
+ filter: none !important;
377
+ mix-blend-mode: normal !important;
378
+ visibility: visible !important;
379
+ }
380
 
381
+ /* ── TOPBAR ── */
382
+ .ev-topbar {
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: space-between;
386
+ padding: 14px 0;
387
+ border-bottom: 1px solid var(--border);
388
+ margin-bottom: 20px;
389
+ }
390
+ .ev-brand {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 12px;
394
+ }
395
+ .ev-logo {
396
+ width: 38px; height: 38px;
397
+ border-radius: 10px;
398
+ background: linear-gradient(135deg, var(--cyan), var(--violet));
399
+ display: flex; align-items: center; justify-content: center;
400
+ font-weight: 900; font-size: 16px; color: #fff;
401
+ box-shadow: var(--glow-cyan);
402
+ }
403
+ .ev-brand-text {
404
+ font-size: 13px;
405
+ letter-spacing: 0.25em;
406
+ text-transform: uppercase;
407
+ font-weight: 700;
408
+ color: var(--text) !important;
409
+ }
410
+ .ev-brand-sub {
411
+ font-size: 10px;
412
+ letter-spacing: 0.15em;
413
+ color: var(--text3) !important;
414
+ text-transform: uppercase;
415
+ }
416
+ .ev-live {
417
+ display: flex;
418
+ align-items: center;
419
+ gap: 8px;
420
+ font-size: 11px;
421
+ color: var(--green) !important;
422
+ letter-spacing: 0.1em;
423
+ }
424
+ .ev-live-dot {
425
+ width: 8px; height: 8px;
426
+ border-radius: 50%;
427
+ background: var(--green);
428
+ box-shadow: 0 0 12px rgba(34,197,94,0.6);
429
+ animation: pulse 2s infinite;
430
+ }
431
+ @keyframes pulse {
432
+ 0%, 100% { opacity: 1; }
433
+ 50% { opacity: 0.4; }
434
+ }
435
 
436
+ /* ── COMMAND BAR ── */
437
+ .ev-command {
438
+ position: relative;
439
+ margin-bottom: 20px;
440
+ }
441
+ .ev-command-input {
442
+ width: 100%;
443
+ height: 56px;
444
+ background: var(--bg1);
445
+ border: 1px solid var(--border-h);
446
+ border-radius: 14px;
447
+ padding: 0 20px 0 52px;
448
+ font-size: 16px;
449
+ color: var(--text) !important;
450
+ outline: none;
451
+ transition: border-color 0.2s, box-shadow 0.2s;
452
+ box-sizing: border-box;
453
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
454
+ }
455
+ .ev-command-input:focus {
456
+ border-color: var(--cyan);
457
+ box-shadow: var(--glow-cyan);
458
+ }
459
+ .ev-command-input::placeholder {
460
+ color: var(--text3);
461
+ font-family: 'Inter', sans-serif;
462
+ }
463
+ .ev-command-icon {
464
+ position: absolute;
465
+ left: 18px;
466
+ top: 50%;
467
+ transform: translateY(-50%);
468
+ font-size: 18px;
469
+ opacity: 0.5;
470
+ }
471
+ .ev-command-btn {
472
+ position: absolute;
473
+ right: 8px;
474
+ top: 50%;
475
+ transform: translateY(-50%);
476
+ background: linear-gradient(135deg, var(--cyan), var(--blue));
477
+ border: none;
478
+ border-radius: 10px;
479
+ padding: 10px 24px;
480
+ color: #fff;
481
+ font-weight: 700;
482
+ font-size: 13px;
483
+ cursor: pointer;
484
+ letter-spacing: 0.08em;
485
+ transition: transform 0.15s;
486
+ }
487
+ .ev-command-btn:hover { transform: translateY(-50%) scale(1.03); }
488
+ .ev-command-btn:active { transform: translateY(-50%) scale(0.97); }
489
+
490
+ /* Quick targets */
491
+ .ev-quick {
492
+ display: flex;
493
+ gap: 6px;
494
+ flex-wrap: wrap;
495
+ margin-top: 10px;
496
+ }
497
+ .ev-quick-btn {
498
+ background: var(--bg2);
499
+ border: 1px solid var(--border);
500
+ border-radius: 8px;
501
+ padding: 5px 12px;
502
+ color: var(--text3) !important;
503
+ font-size: 11px;
504
+ cursor: pointer;
505
+ transition: all 0.15s;
506
+ font-family: 'JetBrains Mono', monospace;
507
+ }
508
+ .ev-quick-btn:hover {
509
+ border-color: var(--cyan);
510
+ color: var(--cyan) !important;
511
+ background: rgba(34,211,238,0.06);
512
+ }
513
 
514
+ /* ── SIGNAL STRIP ── */
515
+ .ev-signals {
516
+ display: grid;
517
+ grid-template-columns: repeat(5, 1fr);
518
+ gap: 12px;
519
+ margin-bottom: 20px;
520
+ }
521
+ @media (max-width: 768px) {
522
+ .ev-signals { grid-template-columns: repeat(2, 1fr); }
523
+ }
524
+ .ev-signal {
525
+ background: var(--bg1);
526
+ border: 1px solid var(--border);
527
+ border-radius: 12px;
528
+ padding: 16px;
529
+ text-align: center;
530
+ transition: border-color 0.2s;
531
+ }
532
+ .ev-signal-val {
533
+ font-size: 28px;
534
+ font-weight: 800;
535
+ font-family: 'JetBrains Mono', monospace;
536
+ line-height: 1.1;
537
+ margin-bottom: 4px;
538
+ }
539
+ .ev-signal-label {
540
+ font-size: 10px;
541
+ text-transform: uppercase;
542
+ letter-spacing: 0.15em;
543
+ color: var(--text3) !important;
544
+ }
545
+ .ev-signal.trust-green { border-color: var(--green); }
546
+ .ev-signal.trust-green .ev-signal-val { color: var(--green) !important; }
547
+ .ev-signal.trust-yellow { border-color: var(--amber); }
548
+ .ev-signal.trust-yellow .ev-signal-val { color: var(--amber) !important; }
549
+ .ev-signal.trust-red { border-color: var(--red); }
550
+ .ev-signal.trust-red .ev-signal-val { color: var(--red) !important; }
551
+ .ev-signal.trust-unknown { border-color: var(--border); }
552
+ .ev-signal.trust-unknown .ev-signal-val { color: var(--text3) !important; }
553
+
554
+ /* ── MAIN GRID ── */
555
+ .ev-grid {
556
+ display: grid;
557
+ grid-template-columns: 1fr 1fr;
558
+ gap: 16px;
559
+ margin-bottom: 16px;
560
+ }
561
+ @media (max-width: 980px) { .ev-grid { grid-template-columns: 1fr; } }
562
 
563
+ .ev-panel {
564
+ background: var(--bg1);
565
+ border: 1px solid var(--border);
566
+ border-radius: var(--radius);
567
+ overflow: hidden;
568
+ }
569
+ .ev-panel-header {
570
+ padding: 12px 16px;
571
+ border-bottom: 1px solid var(--border);
572
+ display: flex;
573
+ align-items: center;
574
+ justify-content: space-between;
575
+ }
576
+ .ev-panel-title {
577
+ font-size: 11px;
578
+ font-weight: 700;
579
+ letter-spacing: 0.2em;
580
+ text-transform: uppercase;
581
+ color: var(--text2) !important;
582
+ }
583
+ .ev-panel-badge {
584
+ font-size: 10px;
585
+ padding: 2px 8px;
586
+ border-radius: 6px;
587
+ font-weight: 600;
588
+ letter-spacing: 0.05em;
589
+ }
590
+ .ev-panel-body {
591
+ padding: 16px;
592
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
 
594
+ /* ── NEC# GRID ── */
595
+ .nec-grid {
596
+ display: grid;
597
+ grid-template-columns: repeat(5, 1fr);
598
+ gap: 8px;
599
+ }
600
+ @media (max-width: 768px) { .nec-grid { grid-template-columns: repeat(4, 1fr); } }
601
+
602
+ .nec-cell {
603
+ border-radius: 10px;
604
+ padding: 10px 8px;
605
+ text-align: center;
606
+ border: 1px solid var(--border);
607
+ transition: all 0.2s;
608
+ cursor: default;
609
+ position: relative;
610
+ }
611
+ .nec-cell.present {
612
+ background: rgba(34,197,94,0.08);
613
+ border-color: rgba(34,197,94,0.3);
614
+ }
615
+ .nec-cell.absent {
616
+ background: rgba(239,68,68,0.08);
617
+ border-color: rgba(239,68,68,0.3);
618
+ }
619
+ .nec-cell.critical {
620
+ background: rgba(239,68,68,0.14);
621
+ border-color: rgba(239,68,68,0.5);
622
+ box-shadow: 0 0 12px rgba(239,68,68,0.15);
623
+ }
624
+ .nec-id {
625
+ font-size: 12px;
626
+ font-weight: 800;
627
+ font-family: 'JetBrains Mono', monospace;
628
+ }
629
+ .nec-cell.present .nec-id { color: var(--green) !important; }
630
+ .nec-cell.absent .nec-id { color: var(--red) !important; }
631
+ .nec-cell.critical .nec-id { color: var(--rose) !important; }
632
+ .nec-status {
633
+ font-size: 9px;
634
+ text-transform: uppercase;
635
+ letter-spacing: 0.1em;
636
+ margin-top: 2px;
637
+ color: var(--text3) !important;
638
+ }
639
+ .nec-severity {
640
+ font-size: 8px;
641
+ margin-top: 2px;
642
+ color: var(--text3) !important;
643
+ opacity: 0.7;
644
+ }
645
 
646
+ /* ── EVIDENCE STRENGTH METER ── */
647
+ .ev-meter {
648
+ display: flex;
649
+ align-items: center;
650
+ gap: 16px;
651
+ margin-bottom: 16px;
652
+ }
653
+ .ev-meter-bar-bg {
654
+ flex: 1;
655
+ height: 12px;
656
+ background: var(--bg2);
657
+ border-radius: 6px;
658
+ overflow: hidden;
659
+ border: 1px solid var(--border);
660
+ }
661
+ .ev-meter-bar {
662
+ height: 100%;
663
+ border-radius: 6px;
664
+ transition: width 0.8s ease;
665
+ }
666
+ .ev-meter-val {
667
+ font-size: 24px;
668
+ font-weight: 800;
669
+ font-family: 'JetBrains Mono', monospace;
670
+ min-width: 60px;
671
+ text-align: right;
672
+ }
673
 
674
+ /* ── CONSTELLATION SVG ── */
675
+ svg.ev-constellation {
676
+ width: 100%;
677
+ min-height: 380px;
678
+ border-radius: 12px;
679
+ background: radial-gradient(ellipse at 30% 20%, rgba(34,211,238,0.06), transparent 60%),
680
+ radial-gradient(ellipse at 70% 80%, rgba(139,92,246,0.06), transparent 60%),
681
+ var(--bg);
682
+ }
683
 
684
+ /* ── FORENSIC REPORT ── */
685
+ .ev-report {
686
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
687
+ font-size: 12px;
688
+ line-height: 1.7;
689
+ color: var(--text2) !important;
690
+ white-space: pre-wrap;
691
+ min-height: 200px;
692
+ padding: 16px;
693
+ background: var(--bg);
694
+ border-radius: 12px;
695
+ border: 1px solid var(--border);
696
+ }
697
 
698
+ /* ── CITATION BOX ── */
699
+ .ev-citation {
700
+ position: relative;
701
+ background: rgba(34,211,238,0.04);
702
+ border: 1px solid rgba(34,211,238,0.2);
703
+ border-radius: 12px;
704
+ padding: 16px 16px 16px 16px;
705
+ }
706
+ .ev-citation-text {
707
+ font-size: 13px;
708
+ line-height: 1.7;
709
+ color: var(--text2) !important;
710
+ font-style: italic;
711
+ }
712
+ .ev-citation-copy {
713
+ position: absolute;
714
+ top: 12px;
715
+ right: 12px;
716
+ background: var(--cyan);
717
+ border: none;
718
+ border-radius: 8px;
719
+ padding: 6px 14px;
720
+ color: var(--bg) !important;
721
+ font-weight: 700;
722
+ font-size: 11px;
723
+ cursor: pointer;
724
+ letter-spacing: 0.05em;
725
+ }
726
 
727
+ /* ── JURISDICTIONS ── */
728
+ .ev-juris {
729
+ display: flex;
730
+ flex-wrap: wrap;
731
+ gap: 6px;
732
+ }
733
+ .ev-juris-tag {
734
+ background: rgba(139,92,246,0.1);
735
+ border: 1px solid rgba(139,92,246,0.25);
736
+ border-radius: 8px;
737
+ padding: 4px 10px;
738
+ font-size: 11px;
739
+ color: var(--violet) !important;
740
+ }
741
 
742
+ /* ── OUTREACH STATUS ── */
743
+ .ev-outreach {
744
+ display: flex;
745
+ align-items: center;
746
+ gap: 10px;
747
+ padding: 12px 16px;
748
+ border-radius: 10px;
749
+ border: 1px solid var(--border);
750
+ background: var(--bg2);
751
+ font-size: 12px;
752
+ }
753
+ .ev-outreach-dot {
754
+ width: 10px; height: 10px;
755
+ border-radius: 50%;
756
+ flex-shrink: 0;
757
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
 
759
+ /* ── FULL WIDTH PANEL ── */
760
+ .ev-full { grid-column: 1 / -1; }
761
+
762
+ /* ── CAPSULE TAB ── */
763
+ .ev-tabs {
764
+ display: flex;
765
+ gap: 4px;
766
+ margin-bottom: 16px;
767
+ background: var(--bg1);
768
+ border-radius: 12px;
769
+ padding: 4px;
770
+ border: 1px solid var(--border);
771
+ width: fit-content;
772
+ }
773
+ .ev-tab {
774
+ padding: 8px 20px;
775
+ border-radius: 8px;
776
+ font-size: 12px;
777
+ font-weight: 600;
778
+ letter-spacing: 0.08em;
779
+ cursor: pointer;
780
+ color: var(--text3) !important;
781
+ border: none;
782
+ background: transparent;
783
+ transition: all 0.15s;
784
+ }
785
+ .ev-tab.active {
786
+ background: var(--bg2);
787
+ color: var(--text) !important;
788
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
789
+ }
790
+ .ev-tab:hover:not(.active) {
791
+ color: var(--text2) !important;
792
+ }
793
+
794
+ /* ── DISCLAIMER ── */
795
+ .ev-disclaimer {
796
+ margin-top: 24px;
797
+ padding: 16px;
798
+ border-radius: 12px;
799
+ border: 1px solid var(--border);
800
+ background: var(--bg1);
801
+ font-size: 11px;
802
+ line-height: 1.7;
803
+ color: var(--text3) !important;
804
+ text-align: center;
805
+ }
806
  </style>
807
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=JetBrains+Mono:wght@400;600;700;800&display=swap" rel="stylesheet">
808
  """
809
 
810
+ # =============================================================================
811
+ # HTML
812
+ # =============================================================================
813
+
814
+ UI_HTML = """
815
+ <!-- TOPBAR -->
816
+ <div class="ev-topbar">
817
+ <div class="ev-brand">
818
+ <div class="ev-logo">C</div>
819
+ <div>
820
+ <div class="ev-brand-text">Crovia · Evidence Machine</div>
821
+ <div class="ev-brand-sub">AI Forensic Evidence Console</div>
822
+ </div>
823
+ </div>
824
+ <div class="ev-live">
825
+ <div class="ev-live-dot"></div>
826
+ <span>LIVE · <span id="ev-stats-models">—</span> models · chain:<span id="ev-stats-chain">—</span></span>
827
+ </div>
828
+ </div>
829
+
830
+ <!-- TABS -->
831
+ <div class="ev-tabs">
832
+ <button class="ev-tab active" id="tab-evidence" onclick="switchTab('evidence')">Evidence Machine</button>
833
+ <button class="ev-tab" id="tab-capsules" onclick="switchTab('capsules')">CEP Capsules</button>
834
+ </div>
835
+
836
+ <!-- TAB: EVIDENCE MACHINE -->
837
+ <div id="panel-evidence">
838
+
839
+ <!-- COMMAND BAR -->
840
+ <div class="ev-command">
841
+ <span class="ev-command-icon">⌘</span>
842
+ <input class="ev-command-input" id="ev-search" type="text"
843
+ placeholder="Enter model ID — e.g. google/gemma-3-12b-it"
844
+ autocomplete="off" />
845
+ <button class="ev-command-btn" id="ev-go">INVESTIGATE</button>
846
+ </div>
847
+ <div class="ev-quick" id="ev-quick-targets"></div>
848
 
849
+ <!-- SIGNAL STRIP -->
850
+ <div class="ev-signals" id="ev-signals">
851
+ <div class="ev-signal trust-unknown" id="sig-trust">
852
+ <div class="ev-signal-val" id="sig-trust-val">—</div>
853
+ <div class="ev-signal-label">Trust Level</div>
854
+ </div>
855
+ <div class="ev-signal" id="sig-strength">
856
+ <div class="ev-signal-val" style="color:var(--cyan)!important" id="sig-strength-val">—</div>
857
+ <div class="ev-signal-label">Evidence Strength</div>
858
  </div>
859
+ <div class="ev-signal">
860
+ <div class="ev-signal-val" style="color:var(--violet)!important" id="sig-chain">—</div>
861
+ <div class="ev-signal-label">Chain Height</div>
862
+ </div>
863
+ <div class="ev-signal">
864
+ <div class="ev-signal-val" style="color:var(--amber)!important" id="sig-gaps">—</div>
865
+ <div class="ev-signal-label">NEC# Gaps</div>
866
+ </div>
867
+ <div class="ev-signal">
868
+ <div class="ev-signal-val" style="color:var(--blue)!important" id="sig-juris"></div>
869
+ <div class="ev-signal-label">Jurisdictions</div>
 
 
 
 
 
 
 
 
 
870
  </div>
 
871
  </div>
872
 
873
+ <!-- EVIDENCE STRENGTH METER -->
874
+ <div class="ev-meter" id="ev-meter" style="display:none;">
875
+ <div class="ev-meter-val" id="meter-val" style="color:var(--red)!important">0%</div>
876
+ <div class="ev-meter-bar-bg">
877
+ <div class="ev-meter-bar" id="meter-bar" style="width:0%;background:var(--red);"></div>
 
878
  </div>
879
+ <div style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:0.1em;">Evidence Coverage</div>
880
+ </div>
881
 
882
+ <!-- MAIN GRID -->
883
+ <div class="ev-grid">
884
+ <!-- NEC# Constellation -->
885
+ <div class="ev-panel">
886
+ <div class="ev-panel-header">
887
+ <div class="ev-panel-title">NEC# Constellation</div>
888
+ <div class="ev-panel-badge" id="nec-badge" style="background:var(--bg2);color:var(--text3);">—</div>
889
+ </div>
890
+ <div class="ev-panel-body">
891
+ <svg class="ev-constellation" id="ev-constellation" viewBox="0 0 600 380" preserveAspectRatio="xMidYMid meet"></svg>
892
+ </div>
893
  </div>
 
894
 
895
+ <!-- NEC# Grid -->
896
+ <div class="ev-panel">
897
+ <div class="ev-panel-header">
898
+ <div class="ev-panel-title">NEC# Element Grid</div>
899
+ <div class="ev-panel-badge" id="nec-grid-badge" style="background:var(--bg2);color:var(--text3);">20 elements</div>
900
+ </div>
901
+ <div class="ev-panel-body">
902
+ <div class="nec-grid" id="nec-grid"></div>
903
+ </div>
904
  </div>
 
905
  </div>
906
 
907
+ <!-- FORENSIC REPORT + JURISDICTIONS -->
908
+ <div class="ev-grid">
909
+ <div class="ev-panel">
910
+ <div class="ev-panel-header">
911
+ <div class="ev-panel-title">Forensic Report</div>
912
+ <div class="ev-panel-badge" style="background:rgba(34,211,238,0.1);color:var(--cyan);">LIVE</div>
913
+ </div>
914
+ <div class="ev-panel-body">
915
+ <pre class="ev-report" id="ev-report">Select a model to generate forensic evidence report.</pre>
916
+ </div>
 
 
 
 
 
 
917
  </div>
918
+
919
+ <div class="ev-panel">
920
+ <div class="ev-panel-header">
921
+ <div class="ev-panel-title">Jurisdictions & Outreach</div>
922
+ </div>
923
+ <div class="ev-panel-body">
924
+ <div style="margin-bottom:12px;font-size:10px;text-transform:uppercase;letter-spacing:0.15em;color:var(--text3)!important;">Applicable Jurisdictions</div>
925
+ <div class="ev-juris" id="ev-juris">
926
+ <span style="color:var(--text3);font-size:12px;">—</span>
927
+ </div>
928
+ <div style="margin:16px 0 8px;font-size:10px;text-transform:uppercase;letter-spacing:0.15em;color:var(--text3)!important;">Outreach Status</div>
929
+ <div class="ev-outreach" id="ev-outreach">
930
+ <div class="ev-outreach-dot" style="background:var(--text3);"></div>
931
+ <span style="color:var(--text3)!important;">Select a model to see outreach status</span>
932
+ </div>
933
+ <div style="margin:16px 0 8px;font-size:10px;text-transform:uppercase;letter-spacing:0.15em;color:var(--text3)!important;">Peer Context</div>
934
+ <div id="ev-peer" style="font-size:12px;color:var(--text3)!important;">—</div>
935
+ </div>
936
  </div>
937
+ </div>
938
+
939
+ <!-- CITATION ENGINE -->
940
+ <div class="ev-panel ev-full" style="margin-bottom:16px;">
941
+ <div class="ev-panel-header">
942
+ <div class="ev-panel-title">Citation Engine</div>
943
+ <div class="ev-panel-badge" style="background:rgba(34,211,238,0.1);color:var(--cyan);">LEGAL-GRADE</div>
944
+ </div>
945
+ <div class="ev-panel-body">
946
+ <div class="ev-citation" id="ev-citation-box">
947
+ <div class="ev-citation-text" id="ev-citation">Select a model to generate a citation-ready evidence statement.</div>
948
+ <button class="ev-citation-copy" id="ev-citation-copy" onclick="copyCitation()">COPY</button>
949
+ </div>
950
+ </div>
951
+ </div>
952
+
953
+ </div><!-- end panel-evidence -->
954
+
955
+ <!-- TAB: CEP CAPSULES (backward compat) -->
956
+ <div id="panel-capsules" style="display:none;">
957
+ <div class="ev-command">
958
+ <span class="ev-command-icon">📦</span>
959
+ <select class="ev-command-input" id="capsule-select" style="padding-left:52px;cursor:pointer;"></select>
960
+ </div>
961
+ <div class="ev-panel" style="margin-top:16px;">
962
+ <div class="ev-panel-header">
963
+ <div class="ev-panel-title">Capsule Inspector</div>
964
+ </div>
965
+ <div class="ev-panel-body">
966
+ <pre class="ev-report" id="capsule-report" style="min-height:300px;">Select a capsule to inspect.</pre>
967
+ </div>
968
  </div>
969
+ </div>
970
+
971
+ <!-- DISCLAIMER -->
972
+ <div class="ev-disclaimer">
973
+ CROVIA EVIDENCE MACHINE · Observation, not judgment. All data derived from publicly observable artifacts.
974
+ No model audit. No legal claim. Presence/absence only. Cryptographic commitments anchor observations immutably.
975
+ <br>Source: <a href="https://registry.croviatrust.com" target="_blank" style="color:var(--cyan);">registry.croviatrust.com</a>
976
+ · <a href="https://huggingface.co/datasets/Crovia/cep-capsules" target="_blank" style="color:var(--cyan);">Crovia/cep-capsules</a>
977
  </div>
978
  """
979
 
980
+ # =============================================================================
981
+ # JAVASCRIPT
982
+ # =============================================================================
983
 
984
  JS = r"""
985
  () => {
986
+ const $ = q => document.querySelector(q);
987
+ const $$ = q => document.querySelectorAll(q);
988
+
989
+ // --- Tab switching ---
990
+ window.switchTab = function(tab) {
991
+ document.querySelectorAll('.ev-tab').forEach(t => t.classList.remove('active'));
992
+ document.querySelector('#tab-' + tab).classList.add('active');
993
+ document.querySelector('#panel-evidence').style.display = tab === 'evidence' ? 'block' : 'none';
994
+ document.querySelector('#panel-capsules').style.display = tab === 'capsules' ? 'block' : 'none';
995
+ };
996
+
997
+ // --- Copy citation ---
998
+ window.copyCitation = function() {
999
+ const text = $('#ev-citation').textContent;
1000
+ navigator.clipboard.writeText(text).then(() => {
1001
+ const btn = $('#ev-citation-copy');
1002
+ btn.textContent = 'COPIED ✓';
1003
+ setTimeout(() => btn.textContent = 'COPY', 2000);
1004
+ });
1005
+ };
1006
+
1007
+ // --- Draw NEC# Constellation ---
1008
+ function drawConstellation(obs) {
1009
+ const svg = $('#ev-constellation');
1010
+ if (!svg) return;
1011
+ while (svg.firstChild) svg.removeChild(svg.firstChild);
1012
+
1013
+ const W = 600, H = 380;
1014
+ const cx = W / 2, cy = H / 2;
1015
+ const R = 140;
1016
+
1017
+ // Background glow
1018
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1019
+ const rg = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
1020
+ rg.id = "cg"; rg.setAttribute("cx","50%"); rg.setAttribute("cy","50%"); rg.setAttribute("r","50%");
1021
+ const s1 = document.createElementNS("http://www.w3.org/2000/svg","stop");
1022
+ s1.setAttribute("offset","0%"); s1.setAttribute("stop-color","rgba(34,211,238,0.08)");
1023
+ const s2 = document.createElementNS("http://www.w3.org/2000/svg","stop");
1024
+ s2.setAttribute("offset","100%"); s2.setAttribute("stop-color","transparent");
1025
+ rg.appendChild(s1); rg.appendChild(s2); defs.appendChild(rg);
1026
+ svg.appendChild(defs);
1027
+
1028
+ const bgc = document.createElementNS("http://www.w3.org/2000/svg","circle");
1029
+ bgc.setAttribute("cx",cx); bgc.setAttribute("cy",cy); bgc.setAttribute("r", R + 30);
1030
+ bgc.setAttribute("fill","url(#cg)"); svg.appendChild(bgc);
1031
+
1032
+ // Center node
1033
+ const cc = document.createElementNS("http://www.w3.org/2000/svg","circle");
1034
+ cc.setAttribute("cx",cx); cc.setAttribute("cy",cy); cc.setAttribute("r",16);
1035
+ cc.setAttribute("fill","#22d3ee"); cc.setAttribute("opacity","0.9");
1036
+ svg.appendChild(cc);
1037
+ const ct = document.createElementNS("http://www.w3.org/2000/svg","text");
1038
+ ct.setAttribute("x",cx); ct.setAttribute("y",cy+4);
1039
+ ct.setAttribute("text-anchor","middle"); ct.setAttribute("fill","#030712");
1040
+ ct.setAttribute("font-size","9"); ct.setAttribute("font-weight","800");
1041
+ ct.textContent = "MODEL"; svg.appendChild(ct);
1042
+
1043
+ if (!obs || !obs.length) {
1044
+ const nt = document.createElementNS("http://www.w3.org/2000/svg","text");
1045
+ nt.setAttribute("x",cx); nt.setAttribute("y",cy+45);
1046
+ nt.setAttribute("text-anchor","middle"); nt.setAttribute("fill","#9ca3af");
1047
+ nt.setAttribute("font-size","12");
1048
+ nt.textContent = "Select a model to illuminate the constellation";
1049
+ svg.appendChild(nt);
1050
+ return;
1051
+ }
1052
+
1053
+ const n = obs.length;
1054
+ obs.forEach((o, i) => {
1055
+ const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
1056
+ const x = cx + R * Math.cos(angle);
1057
+ const y = cy + R * Math.sin(angle);
1058
+
1059
+ // Connection line
1060
+ const line = document.createElementNS("http://www.w3.org/2000/svg","line");
1061
+ line.setAttribute("x1",cx); line.setAttribute("y1",cy);
1062
+ line.setAttribute("x2",x); line.setAttribute("y2",y);
1063
+ line.setAttribute("stroke", o.present ? "rgba(34,197,94,0.25)" : "rgba(239,68,68,0.25)");
1064
+ line.setAttribute("stroke-width", o.severity === "CRITICAL" && !o.present ? "2" : "1");
1065
+ svg.appendChild(line);
1066
+
1067
+ // Node
1068
+ const r = o.severity === "CRITICAL" && !o.present ? 14 : 10;
1069
+ const nc = document.createElementNS("http://www.w3.org/2000/svg","circle");
1070
+ nc.setAttribute("cx",x); nc.setAttribute("cy",y); nc.setAttribute("r",r);
1071
+ if (o.present) {
1072
+ nc.setAttribute("fill","rgba(34,197,94,0.2)");
1073
+ nc.setAttribute("stroke","#22c55e"); nc.setAttribute("stroke-width","2");
1074
+ } else if (o.severity === "CRITICAL") {
1075
+ nc.setAttribute("fill","rgba(239,68,68,0.2)");
1076
+ nc.setAttribute("stroke","#ef4444"); nc.setAttribute("stroke-width","2");
1077
+ } else {
1078
+ nc.setAttribute("fill","rgba(245,158,11,0.15)");
1079
+ nc.setAttribute("stroke","#f59e0b"); nc.setAttribute("stroke-width","1.5");
1080
+ }
1081
+ svg.appendChild(nc);
1082
+
1083
+ // Label
1084
+ const lt = document.createElementNS("http://www.w3.org/2000/svg","text");
1085
+ lt.setAttribute("x",x); lt.setAttribute("y", y + (y < cy ? -r-6 : r+14));
1086
+ lt.setAttribute("text-anchor","middle");
1087
+ lt.setAttribute("fill", o.present ? "#22c55e" : o.severity==="CRITICAL" ? "#ef4444" : "#f59e0b");
1088
+ lt.setAttribute("font-size","9"); lt.setAttribute("font-weight","700");
1089
+ lt.setAttribute("font-family","JetBrains Mono, monospace");
1090
+ lt.textContent = o.nec_id;
1091
+ svg.appendChild(lt);
1092
+ });
1093
  }
1094
 
1095
+ // --- Build NEC# Grid ---
1096
+ function buildNecGrid(obs) {
1097
+ const grid = $('#nec-grid');
1098
+ if (!grid) return;
1099
+ grid.innerHTML = '';
1100
+ if (!obs || !obs.length) {
1101
+ grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#9ca3af;font-size:12px;padding:40px;">Select a model</div>';
1102
+ return;
1103
+ }
1104
+ obs.forEach(o => {
1105
+ const cls = o.present ? 'present' : (o.severity === 'CRITICAL' ? 'critical' : 'absent');
1106
+ const cell = document.createElement('div');
1107
+ cell.className = 'nec-cell ' + cls;
1108
+ cell.title = o.name + ' | ' + o.severity + ' | ' + o.jurisdictions + ' jurisdictions';
1109
+ cell.innerHTML = '<div class="nec-id">' + o.nec_id + '</div>' +
1110
+ '<div class="nec-status">' + (o.present ? '✓' : '✗') + '</div>' +
1111
+ '<div class="nec-severity">' + o.severity + '</div>';
1112
+ grid.appendChild(cell);
1113
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  }
1115
 
1116
+ // --- Build Forensic Report ---
1117
+ function buildReport(p) {
1118
+ const lines = [];
1119
+ lines.push('CROVIA EVIDENCE MACHINE — FORENSIC REPORT');
1120
+ lines.push('═══════════════════════════════════════════');
1121
+ lines.push('');
1122
+ lines.push('TARGET: ' + p.model_id);
1123
+ lines.push('ORG: ' + p.org);
1124
+ lines.push('TIMESTAMP: ' + p.timestamp);
1125
+ lines.push('TPA-ID: ' + (p.tpa_id || 'N/A'));
1126
+ lines.push('CHAIN: ' + p.chain_height);
1127
+ lines.push('');
1128
+ lines.push('TRUST LEVEL: ' + p.trust_level);
1129
+ lines.push('EVIDENCE: ' + p.strength.present + '/' + p.strength.total + ' NEC# present');
1130
+ lines.push('GAPS: ' + p.strength.absent + ' (' + p.strength.critical_gaps + ' critical)');
1131
+ lines.push('STRENGTH: ' + p.strength.score + '%');
1132
+ lines.push('');
1133
+ lines.push('─── NEC# OBSERVATIONS ───');
1134
+ (p.observations || []).forEach(o => {
1135
+ const icon = o.present ? '✓' : '✗';
1136
+ const sev = o.present ? '' : ' [' + o.severity + ']';
1137
+ lines.push(icon + ' ' + o.nec_id.padEnd(7) + o.name.substring(0,50) + sev);
1138
+ });
1139
+ lines.push('');
1140
+ lines.push('─── CRYPTOGRAPHIC ANCHORS ───');
1141
+ (p.observations || []).filter(o => o.commitment_x).slice(0,5).forEach(o => {
1142
+ lines.push(o.nec_id + ' Cx: ' + o.commitment_x.substring(0,24) + '...');
1143
+ lines.push(' Cy: ' + o.commitment_y.substring(0,24) + '...');
1144
+ });
1145
+ if (p.observations && p.observations.length > 5) {
1146
+ lines.push('... + ' + (p.observations.length - 5) + ' more Pedersen commitments');
1147
+ }
1148
+ lines.push('');
1149
+ if (p.lineage) {
1150
+ lines.push('─── LINEAGE DATA ───');
1151
+ lines.push('Compliance Score: ' + p.lineage.compliance_score);
1152
+ lines.push('Severity: ' + p.lineage.severity);
1153
+ lines.push('NEC Absent: ' + p.lineage.nec_absent);
1154
+ lines.push('');
1155
+ }
1156
+ lines.push('─── DISCLAIMER ───');
1157
+ lines.push('Observation, not judgment. All data from public artifacts.');
1158
+ return lines.join('\n');
1159
+ }
1160
 
1161
+ // --- Update UI from payload ---
1162
+ function updateUI(p) {
1163
+ if (!p || p.error) {
1164
+ $('#ev-report').textContent = 'Error: ' + (p ? p.error : 'unknown');
1165
+ return;
1166
+ }
1167
 
1168
+ // Signals
1169
+ const trustEl = $('#sig-trust');
1170
+ trustEl.className = 'ev-signal trust-' + p.trust_level.toLowerCase();
1171
+ $('#sig-trust-val').textContent = p.trust_level;
1172
+ $('#sig-strength-val').textContent = p.strength.score + '%';
1173
+ $('#sig-chain').textContent = p.chain_height.toLocaleString();
1174
+ $('#sig-gaps').textContent = p.strength.absent + '/' + p.strength.total;
1175
+ $('#sig-juris').textContent = p.jurisdictions.length;
1176
+
1177
+ // Meter
1178
+ const meter = $('#ev-meter');
1179
+ meter.style.display = 'flex';
1180
+ $('#meter-val').textContent = p.strength.score + '%';
1181
+ const barColor = p.strength.score >= 60 ? '#22c55e' : p.strength.score >= 30 ? '#f59e0b' : '#ef4444';
1182
+ $('#meter-bar').style.width = p.strength.score + '%';
1183
+ $('#meter-bar').style.background = barColor;
1184
+ $('#meter-val').style.color = barColor + '!important';
1185
+
1186
+ // NEC badge
1187
+ $('#nec-badge').textContent = p.strength.present + '/' + p.strength.total + ' present';
1188
+ $('#nec-badge').style.background = p.trust_level === 'GREEN' ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
1189
+ $('#nec-badge').style.color = p.trust_level === 'GREEN' ? '#22c55e' : '#ef4444';
1190
+
1191
+ // Constellation + Grid
1192
+ drawConstellation(p.observations);
1193
+ buildNecGrid(p.observations);
1194
+
1195
+ // Report
1196
+ $('#ev-report').textContent = buildReport(p);
1197
+
1198
+ // Jurisdictions
1199
+ const jurisEl = $('#ev-juris');
1200
+ jurisEl.innerHTML = '';
1201
+ (p.jurisdictions || []).forEach(j => {
1202
+ const tag = document.createElement('span');
1203
+ tag.className = 'ev-juris-tag';
1204
+ tag.textContent = j;
1205
+ jurisEl.appendChild(tag);
1206
+ });
1207
+ if (!p.jurisdictions || !p.jurisdictions.length) {
1208
+ jurisEl.innerHTML = '<span style="color:#9ca3af;font-size:12px;">No jurisdictions identified</span>';
1209
+ }
1210
 
1211
+ // Outreach
1212
+ const outEl = $('#ev-outreach');
1213
+ if (p.outreach) {
1214
+ const contacted = p.outreach.contacted;
1215
+ const responded = p.outreach.response;
1216
+ const color = responded ? '#22c55e' : contacted ? '#f59e0b' : '#ef4444';
1217
+ const text = responded ? 'Contacted · Response received' :
1218
+ contacted ? 'Contacted · Awaiting response' : 'Not yet contacted';
1219
+ outEl.innerHTML = '<div class="ev-outreach-dot" style="background:' + color + ';box-shadow:0 0 8px ' + color + '44;"></div>' +
1220
+ '<span style="color:' + color + '!important;">' + text + '</span>';
1221
+ } else {
1222
+ outEl.innerHTML = '<div class="ev-outreach-dot" style="background:#6b7280;"></div>' +
1223
+ '<span style="color:#9ca3af!important;">No outreach data for this organization</span>';
1224
+ }
1225
 
1226
+ // Peer context
1227
+ const peerEl = $('#ev-peer');
1228
+ if (p.peer) {
1229
+ peerEl.innerHTML =
1230
+ '<div style="margin-bottom:6px;"><span style="color:var(--text2);">Industry avg:</span> <strong style="color:var(--cyan);">' + p.peer.industry_avg + '%</strong> <span style="color:var(--text3);">(' + p.peer.total_models + ' models)</span></div>' +
1231
+ '<div><span style="color:var(--text2);">Org avg (' + p.peer.org_models + ' models):</span> <strong style="color:var(--violet);">' + p.peer.org_avg + '%</strong></div>';
1232
+ }
1233
 
1234
+ // Citation
1235
+ $('#ev-citation').textContent = p.citation;
1236
+ }
 
 
 
 
 
1237
 
1238
+ // --- Payload watcher ---
1239
+ function attachWatcher() {
1240
+ const root = document.querySelector('#ev_payload');
1241
+ if (!root) return false;
1242
+ const input = root.querySelector('textarea, input');
1243
+ if (!input) return false;
1244
+ let last = '';
1245
+ const tick = () => {
1246
+ const val = input.value || '';
1247
+ if (val && val !== last) {
1248
+ last = val;
1249
+ try { updateUI(JSON.parse(val)); } catch(e) {}
1250
+ }
1251
+ };
1252
+ input.addEventListener('input', tick);
1253
+ setInterval(tick, 200);
1254
+ return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1255
  }
1256
 
1257
+ // --- Stats watcher ---
1258
+ function attachStatsWatcher() {
1259
+ const root = document.querySelector('#ev_stats');
1260
+ if (!root) return false;
1261
+ const input = root.querySelector('textarea, input');
1262
+ if (!input || !input.value) return false;
1263
+ try {
1264
+ const s = JSON.parse(input.value);
1265
+ $('#ev-stats-models').textContent = s.models;
1266
+ $('#ev-stats-chain').textContent = s.chain_height.toLocaleString();
1267
+ } catch(e) {}
1268
+ return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1269
  }
1270
+
1271
+ // --- Targets watcher ---
1272
+ function attachTargetsWatcher() {
1273
+ const root = document.querySelector('#ev_targets');
1274
+ if (!root) return false;
1275
+ const input = root.querySelector('textarea, input');
1276
+ if (!input || !input.value) return false;
1277
+ try {
1278
+ const targets = JSON.parse(input.value);
1279
+ const quick = $('#ev-quick-targets');
1280
+ if (!quick || quick.children.length > 0) return true;
1281
+ targets.slice(0, 8).forEach(t => {
1282
+ const btn = document.createElement('button');
1283
+ btn.className = 'ev-quick-btn';
1284
+ btn.textContent = t.id.split('/').pop().substring(0,16) + ' (' + t.gaps + ' gaps)';
1285
+ btn.title = t.id;
1286
+ btn.addEventListener('click', () => {
1287
+ $('#ev-search').value = t.id;
1288
+ triggerSearch(t.id);
1289
+ });
1290
+ quick.appendChild(btn);
1291
+ });
1292
+ } catch(e) {}
1293
+ return true;
1294
  }
1295
+
1296
+ // --- Connect command bar to Gradio ---
1297
+ function attachCommandBar() {
1298
+ const searchRoot = document.querySelector('#ev_search_in');
1299
+ if (!searchRoot) return false;
1300
+ const searchInput = searchRoot.querySelector('textarea, input');
1301
+ if (!searchInput) return false;
1302
+
1303
+ window.triggerSearch = function(val) {
1304
+ searchInput.value = val;
1305
+ searchInput.dispatchEvent(new Event('input', {bubbles:true}));
1306
+ searchInput.dispatchEvent(new Event('change', {bubbles:true}));
1307
+ };
1308
+
1309
+ $('#ev-go').addEventListener('click', () => {
1310
+ const val = $('#ev-search').value.trim();
1311
+ if (val) triggerSearch(val);
1312
+ });
1313
+ $('#ev-search').addEventListener('keydown', e => {
1314
+ if (e.key === 'Enter') {
1315
+ const val = $('#ev-search').value.trim();
1316
+ if (val) triggerSearch(val);
1317
+ }
1318
+ });
1319
+ return true;
1320
  }
1321
+
1322
+ // --- Capsule tab ---
1323
+ function attachCapsuleTab() {
1324
+ const capsRoot = document.querySelector('#ev_capsules_json');
1325
+ if (!capsRoot) return false;
1326
+ const capsInput = capsRoot.querySelector('textarea, input');
1327
+ if (!capsInput || !capsInput.value) return false;
1328
+
1329
+ const sel = $('#capsule-select');
1330
+ if (!sel || sel.children.length > 0) return true;
1331
+
1332
+ try {
1333
+ const caps = JSON.parse(capsInput.value);
1334
+ caps.forEach(c => {
1335
+ const opt = document.createElement('option');
1336
+ opt.value = c; opt.textContent = c;
1337
+ sel.appendChild(opt);
1338
+ });
1339
+ } catch(e) {}
1340
+
1341
+ // Connect select to Gradio
1342
+ const cepRoot = document.querySelector('#ev_capsule_in');
1343
+ const cepInput = cepRoot ? cepRoot.querySelector('textarea, input') : null;
1344
+ if (!cepInput) return false;
1345
+
1346
+ sel.addEventListener('change', () => {
1347
+ cepInput.value = sel.value;
1348
+ cepInput.dispatchEvent(new Event('input', {bubbles:true}));
1349
+ });
1350
+
1351
+ // Watch capsule result
1352
+ const resRoot = document.querySelector('#ev_capsule_result');
1353
+ const resInput = resRoot ? resRoot.querySelector('textarea, input') : null;
1354
+ if (!resInput) return false;
1355
+
1356
+ let lastCap = '';
1357
+ setInterval(() => {
1358
+ const val = resInput.value || '';
1359
+ if (val && val !== lastCap) {
1360
+ lastCap = val;
1361
+ try {
1362
+ const data = JSON.parse(val);
1363
+ if (data.error) {
1364
+ $('#capsule-report').textContent = 'Error: ' + data.error;
1365
+ } else {
1366
+ const lines = [];
1367
+ lines.push('CROVIA · CEP CAPSULE INSPECTOR');
1368
+ lines.push('════════════════════════════════');
1369
+ lines.push('');
1370
+ lines.push('CEP-ID: ' + data.cep_id);
1371
+ lines.push('Schema: ' + data.schema);
1372
+ lines.push('Model: ' + data.model_id);
1373
+ lines.push('Evidence: ' + data.evidence_nodes + ' nodes');
1374
+ lines.push('Signature: ' + (data.signature ? 'PRESENT ✓' : 'MISSING ✗'));
1375
+ lines.push('Hashchain: ' + (data.hashchain ? 'sha256:' + data.hashchain_short + '...' : 'MISSING ✗'));
1376
+ lines.push('Capsule SHA: ' + data.capsule_sha256);
1377
+ lines.push('');
1378
+ lines.push('Evidence keys: ' + (data.evidence_keys || []).join(', '));
1379
+ $('#capsule-report').textContent = lines.join('\n');
1380
+ }
1381
+ } catch(e) {}
1382
+ }
1383
+ }, 200);
1384
+
1385
+ return true;
1386
+ }
1387
+
1388
+ // --- URL params ---
1389
+ function checkUrlParam() {
1390
+ const params = new URLSearchParams(window.location.search);
1391
+ const model = params.get('model');
1392
+ if (model) {
1393
+ $('#ev-search').value = model;
1394
+ setTimeout(() => {
1395
+ if (window.triggerSearch) triggerSearch(model);
1396
+ }, 500);
1397
  }
1398
+ }
1399
+
1400
+ // --- Boot ---
1401
+ drawConstellation([]);
1402
+ buildNecGrid([]);
1403
+
1404
+ const boot = setInterval(() => {
1405
+ const ok1 = attachWatcher();
1406
+ const ok2 = attachStatsWatcher();
1407
+ const ok3 = attachTargetsWatcher();
1408
+ const ok4 = attachCommandBar();
1409
+ const ok5 = attachCapsuleTab();
1410
+ if (ok1 && ok2 && ok3 && ok4 && ok5) {
1411
+ clearInterval(boot);
1412
+ checkUrlParam();
1413
  }
 
1414
  }, 200);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1415
 
1416
+ return [];
1417
  }
1418
  """
1419
 
1420
+ # =============================================================================
1421
+ # GRADIO APP
1422
+ # =============================================================================
 
 
 
1423
 
1424
+ capsules = _list_capsules()
1425
+ default_cap = capsules[0] if capsules else "CEP-2511-K4I7X2"
1426
+ stats_json = get_registry_stats()
1427
+ targets_json = get_targets_list()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1428
 
1429
+ with gr.Blocks(title="CROVIA · Evidence Machine", css=CSS, js=JS) as demo:
 
 
 
 
 
1430
  gr.HTML(UI_HTML)
1431
 
1432
+ # Hidden Gradio components (JS reads/writes via DOM)
1433
+ ev_search_in = gr.Textbox(value="", visible=False, elem_id="ev_search_in")
1434
+ ev_payload = gr.Textbox(value="", visible=False, elem_id="ev_payload")
1435
+ ev_stats = gr.Textbox(value=stats_json, visible=False, elem_id="ev_stats")
1436
+ ev_targets = gr.Textbox(value=targets_json, visible=False, elem_id="ev_targets")
 
 
 
1437
 
1438
+ # Capsule tab
1439
+ ev_capsules_json = gr.Textbox(value=json.dumps(capsules), visible=False, elem_id="ev_capsules_json")
1440
+ ev_capsule_in = gr.Textbox(value=default_cap, visible=False, elem_id="ev_capsule_in")
1441
+ ev_capsule_result = gr.Textbox(value="", visible=False, elem_id="ev_capsule_result")
 
 
 
1442
 
1443
+ # Events
1444
+ ev_search_in.change(generate_evidence, inputs=ev_search_in, outputs=ev_payload)
1445
+ ev_capsule_in.change(inspect_capsule, inputs=ev_capsule_in, outputs=ev_capsule_result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1446
 
1447
  demo.queue()
1448
  demo.launch()