Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions frontend/e2e/atlas-authoring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ test('atlas creation core: tray, placement popover, right-click create, sticky n
await expect(page.getByTestId('atlas-jump-no-matches')).toBeVisible()
await page.keyboard.press('Escape')

// "My space" seeds four children (goal 0095 slice 3 added "System
// landscape" alongside Getting started/Example area/Scratchpad) --
// a hand-countable 1/4 linked, 0/4 mirrored (same census
// atlas-projections.spec.ts's own coverage test pins).
// "My space" seeds three children (Getting started / Example area /
// Scratchpad; the reference-architecture landscape was de-seeded,
// ADR-0041's Update) -- a hand-countable 1/3 linked, 0/3 mirrored
// (same census atlas-projections.spec.ts's own coverage test pins).
await page.getByTestId('atlas-open-coverage').click()
const coverageDialog = page.locator('[data-component="atlas-coverage-dialog"]')
await expect(coverageDialog).toBeVisible()
await expect(coverageDialog.getByTestId('atlas-coverage-link-value')).toHaveText('1/4 linked')
await expect(coverageDialog.getByTestId('atlas-coverage-mirror-value')).toHaveText('0/4 mirrored')
await expect(coverageDialog.getByTestId('atlas-coverage-link-value')).toHaveText('1/3 linked')
await expect(coverageDialog.getByTestId('atlas-coverage-mirror-value')).toHaveText('0/3 mirrored')
await page.keyboard.press('Escape')
await expect(coverageDialog).not.toBeVisible()

Expand Down
103 changes: 59 additions & 44 deletions frontend/e2e/atlas-perspectives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import { ATLAS_KIND_TOPIC, selectKind } from './fixtures/kindPicker'
// Runs against the seeded "My space" tree (internal/domain/atlas/
// builtin.go): My space (root) holds Getting started, Scratchpad,
// Example area (which holds Ada Lovelace and Project charter), and
// System landscape (which holds Web app/Data store/Sync service, the
// seeded Current/Interim/Target perspectives' own scope); a seeded
// perspectives are user-authored (none ship seeded); each test
// builds what it asserts.
// link connects Getting started -> Ada Lovelace.

async function withServer(testInfo: { parallelIndex: number }, run: (page: Awaited<ReturnType<import('@playwright/test').Browser['newPage']>>) => Promise<void>): Promise<void> {
Expand Down Expand Up @@ -244,61 +244,76 @@ test('membership removes via the board context menu, and deleting a perspective
})
})

