OPC # 0001: Extract OPC into standalone repo
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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); }} />
|
||||
|
||||
Reference in New Issue
Block a user