import React, { useEffect, useState } from 'react';
import Icon from '../Icon.jsx';
import { useStudio, canApprove, resolveSelection, uid, timestamp, log } from '../store.jsx';
import { Badge, Button, Empty, Field, Heading, Modal, Note, SourceDialog } from '../ui.jsx';
async function digest(value) { const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); return [...new Uint8Array(bytes)].map(b => b.toString(16).padStart(2, '0')).join(''); }
function manifest(state, parentId, crIds) { const resolution = resolveSelection(state, parentId, crIds); return { parent: structuredClone(resolution.baseline), changes: structuredClone(resolution.changes), sources: [...new Set([...(resolution.baseline?.sourceIds || []), ...resolution.diff.map(c => c.source)])].map(id => { const d = state.documents.find(d => d.id === id); return { id, revision: d?.revision, checksum: d?.checksum || 'synthetic-sample' }; }) }; }
export function approvalBlockers(state, candidate) {
if (!candidate) return [];
const blockers = [...resolveSelection(state, candidate.parentId, candidate.crIds).issues];
if (candidate.crIds.some(id => !state.crs.find(cr => cr.id === id)?.changes.length)) blockers.push('A selected CR has no change items.');
if (state.questions.some(q => {
const owner = state.workspaces.find(w => w.id === q.workspaceId);
return owner && owner.version === q.version && owner.baselineId === candidate.parentId && owner.crIds.length > 0 && owner.crIds.every(id => candidate.crIds.includes(id)) && q.status !== 'resolved';
})) blockers.push('Selected changes have unresolved clarification questions.');
return blockers;
}
export default function Approvals({ navigate }) {
const { state: s, mutate, workspace: w, notify } = useStudio();
const list = s.candidates.filter(c => c.team === s.session.team);
const [id, setId] = useState(''), [requesting, setRequesting] = useState(false), [name, setName] = useState(''), [description, setDescription] = useState(''), [checks, setChecks] = useState([false, false, false]), [hash, setHash] = useState(''), [source, setSource] = useState(''), [rationale, setRationale] = useState(''), [busy, setBusy] = useState(false);
const candidate = list.find(c => c.id === id) || list[0];
const snapshot = candidate?.manifest || (candidate ? manifest(s, candidate.parentId, candidate.crIds) : null);
const canonical = snapshot ? JSON.stringify(snapshot) : '';
const resolution = candidate ? resolveSelection(s, candidate.parentId, candidate.crIds) : null;
useEffect(() => { setChecks([false, false, false]); setRationale(''); setHash(''); let live = true; if (canonical) digest(canonical).then(value => { if (live) setHash(value); }); return () => { live = false; }; }, [candidate?.id, candidate?.revision, canonical]);
const blockers = approvalBlockers(s, candidate);
async function approve() {
if (!candidate || !canApprove(s) || blockers.length || !checks.every(Boolean) || !hash || busy) return;
setBusy(true); const approvedHash = await digest(canonical), expectedRevision = candidate.revision;
mutate(d => {
const c = d.candidates.find(c => c.id === candidate.id);
if (!canApprove(d)) return;
if (c.revision !== expectedRevision || c.status !== 'Pending review') return;
const rules = new Map(snapshot.parent.rules.map(r => [r.id, r]));
snapshot.changes.forEach(cr => cr.changes.forEach(change => { if (change.op === 'REMOVE') rules.delete(change.ruleId); else rules.set(change.ruleId, { id: change.ruleId, title: change.title, text: change.after, source: change.source }); }));
d.baselines.push({ id: uid('baseline'), ref: c.ref, team: c.team, version: `${Number(snapshot.parent.version.split('.')[0]) + 1}.0`, status: 'Approved', description: c.description, scope: snapshot.parent.scope, sourceIds: snapshot.sources.map(source => source.id), rules: [...rules.values()], incorporatedCRs: [...new Set([...snapshot.parent.incorporatedCRs, ...c.crIds])], createdAt: timestamp(), manifestHash: approvedHash, parentId: c.parentId });
c.status = 'Approved'; c.manifest = snapshot; c.approvedHash = approvedHash; c.decisions.push({ decision: 'Approved', actor: s.session.name, at: timestamp(), rationale, revision: expectedRevision, hash: approvedHash }); log(d, `Approved baseline candidate ${c.ref} · ${approvedHash.slice(0, 12)}`);
}); setBusy(false); notify('New immutable baseline created in the workspace. Existing workspaces keep their selected baseline.');
}
return <><Heading eyebrow={`${s.session.team} · HUMAN REVIEW`} title="System Baseline updates" description="Approve the exact candidate manifest. Existing workspaces stay pinned."><Button icon="plus" variant="primary" disabled={!w} onClick={() => { setName(`${s.session.team}-B${s.baselines.filter(b => b.team === s.session.team).length + 1}`); setDescription(''); setRequesting(true); }}>Request baseline update</Button></Heading>{!candidate ? <section className="card"><Empty title="No baseline candidates" description="Request a candidate from a workspace’s selected baseline and changes."/></section> : <><div className="suite-toolbar"><select className="wide-select" value={candidate.id} onChange={e => setId(e.target.value)}>{list.map(c => <option value={c.id} key={c.id}>{c.ref} · {c.status}</option>)}</select><Badge>{candidate.status}</Badge></div><section className="card padded"><div className="candidate-summary"><span className="soft-icon soft-purple"><Icon name="shield" size={24}/></span><div><h3>{candidate.ref}</h3><p>{candidate.description}</p></div><Badge tone="purple">Revision {candidate.revision}</Badge></div><div className="detail-summary"><span>Parent: <strong>{snapshot?.parent?.ref}</strong></span><span>Incorporated CRs: <strong>{resolution.changes.map(c => c.ref).join(', ') || 'None'}</strong></span><span>Changed rules: <strong>{resolution.diff.length}</strong></span></div><p className="checksum">Exact manifest SHA-256: {hash || 'Calculating reference…'}</p></section><section className="card margin-top"><div className="section-heading"><h2>Candidate knowledge diff</h2><Badge tone="purple">Source-backed revisions</Badge></div>{resolution.diff.map((c, i) => <div className="diff-block" key={i}><div className="diff-heading"><span className={`operation operation-${c.op.toLowerCase()}`}>{c.op}</span><strong>{c.ruleId} · {c.title}</strong><span>{c.crRef}</span><button className="text-button" onClick={() => setSource(c.source)}>Evidence <Icon name="external" size={12}/></button></div><div className="diff-columns"><div><small>BEFORE</small><p>{c.before || 'Not present'}</p></div><div><small>AFTER</small><p>{c.after || 'Removed from manifest'}</p></div></div></div>)}{!resolution.diff.length && <Empty title="No changed rules" description="Select reviewed CR changes before approving a new baseline."/>}</section><div className="detail-grid margin-top"><section className="card padded"><h2>Approval checklist</h2>{blockers.map(b => <Note key={b} tone="amber">{b}</Note>)}{['All changed rules and their source revisions reviewed', 'Contradictions and required dependencies resolved', 'This exact candidate manifest is approved'].map((text, i) => <label className="check-label" key={text}><input type="checkbox" checked={candidate.status === 'Approved' || checks[i]} disabled={candidate.status !== 'Pending review'} onChange={e => setChecks(values => values.map((v, index) => index === i ? e.target.checked : v))}/>{text}</label>)}<Field label="Decision rationale"><textarea value={rationale} onChange={e => setRationale(e.target.value)} disabled={candidate.status !== 'Pending review'} rows={2} placeholder="Record the review rationale…"/></Field><div className="row-actions"><Button variant="primary" icon="shield" disabled={!canApprove(s) || !checks.every(Boolean) || blockers.length > 0 || !hash || busy || candidate.status !== 'Pending review' || !resolution.diff.length} onClick={approve}>{busy ? 'Saving approval…' : 'Approve baseline update'}</Button><Button disabled={!canApprove(s) || candidate.status !== 'Pending review' || !rationale.trim()} onClick={() => { mutate(d => { const c = d.candidates.find(c => c.id === candidate.id); c.status = 'Rejected'; c.decisions.push({ decision: 'Rejected', actor: s.session.name, at: timestamp(), rationale, revision: c.revision }); log(d, `Rejected candidate ${c.ref}`); }); notify('Candidate rejected with rationale.'); }}>Reject</Button></div>{!canApprove(s) && <Note>Only an Administrator can approve System Baseline updates. <button className="text-button" onClick={() => navigate('login')}>Administrator sign-in</button></Note>}</section><section className="card padded"><h2>Decision history</h2>{candidate.decisions.map((decision, i) => <div className="decision-record" key={i}><Badge>{decision.decision}</Badge><strong>{decision.actor}</strong><small>{new Date(decision.at).toLocaleString('en-IN')} · Revision {decision.revision}</small><p>{decision.rationale || 'Exact manifest approved.'}</p></div>)}{!candidate.decisions.length && <Empty title="Awaiting a decision" description="Every review records the actor, revision, time, and rationale."/>}{candidate.status === 'Pending review' && <Field label="Candidate description"><textarea rows={3} value={candidate.description} onChange={e => mutate(d => { const c = d.candidates.find(c => c.id === candidate.id); c.description = e.target.value; c.revision += 1; })}/></Field>}<p className="supporting-copy">Changes to a candidate invalidate its checklist and require review again.</p></section></div></>}{requesting && <Modal title="Request a new System Baseline" close={() => setRequesting(false)}><p className="modal-intro">Use {s.baselines.find(b => b.id === w?.baselineId)?.ref} plus this workspace’s selected CR revisions.</p><form onSubmit={e => { e.preventDefault(); const candidateId = uid('candidate'); mutate(d => { d.candidates.unshift({ id: candidateId, ref: name.trim(), description, team: s.session.team, parentId: w.baselineId, crIds: [...w.crIds], manifest: manifest(s, w.baselineId, w.crIds), status: 'Pending review', revision: 1, createdAt: timestamp(), decisions: [] }); log(d, `Requested baseline candidate ${name}`); }); setId(candidateId); setRequesting(false); notify('Baseline candidate submitted for exact-manifest review.'); }}><Field label="New baseline name"><input required value={name} onChange={e => setName(e.target.value)} maxLength={60}/></Field><Field label="Purpose"><textarea value={description} onChange={e => setDescription(e.target.value)} rows={3}/></Field><Note>This request does not change any existing System Baseline or workspace.</Note><div className="modal-actions"><Button onClick={() => setRequesting(false)}>Cancel</Button><Button type="submit" variant="primary" disabled={!w?.crIds.length || s.baselines.some(b => b.ref === name.trim() && b.team === s.session.team) || s.candidates.some(c => c.ref === name.trim() && c.team === s.session.team && c.status === 'Pending review')}>Request candidate</Button></div></form></Modal>}{source && <SourceDialog sourceId={source} close={() => setSource('')}/>}</>;
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)