mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-22 00:31:17 +00:00
* feat(ingestion): shadow-mode parity harness + static dashboard (#923, RFC #909 Ring 2 PKG) Side-car observability for the RFC #909 registry rollout. Callers that dual-run legacy-DAG + `Registry.lookup` feed their result pairs into the harness; the harness diffs each pair via shared `diffResolutions` (#918), aggregates via `aggregateDiffs`, and persists a per-language parity report that the static dashboard can render offline. ## Shipped ### `gitnexus/src/core/ingestion/shadow-harness.ts` (new) ```ts createShadowHarness(): ShadowHarness ``` API: - `enabled` — `true` iff `GITNEXUS_SHADOW_MODE` is truthy at construction. Captured once; later env-var mutations don't flip it. - `record({ language, callsite, legacy, newResult, primary })` — accumulator. No-op when `enabled === false` (near-zero overhead). - `size()` — diagnostic counter. - `snapshot(now?)` — deterministic `ShadowParityReport` from the accumulated diffs. - `persist(outputDir, now?)` — writes BOTH a timestamped `<runId>.json` and a `latest.json` pointer. Creates outputDir if absent. Returns the per-run file path. - `clear()` — resets the accumulator; preserves `enabled`. Activation: `GITNEXUS_SHADOW_MODE` accepts `'true'` / `'1'` / `'yes'` (case-insensitive, trimmed); same truthy convention as `REGISTRY_PRIMARY_<LANG>` from #924. Typos → disabled (fail-safe). Persisted payload (`PersistedShadowReport`) is schema-versioned (`v1`): ```jsonc { "schemaVersion": 1, "runId": "YYYYMMDD-HHMMSS-xxxxxxxx", "generatedAt": "ISO 8601", "primaryByLanguage": { "python": "legacy", ... }, "report": { /* ShadowParityReport from #918 aggregateDiffs */ } } ``` `runId` prefix is the timestamp so files sort chronologically; the entropy suffix prevents collisions within a clock-second. ### `gitnexus/shadow-parity-dashboard/index.html` (new) Minimal static dashboard — one HTML file, zero build step, zero runtime deps. Fetches `./latest.json` and renders: - Overall summary cards (total calls, both agree, disagree, overall parity %) - Per-language table: language tag ("primary: legacy" / "primary: registry" pill) + total / agree / only-legacy / only-new / disagree / both-empty / parity% - Parity cells colored by threshold: ≥95% green, ≥80% amber, <80% red - Light / dark via `prefers-color-scheme` - Empty-state message when no records yet File-serving is static: `cp .gitnexus/shadow-parity/latest.json gitnexus/shadow-parity-dashboard/` + open in a browser. ## Tests (14, all passing) - **Flag detection** (5): default off · truthy variants case-insensitive · falsy / typo → off · record() is no-op when disabled · env flip AFTER construction doesn't enable (constructed-once semantics) - **Record + snapshot** (4): multi-language accumulation · per-language rows with correct outcomes · snapshot determinism · `clear()` resets accumulator + `primaryByLanguage` - **Persistence** (5): mkdir-p on missing outputDir · per-run + latest.json match byte-for-byte · schema v1 payload shape · runId timestamp prefix sorts chronologically · empty report persists gracefully Tests use a per-test tmpdir (`fs.mkdtemp`), cleaned in `afterEach`, so parallel vitest runs don't collide. `GITNEXUS_SHADOW_MODE` is saved + restored per-test. ## What's deliberately NOT in this PR (call-out in harness docstring) - **Dual-run dispatch.** The harness is a side-car — it does NOT invoke either resolution path. Call-processor integration that actually runs both legacy + registry paths lands as a follow-up. Without that integration, `record()` is never called in production today. The harness is tested in isolation with synthetic inputs. - **CI artifact publishing.** Config work to upload `latest.json` + the dashboard HTML per CI run. Tracked separately; the harness + dashboard are ready when the CI job wires in. - **Fixture-level drill-down.** The issue mentions per-fixture AST snippet + evidence trace drill-down. MVP dashboard shows per-language rows only; drill-down extends the static JSON format + the dashboard JS in a focused follow-up. ## Verification - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`) - 14/14 new tests pass - Full scope-resolution / shadow / model / flag suite: **335/335 pass** ## Part of - Parent: #909 - Depends on (code): #917 (registries), #918 (diff + aggregate) - Unblocks Ring 3 language flips: the parity dashboard becomes the checkpoint before flipping `REGISTRY_PRIMARY_<LANG>=true` for a language — once per-language parity stabilizes, the flip ships. * chore: prettier format on shadow-parity-dashboard index.html
291 lines
8.1 KiB
HTML
291 lines
8.1 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>GitNexus — Shadow Parity Dashboard</title>
|
|
<!--
|
|
Static dashboard for the RFC #909 shadow-mode parity report.
|
|
|
|
Reads `latest.json` from this directory and renders a per-language
|
|
parity table. Zero build step, zero runtime dependencies — a
|
|
single file that any browser or file:// context can open.
|
|
|
|
Usage:
|
|
# from repo root, after a shadow-mode run
|
|
cp .gitnexus/shadow-parity/latest.json gitnexus/shadow-parity-dashboard/
|
|
open gitnexus/shadow-parity-dashboard/index.html
|
|
|
|
CI artifact wiring (follow-up): the CI job publishes a snapshot
|
|
of this directory + latest.json as a downloadable bundle per run.
|
|
-->
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
--fg: #1f2937;
|
|
--fg-muted: #6b7280;
|
|
--bg: #ffffff;
|
|
--bg-muted: #f9fafb;
|
|
--border: #e5e7eb;
|
|
--good: #16a34a;
|
|
--warn: #d97706;
|
|
--bad: #dc2626;
|
|
--primary-tag-legacy: #7c3aed;
|
|
--primary-tag-registry: #0ea5e9;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--fg: #e5e7eb;
|
|
--fg-muted: #9ca3af;
|
|
--bg: #111827;
|
|
--bg-muted: #1f2937;
|
|
--border: #374151;
|
|
}
|
|
}
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font:
|
|
14px/1.45 system-ui,
|
|
-apple-system,
|
|
sans-serif;
|
|
}
|
|
main {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 24px 16px;
|
|
}
|
|
h1 {
|
|
font-size: 20px;
|
|
margin: 0 0 4px;
|
|
}
|
|
.meta {
|
|
color: var(--fg-muted);
|
|
font-size: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.card {
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
background: var(--bg-muted);
|
|
}
|
|
.card .k {
|
|
color: var(--fg-muted);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.card .v {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
th,
|
|
td {
|
|
padding: 6px 10px;
|
|
text-align: right;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
th:first-child,
|
|
td:first-child {
|
|
text-align: left;
|
|
}
|
|
thead th {
|
|
font-weight: 600;
|
|
color: var(--fg-muted);
|
|
font-size: 12px;
|
|
background: var(--bg-muted);
|
|
}
|
|
tbody tr:hover {
|
|
background: var(--bg-muted);
|
|
}
|
|
.parity {
|
|
font-weight: 600;
|
|
}
|
|
.parity.good {
|
|
color: var(--good);
|
|
}
|
|
.parity.warn {
|
|
color: var(--warn);
|
|
}
|
|
.parity.bad {
|
|
color: var(--bad);
|
|
}
|
|
.tag {
|
|
display: inline-block;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
font-size: 10px;
|
|
margin-left: 6px;
|
|
color: white;
|
|
}
|
|
.tag.legacy {
|
|
background: var(--primary-tag-legacy);
|
|
}
|
|
.tag.registry {
|
|
background: var(--primary-tag-registry);
|
|
}
|
|
.empty {
|
|
padding: 40px;
|
|
text-align: center;
|
|
color: var(--fg-muted);
|
|
}
|
|
code {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
background: var(--bg-muted);
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Shadow Parity — RFC #909</h1>
|
|
<div class="meta" id="meta">loading <code>latest.json</code>…</div>
|
|
<div class="cards" id="cards"></div>
|
|
<table id="per-language">
|
|
<thead>
|
|
<tr>
|
|
<th>Language</th>
|
|
<th>Total</th>
|
|
<th>Agree</th>
|
|
<th>Only legacy</th>
|
|
<th>Only new</th>
|
|
<th>Disagree</th>
|
|
<th>Both empty</th>
|
|
<th>Parity</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
<div id="empty" class="empty" style="display: none">
|
|
No records yet. Enable <code>GITNEXUS_SHADOW_MODE=1</code> and run ingestion to populate.
|
|
</div>
|
|
</main>
|
|
<script>
|
|
/* global fetch, document */
|
|
(async function () {
|
|
const tbody = document.querySelector('#per-language tbody');
|
|
const cards = document.getElementById('cards');
|
|
const meta = document.getElementById('meta');
|
|
const empty = document.getElementById('empty');
|
|
const table = document.getElementById('per-language');
|
|
|
|
let payload;
|
|
try {
|
|
const r = await fetch('./latest.json', { cache: 'no-store' });
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
payload = await r.json();
|
|
} catch (err) {
|
|
meta.textContent = 'Failed to load latest.json: ' + err.message;
|
|
table.style.display = 'none';
|
|
empty.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const primary = payload.primaryByLanguage || {};
|
|
const report = payload.report || {};
|
|
const perLang = report.perLanguage || [];
|
|
const overall = report.overall || {};
|
|
|
|
meta.textContent =
|
|
'Run ' +
|
|
payload.runId +
|
|
' — generated ' +
|
|
payload.generatedAt +
|
|
' (schema v' +
|
|
payload.schemaVersion +
|
|
')';
|
|
|
|
// Overall summary cards.
|
|
cards.innerHTML = '';
|
|
const overallParity = overall.parity !== undefined ? overall.parity : 0;
|
|
cards.appendChild(makeCard('Total calls', overall.totalCalls ?? 0));
|
|
cards.appendChild(makeCard('Both agree', overall.bothAgree ?? 0));
|
|
cards.appendChild(makeCard('Disagree', overall.bothDisagree ?? 0));
|
|
cards.appendChild(makeCard('Overall parity', formatPct(overallParity)));
|
|
|
|
if (!perLang.length) {
|
|
table.style.display = 'none';
|
|
empty.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
for (const row of perLang) {
|
|
const tr = document.createElement('tr');
|
|
const primaryTag = primary[row.language];
|
|
const tag = primaryTag
|
|
? '<span class="tag ' + primaryTag + '">primary: ' + primaryTag + '</span>'
|
|
: '';
|
|
const parityClass = parityClassFor(row.parity);
|
|
tr.innerHTML =
|
|
'<td>' +
|
|
escape(row.language) +
|
|
tag +
|
|
'</td>' +
|
|
'<td>' +
|
|
row.totalCalls +
|
|
'</td>' +
|
|
'<td>' +
|
|
row.bothAgree +
|
|
'</td>' +
|
|
'<td>' +
|
|
row.onlyLegacy +
|
|
'</td>' +
|
|
'<td>' +
|
|
row.onlyNew +
|
|
'</td>' +
|
|
'<td>' +
|
|
row.bothDisagree +
|
|
'</td>' +
|
|
'<td>' +
|
|
row.bothEmpty +
|
|
'</td>' +
|
|
'<td class="parity ' +
|
|
parityClass +
|
|
'">' +
|
|
formatPct(row.parity) +
|
|
'</td>';
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
function makeCard(k, v) {
|
|
const div = document.createElement('div');
|
|
div.className = 'card';
|
|
div.innerHTML =
|
|
'<div class="k">' + escape(k) + '</div><div class="v">' + escape(String(v)) + '</div>';
|
|
return div;
|
|
}
|
|
function formatPct(x) {
|
|
if (typeof x !== 'number' || !isFinite(x)) return '—';
|
|
return (x * 100).toFixed(1) + '%';
|
|
}
|
|
function parityClassFor(x) {
|
|
if (typeof x !== 'number') return '';
|
|
if (x >= 0.95) return 'good';
|
|
if (x >= 0.8) return 'warn';
|
|
return 'bad';
|
|
}
|
|
function escape(s) {
|
|
return String(s).replace(/[&<>"']/g, function (c) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|