mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Polish CBM Lens Beta branding and architecture navigation
Rename the reading workspace to CBM Lens Beta, distinguish Beta with an amber badge, and keep build diagnostics out of the header. Put architecture history beside the active context and record meaningful exploration steps. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
7cfc1b8771
commit
b0b0ffdef8
37 files changed
+569
-410
No files matched your search
@@ -131,9 +131,9 @@ Package-manager setup (npm, PyPI, or Go) verifies and publishes a coherent priva
|
|||||||
|
|
||||||
The ordinary `cli` mode is intentionally separate: it runs one command locally and never starts or connects to the coordination daemon, registers a daemon session, or starts watchers/UI. Its only shared state is the OS admission barrier plus per-project locks for graph mutations. While the command is running, a temporary monitor lets activation cancel that operation and its supervised worker safely; the monitor exits with the command and never becomes a standing daemon. See [CLI Mode](#cli-mode) for details.
|
The ordinary `cli` mode is intentionally separate: it runs one command locally and never starts or connects to the coordination daemon, registers a daemon session, or starts watchers/UI. Its only shared state is the OS admission barrier plus per-project locks for graph mutations. While the command is running, a temporary monitor lets activation cancel that operation and its supervised worker safely; the monitor exits with the command and never becomes a standing daemon. See [CLI Mode](#cli-mode) for details.
|
||||||
|
|
||||||
### CBM Atlas — the human interface
|
### CBM Lens Beta — the human interface
|
||||||
|
|
||||||
The MCP tools are the agent's interface to the code graph; **CBM Atlas** is the human's. It is built into the binary — every install on every channel has it:
|
The MCP tools are the agent's interface to the code graph; **CBM Lens Beta** is the human's. It is built into the binary — every install on every channel has it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codebase-memory-mcp --ui=true --port=9749
|
codebase-memory-mcp --ui=true --port=9749
|
||||||
@@ -141,9 +141,9 @@ codebase-memory-mcp --ui=true --port=9749
|
|||||||
|
|
||||||
Open `http://localhost:9749` in your browser. The UI is owned by the shared coordination daemon, so concurrent agent sessions do not start duplicate HTTP servers.
|
Open `http://localhost:9749` in your browser. The UI is owned by the shared coordination daemon, so concurrent agent sessions do not start duplicate HTTP servers.
|
||||||
|
|
||||||
Atlas is **CodeAtlasWeb**, a reading IDE built by [Bernhard Jackiewicz](https://github.com/BernhardJackiewicz) on the CBM read surface (design and tracking in #1964). It runs air-gapped on loopback, reaches no cloud, and every panel names where its facts come from; what the index did not record is shown as a gap, never filled in with a guess.
|
CBM Lens builds on **CodeAtlasWeb**, a reading IDE built by [Bernhard Jackiewicz](https://github.com/BernhardJackiewicz) on the CBM read surface (design and tracking in #1964). It runs air-gapped on loopback, reaches no cloud, and every panel names where its facts come from; what the index did not record is shown as a gap, never filled in with a guess.
|
||||||
|
|
||||||
When something in Atlas does not load, the page has already written it down: its console output, uncaught errors and failed requests go to `${CBM_CACHE_DIR}/logs/ui.log` (default `~/.cache/codebase-memory-mcp/logs/ui.log`, one JSON line per entry, rotated once at 5 MiB). The projects panel (alt+p) shows the tail under "This server", `GET http://localhost:9749/api/ui-log?lines=200` returns it, and a bug report can attach the file.
|
When something in CBM Lens does not load, the page has already written it down: its console output, uncaught errors and failed requests go to `${CBM_CACHE_DIR}/logs/ui.log` (default `~/.cache/codebase-memory-mcp/logs/ui.log`, one JSON line per entry, rotated once at 5 MiB). The projects panel (alt+p) shows the tail under "This server", `GET http://localhost:9749/api/ui-log?lines=200` returns it, and a bug report can attach the file.
|
||||||
|
|
||||||
- **Explorer and reader** — the indexed file tree and a read-only Monaco reader whose source comes from the index.
|
- **Explorer and reader** — the indexed file tree and a read-only Monaco reader whose source comes from the index.
|
||||||
- **Semantic twin** — follows the caret: what the symbol under it holds, calls, raises and touches, as facts or as pseudocode, pitched at who is reading.
|
- **Semantic twin** — follows the caret: what the symbol under it holds, calls, raises and touches, as facts or as pseudocode, pitched at who is reading.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# CodeAtlasWeb
|
# CBM Lens Beta
|
||||||
|
|
||||||
## In this repository
|
## In this repository
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="color-scheme" content="dark" />
|
<meta name="color-scheme" content="dark" />
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Crect width=%2716%27 height=%2716%27 fill=%27%230a0e0d%27/%3E%3Ccircle cx=%278%27 cy=%278%27 r=%274%27 fill=%27%2333ff99%27/%3E%3C/svg%3E" />
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Crect width=%2716%27 height=%2716%27 fill=%27%230a0e0d%27/%3E%3Ccircle cx=%278%27 cy=%278%27 r=%274%27 fill=%27%2333ff99%27/%3E%3C/svg%3E" />
|
||||||
<title>CodeAtlas</title>
|
<title>CBM Lens Beta</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "codeatlasweb",
|
"name": "cbm-lens",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codeatlasweb",
|
"name": "cbm-lens",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@huggingface/transformers": "4.2.0",
|
"@huggingface/transformers": "4.2.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "codeatlasweb",
|
"name": "cbm-lens",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -162,8 +162,9 @@ describe('AtlasChrome', () => {
|
|||||||
|
|
||||||
it('zeigt Marke und Versions-Chip in der Kopfzeile', async () => {
|
it('zeigt Marke und Versions-Chip in der Kopfzeile', async () => {
|
||||||
await render(props());
|
await render(props());
|
||||||
expect(container.querySelector('.atlas-brand')?.textContent).toBe('CODEATLAS');
|
expect(container.querySelector('.atlas-brand')?.textContent).toBe('CBM LENS Beta');
|
||||||
expect(testId('atlas-version')?.textContent).toBe('v0.0.0-dirty');
|
expect(container.querySelector('.atlas-brand-beta')?.textContent).toBe('Beta');
|
||||||
|
expect(testId('atlas-version')?.textContent).toBe('v0.0.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps project switching at the end of the header without a duplicate project chip', async () => {
|
it('keeps project switching at the end of the header without a duplicate project chip', async () => {
|
||||||
@@ -177,18 +178,10 @@ describe('AtlasChrome', () => {
|
|||||||
expect(header?.querySelector('[data-chip="sym"]')?.textContent).toContain('76');
|
expect(header?.querySelector('[data-chip="sym"]')?.textContent).toContain('76');
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
it('omits build diagnostics from the header', async () => {
|
||||||
* Der Zustand des Arbeitsbaums steht neben der Fassung, nicht in ihr.
|
|
||||||
*
|
|
||||||
* Der Chip beantwortet "welche Fassung ist das" und `v1.0.0-dirty` waere
|
|
||||||
* darauf eine Antwort, die es als Release nicht gibt. Ohne Zusatz steht
|
|
||||||
* daneben nichts: ein leeres Element waere ein Platz, an dem jemand eine
|
|
||||||
* Angabe vermutet.
|
|
||||||
*/
|
|
||||||
it('haelt den Bau-Zusatz getrennt vom Versions-Chip', async () => {
|
|
||||||
await render(props({ version: 'v1.0.0', buildSuffix: 'dirty' }));
|
await render(props({ version: 'v1.0.0', buildSuffix: 'dirty' }));
|
||||||
expect(testId('atlas-version')?.textContent).toBe('v1.0.0');
|
expect(testId('atlas-version')?.textContent).toBe('v1.0.0');
|
||||||
expect(testId('atlas-version-suffix')?.textContent).toBe('dirty');
|
expect(testId('atlas-version-suffix')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zeigt gar kein Zusatz-Element, wenn der Baum sauber war', async () => {
|
it('zeigt gar kein Zusatz-Element, wenn der Baum sauber war', async () => {
|
||||||
|
|||||||
@@ -164,16 +164,7 @@ export interface AtlasChromeProps {
|
|||||||
guidance?: Guidance;
|
guidance?: Guidance;
|
||||||
/** Versions-Chip, zur Buildzeit injiziert. Nur die Fassung, ohne Zusatz. */
|
/** Versions-Chip, zur Buildzeit injiziert. Nur die Fassung, ohne Zusatz. */
|
||||||
version: string;
|
version: string;
|
||||||
/**
|
/** Build diagnostics are accepted for compatibility but not shown in the header. */
|
||||||
* Der Zusatz neben dem Chip: `dirty`, wenn beim Bauen etwas uncommitted war.
|
|
||||||
*
|
|
||||||
* Ein eigenes Element und nicht ein Anhaengsel im Chip, weil es eine andere
|
|
||||||
* Aussage ist: der Chip nennt die Fassung, der Zusatz den Zustand des
|
|
||||||
* Baums, aus dem sie gebaut wurde. Zusammengeschrieben liest sich das wie
|
|
||||||
* eine Fassung namens "1.0.0-dirty", die es als Release nie gibt. Fehlt der
|
|
||||||
* Zusatz, steht dort nichts: ein leeres Element waere ein Platz, an dem
|
|
||||||
* jemand eine Angabe vermutet.
|
|
||||||
*/
|
|
||||||
buildSuffix?: string;
|
buildSuffix?: string;
|
||||||
chips: Chip[];
|
chips: Chip[];
|
||||||
tabs: TabDescriptor[];
|
tabs: TabDescriptor[];
|
||||||
@@ -625,15 +616,10 @@ export default function AtlasChrome(props: AtlasChromeProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="atlas-shell" data-workspace={props.workspace ?? 'explore'} data-guidance={props.guidance ?? 'brief'} data-chat-open={showChatColumn} data-has-status={props.workspaceStatus !== undefined}>
|
<div className="atlas-shell" data-workspace={props.workspace ?? 'explore'} data-guidance={props.guidance ?? 'brief'} data-chat-open={showChatColumn} data-has-status={props.workspaceStatus !== undefined}>
|
||||||
<header className="atlas-header" data-testid="atlas-header">
|
<header className="atlas-header" data-testid="atlas-header">
|
||||||
<h1 className="atlas-brand">{messages.app.brand}</h1>
|
<h1 className="atlas-brand">{messages.app.brand}{' '}<span className="atlas-brand-beta">{messages.app.releaseLabel}</span></h1>
|
||||||
<span className="atlas-version" data-testid="atlas-version">
|
<span className="atlas-version" data-testid="atlas-version">
|
||||||
{props.version}
|
{props.version.replace(/-dirty$/, '')}
|
||||||
</span>
|
</span>
|
||||||
{(props.buildSuffix ?? '').length > 0 && (
|
|
||||||
<span className="atlas-version-suffix" data-testid="atlas-version-suffix">
|
|
||||||
{props.buildSuffix}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{/*
|
{/*
|
||||||
* Sechs Eintraege, EINE Gestalt.
|
* Sechs Eintraege, EINE Gestalt.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,11 +5,10 @@
|
|||||||
* Auswahl, Doppelklick und leere Flaeche; Analyse und Routen kommen aus
|
* Auswahl, Doppelklick und leere Flaeche; Analyse und Routen kommen aus
|
||||||
* festen Antworten.
|
* festen Antworten.
|
||||||
*/
|
*/
|
||||||
import { act } from 'react';
|
import { act, type ReactNode } from 'react';
|
||||||
import { createRoot, type Root } from 'react-dom/client';
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import ArchitecturePanel, { type ArchitecturePanelProps } from './ArchitecturePanel';
|
import ArchitecturePanel, { type ArchitecturePanelProps } from './ArchitecturePanel';
|
||||||
import { fitToolbar } from '../galaxy/toolbar-fit';
|
|
||||||
import type { ArchitectureSceneProps } from './ArchitectureScene';
|
import type { ArchitectureSceneProps } from './ArchitectureScene';
|
||||||
import type { ArchitectureOverviewDto } from '../core/intelligence-provider';
|
import type { ArchitectureOverviewDto } from '../core/intelligence-provider';
|
||||||
import type { GraphData, GraphNode } from '../galaxy/types';
|
import type { GraphData, GraphNode } from '../galaxy/types';
|
||||||
@@ -19,7 +18,8 @@ import type { SystemArchitectureLoader, SystemProjection, SystemSymbol } from '.
|
|||||||
vi.mock('./ArchitectureScene', () => ({ ArchitectureScene: (props: ArchitectureSceneProps) => <div data-testid="scene">
|
vi.mock('./ArchitectureScene', () => ({ ArchitectureScene: (props: ArchitectureSceneProps) => <div data-testid="scene">
|
||||||
{props.model.nodes.map(node => <button key={node.id} data-node={node.id} onClick={() => props.onSelect(node.id)} onDoubleClick={() => props.onOpen?.(node.id)}>{node.label}</button>)}
|
{props.model.nodes.map(node => <button key={node.id} data-node={node.id} onClick={() => props.onSelect(node.id)} onDoubleClick={() => props.onOpen?.(node.id)}>{node.label}</button>)}
|
||||||
<button aria-label="Empty background" onClick={props.onClearSelection} /></div> }));
|
<button aria-label="Empty background" onClick={props.onClearSelection} /></div> }));
|
||||||
vi.mock('./ContainerMap', () => ({ default: ({ filter }: { filter: string }) => <div data-testid="container-map" data-filter={filter} /> }));
|
vi.mock('./ContainerMap', () => ({ default: ({ filter, navigation }: { filter: string; navigation?: ReactNode }) =>
|
||||||
|
<div data-testid="container-map" data-filter={filter}><div className="atlas-arch-context" data-testid="container-location">{navigation}</div></div> }));
|
||||||
vi.mock('./route-graph-source', () => ({ loadRouteGraph: vi.fn(async () => ({ relationships: [], truncated: false, warnings: [] })) }));
|
vi.mock('./route-graph-source', () => ({ loadRouteGraph: vi.fn(async () => ({ relationships: [], truncated: false, warnings: [] })) }));
|
||||||
vi.mock('./BehaviorSourceEvidence', () => ({ default: () => <div /> }));
|
vi.mock('./BehaviorSourceEvidence', () => ({ default: () => <div /> }));
|
||||||
vi.mock('../app/atlas-api', () => ({ AtlasApi: class { flows() { return Promise.resolve([]); } } }));
|
vi.mock('../app/atlas-api', () => ({ AtlasApi: class { flows() { return Promise.resolve([]); } } }));
|
||||||
@@ -127,7 +127,7 @@ const filter = () => container.querySelector<HTMLInputElement>('input[type="sear
|
|||||||
const focus = () => container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-focus');
|
const focus = () => container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-focus');
|
||||||
const heading = () => container.querySelector('.behavior-heading h2')?.textContent;
|
const heading = () => container.querySelector('.behavior-heading h2')?.textContent;
|
||||||
const sceneLabels = () => [...container.querySelectorAll('[data-testid="scene"] [data-node]')].map(item => item.textContent);
|
const sceneLabels = () => [...container.querySelectorAll('[data-testid="scene"] [data-node]')].map(item => item.textContent);
|
||||||
/** The "← Back" System structure and Behavior had in their own toolbars before K27; the shared one carries the same words. */
|
/** System structure and Behavior use the shared history rather than another local Back. */
|
||||||
const inViewBack = () => buttons().find(button => button.textContent === '← Back' && !history()?.contains(button));
|
const inViewBack = () => buttons().find(button => button.textContent === '← Back' && !history()?.contains(button));
|
||||||
const systemCamera = () => container.querySelector('[aria-label="System camera"] button[aria-pressed="true"]')?.textContent;
|
const systemCamera = () => container.querySelector('[aria-label="System camera"] button[aria-pressed="true"]')?.textContent;
|
||||||
const behaviorCamera = () => container.querySelector('[aria-label="Behavior camera"] button[aria-pressed="true"]')?.textContent;
|
const behaviorCamera = () => container.querySelector('[aria-label="Behavior camera"] button[aria-pressed="true"]')?.textContent;
|
||||||
@@ -144,89 +144,118 @@ async function openArea(name: string): Promise<void> {
|
|||||||
await press(container.querySelector<HTMLButtonElement>(`[data-testid="scene"] [data-node="area:${name}"]`) ?? byText(name, container.querySelector('[data-testid="scene"]')!));
|
await press(container.querySelector<HTMLButtonElement>(`[data-testid="scene"] [data-node="area:${name}"]`) ?? byText(name, container.querySelector('[data-testid="scene"]')!));
|
||||||
await clickText('Open area →');
|
await clickText('Open area →');
|
||||||
}
|
}
|
||||||
|
const selectView = (name: string) => press(container.querySelector<HTMLButtonElement>(`[data-view="${name}"]`));
|
||||||
|
const position = () => history()?.getAttribute('data-position');
|
||||||
|
async function openRouteGroup(): Promise<void> {
|
||||||
|
await press(byText('/edit · 2', container.querySelector('[data-testid="scene"]')!));
|
||||||
|
await clickText('Show these 2 routes →');
|
||||||
|
}
|
||||||
|
|
||||||
describe('Architecture back and forward (K27)', () => {
|
describe('Architecture back and forward (K27)', () => {
|
||||||
it('goes back and forward across subtabs, an opened area and Plan, naming each target', async () => {
|
it('restores meaningful visits across views, with each visit’s camera and location', async () => {
|
||||||
await render();
|
await render();
|
||||||
expect(back()?.disabled).toBe(true);
|
expect(back()?.disabled).toBe(true);
|
||||||
expect(back()?.title).toBe('Nothing to go back to yet');
|
expect(back()?.title).toBe('Nothing to go back to yet');
|
||||||
expect(forward()?.disabled).toBe(true);
|
expect(forward()?.disabled).toBe(true);
|
||||||
await openArea('django');
|
await openArea('django');
|
||||||
expect(trail()).toEqual(['sample', 'django']);
|
expect(trail()).toEqual(['sample', 'django']);
|
||||||
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
|
||||||
await clickText('Plan', container.querySelector('[aria-label="Map camera"]')!);
|
await clickText('Plan', container.querySelector('[aria-label="Map camera"]')!);
|
||||||
expect(back()?.title).toBe('Back to Overview · django (Alt+Left)');
|
expect(position()).toBe('2/2');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="hotspots"]'));
|
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
||||||
|
await doubleClick('[data-node="file:django/shortcuts.py"]');
|
||||||
|
expect(container.querySelector('[aria-label="Architecture location"]')?.textContent).toContain('shortcuts.py');
|
||||||
|
expect(position()).toBe('3/3');
|
||||||
expect(back()?.title).toBe('Back to Overview · django · Plan (Alt+Left)');
|
expect(back()?.title).toBe('Back to Overview · django · Plan (Alt+Left)');
|
||||||
|
await selectView('routes');
|
||||||
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
|
expect(position()).toBe('3/3');
|
||||||
|
await openRouteGroup();
|
||||||
|
expect(position()).toBe('4/4');
|
||||||
|
|
||||||
|
await goBack();
|
||||||
|
expect([view(), perspective(), filter()]).toEqual(['routes', 'Endpoints', '']);
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), trail(), planar()]).toEqual(['overview', ['sample', 'django'], 'Plan']);
|
expect([view(), trail(), planar()]).toEqual(['overview', ['sample', 'django'], 'Plan']);
|
||||||
expect(forward()?.title).toBe('Forward to Hotspots · Plan (Alt+Right)');
|
expect(forward()?.title).toBe('Forward to Routes · Endpoints · Plan (Alt+Right)');
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), trail(), planar()]).toEqual(['overview', ['sample', 'django'], '3D']);
|
expect([trail(), planar()]).toEqual([['sample'], '3D']);
|
||||||
await goBack();
|
|
||||||
expect(trail()).toEqual(['sample']);
|
|
||||||
expect(back()?.disabled).toBe(true);
|
expect(back()?.disabled).toBe(true);
|
||||||
await goForward(); await goForward(); await goForward();
|
await goForward(); await goForward(); await goForward();
|
||||||
expect(view()).toBe('hotspots');
|
expect([view(), perspective(), filter(), planar()]).toEqual(['routes', 'Endpoints', '/edit', 'Plan']);
|
||||||
expect(forward()?.disabled).toBe(true);
|
expect(forward()?.disabled).toBe(true);
|
||||||
// The location trail stays: it still leads back to the repository, and that is a step of its own.
|
|
||||||
await goBack();
|
await goBack(); await goBack();
|
||||||
await press(byText('sample', container.querySelector('[aria-label="Architecture location"]')!));
|
await press(byText('sample', container.querySelector('[aria-label="Architecture location"]')!));
|
||||||
expect(trail()).toEqual(['sample']);
|
expect(trail()).toEqual(['sample']);
|
||||||
expect(back()?.title).toBe('Back to Overview · django · Plan (Alt+Left)');
|
expect(back()?.title).toBe('Back to Overview · django · Plan (Alt+Left)');
|
||||||
|
expect(forward()?.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('restores the Routes perspective and an opened route group', async () => {
|
it('replaces view and Routes perspective context while route-group drill-ins create stops', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await selectView('routes');
|
||||||
expect(perspective()).toBe('Service map');
|
expect(perspective()).toBe('Service map');
|
||||||
|
expect(container.querySelector('[data-testid="container-map"]')).not.toBeNull();
|
||||||
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
expect(sceneLabels()).toContain('/edit · 2');
|
expect(sceneLabels()).toContain('/edit · 2');
|
||||||
await press(byText('/edit · 2', container.querySelector('[data-testid="scene"]')!));
|
expect(position()).toBe('1/1');
|
||||||
await clickText('Show these 2 routes →');
|
expect(back()?.disabled).toBe(true);
|
||||||
|
await openRouteGroup();
|
||||||
expect(filter()).toBe('/edit');
|
expect(filter()).toBe('/edit');
|
||||||
expect(sceneLabels()).toEqual(['/edit/one/', '/edit/two/']);
|
expect(sceneLabels()).toEqual(['/edit/one/', '/edit/two/']);
|
||||||
expect(back()?.title).toBe('Back to Routes · Endpoints (Alt+Left)');
|
expect(back()?.title).toBe('Back to Routes · Endpoints (Alt+Left)');
|
||||||
|
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), perspective(), filter()]).toEqual(['routes', 'Endpoints', '']);
|
expect([view(), perspective(), filter()]).toEqual(['routes', 'Endpoints', '']);
|
||||||
await goBack();
|
expect(back()?.disabled).toBe(true);
|
||||||
expect([perspective(), filter()]).toEqual(['Service map', '']);
|
await clickText('Service map', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
|
expect(position()).toBe('1/2');
|
||||||
expect(container.querySelector('[data-testid="container-map"]')).not.toBeNull();
|
expect(container.querySelector('[data-testid="container-map"]')).not.toBeNull();
|
||||||
await goBack();
|
expect(forward()?.disabled).toBe(false);
|
||||||
expect(view()).toBe('overview');
|
await goForward();
|
||||||
await goForward(); await goForward(); await goForward();
|
|
||||||
expect([view(), perspective(), filter()]).toEqual(['routes', 'Endpoints', '/edit']);
|
expect([view(), perspective(), filter()]).toEqual(['routes', 'Endpoints', '/edit']);
|
||||||
expect(sceneLabels()).toEqual(['/edit/one/', '/edit/two/']);
|
expect(sceneLabels()).toEqual(['/edit/one/', '/edit/two/']);
|
||||||
|
await goBack();
|
||||||
|
expect([perspective(), filter()]).toEqual(['Service map', '']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('takes a typed filter as one step once typing pauses, not one per key', async () => {
|
it('saves every typed filter immediately without adding stops', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await selectView('routes');
|
||||||
await type('/e'); await type('/ed'); await type('/edit');
|
await type('/e'); await type('/ed'); await type('/edit');
|
||||||
expect(filter()).toBe('/edit');
|
expect(filter()).toBe('/edit');
|
||||||
await act(async () => { await new Promise(resolve => setTimeout(resolve, 900)); });
|
expect(position()).toBe('1/1');
|
||||||
expect(back()?.title).toBe('Back to Routes · Service map (Alt+Left)');
|
expect(back()?.disabled).toBe(true);
|
||||||
|
expect(forward()?.disabled).toBe(true);
|
||||||
|
await type('');
|
||||||
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
|
await openRouteGroup();
|
||||||
|
await type('/edit/one');
|
||||||
|
expect(position()).toBe('2/2');
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), filter()]).toEqual(['routes', '']);
|
expect(filter()).toBe('');
|
||||||
await goBack();
|
expect(forward()?.title).toBe('Forward to Routes · Endpoints · /edit/one (Alt+Right)');
|
||||||
expect(view()).toBe('overview');
|
await goForward();
|
||||||
await goForward(); await goForward();
|
expect(filter()).toBe('/edit/one');
|
||||||
expect(filter()).toBe('/edit');
|
expect(position()).toBe('2/2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makes the System structure focus and expanded groups steps, and its old Back the shared Back', async () => {
|
it('records System structure focus and restores expanded groups without expansion-only stops', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="structure"]'));
|
await selectView('structure');
|
||||||
expect(focus()).toBe('');
|
expect(focus()).toBe('');
|
||||||
// The old "← Back" is absorbed into the shared Back beside the subtabs: one Back control, one meaning.
|
|
||||||
expect(inViewBack()).toBeUndefined();
|
expect(inViewBack()).toBeUndefined();
|
||||||
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
expect(back()?.disabled).toBe(true);
|
||||||
await doubleClick('[data-system-node="g:api"]');
|
await doubleClick('[data-system-node="g:api"]');
|
||||||
expect(focus()).toBe('g:api');
|
expect(focus()).toBe('g:api');
|
||||||
expect(container.querySelector('[data-system-node="member:g:api:api"]')).not.toBeNull();
|
expect(container.querySelector('[data-system-node="member:g:api:api"]')).not.toBeNull();
|
||||||
expect(back()?.title).toBe('Back to System structure (Alt+Left)');
|
expect(back()?.title).toBe('Back to System structure (Alt+Left)');
|
||||||
|
await press(container.querySelector<HTMLButtonElement>('[data-system-node="g:api"]'));
|
||||||
|
await clickText('Collapse group');
|
||||||
|
expect(position()).toBe('2/2');
|
||||||
|
expect(container.querySelector('[data-system-node="member:g:api:api"]')).toBeNull();
|
||||||
|
await clickText('Expand group');
|
||||||
|
expect(position()).toBe('2/2');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-system-node="g:service"]'));
|
await press(container.querySelector<HTMLButtonElement>('[data-system-node="g:service"]'));
|
||||||
await clickText('Focus here');
|
await clickText('Focus here');
|
||||||
expect(focus()).toBe('g:service');
|
expect(focus()).toBe('g:service');
|
||||||
@@ -235,16 +264,13 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
|
|
||||||
await goBack();
|
await goBack();
|
||||||
expect(focus()).toBe('g:api');
|
expect(focus()).toBe('g:api');
|
||||||
|
expect(container.querySelector('[data-system-node="member:g:api:api"]')).not.toBeNull();
|
||||||
await goBack();
|
await goBack();
|
||||||
expect(focus()).toBe('');
|
expect(focus()).toBe('');
|
||||||
expect(container.querySelector('[data-system-node="member:g:api:api"]')).toBeNull();
|
expect(container.querySelector('[data-system-node="member:g:api:api"]')).toBeNull();
|
||||||
// One meaning of Back: the same Back also leaves System structure.
|
expect(back()?.disabled).toBe(true);
|
||||||
await goBack();
|
await goForward();
|
||||||
expect(view()).toBe('overview');
|
|
||||||
await goForward(); await goForward();
|
|
||||||
expect([view(), focus()]).toEqual(['structure', 'g:api']);
|
expect([view(), focus()]).toEqual(['structure', 'g:api']);
|
||||||
expect(container.querySelector('[data-system-node="member:g:api:api"]')).not.toBeNull();
|
|
||||||
// "Whole system" stays the way to the top, as a new step that drops the forward branch.
|
|
||||||
await clickText('Whole system');
|
await clickText('Whole system');
|
||||||
expect(focus()).toBe('');
|
expect(focus()).toBe('');
|
||||||
expect(forward()?.disabled).toBe(true);
|
expect(forward()?.disabled).toBe(true);
|
||||||
@@ -253,10 +279,11 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
|
|
||||||
it('steps through Behavior starts and followed calls, and an automatic start is no extra step', async () => {
|
it('steps through Behavior starts and followed calls, and an automatic start is no extra step', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="structure"]'));
|
await selectView('structure');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="behavior"]'));
|
await doubleClick('[data-system-node="g:api"]');
|
||||||
|
await selectView('behavior');
|
||||||
expect(heading()).toBe('What can main call?');
|
expect(heading()).toBe('What can main call?');
|
||||||
// The suggested start completes the Behavior step instead of adding one Back would bounce on.
|
expect(position()).toBe('2/2');
|
||||||
expect(back()?.title).toBe('Back to System structure (Alt+Left)');
|
expect(back()?.title).toBe('Back to System structure (Alt+Left)');
|
||||||
await choose('Behavior entry point', '2');
|
await choose('Behavior entry point', '2');
|
||||||
expect(heading()).toBe('What can handle call?');
|
expect(heading()).toBe('What can handle call?');
|
||||||
@@ -273,18 +300,16 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
await goBack();
|
await goBack();
|
||||||
expect(heading()).toBe('What can main call?');
|
expect(heading()).toBe('What can main call?');
|
||||||
await goBack();
|
await goBack();
|
||||||
expect(view()).toBe('structure');
|
expect([view(), focus()]).toEqual(['structure', '']);
|
||||||
await goForward(); await goForward(); await goForward();
|
await goForward(); await goForward(); await goForward();
|
||||||
expect(heading()).toBe('What can save call?');
|
expect(heading()).toBe('What can save call?');
|
||||||
expect(container.querySelector<HTMLSelectElement>('select[aria-label="Behavior entry point"]')?.value).toBe('3');
|
expect(container.querySelector<HTMLSelectElement>('select[aria-label="Behavior entry point"]')?.value).toBe('3');
|
||||||
// Empty background still returns from a followed call to where the hops started, as a step.
|
|
||||||
await clickText('System background');
|
await clickText('System background');
|
||||||
expect(heading()).toBe('What can handle call?');
|
expect(heading()).toBe('What can handle call?');
|
||||||
expect(back()?.title).toBe('Back to Behavior · save · followed from handle (Alt+Left)');
|
expect(back()?.title).toBe('Back to Behavior · save · followed from handle (Alt+Left)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('names the start a reset Behavior step shows, after the suggested start was rejected for a stale snapshot', async () => {
|
it('names the shown Behavior start after the suggested start was rejected for a stale snapshot', async () => {
|
||||||
// django-demo: the suggested main is rejected once for another analysis generation; the journey then shows main on its own.
|
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
loader.mockImplementation(async request => {
|
loader.mockImplementation(async request => {
|
||||||
calls++;
|
calls++;
|
||||||
@@ -292,8 +317,10 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
return { status: 'ready', generation: calls === 1 ? 'g1' : 'g2', result: projection() };
|
return { status: 'ready', generation: calls === 1 ? 'g1' : 'g2', result: projection() };
|
||||||
});
|
});
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="behavior"]'));
|
await openArea('django');
|
||||||
|
await selectView('behavior');
|
||||||
expect(heading()).toBe('What can main call?');
|
expect(heading()).toBe('What can main call?');
|
||||||
|
expect(position()).toBe('2/2');
|
||||||
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
||||||
await choose('Behavior entry point', '2');
|
await choose('Behavior entry point', '2');
|
||||||
expect(heading()).toBe('What can handle call?');
|
expect(heading()).toBe('What can handle call?');
|
||||||
@@ -306,135 +333,140 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
expect(forward()?.title).toBe('Forward to Behavior · main (Alt+Right)');
|
expect(forward()?.title).toBe('Forward to Behavior · main (Alt+Right)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makes Plan or 3D in System structure a step that Back restores', async () => {
|
it('restores System Plan or 3D with the focused visit without making camera or tab stops', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="structure"]'));
|
await selectView('structure');
|
||||||
expect(systemCamera()).toBe('3D');
|
expect(systemCamera()).toBe('3D');
|
||||||
expect(container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-planar')).toBe('false');
|
expect(container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-planar')).toBe('false');
|
||||||
|
await doubleClick('[data-system-node="g:api"]');
|
||||||
await clickText('Plan', container.querySelector('[aria-label="System camera"]')!);
|
await clickText('Plan', container.querySelector('[aria-label="System camera"]')!);
|
||||||
expect(container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-planar')).toBe('true');
|
expect(container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-planar')).toBe('true');
|
||||||
expect(back()?.title).toBe('Back to System structure (Alt+Left)');
|
expect(position()).toBe('2/2');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="overview"]'));
|
await press(container.querySelector<HTMLButtonElement>('[data-system-node="g:service"]'));
|
||||||
expect(back()?.title).toBe('Back to System structure · Plan (Alt+Left)');
|
await clickText('Focus here');
|
||||||
|
await selectView('overview');
|
||||||
|
expect(position()).toBe('3/3');
|
||||||
|
expect(back()?.title).toBe('Back to System structure · api · 1 group open · Plan (Alt+Left)');
|
||||||
|
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), systemCamera()]).toEqual(['structure', 'Plan']);
|
expect([view(), focus(), systemCamera()]).toEqual(['structure', 'g:api', 'Plan']);
|
||||||
expect(container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-planar')).toBe('true');
|
expect(container.querySelector('[data-testid="system-scene"]')?.getAttribute('data-planar')).toBe('true');
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), systemCamera()]).toEqual(['structure', '3D']);
|
expect([view(), focus(), systemCamera()]).toEqual(['structure', '', '3D']);
|
||||||
await goForward(); await goForward();
|
await goForward(); await goForward();
|
||||||
expect(view()).toBe('overview');
|
expect(view()).toBe('overview');
|
||||||
// Behavior keeps a camera of its own, as before.
|
await selectView('behavior');
|
||||||
await goBack();
|
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="behavior"]'));
|
|
||||||
expect(behaviorCamera()).toBe('3D');
|
expect(behaviorCamera()).toBe('3D');
|
||||||
|
expect(position()).toBe('3/3');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the Behavior path, the operation on it and Plan or 3D in the entry, and only a path or camera change is a step', async () => {
|
it('records Behavior destinations and paths while retaining chain position and camera as context', async () => {
|
||||||
loader.mockImplementation(async () => ({ status: 'ready', generation: 'g1', result: twoPaths() }));
|
loader.mockImplementation(async () => ({ status: 'ready', generation: 'g1', result: twoPaths() }));
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="behavior"]'));
|
await selectView('behavior');
|
||||||
expect(heading()).toBe('What can main call?');
|
expect(heading()).toBe('What can main call?');
|
||||||
|
expect(position()).toBe('1/1');
|
||||||
await choose('Behavior destination', '3');
|
await choose('Behavior destination', '3');
|
||||||
expect(heading()).toBe('main → save');
|
expect(heading()).toBe('main → save');
|
||||||
expect([chosenPath(), chainPosition()]).toEqual([1, '1 / 3']);
|
expect([chosenPath(), chainPosition()]).toEqual([1, '1 / 3']);
|
||||||
await clickText('Next →');
|
await clickText('Next →');
|
||||||
expect(chainPosition()).toBe('2 / 3');
|
expect(chainPosition()).toBe('2 / 3');
|
||||||
// Walking the chain is no step of its own: Previous and Next do that; the entry keeps where it stands.
|
expect(position()).toBe('2/2');
|
||||||
expect(back()?.title).toBe('Back to Behavior · main (Alt+Left)');
|
expect(back()?.title).toBe('Back to Behavior · main (Alt+Left)');
|
||||||
await press(pathButtons()[1]);
|
await press(pathButtons()[1]);
|
||||||
expect([chosenPath(), chainPosition()]).toEqual([2, '1 / 2']);
|
expect([chosenPath(), chainPosition()]).toEqual([2, '1 / 2']);
|
||||||
expect(back()?.title).toBe('Back to Behavior · main → save (Alt+Left)');
|
expect(back()?.title).toBe('Back to Behavior · main → save (Alt+Left)');
|
||||||
await clickText('Plan', container.querySelector('[aria-label="Behavior camera"]')!);
|
await clickText('Plan', container.querySelector('[aria-label="Behavior camera"]')!);
|
||||||
expect(back()?.title).toBe('Back to Behavior · main → save · Path 2 (Alt+Left)');
|
expect(position()).toBe('3/3');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="overview"]'));
|
expect(back()?.title).toBe('Back to Behavior · main → save (Alt+Left)');
|
||||||
expect(back()?.title).toBe('Back to Behavior · main → save · Path 2 · Plan (Alt+Left)');
|
|
||||||
|
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), behaviorCamera(), chosenPath(), chainPosition()]).toEqual(['behavior', 'Plan', 2, '1 / 2']);
|
expect([heading(), behaviorCamera(), chosenPath(), chainPosition()]).toEqual(['main → save', '3D', 1, '2 / 3']);
|
||||||
await goBack();
|
expect(forward()?.title).toBe('Forward to Behavior · main → save · Path 2 · Plan (Alt+Right)');
|
||||||
expect([behaviorCamera(), chosenPath()]).toEqual(['3D', 2]);
|
|
||||||
await goBack();
|
|
||||||
expect([heading(), chosenPath(), chainPosition()]).toEqual(['main → save', 1, '2 / 3']);
|
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([heading(), container.querySelector('[aria-label="Walk the call chain"]')]).toEqual(['What can main call?', null]);
|
expect([heading(), container.querySelector('[aria-label="Walk the call chain"]')]).toEqual(['What can main call?', null]);
|
||||||
|
expect(back()?.disabled).toBe(true);
|
||||||
await goForward();
|
await goForward();
|
||||||
expect([chosenPath(), chainPosition()]).toEqual([1, '2 / 3']);
|
expect([chosenPath(), chainPosition()]).toEqual([1, '2 / 3']);
|
||||||
await goForward(); await goForward();
|
await goForward();
|
||||||
expect([behaviorCamera(), chosenPath(), chainPosition()]).toEqual(['Plan', 2, '1 / 2']);
|
expect([behaviorCamera(), chosenPath(), chainPosition()]).toEqual(['Plan', 2, '1 / 2']);
|
||||||
// System structure keeps a camera of its own, as before.
|
await selectView('structure');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="structure"]'));
|
|
||||||
expect(systemCamera()).toBe('3D');
|
expect(systemCamera()).toBe('3D');
|
||||||
|
expect(position()).toBe('3/3');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('records typing that has not paused yet as a step when Back, Forward or Recent leaves it', async () => {
|
it('preserves the forward branch while typing and saves the latest text before a recent jump', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await selectView('routes');
|
||||||
await type('/adm');
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
|
await openRouteGroup();
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), filter()]).toEqual(['routes', '']);
|
|
||||||
expect(forward()?.title).toBe('Forward to Routes · Service map · /adm (Alt+Right)');
|
|
||||||
await goForward();
|
|
||||||
expect([view(), filter()]).toEqual(['routes', '/adm']);
|
|
||||||
await act(async () => { await new Promise(resolve => setTimeout(resolve, 900)); });
|
|
||||||
expect(history()?.getAttribute('data-position')).toBe('3/3');
|
|
||||||
// A recent place while typing: the text stays a step before the jump.
|
|
||||||
await type('/home');
|
await type('/home');
|
||||||
const recent = container.querySelector<HTMLDetailsElement>('details.atlas-arch-recent')!;
|
expect(position()).toBe('1/2');
|
||||||
const overviewPlace = [...recent.querySelectorAll<HTMLButtonElement>('[aria-label="Recently visited places"] li button')]
|
expect(forward()?.title).toBe('Forward to Routes · Endpoints · /edit (Alt+Right)');
|
||||||
.find(item => item.querySelector('strong')?.textContent === 'Overview');
|
|
||||||
await press(overviewPlace);
|
|
||||||
expect(view()).toBe('overview');
|
|
||||||
expect(back()?.title).toBe('Back to Routes · Service map · /home (Alt+Left)');
|
|
||||||
await goBack();
|
|
||||||
expect([view(), filter()]).toEqual(['routes', '/home']);
|
|
||||||
// Forward while typing: the text is a new step and drops the forward branch, as after the pause.
|
|
||||||
await goBack();
|
|
||||||
expect(filter()).toBe('/adm');
|
|
||||||
await type('/admin');
|
|
||||||
await goForward();
|
await goForward();
|
||||||
expect(filter()).toBe('/admin');
|
expect(filter()).toBe('/edit');
|
||||||
|
await goBack();
|
||||||
|
expect(filter()).toBe('/home');
|
||||||
|
await type('/admin');
|
||||||
|
const recent = container.querySelector<HTMLDetailsElement>('details.atlas-arch-recent')!;
|
||||||
|
const editPlace = [...recent.querySelectorAll<HTMLButtonElement>('[aria-label="Recently visited places"] li button')]
|
||||||
|
.find(item => item.querySelector('span')?.textContent === 'Endpoints · /edit');
|
||||||
|
await press(editPlace);
|
||||||
|
expect(filter()).toBe('/edit');
|
||||||
|
expect(position()).toBe('2/2');
|
||||||
expect(forward()?.disabled).toBe(true);
|
expect(forward()?.disabled).toBe(true);
|
||||||
expect(back()?.title).toBe('Back to Routes · Service map · /adm (Alt+Left)');
|
expect(back()?.title).toBe('Back to Routes · Endpoints · /admin (Alt+Left)');
|
||||||
|
await goBack();
|
||||||
|
expect(filter()).toBe('/admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports an opened area only to the subtab that shows it, never to Routes or Hotspots', async () => {
|
it('reports opened-area evidence only in its view and restores it with meaningful history visits', async () => {
|
||||||
const onSelectionEvidence = vi.fn();
|
const onSelectionEvidence = vi.fn();
|
||||||
await render({ onSelectionEvidence });
|
await render({ onSelectionEvidence });
|
||||||
await openArea('django');
|
await openArea('django');
|
||||||
expect(lastEvidence(onSelectionEvidence)?.label).toBe('django');
|
expect(lastEvidence(onSelectionEvidence)?.label).toBe('django');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await selectView('routes');
|
||||||
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
expect(sceneLabels()).toContain('/edit · 2');
|
expect(sceneLabels()).toContain('/edit · 2');
|
||||||
// Nothing is selected on the Endpoints map, so the chat context names nothing from Overview.
|
|
||||||
expect(lastEvidence(onSelectionEvidence)).toBeUndefined();
|
expect(lastEvidence(onSelectionEvidence)).toBeUndefined();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="hotspots"]'));
|
await selectView('hotspots');
|
||||||
expect(lastEvidence(onSelectionEvidence)).toBeUndefined();
|
expect(lastEvidence(onSelectionEvidence)).toBeUndefined();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="overview"]'));
|
await selectView('overview');
|
||||||
expect(lastEvidence(onSelectionEvidence)?.label).toBe('django');
|
expect(lastEvidence(onSelectionEvidence)?.label).toBe('django');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
expect(position()).toBe('2/2');
|
||||||
|
await doubleClick('[data-node="file:django/shortcuts.py"]');
|
||||||
|
await selectView('routes');
|
||||||
await goBack();
|
await goBack();
|
||||||
expect([view(), trail()]).toEqual(['overview', ['sample', 'django']]);
|
expect([view(), trail()]).toEqual(['overview', ['sample', 'django']]);
|
||||||
expect(lastEvidence(onSelectionEvidence)?.label).toBe('django');
|
expect(lastEvidence(onSelectionEvidence)?.label).toBe('django');
|
||||||
await goBack(); await goBack();
|
await goForward();
|
||||||
expect([view(), perspective()]).toEqual(['routes', 'Endpoints']);
|
expect([view(), perspective()]).toEqual(['routes', 'Endpoints']);
|
||||||
expect(lastEvidence(onSelectionEvidence)).toBeUndefined();
|
expect(lastEvidence(onSelectionEvidence)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops the forward branch on a new navigation after Back and keeps at most 25 steps', async () => {
|
it('drops the forward branch on a new focus after Back and keeps at most 25 real stops', async () => {
|
||||||
await render();
|
await render();
|
||||||
for (const tab of ['routes', 'hotspots', 'structure']) await press(container.querySelector<HTMLButtonElement>(`[data-view="${tab}"]`));
|
await selectView('structure');
|
||||||
await goBack(); await goBack();
|
await doubleClick('[data-system-node="g:api"]');
|
||||||
expect(view()).toBe('routes');
|
await doubleClick('[data-system-node="g:service"]');
|
||||||
|
await goBack();
|
||||||
|
expect(focus()).toBe('g:api');
|
||||||
expect(forward()?.disabled).toBe(false);
|
expect(forward()?.disabled).toBe(false);
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="behavior"]'));
|
await selectView('behavior');
|
||||||
|
expect(position()).toBe('2/3');
|
||||||
|
expect(forward()?.disabled).toBe(false);
|
||||||
|
await choose('Behavior entry point', '2');
|
||||||
expect(forward()?.disabled).toBe(true);
|
expect(forward()?.disabled).toBe(true);
|
||||||
await goBack();
|
await goBack();
|
||||||
expect(view()).toBe('routes');
|
expect(heading()).toBe('What can main call?');
|
||||||
expect(forward()?.title).toBe('Forward to Behavior · main (Alt+Right)');
|
expect(forward()?.title).toBe('Forward to Behavior · handle (Alt+Right)');
|
||||||
|
|
||||||
for (let step = 0; step < 15; step++) for (const tab of ['overview', 'hotspots']) await press(container.querySelector<HTMLButtonElement>(`[data-view="${tab}"]`));
|
for (let step = 0; step < 15; step++) {
|
||||||
expect(history()?.getAttribute('data-position')).toBe('25/25');
|
await choose('Behavior entry point', '2');
|
||||||
|
await choose('Behavior entry point', '1');
|
||||||
|
}
|
||||||
|
expect(position()).toBe('25/25');
|
||||||
let steps = 0;
|
let steps = 0;
|
||||||
while (!back()!.disabled && steps < 100) { await goBack(); steps++; }
|
while (!back()!.disabled && steps < 100) { await goBack(); steps++; }
|
||||||
expect(steps).toBe(24);
|
expect(steps).toBe(24);
|
||||||
@@ -442,7 +474,8 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
|
|
||||||
it('answers Alt+Left and Alt+Right only in the active workspace and never while typing', async () => {
|
it('answers Alt+Left and Alt+Right only in the active workspace and never while typing', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await openArea('django');
|
||||||
|
await selectView('routes');
|
||||||
let event = key({ key: 'ArrowLeft', altKey: true });
|
let event = key({ key: 'ArrowLeft', altKey: true });
|
||||||
expect(event.defaultPrevented).toBe(true);
|
expect(event.defaultPrevented).toBe(true);
|
||||||
await settle();
|
await settle();
|
||||||
@@ -450,19 +483,15 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
event = key({ key: 'ArrowRight', altKey: true });
|
event = key({ key: 'ArrowRight', altKey: true });
|
||||||
await settle();
|
await settle();
|
||||||
expect(view()).toBe('routes');
|
expect(view()).toBe('routes');
|
||||||
// Typing in the route filter keeps the arrows for the text field.
|
|
||||||
const search = container.querySelector<HTMLInputElement>('input[type="search"]')!;
|
const search = container.querySelector<HTMLInputElement>('input[type="search"]')!;
|
||||||
event = key({ key: 'ArrowLeft', altKey: true }, search);
|
event = key({ key: 'ArrowLeft', altKey: true }, search);
|
||||||
expect(event.defaultPrevented).toBe(false);
|
expect(event.defaultPrevented).toBe(false);
|
||||||
expect(view()).toBe('routes');
|
expect(view()).toBe('routes');
|
||||||
// Without Alt, or with another modifier, the arrows are not history keys.
|
|
||||||
expect(key({ key: 'ArrowLeft' }).defaultPrevented).toBe(false);
|
expect(key({ key: 'ArrowLeft' }).defaultPrevented).toBe(false);
|
||||||
expect(key({ key: 'ArrowLeft', altKey: true, metaKey: true }).defaultPrevented).toBe(false);
|
expect(key({ key: 'ArrowLeft', altKey: true, metaKey: true }).defaultPrevented).toBe(false);
|
||||||
// Another surface on top (help, settings) keeps its keys.
|
|
||||||
await render({ escapeTaken: true });
|
await render({ escapeTaken: true });
|
||||||
expect(key({ key: 'ArrowLeft', altKey: true }).defaultPrevented).toBe(false);
|
expect(key({ key: 'ArrowLeft', altKey: true }).defaultPrevented).toBe(false);
|
||||||
expect(view()).toBe('routes');
|
expect(view()).toBe('routes');
|
||||||
// A hidden Architecture (Galaxy is active) leaves Alt+arrows to that workspace.
|
|
||||||
await render({ active: false });
|
await render({ active: false });
|
||||||
expect(key({ key: 'ArrowLeft', altKey: true }).defaultPrevented).toBe(false);
|
expect(key({ key: 'ArrowLeft', altKey: true }).defaultPrevented).toBe(false);
|
||||||
await settle();
|
await settle();
|
||||||
@@ -472,15 +501,17 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
it('jumps to a recent place as a new navigation and starts a fresh history per project', async () => {
|
it('jumps to a recent place as a new navigation and starts a fresh history per project', async () => {
|
||||||
await render();
|
await render();
|
||||||
await openArea('django');
|
await openArea('django');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await doubleClick('[data-node="file:django/shortcuts.py"]');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="structure"]'));
|
await selectView('routes');
|
||||||
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
|
await openRouteGroup();
|
||||||
await goBack();
|
await goBack();
|
||||||
const recent = container.querySelector<HTMLDetailsElement>('details.atlas-arch-recent')!;
|
const recent = container.querySelector<HTMLDetailsElement>('details.atlas-arch-recent')!;
|
||||||
expect(recent).not.toBeNull();
|
expect(recent).not.toBeNull();
|
||||||
expect(recent.querySelector('summary')?.getAttribute('aria-label')).toBe('Recent');
|
expect(recent.querySelector('summary')?.getAttribute('aria-label')).toBe('Recent');
|
||||||
const places = [...recent.querySelectorAll('[aria-label="Recently visited places"] li button')];
|
const places = [...recent.querySelectorAll('[aria-label="Recently visited places"] li button')];
|
||||||
expect(places.map(item => [item.querySelector('strong')?.textContent, item.querySelector('span')?.textContent ?? ''])).toEqual([
|
expect(places.map(item => [item.querySelector('strong')?.textContent, item.querySelector('span')?.textContent ?? ''])).toEqual([
|
||||||
['Routes', 'Service map'], ['System structure', ''], ['Overview', 'django'], ['Overview', ''],
|
['Routes', 'Endpoints'], ['Routes', 'Endpoints · /edit'], ['Overview', 'django'], ['Overview', ''],
|
||||||
]);
|
]);
|
||||||
expect(places[0]?.getAttribute('aria-current')).toBe('true');
|
expect(places[0]?.getAttribute('aria-current')).toBe('true');
|
||||||
await act(async () => { recent.open = true; });
|
await act(async () => { recent.open = true; });
|
||||||
@@ -488,7 +519,7 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
expect([view(), trail()]).toEqual(['overview', ['sample', 'django']]);
|
expect([view(), trail()]).toEqual(['overview', ['sample', 'django']]);
|
||||||
expect(recent.open).toBe(false);
|
expect(recent.open).toBe(false);
|
||||||
expect(forward()?.disabled).toBe(true);
|
expect(forward()?.disabled).toBe(true);
|
||||||
expect(back()?.title).toBe('Back to Routes · Service map (Alt+Left)');
|
expect(back()?.title).toBe('Back to Routes · Endpoints (Alt+Left)');
|
||||||
|
|
||||||
await render({ projectName: 'other', overview: { ...overview, projectName: 'other' } });
|
await render({ projectName: 'other', overview: { ...overview, projectName: 'other' } });
|
||||||
expect(back()?.disabled).toBe(true);
|
expect(back()?.disabled).toBe(true);
|
||||||
@@ -497,60 +528,70 @@ describe('Architecture back and forward (K27)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
describe('Architecture history controls beside the current location', () => {
|
||||||
* Hand test 2026-10-04: Back, Forward and Recent stood as bare arrows at the right end of the subtab row,
|
|
||||||
* while Galaxy has "← Back", "Forward →" and "▾" at the start of its toolbar (A1), and the Recent menu stayed
|
|
||||||
* open over the content through several steps (A2).
|
|
||||||
*/
|
|
||||||
describe('Architecture history controls as in Galaxy (hand test 2026-10-04)', () => {
|
|
||||||
const recentMenu = () => container.querySelector<HTMLDetailsElement>('details.atlas-arch-recent');
|
const recentMenu = () => container.querySelector<HTMLDetailsElement>('details.atlas-arch-recent');
|
||||||
async function withRecent(): Promise<HTMLDetailsElement> {
|
async function withRecent(): Promise<HTMLDetailsElement> {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await openArea('django');
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="hotspots"]'));
|
|
||||||
const menu = recentMenu();
|
const menu = recentMenu();
|
||||||
expect(menu).not.toBeNull();
|
expect(menu).not.toBeNull();
|
||||||
await act(async () => { menu!.open = true; });
|
await act(async () => { menu!.open = true; });
|
||||||
return menu!;
|
return menu!;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('A1: puts Back, Forward and Recent at the start of the subtab row, worded as in Galaxy', async () => {
|
it('keeps compact, accessible history controls inside the current view and out of the subtab row', async () => {
|
||||||
await render();
|
await render();
|
||||||
await press(container.querySelector<HTMLButtonElement>('[data-view="routes"]'));
|
await openArea('django');
|
||||||
const row = container.querySelector<HTMLElement>('.atlas-arch-tabrow')!;
|
const contexts = [
|
||||||
expect([...row.children].map(item => item.getAttribute('aria-label'))).toEqual(['Architecture history', 'Architecture views']);
|
['overview', '[data-testid="spatial-architecture"]'],
|
||||||
expect(back()?.textContent).toBe('← Back');
|
['routes', '[data-testid="container-map"]'],
|
||||||
expect(back()?.querySelector('.atlas-fit-narrow')?.getAttribute('data-label')).toBe('←');
|
['hotspots', '[data-testid="spatial-architecture"]'],
|
||||||
expect(forward()?.textContent).toBe('Forward →');
|
['structure', '[data-testid="system-architecture"]'],
|
||||||
expect(forward()?.querySelector('.atlas-fit-narrow')?.getAttribute('data-label')).toBe('→');
|
['behavior', '.behavior-journey'],
|
||||||
|
];
|
||||||
|
for (const [name, selector] of contexts) {
|
||||||
|
await selectView(name!);
|
||||||
|
const row = container.querySelector<HTMLElement>('.atlas-arch-tabrow')!;
|
||||||
|
expect([...row.children].map(item => item.getAttribute('aria-label'))).toEqual(['Architecture views']);
|
||||||
|
expect(row.querySelector('[aria-label="Architecture history"]')).toBeNull();
|
||||||
|
expect(container.querySelectorAll('[aria-label="Architecture history"]')).toHaveLength(1);
|
||||||
|
const currentView = container.querySelector(selector!);
|
||||||
|
expect(currentView?.contains(history()!)).toBe(true);
|
||||||
|
expect(history()?.parentElement).toBe(currentView?.querySelector(name === 'behavior' ? '.behavior-requests' : '.atlas-arch-context'));
|
||||||
|
expect(back()?.textContent).toBe('');
|
||||||
|
expect(forward()?.textContent).toBe('');
|
||||||
|
expect(back()?.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true');
|
||||||
|
expect(forward()?.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true');
|
||||||
|
expect(back()?.getAttribute('aria-label')).toBe('Back');
|
||||||
|
expect(forward()?.getAttribute('aria-label')).toBe('Forward');
|
||||||
|
expect(recentMenu()?.querySelector('summary')?.getAttribute('aria-label')).toBe('Recent');
|
||||||
|
expect(recentMenu()?.querySelector('summary svg')?.getAttribute('aria-hidden')).toBe('true');
|
||||||
|
}
|
||||||
|
await selectView('routes');
|
||||||
|
await clickText('Endpoints', container.querySelector('[aria-label="Routes perspective"]')!);
|
||||||
|
expect(container.querySelector('[data-testid="spatial-architecture"]')?.contains(history()!)).toBe(true);
|
||||||
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
expect(back()?.title).toBe('Back to Overview (Alt+Left)');
|
||||||
expect(recentMenu()?.querySelector('summary')?.textContent).toBe('▾');
|
|
||||||
// The row measures itself like Galaxy's toolbar: words while the tabs fit, the glyphs once they would scroll.
|
|
||||||
expect(row.dataset.fit).toBe('full');
|
|
||||||
const tabs = row.querySelector<HTMLElement>('nav')!;
|
|
||||||
Object.defineProperties(tabs, { clientWidth: { configurable: true, value: 300 }, scrollWidth: { configurable: true, value: 520 } });
|
|
||||||
expect(fitToolbar(row)).toBe('compact');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('A2: closes the Recent menu on a press elsewhere and on Escape, which stays with the menu', async () => {
|
it('closes Recent on a press elsewhere and on Escape, which stays with the menu', async () => {
|
||||||
const menu = await withRecent();
|
const menu = await withRecent();
|
||||||
await act(async () => { container.querySelector('.atlas-arch-tabs')!.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })); });
|
await act(async () => { container.querySelector('.atlas-arch-tabs')!.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })); });
|
||||||
expect(menu.open).toBe(false);
|
expect(menu.open).toBe(false);
|
||||||
await act(async () => { menu.open = true; });
|
await act(async () => { menu.open = true; });
|
||||||
expect(key({ key: 'Escape' }, menu.querySelector('summary')!).defaultPrevented).toBe(true);
|
expect(key({ key: 'Escape' }, menu.querySelector('summary')!).defaultPrevented).toBe(true);
|
||||||
expect(menu.open).toBe(false);
|
expect(menu.open).toBe(false);
|
||||||
expect(view()).toBe('hotspots');
|
expect([view(), trail()]).toEqual(['overview', ['sample', 'django']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('A2: closes the Recent menu when Back or Alt+Right change the place', async () => {
|
it('closes Recent when Back or Alt+Right changes the place', async () => {
|
||||||
const menu = await withRecent();
|
const menu = await withRecent();
|
||||||
await goBack();
|
await goBack();
|
||||||
expect(view()).toBe('routes');
|
expect(trail()).toEqual(['sample']);
|
||||||
expect(menu.open).toBe(false);
|
expect(menu.open).toBe(false);
|
||||||
await act(async () => { menu.open = true; });
|
await act(async () => { menu.open = true; });
|
||||||
expect(key({ key: 'ArrowRight', altKey: true }).defaultPrevented).toBe(true);
|
expect(key({ key: 'ArrowRight', altKey: true }).defaultPrevented).toBe(true);
|
||||||
await settle();
|
await settle();
|
||||||
expect(view()).toBe('hotspots');
|
expect(trail()).toEqual(['sample', 'django']);
|
||||||
expect(menu.open).toBe(false);
|
expect(menu.open).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import type { JSX, ReactNode } from 'react';
|
import type { JSX, ReactNode } from 'react';
|
||||||
import type { ArchitectureHotspot, ArchitectureOverviewDto } from '../core/intelligence-provider';
|
import type { ArchitectureHotspot, ArchitectureOverviewDto } from '../core/intelligence-provider';
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +8,6 @@ import type { ArchitectureView, ConfigStorage } from './architecture-model';
|
|||||||
import { architectureHistoryText as historyText, architectureText as text } from './strings';
|
import { architectureHistoryText as historyText, architectureText as text } from './strings';
|
||||||
import { peekNavigation, type NavigationHistory } from '../graph/navigation-history';
|
import { peekNavigation, type NavigationHistory } from '../graph/navigation-history';
|
||||||
import { useDismissibleMenu } from '../graph/use-dismissible-menu';
|
import { useDismissibleMenu } from '../graph/use-dismissible-menu';
|
||||||
import { FitLabel, useToolbarFit } from '../galaxy/toolbar-fit';
|
|
||||||
import {
|
import {
|
||||||
architectureEntryDetail, architectureEntryLabel, architectureEntryName, architectureHistoryOptions, initialArchitecturePlace,
|
architectureEntryDetail, architectureEntryLabel, architectureEntryName, architectureHistoryOptions, initialArchitecturePlace,
|
||||||
type ArchitectureHistoryEntry, type PlaceChange, type RoutesPerspective, type SpatialPlace, type SystemPlace,
|
type ArchitectureHistoryEntry, type PlaceChange, type RoutesPerspective, type SpatialPlace, type SystemPlace,
|
||||||
@@ -123,14 +122,7 @@ function Findings({ view, data, empty, onNavigate }: {
|
|||||||
|
|
||||||
const paletteStyle = scenePaletteStyle();
|
const paletteStyle = scenePaletteStyle();
|
||||||
|
|
||||||
/**
|
/** Compact exploration history beside the current scope. Tooltips name each destination. */
|
||||||
* Back, Forward and the Recent list for the whole workspace (K27). The
|
|
||||||
* tooltips name the target ("Back to Overview · django"); the list holds the
|
|
||||||
* last distinct places, newest first, and the current one is not a target.
|
|
||||||
* They stand at the start of the subtab row and read as in Galaxy, "← Back",
|
|
||||||
* "Forward →" and "▾", with the glyphs alone once the tabs would scroll
|
|
||||||
* (hand test 2026-10-04, A1). The menu closes as Galaxy's does (A2).
|
|
||||||
*/
|
|
||||||
function HistoryControls({ history, place, onGo, onJump }: {
|
function HistoryControls({ history, place, onGo, onJump }: {
|
||||||
history: NavigationHistory<ArchitectureHistoryEntry>; place: ArchitectureHistoryEntry;
|
history: NavigationHistory<ArchitectureHistoryEntry>; place: ArchitectureHistoryEntry;
|
||||||
onGo: (step: -1 | 1) => void; onJump: (entry: ArchitectureHistoryEntry) => void;
|
onGo: (step: -1 | 1) => void; onJump: (entry: ArchitectureHistoryEntry) => void;
|
||||||
@@ -141,12 +133,12 @@ function HistoryControls({ history, place, onGo, onJump }: {
|
|||||||
return <div className="atlas-arch-history" role="group" aria-label={historyText.group} data-position={`${history.index + 1}/${history.entries.length}`}>
|
return <div className="atlas-arch-history" role="group" aria-label={historyText.group} data-position={`${history.index + 1}/${history.entries.length}`}>
|
||||||
<button type="button" aria-label={historyText.back} disabled={!back} onClick={() => onGo(-1)}
|
<button type="button" aria-label={historyText.back} disabled={!back} onClick={() => onGo(-1)}
|
||||||
title={back ? historyText.backTo(architectureEntryLabel(back)) : historyText.noBack}>
|
title={back ? historyText.backTo(architectureEntryLabel(back)) : historyText.noBack}>
|
||||||
<FitLabel wide={historyText.backWide} narrow={historyText.backGlyph} /></button>
|
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12 5-5 5 5 5" /></svg></button>
|
||||||
<button type="button" aria-label={historyText.forward} disabled={!forward} onClick={() => onGo(1)}
|
<button type="button" aria-label={historyText.forward} disabled={!forward} onClick={() => onGo(1)}
|
||||||
title={forward ? historyText.forwardTo(architectureEntryLabel(forward)) : historyText.noForward}>
|
title={forward ? historyText.forwardTo(architectureEntryLabel(forward)) : historyText.noForward}>
|
||||||
<FitLabel wide={historyText.forwardWide} narrow={historyText.forwardGlyph} /></button>
|
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m8 5 5 5-5 5" /></svg></button>
|
||||||
{history.recent.length > 1 && <details className="atlas-arch-recent" ref={menu.ref}>
|
{history.recent.length > 1 && <details className="atlas-arch-recent" ref={menu.ref}>
|
||||||
<summary title={historyText.recentTitle} aria-label={historyText.recent}>{historyText.recentGlyph}</summary>
|
<summary title={historyText.recentTitle} aria-label={historyText.recent}><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 6a7 7 0 1 1-1 7M4 2v4H0m10 0v4l3 2" /></svg></summary>
|
||||||
<ul className="atlas-arch-recent-menu" aria-label={historyText.recentList}>{history.recent.map(entry => {
|
<ul className="atlas-arch-recent-menu" aria-label={historyText.recentList}>{history.recent.map(entry => {
|
||||||
const id = architectureHistoryOptions.recentKey?.(entry);
|
const id = architectureHistoryOptions.recentKey?.(entry);
|
||||||
const current = id === here;
|
const current = id === here;
|
||||||
@@ -169,25 +161,22 @@ function ArchitectureWorkspace({ projectName, overview, loading = false, error,
|
|||||||
* route perspective and filter, System structure focus and groups, Behavior
|
* route perspective and filter, System structure focus and groups, Behavior
|
||||||
* start, destination and position, and Plan or 3D of each scene. The views
|
* start, destination and position, and Plan or 3D of each scene. The views
|
||||||
* below read their part of it and report changes back, so Back and Forward
|
* below read their part of it and report changes back, so Back and Forward
|
||||||
* restore every part at once; the one Back for all of them sits before the
|
* restore every part at once. Only deliberate exploration adds a stop;
|
||||||
* subtabs. A filter is a search for this visit: one saved by an earlier
|
* tabs and display controls update the current visit. A filter saved by an earlier
|
||||||
* session would silently hide routes, so only the subtab is read back.
|
* session would silently hide routes, so only the subtab is read back.
|
||||||
*/
|
*/
|
||||||
const navigation = useArchitectureHistory(() => initialArchitecturePlace(readArchitectureConfig(storage, projectName).view), {
|
const navigation = useArchitectureHistory(() => initialArchitecturePlace(readArchitectureConfig(storage, projectName).view), {
|
||||||
active, escapeTaken, onRestore: (from, to) => { if (from.view !== to.view) onSelectionEvidence?.(undefined); },
|
active, escapeTaken, onRestore: (from, to) => { if (from.view !== to.view) onSelectionEvidence?.(undefined); },
|
||||||
});
|
});
|
||||||
const { place, navigate } = navigation;
|
const { place, navigate } = navigation;
|
||||||
// The row measures itself as Galaxy's toolbar does (toolbar-fit.tsx): it counts as narrow once the tabs would scroll.
|
|
||||||
const tabRow = useRef<HTMLDivElement>(null);
|
|
||||||
useToolbarFit(tabRow, true);
|
|
||||||
useEffect(() => { saveArchitectureConfig(storage, projectName, { view: place.view, filter: place.filter }); }, [place.view, place.filter, projectName, storage]);
|
useEffect(() => { saveArchitectureConfig(storage, projectName, { view: place.view, filter: place.filter }); }, [place.view, place.filter, projectName, storage]);
|
||||||
const setView = useCallback((view: ArchitectureView) => navigate(current => ({ ...current, view: view === 'dependencies' ? 'overview' : view })), [navigate]);
|
const setView = useCallback((view: ArchitectureView) => navigate(current => ({ ...current, view: view === 'dependencies' ? 'overview' : view }), true), [navigate]);
|
||||||
const changeSpatial = useCallback<PlaceChange<SpatialPlace>>((change, automatic) =>
|
const changeSpatial = useCallback<PlaceChange<SpatialPlace>>((change, automatic) =>
|
||||||
navigate(current => ({ ...current, spatial: { ...current.spatial, ...change } }), automatic), [navigate]);
|
navigate(current => ({ ...current, spatial: { ...current.spatial, ...change } }), automatic), [navigate]);
|
||||||
const changeSystem = useCallback<PlaceChange<SystemPlace>>((change, automatic) =>
|
const changeSystem = useCallback<PlaceChange<SystemPlace>>((change, automatic) =>
|
||||||
navigate(current => ({ ...current, system: { ...current.system, ...change } }), automatic), [navigate]);
|
navigate(current => ({ ...current, system: { ...current.system, ...change } }), automatic), [navigate]);
|
||||||
const changePerspective = useCallback((routes: RoutesPerspective) => navigate(current => ({ ...current, routes })), [navigate]);
|
const changePerspective = useCallback((routes: RoutesPerspective) => navigate(current => ({ ...current, routes }), true), [navigate]);
|
||||||
// An opened route group ("Show these N routes") is a step at once; typing waits for a pause.
|
// Opening a route group is navigation; typing only refines the current visit.
|
||||||
const openRouteGroup = useCallback((filter: string) => navigate(current => ({ ...current, filter })), [navigate]);
|
const openRouteGroup = useCallback((filter: string) => navigate(current => ({ ...current, filter })), [navigate]);
|
||||||
// A cached summary from another project must never appear here.
|
// A cached summary from another project must never appear here.
|
||||||
const data = overview?.projectName && overview.projectName !== projectName ? undefined : overview;
|
const data = overview?.projectName && overview.projectName !== projectName ? undefined : overview;
|
||||||
@@ -198,24 +187,25 @@ function ArchitectureWorkspace({ projectName, overview, loading = false, error,
|
|||||||
}, [active, projectName, systemView, ready, graph, onSelectionEvidence]);
|
}, [active, projectName, systemView, ready, graph, onSelectionEvidence]);
|
||||||
// Only the Routes view offers the filter, so no other view may receive a stale one.
|
// Only the Routes view offers the filter, so no other view may receive a stale one.
|
||||||
const filter = place.view === 'routes' ? navigation.filter : '';
|
const filter = place.view === 'routes' ? navigation.filter : '';
|
||||||
|
const historyControls = <HistoryControls history={navigation.history} place={place} onGo={navigation.go} onJump={navigation.jump} />;
|
||||||
const spatialProps = !systemView && ready && data && graph ? {
|
const spatialProps = !systemView && ready && data && graph ? {
|
||||||
project: projectName, generation: graphGeneration, graph, overview: data,
|
project: projectName, generation: graphGeneration, graph, overview: data,
|
||||||
view: place.view === 'dependencies' || place.view === 'structure' || place.view === 'behavior' ? 'overview' as const : place.view,
|
view: place.view === 'dependencies' || place.view === 'structure' || place.view === 'behavior' ? 'overview' as const : place.view,
|
||||||
filter, onFilter: openRouteGroup, active, graphNote, coverage, onSelect, onClearSelection, onSelectionEvidence, selectionPanel, onNavigate,
|
filter, onFilter: openRouteGroup, active, graphNote, coverage, onSelect, onClearSelection, onSelectionEvidence, selectionPanel, onNavigate, navigation: historyControls,
|
||||||
onView: (view: ArchitectureView) => { onSelectionEvidence?.(undefined); setView(view); }, place: place.spatial, onPlace: changeSpatial,
|
onView: (view: ArchitectureView) => { onSelectionEvidence?.(undefined); setView(view); }, place: place.spatial, onPlace: changeSpatial,
|
||||||
} : undefined;
|
} : undefined;
|
||||||
// Every scene below draws with the same palette; the DOM reads it as CSS variables.
|
// Every scene below draws with the same palette; the DOM reads it as CSS variables.
|
||||||
return <section className="atlas-architecture" data-testid="atlas-architecture" data-system-view={systemView} aria-label={text.title} aria-busy={!systemView && loading} style={paletteStyle}>
|
return <section className="atlas-architecture" data-testid="atlas-architecture" data-system-view={systemView} aria-label={text.title} aria-busy={!systemView && loading} style={paletteStyle}>
|
||||||
<div className="atlas-arch-tabrow" ref={tabRow}>
|
<div className="atlas-arch-tabrow">
|
||||||
<HistoryControls history={navigation.history} place={place} onGo={navigation.go} onJump={navigation.jump} />
|
<nav className="atlas-arch-tabs" aria-label={text.navigation}>{ARCHITECTURE_VIEWS.map(view =>
|
||||||
<nav className="atlas-arch-tabs" aria-label={text.navigation} data-fit-whole="">{ARCHITECTURE_VIEWS.map(view =>
|
|
||||||
<button className="atlas-arch-tab" key={view} aria-pressed={place.view === view || (view === 'overview' && ['dependencies', 'entryPoints'].includes(place.view))} data-view={view}
|
<button className="atlas-arch-tab" key={view} aria-pressed={place.view === view || (view === 'overview' && ['dependencies', 'entryPoints'].includes(place.view))} data-view={view}
|
||||||
onClick={() => { if (view !== place.view) onSelectionEvidence?.(undefined); setView(view); }}>{text.views[view]}</button>)}</nav>
|
onClick={() => { if (view !== place.view) onSelectionEvidence?.(undefined); setView(view); }}>{text.views[view]}</button>)}</nav>
|
||||||
</div>
|
</div>
|
||||||
{systemView && projectName ? <Suspense fallback={<div role="status"><Empty>Preparing system analysis…</Empty></div>}><SystemArchitecture
|
{systemView && projectName ? <Suspense fallback={<div role="status"><Empty>Preparing system analysis…</Empty></div>}><SystemArchitecture
|
||||||
project={projectName} generation={graphGeneration} view={systemView} filter={filter} active={active}
|
project={projectName} generation={graphGeneration} view={systemView} filter={filter} active={active}
|
||||||
graph={graph} onSelect={onSelect} onClearSelection={onClearSelection} onSelectionEvidence={onSelectionEvidence} onNavigate={onNavigate} loader={systemArchitectureLoader}
|
graph={graph} onSelect={onSelect} onClearSelection={onClearSelection} onSelectionEvidence={onSelectionEvidence} onNavigate={onNavigate} loader={systemArchitectureLoader}
|
||||||
place={place.system} onPlace={changeSystem} /></Suspense> : null}
|
place={place.system} onPlace={changeSystem} navigation={historyControls} /></Suspense> : null}
|
||||||
|
{!systemView && !spatialProps && <div className="atlas-arch-context">{historyControls}<span>{projectName}</span></div>}
|
||||||
{!systemView && (!projectName ? <Empty>{text.chooseProject}</Empty> : loading ? <div role="status"><Empty>{text.loading}</Empty></div>
|
{!systemView && (!projectName ? <Empty>{text.chooseProject}</Empty> : loading ? <div role="status"><Empty>{text.loading}</Empty></div>
|
||||||
: error ? <div role="alert" className="atlas-arch-empty" data-error="true"><p>{text.loadFailed}</p><p className="atlas-arch-error-detail">{error}</p>
|
: error ? <div role="alert" className="atlas-arch-empty" data-error="true"><p>{text.loadFailed}</p><p className="atlas-arch-error-detail">{error}</p>
|
||||||
{onRefresh && <button className="atlas-arch-action" onClick={onRefresh}>{text.retry}</button>}</div>
|
{onRefresh && <button className="atlas-arch-action" onClick={onRefresh}>{text.retry}</button>}</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
import { behaviorJourney } from './behavior-journey-model';
|
import { behaviorJourney } from './behavior-journey-model';
|
||||||
import { projectionLimits, type SystemSceneModel } from './system-architecture-model';
|
import { projectionLimits, type SystemSceneModel } from './system-architecture-model';
|
||||||
import { architectureText as text } from './strings';
|
import { architectureText as text } from './strings';
|
||||||
@@ -27,6 +27,7 @@ export interface BehaviorJourneyProps {
|
|||||||
*/
|
*/
|
||||||
place?: JourneyPlace;
|
place?: JourneyPlace;
|
||||||
onPlace?: PlaceChange<JourneyPlace>;
|
onPlace?: PlaceChange<JourneyPlace>;
|
||||||
|
navigation?: ReactNode;
|
||||||
/** Reports the start shown when none was requested, so a Back or Forward tooltip can name it (K27). */
|
/** Reports the start shown when none was requested, so a Back or Forward tooltip can name it (K27). */
|
||||||
onShownStart?: (name: string) => void;
|
onShownStart?: (name: string) => void;
|
||||||
onClearSelection?: () => void;
|
onClearSelection?: () => void;
|
||||||
@@ -61,7 +62,7 @@ export function journeyPage(scene: SystemSceneModel, step: number, choices = fal
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BehaviorJourney({ project, generation, data, entries, targets, entryId, targetId, active, pending, error, filter,
|
export default function BehaviorJourney({ project, generation, data, entries, targets, entryId, targetId, active, pending, error, filter,
|
||||||
onRequest, onRefresh, refresh, suggestedEntries, onSelectSymbol, onNavigate, onClearSelection, onSelectionEvidence, from, place: liftedPlace, onPlace, onShownStart }: BehaviorJourneyProps) {
|
onRequest, onRefresh, refresh, suggestedEntries, onSelectSymbol, onNavigate, onClearSelection, onSelectionEvidence, from, place: liftedPlace, onPlace, onShownStart, navigation }: BehaviorJourneyProps) {
|
||||||
const [place, changePlace] = useLiftedPlace<JourneyPlace>(liftedPlace, onPlace, () => ({}));
|
const [place, changePlace] = useLiftedPlace<JourneyPlace>(liftedPlace, onPlace, () => ({}));
|
||||||
const { path: pathIndex = 0, step = 0, page: branchPage = 0, planar = false } = place;
|
const { path: pathIndex = 0, step = 0, page: branchPage = 0, planar = false } = place;
|
||||||
const [overview, setOverview] = useState(false);
|
const [overview, setOverview] = useState(false);
|
||||||
@@ -173,6 +174,7 @@ export default function BehaviorJourney({ project, generation, data, entries, ta
|
|||||||
<p>{path ? 'Follow one recorded call chain across the parts it touches.' : 'Choose a starting operation. Explore its calls, or follow a path to a destination.'}</p></div>
|
<p>{path ? 'Follow one recorded call chain across the parts it touches.' : 'Choose a starting operation. Explore its calls, or follow a path to a destination.'}</p></div>
|
||||||
<RefreshControl labels={text.refreshFeedback.behavior} feedback={refresh} onRefresh={onRefresh} /></header>
|
<RefreshControl labels={text.refreshFeedback.behavior} feedback={refresh} onRefresh={onRefresh} /></header>
|
||||||
<div className="behavior-requests">
|
<div className="behavior-requests">
|
||||||
|
{navigation}
|
||||||
{/*
|
{/*
|
||||||
* The field names the operation the journey shows, also when the projection chose it. Suggestions come
|
* The field names the operation the journey shows, also when the projection chose it. Suggestions come
|
||||||
* first, then every operation alphabetically; the filter before it narrows both (hand test 2026-10-04, A4).
|
* first, then every operation alphabetically; the filter before it narrows both (hand test 2026-10-04, A4).
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface Props {
|
|||||||
project: string; generation?: string; active: boolean; filter: string; onNavigate: (path: string, line?: number) => void; onClearSelection?: () => void; onSelectionEvidence?: SelectionEvidenceListener;
|
project: string; generation?: string; active: boolean; filter: string; onNavigate: (path: string, line?: number) => void; onClearSelection?: () => void; onSelectionEvidence?: SelectionEvidenceListener;
|
||||||
/** Leads from a project without deployment files to the routes it does have. */
|
/** Leads from a project without deployment files to the routes it does have. */
|
||||||
onShowEndpoints?: () => void;
|
onShowEndpoints?: () => void;
|
||||||
|
navigation?: ReactNode;
|
||||||
}
|
}
|
||||||
/** Reading the deployment inventory: a project without Compose files is an answer, not a failure. */
|
/** Reading the deployment inventory: a project without Compose files is an answer, not a failure. */
|
||||||
type Discovery = keyof typeof text.serviceMap | undefined;
|
type Discovery = keyof typeof text.serviceMap | undefined;
|
||||||
@@ -29,7 +30,7 @@ const relativeSource = (absolute: string, selection: ContainerSelection) => {
|
|||||||
return absolute.startsWith(prefix) ? absolute.slice(prefix.length) : undefined;
|
return absolute.startsWith(prefix) ? absolute.slice(prefix.length) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ContainerMap({ project, generation, active, filter, onNavigate, onClearSelection, onSelectionEvidence, onShowEndpoints }: Props) {
|
export default function ContainerMap({ project, generation, active, filter, onNavigate, onClearSelection, onSelectionEvidence, onShowEndpoints, navigation }: Props) {
|
||||||
const [available, setAvailable] = useState<ProjectEntry[]>([]);
|
const [available, setAvailable] = useState<ProjectEntry[]>([]);
|
||||||
const [selections, setSelections] = useState<ContainerSelection[]>([]);
|
const [selections, setSelections] = useState<ContainerSelection[]>([]);
|
||||||
const [discovery, setDiscovery] = useState<Discovery>('reading');
|
const [discovery, setDiscovery] = useState<Discovery>('reading');
|
||||||
@@ -160,7 +161,7 @@ export default function ContainerMap({ project, generation, active, filter, onNa
|
|||||||
{index > 0 && <button aria-label={`Remove ${selection.inventory.project}`} onClick={() => setSelections(current => current.filter((_, i) => i !== index))}>×</button>}</label>)}
|
{index > 0 && <button aria-label={`Remove ${selection.inventory.project}`} onClick={() => setSelections(current => current.filter((_, i) => i !== index))}>×</button>}</label>)}
|
||||||
{selections.length < 4 && <select aria-label="Compare indexed project" value="" disabled={adding} onChange={event => { void addProject(event.target.value); }}><option value="">{adding ? 'Reading project…' : '+ Compare project'}</option>
|
{selections.length < 4 && <select aria-label="Compare indexed project" value="" disabled={adding} onChange={event => { void addProject(event.target.value); }}><option value="">{adding ? 'Reading project…' : '+ Compare project'}</option>
|
||||||
{available.filter(entry => !selections.some(selection => selection.inventory.project === entry.name)).map(entry => <option key={entry.name} value={entry.name}>{entry.name}</option>)}</select>}</div>
|
{available.filter(entry => !selections.some(selection => selection.inventory.project === entry.name)).map(entry => <option key={entry.name} value={entry.name}>{entry.name}</option>)}</select>}</div>
|
||||||
<div className="spatial-controls"><nav aria-label="Service navigation"><button onClick={clearSelection}>All services</button>{focus && <span> / {servicesById.get(focus)?.name} and neighbors</span>}</nav>
|
<div className="spatial-controls"><div className="atlas-arch-context">{navigation}<nav aria-label="Service navigation"><button onClick={clearSelection}>All services</button>{focus && <span> / {servicesById.get(focus)?.name} and neighbors</span>}</nav></div>
|
||||||
<div className="container-edge-key"><span><i className="container-call-key" />Calls</span><label><input type="checkbox" checked={configured} onChange={event => setConfigured(event.target.checked)} />Configured destinations</label>
|
<div className="container-edge-key"><span><i className="container-call-key" />Calls</span><label><input type="checkbox" checked={configured} onChange={event => setConfigured(event.target.checked)} />Configured destinations</label>
|
||||||
<label><input type="checkbox" checked={startup} onChange={event => setStartup(event.target.checked)} />Startup dependencies</label></div></div>
|
<label><input type="checkbox" checked={startup} onChange={event => setStartup(event.target.checked)} />Startup dependencies</label></div></div>
|
||||||
{discovery && <div role="status" className="spatial-notice container-discovery" data-discovery={discovery}><p>{text.serviceMap[discovery]}</p>
|
{discovery && <div role="status" className="spatial-notice container-discovery" data-discovery={discovery}><p>{text.serviceMap[discovery]}</p>
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ export default function RoutesArchitecture({ perspective, onPerspective, ...prop
|
|||||||
<button aria-pressed={mode === 'endpoints'} onClick={() => show('endpoints')}>{text.endpoints}</button>
|
<button aria-pressed={mode === 'endpoints'} onClick={() => show('endpoints')}>{text.endpoints}</button>
|
||||||
</nav>{mode === 'services' ? <Suspense fallback={<p role="status">{text.preparing}</p>}><ContainerMap
|
</nav>{mode === 'services' ? <Suspense fallback={<p role="status">{text.preparing}</p>}><ContainerMap
|
||||||
project={props.project} generation={props.generation} active={props.active} filter={props.filter} onNavigate={props.onNavigate} onClearSelection={props.onClearSelection} onSelectionEvidence={props.onSelectionEvidence}
|
project={props.project} generation={props.generation} active={props.active} filter={props.filter} onNavigate={props.onNavigate} onClearSelection={props.onClearSelection} onSelectionEvidence={props.onSelectionEvidence}
|
||||||
onShowEndpoints={() => show('endpoints')} /></Suspense>
|
onShowEndpoints={() => show('endpoints')} navigation={props.navigation} /></Suspense>
|
||||||
: <SpatialArchitecture {...props} />}</>;
|
: <SpatialArchitecture {...props} />}</>;
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ interface Props {
|
|||||||
/** The opened area or file, the hotspot area and Plan or 3D, lifted to the workspace for Back and Forward (K27). */
|
/** The opened area or file, the hotspot area and Plan or 3D, lifted to the workspace for Back and Forward (K27). */
|
||||||
place?: SpatialPlace;
|
place?: SpatialPlace;
|
||||||
onPlace?: PlaceChange<SpatialPlace>;
|
onPlace?: PlaceChange<SpatialPlace>;
|
||||||
|
navigation?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SceneBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
|
class SceneBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
|
||||||
@@ -50,7 +51,7 @@ const viewNotes: Record<SemanticView, string> = {
|
|||||||
};
|
};
|
||||||
const relationKinds = ['CALLS', 'IMPORTS', 'USAGE', 'INHERITS', 'IMPLEMENTS', 'DATA_FLOWS'];
|
const relationKinds = ['CALLS', 'IMPORTS', 'USAGE', 'INHERITS', 'IMPLEMENTS', 'DATA_FLOWS'];
|
||||||
|
|
||||||
export default function SpatialArchitecture({ project, generation, graph, overview, view, filter, active, graphNote, onSelect, onClearSelection, onSelectionEvidence, selectionPanel, onNavigate, onView, onFilter, coverage, place: liftedPlace, onPlace }: Props) {
|
export default function SpatialArchitecture({ project, generation, graph, overview, view, filter, active, graphNote, onSelect, onClearSelection, onSelectionEvidence, selectionPanel, onNavigate, onView, onFilter, coverage, place: liftedPlace, onPlace, navigation }: Props) {
|
||||||
const [place, changePlace] = useLiftedPlace<SpatialPlace>(liftedPlace, onPlace, () => ({ planar: false }));
|
const [place, changePlace] = useLiftedPlace<SpatialPlace>(liftedPlace, onPlace, () => ({ planar: false }));
|
||||||
const { planar } = place;
|
const { planar } = place;
|
||||||
// The place keeps the opened area or file and the hotspot area for Back and Forward (K27), but only the view
|
// The place keeps the opened area or file and the hotspot area for Back and Forward (K27), but only the view
|
||||||
@@ -188,14 +189,17 @@ export default function SpatialArchitecture({ project, generation, graph, overvi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="spatial-controls">
|
<div className="spatial-controls">
|
||||||
{['overview', 'dependencies', 'entryPoints'].includes(view) && <div className="spatial-projection-modes" role="group" aria-label="Overview mode"><button aria-pressed={view !== 'entryPoints'} onClick={() => onView('overview')}>Structure</button><button aria-pressed={view === 'entryPoints'} onClick={() => onView('entryPoints')}>Entry points</button></div>}
|
<div className="atlas-arch-context">{navigation}
|
||||||
{(view === 'overview' || view === 'dependencies') && <nav aria-label="Architecture location"><button onClick={clearScope}>{project}</button>
|
{(view === 'overview' || view === 'dependencies') && <nav aria-label="Architecture location"><button onClick={clearScope}>{project}</button>
|
||||||
{areaPath && areaLevels(areaPath).map((level, index, levels) => <Fragment key={level}><span>/</span><button onClick={() => { changePlace({ areaPath: level, filePath: undefined }); setSelection(undefined); }}>{index ? level.slice(levels[index - 1].length + 1) : level}</button></Fragment>)}{filePath && <><span>/</span><span>{filePath.split('/').at(-1)}</span></>}
|
{areaPath && areaLevels(areaPath).map((level, index, levels) => <Fragment key={level}><span>/</span><button onClick={() => { changePlace({ areaPath: level, filePath: undefined }); setSelection(undefined); }}>{index ? level.slice(levels[index - 1].length + 1) : level}</button></Fragment>)}{filePath && <><span>/</span><span>{filePath.split('/').at(-1)}</span></>}
|
||||||
</nav>}
|
</nav>}
|
||||||
|
{view === 'hotspots' && (hotspotArea ? <nav aria-label="Hotspot area"><button onClick={clearScope}>All hotspots</button><span>/ {hotspotArea}</span></nav> : <span>All hotspots</span>)}
|
||||||
|
{view === 'routes' && <span title={filter || undefined}>{filter || 'All endpoints'}</span>}
|
||||||
|
</div>
|
||||||
|
{['overview', 'dependencies', 'entryPoints'].includes(view) && <div className="spatial-projection-modes" role="group" aria-label="Overview mode"><button aria-pressed={view !== 'entryPoints'} onClick={() => onView('overview')}>Structure</button><button aria-pressed={view === 'entryPoints'} onClick={() => onView('entryPoints')}>Entry points</button></div>}
|
||||||
{view === 'entryPoints' && <><label>Start <select aria-label="Entry point" value={currentEntry?.id ?? ''} onChange={event => { const node = entries.find(node => node.id === Number(event.target.value)); setEntryChoice(node ? { node, generation } : undefined); setSelection(undefined); }}>
|
{view === 'entryPoints' && <><label>Start <select aria-label="Entry point" value={currentEntry?.id ?? ''} onChange={event => { const node = entries.find(node => node.id === Number(event.target.value)); setEntryChoice(node ? { node, generation } : undefined); setSelection(undefined); }}>
|
||||||
{!entries.length && <option value="">No indexed entry points</option>}{entries.map(node => <option key={node.id} value={node.id}>{node.name} · {node.file_path}</option>)}
|
{!entries.length && <option value="">No indexed entry points</option>}{entries.map(node => <option key={node.id} value={node.id}>{node.name} · {node.file_path}</option>)}
|
||||||
</select></label><label>Call depth <select aria-label="Call depth" value={depth} onChange={event => setDepth(Number(event.target.value))}>{[1, 2, 3, 4].map(value => <option key={value}>{value}</option>)}</select></label></>}
|
</select></label><label>Call depth <select aria-label="Call depth" value={depth} onChange={event => setDepth(Number(event.target.value))}>{[1, 2, 3, 4].map(value => <option key={value}>{value}</option>)}</select></label></>}
|
||||||
{view === 'hotspots' && hotspotArea && <nav aria-label="Hotspot area"><button onClick={clearScope}>All hotspots</button><span>/ {hotspotArea}</span></nav>}
|
|
||||||
{view === 'routes' && <RefreshControl labels={text.refreshFeedback.routes} feedback={routeRefresh.feedback}
|
{view === 'routes' && <RefreshControl labels={text.refreshFeedback.routes} feedback={routeRefresh.feedback}
|
||||||
onRefresh={() => { routeRefresh.begin(); setRouteRevision(value => value + 1); }} />}
|
onRefresh={() => { routeRefresh.begin(); setRouteRevision(value => value + 1); }} />}
|
||||||
{view === 'routes' && <label className="spatial-gravity-toggle"><input type="checkbox" checked={includeTestRoutes} onChange={event => { setIncludeTestRoutes(event.target.checked); setSelection(undefined); }} />{routesChecking ? text.includeTestRoutesChecking : text.includeTestRoutes(model.hiddenRoutes ?? 0)}</label>}
|
{view === 'routes' && <label className="spatial-gravity-toggle"><input type="checkbox" checked={includeTestRoutes} onChange={event => { setIncludeTestRoutes(event.target.checked); setSelection(undefined); }} />{routesChecking ? text.includeTestRoutesChecking : text.includeTestRoutes(model.hiddenRoutes ?? 0)}</label>}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||||
import type { GraphData, GraphNode } from '../galaxy/types';
|
import type { GraphData, GraphNode } from '../galaxy/types';
|
||||||
import { useSelectionEvidence, type SelectionEvidenceListener } from '../galaxy/selection-evidence';
|
import { useSelectionEvidence, type SelectionEvidenceListener } from '../galaxy/selection-evidence';
|
||||||
import { loadSystemArchitecture, type SystemArchitectureLoader, type SystemArchitectureResponse, type SystemProjection, type SystemSymbol, type SystemWitness, type SystemCallEvidence } from './system-architecture-source';
|
import { loadSystemArchitecture, type SystemArchitectureLoader, type SystemArchitectureResponse, type SystemProjection, type SystemSymbol, type SystemWitness, type SystemCallEvidence } from './system-architecture-source';
|
||||||
@@ -29,6 +29,7 @@ export interface SystemArchitectureProps {
|
|||||||
/** Focus, expanded groups, the Behavior start and position, and both cameras, lifted to the workspace for Back and Forward (K27). */
|
/** Focus, expanded groups, the Behavior start and position, and both cameras, lifted to the workspace for Back and Forward (K27). */
|
||||||
place?: SystemPlace;
|
place?: SystemPlace;
|
||||||
onPlace?: PlaceChange<SystemPlace>;
|
onPlace?: PlaceChange<SystemPlace>;
|
||||||
|
navigation?: ReactNode;
|
||||||
}
|
}
|
||||||
/** Ranked call-graph flows (route handlers, call-graph roots) from /api/flows. */
|
/** Ranked call-graph flows (route handlers, call-graph roots) from /api/flows. */
|
||||||
export type FlowsLoader = (project: string) => Promise<FlowSummary[]>;
|
export type FlowsLoader = (project: string) => Promise<FlowSummary[]>;
|
||||||
@@ -80,7 +81,7 @@ const connectionAllowed = (type: string, view: ConnectionView) => view === 'all'
|
|||||||
|| (view === 'calls' ? ['CALLS', 'IMPORTS', 'HTTP_CALLS', 'ASYNC_CALLS'] : ['INHERITS', 'IMPLEMENTS']).includes(type);
|
|| (view === 'calls' ? ['CALLS', 'IMPORTS', 'HTTP_CALLS', 'ASYNC_CALLS'] : ['INHERITS', 'IMPLEMENTS']).includes(type);
|
||||||
|
|
||||||
/** Poll only while this view is active. A new request key hides stale results before its effect runs. */
|
/** Poll only while this view is active. A new request key hides stale results before its effect runs. */
|
||||||
export default function SystemArchitecture({ project, generation, view, filter, active, graph, onSelect, onClearSelection, onSelectionEvidence, onNavigate, loader = loadSystemArchitecture, flowsLoader = loadFlows, operationNames = nameFlowStarts, place: liftedPlace, onPlace }: SystemArchitectureProps) {
|
export default function SystemArchitecture({ project, generation, view, filter, active, graph, onSelect, onClearSelection, onSelectionEvidence, onNavigate, loader = loadSystemArchitecture, flowsLoader = loadFlows, operationNames = nameFlowStarts, place: liftedPlace, onPlace, navigation }: SystemArchitectureProps) {
|
||||||
const [place, changePlace] = useLiftedPlace<SystemPlace>(liftedPlace, onPlace, () => ({ expanded: [] }));
|
const [place, changePlace] = useLiftedPlace<SystemPlace>(liftedPlace, onPlace, () => ({ expanded: [] }));
|
||||||
const entryChoice = place.behavior;
|
const entryChoice = place.behavior;
|
||||||
/** A start the page picks itself (the suggestion, a reset) completes the current step instead of adding one. */
|
/** A start the page picks itself (the suggestion, a reset) completes the current step instead of adding one. */
|
||||||
@@ -347,7 +348,7 @@ export default function SystemArchitecture({ project, generation, view, filter,
|
|||||||
const behaviorPage = view === 'behavior' ? <BehaviorJourney project={project} generation={projectionGeneration} data={queryData}
|
const behaviorPage = view === 'behavior' ? <BehaviorJourney project={project} generation={projectionGeneration} data={queryData}
|
||||||
entries={entries} targets={targets} entryId={requestedEntry} targetId={requestedTarget} active={active} pending={queryPending}
|
entries={entries} targets={targets} entryId={requestedEntry} targetId={requestedTarget} active={active} pending={queryPending}
|
||||||
error={error} filter={filter} onRefresh={refreshAnalysis} refresh={refresh.feedback} suggestedEntries={suggestedStarts} onNavigate={onNavigate} onSelectSymbol={selectSymbol} onClearSelection={onClearSelection} onSelectionEvidence={onSelectionEvidence}
|
error={error} filter={filter} onRefresh={refreshAnalysis} refresh={refresh.feedback} suggestedEntries={suggestedStarts} onNavigate={onNavigate} onSelectSymbol={selectSymbol} onClearSelection={onClearSelection} onSelectionEvidence={onSelectionEvidence}
|
||||||
from={requestedEntry !== undefined ? entryChoice?.from : undefined} place={journeyPlace} onPlace={changeJourney}
|
from={requestedEntry !== undefined ? entryChoice?.from : undefined} place={journeyPlace} onPlace={changeJourney} navigation={navigation}
|
||||||
onShownStart={name => { if (name !== place.shown) changePlace({ shown: name }, true); }}
|
onShownStart={name => { if (name !== place.shown) changePlace({ shown: name }, true); }}
|
||||||
onRequest={(entry, targetId, detail) => {
|
onRequest={(entry, targetId, detail) => {
|
||||||
const analysisGeneration = projectionGeneration ?? sourceGeneration;
|
const analysisGeneration = projectionGeneration ?? sourceGeneration;
|
||||||
@@ -363,7 +364,7 @@ export default function SystemArchitecture({ project, generation, view, filter,
|
|||||||
<RefreshControl className="atlas-arch-action system-refresh" labels={text.refreshFeedback.structure} feedback={refresh.feedback}
|
<RefreshControl className="atlas-arch-action system-refresh" labels={text.refreshFeedback.structure} feedback={refresh.feedback}
|
||||||
onRefresh={() => { refreshAnalysis(); setResetKey(value => value + 1); }} />
|
onRefresh={() => { refreshAnalysis(); setResetKey(value => value + 1); }} />
|
||||||
</div><div className="system-controls">
|
</div><div className="system-controls">
|
||||||
<div className="system-scope-actions"><button disabled={!focusId} onClick={clearSelection}>Whole system</button></div>
|
<div className="system-scope-actions atlas-arch-context">{navigation}<button disabled={!focusId} onClick={clearSelection}>Whole system</button>{place.focus && <span title={place.focus.label}>/ {place.focus.label}</span>}</div>
|
||||||
{view === 'structure' && <label>Connections <select aria-label="Connection view" value={connectionView} onChange={event => { setConnectionView(event.target.value as ConnectionView); setCyclesOnly(false); }}>
|
{view === 'structure' && <label>Connections <select aria-label="Connection view" value={connectionView} onChange={event => { setConnectionView(event.target.value as ConnectionView); setCyclesOnly(false); }}>
|
||||||
<option value="calls">Calls & imports</option><option value="types">Type relationships</option><option value="all">All relationships</option>
|
<option value="calls">Calls & imports</option><option value="types">Type relationships</option><option value="all">All relationships</option>
|
||||||
</select></label>}
|
</select></label>}
|
||||||
|
|||||||
@@ -28,24 +28,26 @@ describe('the Architecture history entry (K27)', () => {
|
|||||||
expect(initialArchitecturePlace('dependencies').view).toBe('overview');
|
expect(initialArchitecturePlace('dependencies').view).toBe('overview');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is a new step for every subtab and for what each subtab opens', () => {
|
it('identifies what each subtab opens without treating display settings as exploration', () => {
|
||||||
const views = ['overview', 'entryPoints', 'routes', 'hotspots', 'structure', 'behavior'] as const;
|
const views = ['overview', 'entryPoints', 'routes', 'hotspots', 'structure', 'behavior'] as const;
|
||||||
expect(new Set(views.map(view => key(at({ view }))))).toHaveProperty('size', views.length);
|
expect(new Set(views.map(view => key(at({ view }))))).toHaveProperty('size', views.length);
|
||||||
// Overview: the opened area and file, and Plan or 3D.
|
// Overview: the opened area and file. Plan or 3D stays with that location.
|
||||||
const django = at({ spatial: { planar: false, areaPath: 'django' } });
|
const django = at({ spatial: { planar: false, areaPath: 'django' } });
|
||||||
expect(key(django)).not.toBe(key(start));
|
expect(key(django)).not.toBe(key(start));
|
||||||
expect(key(at({ spatial: { planar: false, areaPath: 'django', filePath: 'django/shortcuts.py' } }))).not.toBe(key(django));
|
expect(key(at({ spatial: { planar: false, areaPath: 'django', filePath: 'django/shortcuts.py' } }))).not.toBe(key(django));
|
||||||
expect(key(at({ spatial: { planar: true, areaPath: 'django' } }))).not.toBe(key(django));
|
expect(key(at({ spatial: { planar: true, areaPath: 'django' } }))).toBe(key(django));
|
||||||
// Routes: the perspective and the filter an opened route group sets.
|
// Routes: explicit route-group navigation uses the perspective and filter.
|
||||||
|
// The hook replaces the current entry when these are changed by context controls.
|
||||||
const routes = at({ view: 'routes' });
|
const routes = at({ view: 'routes' });
|
||||||
expect(key({ ...routes, routes: 'endpoints' })).not.toBe(key(routes));
|
expect(key({ ...routes, routes: 'endpoints' })).not.toBe(key(routes));
|
||||||
expect(key({ ...routes, routes: 'endpoints', filter: '/edit' })).not.toBe(key({ ...routes, routes: 'endpoints' }));
|
expect(key({ ...routes, routes: 'endpoints', filter: '/edit' })).not.toBe(key({ ...routes, routes: 'endpoints' }));
|
||||||
// System structure: the focus and the expanded groups, in any order.
|
// System structure: the focus is exploration; expanded groups stay with it.
|
||||||
const structure = at({ view: 'structure' });
|
const structure = at({ view: 'structure' });
|
||||||
const focused = { ...structure, system: { expanded: [], focus: { id: 'g:django', label: 'django' } } };
|
const focused = { ...structure, system: { expanded: [], focus: { id: 'g:django', label: 'django' } } };
|
||||||
expect(key(focused)).not.toBe(key(structure));
|
expect(key(focused)).not.toBe(key(structure));
|
||||||
expect(key({ ...structure, system: { expanded: ['g:a', 'g:b'] } })).toBe(key({ ...structure, system: { expanded: ['g:b', 'g:a'] } }));
|
expect(key({ ...structure, system: { expanded: ['g:a', 'g:b'] } })).toBe(key({ ...structure, system: { expanded: ['g:b', 'g:a'] } }));
|
||||||
expect(key({ ...structure, system: { expanded: ['g:a'] } })).not.toBe(key(structure));
|
expect(key({ ...structure, system: { expanded: ['g:a'] } })).toBe(key(structure));
|
||||||
|
expect(key(at({ view: 'hotspots', spatial: { planar: false, hotspotArea: 'django/db' } }))).not.toBe(key(at({ view: 'hotspots' })));
|
||||||
// Behavior: the start and the destination ("Reach").
|
// Behavior: the start and the destination ("Reach").
|
||||||
expect(key(behavior(7, 'handle'))).not.toBe(key(behavior(8, 'get_autocommit')));
|
expect(key(behavior(7, 'handle'))).not.toBe(key(behavior(8, 'get_autocommit')));
|
||||||
expect(key(behavior(7, 'handle', { targetId: 9, targetName: 'save' }))).not.toBe(key(behavior(7, 'handle')));
|
expect(key(behavior(7, 'handle', { targetId: 9, targetName: 'save' }))).not.toBe(key(behavior(7, 'handle')));
|
||||||
@@ -61,7 +63,7 @@ describe('the Architecture history entry (K27)', () => {
|
|||||||
expect(key(behavior(7, 'handle'))).toBe(key(behavior(7, 'renamed', { from: symbol(3, 'main'), generation: 'g2' })));
|
expect(key(behavior(7, 'handle'))).toBe(key(behavior(7, 'renamed', { from: symbol(3, 'main'), generation: 'g2' })));
|
||||||
// A Service map does not draw Plan or 3D.
|
// A Service map does not draw Plan or 3D.
|
||||||
expect(key(at({ view: 'routes', spatial: { planar: true } }))).toBe(key(at({ view: 'routes' })));
|
expect(key(at({ view: 'routes', spatial: { planar: true } }))).toBe(key(at({ view: 'routes' })));
|
||||||
expect(key(at({ view: 'routes', routes: 'endpoints', spatial: { planar: true } }))).not.toBe(key(at({ view: 'routes', routes: 'endpoints' })));
|
expect(key(at({ view: 'routes', routes: 'endpoints', spatial: { planar: true } }))).toBe(key(at({ view: 'routes', routes: 'endpoints' })));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('names the target of Back and Forward', () => {
|
it('names the target of Back and Forward', () => {
|
||||||
@@ -89,11 +91,11 @@ describe('the Architecture history entry (K27)', () => {
|
|||||||
|
|
||||||
it('holds Plan or 3D in System structure and Behavior, and where a call chain stands (K27)', () => {
|
it('holds Plan or 3D in System structure and Behavior, and where a call chain stands (K27)', () => {
|
||||||
const structure = at({ view: 'structure' });
|
const structure = at({ view: 'structure' });
|
||||||
// Plan or 3D is a step in every view that draws a scene, each with its own camera as before.
|
// Plan or 3D is saved context in every view that draws a scene, each with its own camera as before.
|
||||||
expect(key({ ...structure, system: { expanded: [], structurePlanar: true } })).not.toBe(key(structure));
|
expect(key({ ...structure, system: { expanded: [], structurePlanar: true } })).toBe(key(structure));
|
||||||
expect(key({ ...structure, system: { expanded: [], behaviorPlanar: true } })).toBe(key(structure));
|
expect(key({ ...structure, system: { expanded: [], behaviorPlanar: true } })).toBe(key(structure));
|
||||||
const reach = behavior(1, 'main', { targetId: 3, targetName: 'save' });
|
const reach = behavior(1, 'main', { targetId: 3, targetName: 'save' });
|
||||||
expect(key({ ...reach, system: { ...reach.system, behaviorPlanar: true } })).not.toBe(key(reach));
|
expect(key({ ...reach, system: { ...reach.system, behaviorPlanar: true } })).toBe(key(reach));
|
||||||
expect(key({ ...reach, system: { ...reach.system, structurePlanar: true } })).toBe(key(reach));
|
expect(key({ ...reach, system: { ...reach.system, structurePlanar: true } })).toBe(key(reach));
|
||||||
// Another indexed path to the destination is a step; the operation on it and the page of direct calls are not.
|
// Another indexed path to the destination is a step; the operation on it and the page of direct calls are not.
|
||||||
const second = { ...reach, system: { ...reach.system, behavior: { ...reach.system.behavior!, position: { path: 1 } } } };
|
const second = { ...reach, system: { ...reach.system, behavior: { ...reach.system.behavior!, position: { path: 1 } } } };
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
* The Architecture entry for Back and Forward (hand test K27): which place the
|
* The Architecture entry for Back and Forward (hand test K27): which place the
|
||||||
* workspace shows, without what it loaded for it.
|
* workspace shows, without what it loaded for it.
|
||||||
*
|
*
|
||||||
* One entry is the subtab and what that subtab has opened: in Overview the area
|
* One stop is a meaningful exploration: in Overview the area or file, in
|
||||||
* or file, in Routes the perspective and the route filter (an opened route
|
* Routes an explicitly opened route group, in System structure the focus,
|
||||||
* group is that filter), in System structure the focus and the expanded groups,
|
|
||||||
* in Behavior the start, the destination, where a followed call began and where
|
* in Behavior the start, the destination, where a followed call began and where
|
||||||
* the journey stands; and in every view that draws a scene, Plan or 3D. It holds
|
* the journey stands. Each entry also keeps its subtab, perspective, filter,
|
||||||
* identities, names and small numbers only, never projections, scenes or
|
* expanded groups and Plan or 3D setting so returning restores that context.
|
||||||
|
* Context changes update the current stop. It holds identities, names and small
|
||||||
|
* numbers only, never projections, scenes or
|
||||||
* cameras, so nothing grows with the session. The model behind it is the one
|
* cameras, so nothing grows with the session. The model behind it is the one
|
||||||
* Galaxy uses (src/graph/navigation-history.ts); the concept is in
|
* Galaxy uses (src/graph/navigation-history.ts); the concept is in
|
||||||
* docs/development/pr-2068-galaxy-history.md.
|
* docs/development/pr-2068-galaxy-history.md.
|
||||||
@@ -61,7 +62,7 @@ export interface ArchitectureHistoryEntry {
|
|||||||
view: ArchitectureView;
|
view: ArchitectureView;
|
||||||
spatial: SpatialPlace;
|
spatial: SpatialPlace;
|
||||||
routes: RoutesPerspective;
|
routes: RoutesPerspective;
|
||||||
/** The Routes filter, as far as it is a step: a route group or typing that paused. */
|
/** The Routes filter: typing updates context; explicitly opening a route group creates a stop. */
|
||||||
filter: string;
|
filter: string;
|
||||||
system: SystemPlace;
|
system: SystemPlace;
|
||||||
}
|
}
|
||||||
@@ -106,8 +107,8 @@ const behaviorPath = (entry: ArchitectureHistoryEntry): number => (entry.view ==
|
|||||||
const viewOf = (entry: ArchitectureHistoryEntry): ArchitectureView => (entry.view === 'dependencies' ? 'overview' : entry.view);
|
const viewOf = (entry: ArchitectureHistoryEntry): ArchitectureView => (entry.view === 'dependencies' ? 'overview' : entry.view);
|
||||||
|
|
||||||
export const architectureHistoryOptions: NavigationHistoryOptions<ArchitectureHistoryEntry> = {
|
export const architectureHistoryOptions: NavigationHistoryOptions<ArchitectureHistoryEntry> = {
|
||||||
key: (entry) => JSON.stringify([viewOf(entry), ...location(entry), planar(entry),
|
// View, perspective and filter identify restored locations, but their controls replace the current entry.
|
||||||
entry.view === 'structure' ? [...entry.system.expanded].sort() : null, behaviorPath(entry)]),
|
key: (entry) => JSON.stringify([viewOf(entry), ...location(entry), behaviorPath(entry)]),
|
||||||
recentKey: (entry) => JSON.stringify([viewOf(entry), ...location(entry)]),
|
recentKey: (entry) => JSON.stringify([viewOf(entry), ...location(entry)]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,27 +45,22 @@
|
|||||||
}
|
}
|
||||||
.atlas-arch-tab[aria-pressed="true"] { color: var(--atlas-fg); border-bottom-color: var(--atlas-phosphor); }
|
.atlas-arch-tab[aria-pressed="true"] { color: var(--atlas-fg); border-bottom-color: var(--atlas-phosphor); }
|
||||||
.atlas-arch-tab:hover { color: var(--atlas-fg); background: var(--atlas-panel); }
|
.atlas-arch-tab:hover { color: var(--atlas-fg); background: var(--atlas-panel); }
|
||||||
/*
|
/* Exploration history belongs to the current scope; tabs only choose a lens. */
|
||||||
* K27: one Back, Forward and Recent for the whole workspace and the tabs share a row. The menu sits outside the
|
.atlas-arch-tabrow { display: flex; border-bottom: 1px solid var(--atlas-line); margin-bottom: 16px; }
|
||||||
* scrolling tab strip. Hand test 2026-10-04 (A1): the controls come first and read as in Galaxy, "← Back" and
|
|
||||||
* "Forward →" while the tabs fit, the glyphs alone once they would scroll (`data-fit`, galaxy/toolbar-fit.tsx).
|
|
||||||
*/
|
|
||||||
.atlas-arch-tabrow { display: flex; align-items: center; gap: 14px; border-bottom: 1px solid var(--atlas-line); margin-bottom: 16px; }
|
|
||||||
.atlas-arch-tabrow > .atlas-arch-tabs { flex: 1 1 auto; min-width: 0; border-bottom: 0; margin-bottom: 0; }
|
.atlas-arch-tabrow > .atlas-arch-tabs { flex: 1 1 auto; min-width: 0; border-bottom: 0; margin-bottom: 0; }
|
||||||
.atlas-arch-history { display: flex; align-items: center; gap: 4px; flex: none; position: relative; }
|
.atlas-arch-context { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap; }
|
||||||
.atlas-arch-history > button, .atlas-arch-recent > summary {
|
.atlas-arch-context > span { max-width: min(32rem, 70vw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--atlas-dim); }
|
||||||
border: 1px solid var(--atlas-line); border-radius: 6px; background: var(--atlas-panel); color: var(--atlas-fg);
|
.atlas-arch-history { display: flex; align-items: center; gap: 2px; flex: none; position: relative; }
|
||||||
min-width: 30px; min-height: 30px; padding: 3px 10px; line-height: 1.4; white-space: nowrap;
|
.atlas-architecture .atlas-arch-history > button, .atlas-arch-recent > summary {
|
||||||
|
display: grid; place-items: center; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--atlas-dim);
|
||||||
|
width: 32px; height: 32px; min-width: 32px; min-height: 32px; padding: 6px; line-height: 1;
|
||||||
}
|
}
|
||||||
.atlas-arch-history > button:hover:not(:disabled), .atlas-arch-recent > summary:hover { background: var(--atlas-panel-raised); border-color: var(--atlas-dim); }
|
.atlas-architecture .atlas-arch-history > button:hover:not(:disabled), .atlas-arch-recent > summary:hover { background: var(--atlas-panel-raised); color: var(--atlas-fg); border-color: var(--atlas-line); }
|
||||||
.atlas-arch-history > button:disabled { opacity: 0.4; }
|
.atlas-architecture .atlas-arch-history > button:disabled { opacity: 0.28; cursor: default; }
|
||||||
.atlas-arch-history .atlas-fit-narrow { display: none; }
|
.atlas-arch-history svg { display: block; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
.atlas-arch-history .atlas-fit-narrow::before { content: attr(data-label); }
|
.atlas-arch-history :is(button, summary):focus-visible { outline: 2px solid var(--atlas-phosphor); outline-offset: 2px; }
|
||||||
.atlas-arch-tabrow:is([data-fit=compact], [data-fit=tight], [data-fit=wrap]) .atlas-arch-history .atlas-fit-wide { display: none; }
|
.atlas-arch-recent { position: static; }
|
||||||
.atlas-arch-tabrow:is([data-fit=compact], [data-fit=tight], [data-fit=wrap]) .atlas-arch-history .atlas-fit-narrow { display: inline; }
|
.atlas-arch-recent > summary { cursor: pointer; list-style: none; }
|
||||||
.atlas-arch-tabrow:is([data-fit=compact], [data-fit=tight], [data-fit=wrap]) .atlas-arch-history > button { padding-inline: 8px; }
|
|
||||||
.atlas-arch-recent { position: relative; }
|
|
||||||
.atlas-arch-recent > summary { display: block; cursor: pointer; list-style: none; text-align: center; color: var(--atlas-dim); padding-inline: 8px; }
|
|
||||||
.atlas-arch-recent > summary::-webkit-details-marker { display: none; }
|
.atlas-arch-recent > summary::-webkit-details-marker { display: none; }
|
||||||
.atlas-arch-recent-menu {
|
.atlas-arch-recent-menu {
|
||||||
position: absolute; top: calc(100% + 6px); left: 0; z-index: 30; width: min(24rem, calc(100vw - 3rem)); max-height: 20rem; overflow: auto;
|
position: absolute; top: calc(100% + 6px); left: 0; z-index: 30; width: min(24rem, calc(100vw - 3rem)); max-height: 20rem; overflow: auto;
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
/*
|
/*
|
||||||
* Review of K40: since A1 Back, Forward and the subtabs share one row, but the
|
* Review of K40: the subtab row must stay in the same place across Overview,
|
||||||
* row jumped between Overview, Routes and Hotspots on one side and System
|
* Routes, Hotspots, System structure and Behavior. History now lives beside
|
||||||
* structure and Behavior on the other: those two set the view's frame to 16 px
|
* each view's location, so those local controls may have their own layout.
|
||||||
* instead of 28 and 32 and made the tabs narrower. "← Back" stood at x 32, y 98
|
* No system-view rule may change the workspace frame or the tabs themselves.
|
||||||
* in the first three and at x 16, y 86 in the other two. No rule for the
|
|
||||||
* system views may set the view's frame or the row differently, so the row
|
|
||||||
* stands alike in every subtab.
|
|
||||||
*/
|
*/
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { expect, it } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
const SHEETS = ['architecture.css', 'system-architecture.css', 'behavior-journey.css', 'spatial-architecture.css', 'container-map.css', 'architecture-scene.css', 'source-evidence.css']
|
const SHEETS = ['architecture.css', 'system-architecture.css', 'behavior-journey.css', 'spatial-architecture.css', 'container-map.css', 'architecture-scene.css', 'source-evidence.css']
|
||||||
.map((name) => ({ name, css: readFileSync(new URL(`./${name}`, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') }));
|
.map((name) => ({ name, css: readFileSync(new URL(`./${name}`, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') }));
|
||||||
const ROW = /\.atlas-arch-(tabrow|tabs|tab|history|recent)\b/;
|
const ROW = /\.atlas-arch-(tabrow|tabs|tab)\b/;
|
||||||
const BOX = /^(padding|margin)(-|$)/;
|
const BOX = /^(padding|margin)(-|$)/;
|
||||||
|
|
||||||
it('K40: System structure and Behavior keep the subtab row where the other subtabs have it', () => {
|
it('K40: System structure and Behavior keep the subtab row where the other subtabs have it', () => {
|
||||||
@@ -22,7 +19,7 @@ it('K40: System structure and Behavior keep the subtab row where the other subta
|
|||||||
for (const selector of match[1]!.split(',').map((item) => item.trim())) {
|
for (const selector of match[1]!.split(',').map((item) => item.trim())) {
|
||||||
if (!selector.includes('[data-system-view')) continue;
|
if (!selector.includes('[data-system-view')) continue;
|
||||||
const properties = match[2]!.split(';').map((part) => part.split(':')[0]!.trim()).filter(Boolean);
|
const properties = match[2]!.split(';').map((part) => part.split(':')[0]!.trim()).filter(Boolean);
|
||||||
// The view's own frame, or anything about the row with the history buttons and the subtabs.
|
// Keep the workspace frame and the view-only tab row consistent.
|
||||||
const frame = /^\.atlas-architecture\[data-system-view[^\]]*\]$/.test(selector) && properties.some((property) => BOX.test(property));
|
const frame = /^\.atlas-architecture\[data-system-view[^\]]*\]$/.test(selector) && properties.some((property) => BOX.test(property));
|
||||||
if (frame || ROW.test(selector)) offending.push(`${name}: ${selector} { ${properties.join(', ')} }`);
|
if (frame || ROW.test(selector)) offending.push(`${name}: ${selector} { ${properties.join(', ')} }`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { act } from 'react';
|
||||||
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { initialArchitecturePlace, type ArchitectureHistoryEntry } from './architecture-history';
|
||||||
|
import { useArchitectureHistory, type ArchitectureHistory, type ArchitectureHistoryKeys } from './use-architecture-history';
|
||||||
|
|
||||||
|
let host: HTMLDivElement, root: Root;
|
||||||
|
let current: ArchitectureHistory;
|
||||||
|
let initial: ArchitectureHistoryEntry;
|
||||||
|
const restored = vi.fn();
|
||||||
|
function Probe({ active = true, escapeTaken = false }: Partial<ArchitectureHistoryKeys>) {
|
||||||
|
current = useArchitectureHistory(() => initial, { active, escapeTaken, onRestore: restored });
|
||||||
|
return <input aria-label="Routes filter" readOnly value={current.filter} />;
|
||||||
|
}
|
||||||
|
const render = async (keys: Partial<ArchitectureHistoryKeys> = {}) => {
|
||||||
|
await act(async () => { root.render(<Probe {...keys} />); });
|
||||||
|
};
|
||||||
|
const change = async (update: Parameters<ArchitectureHistory['navigate']>[0], context = false) => {
|
||||||
|
await act(async () => { current.navigate(update, context); });
|
||||||
|
};
|
||||||
|
const go = async (step: -1 | 1) => { await act(async () => { current.go(step); }); };
|
||||||
|
const type = async (filter: string) => { await act(async () => { current.type(filter); }); };
|
||||||
|
const area = (areaPath: string) => change(place => ({ ...place, spatial: { ...place.spatial, areaPath, filePath: undefined } }));
|
||||||
|
const file = (filePath: string) => change(place => ({ ...place, spatial: { ...place.spatial, filePath } }));
|
||||||
|
const keypress = async (key: string, target: EventTarget = window, modifiers: KeyboardEventInit = {}) => {
|
||||||
|
const event = new KeyboardEvent('keydown', { key, altKey: true, bubbles: true, cancelable: true, ...modifiers });
|
||||||
|
await act(async () => { target.dispatchEvent(event); });
|
||||||
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(globalThis as unknown as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
initial = initialArchitecturePlace('overview');
|
||||||
|
restored.mockReset();
|
||||||
|
host = document.createElement('div'); document.body.append(host); root = createRoot(host);
|
||||||
|
});
|
||||||
|
afterEach(async () => { await act(async () => { root.unmount(); }); host.remove(); });
|
||||||
|
|
||||||
|
describe('Architecture exploration history', () => {
|
||||||
|
it('updates tabs, perspective and scene settings in place while keeping Forward available', async () => {
|
||||||
|
await render();
|
||||||
|
await area('src');
|
||||||
|
await file('src/main.ts');
|
||||||
|
await go(-1);
|
||||||
|
const forward = current.forward;
|
||||||
|
|
||||||
|
await change(place => ({ ...place, view: 'routes' }), true);
|
||||||
|
await change(place => ({ ...place, routes: 'endpoints' }), true);
|
||||||
|
await change(place => ({ ...place, spatial: { ...place.spatial, planar: true } }));
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, expanded: ['g:src'], structurePlanar: true, behaviorPlanar: true } }));
|
||||||
|
|
||||||
|
expect(current.history.entries).toHaveLength(3);
|
||||||
|
expect(current.history.index).toBe(1);
|
||||||
|
expect(current.forward).toBe(forward);
|
||||||
|
expect(current.history.entries[1]).toBe(current.place);
|
||||||
|
const context = current.place;
|
||||||
|
await go(1);
|
||||||
|
expect(current.place).toBe(forward);
|
||||||
|
expect(current.place.view).toBe('overview');
|
||||||
|
expect(current.place.spatial.planar).toBe(false);
|
||||||
|
await go(-1);
|
||||||
|
expect(current.place).toBe(context);
|
||||||
|
expect(current.place).toMatchObject({ view: 'routes', routes: 'endpoints', spatial: { planar: true }, system: { expanded: ['g:src'], structurePlanar: true, behaviorPlanar: true } });
|
||||||
|
expect(current.history.entries).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves System structure expansion and Plan as context without inserting stops', async () => {
|
||||||
|
initial = initialArchitecturePlace('structure');
|
||||||
|
await render();
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, focus: { id: 'g:src', label: 'src' } } }));
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, expanded: ['g:src'], structurePlanar: true } }));
|
||||||
|
expect(current.history.entries).toHaveLength(2);
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, focus: { id: 'g:api', label: 'api' }, expanded: [], structurePlanar: false } }));
|
||||||
|
await go(-1);
|
||||||
|
expect(current.place.system).toMatchObject({ focus: { id: 'g:src' }, expanded: ['g:src'], structurePlanar: true });
|
||||||
|
expect(current.history.entries).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops the forward branch only when opening another exploration location', async () => {
|
||||||
|
await render();
|
||||||
|
await area('src');
|
||||||
|
await file('src/old.ts');
|
||||||
|
await go(-1);
|
||||||
|
await change(place => ({ ...place, spatial: { ...place.spatial, planar: true } }));
|
||||||
|
expect(current.forward?.spatial.filePath).toBe('src/old.ts');
|
||||||
|
await file('src/new.ts');
|
||||||
|
expect(current.history.entries.map(entry => entry.spatial.filePath)).toEqual([undefined, undefined, 'src/new.ts']);
|
||||||
|
expect(current.forward).toBeUndefined();
|
||||||
|
await go(-1);
|
||||||
|
expect(current.place.spatial).toMatchObject({ areaPath: 'src', planar: true });
|
||||||
|
await go(1);
|
||||||
|
expect(current.place.spatial.filePath).toBe('src/new.ts');
|
||||||
|
expect(current.history.entries).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores every filter edit immediately without a history stop', async () => {
|
||||||
|
initial = initialArchitecturePlace('routes');
|
||||||
|
await render();
|
||||||
|
for (const filter of ['/', '/a', '/api', '']) {
|
||||||
|
await type(filter);
|
||||||
|
expect(current.filter).toBe(filter);
|
||||||
|
expect(current.place.filter).toBe(filter);
|
||||||
|
expect(current.history.entries).toHaveLength(1);
|
||||||
|
expect(current.history.entries[0].filter).toBe(filter);
|
||||||
|
expect(current.back).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores typed filters through immediate Back and Forward while explicit route groups create stops', async () => {
|
||||||
|
initial = initialArchitecturePlace('routes');
|
||||||
|
await render();
|
||||||
|
await type('/typed-at-root');
|
||||||
|
await change(place => ({ ...place, routes: 'endpoints', filter: '/users' }));
|
||||||
|
await type('/users/active');
|
||||||
|
await change(place => ({ ...place, filter: '/orders' }));
|
||||||
|
expect(current.history.entries.map(entry => entry.filter)).toEqual(['/typed-at-root', '/users/active', '/orders']);
|
||||||
|
|
||||||
|
await type('/orders/recent');
|
||||||
|
await go(-1);
|
||||||
|
expect(current.filter).toBe('/users/active');
|
||||||
|
await type('/users/new');
|
||||||
|
expect(current.forward?.filter).toBe('/orders/recent');
|
||||||
|
await go(1);
|
||||||
|
expect(current.filter).toBe('/orders/recent');
|
||||||
|
await go(-1);
|
||||||
|
expect(current.filter).toBe('/users/new');
|
||||||
|
expect(current.history.entries.map(entry => entry.filter)).toEqual(['/typed-at-root', '/users/new', '/orders/recent']);
|
||||||
|
expect(current.history.index).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces automatic starts and reindex resets, and restores them without pushing again', async () => {
|
||||||
|
initial = initialArchitecturePlace('behavior');
|
||||||
|
await render();
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, behavior: { project: 'sample', id: 1, name: 'main' } } }), true);
|
||||||
|
expect(current.history.entries).toHaveLength(1);
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, behavior: { project: 'sample', id: 2, name: 'save' } } }));
|
||||||
|
await go(-1);
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, behavior: { project: 'sample', id: 3, name: 'reindexed main', generation: 'next' } } }), true);
|
||||||
|
expect(current.history.entries).toHaveLength(2);
|
||||||
|
expect(current.forward?.system.behavior?.id).toBe(2);
|
||||||
|
await go(1);
|
||||||
|
expect(current.place.system.behavior?.id).toBe(2);
|
||||||
|
await go(-1);
|
||||||
|
expect(current.place.system.behavior?.id).toBe(3);
|
||||||
|
expect(current.history.entries).toHaveLength(2);
|
||||||
|
expect(restored).toHaveBeenCalledTimes(3);
|
||||||
|
expect(restored.mock.calls.at(-1)?.map((entry: ArchitectureHistoryEntry) => entry.system.behavior?.id)).toEqual([2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records another Behavior path while saving steps, pages and Plan with that path', async () => {
|
||||||
|
initial = { ...initialArchitecturePlace('behavior'), system: { expanded: [], behavior: { project: 'sample', id: 1, targetId: 2 } } };
|
||||||
|
await render();
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, behaviorPlanar: true, behavior: { ...place.system.behavior!, position: { path: 0, step: 2, page: 1 } } } }));
|
||||||
|
expect(current.history.entries).toHaveLength(1);
|
||||||
|
await change(place => ({ ...place, system: { ...place.system, behavior: { ...place.system.behavior!, position: { path: 1 } } } }));
|
||||||
|
expect(current.history.entries).toHaveLength(2);
|
||||||
|
await go(-1);
|
||||||
|
expect(current.place.system.behavior?.position).toEqual({ path: 0, step: 2, page: 1 });
|
||||||
|
expect(current.place.system.behaviorPlanar).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps Alt+Left and Alt+Right available for meaningful stops and leaves typing alone', async () => {
|
||||||
|
await render();
|
||||||
|
await area('src');
|
||||||
|
await file('src/main.ts');
|
||||||
|
expect((await keypress('ArrowLeft')).defaultPrevented).toBe(true);
|
||||||
|
expect(current.place.spatial.filePath).toBeUndefined();
|
||||||
|
expect(current.history.index).toBe(1);
|
||||||
|
expect((await keypress('ArrowRight', host.querySelector('input')!)).defaultPrevented).toBe(false);
|
||||||
|
expect(current.history.index).toBe(1);
|
||||||
|
expect((await keypress('ArrowRight', window, { ctrlKey: true })).defaultPrevented).toBe(false);
|
||||||
|
expect((await keypress('ArrowRight')).defaultPrevented).toBe(true);
|
||||||
|
expect(current.place.spatial.filePath).toBe('src/main.ts');
|
||||||
|
expect(current.history.entries).toHaveLength(3);
|
||||||
|
expect((await keypress('ArrowRight')).defaultPrevented).toBe(false);
|
||||||
|
|
||||||
|
await render({ active: false });
|
||||||
|
expect((await keypress('ArrowLeft')).defaultPrevented).toBe(false);
|
||||||
|
await render({ escapeTaken: true });
|
||||||
|
expect((await keypress('ArrowLeft')).defaultPrevented).toBe(false);
|
||||||
|
expect(current.history.index).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Back and Forward for the Architecture workspace (hand test K27), wired the
|
* Back and Forward for the Architecture workspace (hand test K27), wired the
|
||||||
* way Galaxy wires K2: the current place is state, every change of its key is
|
* way Galaxy wires K2: the current place is state, meaningful exploration is
|
||||||
* a step in the shared bounded model, and a restored entry pushes nothing.
|
* a step in the shared bounded model, and a restored entry pushes nothing.
|
||||||
*
|
*
|
||||||
* Three refinements Architecture needs and Galaxy does not:
|
* Three refinements Architecture needs and Galaxy does not:
|
||||||
*
|
*
|
||||||
* - Typing in the Routes filter becomes one step once it pauses
|
* - Context controls (subtab, perspective and typed Routes filter) replace
|
||||||
* (`FILTER_STEP_MS`), not one step per key. A navigation, Back, Forward or
|
* the current step immediately and preserve its forward branch.
|
||||||
* a recent jump while typing first records what the field showed, so no
|
|
||||||
* typed text is lost.
|
|
||||||
* - A change the page makes itself (the suggested Behavior start, a reset
|
* - A change the page makes itself (the suggested Behavior start, a reset
|
||||||
* after reindexing) replaces the current step instead of adding one, so Back
|
* after reindexing) also replaces the current step, so Back never lands on
|
||||||
* never lands on a state the page would immediately fill in again.
|
* a state the page would immediately fill in again.
|
||||||
* - Alt+Left and Alt+Right act only while this workspace is the active one,
|
* - Alt+Left and Alt+Right act only while this workspace is the active one,
|
||||||
* never while typing and never while another surface takes the keys.
|
* never while typing and never while another surface takes the keys.
|
||||||
*
|
*
|
||||||
@@ -25,19 +23,16 @@ import {
|
|||||||
import { isTypingTarget } from '../app/keyboard';
|
import { isTypingTarget } from '../app/keyboard';
|
||||||
import { architectureHistoryOptions as options, type ArchitectureHistoryEntry } from './architecture-history';
|
import { architectureHistoryOptions as options, type ArchitectureHistoryEntry } from './architecture-history';
|
||||||
|
|
||||||
/** Typing in the Routes filter becomes a step once it pauses this long. */
|
|
||||||
export const FILTER_STEP_MS = 600;
|
|
||||||
|
|
||||||
type Update = (current: ArchitectureHistoryEntry) => ArchitectureHistoryEntry;
|
type Update = (current: ArchitectureHistoryEntry) => ArchitectureHistoryEntry;
|
||||||
|
|
||||||
export interface ArchitectureHistory {
|
export interface ArchitectureHistory {
|
||||||
place: ArchitectureHistoryEntry;
|
place: ArchitectureHistoryEntry;
|
||||||
/** What the filter field shows, including typing that is not a step yet. */
|
/** The Routes filter saved with the current place, including the latest typing. */
|
||||||
filter: string;
|
filter: string;
|
||||||
history: NavigationHistory<ArchitectureHistoryEntry>;
|
history: NavigationHistory<ArchitectureHistoryEntry>;
|
||||||
back?: ArchitectureHistoryEntry;
|
back?: ArchitectureHistoryEntry;
|
||||||
forward?: ArchitectureHistoryEntry;
|
forward?: ArchitectureHistoryEntry;
|
||||||
/** A navigation; `automatic` marks a change the page made itself. Stable across renders. */
|
/** A navigation; `automatic` replaces the current stop for context controls and page-made changes. Stable across renders. */
|
||||||
navigate: (update: Update, automatic?: boolean) => void;
|
navigate: (update: Update, automatic?: boolean) => void;
|
||||||
type: (filter: string) => void;
|
type: (filter: string) => void;
|
||||||
go: (step: -1 | 1) => void;
|
go: (step: -1 | 1) => void;
|
||||||
@@ -54,11 +49,9 @@ export interface ArchitectureHistoryKeys {
|
|||||||
|
|
||||||
export function useArchitectureHistory(initial: () => ArchitectureHistoryEntry, { active, escapeTaken, onRestore }: ArchitectureHistoryKeys): ArchitectureHistory {
|
export function useArchitectureHistory(initial: () => ArchitectureHistoryEntry, { active, escapeTaken, onRestore }: ArchitectureHistoryKeys): ArchitectureHistory {
|
||||||
const [place, setPlace] = useState(initial);
|
const [place, setPlace] = useState(initial);
|
||||||
const [draft, setDraft] = useState<string>();
|
|
||||||
const [history, setHistory] = useState(() => emptyNavigationHistory<ArchitectureHistoryEntry>());
|
const [history, setHistory] = useState(() => emptyNavigationHistory<ArchitectureHistoryEntry>());
|
||||||
const latest = useRef(place);
|
const latest = useRef(place);
|
||||||
latest.current = place;
|
latest.current = place;
|
||||||
const typed = useRef<string | undefined>(undefined);
|
|
||||||
const restoringKey = useRef<string | undefined>(undefined);
|
const restoringKey = useRef<string | undefined>(undefined);
|
||||||
/** An automatic change: the key it was computed from and the key it leads to. */
|
/** An automatic change: the key it was computed from and the key it leads to. */
|
||||||
const automaticStep = useRef<{ base: string; next: string } | undefined>(undefined);
|
const automaticStep = useRef<{ base: string; next: string } | undefined>(undefined);
|
||||||
@@ -84,69 +77,32 @@ export function useArchitectureHistory(initial: () => ArchitectureHistoryEntry,
|
|||||||
// The current step keeps the newest details of its place (a name known only later); after the push above, so a new key is never a refresh.
|
// The current step keeps the newest details of its place (a name known only later); after the push above, so a new key is never a refresh.
|
||||||
useEffect(() => { setHistory((current) => refreshNavigation(current, place, options)); }, [place]);
|
useEffect(() => { setHistory((current) => refreshNavigation(current, place, options)); }, [place]);
|
||||||
|
|
||||||
const clearDraft = useCallback(() => { typed.current = undefined; setDraft(undefined); }, []);
|
|
||||||
|
|
||||||
const navigate = useCallback((update: Update, automatic = false) => {
|
const navigate = useCallback((update: Update, automatic = false) => {
|
||||||
const text = automatic ? undefined : typed.current;
|
|
||||||
if (text !== undefined) {
|
|
||||||
clearDraft();
|
|
||||||
// What the field showed is a step of its own, before the navigation that leaves it.
|
|
||||||
setHistory((current) => pushNavigation(current, { ...latest.current, filter: text }, options));
|
|
||||||
}
|
|
||||||
setPlace((current) => {
|
setPlace((current) => {
|
||||||
const result = update(text === undefined ? current : { ...current, filter: text });
|
const result = update(current);
|
||||||
if (automatic) {
|
if (automatic) {
|
||||||
const before = options.key(current), after = options.key(result);
|
const before = options.key(current), after = options.key(result);
|
||||||
if (after !== before) automaticStep.current = { base: before, next: after };
|
if (after !== before) automaticStep.current = { base: before, next: after };
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}, [clearDraft]);
|
}, []);
|
||||||
|
|
||||||
const type = useCallback((value: string) => { typed.current = value; setDraft(value); }, []);
|
const type = useCallback((filter: string) => { navigate((current) => ({ ...current, filter }), true); }, [navigate]);
|
||||||
useEffect(() => {
|
|
||||||
if (draft === undefined) return;
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
clearDraft();
|
|
||||||
setPlace((current) => ({ ...current, filter: draft }));
|
|
||||||
}, FILTER_STEP_MS);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [draft, clearDraft]);
|
|
||||||
|
|
||||||
const apply = useCallback((entry: ArchitectureHistoryEntry) => {
|
const apply = useCallback((entry: ArchitectureHistoryEntry) => {
|
||||||
restore.current?.(latest.current, entry);
|
restore.current?.(latest.current, entry);
|
||||||
clearDraft();
|
|
||||||
automaticStep.current = undefined;
|
automaticStep.current = undefined;
|
||||||
setPlace(entry);
|
setPlace(entry);
|
||||||
}, [clearDraft]);
|
}, []);
|
||||||
/** Typing that has not paused yet, as the step it is about to become; undefined while nothing is typed. */
|
|
||||||
const typedStep = useCallback((): ArchitectureHistoryEntry | undefined =>
|
|
||||||
(typed.current === undefined ? undefined : { ...latest.current, filter: typed.current }), []);
|
|
||||||
/** The history with that typing as its newest step, the way the pause would record it. */
|
|
||||||
const withTyping = useCallback((): NavigationHistory<ArchitectureHistoryEntry> => {
|
|
||||||
const entry = typedStep();
|
|
||||||
return entry === undefined ? history : pushNavigation(history, entry, options);
|
|
||||||
}, [history, typedStep]);
|
|
||||||
const go = useCallback((step: -1 | 1) => {
|
const go = useCallback((step: -1 | 1) => {
|
||||||
const typedEntry = typedStep();
|
const entry = peekNavigation(history, step);
|
||||||
const base = withTyping();
|
if (entry === undefined) return;
|
||||||
const entry = peekNavigation(base, step);
|
|
||||||
if (entry === undefined) {
|
|
||||||
// Forward while typing: the text is a new step and drops the forward branch, as after the pause.
|
|
||||||
if (typedEntry === undefined) return;
|
|
||||||
clearDraft();
|
|
||||||
setHistory(base);
|
|
||||||
setPlace(typedEntry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
restoringKey.current = options.key(entry);
|
restoringKey.current = options.key(entry);
|
||||||
setHistory(moveNavigation(base, step, options));
|
setHistory(moveNavigation(history, step, options));
|
||||||
apply(entry);
|
apply(entry);
|
||||||
}, [typedStep, withTyping, apply, clearDraft]);
|
}, [history, apply]);
|
||||||
const jump = useCallback((entry: ArchitectureHistoryEntry) => {
|
const jump = useCallback((entry: ArchitectureHistoryEntry) => { apply(entry); }, [apply]);
|
||||||
setHistory(withTyping());
|
|
||||||
apply(entry);
|
|
||||||
}, [withTyping, apply]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active || escapeTaken) return;
|
if (!active || escapeTaken) return;
|
||||||
@@ -155,13 +111,13 @@ export function useArchitectureHistory(initial: () => ArchitectureHistoryEntry,
|
|||||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||||
if (isTypingTarget(event.target instanceof Element ? event.target : null)) return;
|
if (isTypingTarget(event.target instanceof Element ? event.target : null)) return;
|
||||||
const step = event.key === 'ArrowLeft' ? -1 : 1;
|
const step = event.key === 'ArrowLeft' ? -1 : 1;
|
||||||
if (peekNavigation(withTyping(), step) === undefined) return;
|
if (peekNavigation(history, step) === undefined) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
go(step);
|
go(step);
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => window.removeEventListener('keydown', onKey);
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
}, [active, escapeTaken, withTyping, go]);
|
}, [active, escapeTaken, history, go]);
|
||||||
|
|
||||||
return { place, filter: draft ?? place.filter, history, back: peekNavigation(history, -1), forward: peekNavigation(history, 1), navigate, type, go, jump };
|
return { place, filter: place.filter, history, back: peekNavigation(history, -1), forward: peekNavigation(history, 1), navigate, type, go, jump };
|
||||||
}
|
}
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
import { SIDECAR_PORT } from '../llm/sidecar';
|
import { SIDECAR_PORT } from '../llm/sidecar';
|
||||||
|
|
||||||
/** The word over the panel. */
|
/** The word over the panel. */
|
||||||
export const CHAT_TITLE = 'ATLAS_CHAT';
|
export const CHAT_TITLE = 'CBM LENS CHAT';
|
||||||
|
|
||||||
/** What the command line hint says when a question would be sent. */
|
/** What the command line hint says when a question would be sent. */
|
||||||
export const CHAT_HINT_READY = 'enter asks the atlas';
|
export const CHAT_HINT_READY = 'enter asks CBM Lens';
|
||||||
|
|
||||||
/** What it says when the line looks like a question and the model is off. */
|
/** What it says when the line looks like a question and the model is off. */
|
||||||
export const CHAT_HINT_OFF = 'enter asks the atlas: the local model is off';
|
export const CHAT_HINT_OFF = 'enter asks CBM Lens: the local model is off';
|
||||||
|
|
||||||
/** The sentence when the model is off and a question was typed. */
|
/** The sentence when the model is off and a question was typed. */
|
||||||
export const CHAT_OFF_MESSAGE =
|
export const CHAT_OFF_MESSAGE =
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ export const messages = {
|
|||||||
},
|
},
|
||||||
/** Die Kopfzeile und die Flaechen, die zu keinem Panel gehoeren. */
|
/** Die Kopfzeile und die Flaechen, die zu keinem Panel gehoeren. */
|
||||||
app: {
|
app: {
|
||||||
brand: 'CODEATLAS',
|
brand: 'CBM LENS',
|
||||||
|
releaseLabel: 'Beta',
|
||||||
/** Steht in der Tab-Leiste und im Breadcrumb, wenn nichts offen ist. */
|
/** Steht in der Tab-Leiste und im Breadcrumb, wenn nichts offen ist. */
|
||||||
noFileOpen: 'no file open',
|
noFileOpen: 'no file open',
|
||||||
/** Der Name, den eine Oberflaeche ohne Projekt traegt. */
|
/** Der Name, den eine Oberflaeche ohne Projekt traegt. */
|
||||||
@@ -147,14 +148,14 @@ export const messages = {
|
|||||||
* Repository.
|
* Repository.
|
||||||
*/
|
*/
|
||||||
help: {
|
help: {
|
||||||
title: 'CODEATLAS HELP',
|
title: 'CBM LENS HELP',
|
||||||
subtitle: 'a reading surface over an indexed repository',
|
subtitle: 'a reading surface over an indexed repository',
|
||||||
close: '[esc] close',
|
close: '[esc] close',
|
||||||
closeLabel: CLOSE,
|
closeLabel: CLOSE,
|
||||||
|
|
||||||
whatTitle: 'What this is',
|
whatTitle: 'What this is',
|
||||||
what: [
|
what: [
|
||||||
'CodeAtlasWeb reads a repository that the analysis server has indexed and explains it: '
|
'CBM Lens reads a repository that the analysis server has indexed and explains it: '
|
||||||
+ 'the tree and the source, a twin of the symbol under the caret, a walk along the calls, '
|
+ 'the tree and the source, a twin of the symbol under the caret, a walk along the calls, '
|
||||||
+ 'the scope of a change, and the answers of a local model.',
|
+ 'the scope of a change, and the answers of a local model.',
|
||||||
'Everything on this screen comes from that index or from a file the server handed over, '
|
'Everything on this screen comes from that index or from a file the server handed over, '
|
||||||
@@ -400,7 +401,7 @@ export const messages = {
|
|||||||
* Beispiele, wie man den Chat nutzt, sonst weiss niemand, wie man es
|
* Beispiele, wie man den Chat nutzt, sonst weiss niemand, wie man es
|
||||||
* nutzt."
|
* nutzt."
|
||||||
*/
|
*/
|
||||||
placeholder: 'type a command or ask the atlas...',
|
placeholder: 'type a command or ask CBM Lens...',
|
||||||
examplesLabel: 'what you can type here',
|
examplesLabel: 'what you can type here',
|
||||||
hintIdle: 'type 2 letters to search by meaning',
|
hintIdle: 'type 2 letters to search by meaning',
|
||||||
hintOpen: 'up/down picks, enter opens, esc closes',
|
hintOpen: 'up/down picks, enter opens, esc closes',
|
||||||
@@ -422,7 +423,7 @@ export const messages = {
|
|||||||
* Schweigen. Jetzt steht der Satz an der Stelle, an der die Antwort
|
* Schweigen. Jetzt steht der Satz an der Stelle, an der die Antwort
|
||||||
* erwartet wird.
|
* erwartet wird.
|
||||||
*/
|
*/
|
||||||
oneMoreLetter: 'one more letter, then the atlas searches by meaning',
|
oneMoreLetter: 'one more letter, then CBM Lens searches by meaning',
|
||||||
/*
|
/*
|
||||||
* Die Sofort-Vorschlaege, und dass sie vorlaeufig sind.
|
* Die Sofort-Vorschlaege, und dass sie vorlaeufig sind.
|
||||||
*
|
*
|
||||||
@@ -648,7 +649,7 @@ export const messages = {
|
|||||||
|
|
||||||
/** Der Rahmen des Antwort-Panels. Seine Saetze stehen in chat-strings.ts. */
|
/** Der Rahmen des Antwort-Panels. Seine Saetze stehen in chat-strings.ts. */
|
||||||
chat: {
|
chat: {
|
||||||
panelLabel: 'atlas chat',
|
panelLabel: 'CBM Lens chat',
|
||||||
depthGroupLabel: 'neighbourhood depth',
|
depthGroupLabel: 'neighbourhood depth',
|
||||||
/*
|
/*
|
||||||
* `clear` bleibt der einzige Knopf, der loescht, und sagt es.
|
* `clear` bleibt der einzige Knopf, der loescht, und sagt es.
|
||||||
@@ -706,7 +707,7 @@ export const messages = {
|
|||||||
* das wurde am 2026-08-29 gemessen, und der Satz sagt es so.
|
* das wurde am 2026-08-29 gemessen, und der Satz sagt es so.
|
||||||
*/
|
*/
|
||||||
settings: {
|
settings: {
|
||||||
title: 'CODEATLAS SETTINGS',
|
title: 'CBM LENS SETTINGS',
|
||||||
subtitle: 'which model answers, and everything that costs computing time',
|
subtitle: 'which model answers, and everything that costs computing time',
|
||||||
panelLabel: 'settings',
|
panelLabel: 'settings',
|
||||||
close: '[esc] close',
|
close: '[esc] close',
|
||||||
@@ -974,7 +975,7 @@ export const messages = {
|
|||||||
* not about one subject area.
|
* not about one subject area.
|
||||||
*/
|
*/
|
||||||
projects: {
|
projects: {
|
||||||
title: 'CODEATLAS PROJECTS',
|
title: 'CBM LENS PROJECTS',
|
||||||
subtitle: 'what this server has indexed, and how to give it more',
|
subtitle: 'what this server has indexed, and how to give it more',
|
||||||
panelLabel: 'projects',
|
panelLabel: 'projects',
|
||||||
close: '[esc] close',
|
close: '[esc] close',
|
||||||
@@ -1100,7 +1101,7 @@ export const messages = {
|
|||||||
/** Der Rahmen des Twins. Seine Fakten-Saetze stehen in twin/strings.ts. */
|
/** Der Rahmen des Twins. Seine Fakten-Saetze stehen in twin/strings.ts. */
|
||||||
twin: {
|
twin: {
|
||||||
/** Das Etikett des Leseprofils, mit dem dieses Produkt startet. */
|
/** Das Etikett des Leseprofils, mit dem dieses Produkt startet. */
|
||||||
profileLabel: 'CodeAtlasWeb Twin',
|
profileLabel: 'CBM Lens Twin',
|
||||||
/** Der Kopf des Panels, solange kein Symbol vor dem Leser steht. */
|
/** Der Kopf des Panels, solange kein Symbol vor dem Leser steht. */
|
||||||
noSymbol: 'no symbol',
|
noSymbol: 'no symbol',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function impactRiskTooltip(level: 'low' | 'medium' | 'high', reasons: str
|
|||||||
/** How a route was tied to the change. Two different strengths of claim. */
|
/** How a route was tied to the change. Two different strengths of claim. */
|
||||||
export const IMPACT_ENDPOINT_VIA_HANDLER = 'The registration names a handler this change reaches.';
|
export const IMPACT_ENDPOINT_VIA_HANDLER = 'The registration names a handler this change reaches.';
|
||||||
export const IMPACT_ENDPOINT_VIA_FILE =
|
export const IMPACT_ENDPOINT_VIA_FILE =
|
||||||
'The registration is written in a file this change reaches. CodeAtlas cannot name the handler here, so this is the file and not the function.';
|
'The registration is written in a file this change reaches. CBM Lens cannot name the handler here, so this is the file and not the function.';
|
||||||
|
|
||||||
// Die Erzaehlung, ein Satz je Lesung -----------------------------------------
|
// Die Erzaehlung, ein Satz je Lesung -----------------------------------------
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export function impactDownstreamSentence(count: number, distance: number): strin
|
|||||||
|
|
||||||
export function impactEndpointSentence(paths: string[]): string {
|
export function impactEndpointSentence(paths: string[]): string {
|
||||||
if (paths.length === 0) {
|
if (paths.length === 0) {
|
||||||
return 'No HTTP endpoint CodeAtlas recovered is registered in a file this change reaches.';
|
return 'No HTTP endpoint CBM Lens recovered is registered in a file this change reaches.';
|
||||||
}
|
}
|
||||||
return `${countOf(paths.length, 'endpoint is', 'endpoints are')} registered in a file `
|
return `${countOf(paths.length, 'endpoint is', 'endpoints are')} registered in a file `
|
||||||
+ `this change reaches: ${paths.join(', ')}.`;
|
+ `this change reaches: ${paths.join(', ')}.`;
|
||||||
@@ -152,7 +152,7 @@ export const IMPACT_EVIDENCE_TITLE = 'What this rests on';
|
|||||||
|
|
||||||
export const IMPACT_EVIDENCE_SOURCES: Readonly<Record<'detect_changes' | 'architecture' | 'facts', string>> = {
|
export const IMPACT_EVIDENCE_SOURCES: Readonly<Record<'detect_changes' | 'architecture' | 'facts', string>> = {
|
||||||
detect_changes: 'the change set the analysis reported, plus the calls into it',
|
detect_changes: 'the change set the analysis reported, plus the calls into it',
|
||||||
architecture: 'the whole-project summary, including the routes CodeAtlas recovered',
|
architecture: 'the whole-project summary, including the routes CBM Lens recovered',
|
||||||
facts: 'the per-symbol readings: complexity, and callers flagged as test code',
|
facts: 'the per-symbol readings: complexity, and callers flagged as test code',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ export const IMPACT_MENU_LABEL = '[c]hange scope';
|
|||||||
|
|
||||||
/** Der Satz darunter: was gelesen wird und was das Wort am Ende bedeutet. */
|
/** Der Satz darunter: was gelesen wird und was das Wort am Ende bedeutet. */
|
||||||
export const IMPACT_SUBLINE =
|
export const IMPACT_SUBLINE =
|
||||||
'Read from the change set the analysis reports, the calls into it, and the routes CodeAtlas recovered. '
|
'Read from the change set the analysis reports, the calls into it, and the routes CBM Lens recovered. '
|
||||||
+ 'The word on the right is a rule applied to those readings, never a prediction.';
|
+ 'The word on the right is a rule applied to those readings, never a prediction.';
|
||||||
|
|
||||||
export const IMPACT_MODE_LABEL = 'Compare against';
|
export const IMPACT_MODE_LABEL = 'Compare against';
|
||||||
@@ -244,7 +244,7 @@ export const IMPACT_DIRECT_EMPTY =
|
|||||||
export const IMPACT_DOWNSTREAM_EMPTY =
|
export const IMPACT_DOWNSTREAM_EMPTY =
|
||||||
'Nothing in the indexed workspace calls the changed symbols.';
|
'Nothing in the indexed workspace calls the changed symbols.';
|
||||||
export const IMPACT_ENDPOINTS_EMPTY =
|
export const IMPACT_ENDPOINTS_EMPTY =
|
||||||
'No route CodeAtlas recovered is registered in a file this change reaches.';
|
'No route CBM Lens recovered is registered in a file this change reaches.';
|
||||||
export const IMPACT_TESTS_EMPTY = 'No affected symbol could be checked for test callers.';
|
export const IMPACT_TESTS_EMPTY = 'No affected symbol could be checked for test callers.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ describe('the sentence over the block', () => {
|
|||||||
it('leads with what cannot be checked when nothing is unused', () => {
|
it('leads with what cannot be checked when nothing is unused', () => {
|
||||||
const lead = blockLeadOf(document, importsOf([USED, UNCHECKABLE]), 'getOrder');
|
const lead = blockLeadOf(document, importsOf([USED, UNCHECKABLE]), 'getOrder');
|
||||||
expect(lead.kind).toBe('unchecked-imports');
|
expect(lead.kind).toBe('unchecked-imports');
|
||||||
expect(lead.text).toContain('CodeAtlas cannot check either way');
|
expect(lead.text).toContain('CBM Lens cannot check either way');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leads with what lies behind the calls when the imports check out', () => {
|
it('leads with what lies behind the calls when the imports check out', () => {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ describe('where a reader is sent, and what the group says about itself', () => {
|
|||||||
expect(built.used).toBe(5);
|
expect(built.used).toBe(5);
|
||||||
expect(built.unused).toBe(2);
|
expect(built.unused).toBe(2);
|
||||||
expect(built.unknown).toBe(0);
|
expect(built.unknown).toBe(0);
|
||||||
expect(built.tally).toBe('5 used in this file, 2 not used in it, 0 that CodeAtlas cannot check.');
|
expect(built.tally).toBe('5 used in this file, 2 not used in it, 0 that CBM Lens cannot check.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('says how many entries the cap left out rather than trailing off', () => {
|
it('says how many entries the cap left out rather than trailing off', () => {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function pseudocodeCappedNote(hidden: number): string {
|
|||||||
* the one way this feature could mislead without saying anything untrue.
|
* the one way this feature could mislead without saying anything untrue.
|
||||||
*/
|
*/
|
||||||
export const PSEUDOCODE_PARTIAL_SCOPE =
|
export const PSEUDOCODE_PARTIAL_SCOPE =
|
||||||
'CodeAtlas sampled this range rather than reading every line of it, so a very short member may be missing.';
|
'CBM Lens sampled this range rather than reading every line of it, so a very short member may be missing.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The footer, and the sentence that keeps the whole block honest.
|
* The footer, and the sentence that keeps the whole block honest.
|
||||||
@@ -169,7 +169,7 @@ export function pseudocodeLeadUnused(symbol: string, unused: number, total: numb
|
|||||||
|
|
||||||
export function pseudocodeLeadUnchecked(symbol: string, unknown: number): string {
|
export function pseudocodeLeadUnchecked(symbol: string, unknown: number): string {
|
||||||
return `You cannot see this in the code: every name this file pulls in is one the index links to `
|
return `You cannot see this in the code: every name this file pulls in is one the index links to `
|
||||||
+ `${symbol}, except ${countOf(unknown, 'one', 'ones')} CodeAtlas cannot check either way.`;
|
+ `${symbol}, except ${countOf(unknown, 'one', 'ones')} CBM Lens cannot check either way.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pseudocodeLeadBehind(steps: number): string {
|
export function pseudocodeLeadBehind(steps: number): string {
|
||||||
@@ -179,7 +179,7 @@ export function pseudocodeLeadBehind(steps: number): string {
|
|||||||
|
|
||||||
export function pseudocodeLeadNone(symbol: string, total: number): string {
|
export function pseudocodeLeadNone(symbol: string, total: number): string {
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return 'Nothing stands above the steps this time: CodeAtlas found no import in this file, and the '
|
return 'Nothing stands above the steps this time: CBM Lens found no import in this file, and the '
|
||||||
+ 'index records nothing behind the steps below.';
|
+ 'index records nothing behind the steps below.';
|
||||||
}
|
}
|
||||||
return `Nothing stands above the steps this time: every name this file pulls in is one the index links `
|
return `Nothing stands above the steps this time: every name this file pulls in is one the index links `
|
||||||
@@ -293,7 +293,7 @@ export function importsFindingHeading(unused: number, unknown: number, total: nu
|
|||||||
+ 'as far as the index shows';
|
+ 'as far as the index shows';
|
||||||
}
|
}
|
||||||
if (unknown > 0) {
|
if (unknown > 0) {
|
||||||
return `${IMPORTS_GROUP_HEADING}: ${countOf(unknown, 'name', 'names')} CodeAtlas cannot check `
|
return `${IMPORTS_GROUP_HEADING}: ${countOf(unknown, 'name', 'names')} CBM Lens cannot check `
|
||||||
+ 'against this file';
|
+ 'against this file';
|
||||||
}
|
}
|
||||||
return `${IMPORTS_GROUP_HEADING}: every name here is linked to a symbol in this file`;
|
return `${IMPORTS_GROUP_HEADING}: every name here is linked to a symbol in this file`;
|
||||||
@@ -318,7 +318,7 @@ export function importsUnusedLine(name: string, module: string): string {
|
|||||||
|
|
||||||
/** Said on an entry that could not be checked at all. */
|
/** Said on an entry that could not be checked at all. */
|
||||||
export function importsUnknownLine(name: string, module: string): string {
|
export function importsUnknownLine(name: string, module: string): string {
|
||||||
return `${name} from ${module}: CodeAtlas cannot tell whether this file uses it`;
|
return `${name} from ${module}: CBM Lens cannot tell whether this file uses it`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Said on `import * as db from ...` and its equivalents in the other languages. */
|
/** Said on `import * as db from ...` and its equivalents in the other languages. */
|
||||||
@@ -344,7 +344,7 @@ export const IMPORTS_NOTE_UNUSED =
|
|||||||
'The index answered every checked symbol in this file, and none of their recorded facts mentions this name.';
|
'The index answered every checked symbol in this file, and none of their recorded facts mentions this name.';
|
||||||
|
|
||||||
export const IMPORTS_NOTE_UNKNOWN =
|
export const IMPORTS_NOTE_UNKNOWN =
|
||||||
'CodeAtlas does not have a complete answered reading for every callable in this file, so it will not claim the import is unused.';
|
'CBM Lens does not have a complete answered reading for every callable in this file, so it will not claim the import is unused.';
|
||||||
|
|
||||||
export const IMPORTS_NOTE_NAMESPACE =
|
export const IMPORTS_NOTE_NAMESPACE =
|
||||||
'A namespace import binds every export of the module under one name, and the index records a call through it under the member\'s name. So nothing here can be matched against the binding.';
|
'A namespace import binds every export of the module under one name, and the index records a call through it under the member\'s name. So nothing here can be matched against the binding.';
|
||||||
@@ -353,7 +353,7 @@ export const IMPORTS_NOTE_SIDE_EFFECT =
|
|||||||
'This statement binds no name, so there is nothing for the index to match it against. What the module does when it loads is not something the analysis records.';
|
'This statement binds no name, so there is nothing for the index to match it against. What the module does when it loads is not something the analysis records.';
|
||||||
|
|
||||||
export const IMPORTS_NOTE_INDEX_ONLY =
|
export const IMPORTS_NOTE_INDEX_ONLY =
|
||||||
'This came from the dependency the index recorded rather than from the file\'s text, so CodeAtlas has the imported file and not the imported name.';
|
'This came from the dependency the index recorded rather than from the file\'s text, so CBM Lens has the imported file and not the imported name.';
|
||||||
|
|
||||||
/** Said at the end of a group the display cap cut short. */
|
/** Said at the end of a group the display cap cut short. */
|
||||||
export function importsCappedNote(hidden: number, shown: number): string {
|
export function importsCappedNote(hidden: number, shown: number): string {
|
||||||
@@ -362,11 +362,11 @@ export function importsCappedNote(hidden: number, shown: number): string {
|
|||||||
|
|
||||||
/** Said when the file's own text could not be read at all. */
|
/** Said when the file's own text could not be read at all. */
|
||||||
export const IMPORTS_SOURCE_UNREAD =
|
export const IMPORTS_SOURCE_UNREAD =
|
||||||
'CodeAtlas could not read this file\'s text, so what follows is the dependencies the index recorded and not the import statements themselves.';
|
'CBM Lens could not read this file\'s text, so what follows is the dependencies the index recorded and not the import statements themselves.';
|
||||||
|
|
||||||
/** Said when the file pulls in nothing at all. */
|
/** Said when the file pulls in nothing at all. */
|
||||||
export const IMPORTS_EMPTY =
|
export const IMPORTS_EMPTY =
|
||||||
'The index records no dependency for this file and CodeAtlas found no import statement in it.';
|
'The index records no dependency for this file and CBM Lens found no import statement in it.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The standing sentence under the group, in both surfaces.
|
* The standing sentence under the group, in both surfaces.
|
||||||
@@ -377,7 +377,7 @@ export const IMPORTS_EMPTY =
|
|||||||
* the judgement about use comes from every callable declaration the file read.
|
* the judgement about use comes from every callable declaration the file read.
|
||||||
*/
|
*/
|
||||||
export const IMPORTS_SOURCE_NOTE =
|
export const IMPORTS_SOURCE_NOTE =
|
||||||
'The dependency comes from the index; the statement, its line and the name it binds are CodeAtlas reading '
|
'The dependency comes from the index; the statement, its line and the name it binds are CBM Lens reading '
|
||||||
+ 'this file. Use is checked against the recorded facts of every callable symbol in the file.';
|
+ 'this file. Use is checked against the recorded facts of every callable symbol in the file.';
|
||||||
|
|
||||||
/** The three markers. Short enough to sit beside a name, and never the only carrier of meaning. */
|
/** The three markers. Short enough to sit beside a name, and never the only carrier of meaning. */
|
||||||
@@ -395,7 +395,7 @@ export const IMPORTS_SECTION_NOTE = IMPORTS_SOURCE_NOTE;
|
|||||||
|
|
||||||
/** How the group's own tally reads, under the entries. */
|
/** How the group's own tally reads, under the entries. */
|
||||||
export function importsTally(used: number, unused: number, unknown: number): string {
|
export function importsTally(used: number, unused: number, unknown: number): string {
|
||||||
return `${used} used in this file, ${unused} not used in it, ${unknown} that CodeAtlas cannot check.`;
|
return `${used} used in this file, ${unused} not used in it, ${unknown} that CBM Lens cannot check.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -581,7 +581,7 @@ export function explainerWalkBound(depth: number, cap: number): string {
|
|||||||
* entworfen, muss sagen, dass sie aus einem Index gerechnet ist.
|
* entworfen, muss sagen, dass sie aus einem Index gerechnet ist.
|
||||||
*/
|
*/
|
||||||
export const EXPLAINER_GENERATED_NOTE =
|
export const EXPLAINER_GENERATED_NOTE =
|
||||||
'Nobody drew this picture. CodeAtlas laid it out from what the index recorded for this walk, so it can '
|
'Nobody drew this picture. CBM Lens laid it out from what the index recorded for this walk, so it can '
|
||||||
+ 'be incomplete about the code and exact about what was reported.';
|
+ 'be incomplete about the code and exact about what was reported.';
|
||||||
|
|
||||||
/** Die Notiz im Bild, wenn eine Kante in eine schon besuchte Kette zurueckfuehrt. */
|
/** Die Notiz im Bild, wenn eine Kante in eine schon besuchte Kette zurueckfuehrt. */
|
||||||
@@ -636,4 +636,4 @@ export const FLOW_LOADING = 'Reading what this symbol reaches.';
|
|||||||
* hier nichts zu zeigen, und keiner davon heisst "dieses Symbol ruft nichts".
|
* hier nichts zu zeigen, und keiner davon heisst "dieses Symbol ruft nichts".
|
||||||
*/
|
*/
|
||||||
export const FLOW_UNAVAILABLE =
|
export const FLOW_UNAVAILABLE =
|
||||||
'CodeAtlas has no walk for this symbol right now. Place the caret in an indexed function, or reindex the workspace.';
|
'CBM Lens has no walk for this symbol right now. Place the caret in an indexed function, or reindex the workspace.';
|
||||||
@@ -93,8 +93,8 @@ describe('commandPlaceholderFor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('faellt auf die Gattung zurueck, statt einen Namen zu erfinden', () => {
|
it('faellt auf die Gattung zurueck, statt einen Namen zu erfinden', () => {
|
||||||
expect(commandPlaceholderFor(undefined, 'type a command or ask the atlas...'))
|
expect(commandPlaceholderFor(undefined, 'type a command or ask CBM Lens...'))
|
||||||
.toBe('type a command or ask the atlas...');
|
.toBe('type a command or ask CBM Lens...');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sagt ueber der Liste, worum es geht', () => {
|
it('sagt ueber der Liste, worum es geht', () => {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function commandExamplesFor(symbol: string): CommandExample[] {
|
|||||||
{
|
{
|
||||||
id: 'question',
|
id: 'question',
|
||||||
text: `Who calls ${symbol}?`,
|
text: `Who calls ${symbol}?`,
|
||||||
note: 'ask the atlas',
|
note: 'ask CBM Lens',
|
||||||
symbol,
|
symbol,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.atlas-brand {
|
.atlas-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -92,22 +95,23 @@ body {
|
|||||||
color: var(--atlas-phosphor);
|
color: var(--atlas-phosphor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.atlas-version {
|
.atlas-brand-beta {
|
||||||
color: var(--atlas-dim);
|
padding: 3px 6px;
|
||||||
|
border: 1px solid rgb(200 173 114 / 28%);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgb(200 173 114 / 7%);
|
||||||
|
color: #c8ad72;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.atlas-version {
|
||||||
* Der Zustand des Arbeitsbaums, neben der Fassung und leiser als sie.
|
|
||||||
*
|
|
||||||
* Kleiner und gedimmt, weil er eine Randbemerkung ist: wer die Fassung sucht,
|
|
||||||
* liest den Chip, und wer wissen will, ob ein Bild zu einem Commit gehoert,
|
|
||||||
* findet daneben die Antwort.
|
|
||||||
*/
|
|
||||||
.atlas-version-suffix {
|
|
||||||
color: var(--atlas-dim);
|
color: var(--atlas-dim);
|
||||||
opacity: 0.7;
|
opacity: 0.65;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
.atlas-shell[data-has-status=true] { grid-template-rows: auto auto minmax(0, 1fr) auto; }
|
.atlas-shell[data-has-status=true] { grid-template-rows: auto auto minmax(0, 1fr) auto; }
|
||||||
.atlas-header { gap: 12px; padding: 12px 18px; overflow: visible; position: relative; z-index: 20; border-bottom-color: var(--atlas-phosphor-mute); box-shadow: 0 2px 18px var(--atlas-glow); }
|
.atlas-header { gap: 12px; padding: 12px 18px; overflow: visible; position: relative; z-index: 20; border-bottom-color: var(--atlas-phosphor-mute); box-shadow: 0 2px 18px var(--atlas-glow); }
|
||||||
.atlas-brand { font-size: 15px; font-weight: 700; letter-spacing: .22em; color: var(--atlas-phosphor); text-shadow: 0 0 16px var(--atlas-glow); }
|
.atlas-brand { font-size: 15px; font-weight: 700; letter-spacing: .22em; color: var(--atlas-phosphor); text-shadow: 0 0 16px var(--atlas-glow); }
|
||||||
.atlas-version, .atlas-version-suffix { font-size: 10px; color: var(--atlas-dim); }
|
.atlas-version { font-size: 9px; color: var(--atlas-dim); }
|
||||||
.atlas-workspace-tabs { display: flex; gap: 4px; margin-left: 16px; }
|
.atlas-workspace-tabs { display: flex; gap: 4px; margin-left: 16px; }
|
||||||
.atlas-workspace-tabs button, .atlas-guidance, .atlas-tools-menu > summary { border: 1px solid transparent; border-radius: 2px; background: transparent; color: var(--atlas-dim); padding: 7px 12px; font: inherit; cursor: pointer; }
|
.atlas-workspace-tabs button, .atlas-guidance, .atlas-tools-menu > summary { border: 1px solid transparent; border-radius: 2px; background: transparent; color: var(--atlas-dim); padding: 7px 12px; font: inherit; cursor: pointer; }
|
||||||
.atlas-workspace-tabs button[aria-selected=true] { background: var(--atlas-select); color: var(--atlas-phosphor); border-color: var(--atlas-phosphor-mute); box-shadow: inset 0 1px 0 var(--atlas-phosphor); }
|
.atlas-workspace-tabs button[aria-selected=true] { background: var(--atlas-select); color: var(--atlas-phosphor); border-color: var(--atlas-phosphor-mute); box-shadow: inset 0 1px 0 var(--atlas-phosphor); }
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const BUG_WIZARD_ENTRY_TOOLTIP =
|
|||||||
* the first would be honest and useless.
|
* the first would be honest and useless.
|
||||||
*/
|
*/
|
||||||
export const BUG_WIZARD_NO_TRACES =
|
export const BUG_WIZARD_NO_TRACES =
|
||||||
'No observed call came back for this symbol. CodeAtlas does not watch code while it runs; '
|
'No observed call came back for this symbol. CBM Lens does not watch code while it runs; '
|
||||||
+ 'it reads back what somebody handed the analysis backend.';
|
+ 'it reads back what somebody handed the analysis backend.';
|
||||||
|
|
||||||
export const BUG_WIZARD_NO_TRACES_HOW =
|
export const BUG_WIZARD_NO_TRACES_HOW =
|
||||||
@@ -114,7 +114,7 @@ export const BUG_WIZARD_NO_TRACES_WHERE =
|
|||||||
|
|
||||||
/** Said above the observed chains, so nobody reads them as a reading of the index. */
|
/** Said above the observed chains, so nobody reads them as a reading of the index. */
|
||||||
export const BUG_WIZARD_OBSERVED_NOTE =
|
export const BUG_WIZARD_OBSERVED_NOTE =
|
||||||
'Read back from the analysis backend, along the calls the index connects. CodeAtlas did not '
|
'Read back from the analysis backend, along the calls the index connects. CBM Lens did not '
|
||||||
+ 'observe any of this itself.';
|
+ 'observe any of this itself.';
|
||||||
|
|
||||||
/** How many times an observed call was seen, and under which run. */
|
/** How many times an observed call was seen, and under which run. */
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ describe('wenn es nichts zu zeigen gibt', () => {
|
|||||||
ir: undefined,
|
ir: undefined,
|
||||||
symbolName: 'nothing',
|
symbolName: 'nothing',
|
||||||
message: 'This file is not indexed yet.',
|
message: 'This file is not indexed yet.',
|
||||||
hint: 'CodeAtlas is still reading this workspace.',
|
hint: 'CBM Lens is still reading this workspace.',
|
||||||
});
|
});
|
||||||
const empty = q('[data-testid="atlas-twin-empty"]');
|
const empty = q('[data-testid="atlas-twin-empty"]');
|
||||||
expect(empty?.textContent).toContain('not indexed yet');
|
expect(empty?.textContent).toContain('not indexed yet');
|
||||||
@@ -627,7 +627,7 @@ describe('die Pseudocode-Ansicht', () => {
|
|||||||
expect(q('[data-testid="atlas-pseudocode-group"]')?.textContent)
|
expect(q('[data-testid="atlas-pseudocode-group"]')?.textContent)
|
||||||
.toContain('not used anywhere in this file as far as the index shows');
|
.toContain('not used anywhere in this file as far as the index shows');
|
||||||
expect(q('[data-testid="atlas-pseudocode-tally"]')?.textContent)
|
expect(q('[data-testid="atlas-pseudocode-tally"]')?.textContent)
|
||||||
.toContain('that CodeAtlas cannot check');
|
.toContain('that CBM Lens cannot check');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('nennt an jeder Schrittzeile Datei und Zeile des aufgerufenen Symbols', async () => {
|
it('nennt an jeder Schrittzeile Datei und Zeile des aufgerufenen Symbols', async () => {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function countOf(count: number, singular: string, plural: string): string
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const TWIN_EMPTY_MESSAGE = 'Place the cursor inside a function, method, or class.';
|
export const TWIN_EMPTY_MESSAGE = 'Place the cursor inside a function, method, or class.';
|
||||||
export const TWIN_EMPTY_HINT = 'CodeAtlas follows the caret and explains whatever it lands in.';
|
export const TWIN_EMPTY_HINT = 'CBM Lens follows the caret and explains whatever it lands in.';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Twin identity and provenance
|
// Twin identity and provenance
|
||||||
@@ -94,13 +94,13 @@ export const TWIN_FILE_NOT_INDEXED = 'This file is not indexed yet.';
|
|||||||
|
|
||||||
/** Says what would change the sentence above. */
|
/** Says what would change the sentence above. */
|
||||||
export const TWIN_FILE_NOT_INDEXED_HINT =
|
export const TWIN_FILE_NOT_INDEXED_HINT =
|
||||||
'CodeAtlas is still reading this workspace. Facts about this symbol appear as soon as it has.';
|
'CBM Lens is still reading this workspace. Facts about this symbol appear as soon as it has.';
|
||||||
|
|
||||||
/** Shown while the IR for a symbol is being built. */
|
/** Shown while the IR for a symbol is being built. */
|
||||||
export const TWIN_LOADING = 'Reading what the index knows about this symbol.';
|
export const TWIN_LOADING = 'Reading what the index knows about this symbol.';
|
||||||
|
|
||||||
/** Shown when the IR call itself failed, which is a different failure from an empty fact. */
|
/** Shown when the IR call itself failed, which is a different failure from an empty fact. */
|
||||||
export const TWIN_LOAD_FAILED = 'CodeAtlas could not reach the analysis backend for this symbol.';
|
export const TWIN_LOAD_FAILED = 'CBM Lens could not reach the analysis backend for this symbol.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the flow switch in the twin head says, in both states.
|
* What the flow switch in the twin head says, in both states.
|
||||||
@@ -550,7 +550,7 @@ export function proseMoreParts(hidden: number): string {
|
|||||||
|
|
||||||
/** Said out loud, because the sentence above it was assembled rather than written. */
|
/** Said out loud, because the sentence above it was assembled rather than written. */
|
||||||
export const PROSE_GENERATED_NOTE =
|
export const PROSE_GENERATED_NOTE =
|
||||||
'Nobody wrote that description. CodeAtlas put it together from what the index recorded, so it can be wrong about intent and right about counts.';
|
'Nobody wrote that description. CBM Lens put it together from what the index recorded, so it can be wrong about intent and right about counts.';
|
||||||
|
|
||||||
export const PROSE_TESTS_NONE =
|
export const PROSE_TESTS_NONE =
|
||||||
'Nothing that looks like a test calls this, so no automatic check would notice a change here.';
|
'Nothing that looks like a test calls this, so no automatic check would notice a change here.';
|
||||||
@@ -573,13 +573,13 @@ export const PLAIN_SUBJECT_ERRORS = 'How this can fail';
|
|||||||
export function plainAbsence(state: KnowledgeState, subject: string): string {
|
export function plainAbsence(state: KnowledgeState, subject: string): string {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'notIndexed':
|
case 'notIndexed':
|
||||||
return `${subject} is unknown: CodeAtlas has not read this file yet.`;
|
return `${subject} is unknown: CBM Lens has not read this file yet.`;
|
||||||
case 'unsupported':
|
case 'unsupported':
|
||||||
return `${subject} is unknown: CodeAtlas cannot see this for this language.`;
|
return `${subject} is unknown: CBM Lens cannot see this for this language.`;
|
||||||
case 'inferred':
|
case 'inferred':
|
||||||
return `${subject} was worked out from what calls it, not read from the code itself.`;
|
return `${subject} was worked out from what calls it, not read from the code itself.`;
|
||||||
case 'unknown':
|
case 'unknown':
|
||||||
return `${subject} is unknown: nothing answered when CodeAtlas asked.`;
|
return `${subject} is unknown: nothing answered when CBM Lens asked.`;
|
||||||
case 'ambiguous':
|
case 'ambiguous':
|
||||||
case 'known':
|
case 'known':
|
||||||
return '';
|
return '';
|
||||||
@@ -752,7 +752,7 @@ export const TERM_EXPLANATIONS: Readonly<Record<string, string>> = {
|
|||||||
'a value this code takes from outside itself, such as a setting or a configuration key, rather than '
|
'a value this code takes from outside itself, such as a setting or a configuration key, rather than '
|
||||||
+ 'from what it was handed.',
|
+ 'from what it was handed.',
|
||||||
'test caller':
|
'test caller':
|
||||||
'a caller that lives in test code. It is how CodeAtlas answers "is this checked", because this index '
|
'a caller that lives in test code. It is how CBM Lens answers "is this checked", because this index '
|
||||||
+ 'records no test relation of its own for TypeScript.',
|
+ 'records no test relation of its own for TypeScript.',
|
||||||
'entry point':
|
'entry point':
|
||||||
'a place the outside world can start this program: a route, a command, a handler. Work arrives here '
|
'a place the outside world can start this program: a route, a command, a handler. Work arrives here '
|
||||||
@@ -814,7 +814,7 @@ export const GROUND_LEAD_SITS_ON =
|
|||||||
export const GROUND_LEAD_CARRIED =
|
export const GROUND_LEAD_CARRIED =
|
||||||
'What holds this up: the callers, and whether the outside world can reach it directly.';
|
'What holds this up: the callers, and whether the outside world can reach it directly.';
|
||||||
export const GROUND_LEAD_DEBTS =
|
export const GROUND_LEAD_DEBTS =
|
||||||
'What is owed against this symbol, from the rules CodeAtlas applies to the recorded facts. Every line is a '
|
'What is owed against this symbol, from the rules CBM Lens applies to the recorded facts. Every line is a '
|
||||||
+ 'rule over facts and not a fact, which is why they all wear the derived marker.';
|
+ 'rule over facts and not a fact, which is why they all wear the derived marker.';
|
||||||
export const GROUND_LEAD_LIMITS =
|
export const GROUND_LEAD_LIMITS =
|
||||||
'What is not on this page, and why. This is part of the answer and not a footnote: an architect who cannot '
|
'What is not on this page, and why. This is part of the answer and not a footnote: an architect who cannot '
|
||||||
@@ -827,7 +827,7 @@ export const GROUND_EMPTY_CARRIED =
|
|||||||
'Nothing carries this: no recorded caller, and nothing marking it as reachable from outside. A symbol with '
|
'Nothing carries this: no recorded caller, and nothing marking it as reachable from outside. A symbol with '
|
||||||
+ 'no carrier is either an entry the index cannot see or a piece nobody uses.';
|
+ 'no carrier is either an entry the index cannot see or a piece nobody uses.';
|
||||||
export const GROUND_EMPTY_DEBTS =
|
export const GROUND_EMPTY_DEBTS =
|
||||||
'No rule fired against this symbol. That is not a clean bill: the rules CodeAtlas can apply are the ones '
|
'No rule fired against this symbol. That is not a clean bill: the rules CBM Lens can apply are the ones '
|
||||||
+ 'listed under what this index stops at, and the ones it cannot apply stay silent either way.';
|
+ 'listed under what this index stops at, and the ones it cannot apply stay silent either way.';
|
||||||
|
|
||||||
/** How many call sites reach into one module. */
|
/** How many call sites reach into one module. */
|
||||||
@@ -877,7 +877,7 @@ export const LIMIT_NO_HANDLER_SITES =
|
|||||||
'Raised error types are recorded; the places they are caught are not. A short error list is therefore not '
|
'Raised error types are recorded; the places they are caught are not. A short error list is therefore not '
|
||||||
+ 'evidence that the failure is contained.';
|
+ 'evidence that the failure is contained.';
|
||||||
export const LIMIT_NO_ROUTES =
|
export const LIMIT_NO_ROUTES =
|
||||||
'Routes for TypeScript are read out of the source text by CodeAtlas, not reported by the engine, so an '
|
'Routes for TypeScript are read out of the source text by CBM Lens, not reported by the engine, so an '
|
||||||
+ 'entry point the engine did not mark can still exist.';
|
+ 'entry point the engine did not mark can still exist.';
|
||||||
export const LIMIT_CALLS_DEDUPLICATED =
|
export const LIMIT_CALLS_DEDUPLICATED =
|
||||||
'Two calls to the same target inside one body are recorded once, with the last site. A count of call sites '
|
'Two calls to the same target inside one body are recorded once, with the last site. A count of call sites '
|
||||||
@@ -895,12 +895,12 @@ export const VOICE_TITLE =
|
|||||||
export const VOICE_RUNNING = 'The local model is rewording the sentences above.';
|
export const VOICE_RUNNING = 'The local model is rewording the sentences above.';
|
||||||
export const VOICE_RESTORE_LABEL = 'Back to the built text';
|
export const VOICE_RESTORE_LABEL = 'Back to the built text';
|
||||||
export const VOICE_RESTORE_TITLE =
|
export const VOICE_RESTORE_TITLE =
|
||||||
'Drop the reworded sentences and show the ones CodeAtlas assembled from the index.';
|
'Drop the reworded sentences and show the ones CBM Lens assembled from the index.';
|
||||||
|
|
||||||
/** The marker beside a sentence the model worded. Same idiom as the state markers. */
|
/** The marker beside a sentence the model worded. Same idiom as the state markers. */
|
||||||
export const VOICE_MARKER = '[~]';
|
export const VOICE_MARKER = '[~]';
|
||||||
export const VOICE_MARKER_NOTE =
|
export const VOICE_MARKER_NOTE =
|
||||||
'Worded by the local model from the sentence CodeAtlas built. The facts in it were checked against the '
|
'Worded by the local model from the sentence CBM Lens built. The facts in it were checked against the '
|
||||||
+ 'original one by one; the wording is the model.';
|
+ 'original one by one; the wording is the model.';
|
||||||
export const VOICE_APPLIED =
|
export const VOICE_APPLIED =
|
||||||
'The sentences above were reworded by the local model. Every name, number, file and line in them was '
|
'The sentences above were reworded by the local model. Every name, number, file and line in them was '
|
||||||
@@ -911,7 +911,7 @@ export function voiceRefused(reason: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const VOICE_UNAVAILABLE =
|
export const VOICE_UNAVAILABLE =
|
||||||
'The local model is off, so these sentences are the ones CodeAtlas assembled. Nothing here waits for it.';
|
'The local model is off, so these sentences are the ones CBM Lens assembled. Nothing here waits for it.';
|
||||||
|
|
||||||
export const FACET_LABELS: Readonly<Record<Facet, string>> = {
|
export const FACET_LABELS: Readonly<Record<Facet, string>> = {
|
||||||
[Facet.Logic]: 'Logic',
|
[Facet.Logic]: 'Logic',
|
||||||
@@ -1075,7 +1075,7 @@ export const RUNTIME_UNEXPECTED_TOOLTIP =
|
|||||||
|
|
||||||
/** Why the runtime section wears its marker: nothing is missing from the index here. */
|
/** Why the runtime section wears its marker: nothing is missing from the index here. */
|
||||||
export const RUNTIME_SECTION_NOTE =
|
export const RUNTIME_SECTION_NOTE =
|
||||||
'Read from an imported recording, not from the index. CodeAtlas did not watch this run.';
|
'Read from an imported recording, not from the index. CBM Lens did not watch this run.';
|
||||||
|
|
||||||
// The runtime badge in the evidence popover ---------------------------------
|
// The runtime badge in the evidence popover ---------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ describe('importsSection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('schreibt die Bilanz ueber die Zeilen und die Herkunft darunter', () => {
|
it('schreibt die Bilanz ueber die Zeilen und die Herkunft darunter', () => {
|
||||||
expect(built.text).toBe('1 used in this file, 1 not used in it, 0 that CodeAtlas cannot check.');
|
expect(built.text).toBe('1 used in this file, 1 not used in it, 0 that CBM Lens cannot check.');
|
||||||
expect(built.note).toContain('The dependency comes from the index');
|
expect(built.note).toContain('The dependency comes from the index');
|
||||||
expect(built.populated).toBe(true);
|
expect(built.populated).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ test('AC1: design tokens and chrome are in place', () => {
|
|||||||
'atlas-statusbar', 'atlas-tree', 'atlas-breadcrumb']) {
|
'atlas-statusbar', 'atlas-tree', 'atlas-breadcrumb']) {
|
||||||
assert.ok(smoke.testids.includes(id), `data-testid ${id} wurde im Smoke nicht gesehen`);
|
assert.ok(smoke.testids.includes(id), `data-testid ${id} wurde im Smoke nicht gesehen`);
|
||||||
}
|
}
|
||||||
assert.ok(/CODEATLAS/.test(smoke.brandText), 'Marke CODEATLAS fehlt in der Kopfzeile');
|
// Archived W2 evidence predates the CBM Lens name; new smoke runs require the current brand.
|
||||||
|
assert.ok(/CBM LENS|CODEATLAS/.test(smoke.brandText), 'Frontend-Marke fehlt in der Kopfzeile');
|
||||||
assert.match(smoke.versionChip, /^v\d+\.\d+\.\d+/, 'Versions-Chip fehlt oder falsch');
|
assert.match(smoke.versionChip, /^v\d+\.\d+\.\d+/, 'Versions-Chip fehlt oder falsch');
|
||||||
// Spezifikations-Korrektur 2026-08-29 (Orchestrator, Nutzerauftrag):
|
// Spezifikations-Korrektur 2026-08-29 (Orchestrator, Nutzerauftrag):
|
||||||
// Der Test nagelte [f]ile fest, also ausgerechnet einen der vier
|
// Der Test nagelte [f]ile fest, also ausgerechnet einen der vier
|
||||||
@@ -32,7 +33,7 @@ test('AC1: design tokens and chrome are in place', () => {
|
|||||||
// sein. Verlangt bleibt das Buchstaben-Menue als Form, belegt am
|
// sein. Verlangt bleibt das Buchstaben-Menue als Form, belegt am
|
||||||
// verdrahteten [a]tlas.
|
// verdrahteten [a]tlas.
|
||||||
assert.ok(/\[a\]tlas/.test(smoke.menuText), 'Buchstaben-Menue fehlt');
|
assert.ok(/\[a\]tlas/.test(smoke.menuText), 'Buchstaben-Menue fehlt');
|
||||||
assert.ok(/type a command or ask the atlas/.test(smoke.commandPlaceholder),
|
assert.ok(/type a command or ask (?:CBM Lens|the atlas)/.test(smoke.commandPlaceholder),
|
||||||
'Kommandozeilen-Platzhalter fehlt');
|
'Kommandozeilen-Platzhalter fehlt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -534,14 +534,14 @@ async function main() {
|
|||||||
const ok =
|
const ok =
|
||||||
failure === null &&
|
failure === null &&
|
||||||
missingTestids.length === 0 &&
|
missingTestids.length === 0 &&
|
||||||
/CODEATLAS/.test(report.brandText) &&
|
/CBM LENS/.test(report.brandText) &&
|
||||||
/^v\d+\.\d+\.\d+/.test(report.versionChip) &&
|
/^v\d+\.\d+\.\d+/.test(report.versionChip) &&
|
||||||
// Nachgezogen am 2026-08-29 (W7a, Nutzerauftrag): der Lauf nagelte
|
// Nachgezogen am 2026-08-29 (W7a, Nutzerauftrag): der Lauf nagelte
|
||||||
// [f]ile fest, also ausgerechnet einen der vier Menuepunkte ohne
|
// [f]ile fest, also ausgerechnet einen der vier Menuepunkte ohne
|
||||||
// Verdrahtung. Eine Attrappe darf keine Zusicherung sein. Verlangt
|
// Verdrahtung. Eine Attrappe darf keine Zusicherung sein. Verlangt
|
||||||
// bleibt das Buchstaben-Menue als Form, belegt am verdrahteten [a]tlas.
|
// bleibt das Buchstaben-Menue als Form, belegt am verdrahteten [a]tlas.
|
||||||
/\[a\]tlas/.test(report.menuText) &&
|
/\[a\]tlas/.test(report.menuText) &&
|
||||||
/type a command or ask the atlas/.test(report.commandPlaceholder) &&
|
/type a command or ask CBM Lens/.test(report.commandPlaceholder) &&
|
||||||
report.treeEntries >= 10 &&
|
report.treeEntries >= 10 &&
|
||||||
report.treeSource === '/api/tree' &&
|
report.treeSource === '/api/tree' &&
|
||||||
report.openedFile === OPEN_FILE &&
|
report.openedFile === OPEN_FILE &&
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ async function main() {
|
|||||||
&& short.imports.top < short.firstLineTop;
|
&& short.imports.top < short.firstLineTop;
|
||||||
report.importHonestyWordsKept = short.imports.entries
|
report.importHonestyWordsKept = short.imports.entries
|
||||||
.some((entry) => entry.text.includes('not used by this symbol as far as the index shows'))
|
.some((entry) => entry.text.includes('not used by this symbol as far as the index shows'))
|
||||||
&& short.imports.tally.includes('that CodeAtlas cannot check')
|
&& short.imports.tally.includes('that CBM Lens cannot check')
|
||||||
&& short.imports.heading.includes('as far as the index shows');
|
&& short.imports.heading.includes('as far as the index shows');
|
||||||
log(`Importgruppe: "${short.imports.heading}" / "${short.imports.tally}"`);
|
log(`Importgruppe: "${short.imports.heading}" / "${short.imports.tally}"`);
|
||||||
|
|
||||||
@@ -681,7 +681,7 @@ async function main() {
|
|||||||
.filter((line) => !line.targetKnown)
|
.filter((line) => !line.targetKnown)
|
||||||
.every((line) => line.target.includes('the index records no place'));
|
.every((line) => line.target.includes('the index records no place'));
|
||||||
report.importHonestyWordsKept = report.importHonestyWordsKept
|
report.importHonestyWordsKept = report.importHonestyWordsKept
|
||||||
&& long.imports.tally.includes('that CodeAtlas cannot check');
|
&& long.imports.tally.includes('that CBM Lens cannot check');
|
||||||
report.headSaysWhatTheFindingIs = report.headSaysWhatTheFindingIs
|
report.headSaysWhatTheFindingIs = report.headSaysWhatTheFindingIs
|
||||||
&& long.lead.text.length > 0
|
&& long.lead.text.length > 0
|
||||||
&& long.lead.kind.length > 0;
|
&& long.lead.kind.length > 0;
|
||||||
|
|||||||
Reference in new issue
Block a user