Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
714a925
feat(calm-hub): GitHub-backed read-only storage for CALM resources
byrash Sep 8, 2026
6b73d0f
fix(calm-hub): adapt GitHubVersionService to slice 3's cache API and …
jpgough-ms Sep 9, 2026
191d13f
fix(calm-hub): adapt GitHubVersionService to the rescoped GitHubApiRe…
jpgough-ms Sep 9, 2026
5c9a3ee
fix(calm-hub): restore OidcRoleResolver removed as unused by slice 4'…
jpgough-ms Sep 9, 2026
d222dcd
fix(calm-hub): security and correctness fixes on the GitHub storage b…
jpgough-ms Sep 9, 2026
4db3c9e
feat(calm-hub): derive domain-level read access on the GitHub backend
jpgough-ms Sep 9, 2026
e1d7ad8
chore(calm-hub): remove GitHub-mode dead code left over from prior de…
jpgough-ms Sep 9, 2026
6e2c231
docs(calm-hub): restore producer javadoc dropped by the GitHub-mode w…
jpgough-ms Sep 9, 2026
5ad14c5
fix(calm-hub): populate ClasspathCoreSchemaStore's classpath resources
jpgough-ms Sep 9, 2026
1069086
test(calm-hub): close GitHub-mode unit test gaps and drop the now-red…
jpgough-ms Sep 9, 2026
5ab0c36
test(calm-hub): add an end-to-end github-mode integration test for DO…
jpgough-ms Sep 9, 2026
b02d58a
fix(calm-hub): remove the unused @TempDir parameter github-code-quali…
jpgough-ms Sep 9, 2026
ad3cb29
refactor(calm-hub): remove the Building Block concept, alias building…
jpgough-ms Sep 10, 2026
c14327b
fix(calm-hub): revert CONTROL from the namespace-scoped front controller
jpgough-ms Sep 10, 2026
484a912
fix(calm-hub): remove 'latest' from the shared version contract
jpgough-ms Sep 10, 2026
b9247f4
refactor(calm-hub): move GitHubStoreConfig out of the github util pac…
jpgough-ms Sep 10, 2026
ace58c4
refactor(calm-hub): move GitHubRepoSync out of the github util packag…
jpgough-ms Sep 10, 2026
f99b7fc
refactor(calm-hub): move the registry types out of the github util pa…
jpgough-ms Sep 10, 2026
e3235f6
refactor(calm-hub): move GitHubFileReader out of the util package as …
jpgough-ms Sep 10, 2026
496cca4
refactor(calm-hub): move GitHubApiResponseCache out of the github uti…
jpgough-ms Sep 10, 2026
00ba9e4
refactor(calm-hub): move GitHubVersionService out of the util package…
jpgough-ms Sep 10, 2026
5022757
refactor(calm-hub): move GitHubCloneManager out of the github util pa…
jpgough-ms Sep 10, 2026
74c4a11
refactor(calm-hub): move NamespaceAccessFilter out of the github util…
jpgough-ms Sep 10, 2026
fdf7ccb
refactor(calm-hub): finish dissolving the github util package
jpgough-ms Sep 10, 2026
b16ff87
refactor(calm-hub): extract shared GitHub store base, finish construc…
jpgough-ms Sep 10, 2026
0d19241
docs(calm-hub): add house-standard JavaDoc to the remaining GitHub st…
jpgough-ms Sep 10, 2026
45a6c27
test(calm-hub): scope Mockito lenient strictness, drop a package-priv…
jpgough-ms Sep 10, 2026
4e29467
test(calm-hub): add Docker-based end-to-end coverage for this rework'…
jpgough-ms Sep 10, 2026
aa316e0
fix(calm-hub): remove unsanitised filesystem check flagged by CodeQL
jpgough-ms Sep 11, 2026
01eed1b
test(calm-hub): cover GitHubControlStore's registry-race and local-re…
jpgough-ms Sep 11, 2026
01d302f
fix(calm-hub): fix domain derivation and configuration reads in GitHu…
jpgough-ms Sep 11, 2026
1f3b667
fix(calm-hub): make NamespaceAccessFilter use the shared grant-resolu…
jpgough-ms Sep 11, 2026
249f44f
fix(calm-hub): add a git transport timeout, pull the configured branc…
jpgough-ms Sep 11, 2026
8988177
fix(calm-hub): record a sync failure metric when every namespace fail…
jpgough-ms Sep 11, 2026
71718d3
fix(calm-hub): add a GitHub-mode branch to PatternLayoutStoreProducer
jpgough-ms Sep 11, 2026
0f25dbe
fix(calm-hub): make a uniqueId collision deterministic and observable
jpgough-ms Sep 11, 2026
3fce8ef
fix(calm-hub): stop one search result type from starving the others
jpgough-ms Sep 11, 2026
294f3fc
fix(calm-hub): return 501 not 400 when adding a version in GitHub mode
jpgough-ms Sep 11, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { TimelineHeader } from './TimelineHeader.js';

