Spaces:
Paused
Paused
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
README.md
CHANGED
|
@@ -1,205 +1,66 @@
|
|
| 1 |
---
|
| 2 |
-
title: CROVIA ·
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 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:
|
| 12 |
tags:
|
| 13 |
- audit
|
| 14 |
- provenance
|
| 15 |
- ai-act
|
| 16 |
- compliance
|
| 17 |
-
-
|
| 18 |
-
-
|
|
|
|
| 19 |
---
|
| 20 |
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
Public Evidence Inspector for AI Training Datasets
|
| 24 |
|
| 25 |
-
|
| 26 |
-
(CEP.v1 reference capsules) published as open datasets.
|
| 27 |
|
| 28 |
-
|
| 29 |
-
It shows exactly what exists — and what does not.
|
| 30 |
|
| 31 |
-
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 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 |
-
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
|
|
|
| 114 |
|
| 115 |
-
|
| 116 |
-
is intentionally delegated to the Crovia CLI / Core Engine.
|
| 117 |
|
| 118 |
-
|
| 119 |
-
-
|
| 120 |
-
-
|
| 121 |
-
-
|
|
|
|
|
|
|
| 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 |
-
|
| 162 |
|
| 163 |
-
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 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
|
| 15 |
-
#
|
|
|
|
| 16 |
# -----------------------------------------------------------------------------
|
| 17 |
|
| 18 |
CEP_DATASET_ID = "Crovia/cep-capsules"
|
| 19 |
-
|
| 20 |
-
OPEN_EVIDENCE_MODE = True
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 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()
|
| 44 |
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
def
|
| 48 |
-
return
|
| 49 |
|
|
|
|
| 50 |
|
| 51 |
-
def
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
def
|
| 56 |
-
|
| 57 |
-
return ""
|
| 58 |
-
return s[:n] + "…"
|
| 59 |
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def _list_capsules() -> List[str]:
|
| 62 |
-
now =
|
| 63 |
-
if (now -
|
| 64 |
-
return
|
| 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 |
-
|
| 77 |
-
_CAPSULE_LIST_CACHE["items"] = items
|
| 78 |
return items
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
def
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
)
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
missing = [k for k, v in checks.items() if not v]
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
if missing == ["signature"]:
|
| 101 |
-
return ("YELLOW", "Evidence present, but missing signature metadata (publisher not authenticated).", 0.58)
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
|
|
|
| 105 |
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
model_id = model.get("model_id", "unknown-model") if isinstance(model, dict) else "unknown-model"
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 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 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
-
|
| 257 |
-
--
|
| 258 |
-
--
|
| 259 |
-
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
-
|
| 264 |
-
-
|
| 265 |
-
|
| 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 |
-
|
| 542 |
-
<div class="
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
</div>
|
| 547 |
-
<div>
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
<div class="
|
| 557 |
-
|
| 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 |
-
<
|
| 572 |
-
|
| 573 |
-
<div class="
|
| 574 |
-
<div class="
|
| 575 |
-
|
| 576 |
-
<div class="mini" id="copy_hint">—</div>
|
| 577 |
</div>
|
| 578 |
-
|
|
|
|
| 579 |
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
<
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
</div>
|
| 585 |
-
</div>
|
| 586 |
|
| 587 |
-
|
| 588 |
-
<div class="
|
| 589 |
-
|
| 590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
</div>
|
| 592 |
-
</div>
|
| 593 |
</div>
|
| 594 |
|
| 595 |
-
<
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
<
|
| 602 |
-
<
|
| 603 |
-
|
| 604 |
-
|
| 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 |
-
|
| 613 |
-
|
| 614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
</div>
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
</div>
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
</div>
|
| 625 |
"""
|
| 626 |
|
|
|
|
|
|
|
|
|
|
| 627 |
|
| 628 |
JS = r"""
|
| 629 |
() => {
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
}
|
| 661 |
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 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 |
-
|
| 735 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
| 740 |
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
if(val && val !== last){
|
| 756 |
-
last = val;
|
| 757 |
-
try{ updateUI(JSON.parse(val)); }catch(e){}
|
| 758 |
-
}
|
| 759 |
-
};
|
| 760 |
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 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 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 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 |
-
//
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
const
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
}
|
| 897 |
-
|
| 898 |
-
//
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
if(!
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 912 |
}
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 929 |
}
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1044 |
}
|
| 1045 |
"""
|
| 1046 |
|
| 1047 |
-
|
| 1048 |
-
|
| 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 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 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
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 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 |
-
#
|
| 1233 |
-
|
| 1234 |
-
|
| 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 |
-
|
| 1241 |
-
|
| 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()
|
|
|