Skip to content

Commit 8ea8abe

Browse files
alicodingclaude
andauthored
feat: atlas projections -- mirror rendering, traceability matrix, coverage (goal 0064, ADR-0038) (#152)
One graph, three read surfaces, no new storage: MirrorContent (Go-side goldmark render for markdown, plain text passthrough, base64 image, type+size fallback for everything else, size-capped) resolves a card's MirrorPath into the overlay; an editable Mirror path field lets a card point at a file directly, since no write path sets it yet. The traceability matrix and coverage counts project entirely from the frontend's already-fetched card/link/kind data -- no new bound method, pure and unit-tested like atlasGrouping.ts's existing derivations. Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9a694a0 commit 8ea8abe

23 files changed

Lines changed: 1266 additions & 7 deletions

frontend/bindings/github.com/alicoding/mill/internal/domain/atlas/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// This file is automatically generated. DO NOT EDIT
33

44
export {
5+
MirrorKind,
56
ViewMode
67
} from "./models.js";
78

@@ -11,5 +12,6 @@ export type {
1112
LensSetting,
1213
Link,
1314
LinkKind,
15+
MirrorContent,
1416
Position
1517
} from "./models.js";

frontend/bindings/github.com/alicoding/mill/internal/domain/atlas/models.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,58 @@ export interface LinkKind {
179179
"Seed": seedorigin$0.Origin;
180180
}
181181

182+
/**
183+
* MirrorContent is a card's MirrorPath resolved into a read-only
184+
* overlay preview (docs/goals/0064), returned by atlassvc.AtlasService.
185+
* MirrorContent. Size is always populated (even when TooLarge or
186+
* Kind==MirrorKindOther) so the overlay's type+size+reveal fallback
187+
* always has something to show. Content holds rendered HTML for
188+
* MirrorKindMarkdown, raw text for MirrorKindText, or base64-encoded
189+
* bytes (paired with MimeType) for MirrorKindImage -- empty for
190+
* MirrorKindOther or whenever TooLarge is true.
191+
*/
192+
export interface MirrorContent {
193+
"Kind": MirrorKind;
194+
"MimeType": string;
195+
"Size": number;
196+
"Content": string;
197+
"TooLarge": boolean;
198+
}
199+
200+
/**
201+
* MirrorKind classifies a mirrored file for the card overlay's preview
202+
* (docs/goals/0064): a pure, extension-only decision, no I/O -- the
203+
* service layer (atlassvc) is what actually stats/reads the file this
204+
* classifies.
205+
*/
206+
export enum MirrorKind {
207+
/**
208+
* The Go zero value for the underlying type of the enum.
209+
*/
210+
$zero = "",
211+
212+
/**
213+
* MirrorKindMarkdown renders through the markdown adapter to HTML.
214+
*/
215+
MirrorKindMarkdown = "markdown",
216+
217+
/**
218+
* MirrorKindText renders as plain, preformatted text.
219+
*/
220+
MirrorKindText = "text",
221+
222+
/**
223+
* MirrorKindImage renders inline as an image.
224+
*/
225+
MirrorKindImage = "image",
226+
227+
/**
228+
* MirrorKindOther never has its content loaded -- the overlay shows
229+
* only its kind and size, plus the existing reveal-file action.
230+
*/
231+
MirrorKindOther = "other",
232+
};
233+
182234
/**
183235
* Position is a card's location within its PARENT's canvas -- only
184236
* meaningful when the parent's EffectiveViewMode is ViewModeCanvas;

frontend/bindings/github.com/alicoding/mill/internal/services/atlassvc/atlasservice.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ export function Links(): $CancellablePromise<atlas$0.Link[] | null> {
154154
return $Call.ByID(1082434947);
155155
}
156156

157+
/**
158+
* MirrorContent resolves cardID's MirrorPath into a read-only overlay
159+
* preview: markdown renders to HTML, plain text passes through as-is,
160+
* an image becomes base64-encoded bytes paired with its MIME type, and
161+
* anything else (or anything over mirrorPreviewMaxBytes) reports only
162+
* its kind and size -- the overlay's existing reveal-file action is
163+
* what a user reaches for beyond that.
164+
*/
165+
export function MirrorContent(cardID: string): $CancellablePromise<atlas$0.MirrorContent> {
166+
return $Call.ByID(3797489642, cardID);
167+
}
168+
157169
/**
158170
* MoveCard reparents a card (sibling-vs-child move, ADR-0038's
159171
* create-time framing extended to a later move) -- rejects a

frontend/e2e/atlas.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,111 @@ test('the space toolbar Share menu bundles the space as context and copies its l
347347
await expect.poll(() => readClipboardText(page)).toBe('https://example.com/project-charter')
348348
})
349349
})
350+
351+
// Atlas projections (docs/goals/0064, ADR-0038): mirror-content
352+
// rendering, the traceability matrix, and coverage -- each proven
353+
// against the real seed, the same pattern every test above follows.
354+
355+
test('a card with a Mirror path pointing at a markdown file renders its content read-only in the overlay', async ({ page }) => {
356+
const fs = await import('node:fs')
357+
const os = await import('node:os')
358+
const path = await import('node:path')
359+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mill-e2e-atlas-mirror-'))
360+
const file = path.join(dir, 'notes.md')
361+
fs.writeFileSync(file, '# Field notes\n\nSome **captured** text.')
362+
363+
const title = 'ZzE2eAtlasMirrorCard'
364+
await page.goto('/')
365+
await page.getByRole('link', { name: 'Atlas' }).click()
366+
await page.getByTestId('atlas-shelf-card').filter({ hasText: 'My space' }).click()
367+
await expect(page.getByTestId('atlas-canvas')).toBeVisible()
368+
369+
await page.getByTestId('atlas-add-button').click()
370+
await page.getByTestId('atlas-add-child').click()
371+
await page.getByTestId('atlas-create-kind').selectOption({ label: '📄 Document' })
372+
await page.getByTestId('atlas-create-title').fill(title)
373+
await page.getByRole('button', { name: 'Create' }).click()
374+
375+
const newCard = page.getByTestId('atlas-canvas-card').filter({ hasText: title })
376+
await expect(newCard).toBeVisible()
377+
await newCard.getByTestId('atlas-card-info').click()
378+
const overlay = page.locator('[data-component="atlas-card-overlay"]')
379+
await expect(overlay).toBeVisible()
380+
381+
await overlay.getByTestId('atlas-overlay-mirror-path').fill(file)
382+
await overlay.getByTestId('atlas-overlay-save').click()
383+
await expect(overlay).not.toBeVisible()
384+
385+
await newCard.getByTestId('atlas-card-info').click()
386+
await expect(overlay).toBeVisible()
387+
await expect(overlay.getByTestId('atlas-mirror-markdown')).toBeVisible()
388+
await expect(overlay.getByTestId('atlas-mirror-markdown')).toContainText('Field notes')
389+
await expect(overlay.getByTestId('atlas-mirror-markdown').locator('strong')).toContainText('captured')
390+
391+
// Cleanup (testing.md's within-file cleanup discipline).
392+
await overlay.getByTestId('atlas-overlay-delete').click()
393+
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete' }).click()
394+
await expect(newCard).not.toBeVisible()
395+
})
396+
397+
test('the traceability matrix pivots a space\'s cards by kind against link kinds, with an absent cell shown explicitly', async ({ page }) => {
398+
await page.goto('/')
399+
await page.getByRole('link', { name: 'Atlas' }).click()
400+
await page.getByTestId('atlas-shelf-card').filter({ hasText: 'My space' }).click()
401+
await page.getByTestId('atlas-canvas-card').filter({ hasText: 'Example area' }).click()
402+
await expect(page.getByTestId('atlas-shelves')).toBeVisible()
403+
404+
await page.getByTestId('atlas-open-matrix').click()
405+
const dialog = page.locator('[data-component="atlas-matrix-dialog"]')
406+
await expect(dialog).toBeVisible()
407+
408+
// Row kind "Contact" -- the seeded "Ada Lovelace" card has an
409+
// outgoing "relates to" link to "Project charter", so its cell names
410+
// that target.
411+
await dialog.getByTestId('atlas-matrix-row-kind').selectOption({ label: '👤 Contact' })
412+
await expect(dialog.getByTestId('atlas-matrix-target').filter({ hasText: 'Project charter' })).toBeVisible()
413+
414+
// Row kind "Document" -- the seeded "Project charter" card has no
415+
// OUTGOING links of its own (only an incoming one), so its cell is
416+
// explicitly absent, never an ambiguous blank.
417+
await dialog.getByTestId('atlas-matrix-row-kind').selectOption({ label: '📄 Document' })
418+
await expect(dialog.getByTestId('atlas-matrix-absent-cell')).toBeVisible()
419+
await expect(dialog.getByTestId('atlas-matrix-absent-cell')).toHaveText('None')
420+
421+
await page.keyboard.press('Escape')
422+
await expect(dialog).not.toBeVisible()
423+
})
424+
425+
test('coverage counts a space\'s cards missing a link and missing a mirror, with the missing list navigating to a card', async ({ page }) => {
426+
await page.goto('/')
427+
await page.getByRole('link', { name: 'Atlas' }).click()
428+
await page.getByTestId('atlas-shelf-card').filter({ hasText: 'My space' }).click()
429+
await expect(page.getByTestId('atlas-canvas')).toBeVisible()
430+
431+
// "My space" itself has exactly two seeded children: "Getting
432+
// started" (has an outgoing "relates to" link to "Ada Lovelace") and
433+
// "Example area" (the container card itself has no link of its own)
434+
// -- a hand-countable 1/2 linked. Neither child carries a mirror at
435+
// THIS level (the seeded mirror lives one level deeper, on "Project
436+
// charter") -- a hand-countable 0/2 mirrored.
437+
await page.getByTestId('atlas-open-coverage').click()
438+
const dialog = page.locator('[data-component="atlas-coverage-dialog"]')
439+
await expect(dialog).toBeVisible()
440+
441+
await expect(dialog.getByTestId('atlas-coverage-link-value')).toHaveText('1/2 linked')
442+
await expect(dialog.getByTestId('atlas-coverage-mirror-value')).toHaveText('0/2 mirrored')
443+
444+
await dialog.getByTestId('atlas-coverage-link-toggle').click()
445+
await expect(dialog.getByTestId('atlas-coverage-missing-item').filter({ hasText: 'Example area' })).toBeVisible()
446+
447+
await dialog.getByTestId('atlas-coverage-mirror-toggle').click()
448+
const missingItem = dialog.getByTestId('atlas-coverage-missing-item').filter({ hasText: 'Getting started' })
449+
await expect(missingItem).toBeVisible()
450+
await missingItem.click()
451+
452+
await expect(dialog).not.toBeVisible()
453+
const overlay = page.locator('[data-component="atlas-card-overlay"]')
454+
await expect(overlay).toBeVisible()
455+
await expect(overlay.getByTestId('atlas-overlay-title')).toHaveValue('Getting started')
456+
await page.keyboard.press('Escape')
457+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.markdownBody {
2+
max-height: 480px;
3+
overflow-y: auto;
4+
padding: var(--base-size-8) var(--base-size-12);
5+
border: 1px solid var(--borderColor-default);
6+
border-radius: var(--borderRadius-medium);
7+
color: var(--fgColor-default);
8+
font-size: 14px;
9+
line-height: 1.5;
10+
}
11+
.markdownBody :global(h1),
12+
.markdownBody :global(h2),
13+
.markdownBody :global(h3) {
14+
border-bottom: 1px solid var(--borderColor-muted);
15+
padding-bottom: var(--base-size-4);
16+
margin-top: var(--base-size-16);
17+
margin-bottom: var(--base-size-8);
18+
}
19+
.markdownBody :global(code) {
20+
background-color: var(--bgColor-muted);
21+
border-radius: var(--borderRadius-small);
22+
padding: 0 var(--base-size-4);
23+
}
24+
.markdownBody :global(pre) {
25+
background-color: var(--bgColor-muted);
26+
border-radius: var(--borderRadius-medium);
27+
padding: var(--base-size-8);
28+
overflow-x: auto;
29+
}
30+
.markdownBody :global(table) {
31+
border-collapse: collapse;
32+
}
33+
.markdownBody :global(th),
34+
.markdownBody :global(td) {
35+
border: 1px solid var(--borderColor-default);
36+
padding: var(--base-size-4) var(--base-size-8);
37+
}
38+
.textBody {
39+
max-height: 480px;
40+
overflow: auto;
41+
padding: var(--base-size-8) var(--base-size-12);
42+
border: 1px solid var(--borderColor-default);
43+
border-radius: var(--borderRadius-medium);
44+
background-color: var(--bgColor-muted);
45+
font-size: 12px;
46+
white-space: pre-wrap;
47+
word-break: break-word;
48+
}
49+
.image {
50+
max-width: 100%;
51+
max-height: 480px;
52+
border-radius: var(--borderRadius-medium);
53+
border: 1px solid var(--borderColor-default);
54+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useEffect, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { Text } from '@primer/react'
4+
import { MirrorKind } from '../../bindings/github.com/alicoding/mill/internal/domain/atlas/models'
5+
import type { MirrorContent } from '../../bindings/github.com/alicoding/mill/internal/domain/atlas/models'
6+
import { AtlasService } from '../shared/bindings'
7+
import runbookStyles from '../shared/ListCard.module.css'
8+
import styles from './AtlasCardMirrorPreview.module.css'
9+
10+
// 1000-based (KB/MB, not KiB/MiB) -- matches how a file's size is
11+
// quoted everywhere else a user sees one (Finder, most OS file
12+
// pickers), not the binary convention.
13+
function formatMirrorSize(bytes: number): string {
14+
if (bytes < 1000) return `${bytes} B`
15+
if (bytes < 1000 * 1000) return `${(bytes / 1000).toFixed(1)} KB`
16+
return `${(bytes / (1000 * 1000)).toFixed(1)} MB`
17+
}
18+
19+
// Renders a card's MirrorPath read-only inside the overlay (docs/
20+
// goals/0064): markdown through the Go-side converter (goldmark's
21+
// default safe-render mode -- MirrorContent's own doc comment covers
22+
// why dangerouslySetInnerHTML below is safe against an embedded raw
23+
// <script>), plain text preformatted, an image inline via a base64
24+
// data URI, and everything else (plus anything over the server's size
25+
// cap) as a plain kind+size line -- the overlay's existing "Show in
26+
// Finder" action covers the rest. Re-fetches whenever mirrorPath
27+
// changes (a fresh "Update now" refresh, or the field itself edited
28+
// and saved), never on every render.
29+
export function AtlasCardMirrorPreview({ cardID, mirrorPath }: { cardID: string; mirrorPath: string }) {
30+
const { t } = useTranslation('atlas')
31+
const [content, setContent] = useState<MirrorContent | null>(null)
32+
const [error, setError] = useState('')
33+
34+
useEffect(() => {
35+
setContent(null)
36+
setError('')
37+
AtlasService.MirrorContent(cardID)
38+
.then(setContent)
39+
.catch((err) => setError(String(err)))
40+
}, [cardID, mirrorPath])
41+
42+
if (error) {
43+
return <Text as="p" size="small" className={runbookStyles.error} data-testid="atlas-mirror-error">{error}</Text>
44+
}
45+
if (!content) {
46+
return <Text as="p" size="small" className={runbookStyles.muted} data-testid="atlas-mirror-loading">{t('overlay.mirrorLoading')}</Text>
47+
}
48+
49+
if (content.TooLarge || content.Kind === MirrorKind.MirrorKindOther) {
50+
return (
51+
<Text as="p" size="small" className={runbookStyles.muted} data-testid="atlas-mirror-fallback">
52+
{content.TooLarge
53+
? t('overlay.mirrorTooLarge', { size: formatMirrorSize(content.Size) })
54+
: t('overlay.mirrorUnsupported', { size: formatMirrorSize(content.Size) })}
55+
</Text>
56+
)
57+
}
58+
59+
if (content.Kind === MirrorKind.MirrorKindMarkdown) {
60+
return (
61+
<div
62+
className={styles.markdownBody}
63+
data-testid="atlas-mirror-markdown"
64+
// Safe: goldmark's default (non-unsafe) render mode never passes
65+
// raw HTML through unescaped -- render_test.go pins that property.
66+
dangerouslySetInnerHTML={{ __html: content.Content }}
67+
/>
68+
)
69+
}
70+
71+
if (content.Kind === MirrorKind.MirrorKindText) {
72+
return <pre className={styles.textBody} data-testid="atlas-mirror-text">{content.Content}</pre>
73+
}
74+
75+
return <img className={styles.image} data-testid="atlas-mirror-image" src={`data:${content.MimeType};base64,${content.Content}`} alt={mirrorPath} />
76+
}

frontend/src/atlas/AtlasCardOverlay.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { StatusStamp } from '../shared/StatusStamp'
1313
import { AtlasKindChip } from './AtlasKindChip'
1414
import { AtlasFieldsForm } from './AtlasFieldsForm'
1515
import { AtlasCardOverlayLinks } from './AtlasCardOverlayLinks'
16+
import { AtlasCardMirrorPreview } from './AtlasCardMirrorPreview'
1617
import { atlasCardShareActions } from './atlasCardShare'
1718
import { useConfirmDelete } from '../shared/useConfirmDelete'
1819
import runbookStyles from '../shared/ListCard.module.css'
@@ -38,6 +39,7 @@ export function AtlasCardOverlay({ card, kind, allCards, links, linkKinds, onClo
3839
const [note, setNote] = useState(card.Note)
3940
const [fields, setFields] = useState<Record<string, string>>((card.Fields ?? {}) as Record<string, string>)
4041
const [source, setSource] = useState(card.Source)
42+
const [mirrorPath, setMirrorPath] = useState(card.MirrorPath)
4143
const [refreshWorkflowID, setRefreshWorkflowID] = useState(card.RefreshWorkflowID)
4244
const [saving, setSaving] = useState(false)
4345
const [error, setError] = useState('')
@@ -98,7 +100,7 @@ export function AtlasCardOverlay({ card, kind, allCards, links, linkKinds, onClo
98100
setSaving(true)
99101
setError('')
100102
try {
101-
await AtlasService.UpdateCard(card.ID, title, note, fields, source, card.MirrorPath, refreshWorkflowID)
103+
await AtlasService.UpdateCard(card.ID, title, note, fields, source, mirrorPath, refreshWorkflowID)
102104
onSaved()
103105
onClose()
104106
} catch (err) {
@@ -171,13 +173,21 @@ export function AtlasCardOverlay({ card, kind, allCards, links, linkKinds, onClo
171173
)}
172174
</FormControl>
173175

174-
{card.MirrorPath && (
175-
<Text as="p" size="small" className={runbookStyles.muted}>{t('overlay.mirrorPath', { path: card.MirrorPath })}</Text>
176-
)}
176+
<FormControl>
177+
<FormControl.Label>{t('overlay.mirrorPathLabel')}</FormControl.Label>
178+
<TextInput value={mirrorPath} data-testid="atlas-overlay-mirror-path" onChange={(e) => setMirrorPath(e.target.value)} block />
179+
</FormControl>
177180
<Text as="p" size="small" className={runbookStyles.muted}>
178181
{card.LastSyncedAt && formatUpdated(card.LastSyncedAt) ? t('overlay.lastSynced', { when: formatUpdated(card.LastSyncedAt) }) : t('overlay.neverSynced')}
179182
</Text>
180183

184+
{card.MirrorPath && (
185+
<Stack direction="vertical" gap="condensed" data-testid="atlas-card-mirror-content">
186+
<Text weight="semibold">{t('overlay.mirrorContentHeading')}</Text>
187+
<AtlasCardMirrorPreview cardID={card.ID} mirrorPath={card.MirrorPath} />
188+
</Stack>
189+
)}
190+
181191
<FormControl>
182192
<FormControl.Label>{t('overlay.refreshWorkflowLabel')}</FormControl.Label>
183193
<Stack direction="horizontal" gap="condensed" align="center">

0 commit comments

Comments
 (0)