// The seeded reference-architecture example (goal 0095 slice 3,
// internal/domain/atlas/builtin.go's BuiltInPerspectives): a "System
// landscape" card (a direct child of "My space", never disturbing the
// pre-existing top-level census other specs pin -- e.g.
// atlas-projections.spec.ts's coverage stat) holding "Web app"/"Data
// store"/"Sync service", with three seeded perspectives -- "Current"
// (app + data store, wired directly), "Interim" (adds the sync
// service alongside the old link), "Target" (the old direct link is
// gone, only the new shape remains).
// Compare is proven over a USER-authored pair of perspectives -- no
// perspective ships seeded (concepts are the user's; only the
// capability ships). Link-level diff semantics stay proven at the Go
// layer (DiffPerspectives' own tests); this flow pins the UI: author,
// diff, and the empty-group omission.
// eslint-disable-next-line no-empty-pattern -- this test needs `testInfo` (the second arg), not any fixture.
test('the seeded reference-architecture example renders with no regression to the default view, and Compare shows the Current -> Target diff', async ({}, testInfo) => {
test('Compare shows the diff between two user-authored perspectives', async ({}, testInfo) => {
await withServer(testInfo, async (page) => {
// No regression: every pre-existing seeded card still renders
// alongside the new "System landscape" card.
await expect(noteCard(page, 'Getting started')).toBeVisible()
await expect(noteCard(page, 'Scratchpad')).toBeVisible()
await expect(groupCard(page, 'Example area')).toBeVisible()
const landscape = groupCard(page, 'System landscape')
await expect(landscape).toBeVisible()

// The seeded perspectives are scoped to "System landscape" -- drill
// in, then switch to "Interim": all three landscape cards render.
await landscape.getByTestId('atlas-group-header').click()
await expect(page.getByTestId('atlas-breadcrumb')).toContainText('System landscape')
// No perspectives ship: the switcher opens with only "All cards"
// and the create affordance.
await switcherButton(page).click()
await expect(switcherPopover(page).getByText('All cards', { exact: true })).toBeVisible()
await expect(switcherPopover(page).getByText('Compare perspectives', { exact: false })).toHaveCount(0)
await page.keyboard.press('Escape')

await createPerspective(page, 'Before')
await createPerspective(page, 'After')

// "After" is active (creation activates): authoring joins it.
const oldTitle = 'ZzE2eCompareOld'
const newTitle = 'ZzE2eCompareNew'
for (const title of [oldTitle, newTitle]) {
await page.getByTestId('atlas-add-button').click()
await page.getByTestId('atlas-add-child').click()
await selectKind(page, ATLAS_KIND_TOPIC, 'atlas-create-kind')
await page.getByTestId('atlas-create-title').fill(title)
await page.getByRole('button', { name: 'Create' }).click()
await expect(noteCard(page, title)).toBeVisible()
}

// The old card also joins "Before" via the context menu.
await switcherButton(page).click()
await switcherPopover(page).getByText('Interim', { exact: true }).click()
await expect(switcherButton(page)).toHaveText('Interim')
await expect(noteCard(page, 'Web app')).toBeVisible()
await expect(noteCard(page, 'Data store')).toBeVisible()
await expect(noteCard(page, 'Sync service')).toBeVisible()

// Compare Current -> Target: Sync service is the only added card,
// the old direct link is the only removed link, and the new
// shape's two links are added.
await switcherPopover(page).getByText('All cards', { exact: true }).click()
await noteCard(page, oldTitle).click({ button: 'right' })
await expect(contextMenu(page)).toBeVisible()
await contextMenu(page).getByText('Add to perspective', { exact: false }).click()
await expect(contextMenu(page)).toBeVisible()
await contextMenu(page).getByText('Before', { exact: true }).click()
await expect(page.getByTestId('atlas-quiet-toast')).toContainText('Added to Before')

// Compare Before -> After: the new card is the one addition; no
// removed groups render (empty groups are omitted).
await switcherButton(page).click()
await switcherPopover(page).getByText('Compare perspectives', { exact: false }).click()
const dialog = page.locator('[data-component="atlas-perspective-compare-dialog"]')
await expect(dialog).toBeVisible()
await dialog.getByTestId('atlas-compare-from').selectOption({ label: 'Current' })
await dialog.getByTestId('atlas-compare-to').selectOption({ label: 'Target' })
await dialog.getByTestId('atlas-compare-from').selectOption({ label: 'Before' })
await dialog.getByTestId('atlas-compare-to').selectOption({ label: 'After' })

const results = dialog.getByTestId('atlas-compare-results')
await expect(results.getByText('Added cards (1)', { exact: true })).toBeVisible()
await expect(results.getByTestId('atlas-compare-card-row').filter({ hasText: 'Sync service' })).toBeVisible()
await expect(results.getByText('Added links (2)', { exact: true })).toBeVisible()
await expect(results.getByText('Removed links (1)', { exact: true })).toBeVisible()
await expect(
results.getByTestId('atlas-compare-link-row').filter({ hasText: 'Web app → Data store (relates to)' }),
).toBeVisible()
// "Removed cards" stays empty (every Current card is also a Target
// member) -- an empty group is omitted entirely, never rendered.
await expect(results.getByTestId('atlas-compare-card-row').filter({ hasText: newTitle })).toBeVisible()
await expect(dialog.getByText('Removed cards', { exact: false })).toHaveCount(0)

await page.keyboard.press('Escape')
await expect(dialog).not.toBeVisible()

// Cleanup (within-file discipline): cards, then perspectives.
for (const title of [oldTitle, newTitle]) {
await openCard(page, noteCard(page, title))
await deleteViaPageMenu(page, page.locator('[data-component="atlas-card-overlay"]'))
}
for (const name of ['Before', 'After']) {
await switcherButton(page).click()
const row = switcherPopover(page).getByText(name, { exact: true })
await row.click({ button: 'right' })
await contextMenu(page).getByText('Delete', { exact: true }).click()
await expect(page.getByRole('heading', { name: `Delete ${name}?` })).toBeVisible()
await page.getByRole('button', { name: 'Delete', exact: true }).click()
await expect(page.getByRole('heading', { name: `Delete ${name}?` })).toHaveCount(0)
if (await switcherPopover(page).isVisible()) await page.keyboard.press('Escape')
}
})
})
15 changes: 6 additions & 9 deletions frontend/e2e/atlas-projections.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,17 @@ test('coverage counts a space\'s cards missing a link and missing a mirror, with
await page.getByRole('link', { name: 'Atlas' }).click()
await expect(page.getByTestId('atlas-board')).toBeVisible()

// "My space" has four seeded children: "Getting started" (an
// "My space" has three seeded children: "Getting started" (an
// outgoing "relates to" link to "Ada Lovelace"), "Example area",
// "Scratchpad", and "System landscape" (goal 0095 slice 3's seeded
// perspectives example -- none of the three carries a link of its
// own AT THIS level) -- a hand-countable 1/4 linked. None of the
// four carries a mirror directly at THIS level (the seeded mirror
// lives one level deeper, on "Project charter") -- a hand-countable
// 0/4 mirrored.
// and "Scratchpad" -- a hand-countable 1/3 linked. None carries a
// mirror directly at THIS level (the seeded mirror lives one level
// deeper, on "Project charter") -- a hand-countable 0/3 mirrored.
await page.getByTestId('atlas-open-coverage').click()
const dialog = page.locator('[data-component="atlas-coverage-dialog"]')
await expect(dialog).toBeVisible()

await expect(dialog.getByTestId('atlas-coverage-link-value')).toHaveText('1/4 linked')
await expect(dialog.getByTestId('atlas-coverage-mirror-value')).toHaveText('0/4 mirrored')
await expect(dialog.getByTestId('atlas-coverage-link-value')).toHaveText('1/3 linked')
await expect(dialog.getByTestId('atlas-coverage-mirror-value')).toHaveText('0/3 mirrored')

await dialog.getByTestId('atlas-coverage-link-toggle').click()
await expect(dialog.getByTestId('atlas-coverage-missing-item').filter({ hasText: 'Example area' })).toBeVisible()
Expand Down
11 changes: 5 additions & 6 deletions frontend/e2e/atlas-select-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,15 +470,14 @@ test('atlas select-all (Cmd+A): guarded inside an editable field, selects every
await page.keyboard.press('Escape')

// Real dispatch: Cmd+A on the board selects EVERY top-level card at
// this level -- the seeded root ("My space") already carries 4
// (Example area, Getting started, Scratchpad, System landscape --
// goal 0095 slice 3; internal/domain/atlas/builtin.go), plus the 2
// just placed.
// this level -- the seeded root ("My space") carries 3
// (Example area, Getting started, Scratchpad), plus the 2 just
// placed.
await page.keyboard.press('Meta+a')
await expect(selected).toHaveCount(6)
await expect(selected).toHaveCount(5)
const selectionTray = page.getByTestId('atlas-selection-tray')
await expect(selectionTray).toBeVisible()
await expect(page.getByTestId('atlas-selection-count')).toHaveText('6 selected')
await expect(page.getByTestId('atlas-selection-count')).toHaveText('5 selected')

// Cleanup (testing.md's within-file discipline): quick delete +
// clock-controlled toast expiry, same pattern this file's other
Expand Down
135 changes: 31 additions & 104 deletions internal/domain/atlas/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,46 +238,6 @@ func BuiltInCards() []Card {
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(5), // Card gained MirrorChecksum (goal 0088) then DeletedAt (goal 0093) -- shape shifts
},
{
// The seeded perspectives example's own container (goal 0095
// slice 3): a direct child of "My space" (keeps the single-
// root-card navigation intact) holding the three landscape
// cards below, so their own internal links stay invisible one
// level up (resolveBoardEdges.ts skips an edge whose endpoints
// resolve to the same top-level card) -- the pre-existing
// "My space" child census (atlas-projections.spec.ts's
// coverage stat, atlas-scale.spec.ts's dense-fixture edge
// count) only grows by this one new card, never by three.
ID: cardSystemLandscapeID, KindID: kindComponentID, Title: "System landscape",
Note: "Switch perspectives above to see this move from Current to Target.",
ParentID: cardMySpaceID, ViewMode: ViewModeShelves,
// 960 sits right after the seeded row's rightmost card
// (Scratchpad at 746) -- close enough that this card barely
// widens the fit-to-view bounding box other e2e specs'
// zoom-then-click-by-fraction helpers depend on; a much
// farther placement measurably shifts those fractions.
Position: &Position{X: 960, Y: 80},
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
ID: cardWebAppID, KindID: kindComponentID, Title: "Web app",
ParentID: cardSystemLandscapeID,
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
ID: cardDataStoreID, KindID: kindComponentID, Title: "Data store",
ParentID: cardSystemLandscapeID,
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
ID: cardSyncServiceID, KindID: kindComponentID, Title: "Sync service",
ParentID: cardSystemLandscapeID,
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
}
}

Expand All @@ -298,74 +258,20 @@ func BuiltInLinks() []Link {
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
// Current's own link: the web app talks to the data store
// directly.
ID: linkWebToStoreID, FromCardID: cardWebAppID, ToCardID: cardDataStoreID,
LinkKindID: linkKindRelatesToID,
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
// Interim/Target's new shape, half one: the web app talks to
// the sync service.
ID: linkWebToSyncID, FromCardID: cardWebAppID, ToCardID: cardSyncServiceID,
LinkKindID: linkKindRelatesToID,
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
// Interim/Target's new shape, half two: the sync service
// relays to the data store.
ID: linkSyncToStoreID, FromCardID: cardSyncServiceID, ToCardID: cardDataStoreID,
LinkKindID: linkKindRelatesToID,
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
}
}

// BuiltInPerspectives returns the seeded three-perspective reference-
// architecture example (ADR-0041, goal 0095 slice 3): "Current",
// "Interim", "Target" over "System landscape"'s own three Component
// cards, membership telling a migration story -- Current is the web
// app wired straight to the data store; Interim adds the sync service
// alongside that old connection; Target drops the old direct link and
// keeps only the new shape. Every card is a member of every
// perspective (the story is entirely which LINKS are visible); only
// MemberLinkIDs differs.
// BuiltInPerspectives seeds NO perspectives. Perspectives are a
// platform capability; a named set like "Current/Interim/Target" is a
// modeling concept the USER authors -- shipping one made it read as a
// native option in the switcher on every install (the inner-platform
// smell SPEC section 0 exists to prevent). The capability's proof
// lives at the test layer (dedicated-server e2e + Go integration
// tests build their own perspectives) and in the docs, never as
// always-present named objects in user data. The retired IDs below
// remove the previously-shipped set from existing installs.
func BuiltInPerspectives() []Perspective {
now := time.Now()
allThree := []string{cardWebAppID, cardDataStoreID, cardSyncServiceID}
return []Perspective{
{
ID: perspectiveCurrentID, SpaceID: cardSystemLandscapeID, Name: "Current",
Description: "The web app talks straight to the data store.",
Order: 0,
MemberCardIDs: []string{cardWebAppID, cardDataStoreID},
MemberLinkIDs: []string{linkWebToStoreID},
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
ID: perspectiveInterimID, SpaceID: cardSystemLandscapeID, Name: "Interim",
Description: "The sync service comes online alongside the old connection.",
Order: 1,
MemberCardIDs: allThree,
MemberLinkIDs: []string{linkWebToStoreID, linkWebToSyncID, linkSyncToStoreID},
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
{
ID: perspectiveTargetID, SpaceID: cardSystemLandscapeID, Name: "Target",
Description: "The old direct connection is gone; the sync service is the only path.",
Order: 2,
MemberCardIDs: allThree,
MemberLinkIDs: []string{linkWebToSyncID, linkSyncToStoreID},
CreatedAt: now, UpdatedAt: now,
BuiltIn: true, Seed: seedorigin.Stamp(1),
},
}
return nil
}

// RetiredBuiltInKindIDs names a built-in Kind ID that once shipped in
Expand All @@ -389,3 +295,24 @@ func RetiredKindReplacementID(retiredID string) (string, bool) {
}
return "", false
}

// RetiredBuiltInCardIDs names built-in cards that once shipped and no
// longer do -- the reference-architecture landscape (its concept now
// user-authored, never seeded). Reconcile removes an install's copy
// exactly when it is still an untouched golden (Seed.Modified false);
// an edited copy belongs to the user and stays.
func RetiredBuiltInCardIDs() []string {
return []string{cardWebAppID, cardDataStoreID, cardSyncServiceID, cardSystemLandscapeID}
}

// RetiredBuiltInLinkIDs names the retired landscape's own links, same
// contract as RetiredBuiltInCardIDs.
func RetiredBuiltInLinkIDs() []string {
return []string{linkWebToStoreID, linkWebToSyncID, linkSyncToStoreID}
}

// RetiredBuiltInPerspectiveIDs names the previously-seeded perspective
// set, same contract as RetiredBuiltInCardIDs.
func RetiredBuiltInPerspectiveIDs() []string {
return []string{perspectiveCurrentID, perspectiveInterimID, perspectiveTargetID}
}
Loading
Loading