Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: 2

updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: tuesday
time: '08:00'
timezone: Europe/Riga
open-pull-requests-limit: 3
groups:
mozaik:
patterns:
- '@mozaik-ai/*'
development-tooling:
dependency-type: development
patterns:
- '*'
commit-message:
prefix: 'deps'

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: tuesday
time: '08:15'
timezone: Europe/Riga
open-pull-requests-limit: 2
groups:
actions:
patterns:
- '*'
commit-message:
prefix: 'ci'
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ npm run demo -- blocked
npm run demo -- failure
```

Start the visual Control Room at `http://127.0.0.1:4173`:

```bash
npm start
```

The local UI can run all three proof cases, compare evidence lanes, and inspect
the redacted event ledger. Its HTTP server binds to localhost by default and
exposes only synthetic scenarios.

Use `--json` to inspect the redacted event timeline and the three loop IDs:

```bash
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
"build": "tsc -p tsconfig.build.json",
"check": "npm run typecheck && npm test && npm run format:check",
"demo": "tsx src/main.ts",
"dev": "tsx watch src/server.ts",
"format": "prettier --write .",
"format:check": "prettier --check .",
"start": "tsx src/server.ts",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
Expand Down
190 changes: 190 additions & 0 deletions public/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
const roles = ['build', 'policy', 'experiment'];
const roleLabels = {
build: 'Build integrity',
policy: 'Release policy',
experiment: 'Experiment safety',
};

const scenarios = document.querySelectorAll('.scenario');
const runButton = document.querySelector('#run-button');
const runLabel = document.querySelector('#run-label');
const runState = document.querySelector('#run-state');
const resultPanel = document.querySelector('#result');
let selectedFixture = 'ready';

for (const scenario of scenarios) {
scenario.addEventListener('click', () => {
if (runButton.disabled) return;
selectedFixture = scenario.dataset.fixture;
for (const candidate of scenarios) {
candidate.classList.toggle('selected', candidate === scenario);
}
});
}

runButton.addEventListener('click', async () => {
setRunning(true);
showPendingAgents();

try {
const response = await fetch('/api/runs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fixture: selectedFixture }),
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error ?? 'Evaluation failed');
renderResult(payload);
runState.textContent = 'Evaluation complete';
} catch (error) {
resultPanel.hidden = true;
runState.textContent =
error instanceof Error ? error.message : 'Evaluation failed';
} finally {
setRunning(false);
}
});

function setRunning(running) {
runButton.disabled = running;
for (const scenario of scenarios) scenario.disabled = running;
runButton.classList.toggle('running', running);
runLabel.textContent = running
? 'Agents collecting evidence'
: 'Run concurrent evaluation';
if (running) runState.textContent = 'Three loops in flight…';
}

function showPendingAgents() {
resultPanel.hidden = false;
resultPanel.className = 'result pending';
document.querySelector('#verdict').textContent = 'EVALUATING';
document.querySelector('#case-id').textContent = selectedFixture;
document.querySelector('#digest').textContent = 'binding proof case…';
document.querySelector('#reasons').hidden = true;
document.querySelector('#concurrency').textContent = 'starting loops';
document.querySelector('#event-count').textContent = 'collecting events';
document.querySelector('#timeline').replaceChildren();

const cards = roles.map((role) => createAgentCard(role));
document.querySelector('#agents').replaceChildren(...cards);
}

function createAgentCard(role, observation, loopId, failure) {
const card = document.createElement('article');
let visualStatus = 'working';
if (observation) visualStatus = observation.status.toLowerCase();
else if (failure) visualStatus = 'fail';
card.className = `agent-card ${visualStatus}`;

const ordinal = String(roles.indexOf(role) + 1).padStart(2, '0');
const status = observation?.status ?? (failure ? 'ERROR' : 'RUNNING');
const summary =
observation?.summary ??
failure ??
'Inspecting the synthetic evidence source…';
const check =
observation?.check ??
(failure ? 'evidence unavailable' : `${role} evidence`);

card.innerHTML = `
<div class="agent-topline">
<span>Agent ${ordinal}</span>
<span class="agent-status">${escapeHtml(status)}</span>
</div>
<h4>${escapeHtml(roleLabels[role])}</h4>
<p>${escapeHtml(summary)}</p>
<div class="agent-foot">
<span>${escapeHtml(check)}</span>
<code>${escapeHtml(shortLoop(loopId))}</code>
</div>
`;
return card;
}

function renderResult(result) {
const isReady = result.verdict.status === 'READY_FOR_HUMAN';
resultPanel.hidden = false;
resultPanel.className = `result ${isReady ? 'ready' : 'blocked'}`;
document.querySelector('#verdict').textContent = result.verdict.status;
document.querySelector('#case-id').textContent = result.fixture;
document.querySelector('#digest').textContent = shortDigest(
result.verdict.caseDigest,
);
document.querySelector('#concurrency').textContent =
result.concurrencyObserved ? 'overlap verified' : 'overlap not verified';

const reasons = document.querySelector('#reasons');
reasons.hidden = result.verdict.reasons.length === 0;
reasons.replaceChildren(
...result.verdict.reasons.map((reason) => {
const item = document.createElement('p');
item.textContent = reason;
return item;
}),
);

const observations = new Map(
result.verdict.evidence.map((observation) => [
observation.role,
observation,
]),
);
const cards = roles.map((role) => {
const prefixedReason = result.verdict.reasons.find((reason) =>
reason.startsWith(`${role}: `),
);
const failure = prefixedReason?.slice(role.length + 2);
return createAgentCard(
role,
observations.get(role),
result.loopIds[role],
failure,
);
});
document.querySelector('#agents').replaceChildren(...cards);

const visibleEvents = result.timeline.filter((event) =>
[
'case.announced',
'inference.started',
'function_call.completed',
'evidence.observed',
'participant.failed',
'participant.completed',
'gate.updated',
].includes(event.type),
);
document.querySelector('#event-count').textContent =
`${result.timeline.length} events`;
document.querySelector('#timeline').replaceChildren(
...visibleEvents.map((event) => {
const item = document.createElement('li');
item.innerHTML = `
<span>${String(event.sequence).padStart(2, '0')}</span>
<strong>${escapeHtml(event.actor)}</strong>
<code>${escapeHtml(event.type)}</code>
<small>${escapeHtml(shortLoop(event.loopId))}</small>
`;
return item;
}),
);
}

function shortDigest(digest) {
return `${digest.slice(0, 12)}…${digest.slice(-8)}`;
}

function shortLoop(loopId) {
if (!loopId || loopId === 'missing') return 'no evidence';
return `loop ${loopId.slice(0, 8)}`;
}

function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
137 changes: 137 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="ProofGate — concurrent release and experiment evidence control room"
/>
<title>ProofGate Control Room</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div class="shell">
<header class="masthead">
<a class="brand" href="/" aria-label="ProofGate home">
<span class="brand-mark" aria-hidden="true">
<i></i><i></i><i></i><i></i>
</span>
<span>proof<span>gate</span></span>
</a>
<div class="environment">
<span class="environment-dot"></span>
synthetic · read only
</div>
</header>

<main>
<section class="hero" aria-labelledby="page-title">
<div>
<p class="eyebrow">Release & experiment control room</p>
<h1 id="page-title">Evidence before action.</h1>
<p class="lede">
Three concurrent agents inspect independent proof lanes. A
deterministic attestor closes the gate unless every required
signal is present and safe.
</p>
</div>
<div class="guardrail">
<span>Human authority</span>
<strong>No deploy controls</strong>
<p>ProofGate advises. A person decides and acts.</p>
</div>
</section>

<section class="control-panel" aria-labelledby="run-heading">
<div class="panel-heading">
<div>
<p class="section-label">New evaluation</p>
<h2 id="run-heading">Choose a proof case</h2>
</div>
<p id="run-state" class="run-state" aria-live="polite">
Ready to run
</p>
</div>

<fieldset class="scenario-picker">
<legend class="visually-hidden">Proof case</legend>
<button
class="scenario selected"
data-fixture="ready"
type="button"
>
<span class="scenario-code">01</span>
<span
><strong>Ready</strong><small>All evidence passes</small></span
>
</button>
<button class="scenario" data-fixture="blocked" type="button">
<span class="scenario-code">02</span>
<span
><strong>Blocked</strong
><small>Guardrail violation</small></span
>
</button>
<button class="scenario" data-fixture="failure" type="button">
<span class="scenario-code">03</span>
<span
><strong>Source failure</strong
><small>Missing policy proof</small></span
>
</button>
</fieldset>

<button id="run-button" class="run-button" type="button">
<span id="run-label">Run concurrent evaluation</span>
<span aria-hidden="true">→</span>
</button>
</section>

<section id="result" class="result" aria-live="polite" hidden>
<div class="verdict-row">
<div>
<p class="section-label">Deterministic verdict</p>
<h2 id="verdict">—</h2>
</div>
<div class="verdict-meta">
<span id="case-id">—</span>
<code id="digest">—</code>
</div>
</div>

<div id="reasons" class="reasons" hidden></div>

<div class="agents-heading">
<div>
<p class="section-label">Concurrent evidence lanes</p>
<h3>Three agents · one case digest</h3>
</div>
<span id="concurrency" class="concurrency">—</span>
</div>

<div id="agents" class="agents"></div>

<details class="timeline-panel">
<summary>
<span>Event ledger</span>
<span id="event-count">0 events</span>
</summary>
<ol id="timeline" class="timeline"></ol>
</details>

<p class="cloud-note">
Mozaik Cloud telemetry is emitted automatically. Live trace links
appear in the server terminal.
</p>
</section>
</main>

<footer>
<span>ProofGate MVP · Fortemate</span>
<span>BLOCKED or READY_FOR_HUMAN — never auto-release</span>
</footer>
</div>
<script type="module" src="/app.js"></script>
</body>
</html>
Loading