Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
9 changes: 7 additions & 2 deletions next-app/app/api/chat/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ function* fakeStream(text: string) {
}
}

import { preFilter, steerPrompt, postModerate } from '@/lib/safety/pipeline';

function streamForMessage(message: string) {
const ctrl = new AbortController();
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
try {
const reply = `Echo: ${message}`;
const meta = { layer: 'surface', model: 'mock', version: '0.0.1', latencyMs: 42 };
const pre = preFilter(message);
const safePrompt = steerPrompt(message);
const reply = `Echo: ${safePrompt}`;
const post = postModerate(reply);
const meta = { layer: 'surface', model: 'mock', version: '0.0.1', latencyMs: 42, pre, post };
controller.enqueue(encode(`event: meta\ndata: ${JSON.stringify(meta)}\n\n`));
for (const chunk of fakeStream(reply)) {
await new Promise(r => setTimeout(r, 10));
Expand Down
18 changes: 18 additions & 0 deletions next-app/app/api/consent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextRequest } from 'next/server';
import { appendConsentEvent, exportConsent } from '@/lib/privacy/consentLedger';

export const runtime = 'nodejs';

export async function POST(req: NextRequest) {
const { userId = 'demo', sessionId, action } = await req.json();
if (!['persist_on','persist_off','export'].includes(action)) return new Response('bad action', { status: 400 });
const ev = await appendConsentEvent({ userId, sessionId, action, ts: new Date().toISOString() as any });
return Response.json(ev);
}

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const userId = searchParams.get('userId') ?? 'demo';
const data = await exportConsent(userId);
return Response.json(data);
}
11 changes: 11 additions & 0 deletions next-app/app/api/risk/scores/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const runtime = 'nodejs';
export async function GET() {
// Mock time-series risk per layer: core/operational/context
const now = Date.now();
const series = ['core','operational','context'].map((k, i) => ({
key: k,
points: Array.from({ length: 12 }, (_, j) => ({ t: now - (11 - j) * 3600_000, v: clamp(0, 100, 30 + i*20 + Math.sin(j/2+i)*15 + Math.random()*10) }))
}));
return Response.json({ series });
}
function clamp(min:number,max:number,v:number){return Math.max(min,Math.min(max,v));}
5 changes: 3 additions & 2 deletions next-app/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function ChatPage() {

return (
<div className="space-y-4">
<h1 className="text-2xl font-semibold">Chat</h1>
<h1 className="text-2xl font-semibold">Chat <span className="text-xs align-middle text-slate-500">(ephemeral by default)</span></h1>
<div className="rounded border bg-white p-3">
<div className="space-y-3" role="log" aria-live="polite">
{messages.map((m, i) => (
Expand All @@ -63,10 +63,11 @@ export default function ChatPage() {
</div>
))}
</div>
<div className="mt-3 flex gap-2">
<div className="mt-3 flex flex-wrap items-center gap-2">
<input value={input} onChange={e=>setInput(e.target.value)} className="flex-1 rounded border px-3 py-2" placeholder="Type a message..." />
<button onClick={send} disabled={streaming} className="rounded bg-amber-600 px-4 py-2 text-white disabled:opacity-50">Send</button>
{fallback && <span className="text-xs text-slate-500">Fallback in use</span>}
<a href="/api/consent?userId=demo" target="_blank" className="text-xs text-amber-700 underline">Export consent ledger</a>
</div>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions next-app/app/docs/governance-terms-mapping/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import path from 'path';
export const dynamic = 'force-static';
export default function Page() {
const md = readFileSync(path.join(process.cwd(), 'next-app', 'docs', 'governance-terms-mapping.md'), 'utf8');
return <pre className="whitespace-pre-wrap text-sm">{md}</pre>;
}
7 changes: 7 additions & 0 deletions next-app/app/docs/readiness-checklist/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import path from 'path';
export const dynamic = 'force-static';
export default function Page() {
const md = readFileSync(path.join(process.cwd(), 'next-app', 'docs', 'readiness-checklist.md'), 'utf8');
return <pre className="whitespace-pre-wrap text-sm">{md}</pre>;
}
7 changes: 7 additions & 0 deletions next-app/app/docs/roadmap/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import path from 'path';
export const dynamic = 'force-static';
export default function Page() {
const md = readFileSync(path.join(process.cwd(), 'next-app', 'docs', 'roadmap.md'), 'utf8');
return <pre className="whitespace-pre-wrap text-sm">{md}</pre>;
}
7 changes: 7 additions & 0 deletions next-app/app/docs/strategy-map/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import path from 'path';
export const dynamic = 'force-static';
export default function Page() {
const md = readFileSync(path.join(process.cwd(), 'next-app', 'docs', 'strategy-map.md'), 'utf8');
return <pre className="whitespace-pre-wrap text-sm">{md}</pre>;
}
103 changes: 103 additions & 0 deletions next-app/app/governance/maturity/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { readFileSync } from 'fs';
import path from 'path';

export const metadata = { title: 'Governance Capability Matrix' } as const;
export const dynamic = 'force-static';

type Dimension = {
id: string;
name: string;
phase: string;
score: number; // 0-5
evidence: string[];
gaps: string[];
remediation: string[];
links?: Record<string, string>;
};

type Maturity = { dimensions: Dimension[] };

function gateText(score: number) {
if (score < 2) return { label: 'Do not advance', color: '#dc2626', note: 'Address gaps before proceeding' };
if (score < 4) return { label: 'Proceed with guardrails', color: '#f59e0b', note: 'Monitor and document mitigations' };
return { label: 'Clear to advance', color: '#16a34a', note: 'Maintain controls and evidence' };
}

function scoreColor(score: number) {
if (score <= 1) return '#b91c1c';
if (score === 2) return '#e11d48';
if (score === 3) return '#f59e0b';
if (score === 4) return '#10b981';
return '#059669';
}

export default function Page() {
const file = path.join(process.cwd(), 'next-app', 'data', 'maturity.json');
const data: Maturity = JSON.parse(readFileSync(file, 'utf8'));
return (
<main className="space-y-4">
<h1 className="text-2xl font-semibold">Governance Capability Matrix</h1>
<p className="text-sm text-slate-600">Scores (0–5), evidence, gaps, remediation and gating guidance per dimension.</p>

<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{data.dimensions.map((d) => {
const gate = gateText(d.score);
return (
<section key={d.id} className="rounded border bg-white p-4 shadow-sm">
<header className="mb-2 flex items-center justify-between">
<div>
<div className="text-base font-semibold text-slate-800">{d.name}</div>
<div className="text-xs text-slate-500">Phase: {d.phase}</div>
</div>
<div className="text-right">
<span className="inline-flex items-center gap-1 rounded border px-2 py-0.5 text-xs" style={{ borderColor: scoreColor(d.score), color: scoreColor(d.score) }}>
Score <strong className="ml-1">{d.score}</strong>
</span>
<div className="mt-1 text-xs" style={{ color: gate.color }}>{gate.label}</div>
</div>
</header>

{d.evidence?.length ? (
<div className="mb-2">
<div className="mb-1 text-xs font-semibold text-slate-700">Evidence</div>
<ul className="list-disc pl-5 text-sm text-slate-700">
{d.evidence.map((e, i) => (<li key={i}>{e}</li>))}
</ul>
</div>
) : null}

{d.gaps?.length ? (
<div className="mb-2">
<div className="mb-1 text-xs font-semibold text-slate-700">Gaps</div>
<ul className="list-disc pl-5 text-sm text-slate-700">
{d.gaps.map((g, i) => (<li key={i}>{g}</li>))}
</ul>
<div className="mt-2 rounded bg-red-50 p-2 text-xs text-red-700">{gate.note}</div>
</div>
) : null}

{d.remediation?.length ? (
<div className="mb-2">
<div className="mb-1 text-xs font-semibold text-slate-700">Remediation</div>
<ul className="list-disc pl-5 text-sm text-slate-700">
{d.remediation.map((r, i) => (<li key={i}>{r}</li>))}
</ul>
</div>
) : null}

{d.links && Object.keys(d.links).length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2 text-xs">
{Object.entries(d.links).map(([k, v]) => (
<a key={k} href={v} className="rounded border border-amber-300 bg-amber-50 px-2 py-1 text-amber-800 underline">
{k}
</a>
))}
</div>
) : null}
</section>
);
})}
</div>
</main>
);
}
19 changes: 19 additions & 0 deletions next-app/app/governance/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Link from 'next/link';

export const metadata = { title: 'Governance Cockpit' };
export default function GovernancePage() {
return (
<main className="space-y-4">
<h1 className="text-2xl font-semibold">Governance Cockpit</h1>
<p className="text-sm text-slate-600">Board-ready artifact hub with live roadmap, mappings, and templates.</p>
<ul className="list-disc pl-6 text-amber-800">
<li><Link href="/docs/roadmap" className="underline">Roadmap (capacity-aware)</Link></li>
<li><Link href="/docs/governance-terms-mapping" className="underline">Integrated 18‑Point Mapping</Link></li>
<li><Link href="/docs/readiness-checklist" className="underline">Implementation Readiness Checklist</Link></li>
<li><Link href="/templates/artefact-templates" className="underline">Governance Artefact Templates</Link></li>
<li><Link href="/governance/maturity" className="underline">Governance Capability Matrix</Link></li>
<li><Link href="/risk" className="underline">Interactive Risk & Governance Demos</Link></li>
</ul>
</main>
);
}
106 changes: 106 additions & 0 deletions next-app/app/risk/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export const metadata = { title: 'AI Risk Navigator' } as const;
import { PULSE_SCRIPT } from './pulse-script';

export default function RiskPage() {
return (
<main className="space-y-4">
<h1 className="text-2xl font-semibold">Interactive 10-Stage AI Risk Matrix <span id="pulse" className="ml-2 text-xs text-slate-500"></span></h1>
<p className="text-sm text-slate-600">Filterable matrix and governance dashboard demos.</p>
<iframe id="riskFrame" srcDoc={RISK_HTML} className="h-[80vh] w-full rounded border" />
<script dangerouslySetInnerHTML={{__html: PULSE_SCRIPT}} />
</main>
);
}

const RISK_HTML = `<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>html,body{margin:0;padding:0}</style>
</head><body>
${MATRIX_SECTION}
${GOV_DASHBOARD}
<script>window.addEventListener('message',e=>{if(e.data&&e.data.type==='risk-pulse'){document.body.style.boxShadow='inset 0 0 0 3px rgba(234,179,8,.6)';setTimeout(()=>{document.body.style.boxShadow='none';},300);}})</script>
</body></html>`;

const MATRIX_SECTION = `
<div style="padding:16px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);">
<div style="background:rgba(255,255,255,0.95);backdrop-filter:blur(10px);border-radius:16px;padding:16px;max-width:1400px;margin:0 auto;">
<h2 style="margin:0 0 8px 0;color:#2c3e50;font-size:20px;font-weight:600;text-align:center">Interactive Cross-Stage AI Risk Matrix</h2>
<div style="text-align:center;margin-bottom:8px">
<button onclick="toggleColumn('persistent')" style="margin:0 4px;padding:6px 10px;border:none;border-radius:6px;background:#4a5568;color:#fff;cursor:pointer;font-size:12px">Toggle Persistent</button>
<button onclick="toggleColumn('evolving')" style="margin:0 4px;padding:6px 10px;border:none;border-radius:6px;background:#4a5568;color:#fff;cursor:pointer;font-size:12px">Toggle Evolving</button>
<button onclick="toggleColumn('emergent')" style="margin:0 4px;padding:6px 10px;border:none;border-radius:6px;background:#4a5568;color:#fff;cursor:pointer;font-size:12px">Toggle Emergent</button>
</div>
<table id="matrix" style="width:100%;border-collapse:collapse;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 10px 30px rgba(0,0,0,.1)">
<thead>
<tr>
<th style="background:#1a202c;color:#fff;padding:10px;font-size:13px;border:1px solid #4a5568">Development Stage</th>
<th class="persistent" style="background:#1a202c;color:#fff;padding:10px;font-size:13px;border:1px solid #4a5568">Persistent Risks</th>
<th class="evolving" style="background:#1a202c;color:#fff;padding:10px;font-size:13px;border:1px solid #4a5568">Evolving Risks</th>
<th class="emergent" style="background:#1a202c;color:#fff;padding:10px;font-size:13px;border:1px solid #4a5568">Emergent Risks</th>
</tr>
</thead>
<tbody>
${[1,2,3,4,5,6,7,8,9,10].map(n=>`<tr>
<td style="padding:10px;border:1px solid #e2e8f0;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;font-weight:600">Stage ${n}</td>
<td class="persistent" style="padding:10px;border:1px solid #e2e8f0">Persistent risk ${n}</td>
<td class="evolving" style="padding:10px;border:1px solid #e2e8f0">Evolving risk ${n}</td>
<td class="emergent" style="padding:10px;border:1px solid #e2e8f0">Emergent risk ${n}</td>
</tr>`).join('')}
</tbody>
</table>
<div style="display:flex;gap:12px;justify-content:center;margin-top:8px;flex-wrap:wrap">
<span style="display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.8);padding:6px 10px;border-radius:16px"><span style="width:14px;height:14px;background:#38b2ac;border-radius:3px"></span>Persistent</span>
<span style="display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.8);padding:6px 10px;border-radius:16px"><span style="width:14px;height:14px;background:#ed8936;border-radius:3px"></span>Evolving</span>
<span style="display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.8);padding:6px 10px;border-radius:16px"><span style="width:14px;height:14px;background:#d53f8c;border-radius:3px"></span>Emergent</span>
</div>
</div>
</div>
<script>
function toggleColumn(cls){
const cells=[...document.querySelectorAll('#matrix .'+cls)];
const isHidden=cells.every(td=>td.style.display==='none');
cells.forEach(td=>td.style.display=isHidden?'table-cell':'none');
}
</script>
`;

const GOV_DASHBOARD = `
<div style="padding:16px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);">
<div style="background:rgba(255,255,255,0.95);backdrop-filter:blur(10px);border-radius:16px;padding:16px;max-width:1400px;margin:12px auto;">
<h2 style="margin:0 0 8px 0;color:#2c3e50;font-size:20px;font-weight:600;text-align:center">AGI/ASI Governance Dashboard (Lite)</h2>
<div id="status" style="text-align:center;color:#475569;font-size:13px;margin-bottom:8px">Design Phase Active • 4 checkpoints scheduled</div>
<div style="display:grid;grid-template-columns:1fr 340px;gap:16px">
<div style="position:relative;min-height:360px;background:#fff;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:16px">
<div id="rings" style="position:relative;width:320px;height:320px;margin:0 auto">
<div data-layer="context" style="position:absolute;top:4px;left:4px;width:312px;height:312px;border-radius:50%;border:3px solid #667eea;display:flex;align-items:center;justify-content:center;background:linear-gradient(45deg,#55a3ff,#667eea);color:#fff;font-weight:700">Context & Safeguards</div>
<div data-layer="operational" style="position:absolute;top:34px;left:34px;width:252px;height:252px;border-radius:50%;border:3px solid #0984e3;display:flex;align-items:center;justify-content:center;background:linear-gradient(45deg,#74b9ff,#0984e3);color:#fff;font-weight:700">Operational Framework</div>
<div data-layer="core" style="position:absolute;top:94px;left:94px;width:132px;height:132px;border-radius:50%;border:3px solid #ee5a24;display:flex;align-items:center;justify-content:center;background:linear-gradient(45deg,#ff6b6b,#ee5a24);color:#fff;font-weight:700">Core Elements</div>
</div>
</div>
<div style="background:#fff;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.08);padding:16px">
<div style="font-size:14px;font-weight:700;margin-bottom:6px">Layer Detail</div>
<div id="layerTitle" style="font-size:13px;color:#334155;margin-bottom:6px">System Overview</div>
<div style="font-size:12px;color:#475569;margin-bottom:6px">Aggregated Risk: <span id="aggRisk" style="font-weight:700;color:#16a34a">Low</span></div>
<div style="font-size:12px;color:#475569;margin-bottom:6px">Governance: <span id="govBody">Safety Oversight Board</span></div>
<div style="display:flex;gap:8px;margin-top:8px">
<button onclick="parent.postMessage({type:'risk-pulse'},'*')" style="flex:1;padding:8px 10px;border:none;border-radius:8px;background:#667eea;color:#fff;font-weight:600">Export Report</button>
<button onclick="parent.postMessage({type:'risk-pulse'},'*')" style="flex:1;padding:8px 10px;border:none;border-radius:8px;background:#764ba2;color:#fff;font-weight:600">Schedule Review</button>
</div>
</div>
</div>
</div>
</div>
<script>
const layerRisks={core:'Medium',operational:'Medium',context:'High'};
const govBodies={core:'Model Review Board & Algorithm Ethics Panel',operational:'System Integration Board',context:'Safety Oversight Board'};
document.querySelectorAll('#rings [data-layer]').forEach(el=>{
el.addEventListener('click',()=>{
const layer=el.getAttribute('data-layer');
document.getElementById('layerTitle').textContent = layer.charAt(0).toUpperCase()+layer.slice(1)+' Layer';
document.getElementById('aggRisk').textContent = layerRisks[layer];
document.getElementById('aggRisk').style.color = layerRisks[layer]==='High'?'#e11d48':(layerRisks[layer]==='Medium'?'#f59e0b':'#16a34a');
document.getElementById('govBody').textContent = govBodies[layer];
})
})
</script>
`;
18 changes: 18 additions & 0 deletions next-app/app/risk/pulse-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const PULSE_SCRIPT = `
(async function(){
const pulseEl = document.getElementById('pulse');
async function tick(){
try{
const res = await fetch('/api/risk/scores');
const json = await res.json();
const ctx = json.series.find((s:any)=>s.key==='context');
const last = ctx?.points?.[ctx.points.length-1]?.v ?? 0;
if(pulseEl){ pulseEl.textContent = 'Context risk: ' + Math.round(last); }
const iframe = document.getElementById('riskFrame') as HTMLIFrameElement | null;
iframe?.contentWindow?.postMessage({type:'risk-pulse'}, '*');
}catch(e){ if(pulseEl) pulseEl.textContent = 'Risk: n/a'; }
setTimeout(tick, 6000);
}
tick();
})();
`;
7 changes: 7 additions & 0 deletions next-app/app/templates/artefact-templates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import path from 'path';
export const dynamic = 'force-static';
export default function Page() {
const md = readFileSync(path.join(process.cwd(), 'next-app', 'templates', 'artefact-templates.md'), 'utf8');
return <pre className="whitespace-pre-wrap text-sm">{md}</pre>;
}
7 changes: 7 additions & 0 deletions next-app/app/templates/kpi-alignment/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'fs';
import path from 'path';
export const dynamic = 'force-static';
export default function Page() {
const md = readFileSync(path.join(process.cwd(), 'next-app', 'templates', 'kpi-alignment.md'), 'utf8');
return <pre className="whitespace-pre-wrap text-sm">{md}</pre>;
}
Loading
Loading