describe('TimelineHeader', () => {
it('prepends "v" for semver versions', () => {
render(<TimelineHeader currentVersion="1.5.0" />);
const pill = screen.getByTestId('timeline-version-pill');
expect(pill).toHaveTextContent('v1.5.0');
});

it('does not prepend "v" for commit SHAs', () => {
render(<TimelineHeader currentVersion="cb7686e" />);
const pill = screen.getByTestId('timeline-version-pill');
expect(pill).toHaveTextContent('cb7686e');
expect(pill.textContent).not.toMatch(/^v/);
});

it('does not prepend "v" for full-length commit SHAs', () => {
render(<TimelineHeader currentVersion="e46b2d5a1f3c9d8b7e2a0f4c6d8e1b3a5c7d9f0e" />);
const pill = screen.getByTestId('timeline-version-pill');
expect(pill.textContent).not.toMatch(/^v/);
});

it('prepends "v" for versions with non-hex characters', () => {
render(<TimelineHeader currentVersion="2.0.0-beta" />);
const pill = screen.getByTestId('timeline-version-pill');
expect(pill).toHaveTextContent('v2.0.0-beta');
});

it('sets the title attribute with the raw version', () => {
render(<TimelineHeader currentVersion="cb7686e" />);
const pill = screen.getByTestId('timeline-version-pill');
expect(pill).toHaveAttribute('title', 'Viewing version cb7686e');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ interface TimelineHeaderProps {
* explanatory copy makes it obvious that clicking a moment re-renders both the
* Diagram and JSON views.
*/
const isCommitSha = (v: string) => /^[0-9a-f]{5,40}$/.test(v);

export function TimelineHeader({ currentVersion, children }: TimelineHeaderProps) {
const displayVersion = isCommitSha(currentVersion) ? currentVersion : `v${currentVersion}`;
return (
<div className="flex items-center gap-2 shrink-0" style={{ paddingTop: 4 }}>
<IoTimeOutline size={14} style={{ color: colors.ink[500], strokeWidth: 2 }} />
Expand Down Expand Up @@ -46,7 +49,7 @@ export function TimelineHeader({ currentVersion, children }: TimelineHeaderProps
}}
title={`Viewing version ${currentVersion}`}
>
v{currentVersion}
{displayVersion}
</span>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,58 @@ describe('DocumentDetailSection', () => {
expect(mockFetchVersionsByCustomId).toHaveBeenCalledWith('test-ns', 'my-payment-standard', 'Standards');
expect(mockFetchStandardVersions).not.toHaveBeenCalled();
});

it('renders markdown content when data is a markdown string', () => {
const data: Data = {
id: 'std-123',
version: 'latest',
name: 'test-ns',
calmType: 'Standards',
data: '# TLS Policy\n\nAll services must use TLS 1.2+.',
};

render(
<MemoryRouter>
<DocumentDetailSection data={data} />
</MemoryRouter>
);

expect(screen.getByText('All services must use TLS 1.2+.')).toBeInTheDocument();
});

it('shows display name from markdown heading in breadcrumb', () => {
const data: Data = {
id: '12345',
version: 'latest',
name: 'test-ns',
calmType: 'Standards',
data: '# My Standard Name\n\nContent.',
};

const { container } = render(
<MemoryRouter>
<DocumentDetailSection data={data} />
</MemoryRouter>
);

expect(container.textContent).toContain('My Standard Name');
});

it('shows type label in breadcrumb', () => {
const data: Data = {
id: 'std-1',
version: 'latest',
name: 'fae-calm',
calmType: 'Standards',
data: '# Test\n\nBody.',
};

const { container } = render(
<MemoryRouter>
<DocumentDetailSection data={data} />
</MemoryRouter>
);

expect(container.textContent).toContain('Standards');
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { IoGridOutline, IoGitNetworkOutline } from 'react-icons/io5';
import Markdown from 'react-markdown';
import { Data, isSlug } from '../../../model/calm.js';
import { CalmService } from '../../../service/calm-service.js';
import { sortVersionsDescending } from '../../../model/version.js';
Expand All @@ -11,6 +12,24 @@ interface DocumentDetailSectionProps {
data?: Data;
}

function getDisplayName(data: Data): string {
if (typeof data.data === 'string') {
const content = data.data as string;
const headingMatch = content.match(/^#\s+(.+)$/m);
if (headingMatch) return headingMatch[1];
}
if (typeof data.data === 'object' && data.data && 'name' in (data.data as object)) {
return String((data.data as Record<string, unknown>).name);
}
return data.id;
}

function isMarkdownContent(data: Data): boolean {
if (typeof data.data !== 'string') return false;
const content = data.data as string;
return content.startsWith('#') || content.startsWith('---') || !content.startsWith('{');
}

function calmTypeToUrlSegment(calmType: string): string {
switch (calmType) {
case 'Standards': return 'standards';
Expand Down Expand Up @@ -69,14 +88,22 @@ export function DocumentDetailSection({ data }: DocumentDetailSectionProps) {
icon={getIcon()}
namespace={data.name}
id={data.id}
displayName={getDisplayName(data)}
typeLabel={data.calmType}
version={data.version}
typeSegment={calmTypeToUrlSegment(data.calmType)}
versions={versions}
onVersionChange={handleVersionChange}
/>

<div className="flex-1 min-h-0 overflow-auto bg-base-200">
<JsonRenderer json={data} />
{isMarkdownContent(data) ? (
<div className="prose prose-sm max-w-none p-6 bg-base-100">
<Markdown>{data.data as string}</Markdown>
</div>
) : (
<JsonRenderer json={data} />
)}
</div>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions calm-hub-ui/src/hub/components/namespace-page/ItemCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ export function ItemCard({
const chip =
meta !== undefined
? meta
: versionCount !== undefined
: versionCount !== undefined && versionCount > 0
? `${versionCount} ${versionCount === 1 ? 'version' : 'versions'}`
: customId;
: versionCount === 0
? customId || undefined
: customId;

return (
<article
Expand Down
17 changes: 17 additions & 0 deletions calm-hub-ui/src/model/version.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ describe('sortVersionsDescending', () => {
expect(result).toEqual(['2.0.0', '1.5.0', '1.0.0']);
expect(input).toEqual(['1.0.0', '2.0.0', '1.5.0']);
});

it('reverses SHA versions from chronological to newest-first', () => {
const input = ['abc1234', 'def5678', 'f1339ab'];
const result = sortVersionsDescending(input);
expect(result).toEqual(['f1339ab', 'def5678', 'abc1234']);
});

it('does not re-sort SHA versions alphabetically', () => {
const input = ['aaa1111', 'fff9999', 'bbb2222'];
const result = sortVersionsDescending(input);
expect(result).toEqual(['bbb2222', 'fff9999', 'aaa1111']);
});
});

describe('pickLatestVersion', () => {
Expand All @@ -44,4 +56,9 @@ describe('pickLatestVersion', () => {
it('returns the only version when the list has one entry', () => {
expect(pickLatestVersion(['3.4.5'])).toBe('3.4.5');
});

it('returns the last SHA (newest) from chronological list', () => {
const input = ['abc1234', 'def5678', 'f1339ab'];
expect(pickLatestVersion(input)).toBe('f1339ab');
});
});
5 changes: 5 additions & 0 deletions calm-hub-ui/src/model/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ export function compareVersions(a: string, b: string): number {

/**
* Return versions sorted newest-first.
* For commit SHAs, the backend returns chronological (oldest first) —
* reverse to get newest-first, matching semver sort behavior.
*/
export function sortVersionsDescending(versions: string[]): string[] {
if (versions.length > 0 && /^[0-9a-f]{5,40}$/.test(versions[0])) {
return [...versions].reverse();
}
return [...versions].sort((a, b) => compareVersions(b, a));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { DiagramActionsContext } from '../../context/DiagramActionsContext.js';
import { restoreLocation, setHostname } from '../../../test-support/window-location.js';

vi.mock('reactflow', () => ({
Handle: () => null,
Position: { Right: 'right', Left: 'left' },
Handle: ({ position, id }: { position: string; id: string }) => (
<div data-testid={`handle-${id}`} data-position={position} />
),
Position: { Top: 'top', Right: 'right', Bottom: 'bottom', Left: 'left' },
}));

function makeNodeProps(details?: Record<string, unknown>) {
Expand Down Expand Up @@ -188,4 +190,43 @@ describe('CustomNode — external URL support', () => {

expect(screen.getByTitle('Has detailed architecture')).toBeInTheDocument();
});

it('renders handles on all four sides for edge connection', () => {
const props = makeNodeProps();
const { container } = renderNode(props);

expect(container.querySelector('[data-testid="handle-top-target"]')).not.toBeNull();
expect(container.querySelector('[data-testid="handle-bottom-source"]')).not.toBeNull();
expect(container.querySelector('[data-testid="handle-left-target"]')).not.toBeNull();
expect(container.querySelector('[data-testid="handle-right-source"]')).not.toBeNull();
});

it('applies building-block-style background and text colors from metadata', () => {
const props = {
id: 'node-styled',
type: 'custom',
selected: false,
zIndex: 0,
isConnectable: true,
xPos: 0,
yPos: 0,
dragging: false,
data: {
label: 'Styled Node',
description: 'A styled node',
'node-type': 'webclient',
metadata: {
'building-block-style': {
background: '#1C4587',
text: '#ffffff',
},
},
},
};

const { container } = renderNode(props);
const nodeDiv = container.querySelector('[data-testid="custom-node"] > div');
expect(nodeDiv).not.toBeNull();
expect(nodeDiv?.getAttribute('style')).toContain('rgb(28, 69, 135)');
});
});
15 changes: 10 additions & 5 deletions calm-hub-ui/src/visualizer/components/reactflow/CustomNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export function CustomNode({ data }: NodeProps) {
const isUnknownArch = !!detailedArchitecture && archResolution.type === 'unknown';
const archPath = isSameOriginArch ? archResolution.path : undefined;

// Extract building-block-style colors from metadata (if present)
const buildingBlockStyle = data.metadata?.['building-block-style'] as { background?: string; text?: string } | undefined;

// Extract AIGF data (if present in node metadata)
const aigf = data.metadata?.aigf;
const riskLevel = aigf?.['risk-level'] || null;
Expand Down Expand Up @@ -169,21 +172,23 @@ export function CustomNode({ data }: NodeProps) {
{/* Base node - always visible, fixed size */}
<div
style={{
background: THEME.colors.card,
border: `2px solid ${borderColor}`,
background: buildingBlockStyle?.background || `${nodeTypeStyle.color}12`,
border: `2px solid ${buildingBlockStyle?.background || borderColor}`,
borderRadius: '12px',
padding: '16px',
width: '100%',
color: THEME.colors.foreground,
color: buildingBlockStyle?.text || THEME.colors.foreground,
fontSize: '14px',
fontWeight: 500,
boxShadow: isHovered ? THEME.shadows.lg : THEME.shadows.sm,
transition: 'box-shadow 0.3s ease-in-out',
}}
>
{/* Hidden handles to satisfy React Flow; floating edge computes actual attachment */}
<Handle type="source" position={Position.Right} id="source" style={{ opacity: 0 }} />
<Handle type="target" position={Position.Left} id="target" style={{ opacity: 0 }} />
<Handle type="target" position={Position.Top} id="top-target" style={{ opacity: 0 }} />
<Handle type="source" position={Position.Bottom} id="bottom-source" style={{ opacity: 0 }} />
<Handle type="target" position={Position.Left} id="left-target" style={{ opacity: 0 }} />
<Handle type="source" position={Position.Right} id="right-source" style={{ opacity: 0 }} />

<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '8px' }}>
<div style={{ fontWeight: 600, marginBottom: '4px', flex: 1, display: 'flex', alignItems: 'center', gap: '8px' }}>
Expand Down
Loading
Loading