Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4f819be
feat(scan-engine): read intra-repo import edges from source text
devtofunmi Aug 19, 2026
1a4eb4c
test(scan-engine): cover import extraction and specifier resolution
devtofunmi Aug 19, 2026
ac8a174
feat(scan-engine): rank the repo tree to bound what the graph reads
devtofunmi Aug 19, 2026
b5574ae
test(scan-engine): cover candidate ranking and its cap
devtofunmi Aug 19, 2026
dd955a7
fix(scan-engine): remove a raw NUL byte from the edge dedupe key
devtofunmi Aug 19, 2026
5ce9c77
feat(scan-engine): aggregate import edges into a capped module graph
devtofunmi Aug 19, 2026
7527052
test(scan-engine): cover fan-in ranking and module aggregation
devtofunmi Aug 19, 2026
6eea7e2
perf(github-app): cache the installation access token until it expires
devtofunmi Aug 19, 2026
e0e06c7
test(github-app): cover the token freshness margin
devtofunmi Aug 19, 2026
ea876ee
feat(scan-engine): read an explicit list of files a few at a time
devtofunmi Aug 19, 2026
8dd5c9e
feat(scan-engine): ask the model to label modules, and only to label …
devtofunmi Aug 19, 2026
c621a86
feat(scan-engine): pick scan files by fan-in instead of tree order
devtofunmi Aug 19, 2026
a5da9e2
feat(scan-engine): coerce the stored architecture map back defensively
devtofunmi Aug 19, 2026
99246ef
test(scan-engine): cover architecture map coercion
devtofunmi Aug 19, 2026
3a19101
feat(scans): return the architecture map with a scan detail
devtofunmi Aug 19, 2026
451f6e3
feat(scans): lay out the architecture map left to right
devtofunmi Aug 19, 2026
9f09ae7
test(scans): cover architecture layout and cycle handling
devtofunmi Aug 19, 2026
10c97df
feat(scans): draw the architecture map on a scan
devtofunmi Aug 19, 2026
565fd7b
test(scans): cover what the architecture map claims
devtofunmi Aug 19, 2026
a4de3a9
fix(scan-engine): guard label parsing against a non-object payload
devtofunmi Aug 19, 2026
d2a616f
test(scan-engine): rename a shadowed paths binding
devtofunmi Aug 19, 2026
1bce355
feat(scans): show the architecture map above a scan findings
devtofunmi Aug 19, 2026
24f0fac
perf(scan-engine): bucket module collapse by depth instead of rescanning
devtofunmi Aug 20, 2026
e3b037e
fix(scan-engine): stop one large file swallowing the whole prompt budget
devtofunmi Aug 20, 2026
61b7e81
test(scan-engine): cover the prompt budget split
devtofunmi Aug 20, 2026
b187674
fix(scans): bound the architecture map height
devtofunmi Aug 20, 2026
c0d82fa
test(scans): drop the markup-coupled shape count from the map test
devtofunmi Aug 20, 2026
e550051
test(github-app): trim the token freshness cases to the two that matter
devtofunmi Aug 20, 2026
3be1de2
chore(env): register the architecture map flag variable
devtofunmi Aug 20, 2026
7febf79
feat(server): gate the architecture map to named GitHub logins
devtofunmi Aug 20, 2026
4a910cc
test(server): cover the architecture map allowlist
devtofunmi Aug 20, 2026
99eabbd
feat(scans): make the architecture map opt-in per run
devtofunmi Aug 20, 2026
781698f
feat(scans): withhold the architecture map from users without the flag
devtofunmi Aug 20, 2026
88de857
docs(env): document the architecture map allowlist
devtofunmi Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ BACHS_API_KEY="your-bachs-secret-key"
BACHS_API_BASE="https://sandbox-api.bachs.io" # live: https://api.bachs.io
BACHS_PRO_PRODUCT_ID="your-bachs-pro-product-id"
BACHS_WEBHOOK_SECRET="your-bachs-webhook-signing-secret"

# --- Feature flags ---
# Comma-separated GitHub logins that can see the codebase architecture map.
# Unset falls back to the accounts trialling it; "*" opens it to everyone;
# empty disables it for all. Gates both building the map and returning it.
ARCHITECTURE_MAP_LOGINS="devtofunmi,xt42io"
196 changes: 196 additions & 0 deletions src/components/scans/architecture-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { describe, expect, it } from 'vitest'

import {
GAP_X,
GAP_Y,
NODE_HEIGHT,
NODE_WIDTH,
PADDING,
assignLayers,
layoutArchitecture,
moduleName,
moduleParent,
} from './architecture-layout'
import type { ScanArchitectureModule } from '../../server/scan-engine/summary'

const asModule = (id: string): ScanArchitectureModule => ({
id,
label: null,
files: 1,
findings: 0,
counts: { critical: 0, high: 0, medium: 0, low: 0, note: 0 },
topSeverity: null,
})

