OPC # 0001: Extract OPC into standalone repo

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
amadzarak
2026-04-25 19:17:48 -04:00
parent 7561ac7530
commit 76962a6af4
6 changed files with 326 additions and 65 deletions
+141 -29
View File
@@ -11,7 +11,7 @@ import {
listOpcs, createOpc, updateOpc, getNextNumber,
listNotes, addNote,
listArtifacts, createArtifact, updateArtifact, deleteArtifact,
getLinkedCommits, getPinnedCommits, pinCommit, unpinCommit, getBranchCoverage,
getLinkedCommits, getPinnedCommits, pinCommit, unpinCommit, getBranchCoverageForRepo,
listGiteaBranches, createGiteaBranch,
aiAssist,
type LinkedCommit, type PinnedCommit, type BranchCoverage, type GiteaBranch,
@@ -70,6 +70,49 @@ const ARTIFACT_TABS: { type: ArtifactType; label: string; placeholder: string }[
{ type: 'QaTestPath', label: 'QA Test Paths', placeholder: 'Step-by-step QA test scenarios, edge cases, regression checks...' },
];
// -- SDLC delivery chain -------------------------------------------------------
const SDLC_STAGES: { branch: string; label: string; intent: Intent }[] = [
{ branch: 'develop', label: 'Dev', intent: Intent.PRIMARY },
{ branch: 'staging', label: 'Staging', intent: Intent.WARNING },
{ branch: 'uat', label: 'UAT', intent: Intent.DANGER },
{ branch: 'master', label: 'Production', intent: Intent.SUCCESS },
];
function deriveSdlcSummary(coverage: BranchCoverage[]): { label: string; intent: Intent } | null {
for (let i = SDLC_STAGES.length - 1; i >= 0; i--) {
const stage = SDLC_STAGES[i];
const hit = coverage.find(c => c.branch === stage.branch);
if (hit?.contains) return { label: stage.label, intent: stage.intent };
}
return null;
}
// Aggregate per-repo branch coverage into a single view.
// A stage is "reached" only when every repo that recognised at least one hash
// reports contains=true for that branch. Repos that recognised no hashes are
// excluded from the constraint (they have no code linked to this OPC).
const KNOWN_REPOS = ['Clarity', 'OPC', 'Gateway'] as const;
type RepoCoverage = { repoKey: string; coverage: BranchCoverage[] };
function aggregateCoverage(perRepo: RepoCoverage[]): BranchCoverage[] {
const active = perRepo.filter(r => r.coverage.length > 0);
if (active.length === 0) return [];
const branches = [...new Set(active.flatMap(r => r.coverage.map(c => c.branch)))];
return branches.map(branch => {
const entries = active
.map(r => r.coverage.find(c => c.branch === branch))
.filter((c): c is BranchCoverage => c !== undefined);
return {
branch,
contains: entries.length > 0 && entries.every(c => c.contains),
tipHash: entries[0]?.tipHash ?? '',
isHead: entries.some(c => c.isHead),
};
});
}
// -- Helpers -------------------------------------------------------------------
function fmtDate(iso: string): string {
@@ -232,6 +275,14 @@ function ArtifactPanel({ opcId, opcNumber, artifactType, placeholder }: {
);
}
// -- Repo badge ---------------------------------------------------------------
const REPO_INTENT: Record<string, Intent> = {
Clarity: Intent.PRIMARY,
OPC: Intent.WARNING,
Gateway: Intent.SUCCESS,
};
// -- Commit row (shared) -------------------------------------------------------
function CommitRow({ commit, onPin, isPinned, onViewDiff }: { commit: LinkedCommit; onPin?: () => void; isPinned?: boolean; onViewDiff?: (hash: string) => void }) {
@@ -243,6 +294,13 @@ function CommitRow({ commit, onPin, isPinned, onViewDiff }: { commit: LinkedComm
{commit.shortHash}
</code>
</Tooltip>
{commit.repoKey && commit.repoKey !== 'unknown' && (
<Tag minimal round small
intent={REPO_INTENT[commit.repoKey] ?? Intent.NONE}
style={{ fontSize: '0.68rem', flexShrink: 0 }}>
{commit.repoKey}
</Tag>
)}
<div className="opc-commit-info">
<div className="opc-commit-msg">{commit.subject}</div>
<div className="opc-commit-meta">{commit.author} · {commit.date}</div>
@@ -297,7 +355,15 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
setAutoCommits(auto);
setPinned(pins);
const allHashes = [...new Set([...auto.map(c => c.hash), ...pins.map(c => c.hash)])];
if (allHashes.length > 0) setCoverage(await getBranchCoverage(allHashes));
if (allHashes.length > 0) {
const perRepoCoverage = await Promise.all(
KNOWN_REPOS.map(async repoKey => ({
repoKey,
coverage: await getBranchCoverageForRepo(repoKey, allHashes).catch(() => [] as BranchCoverage[]),
}))
);
setCoverage(aggregateCoverage(perRepoCoverage));
}
} catch { /* non-critical — API may not be up */ }
finally { setLoaded(true); }
})();
@@ -329,7 +395,13 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
setPinned(prev => [...prev, c]);
setPinInput('');
const allHashes = [...new Set([...autoCommits.map(x => x.hash), ...pinned.map(x => x.hash), c.hash])];
setCoverage(await getBranchCoverage(allHashes));
const perRepoCoverage = await Promise.all(
KNOWN_REPOS.map(async repoKey => ({
repoKey,
coverage: await getBranchCoverageForRepo(repoKey, allHashes).catch(() => [] as BranchCoverage[]),
}))
);
setCoverage(aggregateCoverage(perRepoCoverage));
} catch (e) { setPinError(String(e)); }
finally { setPinning(false); }
};
@@ -339,14 +411,33 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
const c = await pinCommit(opc.id, commit.hash, 'amadzarak');
setPinned(prev => [...prev, c]);
const allHashes = [...new Set([...autoCommits.map(x => x.hash), ...pinned.map(x => x.hash), c.hash])];
setCoverage(await getBranchCoverage(allHashes));
const perRepoCoverage = await Promise.all(
KNOWN_REPOS.map(async repoKey => ({
repoKey,
coverage: await getBranchCoverageForRepo(repoKey, allHashes).catch(() => [] as BranchCoverage[]),
}))
);
setCoverage(aggregateCoverage(perRepoCoverage));
} catch { /* no-op */ }
};
const handleUnpin = async (hash: string) => {
try {
await unpinCommit(opc.id, hash);
setPinned(prev => prev.filter(c => c.hash !== hash));
const remaining = pinned.filter(c => c.hash !== hash);
setPinned(remaining);
const allHashes = [...new Set([...autoCommits.map(x => x.hash), ...remaining.map(x => x.hash)])];
if (allHashes.length > 0) {
const perRepoCoverage = await Promise.all(
KNOWN_REPOS.map(async repoKey => ({
repoKey,
coverage: await getBranchCoverageForRepo(repoKey, allHashes).catch(() => [] as BranchCoverage[]),
}))
);
setCoverage(aggregateCoverage(perRepoCoverage));
} else {
setCoverage([]);
}
} catch { /* no-op */ }
};
@@ -367,7 +458,7 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
{linkedBranch.name}
</Tag>
<a
href={`https://opc.clarity.test/Clarity/Clarity/src/branch/${encodeURIComponent(linkedBranch.name)}`}
href={`https://opc.clarity.test/ClarityStack/Clarity/src/branch/${encodeURIComponent(linkedBranch.name)}`}
target="_blank" rel="noreferrer"
style={{ fontSize: '0.8rem', color: 'var(--bp4-intent-primary)' }}>
Open in Gitea
@@ -394,26 +485,47 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
</div>
<Divider style={{ margin: '0.5rem 0 0.75rem' }} />
{/* Branch coverage */}
{coverage.length > 0 && (
<div className="opc-branch-coverage">
<div className="opc-field-label" style={{ marginBottom: '0.5rem' }}>Branch Coverage</div>
<div className="opc-branch-chips">
{coverage.map(b => (
<Tooltip key={b.branch}
content={b.contains
? `All linked commits reachable from ${b.branch}`
: `Not all linked commits have reached ${b.branch} yet`}>
<Tag intent={b.contains ? Intent.SUCCESS : Intent.NONE}
icon={b.contains ? 'tick-circle' : 'minus'}
minimal={!b.contains} round>
{b.branch}{b.isHead ? ' ★' : ''}
</Tag>
</Tooltip>
))}
{/* SDLC Delivery Chain */}
{coverage.length > 0 && (() => {
const summary = deriveSdlcSummary(coverage);
return (
<div className="opc-delivery-chain">
<div className="opc-field-label" style={{ marginBottom: '0.6rem' }}>Delivery Chain</div>
<div className="opc-sdlc-pipeline">
{SDLC_STAGES.map((stage, i) => {
const hit = coverage.find(c => c.branch === stage.branch);
const reached = hit?.contains ?? false;
return (
<div key={stage.branch} className="opc-sdlc-stage-item">
{i > 0 && <span className="opc-sdlc-arrow"></span>}
<Tooltip content={
reached
? `All linked commits have reached ${stage.label}`
: hit
? `Not all linked commits have reached ${stage.label} yet`
: `${stage.label} branch not found locally`
}>
<Tag
intent={reached ? stage.intent : Intent.NONE}
icon={reached ? 'tick-circle' : 'circle'}
minimal={!reached}
round
>
{stage.label}
</Tag>
</Tooltip>
</div>
);
})}
</div>
{summary && (
<div className="opc-sdlc-furthest">
Furthest: <strong>{summary.label}</strong>
</div>
)}
</div>
</div>
)}
);
})()}
{/* Auto-detected */}
<div className="opc-commits-section-label">
@@ -615,8 +727,8 @@ function OpcDetailDrawer({ opc, onClose, onUpdate }: {
</div>
} />
{/* Commits */}
<Tab id="commits" title="Commits" panel={
{/* Code & SDLC */}
<Tab id="commits" title="Code & SDLC" panel={
<CommitsTab opc={opc} isActive={activeTab === 'commits'} />
} />
</Tabs>
@@ -666,7 +778,7 @@ function OpcCreateDrawer({ onClose, onCreate }: { onClose: () => void; onCreate:
<Callout intent={Intent.PRIMARY} icon="info-sign" style={{ marginBottom: '1.25rem' }}>
An OPC tracks a change, task, or business requirement through its full lifecycle.
{nextNumber && <> This will be saved as <strong>{nextNumber}</strong>.</>}{' '}
Include the OPC number in commit messages on <code>develop</code> to link check-ins automatically.
Include the OPC number in commit messages to link check-ins automatically across any repo.
</Callout>
{error && <Callout intent={Intent.DANGER} style={{ marginBottom: '1rem' }}>{error}</Callout>}
<FormGroup label="Title" labelInfo="(required)">
@@ -746,7 +858,7 @@ export default function OpcPage() {
<div className="page-header">
<div>
<h1>OPC</h1>
<p>Online Project Communication change orders, tasks, and business requirements.</p>
<p>Online Project Communication track changes, requirements, and SDLC delivery chain.</p>
</div>
<Button intent={Intent.PRIMARY} icon="plus" text="New OPC"
onClick={() => { setSelected(null); setCreating(true); }} />