merge: ship Core Local Assurance Viewer

This commit is contained in:
thanhnv
2026-07-28 23:09:57 +07:00
29 changed files with 3422 additions and 445 deletions
+9
View File
@@ -93,6 +93,15 @@ jobs:
- name: Run CASAN4 harness tests (35 tests)
run: bash packages/casan-harness/tests/run-casan4-harness-tests.sh
- name: Run Core Local Assurance Viewer contracts
run: python3 packages/casan-harness/tests/local-viewer-tests.py
- name: Prove visual reports from the clean Core release artifact
run: bash packages/casan-harness/tests/core-local-viewer-artifact-tests.sh
- name: Prove managed and vendored Core adoption
run: bash packages/casan-devkit/tests/hybrid-install-tests.sh
- name: Run adversarial harness tests (44 tests)
run: bash packages/casan-harness/tests/adversarial-harness-tests.sh
+18 -12
View File
@@ -22,7 +22,7 @@ hay release tooling.
| Thành phần | Trạng thái | Phạm vi |
|---|---|---|
| Core | Implemented | H1–H7 harness, hooks, policy gates, audit, evidence và CLI |
| Core | Implemented | H1–H7 harness, hooks, policy gates, audit, evidence, CLI và Local Assurance Viewer |
| DevKit | Implemented | Core + adoption tooling, domain-pack và CI template |
| Control Plane | Preview | Live H1–H7, H6, run history và evidence export; deploy riêng |
| Enterprise | Chưa phát hành | OIDC/KMS/WORM/HA/DR/SLA; CLI chủ động từ chối |
@@ -48,23 +48,29 @@ capability được đóng gói; runtime mô tả vị trí; maturity mô tả m
## Golden path: prompt → live assurance
Core không export HTML trên hot path. Sau mỗi prompt, hook tự ghi trace/H6 và
trả assurance receipt. Nếu project đã enroll Control Plane, receipt có deep link
đến đúng run; nếu offline, dùng `casan report latest`.
Core không export HTML và không giữ web server trên hot path. Sau mỗi prompt,
hook chỉ ghi trace/H6 và trả assurance receipt. Khi cần xem, `casan view` khởi
động/reuse **Local Assurance Viewer** read-only trên loopback và mở đúng run.
Viewer, H1–H7, H6, history và export đều nằm trong Core, hoạt động offline,
không cần Node/npm hoặc Platform.
```bash
# Platform preview: một lệnh, tự trỏ Control Plane vào project hiện tại
casan dashboard start
# Sau một prompt
casan report latest
casan view # mở run mới nhất
casan report export --format html # snapshot chỉ tạo khi được yêu cầu
casan view # mở run mới nhất trong Core viewer
casan report export --format html # run dossier, chỉ tạo khi được yêu cầu
casan report export --h6 --format html # H6 dossier on-demand
# Lifecycle viewer cục bộ
casan dashboard status
casan dashboard stop
```
`casan dashboard start` là convenience launcher cho local demo/evaluation.
Production triển khai Control Plane như service dùng chung và enroll project
bằng `casan init --dashboard-url https://casan.example`.
Không cần chạy `export` sau mỗi prompt. Evidence là source of truth; HTML/JSON
chỉ là projection on-demand. `casan dashboard start` trong Core mở viewer
single-project. Khi bundle Platform hiện diện, cùng lệnh đó quản lý Control
Plane. Production triển khai Platform như service dùng chung chỉ khi cần
multi-project, RBAC tập trung, approvals và fleet operations.
## Quick start
+1 -1
View File
@@ -1 +1 @@
1.0.5
1.0.6
+4 -8
View File
@@ -87,10 +87,10 @@ Commands:
prompt verify Verify the adopted prompt-enforcement contract
prompt trace <trace-id> Verify that a prompt trace is H1-H7 certified
report latest [--json] Show the latest prompt assurance receipt
report export [trace] [--format] Export a trace snapshot on demand
view [trace-id] Open a trace in the enrolled Control Plane
report export [trace] [--format] Export a run/H6 dossier on demand
view [trace-id] Open the Core Local Assurance Viewer
pipeline [--manifest path] Run the manifest-driven SRS→test pipeline
dashboard <start|status|stop|open> Run the local Control Plane (Platform only)
dashboard <start|status|stop|open> Run local assurance (Core) or Control Plane
version Print version
help This help
@@ -186,11 +186,7 @@ case "$cmd" in
if [[ -f "$CONTROL_PLANE_SCRIPT" ]]; then
exec bash "$CONTROL_PLANE_SCRIPT" "$@"
fi
[[ -f "$BASH_DIR/dashboard-serve.sh" ]] || {
echo "casan: dashboard requires the Platform bundle" >&2
exit 1
}
exec bash "$BASH_DIR/dashboard-serve.sh" "$@" ;;
exec python3 "$HARNESS/scripts/python/report_cli.py" --root "$CASAN_APP_ROOT" viewer "$@" ;;
version|-v|--version) version ;;
help|-h|--help) usage ;;
*) echo "casan: unknown command '$cmd'" >&2; usage >&2; exit 64 ;;
+13
View File
@@ -20,6 +20,17 @@ The project defaults to Level 1/Core with runtime mode `managed`. Core remains
under `$CASAN_HOME`; the repository receives `.casan` config/lock/bootstrap and
the selected client hooks. The CLI output states the resolved runtime path.
After any prompt, visual assurance is available from Core itself:
```bash
casan report latest
casan view
```
The viewer is single-project, offline-capable and read-only. It starts lazily on
`127.0.0.1`, so projects do not need Platform, Node/npm or an always-running
service merely to inspect H1–H7 and H6.
Use this for developer workstations and managed CI runners. CI must install the
same release recorded by `.casan/version.lock` before running gates.
@@ -88,6 +99,8 @@ bin/casan pipeline --manifest apps/<project>/domain/project.manifest.json
- **Project lock/config** → `.casan/config.json` and `.casan/version.lock`.
- **Your domain data** → `apps/<project>/domain/` (via `CASAN_DOMAIN_ROOT`).
- **Runtime state** → `.specify/` (logs, audit, governance — created on first run).
- **Local viewer state** → `.specify/state/local-viewer.json` (`0600` on POSIX,
project ACL on Windows; ephemeral loopback port/session token; removed on stop).
The bootstrap resolves the mode/path from the project lock and verifies the live
Core hash before dispatch.
+6 -5
View File
@@ -9,7 +9,7 @@ contents + maturity: [`packaging/levels.json`](../../packaging/levels.json).
| Edition | Package names | Status | What it is |
|---|---|:--:|---|
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | Minimal H1–H7 production runtime: security + action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, `bin/casan` CLI |
| **1 — Core Harness** | `casan-core`, `casan-harness` | ✅ implemented | Minimal H1–H7 production runtime plus zero-dependency Local Assurance Viewer: latest/history, interactive H1–H7, H6 and on-demand export |
| **2 — DevKit / Adoption Kit** | `casan-devkit`, `casan-project-kit` | ✅ implemented | Level 1 + project templates, domain-pack scaffold, Gitea workflow template, harness Dockerfile, install script, adoption/CI/domain-pack guides |
| **3 — Platform Components** | `casan-platform`, `casan-control-panel` | 🟡 preview | Control Panel, Dashboard, Run History, governed chat MVP and **Evidence Pack Viewer**. Attack Battery Viewer, Gitea evidence publishing, and managed rollout are still pending. |
| **4 — Enterprise / Governed Console** | `casan-enterprise`, `casan-governed-console` | 📋 future | Promotion layer requiring managed deployment, KMS/Object Lock operations, HA/DR/SLA, external review and compliance/support evidence. |
@@ -53,7 +53,7 @@ Production Core/DevKit artifacts never include:
- internal CI/adversarial runners (`ci-harness-gate.sh`, `security-gate.sh`,
`test-integrity.py`, cloud smoke);
- the legacy `packages/casan-harness/level5/` directory;
- Platform-only dashboard/local-infrastructure helpers;
- Platform-only centralized Control Plane/local-infrastructure helpers;
- source documentation, installer copies, Dockerfiles, caches, or release tooling
inside the installed version directory.
@@ -63,10 +63,11 @@ its tests so CASAN itself remains verifiable; only the install/release boundary
minimal.
## Who adopts what
- **Governance-harness-only / BJT initial / CI gate** → `casan-core`.
- **Governance-harness-only / BJT initial / CI gate / single-project visual assurance** → `casan-core`.
- **New project adopting CASAN** → `casan-devkit` (install.sh scaffolds domain + CI).
- **Want dashboards/visibility** → `casan-platform` (preview; live H1–H7,
H6 coverage and on-demand run/H6 exports today).
- **Want centralized multi-project operations, RBAC and approvals** →
`casan-platform` (preview). Core already provides single-project H1–H7, H6,
history and on-demand run/H6 exports.
- **Enterprise governed console** → future; building blocks (RBAC/tenant/KMS/WORM/approval)
already live in core.
@@ -0,0 +1,99 @@
# CASAN Core Local Assurance Viewer
## Product decision
A project that installs only CASAN Core can inspect production-quality visual
reports. Platform is not a prerequisite for basic visibility.
Core owns the single-project review loop:
- latest assurance receipt and run history;
- interactive H1→H7 evidence spine;
- H6 runtime, reliability, token/cost coverage, freshness and findings;
- loading, empty, error, legacy and partial-telemetry states;
- self-contained HTML and machine-auditable JSON export on demand.
Platform remains the centralized operations layer: multi-project fleet views,
organization RBAC, shared approval queues, remote ingestion, governed settings
and managed retention.
## Lifecycle
```text
Prompt finishes
└─ hook writes canonical trace/events/H6 + latest-run receipt
└─ no server, no HTML export, no second model call
Developer runs `casan view`
└─ Core starts or reuses a loopback viewer
├─ reads bounded evidence projections
├─ opens latest/specified trace
└─ generates HTML/JSON only when Export is selected
```
Do not run `casan report export` after every prompt. Export is an independent
review artifact, not the evidence source of truth.
## Commands
```bash
casan report latest
casan view [trace-id] [--no-open]
casan report export [trace-id] --format html|json [-o path]
casan report export --h6 --format html|json [-o path]
casan dashboard start [--port 0]
casan dashboard open
casan dashboard status
casan dashboard stop
```
When Platform code is installed, `casan dashboard` manages the centralized
Control Plane. `casan view` remains the predictable Core single-project viewer.
## Runtime and packaging contract
- Python standard library only; no Node/npm or network dependency.
- Static HTML/CSS/JS ships inside `casan-core`.
- System fonts only; no CDN, analytics or external asset requests.
- Managed and vendored Core use the same runtime allowlist.
- Runtime tests and source-only tooling do not cross the release boundary.
- Evidence remains under the project's `.specify/` tree.
## Security model
- binds only to `127.0.0.1` on an ephemeral port by default;
- rejects non-loopback clients and non-loopback `Host` headers;
- requires a high-entropy session token for every API/export request;
- stores viewer state as `0600` on POSIX and under the project ACL on Windows;
- exposes GET-only evidence APIs; mutation verbs return `405`;
- sends CSP, `no-store`, `nosniff`, frame denial and no-referrer headers;
- validates trace/project filters and never resolves a request path as a file;
- bounds source reads, record counts and response size;
- rotates the non-evidence request log at 1 MiB and never logs the session token;
- redacts prompt bodies, credentials, tool input/output and authorization data;
- keeps missing token/cost values `null`, never fabricated as zero.
The UI shell itself is non-sensitive and may load without a token. All project
metadata, evidence and exports require the session token.
## Production acceptance gate
The Core viewer is releasable only when all of these pass:
1. unit contracts for empty, legacy, partial, certified and unsafe-input states;
2. clean-project test from the built `casan-core` tarball;
3. no `packages/casan-control-panel`, `package.json` or Node dependency in Core;
4. unauthorized API, mutation and Host-header checks fail closed;
5. run/H6 HTML and JSON export work offline;
6. browser validation covers the report hierarchy, interaction and responsive
navigation;
7. existing harness, DevKit installation and packaging suites remain green.
## Maturity statement
This viewer improves evidence usability; it does not grant CASAN Maturity L4.
Maturity is assessed from real operational evidence, ownership, controls and
repeatability. The interface must show the recorded maturity status and must
not infer a level from the installed edition.
@@ -15,9 +15,11 @@ CASAN Maturity L1–L5 describes evidence-backed operational adoption.
| Gitea CI gate template | — | Included | Included | — |
| Control Panel: runs, governance, security, cost, approvals | — | — | Included | Not a shipped edition |
| Prompt assurance receipt + latest-run discovery | Included | Included | Included | Building blocks only |
| Clickable trace deep link when enrolled | Included | Included | Included | Building blocks only |
| One-command local Control Plane launcher | — | — | Included | Not a shipped edition |
| H1–H7 live assurance rail + per-run HTML/JSON export | — | — | Included | Not a shipped edition |
| Core Local Assurance Viewer (single project, offline, read-only) | Included | Included | Included | Building blocks only |
| H1–H7 assurance rail + per-run HTML/JSON export | Included | Included | Included | Building blocks only |
| H6 AgentOps dossier + history + filters + HTML/JSON export | Included | Included | Included | Building blocks only |
| Clickable local trace deep link; central link when enrolled | Included | Included | Included | Building blocks only |
| One-command centralized Control Plane launcher | — | — | Included | Not a shipped edition |
| Async HMAC telemetry delivery with durable local spool | Included | Included | Included | Building blocks only |
| Evidence Pack Viewer | — | — | Included | Not a shipped edition |
| Governed chat/operator/codegen MVP | — | — | Included, preview | Not a shipped edition |
@@ -31,11 +33,13 @@ CASAN Maturity L1–L5 describes evidence-backed operational adoption.
## Edition rules
- **Core** is the first paid-PoC product: use it with the customer’s existing
AI coding tools and CI. It is self-hosted and evidence-oriented.
AI coding tools and CI. It includes a production-quality single-project
Local Assurance Viewer without Node/npm or a Platform dependency.
- **DevKit** adds repeatable adoption material. It does not add a managed
service or a security certification.
- **Platform Preview** includes the Control Panel and the Evidence Pack Viewer,
but is not production enterprise software. The bundle has a
- **Platform Preview** includes the Control Panel and Evidence Pack Viewer,
plus centralized multi-project operations, RBAC and approvals. It is not
production enterprise software. The bundle has a
`PREVIEW-INCOMPLETE.txt` marker by design.
- **Enterprise** remains `future` and `package-release.sh enterprise` refuses
to generate an artifact. Do not advertise it as a purchasable edition.
@@ -11,51 +11,113 @@ const number = (value: number): string => new Intl.NumberFormat('en-US').format(
const money = (value: number): string => `$${value.toFixed(6)}`;
function breakdownRows(rows: H6Breakdown[]): string {
if (rows.length === 0) return '<tr><td colspan="6" class="empty">No records in the selected scope.</td></tr>';
if (rows.length === 0) return '<tr><td colspan="6" class="empty">No records in the selected evidence boundary.</td></tr>';
return rows.map((row) => `<tr>
<td>${escapeHtml(row.key)}</td><td>${number(row.runs)}</td><td>${number(row.failures)}</td>
<td>${number(row.latency_avg_ms)} ms</td><td>${row.tokens === null ? 'Unavailable' : number(row.tokens)}</td><td>${row.cost_usd === null ? 'Unavailable' : money(row.cost_usd)}</td>
<td><strong>${escapeHtml(row.key)}</strong></td>
<td class="numeric">${number(row.runs)}</td>
<td class="numeric ${row.failures > 0 ? 'negative' : ''}">${number(row.failures)}</td>
<td class="numeric">${number(row.latency_avg_ms)} ms</td>
<td class="numeric">${row.tokens === null ? '<span class="unavailable">Unavailable</span>' : number(row.tokens)}</td>
<td class="numeric">${row.cost_usd === null ? '<span class="unavailable">Unavailable</span>' : money(row.cost_usd)}</td>
</tr>`).join('');
}
function coverage(label: string, value: number, detail: string): string {
const bounded = Math.min(Math.max(value, 0), 100);
const tone = bounded >= 95 ? 'good' : bounded > 0 ? 'warn' : 'empty';
return `<div class="coverage">
<div class="coverage-head"><span>${escapeHtml(label)}</span><strong>${bounded}%</strong></div>
<div class="track"><i class="${tone}" style="width:${bounded}%"></i></div>
<small>${escapeHtml(detail)}</small>
</div>`;
}
export function renderH6ReportHtml(report: H6Report): string {
const verdictClass = report.verdict === 'pass' ? 'pass' : report.verdict === 'fail' ? 'fail' : report.verdict === 'attention' ? 'attention' : 'neutral';
const scope = [
report.scope.project ? `Project: ${report.scope.project}` : 'All projects',
report.scope.run ? `Run: ${report.scope.run}` : 'All runs',
report.scope.from ? `From: ${report.scope.from}` : null,
report.scope.to ? `To: ${report.scope.to}` : null,
report.scope.project ? `Project · ${report.scope.project}` : 'All projects',
report.scope.run ? `Run · ${report.scope.run}` : 'All runs',
report.scope.from ? `From · ${report.scope.from}` : null,
report.scope.to ? `To · ${report.scope.to}` : null,
].filter((value): value is string => Boolean(value));
const findings = report.findings.length
? report.findings.map((finding) => `<li class="${finding.severity}"><strong>${escapeHtml(finding.code)}</strong><span>${escapeHtml(finding.message)}</span></li>`).join('')
: '<li class="info"><strong>NO_FINDINGS</strong><span>No threshold breach was detected in this scope.</span></li>';
const sourceRows = report.evidence_sources.map((source) => `<tr>
<td>${escapeHtml(source.source)}</td><td>${source.present ? 'present' : 'missing'}</td><td>${source.stale ? 'stale' : 'fresh'}</td>
<td>${source.age_s === null ? '—' : `${number(source.age_s)} s`}</td><td>${number(source.records)}</td><td><code>${escapeHtml(source.path)}</code></td>
</tr>`).join('');
? report.findings.map((finding, index) => `<article class="finding ${escapeHtml(finding.severity)}">
<div class="finding-index">${String(index + 1).padStart(2, '0')}</div>
<div><div class="finding-head"><strong>${escapeHtml(finding.code)}</strong>${finding.metric ? `<code>${escapeHtml(finding.metric)}: ${escapeHtml(finding.value)}${finding.threshold !== undefined ? ` / ${escapeHtml(finding.threshold)}` : ''}</code>` : ''}</div><p>${escapeHtml(finding.message)}</p></div>
</article>`).join('')
: '<article class="finding success"><div class="finding-index">✓</div><div><div class="finding-head"><strong>NO_THRESHOLD_BREACH</strong></div><p>No threshold breach was detected in this evidence boundary.</p></div></article>';
const sourceRows = report.evidence_sources.map((source) => {
const status = !source.present ? 'missing' : source.stale ? 'stale' : 'fresh';
return `<tr>
<td><strong>${escapeHtml(source.source)}</strong></td>
<td><span class="status ${status}">${status}</span></td>
<td class="numeric">${source.age_s === null ? 'Unavailable' : `${number(source.age_s)} s`}</td>
<td class="numeric">${number(source.records)}</td>
<td><code>${escapeHtml(source.path)}</code></td>
</tr>`;
}).join('');
const warnings = report.data_quality.warnings.length
? `<ul>${report.data_quality.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>`
: '<p>No data-quality warning.</p>';
? report.data_quality.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')
: '<li class="positive">All required sources are present and no estimation warning was detected.</li>';
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escapeHtml(report.title)} · ${escapeHtml(report.report_id)}</title>
<style>
:root{color-scheme:light;--ink:#172033;--muted:#64748b;--line:#dbe2ec;--paper:#fff;--wash:#f4f6fa;--indigo:#4f46e5;--teal:#0f766e}
*{box-sizing:border-box}body{margin:0;background:var(--wash);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.45}
main{max-width:1180px;margin:0 auto;padding:42px 28px 72px}.hero{position:relative;overflow:hidden;border:1px solid #cbd5e1;border-radius:20px;background:#111827;color:#f8fafc;padding:30px;box-shadow:0 24px 50px rgba(15,23,42,.14)}
.hero:after{content:"";position:absolute;right:-90px;top:-110px;width:300px;height:300px;border-radius:999px;background:radial-gradient(circle,#6366f1 0,transparent 68%);opacity:.5}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:#a5b4fc;font-weight:800}h1{font-size:34px;line-height:1.1;margin:8px 0}.scope{display:flex;gap:8px;flex-wrap:wrap;color:#cbd5e1;font-size:12px}.scope span{border:1px solid #334155;border-radius:999px;padding:6px 10px;background:#0f172a}.verdict{position:relative;z-index:1;display:inline-flex;margin-top:22px;border-radius:999px;padding:9px 14px;font-size:12px;text-transform:uppercase;letter-spacing:.12em;font-weight:900}.verdict.pass{background:#d1fae5;color:#065f46}.verdict.fail{background:#fee2e2;color:#991b1b}.verdict.attention{background:#fef3c7;color:#92400e}.verdict.neutral{background:#e2e8f0;color:#334155}
.meta{margin-top:10px;color:#94a3b8;font-size:12px}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:18px}.card,.section{background:var(--paper);border:1px solid var(--line);border-radius:16px;box-shadow:0 5px 14px rgba(15,23,42,.04)}.card{padding:17px}.card span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.1em;font-weight:700}.card strong{display:block;margin-top:6px;font-size:26px}.section{padding:22px;margin-top:18px}h2{font-size:16px;margin:0 0 14px}table{border-collapse:collapse;width:100%;font-size:12px}th{text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.1em;padding:9px;border-bottom:1px solid var(--line)}td{padding:10px 9px;border-bottom:1px solid #edf1f5;vertical-align:top}.empty{color:var(--muted);text-align:center;padding:24px}code{font-size:11px;word-break:break-all}.findings{list-style:none;padding:0;margin:0;display:grid;gap:9px}.findings li{display:grid;grid-template-columns:190px 1fr;gap:12px;border-left:4px solid #94a3b8;background:#f8fafc;padding:11px 13px;border-radius:8px}.findings li.warning{border-color:#f59e0b}.findings li.critical{border-color:#ef4444}.findings span{color:#475569}.quality{border-left:4px solid var(--indigo);background:#eef2ff;padding:14px 16px;border-radius:10px;font-size:13px}.quality p,.quality ul{margin:0}.quality ul{padding-left:18px}.footer{margin-top:24px;color:var(--muted);font-size:11px;text-align:center}
@media(max-width:800px){main{padding:20px 14px 50px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.hero{padding:22px}h1{font-size:28px}.table-wrap{overflow:auto}.findings li{grid-template-columns:1fr}}
@media print{body{background:#fff}main{max-width:none;padding:0}.hero,.card,.section{box-shadow:none}.hero{background:#111827!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}.section{break-inside:avoid}.footer{margin-top:10px}}
:root{color-scheme:light;--ink:#101828;--muted:#667085;--soft:#98a2b3;--line:#e4e7ec;--paper:#fff;--canvas:#f2f4f7;--navy:#0b1220;--cyan:#06b6d4;--green:#12b76a;--amber:#f79009;--red:#f04438}
*{box-sizing:border-box}html{background:var(--canvas)}body{margin:0;color:var(--ink);font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:1180px;margin:0 auto;padding:38px 24px 64px}
.cover{position:relative;overflow:hidden;border-left:7px solid var(--cyan);border-radius:22px;background:var(--navy);color:#fff;padding:30px 32px;box-shadow:0 24px 64px rgba(16,24,40,.18)}.cover:after{position:absolute;inset:0;content:"";opacity:.12;background-image:linear-gradient(rgba(255,255,255,.2) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.2) 1px,transparent 1px);background-size:32px 32px}.cover>*{position:relative;z-index:1}
.eyebrow{color:#67e8f9;font-size:10px;font-weight:900;letter-spacing:.22em;text-transform:uppercase}.cover h1{max-width:800px;margin:12px 0 8px;font-size:36px;line-height:1.08;letter-spacing:-.035em}.cover p{max-width:720px;margin:0;color:#b3b8c3;font-size:12px}.scope{display:flex;flex-wrap:wrap;gap:7px;margin-top:22px}.scope span{border:1px solid #344054;border-radius:999px;background:#111827;padding:6px 9px;color:#d0d5dd;font:9px ui-monospace,SFMono-Regular,Menlo,monospace}
.cover-foot{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-top:26px}.verdict{display:inline-flex;align-items:center;gap:8px;border:1px solid #344054;border-radius:999px;padding:8px 13px;font-size:10px;font-weight:900;letter-spacing:.15em;text-transform:uppercase}.verdict:before{width:7px;height:7px;border-radius:99px;background:#98a2b3;content:""}.verdict.pass:before{background:var(--green)}.verdict.attention:before{background:var(--amber)}.verdict.fail:before{background:var(--red)}.document-meta{color:#98a2b3;font:9px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:right}.document-meta strong{display:block;margin-top:3px;color:#e4e7ec;font-size:10px}
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:16px}.metric{min-height:112px;border:1px solid var(--line);border-radius:15px;background:var(--paper);padding:16px;box-shadow:0 8px 22px rgba(16,24,40,.045)}.metric span{display:block;color:var(--muted);font-size:9px;font-weight:900;letter-spacing:.13em;text-transform:uppercase}.metric strong{display:block;margin-top:9px;font-size:22px;line-height:1.15;letter-spacing:-.025em}.metric small{display:block;margin-top:6px;color:var(--soft);font-size:10px}.unavailable{color:#b54708;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-weight:700}
.section{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;border-bottom:1px solid #f2f4f7;padding:17px 20px}.kicker{color:#087e8b;font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.section-head h2{margin:3px 0 0;font-size:16px;letter-spacing:-.01em}.section-head p{max-width:580px;margin:3px 0 0;color:var(--muted);font-size:10px}.section-body{padding:18px 20px}
.findings{display:grid;gap:10px}.finding{display:grid;grid-template-columns:38px minmax(0,1fr);gap:12px;border:1px solid #eaecf0;border-left:4px solid var(--cyan);border-radius:10px;padding:12px}.finding.warning{border-left-color:var(--amber);background:#fffcf5}.finding.critical{border-left-color:var(--red);background:#fffbfa}.finding.success{border-left-color:var(--green);background:#f6fef9}.finding-index{color:#98a2b3;font:15px ui-monospace,SFMono-Regular,Menlo,monospace}.finding-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.finding-head strong{font:10px ui-monospace,SFMono-Regular,Menlo,monospace}.finding-head code{color:#667085;font-size:8px}.finding p{margin:5px 0 0;color:#475467;font-size:11px}
.quality-grid{display:grid;grid-template-columns:.75fr 1.25fr;gap:20px}.coverage+.coverage{margin-top:20px}.coverage-head{display:flex;justify-content:space-between;gap:12px}.coverage-head span{color:#667085;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.coverage-head strong{font:13px ui-monospace,SFMono-Regular,Menlo,monospace}.track{height:6px;margin-top:7px;overflow:hidden;border-radius:99px;background:#f2f4f7}.track i{display:block;height:100%;border-radius:99px;background:#d0d5dd}.track i.good{background:var(--green)}.track i.warn{background:var(--amber)}.coverage small{display:block;margin-top:6px;color:#98a2b3}.disclosure{border:1px solid #eaecf0;border-radius:12px;background:#f9fafb;padding:14px}.disclosure strong{font-size:10px;text-transform:uppercase;letter-spacing:.1em}.disclosure ul{margin:10px 0 0;padding-left:17px;color:#475467;font-size:10px}.disclosure li+li{margin-top:6px}.disclosure .positive{color:#027a48}
.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;font-size:10px}th{border-block:1px solid #eaecf0;background:#f9fafb;padding:9px 10px;color:#667085;font-size:8px;letter-spacing:.1em;text-align:left;text-transform:uppercase}td{border-bottom:1px solid #f2f4f7;padding:10px;vertical-align:top}tr:last-child td{border-bottom:0}.numeric{text-align:right;font:9px ui-monospace,SFMono-Regular,Menlo,monospace}.negative{color:#b42318;font-weight:700}td code{display:block;max-width:420px;color:#98a2b3;font-size:8px;word-break:break-all}.status{display:inline-flex;border:1px solid #d0d5dd;border-radius:999px;padding:3px 7px;color:#475467;font-size:8px;font-weight:900;text-transform:uppercase}.status.fresh{border-color:#a6f4c5;background:#ecfdf3;color:#027a48}.status.stale{border-color:#fedf89;background:#fffaeb;color:#b54708}.status.missing{border-color:#fecdca;background:#fef3f2;color:#b42318}.empty{padding:30px;color:#98a2b3;text-align:center}
.footer{display:flex;justify-content:space-between;gap:20px;margin-top:20px;border-top:1px solid var(--line);padding-top:12px;color:#98a2b3;font-size:9px}
@media(max-width:820px){main{padding:18px 12px 44px}.cover{padding:24px}.cover h1{font-size:28px}.cover-foot{display:block}.document-meta{margin-top:18px;text-align:left}.summary{grid-template-columns:repeat(2,1fr)}.quality-grid{grid-template-columns:1fr}.finding-head{display:block}.finding-head code{display:block;margin-top:4px}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.section,.metric,.finding{break-inside:avoid}.cover h1{font-size:30px}}
</style></head><body><main>
<section class="hero"><div class="eyebrow">CASAN assurance dossier · contract v${report.schema_version}</div><h1>${escapeHtml(report.title)}</h1><div class="scope">${scope.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div><div class="verdict ${verdictClass}">${escapeHtml(report.verdict)}</div><div class="meta">Generated ${escapeHtml(report.generated_at)} · ${escapeHtml(report.report_id)} · Freshness ${escapeHtml(report.freshness.status)}</div></section>
<section class="grid"><div class="card"><span>Governed runs</span><strong>${number(report.summary.runs)}</strong></div><div class="card"><span>Failure rate</span><strong>${report.summary.failure_rate_pct}%</strong></div><div class="card"><span>P95 latency</span><strong>${number(report.summary.latency_ms.p95)} ms</strong></div><div class="card"><span>Provider tokens</span><strong>${report.summary.coverage.token_records > 0 ? number(report.summary.tokens.provider_total ?? report.summary.tokens.total ?? 0) : 'Unavailable'}</strong></div><div class="card"><span>Actual provider cost</span><strong>${report.summary.cost_usd.provider_actual !== null ? money(report.summary.cost_usd.provider_actual) : 'Unavailable'}</strong></div><div class="card"><span>Token coverage</span><strong>${report.summary.coverage.token_pct}%</strong></div><div class="card"><span>Cost coverage</span><strong>${report.summary.coverage.cost_pct}%</strong></div><div class="card"><span>Retries</span><strong>${number(report.summary.retries)}</strong></div></section>
<section class="section"><h2>Verdict findings</h2><ul class="findings">${findings}</ul></section>
<section class="section"><h2>Evidence freshness</h2><div class="table-wrap"><table><thead><tr><th>Source</th><th>Presence</th><th>State</th><th>Age</th><th>Records</th><th>Path</th></tr></thead><tbody>${sourceRows}</tbody></table></div></section>
<section class="section"><h2>Step breakdown</h2><div class="table-wrap"><table><thead><tr><th>Step</th><th>Runs</th><th>Failures</th><th>Avg latency</th><th>Tokens</th><th>Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_step)}</tbody></table></div></section>
<section class="section"><h2>Provider/model breakdown</h2><div class="table-wrap"><table><thead><tr><th>Provider · model</th><th>Calls</th><th>Failures</th><th>Avg latency</th><th>Tokens</th><th>Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_provider)}</tbody></table></div></section>
<section class="section"><h2>Data quality · ${escapeHtml(report.data_quality.status)}</h2><div class="quality">${warnings}</div></section>
<div class="footer">Generated from CASAN runtime evidence. No maturity score or telemetry value is hard-coded in this document.</div>
<header class="cover">
<div class="eyebrow">CASAN · H6 AgentOps assurance</div>
<h1>Operational evidence, ready for a decision.</h1>
<p>${escapeHtml(report.title)} · Runtime health, reliability, provider usage, cost provenance and telemetry integrity.</p>
<div class="scope">${scope.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div>
<div class="cover-foot">
<div class="verdict ${verdictClass}">${escapeHtml(report.verdict)}</div>
<div class="document-meta">Report ID<strong>${escapeHtml(report.report_id)}</strong>Generated ${escapeHtml(report.generated_at)} · Freshness ${escapeHtml(report.freshness.status)}</div>
</div>
</header>
<section class="summary">
<div class="metric"><span>Governed runs</span><strong>${number(report.summary.runs)}</strong><small>${number(report.summary.success)} successful · ${number(report.summary.degraded)} degraded</small></div>
<div class="metric"><span>Failure rate</span><strong>${report.summary.failure_rate_pct}%</strong><small>${number(report.summary.failed)} failed · ${number(report.summary.alerts)} alerts</small></div>
<div class="metric"><span>P95 latency</span><strong>${number(report.summary.latency_ms.p95)} ms</strong><small>P50 ${number(report.summary.latency_ms.p50)} ms</small></div>
<div class="metric"><span>Data quality</span><strong>${escapeHtml(report.data_quality.status.replaceAll('_', ' '))}</strong><small>Coverage disclosed below</small></div>
<div class="metric"><span>Provider tokens</span><strong>${report.summary.coverage.token_records > 0 ? number(report.summary.tokens.provider_total ?? report.summary.tokens.total ?? 0) : '<span class="unavailable">Unavailable</span>'}</strong><small>${report.summary.coverage.token_pct}% record coverage</small></div>
<div class="metric"><span>Actual provider cost</span><strong>${report.summary.cost_usd.provider_actual !== null ? money(report.summary.cost_usd.provider_actual) : '<span class="unavailable">Unavailable</span>'}</strong><small>${number(report.summary.provider_calls)} provider calls</small></div>
<div class="metric"><span>Cost attribution</span><strong>${report.summary.coverage.cost_pct}%</strong><small>${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records</small></div>
<div class="metric"><span>Retries</span><strong>${number(report.summary.retries)}</strong><small>Observed reroute attempts</small></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Decision brief</div><h2>What requires attention</h2><p>Threshold findings are operational review signals, not maturity claims.</p></div><span class="status ${verdictClass}">${escapeHtml(report.verdict)}</span></div>
<div class="section-body"><div class="findings">${findings}</div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Evidence quality</div><h2>Coverage and disclosure</h2><p>Unavailable telemetry remains null and is never rendered as zero.</p></div><span class="status">${escapeHtml(report.data_quality.status)}</span></div>
<div class="section-body"><div class="quality-grid"><div>${coverage('Token attribution', report.summary.coverage.token_pct, `${report.summary.coverage.token_records}/${report.summary.coverage.runtime_records} runtime records`)}${coverage('Cost attribution', report.summary.coverage.cost_pct, `${report.summary.coverage.cost_records}/${report.summary.coverage.runtime_records} runtime records`)}</div><div class="disclosure"><strong>Disclosure notes</strong><ul>${warnings}</ul></div></div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Source integrity</div><h2>Evidence provenance</h2><p>Freshness and presence are evaluated independently for every canonical source.</p></div></div>
<div class="table-wrap"><table><thead><tr><th>Source</th><th>State</th><th class="numeric">Age</th><th class="numeric">Records</th><th>Canonical path</th></tr></thead><tbody>${sourceRows}</tbody></table></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Runtime</div><h2>Execution profile by step</h2></div></div>
<div class="table-wrap"><table><thead><tr><th>Step</th><th class="numeric">Runs</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_step)}</tbody></table></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Provider</div><h2>Model usage and provenance</h2><p>Only provider-reported token and actual cost values are shown.</p></div></div>
<div class="table-wrap"><table><thead><tr><th>Provider · model</th><th class="numeric">Calls</th><th class="numeric">Failures</th><th class="numeric">Avg latency</th><th class="numeric">Tokens</th><th class="numeric">Cost</th></tr></thead><tbody>${breakdownRows(report.details.by_provider)}</tbody></table></div>
</section>
<footer class="footer"><span>Generated from CASAN runtime evidence. No maturity score or telemetry value is hard-coded in this document.</span><span>Contract v${report.schema_version} · ${escapeHtml(report.report_id)}</span></footer>
</main></body></html>`;
}
@@ -1,4 +1,4 @@
import type { RunAssuranceReport } from './run-report.js';
import type { RunAssuranceReport, RunGateSnapshot } from './run-report.js';
const escapeHtml = (value: unknown): string => String(value ?? '')
.replaceAll('&', '&amp;')
@@ -14,44 +14,91 @@ const badgeClass = (status: string): string => {
return 'fail';
};
const evidenceFields = (gate: RunGateSnapshot): string => {
const entries = Object.entries(gate.evidence);
if (entries.length === 0) return '<div class="empty-evidence">No safe evidence fields were emitted.</div>';
return `<dl class="manifest">${entries.map(([key, value]) => `
<div><dt>${escapeHtml(key)}</dt><dd>${escapeHtml(
typeof value === 'string' ? value : JSON.stringify(value),
)}</dd></div>`).join('')}</dl>`;
};
export function renderRunAssuranceHtml(report: RunAssuranceReport): string {
const gates = report.gates.map((gate) => `
<article class="gate">
<div class="gate-head"><strong>${escapeHtml(gate.title)}</strong><span class="badge ${badgeClass(gate.status)}">${escapeHtml(gate.status)}</span></div>
<p>${escapeHtml(gate.reason)}</p>
<small>${escapeHtml(gate.updated_at ?? 'No timestamp')}</small>
<details><summary>Evidence fields</summary><pre>${escapeHtml(JSON.stringify(gate.evidence, null, 2))}</pre></details>
</article>`).join('');
const tokenValue = report.summary.token_usage_available ? 'Available' : 'Unavailable';
const costValue = report.summary.cost_available ? 'Available' : 'Unavailable';
const evidenceState = report.source.trace_found && report.source.graph_found ? 'Verified' : 'Partial';
const gates = report.gates.map((gate, index) => `
<article class="gate ${badgeClass(gate.status)}">
<div class="gate-index">H${index + 1}</div>
<div class="gate-copy">
<div class="gate-heading"><h3>${escapeHtml(gate.title.split(' · ')[1] ?? gate.title)}</h3><span class="badge ${badgeClass(gate.status)}">${escapeHtml(gate.status)}</span></div>
<p class="gate-description">${escapeHtml(gate.description)}</p>
<div class="decision"><span>Latest decision</span><strong>${escapeHtml(gate.reason || 'No decision reason recorded.')}</strong><small>${escapeHtml(gate.updated_at ?? 'No timestamp')}</small></div>
<div class="evidence-title">Evidence fields</div>
${evidenceFields(gate)}
</div>
</article>`).join('');
const spine = report.gates.map((gate, index) => `
<div class="spine-node ${badgeClass(gate.status)}">
<span class="spine-dot">${gate.status === 'pass' ? '✓' : gate.status === 'warning' ? '!' : gate.status === 'queued' ? '○' : '×'}</span>
<strong>H${index + 1}</strong>
<small>${escapeHtml(gate.title.split(' · ')[1] ?? gate.title)}</small>
</div>`).join('');
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>CASAN assurance receipt · ${escapeHtml(report.trace_id)}</title>
<title>CASAN assurance dossier · ${escapeHtml(report.trace_id)}</title>
<style>
:root{color-scheme:light;--ink:#0f172a;--muted:#64748b;--line:#e2e8f0;--panel:#fff;--bg:#f8fafc;--indigo:#4f46e5}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}
main{max-width:1160px;margin:0 auto;padding:40px 24px 64px}.hero{position:relative;overflow:hidden;border-radius:28px;background:#111827;color:#fff;padding:32px;box-shadow:0 24px 60px rgba(15,23,42,.18)}
.hero:after{content:"";position:absolute;right:-80px;top:-100px;width:280px;height:280px;border-radius:50%;background:rgba(99,102,241,.3);filter:blur(50px)}.eyebrow{color:#a5b4fc;font-size:11px;font-weight:800;letter-spacing:.18em;text-transform:uppercase}
h1{position:relative;margin:8px 0 2px;font-size:30px;letter-spacing:-.035em}.trace{position:relative;color:#94a3b8;font:12px ui-monospace,SFMono-Regular,monospace;word-break:break-all}
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:18px 0}.metric,.section{border:1px solid var(--line);background:var(--panel);border-radius:18px;padding:18px}.metric span{display:block;color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.metric strong{display:block;margin-top:8px;font-size:22px}
.section{margin-top:16px}.section h2{margin:0 0 14px;font-size:16px}.rail{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:10px}.gate{min-width:0;border:1px solid var(--line);border-radius:15px;padding:14px;background:#fff}.gate-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.gate p{min-height:44px;color:#475569;font-size:12px}.gate small{color:#94a3b8;font-size:10px}.gate details{margin-top:10px;color:#64748b;font-size:10px}.gate summary{cursor:pointer;font-weight:700}.gate pre{max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:8px;background:#f8fafc;padding:8px;font-size:9px}.badge{border-radius:999px;padding:3px 7px;font-size:9px;font-weight:800;text-transform:uppercase}.pass{background:#dcfce7;color:#166534}.warn{background:#fef3c7;color:#92400e}.fail{background:#ffe4e6;color:#9f1239}.muted{background:#f1f5f9;color:#64748b}
.note{color:var(--muted);font-size:12px}.footer{margin-top:18px;color:#94a3b8;font-size:11px}
@media(max-width:900px){.summary{grid-template-columns:repeat(2,1fr)}.rail{grid-template-columns:1fr 1fr}}@media(max-width:520px){main{padding:18px 12px}.hero{padding:22px}.rail,.summary{grid-template-columns:1fr}}
@media print{body{background:#fff}main{max-width:none;padding:0}.hero,.metric,.section{box-shadow:none;break-inside:avoid}}
:root{color-scheme:light;--ink:#101828;--muted:#667085;--soft:#98a2b3;--line:#e4e7ec;--paper:#fff;--canvas:#f2f4f7;--navy:#0b1220;--cyan:#06b6d4;--green:#12b76a;--amber:#f79009;--red:#f04438}
*{box-sizing:border-box}html{background:var(--canvas)}body{margin:0;color:var(--ink);font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:1180px;margin:0 auto;padding:38px 24px 64px}.cover{position:relative;overflow:hidden;border-left:7px solid var(--cyan);border-radius:22px;background:var(--navy);color:#fff;padding:30px 32px;box-shadow:0 24px 64px rgba(16,24,40,.18)}
.cover:after{position:absolute;inset:0;content:"";opacity:.12;background-image:linear-gradient(rgba(255,255,255,.2) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.2) 1px,transparent 1px);background-size:32px 32px}.cover>*{position:relative;z-index:1}.eyebrow{color:#67e8f9;font-size:10px;font-weight:900;letter-spacing:.22em;text-transform:uppercase}.cover h1{max-width:780px;margin:12px 0 6px;font-size:36px;line-height:1.08;letter-spacing:-.035em}.trace{color:#98a2b3;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}
.cover-grid{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:24px;margin-top:26px}.verdict{display:inline-flex;align-items:center;gap:8px;border:1px solid #344054;border-radius:999px;padding:8px 13px;font-size:10px;font-weight:900;letter-spacing:.15em;text-transform:uppercase}.verdict:before{width:7px;height:7px;border-radius:99px;background:var(--cyan);content:""}.verdict.certified:before{background:var(--green)}.verdict.non_certified:before{background:var(--red)}
.document-meta{display:grid;grid-template-columns:repeat(2,minmax(130px,1fr));gap:8px 22px;color:#98a2b3;font-size:10px}.document-meta strong{display:block;margin-top:2px;color:#e4e7ec;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}
.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:16px}.metric{min-height:112px;border:1px solid var(--line);border-radius:15px;background:var(--paper);padding:16px;box-shadow:0 8px 22px rgba(16,24,40,.045)}.metric span{display:block;color:var(--muted);font-size:9px;font-weight:900;letter-spacing:.13em;text-transform:uppercase}.metric strong{display:block;margin-top:9px;font-size:22px;line-height:1.15;letter-spacing:-.025em}.metric small{display:block;margin-top:6px;color:var(--soft);font-size:10px}
.section{margin-top:16px;border:1px solid var(--line);border-radius:17px;background:var(--paper);box-shadow:0 8px 22px rgba(16,24,40,.04)}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:20px;border-bottom:1px solid #f2f4f7;padding:17px 20px}.section-head .kicker{color:#087e8b;font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.section-head h2{margin:3px 0 0;font-size:16px;letter-spacing:-.01em}.section-head p{max-width:540px;margin:3px 0 0;color:var(--muted);font-size:10px}
.spine{position:relative;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));padding:24px 18px 20px}.spine:before{position:absolute;left:8%;right:8%;top:43px;height:1px;background:var(--line);content:""}.spine-node{position:relative;text-align:center}.spine-dot{display:flex;width:38px;height:38px;margin:0 auto;align-items:center;justify-content:center;border:2px solid #d0d5dd;border-radius:99px;background:#fff;color:#667085;font-weight:900;box-shadow:0 0 0 5px #fff}.spine-node.pass .spine-dot{border-color:#6ce9a6;color:#027a48}.spine-node.warn .spine-dot{border-color:#fec84b;color:#b54708}.spine-node.fail .spine-dot{border-color:#fda29b;color:#b42318}.spine-node strong{display:block;margin-top:8px;color:#98a2b3;font-size:9px;letter-spacing:.13em}.spine-node small{display:block;margin-top:3px;overflow:hidden;color:#344054;font-size:9px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}
.controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:18px}.gate{display:grid;grid-template-columns:48px minmax(0,1fr);overflow:hidden;border:1px solid var(--line);border-radius:14px;background:#fff;break-inside:avoid}.gate-index{display:flex;align-items:flex-start;justify-content:center;border-right:1px solid var(--line);padding-top:18px;color:#98a2b3;font-size:12px;font-weight:900}.gate.pass{border-top:3px solid var(--green)}.gate.warn{border-top:3px solid var(--amber)}.gate.fail{border-top:3px solid var(--red)}.gate-copy{padding:15px}.gate-heading{display:flex;align-items:center;justify-content:space-between;gap:10px}.gate-heading h3{margin:0;font-size:14px}.badge{border:1px solid #d0d5dd;border-radius:999px;padding:3px 7px;color:#475467;font-size:8px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.badge.pass{border-color:#a6f4c5;background:#ecfdf3;color:#027a48}.badge.warn{border-color:#fedf89;background:#fffaeb;color:#b54708}.badge.fail{border-color:#fecdca;background:#fef3f2;color:#b42318}.gate-description{min-height:34px;margin:6px 0;color:var(--muted);font-size:10px}.decision{margin-top:10px;border-left:3px solid #d0d5dd;background:#f9fafb;padding:9px 10px}.decision span,.evidence-title{display:block;color:#667085;font-size:8px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.decision strong{display:block;margin-top:4px;font-size:10px;font-weight:600}.decision small{display:block;margin-top:4px;color:#98a2b3;font:8px ui-monospace,SFMono-Regular,Menlo,monospace}.evidence-title{margin-top:12px}.manifest{margin:6px 0 0;border:1px solid #eaecf0;border-radius:8px}.manifest div{display:grid;grid-template-columns:110px minmax(0,1fr);border-bottom:1px solid #eaecf0}.manifest div:last-child{border-bottom:0}.manifest dt,.manifest dd{margin:0;padding:6px 7px;font:8px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}.manifest dt{background:#f9fafb;color:#667085;font-weight:700}.manifest dd{color:#344054}.empty-evidence{margin-top:6px;border:1px dashed #d0d5dd;border-radius:8px;padding:10px;color:#98a2b3;font-size:9px;text-align:center}
.certification{display:grid;grid-template-columns:180px minmax(0,1fr);gap:20px;padding:19px 20px}.certification label{color:#667085;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.certification strong{display:block;margin-top:5px;font-size:15px}.certification p{margin:0;color:#475467;font-size:11px}.footer{display:flex;justify-content:space-between;gap:20px;margin-top:20px;border-top:1px solid var(--line);padding-top:12px;color:#98a2b3;font-size:9px}
@media(max-width:820px){main{padding:18px 12px 44px}.cover{padding:24px}.cover h1{font-size:28px}.cover-grid{grid-template-columns:1fr}.summary{grid-template-columns:repeat(2,1fr)}.spine{min-width:720px}.spine-wrap{overflow:auto}.controls{grid-template-columns:1fr}.document-meta{grid-template-columns:1fr 1fr}.certification{grid-template-columns:1fr}}
@page{size:A4;margin:12mm}@media print{html,body{background:#fff}main{max-width:none;padding:0}.cover,.metric,.section{box-shadow:none}.cover{-webkit-print-color-adjust:exact;print-color-adjust:exact}.summary{grid-template-columns:repeat(4,1fr)}.spine{padding-inline:8px}.controls{grid-template-columns:repeat(2,1fr);padding:12px}.section,.metric,.gate{break-inside:avoid}.footer{position:relative}.cover h1{font-size:30px}}
</style></head><body><main>
<section class="hero"><div class="eyebrow">CASAN · evidence-backed assurance</div><h1>${escapeHtml(report.verdict.replace('_', ' ').toUpperCase())}</h1><div class="trace">${escapeHtml(report.trace_id)}</div></section>
<header class="cover">
<div class="eyebrow">CASAN · independent assurance dossier</div>
<h1>${escapeHtml(report.verdict.replace('_', ' ').toUpperCase())} governed run</h1>
<div class="trace">${escapeHtml(report.trace_id)}</div>
<div class="cover-grid">
<div class="verdict ${escapeHtml(report.verdict)}">${escapeHtml(report.verdict.replace('_', ' '))}</div>
<div class="document-meta">
<div>Report ID<strong>${escapeHtml(report.report_id)}</strong></div>
<div>Generated<strong>${escapeHtml(report.generated_at)}</strong></div>
<div>Project<strong>${escapeHtml(report.project.id)}</strong></div>
<div>Edition / maturity<strong>${escapeHtml(report.project.edition ?? 'unknown')} · ${escapeHtml(report.project.maturity?.status ?? 'not assessed')}</strong></div>
</div>
</div>
</header>
<section class="summary">
<div class="metric"><span>Harness gates</span><strong>${report.summary.gates_observed}/7</strong></div>
<div class="metric"><span>H6 quality</span><strong>${escapeHtml(report.summary.telemetry_quality)}</strong></div>
<div class="metric"><span>Token usage</span><strong>${tokenValue}</strong></div>
<div class="metric"><span>Cost</span><strong>${costValue}</strong></div>
<div class="metric"><span>Duration</span><strong>${report.summary.duration_ms === null ? 'Unavailable' : `${report.summary.duration_ms} ms`}</strong></div>
<div class="metric"><span>Tool calls</span><strong>${report.summary.tool_calls}</strong></div>
<div class="metric"><span>Failures</span><strong>${report.summary.failures}</strong></div>
<div class="metric"><span>Evidence source</span><strong>${report.source.trace_found && report.source.graph_found ? 'Verified' : 'Partial'}</strong></div>
<div class="metric"><span>Harness controls</span><strong>${report.summary.gates_observed}/7</strong><small>H1–H7 observed</small></div>
<div class="metric"><span>Evidence source</span><strong>${evidenceState}</strong><small>Trace + graph reconstruction</small></div>
<div class="metric"><span>H6 quality</span><strong>${escapeHtml(report.summary.telemetry_quality)}</strong><small>Telemetry disclosure status</small></div>
<div class="metric"><span>Duration</span><strong>${report.summary.duration_ms === null ? 'Unavailable' : `${report.summary.duration_ms} ms`}</strong><small>End-to-end governed run</small></div>
<div class="metric"><span>Tool calls</span><strong>${report.summary.tool_calls}</strong><small>Observed side-effect attempts</small></div>
<div class="metric"><span>Failures</span><strong>${report.summary.failures}</strong><small>Recorded execution failures</small></div>
<div class="metric"><span>Token usage</span><strong>${tokenValue}</strong><small>Never inferred as zero</small></div>
<div class="metric"><span>Actual cost</span><strong>${costValue}</strong><small>Provider attribution only</small></div>
</section>
<section class="section"><h2>Live assurance rail · H1 → H7</h2><div class="rail">${gates}</div></section>
<section class="section"><h2>Certification</h2><p>Strength: <strong>${escapeHtml(report.certification.strength ?? 'unknown')}</strong></p><p class="note">${escapeHtml(report.certification.reasons.join(' · ') || 'No certification reason recorded.')}</p></section>
<div class="footer">Generated ${escapeHtml(report.generated_at)} · Project ${escapeHtml(report.project.id)} · Edition ${escapeHtml(report.project.edition ?? 'unknown')} · Maturity ${escapeHtml(report.project.maturity?.status ?? 'not assessed')}. No maturity score or unavailable telemetry value is hard-coded.</div>
<section class="section">
<div class="section-head"><div><div class="kicker">Control sequence</div><h2>Live assurance rail · H1 → H7</h2><p>The evidence spine shows the terminal state of every governed control.</p></div><span class="badge ${badgeClass(report.verdict)}">${escapeHtml(report.verdict)}</span></div>
<div class="spine-wrap"><div class="spine">${spine}</div></div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Control dossiers</div><h2>Decision and evidence detail</h2><p>Every field below is sanitized before persistence. Raw prompts and tool output are excluded.</p></div></div>
<div class="controls">${gates}</div>
</section>
<section class="section">
<div class="section-head"><div><div class="kicker">Certification</div><h2>Assurance conclusion</h2></div></div>
<div class="certification"><div><label>Strength</label><strong>${escapeHtml(report.certification.strength ?? 'unknown')}</strong></div><p>${escapeHtml(report.certification.reasons.join(' · ') || 'No certification reason recorded.')}</p></div>
</section>
<footer class="footer"><span>Generated from CASAN runtime evidence. Unavailable values remain null.</span><span>Schema v${report.schema_version} · ${escapeHtml(report.trace_id)}</span></footer>
</main></body></html>`;
}
@@ -144,6 +144,9 @@ test('HTML export is standalone, escaped and contains no hard-coded maturity sco
assert.doesNotMatch(html, /<script>alert\(1\)<\/script>/);
assert.doesNotMatch(html, /Average\s+\d|\/100|218 core tests/i);
assert.match(html, /No maturity score or telemetry value is hard-coded/);
assert.match(html, /Operational evidence, ready for a decision/);
assert.match(html, /Evidence provenance/);
assert.match(html, /@page\{size:A4/);
});
test('per-run assurance export carries H1-H7 and truthful H6 availability', () => {
@@ -188,6 +191,8 @@ test('per-run assurance export carries H1-H7 and truthful H6 availability', () =
const html = renderRunAssuranceHtml(report);
assert.match(html, /Live assurance rail · H1 → H7/);
assert.match(html, /Evidence fields/);
assert.match(html, /Decision and evidence detail/);
assert.match(html, /@page\{size:A4/);
assert.match(html, /Unavailable/);
assert.doesNotMatch(html, /\$0(?:\.0+)?/);
});
@@ -0,0 +1,176 @@
import type { ReactNode } from 'react';
type Tone = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
const TONE: Record<Tone, {
badge: string;
line: string;
value: string;
wash: string;
}> = {
neutral: {
badge: 'border-slate-600 bg-slate-800 text-slate-200',
line: 'bg-slate-500',
value: 'text-slate-950',
wash: 'border-slate-200 bg-white',
},
success: {
badge: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-200',
line: 'bg-emerald-500',
value: 'text-emerald-700',
wash: 'border-emerald-200 bg-emerald-50/45',
},
warning: {
badge: 'border-amber-400/40 bg-amber-400/10 text-amber-100',
line: 'bg-amber-500',
value: 'text-amber-700',
wash: 'border-amber-200 bg-amber-50/45',
},
danger: {
badge: 'border-rose-400/40 bg-rose-400/10 text-rose-100',
line: 'bg-rose-500',
value: 'text-rose-700',
wash: 'border-rose-200 bg-rose-50/45',
},
info: {
badge: 'border-cyan-400/40 bg-cyan-400/10 text-cyan-100',
line: 'bg-cyan-500',
value: 'text-cyan-700',
wash: 'border-cyan-200 bg-cyan-50/45',
},
};
export function reportTone(value: string): Tone {
const normalized = value.toLowerCase();
if (['pass', 'fresh', 'certified', 'success', 'verified'].includes(normalized)) return 'success';
if (['attention', 'warning', 'warn', 'stale', 'insufficient', 'degraded'].includes(normalized)) return 'warning';
if (['fail', 'failed', 'error', 'blocked', 'critical'].includes(normalized)) return 'danger';
if (['running', 'live', 'streaming'].includes(normalized)) return 'info';
return 'neutral';
}
export function ReportHero({
eyebrow,
title,
description,
verdict,
meta,
actions,
}: {
eyebrow: string;
title: string;
description: string;
verdict?: string;
meta?: ReactNode;
actions?: ReactNode;
}) {
const tone = reportTone(verdict ?? 'neutral');
return (
<section className="relative overflow-hidden rounded-[1.75rem] border border-slate-800 bg-slate-950 text-white shadow-[0_28px_80px_rgba(15,23,42,0.22)]">
<div className="pointer-events-none absolute inset-0 opacity-[0.16] [background-image:linear-gradient(rgba(148,163,184,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.18)_1px,transparent_1px)] [background-size:32px_32px]" />
<div className={`absolute inset-y-0 left-0 w-1.5 ${TONE[tone].line}`} />
<div className="relative grid gap-8 px-5 py-6 sm:px-8 sm:py-8 xl:grid-cols-[minmax(0,1fr)_auto] xl:items-end">
<div>
<div className="flex flex-wrap items-center gap-3">
<span className="text-[10px] font-black uppercase tracking-[0.24em] text-cyan-300">{eyebrow}</span>
<span className="h-px w-10 bg-slate-700" />
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Evidence-backed</span>
</div>
<h1 className="mt-4 max-w-4xl text-[2rem] font-semibold leading-[1.08] tracking-[-0.035em] text-white sm:text-[2.65rem]">{title}</h1>
<p className="mt-4 max-w-3xl text-sm leading-6 text-slate-300 sm:text-[15px]">{description}</p>
{meta && <div className="mt-5 flex flex-wrap items-center gap-x-5 gap-y-2 font-mono text-[11px] text-slate-400">{meta}</div>}
</div>
<div className="flex flex-col items-start gap-3 xl:items-end">
{verdict && (
<span className={`inline-flex min-h-9 items-center rounded-full border px-4 py-2 text-[11px] font-black uppercase tracking-[0.18em] ${TONE[tone].badge}`}>
<span className={`mr-2 h-1.5 w-1.5 rounded-full ${TONE[tone].line}`} />
{verdict.replaceAll('_', ' ')}
</span>
)}
{actions && <div className="flex flex-wrap gap-2 xl:justify-end">{actions}</div>}
</div>
</div>
</section>
);
}
export function ReportPanel({
eyebrow,
title,
description,
right,
children,
className = '',
}: {
eyebrow?: string;
title: string;
description?: string;
right?: ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<section className={`overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-[0_14px_36px_rgba(15,23,42,0.055)] ${className}`}>
<header className="flex flex-col gap-4 border-b border-slate-100 px-5 py-5 sm:flex-row sm:items-start sm:justify-between sm:px-6">
<div>
{eyebrow && <p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-700">{eyebrow}</p>}
<h2 className={`${eyebrow ? 'mt-1.5' : ''} text-base font-semibold tracking-[-0.01em] text-slate-950`}>{title}</h2>
{description && <p className="mt-1.5 max-w-3xl text-xs leading-5 text-slate-500">{description}</p>}
</div>
{right}
</header>
<div className="p-5 sm:p-6">{children}</div>
</section>
);
}
export function SignalMetric({
label,
value,
detail,
tone = 'neutral',
primary = false,
}: {
label: string;
value: ReactNode;
detail?: ReactNode;
tone?: Tone;
primary?: boolean;
}) {
return (
<div className={`relative min-h-[8.25rem] overflow-hidden rounded-2xl border p-4 sm:p-5 ${TONE[tone].wash}`}>
<span className={`absolute inset-y-0 left-0 w-1 ${TONE[tone].line}`} />
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">{label}</p>
<div className={`mt-3 font-semibold tracking-[-0.035em] ${primary ? 'text-3xl sm:text-[2.1rem]' : 'text-2xl'} ${TONE[tone].value}`}>{value}</div>
{detail && <div className="mt-2 text-xs leading-5 text-slate-500">{detail}</div>}
</div>
);
}
export function CoverageBar({
label,
value,
detail,
}: {
label: string;
value: number;
detail: string;
}) {
const bounded = Math.min(Math.max(value, 0), 100);
const tone = bounded >= 95 ? 'bg-emerald-500' : bounded >= 70 ? 'bg-cyan-500' : bounded > 0 ? 'bg-amber-500' : 'bg-slate-300';
return (
<div>
<div className="flex items-baseline justify-between gap-4">
<span className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-500">{label}</span>
<span className="font-mono text-sm font-semibold text-slate-900">{bounded}%</span>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-slate-100">
<div className={`h-full rounded-full ${tone}`} style={{ width: `${bounded}%` }} />
</div>
<p className="mt-2 text-xs text-slate-500">{detail}</p>
</div>
);
}
export const reportActionPrimary = 'inline-flex min-h-11 items-center justify-center rounded-xl bg-white px-4 py-2.5 text-xs font-bold text-slate-950 shadow-sm transition hover:-translate-y-px hover:bg-cyan-50 focus:outline-none focus:ring-2 focus:ring-cyan-300';
export const reportActionSecondary = 'inline-flex min-h-11 items-center justify-center rounded-xl border border-slate-700 bg-slate-900/70 px-4 py-2.5 text-xs font-bold text-slate-200 transition hover:-translate-y-px hover:border-slate-500 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-cyan-400';
@@ -1,13 +1,18 @@
import type { HarnessGateStatus, HarnessTraceGraph } from '../../lib/api';
const TONE: Record<HarnessGateStatus, string> = {
queued: 'border-slate-200 bg-slate-100 text-slate-400',
running: 'border-blue-300 bg-blue-50 text-blue-700',
pass: 'border-emerald-300 bg-emerald-50 text-emerald-700',
warning: 'border-amber-300 bg-amber-50 text-amber-700',
blocked: 'border-rose-300 bg-rose-50 text-rose-700',
error: 'border-rose-300 bg-rose-50 text-rose-700',
skipped: 'border-slate-200 bg-slate-100 text-slate-400',
const TONE: Record<HarnessGateStatus, {
ring: string;
dot: string;
text: string;
surface: string;
}> = {
queued: { ring: 'border-slate-300', dot: 'bg-slate-300', text: 'text-slate-500', surface: 'bg-slate-50' },
running: { ring: 'border-cyan-400', dot: 'bg-cyan-400', text: 'text-cyan-800', surface: 'bg-cyan-50' },
pass: { ring: 'border-emerald-400', dot: 'bg-emerald-500', text: 'text-emerald-800', surface: 'bg-emerald-50' },
warning: { ring: 'border-amber-400', dot: 'bg-amber-500', text: 'text-amber-800', surface: 'bg-amber-50' },
blocked: { ring: 'border-rose-500', dot: 'bg-rose-500', text: 'text-rose-800', surface: 'bg-rose-50' },
error: { ring: 'border-rose-500', dot: 'bg-rose-500', text: 'text-rose-800', surface: 'bg-rose-50' },
skipped: { ring: 'border-slate-300', dot: 'bg-slate-300', text: 'text-slate-500', surface: 'bg-slate-50' },
};
const MARK: Record<HarnessGateStatus, string> = {
@@ -20,22 +25,47 @@ const MARK: Record<HarnessGateStatus, string> = {
skipped: '–',
};
export function AssuranceRail({ graph }: { graph: HarnessTraceGraph }) {
export function AssuranceRail({
graph,
selectedId,
onSelect,
}: {
graph: HarnessTraceGraph;
selectedId?: string;
onSelect?: (id: string) => void;
}) {
return (
<div className="overflow-x-auto pb-1">
<div className="flex min-w-[760px] items-center">
{graph.nodes.map((node, index) => (
<div key={node.id} className="flex min-w-0 flex-1 items-center">
<div className={`min-w-0 flex-1 rounded-xl border p-3 ${TONE[node.status]}`}>
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-black uppercase tracking-[0.12em]">{node.title.split(' · ')[0]}</span>
<span className={`flex h-5 w-5 items-center justify-center rounded-full border border-current text-[10px] ${node.status === 'running' ? 'animate-pulse' : ''}`}>{MARK[node.status]}</span>
</div>
<div className="mt-2 truncate text-xs font-semibold">{node.title.split(' · ')[1]}</div>
</div>
{index < graph.nodes.length - 1 && <div className={`h-px w-3 shrink-0 ${node.status === 'pass' ? 'bg-emerald-300' : 'bg-slate-200'}`} />}
</div>
))}
<div className="overflow-x-auto pb-2">
<div className="relative min-w-[860px] px-3 pt-2">
<div className="absolute left-[4.3rem] right-[4.3rem] top-[2.05rem] h-px bg-slate-200" aria-hidden="true" />
<div className="relative grid grid-cols-7">
{graph.nodes.map((node, index) => {
const tone = TONE[node.status];
const selected = selectedId === node.id;
const Element = onSelect ? 'button' : 'div';
return (
<Element
key={node.id}
{...(onSelect ? {
type: 'button' as const,
onClick: () => onSelect(node.id),
'aria-pressed': selected,
} : {})}
className={`group min-w-0 px-1.5 text-center ${onSelect ? 'cursor-pointer focus:outline-none' : ''}`}
>
<div className={`relative mx-auto flex h-11 w-11 items-center justify-center rounded-full border-2 bg-white text-sm font-black shadow-[0_0_0_5px_white] transition ${tone.ring} ${tone.text} ${selected ? 'scale-110 ring-4 ring-cyan-100' : onSelect ? 'group-hover:-translate-y-0.5 group-hover:shadow-md' : ''}`}>
<span className={`absolute right-0 top-0 h-2.5 w-2.5 rounded-full border-2 border-white ${tone.dot} ${node.status === 'running' ? 'animate-pulse' : ''}`} />
{MARK[node.status]}
</div>
<div className={`mx-auto mt-3 rounded-lg px-2 py-2 transition ${selected ? tone.surface : 'bg-transparent'}`}>
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">H{index + 1}</p>
<p className={`mt-1 truncate text-xs font-semibold ${tone.text}`}>{node.title.split(' · ')[1] ?? node.title}</p>
<p className="mt-1 truncate text-[10px] capitalize text-slate-400">{node.status}</p>
</div>
</Element>
);
})}
</div>
</div>
</div>
);
@@ -1,56 +1,86 @@
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api, runReportExportUrl, type HarnessGateNode, type HarnessGateStatus, type HarnessTraceGraph } from '../../lib/api';
import { Card, StatusBadge } from '../ui/Card';
import { api, runReportExportUrl, type HarnessGateNode, type HarnessTraceGraph } from '../../lib/api';
import { ReportPanel } from '../report/ReportPrimitives';
import { StatusBadge } from '../ui/Card';
import { AssuranceRail } from './AssuranceRail';
const NODE_TONE: Record<HarnessGateStatus, string> = {
queued: 'border-slate-200 bg-slate-50 text-slate-500',
running: 'border-blue-300 bg-blue-50 text-blue-800 shadow-[0_0_0_4px_rgba(59,130,246,0.1)]',
pass: 'border-emerald-300 bg-emerald-50 text-emerald-800',
warning: 'border-amber-300 bg-amber-50 text-amber-800',
blocked: 'border-rose-400 bg-rose-50 text-rose-800 shadow-[0_0_0_4px_rgba(244,63,94,0.09)]',
error: 'border-rose-400 bg-rose-50 text-rose-800 shadow-[0_0_0_4px_rgba(244,63,94,0.09)]',
skipped: 'border-slate-200 bg-slate-100 text-slate-400',
};
function displayTime(value: string | null): string {
if (!value) return 'No timestamp';
return value.replace('T', ' ').replace('Z', ' UTC');
}
const STATUS_MARK: Record<HarnessGateStatus, string> = {
queued: '○',
running: '●',
pass: '✓',
warning: '!',
blocked: '×',
error: '×',
skipped: '–',
};
function EvidenceValue({ value }: { value: unknown }) {
if (value === null || value === undefined || value === '') return <span className="text-slate-500">Unavailable</span>;
return <>{typeof value === 'string' ? value : JSON.stringify(value)}</>;
}
function Evidence({ node }: { node: HarnessGateNode }) {
function GateDossier({ node }: { node: HarnessGateNode }) {
const entries = Object.entries(node.evidence);
return (
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.8fr)]">
<div>
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-lg font-semibold text-slate-900">{node.title}</h3>
<StatusBadge value={node.status} />
<div className="grid gap-5 xl:grid-cols-[minmax(0,0.88fr)_minmax(24rem,1.12fr)]">
<div className="space-y-4">
<div>
<div className="flex flex-wrap items-center gap-2">
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-700">Selected control</p>
<StatusBadge value={node.status} />
</div>
<h3 className="mt-2 text-xl font-semibold tracking-[-0.02em] text-slate-950">{node.title}</h3>
<p className="mt-2 text-sm leading-6 text-slate-500">{node.description}</p>
</div>
<p className="mt-1 text-sm text-slate-500">{node.description}</p>
<div className="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Latest decision</div>
<p className="mt-2 text-sm leading-6 text-slate-700">{node.reason}</p>
{node.updated_at && <p className="mt-2 text-xs text-slate-400">{node.updated_at.replace('T', ' ').replace('Z', '')}</p>}
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4">
<div className="flex items-center justify-between gap-3">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Latest decision</p>
<span className="font-mono text-[10px] text-slate-400">{displayTime(node.updated_at)}</span>
</div>
<p className="mt-3 text-sm font-medium leading-6 text-slate-800">{node.reason || 'No decision reason was emitted.'}</p>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-4">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Event history</p>
<div className="mt-3 space-y-3">
{node.events.slice(-4).reverse().map((event, index) => (
<div key={`${event.timestamp}-${index}`} className="grid grid-cols-[0.65rem_minmax(0,1fr)] gap-3">
<div className="flex flex-col items-center">
<span className={`mt-1 h-2 w-2 rounded-full ${event.status === 'pass' ? 'bg-emerald-500' : event.status === 'warning' ? 'bg-amber-500' : event.status === 'error' || event.status === 'blocked' ? 'bg-rose-500' : 'bg-cyan-500'}`} />
{index < Math.min(node.events.length, 4) - 1 && <span className="mt-1 h-full w-px bg-slate-200" />}
</div>
<div className="pb-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-xs font-semibold capitalize text-slate-800">{event.status}</span>
<span className="font-mono text-[10px] text-slate-400">{displayTime(event.timestamp)}</span>
</div>
<p className="mt-1 text-xs leading-5 text-slate-500">{event.reason}</p>
</div>
</div>
))}
{node.events.length === 0 && <p className="text-sm text-slate-500">No individual event was emitted for this control.</p>}
</div>
</div>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-950 p-4 text-slate-100">
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Safe evidence</div>
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 text-slate-100 shadow-[0_18px_50px_rgba(15,23,42,0.18)]">
<div className="flex items-center justify-between gap-3 border-b border-slate-800 px-5 py-4">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-300">Safe evidence manifest</p>
<p className="mt-1 text-xs text-slate-500">Sanitized fields persisted by the harness</p>
</div>
<span className="rounded-full border border-slate-700 bg-slate-900 px-2.5 py-1 font-mono text-[10px] text-slate-400">{entries.length} fields</span>
</div>
{entries.length > 0 ? (
<dl className="mt-3 space-y-2 text-xs">
<dl className="divide-y divide-slate-800">
{entries.map(([key, value]) => (
<div key={key} className="grid grid-cols-[8rem_minmax(0,1fr)] gap-3 border-b border-slate-800 pb-2 last:border-0">
<dt className="font-medium text-slate-400">{key}</dt>
<dd className="break-all font-mono text-slate-200">{typeof value === 'string' ? value : JSON.stringify(value)}</dd>
<div key={key} className="grid gap-1 px-5 py-3.5 sm:grid-cols-[10rem_minmax(0,1fr)] sm:gap-4">
<dt className="font-mono text-[10px] font-semibold uppercase tracking-[0.08em] text-slate-500">{key}</dt>
<dd className="break-all font-mono text-xs leading-5 text-slate-200"><EvidenceValue value={value} /></dd>
</div>
))}
</dl>
) : <p className="mt-3 text-sm text-slate-500">No evidence has been emitted for this gate yet.</p>}
) : (
<div className="px-5 py-12 text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full border border-slate-700 text-slate-500">∅</div>
<p className="mt-3 text-sm text-slate-400">No safe evidence fields are available yet.</p>
</div>
)}
</div>
</div>
);
@@ -79,61 +109,60 @@ export function TraceExplorer({ traceId, onClose }: { traceId: string; onClose?:
[graph, selectedId],
);
if (query.isError && !graph) {
return <div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800">The assurance trace could not be loaded.</div>;
}
return (
<Card
title="Harness trace explorer"
right={<div className="flex items-center gap-2">
<a href={`/reports/h6?run=${encodeURIComponent(traceId)}`} className="rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-xs font-semibold text-indigo-700 transition hover:bg-indigo-100">Open H6</a>
<a href={runReportExportUrl(traceId, 'html')} className="rounded-lg bg-slate-900 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-slate-700">Export evidence</a>
{onClose && <button type="button" onClick={onClose} className="rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-50">Close</button>}
</div>}
className="overflow-hidden"
>
<div className="mb-5 flex flex-wrap items-end justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.13em] text-slate-400">Trace ID</p>
<p className="mt-1 break-all font-mono text-sm text-slate-700">{traceId}</p>
<ReportPanel
eyebrow="Run assurance"
title="Governed execution dossier"
description="A control-by-control reconstruction of the run, backed by sanitized lifecycle evidence."
right={(
<div className="flex flex-wrap gap-2">
<a href={`/reports/h6?run=${encodeURIComponent(traceId)}`} className="inline-flex min-h-11 items-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 transition hover:-translate-y-px hover:bg-slate-50">Open H6</a>
<a href={runReportExportUrl(traceId, 'json')} className="inline-flex min-h-11 items-center rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 transition hover:-translate-y-px hover:bg-slate-50">JSON</a>
<a href={runReportExportUrl(traceId, 'html')} className="inline-flex min-h-11 items-center rounded-xl bg-slate-950 px-4 py-2 text-xs font-bold text-white transition hover:-translate-y-px hover:bg-slate-800">Export dossier</a>
{onClose && <button type="button" onClick={onClose} className="inline-flex min-h-11 items-center rounded-xl border border-slate-300 px-4 py-2 text-xs font-bold text-slate-600 transition hover:bg-slate-50">Close</button>}
</div>
<div className="flex items-center gap-2 text-xs text-slate-500">
<span className={`h-2 w-2 rounded-full ${graph?.terminal ? 'bg-emerald-500' : 'animate-pulse bg-blue-500'}`} />
{graph?.terminal ? 'Completed' : 'Live · waiting for events'}
<span className="text-slate-300">·</span>
{graph?.progress ?? 0}/7 gates
)}
className="scroll-mt-24"
>
<div className="overflow-hidden rounded-2xl border border-slate-800 bg-slate-950 px-5 py-5 text-white sm:px-6">
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-slate-500">Trace identity</p>
<p className="mt-2 break-all font-mono text-sm text-slate-200">{traceId}</p>
<div className="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-xs text-slate-400">
<span><strong className="text-white">{graph?.progress ?? 0}/7</strong> controls observed</span>
<span><strong className="text-white">{graph?.events.length ?? 0}</strong> evidence events</span>
<span>Updated <strong className="text-white">{displayTime(graph?.updated_at ?? null)}</strong></span>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`h-2.5 w-2.5 rounded-full ${graph?.terminal ? 'bg-emerald-400' : 'animate-pulse bg-cyan-400'}`} />
<span className="text-xs font-bold uppercase tracking-[0.14em] text-slate-300">{graph?.terminal ? 'Evidence sealed' : 'Live evidence stream'}</span>
</div>
</div>
</div>
{query.isLoading && !graph ? <div className="py-10 text-center text-sm text-slate-500">Loading trace…</div> : (
{query.isLoading && !graph ? (
<div className="py-16 text-center text-sm text-slate-500">Reconstructing assurance evidence…</div>
) : graph ? (
<>
<div className="overflow-x-auto pb-5">
<div className="flex min-w-max items-center px-1 py-3">
{graph?.nodes.map((node, index) => {
const next = graph.nodes[index + 1];
const edgeActive = node.status === 'pass' || node.status === 'running' || node.status === 'warning' || next?.status === 'running';
return (
<div key={node.id} className="flex items-center">
<button
type="button"
aria-pressed={selectedId === node.id}
onClick={() => setSelectedId(node.id)}
className={`w-40 rounded-xl border p-3 text-left transition duration-200 hover:-translate-y-0.5 hover:shadow-md ${NODE_TONE[node.status]} ${selectedId === node.id ? 'ring-2 ring-slate-900/10' : ''}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-bold tracking-wide">{node.title.split(' · ')[0]}</span>
<span className={`flex h-6 w-6 items-center justify-center rounded-full border border-current text-xs ${node.status === 'running' ? 'animate-pulse' : ''}`}>{STATUS_MARK[node.status]}</span>
</div>
<div className="mt-3 text-sm font-semibold">{node.title.split(' · ')[1]}</div>
<div className="mt-1 truncate text-[11px] opacity-70">{node.reason}</div>
</button>
{next && <div className={`trace-edge mx-2 ${edgeActive ? 'trace-edge-active' : ''}`} aria-hidden="true" />}
</div>
);
})}
<div className="mt-7">
<div className="mb-3 flex items-center justify-between gap-4">
<p className="text-[10px] font-black uppercase tracking-[0.17em] text-slate-500">Evidence spine · select a control</p>
<span className="text-xs text-slate-400">H1 context → H7 orchestration</span>
</div>
<AssuranceRail graph={graph} selectedId={selected?.id} onSelect={setSelectedId} />
</div>
{selected && <Evidence node={selected} />}
{!graph?.found && <div className="mt-4 rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">This is a legacy or not-yet-started trace. New chat turns emit detailed H1–H7 events automatically.</div>}
<div className="mt-7 border-t border-slate-100 pt-7">
{selected && <GateDossier node={selected} />}
</div>
{!graph.found && <div className="mt-5 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">This is a legacy or not-yet-started trace. Detailed H1–H7 evidence becomes available as new lifecycle events arrive.</div>}
</>
)}
</Card>
) : null}
</ReportPanel>
);
}
@@ -1,17 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api, h6ReportExportUrl, type H6Breakdown, type H6ReportQuery, type HarnessReportVerdict } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
import {
CoverageBar,
ReportHero,
ReportPanel,
SignalMetric,
reportActionPrimary,
reportActionSecondary,
reportTone,
} from '../components/report/ReportPrimitives';
import { StatusBadge } from '../components/ui/Card';
import { api, h6ReportExportUrl, type H6Breakdown, type H6ReportQuery } from '../lib/api';
const money = (value: number): string => `$${value.toFixed(6)}`;
const integer = (value: number): string => new Intl.NumberFormat('en-US').format(value);
const verdictTone: Record<HarnessReportVerdict, string> = {
pass: 'border-emerald-300 bg-emerald-50 text-emerald-900',
attention: 'border-amber-300 bg-amber-50 text-amber-950',
fail: 'border-rose-300 bg-rose-50 text-rose-950',
no_data: 'border-slate-300 bg-slate-50 text-slate-800',
};
function queryFromSearch(search: URLSearchParams): H6ReportQuery {
return {
@@ -24,31 +27,62 @@ function queryFromSearch(search: URLSearchParams): H6ReportQuery {
}
function FilterField({ label, children }: { label: string; children: ReactNode }) {
return <label className="block"><span className="mb-1.5 block text-[10px] font-bold uppercase tracking-[0.13em] text-slate-500">{label}</span>{children}</label>;
}
function MetricCard({ label, value, sub, accent = false }: { label: string; value: string; sub?: string; accent?: boolean }) {
return <div className={`relative overflow-hidden rounded-2xl border p-4 ${accent ? 'border-indigo-200 bg-indigo-50/70' : 'border-slate-200 bg-white'}`}>
{accent && <span className="absolute -right-5 -top-5 h-16 w-16 rounded-full bg-indigo-200/50 blur-xl" />}
<div className="relative text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">{label}</div>
<div className={`relative mt-2 font-semibold tracking-tight text-slate-900 ${value.length > 9 ? 'text-base' : 'text-2xl'}`}>{value}</div>
{sub && <div className="relative mt-1 text-xs text-slate-500">{sub}</div>}
</div>;
return (
<label className="block">
<span className="mb-2 block text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">{label}</span>
{children}
</label>
);
}
function BreakdownTable({ rows, subject }: { rows: H6Breakdown[]; subject: string }) {
return <div className="overflow-x-auto"><table className="w-full min-w-[680px] text-sm">
<thead><tr className="border-b border-slate-200 text-left text-[10px] font-bold uppercase tracking-[0.1em] text-slate-400">
<th className="py-2.5 pr-3">{subject}</th><th>runs</th><th>failures</th><th>avg latency</th><th>tokens</th><th>cost</th>
</tr></thead>
<tbody>{rows.map((row) => <tr key={row.key} className="border-b border-slate-100 transition-colors hover:bg-slate-50/80">
<td className="max-w-[300px] py-3 pr-3 font-medium text-slate-800">{row.key}</td><td>{integer(row.runs)}</td><td>{integer(row.failures)}</td><td>{integer(row.latency_avg_ms)} ms</td><td>{row.tokens === null ? 'Unavailable' : integer(row.tokens)}</td><td>{row.cost_usd === null ? 'Unavailable' : money(row.cost_usd)}</td>
</tr>)}{rows.length === 0 && <tr><td colSpan={6} className="py-8 text-center text-slate-500">No matching {subject.toLowerCase()} records.</td></tr>}</tbody>
</table></div>;
return (
<div className="-mx-5 overflow-x-auto sm:-mx-6">
<table className="w-full min-w-[760px] text-sm">
<thead>
<tr className="border-y border-slate-100 bg-slate-50 text-left text-[10px] font-black uppercase tracking-[0.12em] text-slate-500">
<th className="px-5 py-3 sm:pl-6">{subject}</th>
<th className="px-3 py-3 text-right">Runs</th>
<th className="px-3 py-3 text-right">Failures</th>
<th className="px-3 py-3 text-right">Avg latency</th>
<th className="px-3 py-3 text-right">Tokens</th>
<th className="px-5 py-3 text-right sm:pr-6">Cost</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.key} className="border-b border-slate-100 transition-colors last:border-0 hover:bg-cyan-50/30">
<td className="max-w-[320px] px-5 py-4 font-semibold text-slate-900 sm:pl-6">{row.key}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{integer(row.runs)}</td>
<td className={`px-3 py-4 text-right font-mono text-xs ${row.failures > 0 ? 'font-semibold text-rose-700' : 'text-slate-600'}`}>{integer(row.failures)}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{integer(row.latency_avg_ms)} ms</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{row.tokens === null ? <span className="font-sans text-amber-700">Unavailable</span> : integer(row.tokens)}</td>
<td className="px-5 py-4 text-right font-mono text-xs text-slate-600 sm:pr-6">{row.cost_usd === null ? <span className="font-sans text-amber-700">Unavailable</span> : money(row.cost_usd)}</td>
</tr>
))}
{rows.length === 0 && (
<tr>
<td colSpan={6} className="px-6 py-14 text-center">
<div className="mx-auto flex h-10 w-10 items-center justify-center rounded-full border border-slate-200 text-slate-400">∅</div>
<p className="mt-3 text-sm text-slate-500">No matching {subject.toLowerCase()} records.</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function LoadingReport() {
return <div className="space-y-4" aria-label="Loading H6 report"><div className="h-44 animate-pulse rounded-3xl bg-slate-200" /><div className="grid grid-cols-2 gap-3 lg:grid-cols-4">{Array.from({ length: 8 }, (_, index) => <div key={index} className="h-28 animate-pulse rounded-2xl bg-slate-200" />)}</div></div>;
return (
<div className="space-y-5" aria-label="Loading H6 report">
<div className="h-72 animate-pulse rounded-[1.75rem] bg-slate-200" />
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
{Array.from({ length: 4 }, (_, index) => <div key={index} className="h-36 animate-pulse rounded-2xl bg-slate-200" />)}
</div>
</div>
);
}
export function H6ReportPage() {
@@ -60,7 +94,11 @@ export function H6ReportPage() {
to: searchParams.get('to') ?? '',
run: searchParams.get('run') ?? '',
}));
const report = useQuery({ queryKey: ['reports', 'h6', query], queryFn: () => api.h6Report(query), retry: false });
const report = useQuery({
queryKey: ['reports', 'h6', query],
queryFn: () => api.h6Report(query),
retry: false,
});
const applyFilters = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -77,80 +115,201 @@ export function H6ReportPage() {
setSearchParams({});
};
const inputClass = 'w-full rounded-xl border border-slate-300 bg-white px-3 py-2.5 text-sm text-slate-800 outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100';
const inputClass = 'min-h-11 w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 hover:border-slate-400 focus:border-cyan-500 focus:ring-4 focus:ring-cyan-100';
const data = report.data;
return <div className="space-y-5">
<section className="relative overflow-hidden rounded-3xl border border-slate-800 bg-[#111827] px-5 py-6 text-white shadow-[0_22px_55px_rgba(15,23,42,0.16)] sm:px-7">
<div className="absolute right-[-60px] top-[-90px] h-64 w-64 rounded-full bg-indigo-500/25 blur-3xl" />
<div className="absolute bottom-[-120px] left-1/3 h-56 w-56 rounded-full bg-teal-500/10 blur-3xl" />
<div className="relative flex flex-col justify-between gap-5 lg:flex-row lg:items-end">
<div><div className="text-[10px] font-bold uppercase tracking-[0.2em] text-indigo-300">H6 · evidence-backed operations</div><h2 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">AgentOps assurance dossier</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-slate-300">One report surface for runtime health, provider usage, cost provenance, failures, retries, alerts and source freshness.</p></div>
{report.data && <div className="flex flex-wrap items-center gap-2"><span className={`rounded-full border px-3 py-2 text-xs font-black uppercase tracking-[0.12em] ${verdictTone[report.data.verdict]}`}>{report.data.verdict}</span><span className="rounded-full border border-slate-700 bg-slate-900/70 px-3 py-2 text-xs text-slate-300">{report.data.freshness.status} · {report.data.report_id}</span></div>}
</div>
</section>
return (
<div className="space-y-5">
<ReportHero
eyebrow="H6 · AgentOps assurance"
title="Operational evidence, ready for a decision."
description="A production dossier for runtime health, reliability, cost provenance and telemetry integrity—designed to answer what happened, why it matters and where the evidence lives."
verdict={data?.verdict}
meta={data ? (
<>
<span>{data.report_id}</span>
<span>Generated {data.generated_at.replace('T', ' ').replace('Z', ' UTC')}</span>
<span>Freshness {data.freshness.status}</span>
</>
) : <span>Generating from canonical evidence…</span>}
actions={data ? (
<>
<a href={h6ReportExportUrl(query, 'json')} className={reportActionSecondary}>JSON evidence</a>
<a href={h6ReportExportUrl(query, 'html')} className={reportActionPrimary}>Export dossier</a>
</>
) : undefined}
/>
<Card title="Report scope" right={<span className="text-xs font-normal normal-case tracking-normal text-slate-400">Filters apply to API and both exports</span>}>
<form onSubmit={applyFilters} className="grid gap-3 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1fr_1.4fr_auto] xl:items-end">
<FilterField label="Project"><select value={draft.project} onChange={(event) => setDraft((current) => ({ ...current, project: event.target.value }))} className={inputClass}><option value="">All projects</option>{report.data?.available_filters.projects.map((project) => <option key={project} value={project}>{project}</option>)}</select></FilterField>
<FilterField label="From"><input type="date" value={draft.from} onChange={(event) => setDraft((current) => ({ ...current, from: event.target.value }))} className={inputClass} /></FilterField>
<FilterField label="To"><input type="date" value={draft.to} onChange={(event) => setDraft((current) => ({ ...current, to: event.target.value }))} className={inputClass} /></FilterField>
<FilterField label="Run / trace"><input list="h6-run-options" value={draft.run} onChange={(event) => setDraft((current) => ({ ...current, run: event.target.value }))} placeholder="All runs" className={inputClass} /><datalist id="h6-run-options">{report.data?.available_filters.runs.map((run) => <option key={run} value={run} />)}</datalist></FilterField>
<div className="flex gap-2"><button type="submit" className="rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">Apply</button><button type="button" onClick={clearFilters} className="rounded-xl border border-slate-300 bg-white px-3 py-2.5 text-sm font-semibold text-slate-600 transition hover:bg-slate-50">Clear</button></div>
</form>
</Card>
{report.isLoading && <LoadingReport />}
{report.isError && <div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800"><div className="font-semibold">The H6 report could not be generated.</div><p className="mt-1 text-rose-700">Check the selected time range and confirm the Control Panel API can read the canonical telemetry sources.</p></div>}
{report.data && <>
<section className="grid grid-cols-2 gap-3 lg:grid-cols-4 xl:grid-cols-8">
<MetricCard label="Runs" value={integer(report.data.summary.runs)} sub={`${report.data.summary.success} successful`} accent />
<MetricCard label="Failures" value={integer(report.data.summary.failed)} sub={`${report.data.summary.failure_rate_pct}% rate`} />
<MetricCard label="Degraded" value={integer(report.data.summary.degraded)} />
<MetricCard label="P95 latency" value={`${integer(report.data.summary.latency_ms.p95)}ms`} sub={`P50 ${integer(report.data.summary.latency_ms.p50)}ms`} />
<MetricCard
label="Provider tokens"
value={report.data.summary.coverage.token_records > 0 ? integer(report.data.summary.tokens.provider_total ?? report.data.summary.tokens.total ?? 0) : 'Unavailable'}
sub={`${report.data.summary.coverage.token_pct}% run coverage`}
accent
/>
<MetricCard
label="Actual cost"
value={report.data.summary.cost_usd.provider_actual !== null ? money(report.data.summary.cost_usd.provider_actual) : 'Unavailable'}
sub={`${report.data.summary.provider_calls} provider calls`}
/>
<MetricCard
label="Cost coverage"
value={`${report.data.summary.coverage.cost_pct}%`}
sub={`${report.data.summary.coverage.cost_records}/${report.data.summary.coverage.runtime_records} runs`}
/>
<MetricCard label="Alerts" value={integer(report.data.summary.alerts)} sub={`${report.data.summary.retries} retries`} />
</section>
<div className="grid gap-5 xl:grid-cols-[1.15fr_0.85fr]">
<Card title="Verdict findings" right={<StatusBadge value={report.data.verdict} />}>
<div className="space-y-2.5">{report.data.findings.map((finding) => <div key={finding.code} className={`rounded-xl border-l-4 p-3.5 ${finding.severity === 'critical' ? 'border-rose-500 bg-rose-50' : finding.severity === 'warning' ? 'border-amber-500 bg-amber-50' : 'border-indigo-500 bg-indigo-50'}`}><div className="flex flex-wrap items-center justify-between gap-2"><span className="font-mono text-xs font-bold text-slate-800">{finding.code}</span>{finding.metric && <span className="text-[11px] text-slate-500">{finding.metric}: {String(finding.value)}{finding.threshold !== undefined ? ` · threshold ${String(finding.threshold)}` : ''}</span>}</div><p className="mt-1 text-sm leading-5 text-slate-600">{finding.message}</p></div>)}{report.data.findings.length === 0 && <div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">No threshold breach was detected in this scope.</div>}</div>
</Card>
<Card title="Evidence freshness" right={<StatusBadge value={report.data.freshness.status} />}>
<div className="space-y-3">{report.data.evidence_sources.map((source) => <div key={source.source} className="rounded-xl border border-slate-200 bg-slate-50/70 p-3"><div className="flex items-center justify-between gap-3"><span className="text-sm font-semibold capitalize text-slate-800">{source.source}</span><StatusBadge value={!source.present ? 'missing' : source.stale ? 'stale' : 'fresh'} /></div><div className="mt-2 flex justify-between text-xs text-slate-500"><span>{source.records} records</span><span>{source.age_s === null ? 'no timestamp' : `${integer(source.age_s)}s old`}</span></div><div className="mt-2 break-all font-mono text-[10px] leading-4 text-slate-400">{source.path}</div></div>)}</div>
</Card>
</div>
<div className="grid gap-5 xl:grid-cols-2"><Card title="Runtime by step"><BreakdownTable rows={report.data.details.by_step} subject="Step" /></Card><Card title="Provider usage"><BreakdownTable rows={report.data.details.by_provider} subject="Provider · model" /></Card></div>
<div className="grid gap-5 xl:grid-cols-[0.8fr_1.2fr]">
<Card title={`Data quality · ${report.data.data_quality.status}`}>
<div className="mb-4 grid grid-cols-2 gap-3">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Token coverage</div><div className="mt-1 text-xl font-semibold text-slate-900">{report.data.summary.coverage.token_pct}%</div></div>
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wider text-slate-400">Cost coverage</div><div className="mt-1 text-xl font-semibold text-slate-900">{report.data.summary.coverage.cost_pct}%</div></div>
<ReportPanel
eyebrow="Scope"
title="Evidence boundary"
description="Filters are applied server-side to the screen, JSON contract and print-ready dossier."
right={Object.values(draft).some(Boolean) ? <span className="rounded-full bg-cyan-50 px-3 py-1 text-[10px] font-black uppercase tracking-[0.12em] text-cyan-700">Filtered view</span> : <span className="text-xs text-slate-400">All available evidence</span>}
>
<form onSubmit={applyFilters} className="grid gap-4 md:grid-cols-2 xl:grid-cols-[1fr_1fr_1fr_1.45fr_auto] xl:items-end">
<FilterField label="Project">
<select value={draft.project} onChange={(event) => setDraft((current) => ({ ...current, project: event.target.value }))} className={inputClass}>
<option value="">All projects</option>
{data?.available_filters.projects.map((project) => <option key={project} value={project}>{project}</option>)}
</select>
</FilterField>
<FilterField label="From">
<input type="date" value={draft.from} onChange={(event) => setDraft((current) => ({ ...current, from: event.target.value }))} className={inputClass} />
</FilterField>
<FilterField label="To">
<input type="date" value={draft.to} onChange={(event) => setDraft((current) => ({ ...current, to: event.target.value }))} className={inputClass} />
</FilterField>
<FilterField label="Run / trace">
<input list="h6-run-options" value={draft.run} onChange={(event) => setDraft((current) => ({ ...current, run: event.target.value }))} placeholder="All governed runs" className={inputClass} />
<datalist id="h6-run-options">{data?.available_filters.runs.map((run) => <option key={run} value={run} />)}</datalist>
</FilterField>
<div className="flex gap-2">
<button type="submit" className="min-h-11 rounded-xl bg-slate-950 px-5 py-2.5 text-sm font-bold text-white transition hover:-translate-y-px hover:bg-slate-800 focus:outline-none focus:ring-4 focus:ring-cyan-100">Apply</button>
<button type="button" onClick={clearFilters} className="min-h-11 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-bold text-slate-600 transition hover:bg-slate-50">Clear</button>
</div>
{report.data.data_quality.warnings.length > 0 ? <ul className="space-y-2 text-sm leading-5 text-slate-600">{report.data.data_quality.warnings.map((warning) => <li key={warning} className="flex gap-2"><span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-amber-400" />{warning}</li>)}</ul> : <div className="text-sm text-emerald-700">All required sources are present and no estimation warning was detected.</div>}
</Card>
<Card title="Independent export" right={<span className="text-xs font-normal normal-case tracking-normal text-slate-400">Generated on demand · no hard-coded score</span>}>
<p className="text-sm leading-6 text-slate-600">Both exports use the same contract, filters, verdict rules and canonical sources as this screen. JSON is machine-auditable; HTML is print-ready for team review.</p>
<div className="mt-4 flex flex-wrap gap-3"><a href={h6ReportExportUrl(query, 'json')} className="rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-700">Download JSON</a><a href={h6ReportExportUrl(query, 'html')} className="rounded-xl border border-indigo-300 bg-indigo-50 px-4 py-2.5 text-sm font-semibold text-indigo-700 transition hover:bg-indigo-100">Download HTML</a></div>
</Card>
</div>
</>}
</div>;
</form>
</ReportPanel>
{report.isLoading && <LoadingReport />}
{report.isError && (
<div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800">
<div className="font-semibold">The H6 dossier could not be generated.</div>
<p className="mt-1 text-rose-700">Check the selected range and confirm that the Control Plane can read the canonical telemetry sources.</p>
</div>
)}
{data && (
<>
<section aria-label="Executive signals" className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<SignalMetric label="Governed runs" value={integer(data.summary.runs)} detail={`${data.summary.success} successful · ${data.summary.degraded} degraded`} tone="info" primary />
<SignalMetric label="Reliability" value={`${data.summary.failure_rate_pct}%`} detail={`${data.summary.failed} failed · ${data.summary.alerts} alerts`} tone={data.summary.failed > 0 ? 'danger' : 'success'} primary />
<SignalMetric label="P95 latency" value={`${integer(data.summary.latency_ms.p95)} ms`} detail={`P50 ${integer(data.summary.latency_ms.p50)} ms`} tone={data.findings.some((finding) => finding.code.includes('LATENCY')) ? 'warning' : 'neutral'} primary />
<SignalMetric label="Telemetry integrity" value={data.data_quality.status.replaceAll('_', ' ')} detail={`${data.summary.coverage.token_pct}% token · ${data.summary.coverage.cost_pct}% cost coverage`} tone={reportTone(data.data_quality.status)} primary />
</section>
<section className="grid gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-[0_12px_32px_rgba(15,23,42,0.04)] sm:grid-cols-2 lg:grid-cols-4">
<div className="border-b border-slate-100 p-2 sm:border-b-0 sm:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Provider tokens</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{data.summary.coverage.token_records > 0 ? integer(data.summary.tokens.provider_total ?? data.summary.tokens.total ?? 0) : 'Unavailable'}</p>
</div>
<div className="border-b border-slate-100 p-2 sm:border-b-0 lg:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Actual cost</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{data.summary.cost_usd.provider_actual !== null ? money(data.summary.cost_usd.provider_actual) : 'Unavailable'}</p>
</div>
<div className="border-b border-slate-100 p-2 sm:border-b-0 sm:border-r">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Provider calls</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{integer(data.summary.provider_calls)}</p>
</div>
<div className="p-2">
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">Retries</p>
<p className="mt-2 font-mono text-lg font-semibold text-slate-900">{integer(data.summary.retries)}</p>
</div>
</section>
<div className="grid gap-5 xl:grid-cols-[1.18fr_0.82fr]">
<ReportPanel
eyebrow="Decision brief"
title="What requires attention"
description="Threshold findings are ordered as review signals, not maturity claims."
right={<StatusBadge value={data.verdict} />}
>
<div className="space-y-3">
{data.findings.map((finding, index) => {
const tone = finding.severity === 'critical' ? 'border-rose-500 bg-rose-50/60' : finding.severity === 'warning' ? 'border-amber-500 bg-amber-50/60' : 'border-cyan-500 bg-cyan-50/60';
return (
<article key={finding.code} className={`grid gap-3 rounded-xl border-l-4 p-4 sm:grid-cols-[2rem_minmax(0,1fr)] ${tone}`}>
<div className="font-mono text-lg font-semibold text-slate-400">{String(index + 1).padStart(2, '0')}</div>
<div>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-mono text-xs font-bold text-slate-900">{finding.code}</span>
{finding.metric && <span className="font-mono text-[10px] text-slate-500">{finding.metric}: {String(finding.value)}{finding.threshold !== undefined ? ` / ${String(finding.threshold)}` : ''}</span>}
</div>
<p className="mt-2 text-sm leading-6 text-slate-700">{finding.message}</p>
</div>
</article>
);
})}
{data.findings.length === 0 && (
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-5 text-sm text-emerald-800">No threshold breach was detected in the selected evidence boundary.</div>
)}
</div>
</ReportPanel>
<ReportPanel
eyebrow="Source integrity"
title="Evidence provenance"
description="Freshness is evaluated independently for every canonical source."
right={<StatusBadge value={data.freshness.status} />}
>
<div className="space-y-3">
{data.evidence_sources.map((source) => {
const state = !source.present ? 'missing' : source.stale ? 'stale' : 'fresh';
return (
<article key={source.source} className="rounded-xl border border-slate-200 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-sm font-semibold capitalize text-slate-900">{source.source}</h3>
<p className="mt-1 font-mono text-[10px] text-slate-400">{source.records} records · {source.age_s === null ? 'no timestamp' : `${integer(source.age_s)}s old`}</p>
</div>
<StatusBadge value={state} />
</div>
<p className="mt-3 break-all border-t border-slate-100 pt-3 font-mono text-[10px] leading-4 text-slate-400">{source.path}</p>
</article>
);
})}
</div>
</ReportPanel>
</div>
<div className="grid gap-5 xl:grid-cols-2">
<ReportPanel eyebrow="Runtime" title="Execution profile by step" description="Latency, failure and attribution by governed lifecycle step.">
<BreakdownTable rows={data.details.by_step} subject="Step" />
</ReportPanel>
<ReportPanel eyebrow="Provider" title="Model usage and provenance" description="Only provider-reported token and actual cost values are shown.">
<BreakdownTable rows={data.details.by_provider} subject="Provider · model" />
</ReportPanel>
</div>
<ReportPanel
eyebrow="Evidence quality"
title="Coverage and disclosure"
description="Unavailable telemetry remains null. Coverage gaps are surfaced as review findings."
right={<StatusBadge value={data.data_quality.status} />}
>
<div className="grid gap-7 xl:grid-cols-[0.75fr_1.25fr]">
<div className="space-y-6">
<CoverageBar label="Token attribution" value={data.summary.coverage.token_pct} detail={`${data.summary.coverage.token_records}/${data.summary.coverage.runtime_records} runtime records`} />
<CoverageBar label="Cost attribution" value={data.summary.coverage.cost_pct} detail={`${data.summary.coverage.cost_records}/${data.summary.coverage.runtime_records} runtime records`} />
</div>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-5">
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-500">Disclosure notes</p>
{data.data_quality.warnings.length > 0 ? (
<ul className="mt-4 space-y-3">
{data.data_quality.warnings.map((warning) => (
<li key={warning} className="grid grid-cols-[0.55rem_minmax(0,1fr)] gap-3 text-sm leading-6 text-slate-700">
<span className="mt-2 h-2 w-2 rounded-full bg-amber-500" />
<span>{warning}</span>
</li>
))}
</ul>
) : <p className="mt-4 text-sm text-emerald-700">All required sources are present and no estimation warning was detected.</p>}
</div>
</div>
</ReportPanel>
<div className="rounded-2xl border border-slate-800 bg-slate-950 p-5 text-white shadow-[0_18px_48px_rgba(15,23,42,0.16)] sm:flex sm:items-center sm:justify-between sm:gap-6 sm:p-6">
<div>
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-cyan-300">Independent evidence package</p>
<h2 className="mt-2 text-lg font-semibold">Same contract. Two review formats.</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-400">JSON is machine-auditable. HTML is a self-contained, print-ready assurance dossier generated only when requested.</p>
</div>
<div className="mt-5 flex shrink-0 flex-wrap gap-2 sm:mt-0">
<a href={h6ReportExportUrl(query, 'json')} className={reportActionSecondary}>Download JSON</a>
<a href={h6ReportExportUrl(query, 'html')} className={reportActionPrimary}>Download HTML</a>
</div>
</div>
</>
)}
</div>
);
}
@@ -1,53 +1,114 @@
import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
import { api, type HarnessRunRecord } from '../lib/api';
import { Card, StatusBadge } from '../components/ui/Card';
import { Link, useSearchParams } from 'react-router-dom';
import { ReportHero, ReportPanel, SignalMetric, reportActionPrimary } from '../components/report/ReportPrimitives';
import { TraceExplorer } from '../components/trace/TraceExplorer';
import { StatusBadge } from '../components/ui/Card';
import { api, type HarnessRunRecord } from '../lib/api';
const integer = (value: number): string => new Intl.NumberFormat('en-US').format(value);
export function Runs() {
const { data, isLoading } = useQuery({ queryKey: ['runs'], queryFn: () => api.runs(100) });
const { data, isLoading, isError } = useQuery({ queryKey: ['runs'], queryFn: () => api.runs(100) });
const [searchParams, setSearchParams] = useSearchParams();
const selectedTrace = searchParams.get('trace') ?? '';
const runs = data?.runs ?? [];
const failures = runs.filter((run) => run.status === 'failed').length;
const attributedCost = runs.filter((run) => run.cost_estimate != null).length;
const latencyValues = runs.map((run) => Number(run.latency_ms)).filter((value) => Number.isFinite(value) && value > 0);
const averageLatency = latencyValues.length > 0
? Math.round(latencyValues.reduce((sum, value) => sum + value, 0) / latencyValues.length)
: null;
if (isLoading || !data) return <div className="text-slate-500">Loading…</div>;
return (
<div className="space-y-5">
<ReportHero
eyebrow="Run assurance registry"
title="Every governed run, one evidence trail."
description="Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent assurance dossier."
verdict={failures > 0 ? 'attention' : data ? 'operational' : undefined}
meta={data ? (
<>
<span>{data.count} records in current registry</span>
<span>{failures} failures</span>
<span>{attributedCost}/{runs.length} cost-attributed</span>
</>
) : <span>Loading governed run registry…</span>}
actions={<Link to="/reports/h6" className={reportActionPrimary}>Open H6 dossier</Link>}
/>
{selectedTrace && <TraceExplorer traceId={selectedTrace} onClose={() => setSearchParams({})} />}
<Card
title={`Recent runs (${data.count})`}
right={<span className="text-xs font-normal normal-case tracking-normal text-slate-400">Select a run to inspect H1–H7</span>}
>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead><tr className="border-b border-slate-200 text-left text-slate-500">
<th className="py-2">time</th><th>step</th><th>status</th><th>latency</th><th>tokens</th><th>cost</th><th className="text-right">trace</th>
</tr></thead>
<tbody>
{data.runs.map((run: HarnessRunRecord, index: number) => (
<tr key={`${run.trace_id ?? 'run'}-${index}`} className="border-b border-slate-100 transition-colors hover:bg-slate-50/80">
<td className="py-2 text-slate-500">{run.timestamp?.replace('T', ' ').replace('Z', '')}</td>
<td className="text-slate-700">{run.step ?? run.harness}</td>
<td><StatusBadge value={run.status ?? '—'} /></td>
<td>{run.latency_ms ?? '—'} ms</td>
<td>{run.total_tokens ?? '—'}</td>
<td>{run.cost_estimate == null ? <span className="font-medium text-amber-600">Unavailable</span> : `$${Number(run.cost_estimate).toFixed(5)}`}</td>
<td className="py-2 text-right">
{run.trace_id ? (
<button
type="button"
onClick={() => setSearchParams({ trace: run.trace_id ?? '' })}
className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-semibold text-blue-700 transition-colors hover:bg-blue-100"
>
Open H1–H7
</button>
) : <span className="text-slate-300">—</span>}
</td>
</tr>
))}
</tbody>
</table>
{isLoading && (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{Array.from({ length: 4 }, (_, index) => <div key={index} className="h-32 animate-pulse rounded-2xl bg-slate-200" />)}
</div>
</Card>
)}
{isError && <div role="alert" className="rounded-2xl border border-rose-200 bg-rose-50 p-5 text-sm text-rose-800">The governed run registry could not be loaded.</div>}
{data && (
<>
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<SignalMetric label="Governed records" value={integer(data.count)} detail="Latest 100 lifecycle records" tone="info" />
<SignalMetric label="Failures" value={integer(failures)} detail={`${data.count ? Math.round((failures / data.count) * 100) : 0}% observed failure rate`} tone={failures > 0 ? 'danger' : 'success'} />
<SignalMetric label="Average latency" value={averageLatency === null ? 'Unavailable' : `${integer(averageLatency)} ms`} detail={`${latencyValues.length}/${runs.length} records measured`} tone="neutral" />
<SignalMetric label="Cost attribution" value={`${runs.length ? Math.round((attributedCost / runs.length) * 100) : 0}%`} detail={`${attributedCost}/${runs.length} records`} tone={attributedCost === runs.length && runs.length > 0 ? 'success' : 'warning'} />
</section>
<ReportPanel
eyebrow="Evidence index"
title="Recent governed runs"
description="Operational values are shown only when present in canonical telemetry. Select a trace to open the evidence spine."
right={<span className="text-xs text-slate-400">{data.count} total records</span>}
>
<div className="-mx-5 overflow-x-auto sm:-mx-6">
<table className="w-full min-w-[900px] text-sm">
<thead>
<tr className="border-y border-slate-100 bg-slate-50 text-left text-[10px] font-black uppercase tracking-[0.12em] text-slate-500">
<th className="px-5 py-3 sm:pl-6">Observed at</th>
<th className="px-3 py-3">Lifecycle step</th>
<th className="px-3 py-3">Verdict</th>
<th className="px-3 py-3 text-right">Latency</th>
<th className="px-3 py-3 text-right">Tokens</th>
<th className="px-3 py-3 text-right">Cost</th>
<th className="px-5 py-3 text-right sm:pr-6">Evidence</th>
</tr>
</thead>
<tbody>
{runs.map((run: HarnessRunRecord, index: number) => (
<tr key={`${run.trace_id ?? 'run'}-${index}`} className="border-b border-slate-100 transition-colors last:border-0 hover:bg-cyan-50/30">
<td className="whitespace-nowrap px-5 py-4 font-mono text-[11px] text-slate-500 sm:pl-6">{run.timestamp?.replace('T', ' ').replace('Z', ' UTC') ?? 'Unavailable'}</td>
<td className="px-3 py-4 font-semibold text-slate-900">{run.step ?? run.harness}</td>
<td className="px-3 py-4"><StatusBadge value={run.status ?? 'unknown'} /></td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{run.latency_ms == null ? 'Unavailable' : `${integer(Number(run.latency_ms))} ms`}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{run.total_tokens ?? 'Unavailable'}</td>
<td className="px-3 py-4 text-right font-mono text-xs text-slate-600">{run.cost_estimate == null ? <span className="font-sans text-amber-700">Unavailable</span> : `$${Number(run.cost_estimate).toFixed(5)}`}</td>
<td className="px-5 py-4 text-right sm:pr-6">
{run.trace_id ? (
<button
type="button"
onClick={() => {
setSearchParams({ trace: run.trace_id ?? '' });
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className="min-h-11 rounded-xl border border-slate-300 bg-white px-4 py-2 text-xs font-bold text-slate-700 transition hover:-translate-y-px hover:border-cyan-400 hover:text-cyan-800 focus:outline-none focus:ring-4 focus:ring-cyan-100"
>
Inspect H1–H7
</button>
) : <span className="text-slate-300">—</span>}
</td>
</tr>
))}
{runs.length === 0 && (
<tr>
<td colSpan={7} className="px-6 py-16 text-center text-sm text-slate-500">No governed run has been recorded yet.</td>
</tr>
)}
</tbody>
</table>
</div>
</ReportPanel>
</>
)}
</div>
);
}
@@ -45,7 +45,7 @@ assert {path.name for path in (root / "packages").iterdir()} == {
"casan-harness", "casan-devkit",
}
assert {path.name for path in (root / "packages/casan-harness").iterdir()} == {
"adapters", "agentops", "config", "governance", "memory", "prompts",
"adapters", "agentops", "assets", "config", "governance", "memory", "prompts",
"schemas", "scripts", "security", "templates",
}
assert {path.name for path in (root / "packages/casan-devkit").iterdir()} == {
@@ -70,10 +70,16 @@ for forbidden in (
):
assert not (root / forbidden).exists(), forbidden
for required in (
"packages/casan-harness/assets/local-viewer/index.html",
"packages/casan-harness/assets/local-viewer/app.css",
"packages/casan-harness/assets/local-viewer/app.js",
"packages/casan-harness/config/project-registry.json",
"packages/casan-harness/config/tool-registry.yaml",
"packages/casan-harness/scripts/bash/project-gate.sh",
"packages/casan-harness/scripts/python/agentic_bridge.py",
"packages/casan-harness/scripts/python/local_report.py",
"packages/casan-harness/scripts/python/local_viewer.py",
"packages/casan-harness/scripts/python/report_cli.py",
):
assert (root / required).is_file(), required
registry = json.loads(
@@ -138,8 +144,10 @@ for forbidden in (
):
assert not any(name == forbidden or name.startswith(forbidden + "/") for name in names), forbidden
for required in (
"packages/casan-harness/assets/local-viewer/index.html",
"packages/casan-harness/config/tool-registry.yaml",
"packages/casan-harness/scripts/bash/casan-harness.sh",
"packages/casan-harness/scripts/python/local_viewer.py",
"packaging/runtime-layout.json",
"scripts/copy-runtime.py",
"install.sh",
+15
View File
@@ -5,6 +5,7 @@ Populated incrementally by Plan-01 (Phase 0→6). During migration, files move h
`.specify/` wave-by-wave; the full harness gate must stay green (`PASS=64 FAIL=0`) after each phase.
Layout:
- `assets/local-viewer/` — production static UI for the Core Local Assurance Viewer
- `scripts/` — bash + powershell gate logic (H1→H7), path resolver `casan-paths.sh`
- `security/` — filter/policy rules (prompt-filter, pii-rules, output-policy, ...)
- `governance/`, `agentops/` — H5/H6 code
@@ -12,6 +13,20 @@ Layout:
- `templates/` — spec/plan templates
- `tests/` — source-hub verification only; excluded from production installs
Core visual reporting is intentionally lazy and zero-dependency:
```bash
casan report latest
casan view
casan report export --format html
casan report export --h6 --format html
```
Prompt hooks only write canonical evidence and a small receipt. `casan view`
starts a loopback-only, token-protected, read-only Python server on demand.
No Node/npm, external font/CDN, Platform service or per-prompt HTML generation
is required.
Runtime state (logs, audit chain, tenant state) is NOT part of this package — it stays with
the app under `CASAN_STATE_ROOT`. Domain data (golden-runs, corpus, input) lives in
`apps/okr/domain/` under `CASAN_DOMAIN_ROOT`.
@@ -0,0 +1,271 @@
:root {
--ink: #101828;
--muted: #667085;
--faint: #98a2b3;
--line: #e4e7ec;
--paper: #f6f7f9;
--white: #fff;
--navy: #101828;
--navy-2: #1d2939;
--cyan: #0891b2;
--cyan-soft: #ecfeff;
--green: #067647;
--green-soft: #ecfdf3;
--amber: #b54708;
--amber-soft: #fffaeb;
--rose: #b42318;
--rose-soft: #fef3f2;
--shadow: 0 12px 36px rgba(16, 24, 40, 0.06);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible {
outline: 3px solid rgba(6, 182, 212, 0.28);
outline-offset: 2px;
}
.hidden { display: none !important; }
.skip-link {
position: fixed; z-index: 100; left: 16px; top: -60px; padding: 10px 14px;
border-radius: 10px; background: white; color: var(--ink); font-weight: 700;
}
.skip-link:focus { top: 16px; }
.sidebar {
position: fixed; inset: 0 auto 0 0; z-index: 30; width: 244px; display: flex;
flex-direction: column; border-right: 1px solid #253044; background: var(--navy);
color: white; padding: 22px 16px;
}
.brand { display: flex; align-items: center; gap: 12px; color: white; text-decoration: none; padding: 4px 8px 24px; }
.brand-mark {
display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid #344054;
border-radius: 12px; background: #172033; color: #67e8f9; font: 800 16px ui-monospace, monospace;
}
.brand strong, .brand small { display: block; }
.brand strong { font-size: 14px; letter-spacing: .08em; }
.brand small { margin-top: 3px; color: #98a2b3; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
.sidebar nav { display: grid; gap: 5px; }
.nav-item {
display: grid; grid-template-columns: 30px 1fr; align-items: center; min-height: 46px; width: 100%;
border: 1px solid transparent; border-radius: 12px; background: transparent; color: #98a2b3;
padding: 8px 12px; text-align: left; font-size: 13px; font-weight: 650; cursor: pointer; transition: .18s ease;
}
.nav-item span { color: #667085; font: 700 11px ui-monospace, monospace; }
.nav-item:hover { background: #172033; color: white; }
.nav-item.active { border-color: #344054; background: #1d2939; color: white; }
.nav-item.active span { color: #67e8f9; }
.local-seal {
display: grid; grid-template-columns: 8px 1fr; gap: 10px; align-items: start; margin-top: auto;
border: 1px solid #344054; border-radius: 14px; background: #172033; padding: 13px;
}
.local-seal strong, .local-seal small { display: block; }
.local-seal strong { font-size: 11px; }
.local-seal small { margin-top: 4px; color: #667085; font: 10px ui-monospace, monospace; }
.live-dot { width: 7px; height: 7px; margin-top: 3px; border-radius: 50%; background: #32d583; box-shadow: 0 0 0 4px rgba(50,213,131,.12); }
.shell { margin-left: 244px; min-height: 100vh; }
.topbar {
position: sticky; top: 0; z-index: 20; display: flex; align-items: center; min-height: 76px;
border-bottom: 1px solid var(--line); background: rgba(255,255,255,.92); padding: 12px clamp(20px,4vw,48px);
backdrop-filter: blur(16px);
}
.topbar h1 { margin: 2px 0 0; font-size: 18px; letter-spacing: -.02em; }
.top-eyebrow { margin: 0; color: var(--faint); font-size: 9px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
.top-actions { display: flex; align-items: center; gap: 9px; margin-left: auto; }
.icon-button {
display: grid; place-items: center; min-width: 44px; min-height: 44px; border: 1px solid var(--line);
border-radius: 12px; background: white; color: #475467; cursor: pointer; transition: .18s ease;
}
.icon-button:hover { border-color: #98a2b3; transform: translateY(-1px); }
.menu-button { display: none; margin-right: 12px; }
main { max-width: 1500px; margin: 0 auto; padding: 28px clamp(20px,4vw,48px) 64px; }
.view { display: grid; gap: 18px; }
.hero {
position: relative; overflow: hidden; border-radius: 28px; background: var(--navy); color: white;
padding: clamp(28px,5vw,48px); box-shadow: 0 24px 70px rgba(16,24,40,.16);
}
.hero::after {
content: ""; position: absolute; width: 360px; height: 360px; right: -160px; top: -230px;
border: 1px solid rgba(103,232,249,.15); border-radius: 50%; box-shadow: 0 0 0 60px rgba(103,232,249,.025), 0 0 0 120px rgba(103,232,249,.02);
}
.hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 28px; align-items: end; }
.eyebrow { margin: 0; color: var(--cyan); font-size: 10px; font-weight: 850; letter-spacing: .18em; text-transform: uppercase; }
.hero .eyebrow { color: #67e8f9; }
.hero h2 { max-width: 760px; margin: 13px 0 12px; font-size: clamp(30px,4vw,48px); line-height: 1.02; letter-spacing: -.045em; }
.hero-description { max-width: 760px; margin: 0; color: #98a2b3; font-size: 14px; line-height: 1.7; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 24px; color: #667085; font: 10px ui-monospace, monospace; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: flex-end; }
.trace-hero { border-radius: 18px; padding: 20px 22px; box-shadow: none; }
.trace-title { margin: 8px 0 !important; font-size: 18px !important; }
.trace-meta { margin-top: 8px; }
.rail-spaced { margin-top: 20px; }
.button {
display: inline-flex; align-items: center; justify-content: center; min-height: 44px; border: 1px solid transparent;
border-radius: 12px; padding: 9px 15px; text-decoration: none; font-size: 12px; font-weight: 750; cursor: pointer; transition: .18s ease;
}
.button:hover { transform: translateY(-1px); }
.button.primary { background: #67e8f9; color: var(--navy); }
.button.primary:hover { background: #a5f3fc; }
.button.secondary { border-color: #344054; background: #1d2939; color: #e4e7ec; }
.button.light { border-color: #d0d5dd; background: white; color: #344054; }
.pill {
display: inline-flex; align-items: center; min-height: 28px; border-radius: 999px; padding: 5px 10px;
font-size: 9px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap;
}
.pill.pass, .pill.certified, .pill.success, .pill.fresh, .pill.operational { background: var(--green-soft); color: var(--green); }
.pill.warning, .pill.attention, .pill.partial, .pill.stale, .pill.in-progress, .pill.in_progress { background: var(--amber-soft); color: var(--amber); }
.pill.error, .pill.failed, .pill.fail, .pill.blocked, .pill.non-certified, .pill.non_certified { background: var(--rose-soft); color: var(--rose); }
.pill.neutral, .pill.queued, .pill.unknown, .pill.no-data, .pill.no_data { background: #f2f4f7; color: #475467; }
.hero .pill { border: 1px solid #344054; background: #1d2939; color: #d0d5dd; }
.metric-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; }
.metric-card, .panel {
border: 1px solid var(--line); border-radius: 18px; background: white; box-shadow: var(--shadow);
}
.metric-card { min-height: 136px; padding: 19px; }
.metric-label { color: var(--faint); font-size: 9px; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; }
.metric-value { margin-top: 18px; font-size: 27px; font-weight: 720; letter-spacing: -.04em; }
.metric-detail { margin-top: 7px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.panel { overflow: hidden; }
.panel-head { display: flex; justify-content: space-between; gap: 20px; align-items: start; padding: 23px 24px 18px; }
.panel h2 { margin: 6px 0 0; font-size: 19px; letter-spacing: -.025em; }
.panel-description { max-width: 720px; margin: 7px 0 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
.panel-body { border-top: 1px solid #f2f4f7; padding: 22px 24px 24px; }
.two-col { display: grid; grid-template-columns: minmax(0,1.2fr) minmax(330px,.8fr); gap: 18px; }
.rail-wrap { overflow-x: auto; padding-bottom: 4px; }
.evidence-rail { display: grid; grid-template-columns: repeat(7,minmax(116px,1fr)); min-width: 840px; gap: 7px; }
.gate-button {
position: relative; min-height: 92px; border: 1px solid var(--line); border-radius: 14px; background: #fafafa;
padding: 13px; color: #344054; text-align: left; cursor: pointer; transition: .18s ease;
}
.gate-button::after { content: ""; position: absolute; top: 27px; right: -8px; width: 8px; height: 1px; background: #d0d5dd; }
.gate-button:last-child::after { display: none; }
.gate-button:hover { border-color: #98a2b3; transform: translateY(-1px); }
.gate-button.selected { border-color: var(--cyan); background: var(--cyan-soft); box-shadow: 0 0 0 3px rgba(6,182,212,.09); }
.gate-index { color: var(--faint); font: 800 9px ui-monospace, monospace; letter-spacing: .1em; }
.gate-name { display: block; margin-top: 14px; font-size: 11px; font-weight: 750; }
.gate-state { display: flex; align-items: center; gap: 6px; margin-top: 8px; color: var(--muted); font-size: 9px; text-transform: uppercase; }
.state-dot { width: 7px; height: 7px; border-radius: 50%; background: #98a2b3; }
.state-dot.pass { background: #12b76a; }.state-dot.warning { background: #f79009; }.state-dot.error,.state-dot.blocked { background: #f04438; }.state-dot.running { background: #06b6d4; }
.dossier { display: grid; grid-template-columns: minmax(0,.9fr) minmax(360px,1.1fr); gap: 18px; margin-top: 20px; }
.decision-card { display: grid; gap: 12px; }
.inset { border: 1px solid var(--line); border-radius: 14px; background: #f9fafb; padding: 15px; }
.inset-label { color: var(--faint); font-size: 9px; font-weight: 850; letter-spacing: .13em; text-transform: uppercase; }
.inset p { margin: 9px 0 0; color: #344054; font-size: 12px; line-height: 1.6; }
.manifest { overflow: hidden; border: 1px solid #1d2939; border-radius: 16px; background: var(--navy); color: #e4e7ec; }
.manifest-head { display: flex; justify-content: space-between; gap: 16px; padding: 16px 18px; border-bottom: 1px solid #1d2939; }
.manifest-head .eyebrow { color: #67e8f9; }
.manifest dl { margin: 0; }
.manifest-row { display: grid; grid-template-columns: 140px minmax(0,1fr); gap: 16px; border-bottom: 1px solid #1d2939; padding: 12px 18px; }
.manifest-row:last-child { border-bottom: 0; }
.manifest dt { color: #667085; font: 750 9px ui-monospace,monospace; letter-spacing: .07em; text-transform: uppercase; }
.manifest dd { margin: 0; overflow-wrap: anywhere; color: #d0d5dd; font: 11px/1.5 ui-monospace,monospace; }
.timeline { display: grid; gap: 10px; margin-top: 12px; }
.timeline-item { display: grid; grid-template-columns: 8px minmax(0,1fr); gap: 10px; }
.timeline-line { display: flex; flex-direction: column; align-items: center; }
.timeline-line .state-dot { margin-top: 5px; }
.timeline-line::after { content:""; width: 1px; flex: 1; margin-top: 4px; background: var(--line); }
.timeline-item:last-child .timeline-line::after { display: none; }
.timeline-copy { padding-bottom: 7px; }
.timeline-copy strong { font-size: 11px; text-transform: capitalize; }
.timeline-copy time { float: right; color: var(--faint); font: 9px ui-monospace,monospace; }
.timeline-copy p { margin: 4px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.table-scroll { overflow-x: auto; }
table { width: 100%; min-width: 780px; border-collapse: collapse; font-size: 12px; }
th { background: #f9fafb; color: var(--faint); padding: 11px 15px; text-align: left; font-size: 9px; letter-spacing: .12em; text-transform: uppercase; }
td { border-top: 1px solid #f2f4f7; padding: 14px 15px; color: #344054; }
tbody tr { transition: background .15s ease; }
tbody tr:hover { background: #f0fdfa; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
.right { text-align: right; }
.empty { padding: 52px 20px; color: var(--muted); text-align: center; }
.empty-mark { display: grid; place-items: center; width: 42px; height: 42px; margin: 0 auto 13px; border: 1px solid var(--line); border-radius: 50%; color: var(--faint); }
.notice { border: 1px solid; border-radius: 14px; padding: 15px 18px; font-size: 12px; line-height: 1.55; }
.notice.warning { border-color: #fedf89; background: var(--amber-soft); color: var(--amber); }
.notice.error { border-color: #fecdca; background: var(--rose-soft); color: var(--rose); }
.notice.success { border-color: #abefc6; background: var(--green-soft); color: var(--green); }
.findings { display: grid; gap: 10px; }
.finding { display: grid; grid-template-columns: 30px minmax(0,1fr); gap: 12px; border-left: 4px solid var(--cyan); border-radius: 11px; background: #f9fafb; padding: 14px; }
.finding.critical { border-color: #f04438; background: var(--rose-soft); }
.finding.warning { border-color: #f79009; background: var(--amber-soft); }
.finding-index { color: var(--faint); font: 700 14px ui-monospace,monospace; }
.finding code { font-size: 10px; font-weight: 800; color: #344054; }
.finding p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.55; }
.source-grid { display: grid; gap: 10px; }
.source { border: 1px solid var(--line); border-radius: 13px; padding: 14px; }
.source-top { display: flex; justify-content: space-between; gap: 12px; }
.source h3 { margin: 0; font-size: 12px; text-transform: capitalize; }
.source-meta, .source-path { color: var(--faint); font: 9px/1.5 ui-monospace,monospace; }
.source-meta { margin-top: 5px; }.source-path { overflow-wrap:anywhere; border-top: 1px solid #f2f4f7; margin-top: 12px; padding-top: 10px; }
.coverage { display: grid; gap: 18px; }
.coverage-head { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; }
.coverage-track { overflow: hidden; height: 7px; margin-top: 8px; border-radius: 99px; background: #eaecf0; }
.coverage-fill { height: 100%; border-radius: inherit; background: var(--cyan); }
.coverage-fill.width-0 { width: 0; }.coverage-fill.width-5 { width: 5%; }
.coverage-fill.width-10 { width: 10%; }.coverage-fill.width-15 { width: 15%; }
.coverage-fill.width-20 { width: 20%; }.coverage-fill.width-25 { width: 25%; }
.coverage-fill.width-30 { width: 30%; }.coverage-fill.width-35 { width: 35%; }
.coverage-fill.width-40 { width: 40%; }.coverage-fill.width-45 { width: 45%; }
.coverage-fill.width-50 { width: 50%; }.coverage-fill.width-55 { width: 55%; }
.coverage-fill.width-60 { width: 60%; }.coverage-fill.width-65 { width: 65%; }
.coverage-fill.width-70 { width: 70%; }.coverage-fill.width-75 { width: 75%; }
.coverage-fill.width-80 { width: 80%; }.coverage-fill.width-85 { width: 85%; }
.coverage-fill.width-90 { width: 90%; }.coverage-fill.width-95 { width: 95%; }
.coverage-fill.width-100 { width: 100%; }
.success-copy { color: var(--green); }
.filters { display: grid; grid-template-columns: 1fr 1fr 1fr 1.5fr auto; gap: 12px; align-items: end; }
.field span { display: block; margin-bottom: 7px; color: var(--faint); font-size: 9px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
.field input, .field select { width: 100%; min-height: 44px; border: 1px solid #d0d5dd; border-radius: 11px; background: white; padding: 8px 11px; color: var(--ink); font-size: 12px; }
.field input:hover, .field select:hover { border-color: #98a2b3; }
.filter-actions { display: flex; gap: 7px; }
.breakdown-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.loading-state { display: grid; gap: 18px; }
.skeleton { position: relative; overflow: hidden; border-radius: 20px; background: #eaecf0; }
.skeleton::after { content:""; position:absolute; inset:0; transform:translateX(-100%); background:linear-gradient(90deg,transparent,#ffffff80,transparent); animation: shimmer 1.4s infinite; }
.hero-skeleton { height: 280px; }.metric-skeleton { height: 136px; }
@keyframes shimmer { 100% { transform: translateX(100%); } }
.nav-scrim { position: fixed; z-index: 25; inset: 0; background: rgba(16,24,40,.5); }
@media (max-width: 1120px) {
.metric-grid { grid-template-columns: repeat(2,1fr); }
.two-col, .breakdown-grid, .dossier { grid-template-columns: 1fr; }
.hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; }
.panel-head { flex-wrap: wrap; }
.filters { grid-template-columns: 1fr 1fr; }
.filter-actions { grid-column: 1/-1; }
}
@media (max-width: 820px) {
.sidebar { transform: translateX(-100%); transition: transform .2s ease; box-shadow: 18px 0 50px rgba(16,24,40,.28); }
.sidebar.open { transform: translateX(0); }
.shell { margin-left: 0; }
.menu-button { display: grid; }
.hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; }
.dossier { grid-template-columns: 1fr; }
.top-actions .pill { display: none; }
}
@media (max-width: 560px) {
main { padding: 18px 14px 52px; }
.topbar { padding: 10px 14px; }
.metric-grid, .filters { grid-template-columns: 1fr; }
.hero { border-radius: 20px; padding: 25px 20px; }
.hero h2 { font-size: 31px; }
.panel-head, .panel-body { padding-left: 17px; padding-right: 17px; }
.manifest-row { grid-template-columns: 1fr; gap: 5px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
@media print {
.sidebar, .topbar { display: none; }
.shell { margin: 0; }
main { max-width: none; padding: 0; }
.hero, .metric-card, .panel { box-shadow: none; break-inside: avoid; }
}
@@ -0,0 +1,383 @@
(() => {
"use strict";
const params = new URLSearchParams(location.search);
const suppliedToken = params.get("token");
if (suppliedToken) sessionStorage.setItem("casan_viewer_token", suppliedToken);
const token = suppliedToken || sessionStorage.getItem("casan_viewer_token") || "";
params.delete("token");
history.replaceState({}, "", `${location.pathname}${params.size ? `?${params}` : ""}${location.hash}`);
const state = {
view: params.get("view") || (params.get("trace") ? "runs" : "overview"),
trace: params.get("trace") || "",
project: null,
registry: null,
h6: null,
run: null,
selectedGate: "H1-context",
};
const $ = (selector) => document.querySelector(selector);
const esc = (value) => String(value ?? "")
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll('"', "&quot;").replaceAll("'", "&#039;");
const slug = (value) => String(value || "unknown").toLowerCase().replaceAll("_", "-").replace(/[^a-z0-9-]/g, "");
const text = (value, fallback = "Unavailable") => value === null || value === undefined || value === "" ? fallback : String(value);
const number = (value) => value === null || value === undefined || !Number.isFinite(Number(value))
? "Unavailable" : new Intl.NumberFormat("en-US").format(Number(value));
const time = (value) => value ? String(value).replace("T", " ").replace("Z", " UTC") : "No timestamp";
const money = (value) => value === null || value === undefined ? "Unavailable" : `$${Number(value).toFixed(6)}`;
const pill = (value) => `<span class="pill ${slug(value)}">${esc(String(value || "unknown").replaceAll("_", " "))}</span>`;
async function api(path) {
const response = await fetch(path, {
headers: { "X-CASAN-Viewer-Token": token },
cache: "no-store",
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}
function downloadUrl(path, query = {}) {
const next = new URLSearchParams({ ...query, token });
return `${path}?${next}`;
}
function hero({ eyebrow, title, description, verdict, meta = [], actions = "" }) {
return `<article class="hero">
<div class="hero-grid">
<div>
<p class="eyebrow">${esc(eyebrow)}</p>
<h2>${esc(title)}</h2>
<p class="hero-description">${esc(description)}</p>
<div class="hero-meta">${meta.map((item) => `<span>${esc(item)}</span>`).join("")}</div>
</div>
<div class="hero-actions">${verdict ? pill(verdict) : ""}${actions}</div>
</div>
</article>`;
}
function metric(label, value, detail) {
return `<article class="metric-card"><div class="metric-label">${esc(label)}</div>
<div class="metric-value">${esc(text(value))}</div><div class="metric-detail">${esc(detail)}</div></article>`;
}
function panel(eyebrow, title, description, body, right = "") {
return `<article class="panel"><header class="panel-head"><div><p class="eyebrow">${esc(eyebrow)}</p>
<h2>${esc(title)}</h2><p class="panel-description">${esc(description)}</p></div>${right}</header>
<div class="panel-body">${body}</div></article>`;
}
function empty(title, detail) {
return `<div class="empty"><div class="empty-mark">∅</div><strong>${esc(title)}</strong><p>${esc(detail)}</p></div>`;
}
function setView(view, push = true) {
state.view = ["overview", "runs", "h6"].includes(view) ? view : "overview";
document.querySelectorAll(".nav-item").forEach((item) => {
const active = item.dataset.view === state.view;
item.classList.toggle("active", active);
if (active) item.setAttribute("aria-current", "page");
else item.removeAttribute("aria-current");
});
document.querySelectorAll(".view").forEach((item) => item.classList.add("hidden"));
$(`#${state.view}-view`).classList.remove("hidden");
$("#page-title").textContent = {
overview: "Assurance overview", runs: "Run assurance registry", h6: "H6 AgentOps dossier",
}[state.view];
if (push) {
const next = new URL(location.href);
next.searchParams.set("view", state.view);
if (state.trace) next.searchParams.set("trace", state.trace);
else next.searchParams.delete("trace");
history.pushState({}, "", `${next.pathname}?${next.searchParams}`);
}
if (state.view === "runs") renderRuns();
if (state.view === "h6") renderH6();
closeNav();
}
function renderOverview() {
const profile = state.project;
const registry = state.registry;
const latest = registry.latest || {};
const recent = registry.runs || [];
const failures = recent.filter((row) => row.status === "failed").length;
const certified = latest.certified === true;
const latestTrace = latest.trace_id || recent[0]?.trace_id;
const maturity = profile.maturity || {};
const actions = latestTrace
? `<button class="button primary" type="button" data-open-trace="${esc(latestTrace)}">Inspect latest run</button>
<button class="button secondary" type="button" data-open-h6="${esc(latestTrace)}">Open H6</button>`
: `<button class="button secondary" type="button" data-view-target="runs">View registry</button>`;
$("#overview-view").innerHTML =
hero({
eyebrow: "CASAN Core · Local assurance",
title: latestTrace ? "One prompt. One evidence trail." : "Ready for the first governed run.",
description: "A read-only, offline-capable assurance workspace reconstructed from canonical project evidence. No Platform service or HTML generation on the prompt hot path.",
verdict: latestTrace ? (certified ? "certified" : "review required") : "ready",
meta: [
`${profile.project_name} · ${profile.edition}`,
latestTrace ? `Trace ${latestTrace}` : "No finalized trace yet",
`Maturity ${maturity.level == null ? "not assessed" : `L${maturity.level}`} · ${maturity.status}`,
],
actions,
}) +
`<section class="metric-grid">
${metric("Governed records", number(registry.count), `${recent.length} recent records loaded`)}
${metric("Latest H6 quality", latest.telemetry_quality || "Unavailable", "Missing values are disclosed, never coerced to zero")}
${metric("Observed failures", number(failures), `${recent.length ? Math.round((failures / recent.length) * 100) : 0}% of loaded records`)}
${metric("Evidence gates", latestTrace ? "H1 → H7" : "Waiting", latestTrace ? "Control-by-control reconstruction available" : "Evidence appears after a prompt finalizes")}
</section>` +
panel(
"Product boundary",
"Core is visual by default",
"The local viewer covers the complete single-project review workflow; centralized operations remain a Platform capability.",
`<div class="two-col">
<div class="notice success"><strong>Included in Core</strong><br>Latest run, history, interactive H1–H7 evidence, H6 AgentOps, and on-demand JSON/HTML export.</div>
<div class="notice warning"><strong>Platform boundary</strong><br>Multi-project fleet views, centralized RBAC, shared approvals, ingestion and organization-wide retention.</div>
</div>`
) +
recentRunsPanel(recent.slice(0, 6), "Latest governed evidence");
bindActions();
}
function recentRunsPanel(rows, title = "Recent governed runs") {
const body = rows.length ? `<div class="table-scroll"><table>
<thead><tr><th>Observed at</th><th>Lifecycle step</th><th>Verdict</th><th class="right">Latency</th><th class="right">Tokens</th><th class="right">Evidence</th></tr></thead>
<tbody>${rows.map((row) => `<tr>
<td class="mono">${esc(time(row.timestamp))}</td><td><strong>${esc(row.step || row.harness || "Agentic turn")}</strong></td>
<td>${pill(row.status)}</td><td class="right mono">${esc(row.latency_ms == null ? "Unavailable" : `${number(row.latency_ms)} ms`)}</td>
<td class="right mono">${esc(number(row.total_tokens))}</td>
<td class="right">${row.trace_id ? `<button class="button light" type="button" data-open-trace="${esc(row.trace_id)}">Inspect H1–H7</button>` : "—"}</td>
</tr>`).join("")}</tbody></table></div>` : empty("No governed run yet", "Complete a prompt in a configured client, then refresh this viewer.");
return panel("Evidence index", title, "Operational values are shown only when present in canonical telemetry.", body);
}
function renderRuns() {
const rows = state.registry.runs || [];
const selected = state.run;
const body = hero({
eyebrow: "Run assurance registry",
title: "Every governed run, one evidence trail.",
description: "Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent dossier.",
verdict: rows.some((row) => row.status === "failed") ? "attention" : "operational",
meta: [`${state.registry.count} records`, `${rows.filter((row) => row.status === "failed").length} failures`, "Local canonical evidence"],
actions: `<button class="button primary" type="button" data-view-target="h6">Open H6 dossier</button>`,
}) + (selected ? renderTrace(selected) : "") + recentRunsPanel(rows);
$("#runs-view").innerHTML = body;
bindActions();
bindGates();
}
function renderTrace(report) {
const gates = report.gates || [];
let selected = gates.find((gate) => gate.id === state.selectedGate) || gates[0];
if (!selected) return "";
const rail = gates.map((gate, index) => `<button type="button" aria-pressed="${gate.id === selected.id}" class="gate-button ${gate.id === selected.id ? "selected" : ""}" data-gate="${esc(gate.id)}">
<span class="gate-index">0${index + 1}</span><span class="gate-name">${esc(gate.title)}</span>
<span class="gate-state"><span class="state-dot ${slug(gate.status)}"></span>${esc(gate.status)}</span></button>`).join("");
const events = (selected.events || []).slice(-4).reverse();
const timeline = events.length ? `<div class="timeline">${events.map((event) => `<div class="timeline-item">
<div class="timeline-line"><span class="state-dot ${slug(event.status)}"></span></div>
<div class="timeline-copy"><strong>${esc(event.status)}</strong><time>${esc(time(event.timestamp))}</time><p>${esc(event.reason)}</p></div>
</div>`).join("")}</div>` : `<p class="metric-detail">No individual event was emitted for this control.</p>`;
const entries = Object.entries(selected.evidence || {});
const manifest = entries.length ? `<dl>${entries.map(([key, value]) => `<div class="manifest-row"><dt>${esc(key)}</dt><dd>${esc(typeof value === "string" ? value : JSON.stringify(value))}</dd></div>`).join("")}</dl>`
: empty("No safe evidence fields", "This control has not emitted a sanitized evidence manifest.");
const actions = `<a class="button secondary" href="${downloadUrl(`/api/v1/reports/run/${encodeURIComponent(report.trace_id)}/export`, { format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl(`/api/v1/reports/run/${encodeURIComponent(report.trace_id)}/export`, { format: "html" })}">Export dossier</a>`;
return panel(
"Run assurance",
"Governed execution dossier",
"A control-by-control reconstruction backed by sanitized lifecycle evidence.",
`<div class="hero trace-hero">
<div class="hero-grid"><div><p class="eyebrow">Trace provenance</p><h2 class="trace-title">${esc(report.trace_id)}</h2>
<div class="hero-meta trace-meta"><span>${report.summary.gates_observed}/7 controls observed</span><span>${report.project.edition} edition</span></div></div>
<div>${pill(report.verdict)}</div></div></div>
<div class="rail-wrap rail-spaced"><div class="evidence-rail">${rail}</div></div>
<div class="dossier">
<div class="decision-card"><div><p class="eyebrow">Selected control</p><h2>${esc(selected.title)}</h2><p class="panel-description">${esc(selected.description)}</p></div>
<div class="inset"><span class="inset-label">Latest decision · ${esc(time(selected.updated_at))}</span><p>${esc(selected.reason)}</p></div>
<div class="inset"><span class="inset-label">Event history</span>${timeline}</div>
</div>
<div class="manifest"><div class="manifest-head"><div><p class="eyebrow">Safe evidence manifest</p><div class="metric-detail">Sanitized fields persisted by the harness</div></div>${pill(`${entries.length} fields`)}</div>${manifest}</div>
</div>`,
actions,
);
}
function renderH6() {
const data = state.h6;
if (!data) return;
const query = data.scope || {};
const queryObject = Object.fromEntries(Object.entries(query).filter(([, value]) => value));
const actions = `<a class="button secondary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "html" })}">Export dossier</a>`;
const findings = data.findings.length ? `<div class="findings">${data.findings.map((item, index) => `<article class="finding ${slug(item.severity)}">
<span class="finding-index">${String(index + 1).padStart(2, "0")}</span><div><code>${esc(item.code)}</code><p>${esc(item.message)}</p></div></article>`).join("")}</div>`
: `<div class="notice success">No threshold breach was detected in the selected evidence boundary.</div>`;
const sources = data.evidence_sources.map((source) => {
const status = !source.present ? "missing" : source.stale ? "stale" : "fresh";
return `<article class="source"><div class="source-top"><div><h3>${esc(source.source)}</h3><div class="source-meta">${source.records} records · ${source.age_s == null ? "no timestamp" : `${number(source.age_s)}s old`}</div></div>${pill(status)}</div>
<div class="source-path">${esc(source.path)}</div></article>`;
}).join("");
const breakdown = (rows, subject) => `<div class="table-scroll"><table><thead><tr><th>${esc(subject)}</th><th class="right">Runs</th><th class="right">Failures</th><th class="right">Avg latency</th><th class="right">Tokens</th><th class="right">Cost</th></tr></thead>
<tbody>${rows.length ? rows.map((row) => `<tr><td><strong>${esc(row.key)}</strong></td><td class="right mono">${row.runs}</td><td class="right mono">${row.failures}</td><td class="right mono">${number(row.latency_avg_ms)} ms</td><td class="right mono">${number(row.tokens)}</td><td class="right mono">${money(row.cost_usd)}</td></tr>`).join("") : `<tr><td colspan="6">${empty("No matching records", `No ${subject.toLowerCase()} evidence is available.`)}</td></tr>`}</tbody></table></div>`;
$("#h6-view").innerHTML =
hero({
eyebrow: "H6 · AgentOps assurance",
title: "Operational evidence, ready for a decision.",
description: "Runtime health, reliability, cost provenance and telemetry integrity—designed to answer what happened, why it matters and where the evidence lives.",
verdict: data.verdict,
meta: [data.report_id, `Generated ${time(data.generated_at)}`, `Freshness ${data.freshness.status}`],
actions,
}) +
panel("Scope", "Evidence boundary", "Filters apply to the screen and both export formats.", h6Filters(data)) +
`<section class="metric-grid">
${metric("Governed runs", number(data.summary.runs), `${data.summary.success} successful · ${data.summary.degraded} degraded`)}
${metric("Failure rate", `${data.summary.failure_rate_pct}%`, `${data.summary.failed} failed · ${data.summary.alerts} alerts`)}
${metric("P95 latency", `${number(data.summary.latency_ms.p95)} ms`, `P50 ${number(data.summary.latency_ms.p50)} ms`)}
${metric("Telemetry integrity", data.data_quality.status.replaceAll("_", " "), `${data.summary.coverage.token_pct}% token · ${data.summary.coverage.cost_pct}% cost`)}
</section>` +
`<div class="two-col">
${panel("Decision brief", "What requires attention", "Threshold findings are review signals, not maturity claims.", findings, pill(data.verdict))}
${panel("Source integrity", "Evidence provenance", "Freshness is evaluated independently for each canonical source.", `<div class="source-grid">${sources}</div>`, pill(data.freshness.status))}
</div>` +
`<div class="breakdown-grid">
${panel("Runtime", "Execution profile by step", "Latency, failure and attribution by governed lifecycle step.", breakdown(data.details.by_step, "Step"))}
${panel("Provider", "Model usage and provenance", "Only provider-reported token and actual cost values are shown.", breakdown(data.details.by_provider, "Provider · model"))}
</div>` +
panel("Evidence quality", "Coverage and disclosure", "Unavailable telemetry remains null. Coverage gaps stay visible.", `<div class="two-col">
<div class="coverage">${coverage("Token attribution", data.summary.coverage.token_pct, `${data.summary.coverage.token_records}/${data.summary.coverage.runtime_records} records`)}
${coverage("Cost attribution", data.summary.coverage.cost_pct, `${data.summary.coverage.cost_records}/${data.summary.coverage.runtime_records} records`)}</div>
<div class="inset"><span class="inset-label">Disclosure notes</span>${data.data_quality.warnings.length ? `<ul>${data.data_quality.warnings.map((warning) => `<li class="metric-detail">${esc(warning)}</li>`).join("")}</ul>` : `<p class="success-copy">All required sources are present with no estimation warning.</p>`}</div>
</div>`, pill(data.data_quality.status));
bindH6Filters();
}
function coverage(label, value, detail) {
const width = Math.round(Math.max(0, Math.min(100, Number(value) || 0)) / 5) * 5;
return `<div><div class="coverage-head"><strong>${esc(label)}</strong><span>${esc(value)}%</span></div>
<div class="coverage-track"><div class="coverage-fill width-${width}"></div></div>
<div class="metric-detail">${esc(detail)}</div></div>`;
}
function h6Filters(data) {
const scope = data.scope || {};
const options = data.available_filters || { projects: [], runs: [] };
const date = (value) => value ? value.slice(0, 10) : "";
return `<form id="h6-filters" class="filters">
<label class="field"><span>Project</span><select name="project"><option value="">All projects</option>${options.projects.map((value) => `<option value="${esc(value)}" ${scope.project === value ? "selected" : ""}>${esc(value)}</option>`).join("")}</select></label>
<label class="field"><span>From</span><input type="date" name="from" value="${esc(date(scope.from))}"></label>
<label class="field"><span>To</span><input type="date" name="to" value="${esc(date(scope.to))}"></label>
<label class="field"><span>Run / trace</span><input name="run" list="run-options" value="${esc(scope.run || "")}" placeholder="All governed runs"><datalist id="run-options">${options.runs.map((value) => `<option value="${esc(value)}"></option>`).join("")}</datalist></label>
<div class="filter-actions"><button class="button primary" type="submit">Apply</button><button class="button light" type="button" id="clear-h6">Clear</button></div>
</form>`;
}
async function loadTrace(traceId) {
if (!traceId) return;
state.trace = traceId;
state.selectedGate = "H1-context";
$("#loading").classList.remove("hidden");
try {
state.run = await api(`/api/v1/reports/run/${encodeURIComponent(traceId)}`);
setView("runs");
} catch (error) {
showError(`Run report could not be reconstructed: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
async function loadH6(query = {}) {
const next = new URLSearchParams(Object.fromEntries(Object.entries(query).filter(([, value]) => value)));
$("#loading").classList.remove("hidden");
try {
state.h6 = await api(`/api/v1/reports/h6${next.size ? `?${next}` : ""}`);
renderH6();
} catch (error) {
showError(`H6 report could not be generated: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
function bindActions() {
document.querySelectorAll("[data-view-target]").forEach((button) => button.addEventListener("click", () => setView(button.dataset.viewTarget)));
document.querySelectorAll("[data-open-trace]").forEach((button) => button.addEventListener("click", () => loadTrace(button.dataset.openTrace)));
document.querySelectorAll("[data-open-h6]").forEach((button) => button.addEventListener("click", async () => {
await loadH6({ run: button.dataset.openH6 });
setView("h6");
}));
}
function bindGates() {
document.querySelectorAll("[data-gate]").forEach((button) => button.addEventListener("click", () => {
state.selectedGate = button.dataset.gate;
renderRuns();
}));
}
function bindH6Filters() {
$("#h6-filters")?.addEventListener("submit", async (event) => {
event.preventDefault();
const values = Object.fromEntries(new FormData(event.currentTarget));
await loadH6(values);
});
$("#clear-h6")?.addEventListener("click", () => loadH6());
}
function showError(message) {
$("#error").textContent = message;
$("#error").classList.remove("hidden");
}
function closeNav() {
$(".sidebar").classList.remove("open");
$("#nav-scrim").classList.add("hidden");
$("#menu-button").setAttribute("aria-expanded", "false");
}
async function loadAll() {
$("#error").classList.add("hidden");
$("#loading").classList.remove("hidden");
try {
[state.project, state.registry, state.h6] = await Promise.all([
api("/api/v1/project"),
api("/api/v1/runs?limit=100"),
api("/api/v1/reports/h6?limit=50"),
]);
$("#freshness-pill").textContent = `${state.h6.freshness.status} evidence`;
$("#freshness-pill").className = `pill ${slug(state.h6.freshness.status)}`;
renderOverview();
renderH6();
if (state.trace) {
await loadTrace(state.trace);
} else {
setView(state.view, false);
}
} catch (error) {
showError(`Local evidence could not be loaded: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
document.querySelectorAll(".nav-item").forEach((button) => button.addEventListener("click", () => setView(button.dataset.view)));
$("#refresh-button").addEventListener("click", loadAll);
$("#menu-button").addEventListener("click", () => {
const open = !$(".sidebar").classList.contains("open");
$(".sidebar").classList.toggle("open", open);
$("#nav-scrim").classList.toggle("hidden", !open);
$("#menu-button").setAttribute("aria-expanded", String(open));
});
$("#nav-scrim").addEventListener("click", closeNav);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeNav();
});
window.addEventListener("popstate", () => setView(new URLSearchParams(location.search).get("view") || "overview", false));
loadAll();
})();
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="16" fill="#101828"/>
<rect x="10" y="10" width="44" height="44" rx="12" fill="#172033" stroke="#344054"/>
<path d="M39.5 22.5a14 14 0 1 0 0 19" fill="none" stroke="#67e8f9" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#101828" />
<title>CASAN · Local Assurance Viewer</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/app.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to report</a>
<div id="app">
<aside class="sidebar" aria-label="Primary navigation">
<a class="brand" href="#overview" aria-label="CASAN overview">
<span class="brand-mark" aria-hidden="true">C</span>
<span><strong>CASAN</strong><small>Local Assurance</small></span>
</a>
<nav>
<button class="nav-item active" data-view="overview" type="button"><span aria-hidden="true">◫</span>Overview</button>
<button class="nav-item" data-view="runs" type="button"><span aria-hidden="true">⌁</span>Run registry</button>
<button class="nav-item" data-view="h6" type="button"><span aria-hidden="true">H6</span>AgentOps</button>
</nav>
<div class="local-seal">
<span class="live-dot"></span>
<div><strong>Local Core</strong><small>Loopback · read-only</small></div>
</div>
</aside>
<div class="shell">
<header class="topbar">
<button id="menu-button" class="icon-button menu-button" type="button" aria-label="Open navigation" aria-expanded="false">☰</button>
<div>
<p class="top-eyebrow">Independent evidence workspace</p>
<h1 id="page-title">Assurance overview</h1>
</div>
<div class="top-actions">
<span id="freshness-pill" class="pill neutral">Evidence loading</span>
<button id="refresh-button" class="icon-button" type="button" aria-label="Refresh evidence">↻</button>
</div>
</header>
<main id="main" tabindex="-1">
<div id="loading" class="loading-state" aria-live="polite">
<div class="skeleton hero-skeleton"></div>
<div class="metric-grid">
<div class="skeleton metric-skeleton"></div><div class="skeleton metric-skeleton"></div>
<div class="skeleton metric-skeleton"></div><div class="skeleton metric-skeleton"></div>
</div>
</div>
<div id="error" class="notice error hidden" role="alert"></div>
<section id="overview-view" class="view hidden" aria-labelledby="page-title"></section>
<section id="runs-view" class="view hidden" aria-labelledby="page-title"></section>
<section id="h6-view" class="view hidden" aria-labelledby="page-title"></section>
</main>
</div>
<div id="nav-scrim" class="nav-scrim hidden"></div>
</div>
<script src="/app.js" defer></script>
</body>
</html>
@@ -0,0 +1,701 @@
#!/usr/bin/env python3
"""Zero-dependency, read-only reporting engine for CASAN Core.
The evidence files under .specify remain the source of truth. This module only
builds bounded, sanitized projections for the Local Assurance Viewer and
on-demand JSON/HTML exports.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from datetime import datetime, timezone
from html import escape
import json
import math
import os
from pathlib import Path
import re
from typing import Any
SAFE_ID = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$")
STALE_AFTER_S = 900
MAX_SOURCE_BYTES = 20 * 1024 * 1024
MAX_RECORDS = 2000
GATES = (
("H1-context", "H1 · Context", "Prompt contract, mode and risk classification"),
("H2-tool", "H2 · Tool", "Allowlisted source and tool preparation"),
("H3-eval", "H3 · Eval", "Grounded synthesis and quality evaluation"),
("H4-security", "H4 · Security", "Input and output security boundary"),
("H5-governance", "H5 · Governance", "Decision policy and append-only audit"),
("H6-agentops", "H6 · AgentOps", "Runtime, token, cost and failure telemetry"),
("H7-orchestration", "H7 · Orchestration", "Final governed outcome and certification"),
)
GATE_MAP = {f"H{index}": gate[0] for index, gate in enumerate(GATES, 1)}
SENSITIVE_KEYS = {
"authorization", "api_key", "apikey", "access_token", "refresh_token",
"password", "secret", "prompt", "raw_prompt", "tool_input", "tool_output",
"request_body", "response_body",
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def state_root(root: str | Path) -> Path:
override = os.environ.get("CASAN_STATE_ROOT")
return Path(override).expanduser().resolve() if override else Path(root).resolve() / ".specify"
def _read_text(path: Path, maximum: int = MAX_SOURCE_BYTES) -> str:
try:
if not path.is_file() or path.stat().st_size > maximum:
return ""
return path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
def _read_tail_text(path: Path, maximum: int = MAX_SOURCE_BYTES) -> str:
try:
if not path.is_file():
return ""
size = path.stat().st_size
with path.open("rb") as handle:
if size > maximum:
handle.seek(-maximum, os.SEEK_END)
handle.readline() # discard a possibly partial first record
return handle.read(maximum).decode("utf-8", errors="replace")
except OSError:
return ""
def read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(_read_text(path))
return value if isinstance(value, dict) else {}
except (TypeError, ValueError):
return {}
def read_jsonl(path: Path, maximum_records: int = MAX_RECORDS) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for line in _read_tail_text(path).splitlines()[-maximum_records:]:
try:
value = json.loads(line)
except ValueError:
continue
if isinstance(value, dict):
rows.append(value)
return rows
def _safe_value(value: Any, depth: int = 0) -> Any:
if depth > 5:
return "[depth limited]"
if isinstance(value, dict):
result: dict[str, Any] = {}
for key, item in list(value.items())[:80]:
normalized = str(key).lower().replace("-", "_")
if normalized in SENSITIVE_KEYS:
result[str(key)] = "[redacted]"
else:
result[str(key)] = _safe_value(item, depth + 1)
return result
if isinstance(value, list):
return [_safe_value(item, depth + 1) for item in value[:80]]
if isinstance(value, str):
return value if len(value) <= 4096 else value[:4096] + "…"
if value is None or isinstance(value, (bool, int, float)):
return value
return str(value)[:4096]
def sanitize(row: dict[str, Any]) -> dict[str, Any]:
value = _safe_value(row)
return value if isinstance(value, dict) else {}
def _number(value: Any) -> float | None:
if isinstance(value, bool):
return None
if isinstance(value, (int, float)) and math.isfinite(float(value)):
return float(value)
return None
def _threshold(name: str, fallback: float) -> float:
try:
value = float(os.environ.get(name, str(fallback)))
return value if math.isfinite(value) and value >= 0 else fallback
except ValueError:
return fallback
def _text(value: Any, fallback: str = "") -> str:
return value.strip() if isinstance(value, str) and value.strip() else fallback
def _row_run(row: dict[str, Any]) -> str:
return _text(row.get("run_id") or row.get("trace_id"), "unattributed")
def _row_project(row: dict[str, Any]) -> str:
return _text(row.get("project") or row.get("project_id"), "default")
def _parse_time(value: Any) -> float | None:
if not isinstance(value, str) or not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
except ValueError:
return None
def _status(value: Any) -> str:
status = _text(value).lower()
if status in {"success", "pass", "passed", "allow", "allowed", "answered", "opened", "certified"}:
return "pass"
if status in {"warn", "warning", "degraded", "partial", "insufficient"}:
return "warning"
if status in {"block", "blocked", "deny", "denied", "flag", "non_certified"}:
return "blocked"
if status in {"fail", "failed", "error"}:
return "error"
if status in {"running", "skipped"}:
return status
return "queued"
def paths(root: str | Path) -> dict[str, Path]:
state = state_root(root)
return {
"config": Path(root).resolve() / ".casan" / "config.json",
"receipt": state / "state" / "latest-run.json",
"metrics": state / "logs" / "cost" / "metrics.jsonl",
"provider": state / "logs" / "level5" / "provider-usage.jsonl",
"alerts": state / "agentops" / "alerts.log",
"trace_dir": state / "logs" / "trace",
"event_dir": state / "logs" / "trace-events",
}
def project_profile(root: str | Path) -> dict[str, Any]:
config = read_json(paths(root)["config"])
maturity = config.get("maturity") if isinstance(config.get("maturity"), dict) else {}
return {
"project_id": _text(config.get("project_id"), Path(root).resolve().name),
"project_name": _text(config.get("project_name"), Path(root).resolve().name),
"edition": _text(config.get("edition") or config.get("target_level_name"), "core"),
"edition_status": _text(config.get("edition_status"), "configured"),
"enforcement_mode": _text(config.get("enforcement_mode"), "unknown"),
"integration_mode": _text(config.get("integration_mode"), "unknown"),
"clients": [str(value) for value in config.get("clients", []) if isinstance(value, str)],
"maturity": {
"level": maturity.get("level") if _number(maturity.get("level")) is not None else None,
"status": _text(maturity.get("status"), "not_assessed"),
"evidence": _text(maturity.get("evidence")) or None,
},
"viewer": {
"mode": "local_core",
"scope": "single_project",
"read_only": True,
"network": "loopback_only",
},
}
def _trace(root: str | Path, trace_id: str) -> dict[str, Any]:
if not SAFE_ID.fullmatch(trace_id):
return {}
return read_json(paths(root)["trace_dir"] / f"agentic-{trace_id}.json")
def _metric_rows(root: str | Path) -> list[dict[str, Any]]:
return read_jsonl(paths(root)["metrics"])
def trace_graph(root: str | Path, trace_id: str) -> dict[str, Any]:
if not SAFE_ID.fullmatch(trace_id):
return _empty_graph(trace_id)
source = paths(root)
events: list[dict[str, Any]] = []
for row in read_jsonl(source["event_dir"] / f"{trace_id}.jsonl", 500):
if row.get("trace_id") != trace_id or row.get("gate_id") not in {gate[0] for gate in GATES}:
continue
events.append({
"timestamp": _text(row.get("timestamp")),
"trace_id": trace_id,
"gate_id": _text(row.get("gate_id")),
"status": _status(row.get("status")),
"reason": _text(row.get("reason"), "Evidence observed"),
"evidence": sanitize(row.get("evidence") if isinstance(row.get("evidence"), dict) else {}),
})
if not events:
trace = _trace(root, trace_id)
harness = _text(trace.get("harness"))
if harness in {gate[0] for gate in GATES}:
events.append({
"timestamp": _text(trace.get("timestamp")),
"trace_id": trace_id,
"gate_id": harness,
"status": _status(trace.get("status") or trace.get("action")),
"reason": f"Legacy {harness} trace",
"evidence": sanitize({
"mode": trace.get("mode"),
"action": trace.get("action"),
"risk_level": trace.get("risk_level"),
}),
})
evidence_rows = trace.get("evidence") if isinstance(trace.get("evidence"), list) else []
for evidence in evidence_rows[:200]:
if not isinstance(evidence, dict):
continue
for marker in _text(evidence.get("h")).split("/"):
gate_id = GATE_MAP.get(marker)
if gate_id:
events.append({
"timestamp": _text(evidence.get("at") or trace.get("finalized_at")),
"trace_id": trace_id,
"gate_id": gate_id,
"status": _status(evidence.get("decision")),
"reason": f"{_text(evidence.get('kind'), 'legacy-evidence')}: {_text(evidence.get('detail'))}",
"evidence": sanitize({
"kind": evidence.get("kind"),
"decision": evidence.get("decision"),
"detail": evidence.get("detail"),
"certification_strength": trace.get("certification_strength"),
}),
})
for metric in _metric_rows(root):
if metric.get("trace_id") != trace_id:
continue
quality = _text(metric.get("telemetry_quality"))
events.append({
"timestamp": _text(metric.get("timestamp")),
"trace_id": trace_id,
"gate_id": "H6-agentops",
"status": "warning" if quality in {"partial", "insufficient"} else _status(metric.get("status")),
"reason": _text(metric.get("step"), "Runtime metric"),
"evidence": sanitize({
"latency_ms": metric.get("latency_ms"),
"total_tokens": metric.get("total_tokens"),
"cost_estimate": metric.get("cost_estimate"),
"telemetry_quality": metric.get("telemetry_quality"),
}),
})
events.sort(key=lambda row: _text(row.get("timestamp")))
nodes = []
for gate_id, title, description in GATES:
gate_events = [row for row in events if row["gate_id"] == gate_id]
latest = gate_events[-1] if gate_events else {}
nodes.append({
"id": gate_id,
"title": title,
"description": description,
"status": latest.get("status", "queued"),
"reason": latest.get("reason", "Waiting for evidence"),
"updated_at": latest.get("timestamp") or None,
"evidence": latest.get("evidence", {}),
"events": gate_events,
})
outcome = nodes[-1]
return {
"found": bool(events),
"trace_id": trace_id,
"updated_at": events[-1]["timestamp"] if events else None,
"terminal": outcome["status"] in {"pass", "blocked", "error"},
"progress": sum(1 for node in nodes if node["status"] != "queued"),
"nodes": nodes,
"events": events,
}
def _empty_graph(trace_id: str) -> dict[str, Any]:
nodes = [{
"id": gate_id, "title": title, "description": description,
"status": "queued", "reason": "Waiting for evidence",
"updated_at": None, "evidence": {}, "events": [],
} for gate_id, title, description in GATES]
return {
"found": False, "trace_id": trace_id, "updated_at": None,
"terminal": False, "progress": 0, "nodes": nodes, "events": [],
}
def run_report(root: str | Path, trace_id: str) -> dict[str, Any]:
graph = trace_graph(root, trace_id)
trace = _trace(root, trace_id)
metric = next(
(row for row in reversed(_metric_rows(root)) if row.get("trace_id") == trace_id),
None,
)
profile = project_profile(root)
metric_row = metric or {}
duration = _number(metric_row.get("duration_ms"))
if duration is None:
duration = _number(metric_row.get("latency_ms"))
found = bool(trace or graph["found"] or metric)
finalized = _text(trace.get("finalized_at") or metric_row.get("finished_at")) or None
if not found:
verdict = "not_found"
elif not graph["terminal"] and not finalized:
verdict = "in_progress"
elif trace.get("certified") is True:
verdict = "certified"
else:
verdict = "non_certified"
cost = metric_row.get("cost") if isinstance(metric_row.get("cost"), dict) else {}
reasons = trace.get("certification_reasons") if isinstance(trace.get("certification_reasons"), list) else []
return {
"schema_version": 1,
"report_id": f"RUN-{trace_id}",
"generated_at": utc_now(),
"trace_id": trace_id,
"project": {
"id": profile["project_id"],
"name": profile["project_name"],
"edition": profile["edition"],
"maturity": profile["maturity"],
},
"verdict": verdict,
"certification": {
"strength": _text(trace.get("certification_strength") or metric_row.get("certification_strength")) or None,
"reasons": [str(value) for value in reasons if isinstance(value, str)],
"finalized_at": finalized,
},
"summary": {
"gates_observed": graph["progress"],
"gates_total": 7,
"tool_calls": int(_number(trace.get("tool_calls") or metric_row.get("tool_calls")) or 0),
"failures": int(_number(trace.get("failures") or metric_row.get("failures")) or 0),
"duration_ms": duration,
"telemetry_quality": _text(metric_row.get("telemetry_quality"), "unknown"),
"token_usage_available": any(_number(metric_row.get(key)) is not None for key in ("total_tokens", "input_tokens", "output_tokens")),
"cost_available": _number(metric_row.get("cost_estimate")) is not None or _number(cost.get("amount")) is not None,
},
"gates": graph["nodes"],
"h6": sanitize(metric_row) if metric else None,
"source": {
"trace_found": bool(trace),
"graph_found": graph["found"],
"metric_found": metric is not None,
},
}
def runs(root: str | Path, limit: int = 100) -> dict[str, Any]:
limit = max(1, min(int(limit), 200))
rows = _metric_rows(root)
receipts = read_json(paths(root)["receipt"])
safe_rows = [sanitize(row) for row in reversed(rows[-limit:])]
if not safe_rows and receipts.get("trace_id"):
safe_rows = [sanitize({
"trace_id": receipts.get("trace_id"),
"project_id": receipts.get("project_id"),
"status": "success" if receipts.get("certified") else "failed",
"timestamp": receipts.get("finalized_at"),
"latency_ms": receipts.get("duration_ms"),
"tool_calls": receipts.get("tool_calls"),
"failures": receipts.get("failures"),
"telemetry_quality": receipts.get("telemetry_quality"),
})]
return {"count": len(rows) if rows else len(safe_rows), "runs": safe_rows, "latest": sanitize(receipts)}
def _freshness(path: Path, source: str, required: bool) -> dict[str, Any]:
try:
stat = path.stat()
age = max(0, int(datetime.now(timezone.utc).timestamp() - stat.st_mtime))
present = path.is_file()
except OSError:
age, present = None, False
return {
"source": source,
"path": str(path),
"present": present,
"updated_at": datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat().replace("+00:00", "Z") if present else None,
"age_s": age,
"stale": bool(present and age is not None and age > STALE_AFTER_S),
"required": required,
}
def _display_path(root: str | Path, path: Path) -> str:
try:
return str(path.resolve().relative_to(Path(root).resolve()))
except (OSError, ValueError):
return f"[external]/{path.name}"
def _percentile(values: list[float], pct: int) -> float:
if not values:
return 0
ordered = sorted(values)
return ordered[max(0, math.ceil((pct / 100) * len(ordered)) - 1)]
def _grouped(rows: list[dict[str, Any]], key) -> list[dict[str, Any]]:
groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
groups[key(row)].append(row)
result = []
for name, records in groups.items():
latencies = [_number(row.get("latency_ms")) for row in records]
latencies = [value for value in latencies if value is not None and value > 0]
tokens = [_number(row.get("total_tokens")) for row in records]
cost_values = [
_number(row.get("cost_usd")) if _number(row.get("cost_usd")) is not None
else _number(row.get("cost_estimate"))
for row in records
]
result.append({
"key": name,
"runs": len(records),
"failures": sum(1 for row in records if _text(row.get("status"), "unknown") == "failed"),
"latency_avg_ms": round(sum(latencies) / len(latencies)) if latencies else 0,
"tokens": round(sum(value for value in tokens if value is not None)) if any(value is not None for value in tokens) else None,
"cost_usd": round(sum(value for value in cost_values if value is not None), 6) if any(value is not None for value in cost_values) else None,
})
return sorted(result, key=lambda row: (-row["runs"], row["key"]))
def _matches(row: dict[str, Any], project: str | None, run: str | None,
start: float | None, end: float | None) -> bool:
if project and _row_project(row) != project:
return False
if run and _row_run(row) != run:
return False
timestamp = _parse_time(row.get("timestamp"))
if start is not None and (timestamp is None or timestamp < start):
return False
if end is not None and (timestamp is None or timestamp > end):
return False
return True
def _date_boundary(value: str | None, end: bool = False) -> tuple[str | None, float | None]:
if not value:
return None, None
normalized = value
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
normalized += "T23:59:59.999Z" if end else "T00:00:00.000Z"
parsed = _parse_time(normalized)
if parsed is None:
raise ValueError("invalid report time boundary")
return datetime.fromtimestamp(parsed, timezone.utc).isoformat().replace("+00:00", "Z"), parsed
def h6_report(root: str | Path, project: str | None = None, run: str | None = None,
from_date: str | None = None, to_date: str | None = None,
limit: int = 50) -> dict[str, Any]:
for value in (project, run):
if value and not SAFE_ID.fullmatch(value):
raise ValueError("invalid project or run filter")
normalized_from, start = _date_boundary(from_date)
normalized_to, end = _date_boundary(to_date, True)
if start is not None and end is not None and start > end:
raise ValueError("invalid report time range")
limit = max(1, min(int(limit), 200))
source = paths(root)
all_metrics = _metric_rows(root)
all_provider = read_jsonl(source["provider"])
all_alerts = read_jsonl(source["alerts"])
metrics = [row for row in all_metrics if _matches(row, project, run, start, end)]
provider = [row for row in all_provider if _matches(row, project, run, start, end)]
alerts = [row for row in all_alerts if _matches(row, project, run, start, end)]
latencies = [value for value in (_number(row.get("latency_ms")) for row in metrics) if value is not None and value > 0]
failed = sum(1 for row in metrics if _text(row.get("status"), "unknown") == "failed")
degraded = sum(1 for row in metrics if _text(row.get("status"), "unknown") == "degraded")
success = sum(1 for row in metrics if _text(row.get("status"), "unknown") in {"success", "pass", "passed"})
provider_token_runs = {_row_run(row) for row in provider if _number(row.get("total_tokens")) is not None}
provider_cost_runs = {_row_run(row) for row in provider if _number(row.get("cost_usd")) is not None}
token_records = sum(1 for row in metrics if any(_number(row.get(key)) is not None for key in ("total_tokens", "input_tokens", "output_tokens")) or _row_run(row) in provider_token_runs)
cost_records = sum(1 for row in metrics if _number(row.get("cost_estimate")) is not None or _row_run(row) in provider_cost_runs)
token_pct = round((token_records / len(metrics)) * 100, 1) if metrics else 0
cost_pct = round((cost_records / len(metrics)) * 100, 1) if metrics else 0
quality = Counter(_text(row.get("telemetry_quality"), "unknown") for row in metrics)
evidence = [
{**_freshness(source["metrics"], "metrics", True), "records": len(all_metrics)},
{**_freshness(source["provider"], "provider", False), "records": len(all_provider)},
{**_freshness(source["alerts"], "alerts", False), "records": len(all_alerts)},
]
for item in evidence:
item["path"] = _display_path(root, Path(item["path"]))
primary = evidence[0]
failure_rate = round((failed / len(metrics)) * 100, 1) if metrics else 0
p95 = _percentile(latencies, 95)
failure_threshold = _threshold("CASAN_H6_FAILURE_RATE_THRESHOLD_PCT", 5)
p95_threshold = _threshold("CASAN_H6_P95_LATENCY_THRESHOLD_MS", 5000)
findings: list[dict[str, Any]] = []
warnings: list[str] = []
if not primary["present"]:
findings.append({"severity": "critical", "code": "METRICS_MISSING", "message": "The required runtime metrics source is missing."})
elif primary["stale"]:
findings.append({"severity": "warning", "code": "METRICS_STALE", "message": "The primary runtime metrics source is stale.", "metric": "age_s", "value": primary["age_s"], "threshold": STALE_AFTER_S})
if failure_rate > failure_threshold:
findings.append({"severity": "critical", "code": "FAILURE_RATE_BREACH", "message": "Failure rate exceeds the configured H6 threshold.", "metric": "failure_rate_pct", "value": failure_rate, "threshold": failure_threshold})
elif failed:
findings.append({"severity": "warning", "code": "FAILURES_PRESENT", "message": f"{failed} failed run(s) are present in the selected scope.", "metric": "failed", "value": failed})
if p95 > p95_threshold:
findings.append({"severity": "warning", "code": "P95_LATENCY_BREACH", "message": "P95 latency exceeds the configured H6 threshold.", "metric": "p95_latency_ms", "value": p95, "threshold": p95_threshold})
alert_counter: Counter[str] = Counter()
fingerprints: set[tuple[str, str]] = set()
for row in metrics + alerts:
alert_types: list[str] = []
if isinstance(row.get("alerts"), list):
alert_types.extend(str(value) for value in row["alerts"] if isinstance(value, str))
body = row.get("body") if isinstance(row.get("body"), dict) else {}
if isinstance(body.get("alert.type"), str):
alert_types.append(body["alert.type"])
for alert in alert_types:
row_run = _row_run(row)
fingerprint = (
_text(row.get("timestamp"), "unknown-time")
if row_run == "unattributed" else row_run,
alert,
)
if fingerprint not in fingerprints:
alert_counter[alert] += 1
fingerprints.add(fingerprint)
alert_count = sum(alert_counter.values())
if alert_count:
findings.append({"severity": "warning", "code": "ALERTS_PRESENT", "message": f"{alert_count} alert signal(s) require review.", "metric": "alerts", "value": alert_count})
if degraded:
findings.append({"severity": "warning", "code": "DEGRADED_RUNS_PRESENT", "message": f"{degraded} degraded run(s) are present in the selected scope.", "metric": "degraded", "value": degraded})
if metrics and (token_pct < 100 or cost_pct < 100):
findings.append({"severity": "warning", "code": "TELEMETRY_COVERAGE_GAP", "message": "Some runs do not have reliable provider token or cost attribution.", "metric": "token_cost_coverage_pct", "value": f"{token_pct}/{cost_pct}", "threshold": "100/100"})
if not provider:
warnings.append("No provider usage records matched the selected scope; token and actual-cost breakdown may be incomplete.")
if metrics and token_pct < 100:
warnings.append(f"{token_pct}% of runtime records have reliable token attribution; unavailable values remain null, never zero.")
if metrics and cost_pct < 100:
warnings.append(f"{cost_pct}% of runtime records have reliable cost attribution; unavailable values remain null, never zero.")
for item in evidence[1:]:
if not item["present"]:
warnings.append(f"Optional {item['source']} telemetry source is missing; its breakdown is unavailable.")
elif item["stale"]:
warnings.append(f"Optional {item['source']} telemetry source is stale; its breakdown may not reflect recent activity.")
critical = any(item["severity"] == "critical" for item in findings)
warning = any(item["severity"] == "warning" for item in findings)
verdict = "no_data" if not metrics and not provider else "fail" if critical else "attention" if warning else "pass"
status_counts = Counter(_text(row.get("status"), "unknown") for row in metrics)
generated_at = utc_now()
total = lambda rows, key: sum(value for value in (_number(row.get(key)) for row in rows) if value is not None)
return {
"schema_version": 1,
"report_id": "H6-" + re.sub(r"[-:.TZ+]", "", generated_at)[:14],
"harness": "H6",
"title": "H6 · AgentOps Report",
"description": "Evidence-backed runtime, token, cost, failure, retry and alert telemetry.",
"generated_at": generated_at,
"scope": {"project": project, "from": normalized_from, "to": normalized_to, "run": run},
"verdict": verdict,
"verdict_reasons": [item["code"] for item in findings],
"freshness": {"status": "missing" if not primary["present"] else "stale" if primary["stale"] else "live", "stale_after_s": STALE_AFTER_S, "primary_age_s": primary["age_s"], "sources": evidence},
"summary": {
"runs": len(metrics), "success": success, "failed": failed, "degraded": degraded,
"failure_rate_pct": failure_rate, "retries": int(total(metrics, "retry_count")),
"latency_ms": {
"average": round(sum(latencies) / len(latencies)) if latencies else 0,
"p50": _percentile(latencies, 50), "p95": p95,
"p99": _percentile(latencies, 99), "max": max(latencies) if latencies else 0,
},
"tokens": {
"input": total(metrics, "input_tokens") if any(_number(row.get("input_tokens")) is not None for row in metrics) else None,
"output": total(metrics, "output_tokens") if any(_number(row.get("output_tokens")) is not None for row in metrics) else None,
"total": total(metrics, "total_tokens") if any(_number(row.get("total_tokens")) is not None for row in metrics) else None,
"provider_total": total(provider, "total_tokens") if any(_number(row.get("total_tokens")) is not None for row in provider) else None,
},
"cost_usd": {
"provider_actual": round(total(provider, "cost_usd"), 6) if any(_number(row.get("cost_usd")) is not None for row in provider) else None,
"estimated": round(total(metrics, "cost_estimate"), 6) if any(_number(row.get("cost_estimate")) is not None for row in metrics) else None,
},
"provider_calls": len(provider), "alerts": alert_count,
"coverage": {
"runtime_records": len(metrics), "token_records": token_records, "cost_records": cost_records,
"token_pct": token_pct, "cost_pct": cost_pct,
"quality": {name: quality.get(name, 0) for name in ("complete", "partial", "insufficient", "unknown")},
},
},
"thresholds": {"failure_rate_pct": failure_threshold, "p95_latency_ms": p95_threshold, "freshness_age_s": STALE_AFTER_S},
"findings": findings,
"evidence_sources": evidence,
"data_quality": {"status": "insufficient" if not primary["present"] or (metrics and not token_records and not cost_records) else "partial" if warnings else "complete", "warnings": warnings},
"available_filters": {
"projects": sorted({_row_project(row) for row in all_metrics + all_provider + all_alerts}),
"runs": sorted({_row_run(row) for row in all_metrics + all_provider + all_alerts} - {"unattributed"}),
},
"details": {
"by_status": [{"status": key, "count": value} for key, value in status_counts.most_common()],
"by_step": _grouped(metrics, lambda row: _text(row.get("step"), "unknown-step")),
"by_provider": _grouped(provider, lambda row: f"{_text(row.get('provider'), 'unknown-provider')} · {_text(row.get('model'), 'unknown-model')}"),
"by_cost_source": [{"source": row["key"], "records": row["runs"], "cost_usd": row["cost_usd"] or 0} for row in _grouped(metrics, lambda row: _text(row.get("cost_source"), "unknown"))],
"by_alert": [{"alert": key, "count": value} for key, value in alert_counter.most_common()],
"recent_runs": [sanitize(row) for row in reversed(metrics[-limit:])],
"recent_alerts": [sanitize(row) for row in reversed(alerts[-limit:])],
},
}
def report_html(report: dict[str, Any], kind: str) -> str:
"""Render a self-contained, print-ready evidence dossier."""
title = report.get("title") or (
f"Run assurance · {report.get('trace_id', 'unknown')}"
)
verdict = _text(report.get("verdict"), "unknown").replace("_", " ")
if kind == "h6":
summary = report.get("summary", {})
metrics = [
("Governed runs", summary.get("runs")),
("Failure rate", f"{summary.get('failure_rate_pct', 0)}%"),
("P95 latency", f"{summary.get('latency_ms', {}).get('p95', 0)} ms"),
("Data quality", report.get("data_quality", {}).get("status")),
]
findings = report.get("findings", [])
else:
summary = report.get("summary", {})
metrics = [
("Evidence gates", f"{summary.get('gates_observed', 0)}/7"),
("Tool calls", summary.get("tool_calls")),
("Failures", summary.get("failures")),
("H6 quality", summary.get("telemetry_quality")),
]
findings = [
{"code": gate.get("title"), "severity": gate.get("status"), "message": gate.get("reason")}
for gate in report.get("gates", [])
]
cards = "".join(
f'<div class="metric"><span>{escape(str(label))}</span><strong>{escape(str(value if value is not None else "Unavailable"))}</strong></div>'
for label, value in metrics
)
rows = "".join(
'<article class="finding"><div><code>%s</code><em>%s</em></div><p>%s</p></article>' % (
escape(str(item.get("code", "EVIDENCE"))),
escape(str(item.get("severity", "info"))),
escape(str(item.get("message", "No explanation emitted."))),
) for item in findings
) or '<div class="clear">No threshold breach was detected.</div>'
contract = escape(json.dumps(report, ensure_ascii=False, indent=2))
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{escape(str(title))}</title>
<style>
:root{{--ink:#101828;--muted:#667085;--line:#e4e7ec;--paper:#f7f8fa;--navy:#101828;--cyan:#06b6d4;--green:#067647;--amber:#b54708}}
*{{box-sizing:border-box}}body{{margin:0;background:var(--paper);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}}
main{{max-width:1060px;margin:auto;padding:44px 24px 72px}}header{{border-radius:28px;background:var(--navy);color:white;padding:38px;box-shadow:0 24px 70px #10182820}}
.eyebrow,dt,.metric span{{font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}}.eyebrow{{color:#67e8f9}}h1{{max-width:760px;margin:12px 0 14px;font-size:42px;line-height:1.05;letter-spacing:-.04em}}
.meta{{display:flex;flex-wrap:wrap;gap:12px;color:#98a2b3;font:12px ui-monospace,monospace}}.badge{{display:inline-block;margin-top:20px;border:1px solid #344054;border-radius:999px;padding:8px 12px;font-size:11px;font-weight:800;text-transform:uppercase}}
.grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}}.metric,.panel{{border:1px solid var(--line);border-radius:18px;background:white;padding:20px;box-shadow:0 10px 30px #10182808}}
.metric span{{display:block;color:var(--muted)}}.metric strong{{display:block;margin-top:16px;font-size:24px;letter-spacing:-.03em}}.panel{{margin-top:18px;padding:26px}}h2{{margin:0 0 6px;font-size:20px}}.sub{{margin:0 0 20px;color:var(--muted);font-size:14px}}
.finding{{border-left:4px solid var(--cyan);border-radius:10px;background:#f8fafc;padding:14px 16px;margin-top:10px}}.finding div{{display:flex;justify-content:space-between;gap:16px}}code{{font-weight:800}}em{{color:var(--muted);font-size:11px;text-transform:uppercase}}.finding p{{margin:8px 0 0;color:#344054;font-size:14px;line-height:1.55}}.clear{{border-radius:12px;background:#ecfdf3;color:var(--green);padding:16px}}
details{{margin-top:18px}}summary{{cursor:pointer;font-weight:700}}pre{{overflow:auto;max-height:720px;border-radius:14px;background:#101828;color:#d0d5dd;padding:20px;font:11px/1.6 ui-monospace,monospace}}
footer{{margin-top:22px;color:var(--muted);font-size:11px}}@media(max-width:760px){{h1{{font-size:32px}}.grid{{grid-template-columns:1fr 1fr}}header{{padding:28px}}}}@media print{{body{{background:white}}main{{max-width:none;padding:0}}header,.panel,.metric{{box-shadow:none}}details{{break-before:page}}}}
</style></head><body><main><header><div class="eyebrow">CASAN · Independent assurance dossier</div><h1>{escape(str(title))}</h1>
<div class="meta"><span>{escape(str(report.get("report_id", "")))}</span><span>Generated {escape(str(report.get("generated_at", "")))}</span></div><div class="badge">Verdict · {escape(verdict)}</div></header>
<section class="grid">{cards}</section><section class="panel"><h2>Decision evidence</h2><p class="sub">Threshold findings and control outcomes from canonical local evidence.</p>{rows}</section>
<section class="panel"><h2>Machine-auditable contract</h2><p class="sub">The complete sanitized report payload used to render this dossier.</p><details><summary>Open JSON evidence</summary><pre>{contract}</pre></details></section>
<footer>Generated on demand by CASAN Core Local Assurance Viewer · read-only · offline-capable</footer></main></body></html>"""
@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""Loopback-only HTTP server for the CASAN Core Local Assurance Viewer."""
from __future__ import annotations
import argparse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import mimetypes
import os
from pathlib import Path
import signal
import threading
from urllib.parse import parse_qs, unquote, urlparse
from local_report import (
SAFE_ID,
h6_report,
project_profile,
report_html,
run_report,
runs,
trace_graph,
)
ASSET_ROOT = Path(__file__).resolve().parents[2] / "assets" / "local-viewer"
MAX_RESPONSE_BYTES = 24 * 1024 * 1024
def atomic_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
temporary.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
try:
os.chmod(temporary, 0o600)
except OSError:
if os.name != "nt":
raise
os.replace(temporary, path)
class ViewerServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = False
def __init__(self, address, root: Path, token: str):
self.root = root.resolve()
self.token = token
super().__init__(address, ViewerHandler)
class ViewerHandler(BaseHTTPRequestHandler):
server_version = "CASAN-Core-Viewer/1"
sys_version = ""
@property
def viewer(self) -> ViewerServer:
return self.server # type: ignore[return-value]
def log_message(self, fmt: str, *args) -> None:
# Never persist the session token from the initial/query download URL.
print(
"%s - - [%s] %s %s" % (
self.client_address[0],
self.log_date_time_string(),
self.command,
urlparse(self.path).path,
),
flush=True,
)
def _host_allowed(self) -> bool:
host = self.headers.get("Host", "").split(":", 1)[0].strip("[]").lower()
return host in {"127.0.0.1", "localhost", "::1"}
def _authorized(self, query: dict[str, list[str]]) -> bool:
supplied = self.headers.get("X-CASAN-Viewer-Token", "")
if not supplied:
supplied = query.get("token", [""])[0]
return bool(supplied) and supplied == self.viewer.token
def _headers(self, status: int, content_type: str, length: int) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(length))
self.send_header("Cache-Control", "no-store")
self.send_header("Pragma", "no-cache")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Cross-Origin-Resource-Policy", "same-origin")
self.send_header(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; "
"img-src 'self' data:; connect-src 'self'; object-src 'none'; "
"base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
)
self.end_headers()
def _send(self, status: int, body: bytes, content_type: str) -> None:
if len(body) > MAX_RESPONSE_BYTES:
self._json(413, {"error": "CASAN_VIEW_RESPONSE_TOO_LARGE"})
return
self._headers(status, content_type, len(body))
if self.command != "HEAD":
self.wfile.write(body)
def _json(self, status: int, payload) -> None:
self._send(
status,
(json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8"),
"application/json; charset=utf-8",
)
def _asset(self, name: str) -> None:
safe_name = "index.html" if name in {"", "/"} else name.lstrip("/")
if safe_name not in {"index.html", "app.css", "app.js", "favicon.svg"}:
self._json(404, {"error": "CASAN_VIEW_NOT_FOUND"})
return
path = ASSET_ROOT / safe_name
try:
body = path.read_bytes()
except OSError:
self._json(503, {"error": "CASAN_VIEW_ASSET_MISSING"})
return
content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
if content_type.startswith("text/") or content_type == "application/javascript":
content_type += "; charset=utf-8"
self._send(200, body, content_type)
def _download(self, report: dict, fmt: str, filename: str) -> None:
if fmt == "json":
body = (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
content_type = "application/json; charset=utf-8"
elif fmt == "html":
body = report_html(report, "h6" if report.get("harness") == "H6" else "run").encode("utf-8")
content_type = "text/html; charset=utf-8"
else:
self._json(400, {"error": "CASAN_REPORT_FORMAT_INVALID"})
return
if len(body) > MAX_RESPONSE_BYTES:
self._json(413, {"error": "CASAN_REPORT_TOO_LARGE"})
return
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Disposition", f'attachment; filename="{filename}.{fmt}"')
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Referrer-Policy", "no-referrer")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def do_HEAD(self) -> None:
self.do_GET()
def do_GET(self) -> None:
if self.client_address[0] not in {"127.0.0.1", "::1"} or not self._host_allowed():
self._json(403, {"error": "CASAN_VIEW_LOOPBACK_ONLY"})
return
parsed = urlparse(self.path)
path = unquote(parsed.path)
query = parse_qs(parsed.query, keep_blank_values=False)
if path in {"/", "/index.html", "/app.css", "/app.js"}:
self._asset(path)
return
if not self._authorized(query):
self._json(401, {"error": "CASAN_VIEW_TOKEN_REQUIRED"})
return
try:
if path == "/healthz":
self._json(200, {
"status": "ok",
"mode": "local_core",
"project_id": project_profile(self.viewer.root)["project_id"],
"pid": os.getpid(),
})
elif path == "/api/v1/project":
self._json(200, project_profile(self.viewer.root))
elif path == "/api/v1/runs":
self._json(200, runs(self.viewer.root, int(query.get("limit", ["100"])[0])))
elif path == "/api/v1/reports/h6":
report = self._h6(query)
self._json(200, report)
elif path == "/api/v1/reports/h6/export":
report = self._h6(query)
self._download(report, query.get("format", ["html"])[0], report["report_id"])
elif path == "/__shutdown":
self._json(200, {"status": "stopping"})
threading.Thread(target=self.viewer.shutdown, daemon=True).start()
elif path.startswith("/api/v1/runs/") and path.endswith("/graph"):
trace_id = path[len("/api/v1/runs/"):-len("/graph")].strip("/")
self._trace_required(trace_id)
self._json(200, trace_graph(self.viewer.root, trace_id))
elif path.startswith("/api/v1/reports/run/"):
suffix = path[len("/api/v1/reports/run/"):].strip("/")
exporting = suffix.endswith("/export")
trace_id = suffix[:-len("/export")].strip("/") if exporting else suffix
self._trace_required(trace_id)
report = run_report(self.viewer.root, trace_id)
if report["verdict"] == "not_found":
self._json(404, {"error": "CASAN_RUN_NOT_FOUND"})
elif exporting:
self._download(
report,
query.get("format", ["html"])[0],
report["report_id"],
)
else:
self._json(200, report)
else:
self._json(404, {"error": "CASAN_VIEW_NOT_FOUND"})
except (TypeError, ValueError):
self._json(400, {"error": "CASAN_VIEW_INVALID_QUERY"})
except BrokenPipeError:
return
except Exception as error: # fail closed without leaking project paths
self.log_error("request failed: %s", type(error).__name__)
self._json(500, {"error": "CASAN_VIEW_INTERNAL_ERROR"})
def _trace_required(self, trace_id: str) -> None:
if not SAFE_ID.fullmatch(trace_id):
raise ValueError("unsafe trace id")
def _h6(self, query: dict[str, list[str]]) -> dict:
return h6_report(
self.viewer.root,
project=query.get("project", [None])[0],
run=query.get("run", [None])[0],
from_date=query.get("from", [None])[0],
to_date=query.get("to", [None])[0],
limit=int(query.get("limit", ["50"])[0]),
)
def do_POST(self) -> None:
self._json(405, {"error": "CASAN_VIEW_READ_ONLY"})
do_PUT = do_POST
do_PATCH = do_POST
do_DELETE = do_POST
def serve(root: Path, port: int, token: str, state_file: Path) -> int:
if not token or len(token) < 32:
raise ValueError("viewer token is too short")
server = ViewerServer(("127.0.0.1", port), root, token)
actual_port = server.server_address[1]
atomic_json(state_file, {
"schema_version": 1,
"pid": os.getpid(),
"port": actual_port,
"token": token,
"project_root": str(root.resolve()),
"started_at": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc
).isoformat().replace("+00:00", "Z"),
})
def stop(_signum, _frame):
threading.Thread(target=server.shutdown, daemon=True).start()
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
try:
server.serve_forever(poll_interval=0.25)
finally:
server.server_close()
try:
current = json.loads(state_file.read_text(encoding="utf-8"))
if current.get("pid") == os.getpid():
state_file.unlink()
except (OSError, ValueError):
pass
return 0
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", required=True)
parser.add_argument("--port", type=int, default=0)
parser.add_argument("--token", required=True)
parser.add_argument("--state-file", required=True)
args = parser.parse_args(argv)
return serve(
Path(args.root).expanduser().resolve(),
max(0, min(args.port, 65535)),
args.token,
Path(args.state_file).expanduser().resolve(),
)
if __name__ == "__main__":
raise SystemExit(main())
+303 -102
View File
@@ -1,102 +1,183 @@
#!/usr/bin/env python3
"""Read-only CASAN report discovery for Core installations.
Core owns the trace/evidence source of truth but not the Control Plane UI. This
helper exposes the latest materialized receipt and opens an enrolled dashboard
without generating HTML on the prompt hot path.
"""
"""CASAN Core report, export and Local Assurance Viewer commands."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import re
import secrets
import subprocess
import sys
import time
from urllib.error import URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
import webbrowser
from urllib.parse import quote
from local_report import (
SAFE_ID,
h6_report,
read_json,
report_html,
run_report,
)
def find_root(start):
current = os.path.abspath(start)
while current != os.path.dirname(current):
if os.path.isfile(os.path.join(current, ".casan", "config.json")):
return current
current = os.path.dirname(current)
return os.path.abspath(start)
def find_root(start: str) -> Path:
current = Path(start).expanduser().resolve()
for candidate in (current, *current.parents):
if (candidate / ".casan" / "config.json").is_file():
return candidate
return current
def read_json(path):
def latest_receipt(root: Path) -> dict:
return read_json(root / ".specify" / "state" / "latest-run.json")
def require_trace(value, receipt) -> str:
trace_id = str(value or receipt.get("trace_id") or "")
if not SAFE_ID.fullmatch(trace_id):
raise ValueError("no safe trace id was supplied and no latest run exists")
return trace_id
def viewer_state_path(root: Path) -> Path:
return root / ".specify" / "state" / "local-viewer.json"
def viewer_log_path(root: Path) -> Path:
return root / ".specify" / "state" / "local-viewer.log"
def load_viewer_state(root: Path) -> dict:
state = read_json(viewer_state_path(root))
if state.get("project_root") != str(root.resolve()):
return {}
if not isinstance(state.get("port"), int) or not isinstance(state.get("token"), str):
return {}
return state
def health(state: dict, timeout: float = 0.7) -> dict:
if not state:
return {}
request = Request(
f"http://127.0.0.1:{state['port']}/healthz",
headers={"X-CASAN-Viewer-Token": state["token"]},
)
try:
with open(path, "r", encoding="utf-8") as handle:
value = json.load(handle)
return value if isinstance(value, dict) else {}
except (OSError, ValueError):
with urlopen(request, timeout=timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
return payload if response.status == 200 and isinstance(payload, dict) else {}
except (OSError, URLError, ValueError):
return {}
def dashboard_base(root):
configured = os.environ.get("CASAN_DASHBOARD_URL")
if not configured:
control_plane = read_json(
os.path.join(root, ".casan", "config.json")
).get("control_plane")
if isinstance(control_plane, dict):
configured = control_plane.get("dashboard_url")
base = str(configured or "").strip().rstrip("/")
return base if re.match(r"^https?://[a-zA-Z0-9]", base) else None
def ensure_viewer(root: Path, port: int = 0) -> dict:
existing = load_viewer_state(root)
if health(existing):
return existing
state_path = viewer_state_path(root)
state_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = state_path.with_suffix(".lock")
acquired = False
for _attempt in range(70):
try:
lock_path.mkdir()
acquired = True
break
except FileExistsError:
state = load_viewer_state(root)
if health(state):
return state
time.sleep(0.1)
if not acquired:
try:
if time.time() - lock_path.stat().st_mtime > 15:
lock_path.rmdir()
lock_path.mkdir()
acquired = True
except OSError:
pass
if not acquired:
raise RuntimeError("another viewer start is still in progress")
try:
return _start_viewer(root, port, state_path)
finally:
try:
lock_path.rmdir()
except OSError:
pass
def latest_receipt(root):
return read_json(
os.path.join(root, ".specify", "state", "latest-run.json")
)
def _start_viewer(root: Path, port: int, state_path: Path) -> dict:
existing = load_viewer_state(root)
if health(existing):
return existing
try:
state_path.unlink()
except OSError:
pass
log_path = viewer_log_path(root)
try:
if log_path.stat().st_size > 1024 * 1024:
rotated = log_path.with_suffix(".log.1")
try:
rotated.unlink()
except OSError:
pass
os.replace(log_path, rotated)
except OSError:
pass
token = secrets.token_urlsafe(32)
server = Path(__file__).with_name("local_viewer.py")
with log_path.open("ab", buffering=0) as log:
popen_options = {
"cwd": str(root),
"stdin": subprocess.DEVNULL,
"stdout": log,
"stderr": subprocess.STDOUT,
"close_fds": True,
}
if os.name == "nt":
popen_options["creationflags"] = (
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
| getattr(subprocess, "DETACHED_PROCESS", 0)
)
else:
popen_options["start_new_session"] = True
subprocess.Popen(
[
sys.executable, str(server),
"--root", str(root),
"--port", str(port),
"--token", token,
"--state-file", str(state_path),
],
**popen_options,
)
for _attempt in range(50):
time.sleep(0.1)
state = load_viewer_state(root)
if state.get("token") == token and health(state):
return state
raise RuntimeError(f"viewer did not become ready; inspect {log_path}")
def require_trace(value, receipt):
trace_id = value or receipt.get("trace_id")
if not trace_id or not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$", str(trace_id)):
raise ValueError("no safe trace id was supplied and no latest run exists")
return str(trace_id)
def viewer_url(state: dict, trace_id: str | None = None, view: str | None = None) -> str:
query = {"token": state["token"]}
if trace_id:
query.update({"view": "runs", "trace": trace_id})
elif view:
query["view"] = view
return f"http://127.0.0.1:{state['port']}/?{urlencode(query)}"
def report_url(root, trace_id):
base = dashboard_base(root)
if not base:
return None
return "%s/runs?trace=%s" % (base, quote(trace_id, safe=""))
def export_url(root, trace_id, export_format):
base = dashboard_base(root)
if not base:
return None
return "%s/api/v1/reports/run/%s/export?format=%s" % (
base,
quote(trace_id, safe=""),
export_format,
)
def print_receipt(receipt):
if not receipt:
print("CASAN_REPORT_NONE — no finalized prompt receipt exists yet")
return 1
print("CASAN %s" % ("CERTIFIED" if receipt.get("certified") else "NON-CERTIFIED"))
print("trace_id=%s" % (receipt.get("trace_id") or "unknown"))
print("project_id=%s" % (receipt.get("project_id") or "unknown"))
print("h6_quality=%s" % (receipt.get("telemetry_quality") or "unknown"))
print("duration_ms=%s tool_calls=%s failures=%s" % (
receipt.get("duration_ms"),
receipt.get("tool_calls", 0),
receipt.get("failures", 0),
))
if receipt.get("report_url"):
print("report_url=%s" % receipt["report_url"])
return 0
def open_or_print(url, no_open):
def open_or_print(url: str, no_open: bool) -> int:
print(url)
if no_open:
return 0
@@ -106,52 +187,172 @@ def open_or_print(url, no_open):
return 0
def main(argv=None):
parser = argparse.ArgumentParser(description="CASAN Core report discovery")
parser.add_argument("--root", default=os.environ.get("CASAN_APP_ROOT") or os.getcwd())
commands = parser.add_subparsers(dest="command", required=True)
def print_receipt(receipt: dict, root: Path) -> int:
if not receipt:
print("CASAN_REPORT_NONE — no finalized prompt receipt exists yet")
print("next=complete a prompt, then run `casan view`")
return 1
certified = receipt.get("certified") is True
print("CASAN %s" % ("CERTIFIED" if certified else "NON-CERTIFIED"))
print("trace_id=%s" % (receipt.get("trace_id") or "unknown"))
print("project_id=%s" % (receipt.get("project_id") or "unknown"))
print("h6_quality=%s" % (receipt.get("telemetry_quality") or "unknown"))
print("duration_ms=%s tool_calls=%s failures=%s" % (
receipt.get("duration_ms"),
receipt.get("tool_calls", 0),
receipt.get("failures", 0),
))
print("view_command=casan view %s" % (receipt.get("trace_id") or ""))
active_viewer = load_viewer_state(root)
if health(active_viewer):
print("local_report_url=%s" % viewer_url(
active_viewer, str(receipt.get("trace_id") or "") or None))
if receipt.get("report_url"):
print("central_report_url=%s" % receipt["report_url"])
return 0
def export_report(root: Path, args, receipt: dict) -> int:
if args.h6:
report = h6_report(
root,
project=args.project,
run=args.trace_id,
from_date=args.from_date,
to_date=args.to_date,
limit=args.limit,
)
stem = report["report_id"].lower()
kind = "h6"
else:
trace_id = require_trace(args.trace_id, receipt)
report = run_report(root, trace_id)
if report["verdict"] == "not_found":
print("CASAN_REPORT_NOT_FOUND — no evidence exists for that trace", file=sys.stderr)
return 2
stem = re.sub(r"[^a-zA-Z0-9._-]", "-", report["report_id"]).lower()
kind = "run"
destination = Path(args.output or f"{stem}.{args.format}").expanduser()
if not destination.is_absolute():
destination = Path.cwd() / destination
destination.parent.mkdir(parents=True, exist_ok=True)
if args.format == "json":
body = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
else:
body = report_html(report, kind)
destination.write_text(body, encoding="utf-8")
print(str(destination.resolve()))
return 0
def viewer_command(root: Path, action: str, port: int, no_open: bool) -> int:
if action in {"start", "open"}:
try:
state = ensure_viewer(root, port)
except RuntimeError as error:
print(f"CASAN_VIEW_START_FAILED — {error}", file=sys.stderr)
return 3
return open_or_print(viewer_url(state), no_open or action == "start")
state = load_viewer_state(root)
status = health(state)
if action == "status":
if not status:
print("CASAN_VIEW_STOPPED")
return 1
print(json.dumps({
"status": "running",
"pid": state.get("pid"),
"url": viewer_url(state),
"project_id": status.get("project_id"),
"mode": "local_core",
}, indent=2))
return 0
if action == "stop":
if not status:
try:
viewer_state_path(root).unlink()
except OSError:
pass
print("CASAN_VIEW_ALREADY_STOPPED")
return 0
request = Request(
f"http://127.0.0.1:{state['port']}/__shutdown",
headers={"X-CASAN-Viewer-Token": state["token"]},
)
try:
urlopen(request, timeout=1).read()
except (OSError, URLError):
print("CASAN_VIEW_STOP_FAILED", file=sys.stderr)
return 3
for _attempt in range(30):
time.sleep(0.1)
if not health(state, timeout=0.1):
print("CASAN_VIEW_STOPPED")
return 0
print("CASAN_VIEW_STOP_PENDING")
return 0
return 64
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser(description=__doc__)
value.add_argument("--root", default=os.environ.get("CASAN_APP_ROOT") or os.getcwd())
commands = value.add_subparsers(dest="command", required=True)
latest = commands.add_parser("latest", help="show the latest finalized prompt receipt")
latest.add_argument("--json", action="store_true")
view = commands.add_parser("view", help="open a trace in the enrolled Control Plane")
view = commands.add_parser("view", help="open a trace in the local Core viewer")
view.add_argument("trace_id", nargs="?")
view.add_argument("--no-open", action="store_true", help="print the URL without opening a browser")
view.add_argument("--no-open", action="store_true")
view.add_argument("--port", type=int, default=0)
export = commands.add_parser("export", help="download a trace evidence snapshot on demand")
export = commands.add_parser("export", help="write a self-contained report on demand")
export.add_argument("trace_id", nargs="?")
export.add_argument("--h6", action="store_true", help="export the H6 dossier instead of a run dossier")
export.add_argument("--format", choices=["html", "json"], default="html")
export.add_argument("--no-open", action="store_true", help="print the URL without opening a browser")
export.add_argument("--output", "-o")
export.add_argument("--project")
export.add_argument("--from", dest="from_date")
export.add_argument("--to", dest="to_date")
export.add_argument("--limit", type=int, default=50)
args = parser.parse_args(argv)
viewer = commands.add_parser("viewer", help="manage the local Core viewer")
viewer.add_argument("action", choices=["start", "status", "stop", "open"])
viewer.add_argument("--port", type=int, default=0)
viewer.add_argument("--no-open", action="store_true")
return value
def main(argv=None) -> int:
args = parser().parse_args(argv)
root = find_root(args.root)
receipt = latest_receipt(root)
if args.command == "latest":
if args.json:
print(json.dumps(receipt, ensure_ascii=False, indent=2))
return 0 if receipt else 1
return print_receipt(receipt)
return print_receipt(receipt, root)
if args.command == "viewer":
return viewer_command(root, args.action, args.port, args.no_open)
if args.command == "export":
try:
return export_report(root, args, receipt)
except ValueError as error:
print(f"CASAN_REPORT_INVALID — {error}", file=sys.stderr)
return 2
try:
trace_id = require_trace(args.trace_id, receipt)
if args.trace_id and not SAFE_ID.fullmatch(str(args.trace_id)):
raise ValueError("unsafe trace id")
trace_id = str(args.trace_id or receipt.get("trace_id") or "") or None
state = ensure_viewer(root, args.port)
except ValueError as error:
print("CASAN_REPORT_NOT_FOUND — %s" % error, file=sys.stderr)
print(f"CASAN_REPORT_NOT_FOUND — {error}", file=sys.stderr)
return 2
url = (
report_url(root, trace_id)
if args.command == "view"
else export_url(root, trace_id, args.format)
)
if not url:
print(
"CASAN_CONTROL_PLANE_NOT_ENROLLED — run `casan init "
"--dashboard-url https://your-casan.example` or set CASAN_DASHBOARD_URL",
file=sys.stderr,
)
except RuntimeError as error:
print(f"CASAN_VIEW_START_FAILED — {error}", file=sys.stderr)
return 3
return open_or_print(url, args.no_open)
return open_or_print(viewer_url(state, trace_id), args.no_open)
if __name__ == "__main__":
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -euo pipefail
# Proves that the release artifact—not the source checkout—contains a complete,
# offline, read-only Local Assurance Viewer.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
WORK="$(mktemp -d)"
DIST="$WORK/dist"
PROJECT="$WORK/clean-project"
VIEWER_STARTED=0
cleanup() {
if [[ "$VIEWER_STARTED" == "1" && -n "${CASAN_BIN:-}" ]]; then
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard stop >/dev/null 2>&1 || true
fi
rm -rf "$WORK"
}
trap cleanup EXIT
CASAN_DIST_DIR="$DIST" bash "$ROOT/scripts/package-release.sh" core >/dev/null
ARTIFACT="$DIST/casan-core-v$(cat "$ROOT/VERSION").tar.gz"
tar -xzf "$ARTIFACT" -C "$WORK"
BUNDLE="$WORK/casan-core-v$(cat "$ROOT/VERSION")"
CASAN_BIN="$BUNDLE/bin/casan"
test -f "$BUNDLE/packages/casan-harness/assets/local-viewer/index.html"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_report.py"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_viewer.py"
test ! -e "$BUNDLE/packages/casan-control-panel"
test ! -e "$BUNDLE/package.json"
mkdir -p \
"$PROJECT/.casan" \
"$PROJECT/.specify/state" \
"$PROJECT/.specify/logs/trace" \
"$PROJECT/.specify/logs/trace-events" \
"$PROJECT/.specify/logs/cost"
python3 - "$PROJECT" <<'PY'
import json
from pathlib import Path
import sys
root = Path(sys.argv[1])
(root / ".casan/config.json").write_text(json.dumps({
"schema_version": "21.3",
"project_id": "artifact-project",
"project_name": "Artifact Project",
"edition": "core",
"enforcement_mode": "enforce",
}), encoding="utf-8")
trace_id = "artifact-run-1"
trace = {
"schema_version": "20.1",
"trace_id": trace_id,
"project_id": "artifact-project",
"certified": True,
"certification_strength": "project_hook",
"finalized_at": "2026-07-28T10:00:07Z",
"tool_calls": 1,
"failures": 0,
"evidence": [
{"h": f"H{i}", "at": f"2026-07-28T10:00:0{i}Z", "decision": "pass", "kind": "gate", "detail": f"H{i} passed"}
for i in range(1, 8)
],
}
(root / f".specify/logs/trace/agentic-{trace_id}.json").write_text(
json.dumps(trace), encoding="utf-8")
metric = {
"timestamp": "2026-07-28T10:00:06Z",
"trace_id": trace_id,
"project_id": "artifact-project",
"harness": "H6-agentic",
"step": "agentic-turn",
"status": "success",
"latency_ms": 812,
"retry_count": 0,
"input_tokens": None,
"output_tokens": None,
"total_tokens": None,
"cost_estimate": None,
"telemetry_quality": "insufficient",
"alerts": [],
}
(root / ".specify/logs/cost/metrics.jsonl").write_text(
json.dumps(metric) + "\n", encoding="utf-8")
(root / ".specify/state/latest-run.json").write_text(json.dumps({
"schema_version": 1,
"trace_id": trace_id,
"project_id": "artifact-project",
"certified": True,
"telemetry_quality": "insufficient",
"duration_ms": 812,
"tool_calls": 1,
"failures": 0,
"finalized_at": "2026-07-28T10:00:07Z",
}), encoding="utf-8")
PY
(
cd "$PROJECT"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report latest | grep -q "view_command=casan view artifact-run-1"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export artifact-run-1 \
--format html --output "$WORK/run.html" >/dev/null
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export --h6 \
--format json --output "$WORK/h6.json" >/dev/null
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export --h6 \
--format html --output "$WORK/h6.html" >/dev/null
)
grep -q "Independent assurance dossier" "$WORK/run.html"
grep -q "H6 · AgentOps Report" "$WORK/h6.html"
python3 - "$WORK/h6.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["harness"] == "H6"
assert d["summary"]["runs"] == 1
assert d["summary"]["tokens"]["total"] is None
assert d["summary"]["cost_usd"]["estimated"] is None
PY
URL="$(CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" view artifact-run-1 --no-open)"
VIEWER_STARTED=1
python3 - "$URL" <<'PY'
import json
import sys
from urllib.error import HTTPError
from urllib.parse import parse_qs, urlparse
from urllib.request import Request, urlopen
url = urlparse(sys.argv[1])
token = parse_qs(url.query)["token"][0]
base = f"{url.scheme}://{url.netloc}"
def get(path):
request = Request(base + path, headers={"X-CASAN-Viewer-Token": token})
return json.loads(urlopen(request, timeout=3).read())
project = get("/api/v1/project")
assert project["project_id"] == "artifact-project"
assert project["viewer"] == {
"mode": "local_core",
"scope": "single_project",
"read_only": True,
"network": "loopback_only",
}
run = get("/api/v1/reports/run/artifact-run-1")
assert run["verdict"] == "certified"
assert run["summary"]["gates_observed"] == 7
h6 = get("/api/v1/reports/h6?run=artifact-run-1")
assert h6["summary"]["runs"] == 1
try:
urlopen(Request(base + "/api/v1/project"), timeout=3)
raise AssertionError("API accepted a request without the session token")
except HTTPError as error:
assert error.code == 401
try:
urlopen(Request(
base + "/api/v1/project",
method="POST",
headers={"X-CASAN-Viewer-Token": token},
), timeout=3)
raise AssertionError("read-only viewer accepted POST")
except HTTPError as error:
assert error.code == 405
try:
urlopen(Request(
base + "/api/v1/project",
headers={"X-CASAN-Viewer-Token": token, "Host": "attacker.example"},
), timeout=3)
raise AssertionError("viewer accepted a non-loopback Host header")
except HTTPError as error:
assert error.code == 403
PY
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard status | grep -q '"mode": "local_core"'
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard stop | grep -q "CASAN_VIEW_STOPPED"
VIEWER_STARTED=0
echo "PASS: Core release artifact provides offline visual reports without Platform or Node"
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Acceptance tests for the stdlib-only CASAN Core reporting boundary."""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
MODULE = Path(__file__).resolve().parents[1] / "scripts" / "python" / "local_report.py"
SPEC = importlib.util.spec_from_file_location("casan_local_report", MODULE)
REPORT = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(REPORT)
class LocalReportTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
(self.root / ".casan").mkdir()
(self.root / ".casan" / "config.json").write_text(json.dumps({
"project_id": "project-one",
"project_name": "Project One",
"edition": "core",
"enforcement_mode": "enforce",
"maturity": {"level": 4, "status": "assessed"},
}), encoding="utf-8")
for path in (
".specify/state",
".specify/logs/trace",
".specify/logs/trace-events",
".specify/logs/cost",
".specify/logs/level5",
".specify/agentops",
):
(self.root / path).mkdir(parents=True, exist_ok=True)
def tearDown(self):
self.temporary.cleanup()
def write_jsonl(self, relative, rows):
path = self.root / relative
path.write_text(
"".join(json.dumps(row, separators=(",", ":")) + "\n" for row in rows),
encoding="utf-8",
)
def write_run(self, trace_id="trace-1", quality="partial"):
trace = {
"trace_id": trace_id,
"project_id": "project-one",
"certified": True,
"certification_strength": "project_hook",
"finalized_at": "2026-07-28T10:00:07Z",
"tool_calls": 2,
"failures": 0,
"evidence": [
{"h": f"H{index}", "at": f"2026-07-28T10:00:0{index}Z", "decision": "pass", "kind": "gate", "detail": f"H{index} passed"}
for index in range(1, 8)
],
}
(self.root / f".specify/logs/trace/agentic-{trace_id}.json").write_text(
json.dumps(trace), encoding="utf-8")
metric = {
"timestamp": "2026-07-28T10:00:06Z",
"trace_id": trace_id,
"project_id": "project-one",
"step": "agentic-turn",
"status": "success",
"latency_ms": 1200,
"retry_count": 0,
"input_tokens": None,
"output_tokens": None,
"total_tokens": None,
"cost_estimate": None,
"telemetry_quality": quality,
"alerts": [],
}
self.write_jsonl(".specify/logs/cost/metrics.jsonl", [metric])
return metric
def test_core_empty_state_is_truthful(self):
registry = REPORT.runs(self.root)
h6 = REPORT.h6_report(self.root)
self.assertEqual(registry["count"], 0)
self.assertEqual(h6["verdict"], "no_data")
self.assertEqual(h6["summary"]["runs"], 0)
self.assertIsNone(h6["summary"]["tokens"]["total"])
self.assertTrue(any(item["code"] == "METRICS_MISSING" for item in h6["findings"]))
def test_run_reconstructs_all_gates_and_redacts_sensitive_fields(self):
self.write_run()
event = {
"timestamp": "2026-07-28T10:00:01Z",
"trace_id": "trace-1",
"gate_id": "H1-context",
"status": "pass",
"reason": "admitted",
"evidence": {"kind": "context", "prompt": "private customer prompt", "access_token": "secret"},
}
self.write_jsonl(".specify/logs/trace-events/trace-1.jsonl", [event])
report = REPORT.run_report(self.root, "trace-1")
self.assertEqual(report["verdict"], "certified")
self.assertEqual(report["summary"]["gates_observed"], 1)
self.assertEqual(report["gates"][0]["evidence"]["prompt"], "[redacted]")
self.assertEqual(report["gates"][0]["evidence"]["access_token"], "[redacted]")
self.assertIsNone(report["h6"]["total_tokens"])
def test_legacy_trace_reconstructs_h1_to_h7(self):
self.write_run()
graph = REPORT.trace_graph(self.root, "trace-1")
self.assertTrue(graph["terminal"])
self.assertEqual(graph["progress"], 7)
self.assertEqual([node["status"] for node in graph["nodes"]], [
"pass", "pass", "pass", "pass", "pass", "warning", "pass",
])
def test_h6_preserves_unknown_token_and_cost_as_null(self):
self.write_run()
report = REPORT.h6_report(self.root, run="trace-1")
self.assertEqual(report["summary"]["coverage"]["token_pct"], 0)
self.assertEqual(report["summary"]["coverage"]["cost_pct"], 0)
self.assertIsNone(report["summary"]["tokens"]["total"])
self.assertIsNone(report["summary"]["cost_usd"]["estimated"])
self.assertEqual(report["data_quality"]["status"], "insufficient")
self.assertIn("TELEMETRY_COVERAGE_GAP", report["verdict_reasons"])
def test_self_contained_html_escapes_evidence(self):
self.write_run()
report = REPORT.run_report(self.root, "trace-1")
report["gates"][0]["reason"] = "<script>alert(1)</script>"
html = REPORT.report_html(report, "run")
self.assertIn("<!doctype html>", html.lower())
self.assertNotIn("<script>alert(1)</script>", html)
self.assertIn("&lt;script&gt;alert(1)&lt;/script&gt;", html)
self.assertIn("Machine-auditable contract", html)
def test_unsafe_trace_id_never_reads_outside_project(self):
report = REPORT.run_report(self.root, "../../etc/passwd")
self.assertEqual(report["verdict"], "not_found")
self.assertFalse(report["source"]["trace_found"])
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -7,7 +7,7 @@
"level": 1,
"package_names": ["casan-core", "casan-harness"],
"status": "implemented",
"summary": "Minimal H1-H7 production runtime: security/action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, and lightweight CLI. Source-only tests and Platform helpers are excluded.",
"summary": "Minimal H1-H7 production runtime: security/action gates, evidence pack, audit, cost/telemetry, policy/config defaults, adapters, lightweight CLI, and a zero-dependency single-project Local Assurance Viewer with H1-H7, H6, history and on-demand export. Source-only tests and Platform helpers are excluded.",
"target_users": ["governance-harness-only projects", "BJT initial adoption", "CI gate usage"],
"includes": [
"packages/casan-harness",
@@ -43,6 +43,7 @@
"docs/packaging/DOMAIN_PACK_GUIDE.md",
"docs/packaging/DOCKER_GUIDE.md",
"docs/packaging/CASAN_PACKAGING_PLAN.md",
"docs/packaging/CORE_LOCAL_ASSURANCE_VIEWER.md",
"scripts/package-release.sh"
],
"artifact": "casan-devkit-v{VERSION}.tar.gz"
+7
View File
@@ -23,6 +23,10 @@
"config/project-registry.json": "empty_project_registry"
},
"required": [
"assets/local-viewer/index.html",
"assets/local-viewer/app.css",
"assets/local-viewer/app.js",
"assets/local-viewer/favicon.svg",
"adapters/claude-code/claude_hook.py",
"adapters/codex/codex_hook.py",
"adapters/vscode/vscode_hook.py",
@@ -33,6 +37,9 @@
"scripts/bash/security-check.sh",
"scripts/bash/tool-registry-gate.sh",
"scripts/python/agentic_bridge.py",
"scripts/python/local_report.py",
"scripts/python/local_viewer.py",
"scripts/python/report_cli.py",
"scripts/python/harness_hash.py"
]
},