describe('assignLayers', () => {
it('puts a module one column right of what imports it', () => {
const layers = assignLayers(['routes', 'server', 'db'].map(asModule), [
{ from: 'routes', to: 'server', weight: 1 },
{ from: 'server', to: 'db', weight: 1 },
])

expect(layers.get('routes')).toBe(0)
expect(layers.get('server')).toBe(1)
expect(layers.get('db')).toBe(2)
})

it('uses the longest path when a module has two importers', () => {
const layers = assignLayers(['a', 'b', 'c'].map(asModule), [
{ from: 'a', to: 'b', weight: 1 },
{ from: 'b', to: 'c', weight: 1 },
{ from: 'a', to: 'c', weight: 1 },
])

// Reachable at depth 1 directly and depth 2 via b; the deeper one wins so
// the arrow never points backwards.
expect(layers.get('c')).toBe(2)
})

it('ignores the edge that closes a cycle when placing columns', () => {
const layers = assignLayers(['a', 'b', 'c'].map(asModule), [
{ from: 'a', to: 'b', weight: 1 },
{ from: 'b', to: 'c', weight: 1 },
{ from: 'c', to: 'a', weight: 1 },
])

// Without dropping c -> a from the maths, the cycle would push every module
// to the last column and the diagram would collapse into one stack.
expect(layers.get('a')).toBe(0)
expect(layers.get('b')).toBe(1)
expect(layers.get('c')).toBe(2)
})

it('keeps a module out of a column its own dependents pushed it past', () => {
// A two-module cycle plus a third importer: the cycle must not drag `hub`
// rightwards past everything that depends on it.
const layers = assignLayers(['app', 'hub', 'util'].map(asModule), [
{ from: 'app', to: 'hub', weight: 1 },
{ from: 'hub', to: 'util', weight: 1 },
{ from: 'util', to: 'hub', weight: 1 },
])

expect(layers.get('app')).toBe(0)
expect(layers.get('hub')).toBe(1)
expect(layers.get('util')).toBe(2)
})

it('still terminates when every module is in one cycle', () => {
const ids = ['a', 'b', 'c', 'd']
const layers = assignLayers(
ids.map(asModule),
ids.map((id, index) => ({
from: id,
to: ids[(index + 1) % ids.length],
weight: 1,
})),
)

for (const id of ids) {
expect(layers.get(id)).toBeLessThanOrEqual(ids.length - 1)
expect(layers.get(id)).toBeGreaterThanOrEqual(0)
}
})

it('ignores an edge naming a module that is not present', () => {
const layers = assignLayers(
[asModule('a')],
[
{ from: 'a', to: 'ghost', weight: 1 },
{ from: 'ghost', to: 'a', weight: 1 },
],
)

expect(layers.get('a')).toBe(0)
expect(layers.has('ghost')).toBe(false)
})
})

describe('layoutArchitecture', () => {
it('lays out nothing for an empty map', () => {
expect(layoutArchitecture([], [])).toEqual({
nodes: [],
edges: [],
width: 0,
height: 0,
})
})

it('sizes the canvas from the column count and the tallest column', () => {
const layout = layoutArchitecture(['a', 'b', 'c'].map(asModule), [
{ from: 'a', to: 'c', weight: 1 },
{ from: 'b', to: 'c', weight: 1 },
])

// Two columns: [a, b] then [c].
expect(layout.width).toBe(2 * NODE_WIDTH + GAP_X + PADDING * 2)
expect(layout.height).toBe(2 * NODE_HEIGHT + GAP_Y + PADDING * 2)
})

it('centres a short column against the tallest one', () => {
const layout = layoutArchitecture(['a', 'b', 'c'].map(asModule), [
{ from: 'a', to: 'c', weight: 1 },
{ from: 'b', to: 'c', weight: 1 },
])

const c = layout.nodes.find((node) => node.id === 'c')!
const a = layout.nodes.find((node) => node.id === 'a')!
const b = layout.nodes.find((node) => node.id === 'b')!

expect(c.y).toBeCloseTo((a.y + b.y) / 2)
expect(c.x).toBeGreaterThan(a.x)
})

it('draws an edge between the two boxes it connects', () => {
const layout = layoutArchitecture(['a', 'b'].map(asModule), [
{ from: 'a', to: 'b', weight: 3 },
])

const a = layout.nodes.find((node) => node.id === 'a')!
const b = layout.nodes.find((node) => node.id === 'b')!

expect(layout.edges).toHaveLength(1)
expect(layout.edges[0].weight).toBe(3)
expect(layout.edges[0].path).toContain(
`M ${a.x + NODE_WIDTH} ${a.y + NODE_HEIGHT / 2}`,
)
expect(layout.edges[0].path).toContain(`${b.x} ${b.y + NODE_HEIGHT / 2}`)
})

it('drops an edge whose endpoint is not on the map', () => {
const layout = layoutArchitecture(
[asModule('a')],
[{ from: 'a', to: 'ghost', weight: 1 }],
)

expect(layout.edges).toEqual([])
})

it('draws the same picture for the same map', () => {
const modules = ['a', 'b', 'c'].map(asModule)
const edges = [{ from: 'a', to: 'b', weight: 1 }]

expect(layoutArchitecture(modules, edges)).toEqual(
layoutArchitecture(modules, edges),
)
})
})

describe('moduleName', () => {
it('emphasises the segment that distinguishes the module', () => {
expect(moduleName('src/server/scan-engine')).toBe('scan-engine')
expect(moduleParent('src/server/scan-engine')).toBe('src/server')
})

it('has no parent for a top-level module', () => {
expect(moduleName('src')).toBe('src')
expect(moduleParent('src')).toBeNull()
})

it('shortens a parent path that would not fit', () => {
const parent = moduleParent(
'packages/platform/services/internal/deep/nested/leaf',
)
expect(parent!.length).toBeLessThanOrEqual(24)
expect(parent).toContain('…')
})
})
Loading
Loading