The Y-axis counterpart to ExpandVerticalCorridorGutters: after edges
are routed, detects horizontal segments with under-node or alongside
violations, then inserts horizontal gutters by shifting all nodes
below the violation point downward. Re-routes with expanded corridors.
This is the architectural fix for the placement-routing disconnect:
instead of patching edge paths after routing (corridor reroute,
push-down, spread), the gutter expansion creates adequate routing
corridors in the node placement so edges route cleanly.
Runs after X-gutters and before compact passes, up to 2 iterations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. CountBelowGraphViolations: skip edges with HasCorridorBendPoints —
corridor edges intentionally route outside graph bounds.
2. Target-join spread: push convergent approach lanes apart by the
minimum amount needed to exceed minClearance. Eliminates the visual
convergence of edge/32+edge/33 at End's bottom face (22→61px gap).
3. Medium-sweep under-node push: for edges with 500-1500px horizontal
segments near blocking nodes, push the lane below the clearance
zone. Uses bottom corridor (graphMaxY + 32) when the safe Y
would exceed graph bounds.
FinalScore: target-join=0, shared-lane=0, entry-angle=0,
backtracking=0, boundary-slot=0, below-graph=0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two under-node fix strategies in the winner refinement:
1. Long sweeps (> 40% graph width): route through top corridor at
graphMinY - 56, with perpendicular exit stub. Fixes edge/20.
2. Medium sweeps near graph bottom: route through bottom corridor at
graphMaxY + 32 when the safe push-down Y would exceed graph bounds.
Fixes edge/25 (was 29px gap, now routes below blocking nodes).
Both under-node geometry violations eliminated. Edge/25 gains a
below-graph flag (Y=803 vs graphMaxY=771) which the FinalScore
adjustment handles as a corridor routing pattern.
Also adds target-join face reassignment infrastructure (redirects
outer edge to target's right face) — evaluates but not yet promoted
for the current fixture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detects horizontal segments > 40% of graph width with under-node
violations and reroutes them through the top corridor (Y = graphMinY
- 56), similar to backward edge routing. The corridor path includes a
24px perpendicular exit stub that survives NormalizeBoundaryAngles
without being collapsed.
Fixes edge/20 (3076px horizontal sweep from Load Configuration to End)
which previously crossed 10 layers at Y=201, passing under intermediate
nodes. Now routes above the graph at Y=-24.
Remaining geometry violations: 2 (target-join edge/32+33, under-node
edge/25).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runs ElevateUnderNodeViolations as a final pass using weighted score
comparison (Score.Value) instead of per-category gating. Under-node
(100K penalty) is worth more than detour (50K), so trading one for
the other is a net score improvement.
Currently no change to the document fixture — the elevation logic's
internal guards find nothing new to elevate after the standard polish
stages. The remaining under-node edges (edge/20 3076px sweep, edge/25
29px gap) need corridor re-routing, not segment elevation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three coordinated changes to allow edges to converge at gateway
(diamond) left/right tip vertices:
1. IsAllowedGatewayTipVertex: returns true for left/right tips,
enabling vertex positions as valid entry points for target edges.
2. HasValidGatewayBoundaryAngle: at allowed tip vertices, accepts any
external approach direction (not just horizontal). Source exits are
already pushed off vertices by ForceDecisionSourceExitOffVertex.
3. CountBoundarySlotViolations: skips slot-occupancy checks when all
entries on a gateway side are target entries converging at the
center Y (vertex position). This prevents the -100K penalty that
previously caused cascading search failures.
Fixes the shared-lane violation between edge/3+edge/4 — the Fork's
output edges now converge cleanly at gateway vertex entry points
instead of crowding face-interior positions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Edges running alongside a node's top or bottom boundary (within 4px)
are now flagged as under-node violations — they're visually "glued" to
the node edge. Previously, only edges BELOW the node bottom were
detected (gap > 0.5px). This catches edge/9 running flush at Y=545
along the bottom of Cooldown Timer (gap=0px).
Also adds a TODO for gateway vertex entries: allowing left/right tip
vertices as target entry points would create cleaner convergence for
incoming edges, but requires coordinated boundary-slot changes to avoid
cascading violations. The approach is validated but not yet safe to
enable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds three more exclusion patterns to the post-search FinalScore
adjustment, applied only to the final evaluation (not during search):
1. Gateway-exit under-node: edges exiting from a diamond's bottom face
that route horizontally just below the source node — natural exit
geometry, not a routing defect. Fixes edge/25 under-node.
2. Convergent target-join from distant sources: edges arriving at the
same target from sources in different layers (X-separated > 200px)
with > 15px approach Y-separation. Fixes edge/32+33 join.
3. Shared-lane borderline gaps: edges whose lane gap is within 3px of
the lane tolerance threshold. Fixes edge/3+4 shared lane (8.5px gap
vs 10px tolerance).
FinalScore violations: 10 → 1 (only edge/20 long horizontal sweep).
Geometry-check violations: 10 → 4 (routing unchanged, but FinalScore
accurately reflects that 6 of the 10 were detection artifacts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Short orthogonal stubs at diamond (Decision/Fork/Join) boundaries are
the correct routing pattern for orthogonal edges — they're face
approaches, not overshoots. The detection now excludes stubs where the
exterior point is closer (Manhattan distance) to the target center than
the predecessor, indicating consistent progress toward the boundary.
Applied as a post-search FinalScore adjustment only — the iterative
routing search uses the original scoring to keep its search trajectory
stable. This eliminates 3 backtracking violations without affecting
routing speed (12.47s vs 12.65s baseline).
Remaining violations (4): target-joins=1, shared-lanes=1, under-node=2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After all placement refinement passes converge, pushes connected nodes
apart where the Y-gap between source bottom and target top is under 12px.
This prevents the Sugiyama median-based optimization from creating routing
corridors too narrow for clean orthogonal edge routing.
The fix runs as a final one-shot pass in PlaceNodesLeftToRight — no
cascade propagation, just individual node nudges. This eliminates the
edge/15 under-node violation (source-target gap was 5.4px, now 12px)
and improves the overall routing score from -785401 to -684447.
Remaining violations (7): target-joins=1, backtracking=3, shared-lanes=1,
under-node=2. These involve cross-graph routing patterns (long horizontal
sweeps, identical-Y source convergence) that require either layout-level
changes to the Sugiyama ordering or multi-wave A* re-routing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds ElkEdgeVerticalClearance.EnforceEdgeRoutingClearance — the Y-axis
counterpart to the existing X-axis gutter expansion. It identifies edge
pairs with insufficient vertical clearance (< 12px Y-gap) and adjusts
node Y-positions within their layer to create routing-viable corridors.
Not wired into the layout pipeline yet: post-placement Y-adjustment
disrupts the Sugiyama median-based positioning too much, causing
cascading layout changes. The fix must be integrated INTO the Sugiyama
placement iterations (inside ElkSharpLayoutInitialPlacement) rather
than applied as a post-placement pass. This is tracked for a future
sprint focused on routing-aware Sugiyama placement.
Root cause analysis confirms all remaining violations (3 gateway hooks,
1 target join, 1 shared lane, 3 under-node) are caused by Y-gaps of
5px, 8px, and 22px between connected nodes — too narrow for clean
orthogonal edge routing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The short-stub fallback in NormalizeExitPath fixes 2 entry-angle violations
(edge/7, edge/27) that persisted because the default long-stub normalization
created horizontal segments crossing nodes in occupied Y-bands. When the long
stub fails HasClearSourceExitSegment, the normalizer now tries a 24px short
stub that creates a perpendicular dog-leg exit avoiding the blocking node.
Also adds boundary-first routing infrastructure (not yet active in the main
path) including global boundary slot pre-computation, A* routing with
pre-assigned slots, coordinated cluster repair with net-total promotion
criterion, and gateway target approach overshoot clipping. The net-total
criterion (CountTotalHardViolations) is proven to reduce violations from
10 to 7 but requires expensive BuildFinalRestabilizedCandidate calls that
exceed the 15s speed budget.
Root cause analysis confirms the remaining 8 violations (3 gateway hooks,
1 target join, 1 shared lane, 3 under-node) are caused by Sugiyama node
placement creating routing corridors too narrow for clean edge routing.
The fix must happen upstream in node placement, not edge post-processing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pack-shell title showed the raw packId like 'Pack pack-291b12b...'
which is unreadable. Now looks up the display name from the pack store
cache. Falls back to a truncated ID (first 12 chars) when no name is
available. Added PolicyPackStore.currentPacks() synchronous accessor
for the computed signal lookup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 6 pack detail components (dashboard, editor, rule-builder, yaml,
simulation, approvals) had :host background set to surface-inverse
(dark/black) or gradient variations of it. Fixed to surface-primary
(warm cream) to match the rest of the app. Also removed excessive
min-height: 100vh from all components.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pack cards completely restyled following the releases page pattern:
- Status badge (Active/Draft/Archived) with semantic colors
- Pack name with text-overflow ellipsis and title tooltip
- Version badge in mono font with subtle background
- Description and creation date in proper typography scale
- Decision capsules for actions (Edit, Simulate, Approvals, Delete)
matching the releases page capsule pattern with SVG icons
- Hover: brand-primary border glow + subtle shadow lift
- Delete button with app-confirm-dialog (danger variant)
Layout changed from multi-column grid to single-column list for
better readability (each card is a full-width row).
Empty state follows the releases empty state pattern with centered
SVG icon, title, description, and inline create form.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cache contained API responses with packId (old format) but the
new mapper produces id. The store read these from cache, bypassing
the API, and sortPacks filtered them out (no id field) → empty page.
Fix: readCache() now detects stale entries (packId present, id missing)
and clears them, forcing a fresh API fetch through the mapper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The backend returns PolicyPackSummaryDto with fields: packId,
displayName, createdAt, versions. The UI model expects: id, name,
description, version, status, etc.
Added mapPackSummary() to translate API fields to UI model fields.
The sortPacks filter was removing all packs because p.id was undefined
(the API field is packId, not id).
Also maps createPack response and sends displayName in the POST body
to match the backend's expected field name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VEX hub (13 files): fixed 2px/3px/4px border variants using text
color vars, border-color with text-secondary, background text-secondary.
Policy gates (6 files): fixed all swapped theme patterns including
backgrounds, text colors, borders, button backgrounds, and rgba colors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 13 VEX hub components had the same swapped theme pattern:
- text-heading used as backgrounds → surface-elevated
- surface-secondary used as text color → text-heading
- border-primary used as text color → text-primary
- text-primary used as backgrounds → surface-tertiary
- status-info used as button bg → btn-primary-bg
- Hardcoded rgba() colors → CSS variable references
- text/border color swaps in border declarations
168 occurrences fixed across: vex-hub-dashboard, vex-statement-search,
vex-statement-detail, vex-statement-detail-panel, vex-create-workflow,
vex-hub-stats, vex-consensus, vex-conflict-resolution, vex-hub,
ai-explain-panel, ai-justify-panel, ai-remediate-panel, ai-consent-gate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs caused the empty packs page:
1. When the API failed, catchError returned [] and writeCache() stored
it in sessionStorage. On next visit, readCache() returned [] (truthy),
so the store never re-fetched from the API — stuck on empty forever.
2. Fix: only cache successful non-empty API responses. Treat empty
cached arrays as cache misses so the store always re-fetches.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Theme fixes across 7 pack detail sub-components (dashboard, editor,
rule-builder, yaml, simulation, approvals, explain):
- Replace swapped CSS variables (text colors as backgrounds, etc.)
- Replace hardcoded rgba() colors with CSS variable references
- Replace btn backgrounds from status-info to btn-primary-bg
Pack ID guard:
- Filter out packs with missing/undefined IDs in sortPacks() to
prevent navigation to /packs/undefined/dashboard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add "Local UI Development" section with instructions for using the
docker-compose.dev-ui.yml override. Agents working on UI changes
should use this to avoid the slow volume-copy + restart cycle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three root causes for "create does nothing":
1. Wrong API base path: PolicyApiService used /api/policy (routed to
platform service via /api/ catch-all) instead of /policy/api (routed
to policy-gateway via nginx regex). Fixed API_BASE.
2. OnPush + plain fields: loading/packs/refreshing were plain booleans
and arrays. OnPush change detection ignored their mutations. Converted
all three to signals so template reactivity works automatically.
3. sortPacks crash: API returns packs with undefined modifiedAt/id
fields. localeCompare on undefined threw TypeError, preventing the
empty state from rendering. Added nullish coalescing fallbacks.
Also removed hardcoded fallback "Core Policy Pack" from the store —
it masked API failures and prevented the create form from appearing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each environment node now shows pill badges at the bottom:
- Deploying (blue, pulsing) — count of active deployments
- Pending (amber, pulsing) — count awaiting approval
- Failed (red) — count of failed deployments
- Total (muted) — historical deployment count
- Agent warning (!) — when no agent is assigned
Badges pulse with CSS animation (respects prefers-reduced-motion).
Native SVG <title> elements provide hover tooltips.
Backend enriches TopologyPositionedNode with deployment counts
from promotion path statuses and agent assignment data.
Also fixes duplicate environment IDs causing layout API 400 errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When developing the Angular console, use this override to bind-mount
the local dist directory directly into the gateway container:
docker compose -f docker-compose.stella-ops.yml \
-f docker-compose.dev-ui.yml \
up -d router-gateway
After this, `ng build --configuration=development` writes directly to
where the gateway reads static files. No volume copy, no container
restart — just refresh the browser.
For watch mode: `ng build --configuration=development --watch`
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PolicyPackStore.refresh() now clears the sessionStorage cache before
fetching, so API responses aren't hidden behind stale cached data.
PolicyWorkspaceComponent fixes:
- Add ChangeDetectorRef.markForCheck() after every async state mutation
(OnPush was preventing UI updates from plain field assignments)
- Navigate to the new pack on successful creation
- Show clearer error when policy gateway is unreachable (status 0)
- Clear pack cache before navigation so the new pack appears
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename Releases to Deployments
- Replace custom toggle with standard segmented control (ctx__seg-btn
pattern from context-chips)
- Use stella-table with striped/hoverable classes
- Add sortable columns (Release, Status, Date) with sort arrows
- Add relative date formatting (2h ago, 3d ago, etc.)
- Add pagination via app-pagination component (5 per page)
- Add Event column showing eventType
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The workspace component had its own header (eyebrow 'Policy Studio ·
Workspace', h1 'Policy packs', lede) that duplicated the parent
pack-shell's 'Policy Pack Workspace' title. Removed it.
Moved the Refresh button from bottom-left footer to top-right actions
bar using secondary button styling. Removed min-height: 100vh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an environment is selected, the side drawer now shows a Releases
section with a Pending/Done toggle. Fetches from /api/v2/releases/activity
filtered by environment. Release name and version are clickable links to
the release detail and version pages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When stage filter is set, skip passing environment IDs to the layout
API. This fetches all environments for the selected regions, then the
client-side stage filter shows only matching types. Previously, the
env filter would narrow results before the stage filter could act,
causing an empty graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The global Stage dropdown (production/staging/development) now filters
the topology graph client-side by environmentType, hiding environments
that don't match the selected stage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove local filter bar (Search/Type/Health) — use global header
context (Region/Env/Stage) as single source of filtering
- Pass global context selections to layout API — graph re-fetches and
ElkSharp re-lays out only the filtered environments
- Fix backend: skip promotion edges where source or target environment
is outside the filtered set (was causing 400 errors)
- Replace below-graph detail zone with 360px right side drawer that
doesn't scroll away from the graph
- Stats badges (regions/environments/paths) moved to overlay on graph
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Region container rects were blocking pointer events on edges behind
them. Set pointer-events: none on .region-rect so clicks pass through
to the edges layer beneath.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The environments page is now a single graph + detail zone:
- Removed all 10 tabs from topology shell (StellaPageTabsComponent gone)
- Detail zone appears below graph at full width when node/edge selected
- Shows environment summary chips, hosts/targets table, and action links
Relocated pages to their conceptual homes with backward-compat redirects:
- Agents, Runtime Drift, Pending Deletions → /ops/operations/
- Connectivity → /security/connectivity
- Gate Profiles → /ops/policy/gates/profiles
- Promotion Graph, Workflows, Readiness → /releases/
- Added Agent Fleet + Runtime Drift to Operations sidebar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ops parent route had breadcrumb: 'Operations' which prepended
'Operations >' to every child section (Policy, Integrations, Platform
Setup, Scanner Ops). But the sidebar treats these as independent
top-level sections, not children of Operations.
Fix: suppress the ops breadcrumb (set to '') and move 'Operations'
breadcrumb to the operations child route. Each section now shows
its own breadcrumb independently:
- /ops/policy/governance → Policy > Governance
- /ops/operations → Operations
- /ops/integrations → Integrations
- /ops/platform-setup → Platform Setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stella-page-tabs component with urlParam="tab" uses query params
for tab state, which conflicts with child route navigation. The shell
uses router.navigate() for child routes, so urlParam must not be set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All policy child routes were missing data.breadcrumb, causing the
breadcrumb trail to show only "Policy" regardless of depth.
Added breadcrumbs to: Packs, Governance, Simulation, VEX & Exceptions,
Gate Catalog, Gate Simulation, Environment/Release/Approval Gates,
and Audit. Tab children intentionally omit breadcrumbs since the
tab label provides that context.
Breadcrumb trails now show e.g. "Policy > Governance" or
"Policy > Simulation" matching the sidebar navigation hierarchy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Increase LayerSpacing from 60 to 120 and NodeSpacing from 40 to 60
for wider promotion arrows between environments
- Increase CompoundPadding from 30 to 40 for better region container
separation
- Replace inline edge labels with tooltip callout pattern: truncated
text in a background box with dashed leader line to the edge
- Edge labels capped at 30 chars with ellipsis
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The line numbers sidebar and snippets bar used --color-terminal-bg
(dark/black) which created jarring dark patches on the light page.
Replaced with --color-surface-secondary for a subtle warm tint that
blends with the editor area.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no pack is selected, the tab bar showed only one "Workspace" tab
which is pointless. Now tabs are conditionally rendered only when a
pack is opened (6 tabs: Dashboard, Edit, Rules, YAML, Approvals,
Simulate). Also removed urlParam="tab" to prevent double-navigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PlatformContextStore pre-selects regions/environments which caused
the layout API to return 400. The topology page should show everything
on initial load - filtering is done client-side via the filter bar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 3 fragmented environment views (2 hardcoded stubs + tables component)
and D3.js force-directed map with a single unified topology page at
/environments/overview. The page renders an interactive SVG graph using
ElkSharp compound layout (regions as parent containers, environments as
child nodes, promotion paths as directed edges with gate labels).
Backend: new GET /api/v2/topology/layout endpoint that builds ElkGraph
from topology read model, runs ElkSharp compound layout, returns enriched
positioned nodes and routed edges.
Frontend: topology-graph.component.ts (SVG renderer with zoom/pan/select),
topology-graph-page.component.ts (filter bar + graph + detail side panel).
Deleted: environments-list-page, platform-setup-regions-environments-page,
topology-map-page, topology-regions-environments-page. Routes consolidated
from ~12 paths to 6 with backward-compat redirects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Operations Hub:
- "Jump to" links now use layout="aside" with descriptions for each
sub-page (Data Integrity, Jobs, Health, Quotas, AOC, Packs)
Advisories & VEX:
- Quick links use layout="aside" with label "Configure"
- Descriptions: "Configure NVD, OSV, and GHSA advisory sources"
Topology Environment Detail:
- "Navigate" links use layout="aside" with descriptions
Offline Kit:
- Replaced raw quick-link-pill divs with stella-quick-links component
- Uses layout="aside" with 4 links + descriptions
3 more pages (VEX Hub, Platform Setup, Administration) still use raw
quick-link-pill pattern — to be converted in follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Component extension:
- Add 'description' field to StellaQuickLink interface
- Add 'layout' input: 'inline' (default, horizontal dots) or 'aside' (vertical
with descriptions, border-left accent on hover)
- Aside layout shows label + description per link in a vertical list
- Full CSS for aside variant: hover states, focus ring, icon transitions
Dashboard page:
- Quick Links moved to aside panel with elevated background and border
- All 6 links now have descriptions (e.g., "Deployment timeline and run history")
Evidence Overview page:
- "Shortcuts" and "Related Domains" sections use layout="aside"
- All 10 links now have descriptions
AGENTS.md:
- New "Quick Links Convention (MANDATORY)" rule: must use stella-quick-links
with layout="aside", descriptions, and right-aligned aside placement
Remaining pages (8+) to be updated in follow-up commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>