Update compose config, policy simulation, and workflow replay

- devops/compose: README, docker-compose, hosts updates
- Policy simulation: pre-promotion and test-validate panels,
  routes, dashboard, and spec updates
- Workflow visualization: run-graph replay page template update
- Claude settings update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 17:26:02 +03:00
parent 260fce8ef8
commit 8536a6c707
11 changed files with 637 additions and 334 deletions

View File

@@ -8,14 +8,14 @@ import { PolicySimulationStudioComponent } from './policy-simulation.component';
@Component({ selector: 'app-shadow-mode-dashboard', template: '', standalone: true })
class MockShadowModeDashboardComponent {}
@Component({ selector: 'app-simulation-console', template: '', standalone: true })
class MockSimulationConsoleComponent {}
@Component({ selector: 'app-promotion-gate', template: '', standalone: true })
class MockPromotionGateComponent {}
@Component({ selector: 'app-coverage-fixture', template: '', standalone: true })
class MockCoverageFixtureComponent {}
@Component({ selector: 'app-simulation-test-validate-panel', template: '', standalone: true })
class MockSimulationTestValidatePanelComponent {}
@Component({ selector: 'app-policy-audit-log', template: '', standalone: true })
class MockPolicyAuditLogComponent {}
@Component({ selector: 'app-simulation-pre-promotion-panel', template: '', standalone: true })
class MockSimulationPrePromotionPanelComponent {}
@Component({ selector: 'app-effective-policy-viewer', template: '', standalone: true })
class MockEffectivePolicyViewerComponent {}
@@ -23,27 +23,6 @@ class MockEffectivePolicyViewerComponent {}
@Component({ selector: 'app-policy-exception', template: '', standalone: true })
class MockPolicyExceptionComponent {}
@Component({ selector: 'app-policy-lint', template: '', standalone: true })
class MockPolicyLintComponent {}
@Component({ selector: 'app-promotion-gate', template: '', standalone: true })
class MockPromotionGateComponent {}
@Component({ selector: 'app-policy-diff-viewer', template: '', standalone: true })
class MockPolicyDiffViewerComponent {}
@Component({ selector: 'app-policy-merge-preview', template: '', standalone: true })
class MockPolicyMergePreviewComponent {}
@Component({ selector: 'app-simulation-history', template: '', standalone: true })
class MockSimulationHistoryComponent {}
@Component({ selector: 'app-conflict-detection', template: '', standalone: true })
class MockConflictDetectionComponent {}
@Component({ selector: 'app-batch-evaluation', template: '', standalone: true })
class MockBatchEvaluationComponent {}
describe('PolicySimulationStudioComponent', () => {
let component: PolicySimulationStudioComponent;
let fixture: ComponentFixture<PolicySimulationStudioComponent>;
@@ -53,36 +32,22 @@ describe('PolicySimulationStudioComponent', () => {
imports: [
PolicySimulationStudioComponent,
MockShadowModeDashboardComponent,
MockSimulationConsoleComponent,
MockCoverageFixtureComponent,
MockPolicyAuditLogComponent,
MockPromotionGateComponent,
MockSimulationTestValidatePanelComponent,
MockSimulationPrePromotionPanelComponent,
MockEffectivePolicyViewerComponent,
MockPolicyExceptionComponent,
MockPolicyLintComponent,
MockPromotionGateComponent,
MockPolicyDiffViewerComponent,
MockPolicyMergePreviewComponent,
MockSimulationHistoryComponent,
MockConflictDetectionComponent,
MockBatchEvaluationComponent,
],
})
.overrideComponent(PolicySimulationStudioComponent, {
set: {
imports: [
MockShadowModeDashboardComponent,
MockSimulationConsoleComponent,
MockCoverageFixtureComponent,
MockPolicyAuditLogComponent,
MockPromotionGateComponent,
MockSimulationTestValidatePanelComponent,
MockSimulationPrePromotionPanelComponent,
MockEffectivePolicyViewerComponent,
MockPolicyExceptionComponent,
MockPolicyLintComponent,
MockPromotionGateComponent,
MockPolicyDiffViewerComponent,
MockPolicyMergePreviewComponent,
MockSimulationHistoryComponent,
MockConflictDetectionComponent,
MockBatchEvaluationComponent,
],
},
})
@@ -103,46 +68,34 @@ describe('PolicySimulationStudioComponent', () => {
});
it('should have all tab definitions', () => {
expect(component.tabs.length).toBe(13);
expect(component.tabs.length).toBe(6);
});
it('should have tab IDs for all expected features', () => {
const tabIds = component.tabs.map((t) => t.id);
expect(tabIds).toContain('shadow');
expect(tabIds).toContain('simulation');
expect(tabIds).toContain('coverage');
expect(tabIds).toContain('lint');
expect(tabIds).toContain('audit');
expect(tabIds).toContain('promotion');
expect(tabIds).toContain('testing');
expect(tabIds).toContain('review');
expect(tabIds).toContain('effective');
expect(tabIds).toContain('exceptions');
expect(tabIds).toContain('promotion');
expect(tabIds).toContain('diff');
expect(tabIds).toContain('merge');
expect(tabIds).toContain('history');
expect(tabIds).toContain('conflicts');
expect(tabIds).toContain('batch');
});
});
describe('Tab Navigation', () => {
it('should change active tab on setActiveTab', () => {
component.setActiveTab('simulation');
expect(component.activeTab()).toBe('simulation');
component.setActiveTab('promotion');
expect(component.activeTab()).toBe('promotion');
});
it('should change active tab to coverage', () => {
component.setActiveTab('coverage');
expect(component.activeTab()).toBe('coverage');
it('should change active tab to testing', () => {
component.setActiveTab('testing');
expect(component.activeTab()).toBe('testing');
});
it('should change active tab to audit', () => {
component.setActiveTab('audit');
expect(component.activeTab()).toBe('audit');
});
it('should change active tab to lint', () => {
component.setActiveTab('lint');
expect(component.activeTab()).toBe('lint');
it('should change active tab to review', () => {
component.setActiveTab('review');
expect(component.activeTab()).toBe('review');
});
it('should change active tab to effective', () => {
@@ -159,7 +112,7 @@ describe('PolicySimulationStudioComponent', () => {
describe('Template Rendering', () => {
it('should render navigation tabs', () => {
const navTabs = fixture.nativeElement.querySelectorAll('.nav-tab');
expect(navTabs.length).toBe(13);
expect(navTabs.length).toBe(6);
});
it('should mark active tab with active class', () => {
@@ -171,12 +124,12 @@ describe('PolicySimulationStudioComponent', () => {
it('should display tab labels', () => {
const tabLabels = fixture.nativeElement.querySelectorAll('.nav-tab__label');
expect(tabLabels[0].textContent).toContain('Shadow Mode');
expect(tabLabels[1].textContent).toContain('Simulation Console');
expect(tabLabels[1].textContent).toContain('Promotion Gate');
});
it('should display tab icons', () => {
const tabIcons = fixture.nativeElement.querySelectorAll('.nav-tab__icon');
expect(tabIcons.length).toBe(13);
expect(tabIcons.length).toBe(6);
});
it('should have navigation with tablist role', () => {
@@ -186,7 +139,7 @@ describe('PolicySimulationStudioComponent', () => {
it('should have buttons with tab role', () => {
const tabButtons = fixture.nativeElement.querySelectorAll('button[role="tab"]');
expect(tabButtons.length).toBe(13);
expect(tabButtons.length).toBe(6);
});
it('should set aria-selected on active tab', () => {
@@ -207,15 +160,15 @@ describe('PolicySimulationStudioComponent', () => {
tick();
fixture.detectChanges();
expect(component.activeTab()).toBe('simulation');
expect(component.activeTab()).toBe('promotion');
}));
it('should update active class on tab change', fakeAsync(() => {
component.setActiveTab('coverage');
component.setActiveTab('testing');
fixture.detectChanges();
const activeTab = fixture.nativeElement.querySelector('.nav-tab--active');
expect(activeTab.textContent).toContain('Coverage');
expect(activeTab.textContent).toContain('Test & Validate');
}));
});
@@ -225,44 +178,34 @@ describe('PolicySimulationStudioComponent', () => {
expect(shadowComponent).toBeTruthy();
});
it('should display simulation console when selected', fakeAsync(() => {
component.setActiveTab('simulation');
it('should display promotion gate when selected', fakeAsync(() => {
component.setActiveTab('promotion');
fixture.detectChanges();
tick();
fixture.detectChanges();
const simulationComponent = fixture.debugElement.query(By.directive(MockSimulationConsoleComponent));
expect(simulationComponent).toBeTruthy();
const promotionComponent = fixture.debugElement.query(By.directive(MockPromotionGateComponent));
expect(promotionComponent).toBeTruthy();
}));
it('should display coverage fixture when selected', fakeAsync(() => {
component.setActiveTab('coverage');
it('should display test & validate panel when selected', fakeAsync(() => {
component.setActiveTab('testing');
fixture.detectChanges();
tick();
fixture.detectChanges();
const coverageComponent = fixture.debugElement.query(By.directive(MockCoverageFixtureComponent));
expect(coverageComponent).toBeTruthy();
const testingComponent = fixture.debugElement.query(By.directive(MockSimulationTestValidatePanelComponent));
expect(testingComponent).toBeTruthy();
}));
it('should display policy lint when selected', fakeAsync(() => {
component.setActiveTab('lint');
it('should display pre-promotion review panel when selected', fakeAsync(() => {
component.setActiveTab('review');
fixture.detectChanges();
tick();
fixture.detectChanges();
const lintComponent = fixture.debugElement.query(By.directive(MockPolicyLintComponent));
expect(lintComponent).toBeTruthy();
}));
it('should display audit log when selected', fakeAsync(() => {
component.setActiveTab('audit');
fixture.detectChanges();
tick();
fixture.detectChanges();
const auditComponent = fixture.debugElement.query(By.directive(MockPolicyAuditLogComponent));
expect(auditComponent).toBeTruthy();
const reviewComponent = fixture.debugElement.query(By.directive(MockSimulationPrePromotionPanelComponent));
expect(reviewComponent).toBeTruthy();
}));
it('should display effective policies when selected', fakeAsync(() => {
@@ -284,66 +227,6 @@ describe('PolicySimulationStudioComponent', () => {
const exceptionsComponent = fixture.debugElement.query(By.directive(MockPolicyExceptionComponent));
expect(exceptionsComponent).toBeTruthy();
}));
it('should display promotion gate when selected', fakeAsync(() => {
component.setActiveTab('promotion');
fixture.detectChanges();
tick();
fixture.detectChanges();
const promotionComponent = fixture.debugElement.query(By.directive(MockPromotionGateComponent));
expect(promotionComponent).toBeTruthy();
}));
it('should display diff viewer when selected', fakeAsync(() => {
component.setActiveTab('diff');
fixture.detectChanges();
tick();
fixture.detectChanges();
const diffComponent = fixture.debugElement.query(By.directive(MockPolicyDiffViewerComponent));
expect(diffComponent).toBeTruthy();
}));
it('should display merge preview when selected', fakeAsync(() => {
component.setActiveTab('merge');
fixture.detectChanges();
tick();
fixture.detectChanges();
const mergeComponent = fixture.debugElement.query(By.directive(MockPolicyMergePreviewComponent));
expect(mergeComponent).toBeTruthy();
}));
it('should display simulation history when selected', fakeAsync(() => {
component.setActiveTab('history');
fixture.detectChanges();
tick();
fixture.detectChanges();
const historyComponent = fixture.debugElement.query(By.directive(MockSimulationHistoryComponent));
expect(historyComponent).toBeTruthy();
}));
it('should display conflict detection when selected', fakeAsync(() => {
component.setActiveTab('conflicts');
fixture.detectChanges();
tick();
fixture.detectChanges();
const conflictsComponent = fixture.debugElement.query(By.directive(MockConflictDetectionComponent));
expect(conflictsComponent).toBeTruthy();
}));
it('should display batch evaluation when selected', fakeAsync(() => {
component.setActiveTab('batch');
fixture.detectChanges();
tick();
fixture.detectChanges();
const batchComponent = fixture.debugElement.query(By.directive(MockBatchEvaluationComponent));
expect(batchComponent).toBeTruthy();
}));
});
describe('Tab Configuration', () => {

View File

@@ -2,40 +2,38 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { ShadowModeDashboardComponent } from './shadow-mode-dashboard.component';
import { SimulationConsoleComponent } from './simulation-console.component';
import { CoverageFixtureComponent } from './coverage-fixture.component';
import { PolicyAuditLogComponent } from './policy-audit-log.component';
import { PromotionGateComponent } from './promotion-gate.component';
import { SimulationTestValidatePanelComponent } from './simulation-test-validate-panel.component';
import { SimulationPrePromotionPanelComponent } from './simulation-pre-promotion-panel.component';
import { EffectivePolicyViewerComponent } from './effective-policy-viewer.component';
import { PolicyExceptionComponent } from './policy-exception.component';
import { PolicyLintComponent } from './policy-lint.component';
import { PromotionGateComponent } from './promotion-gate.component';
import { PolicyDiffViewerComponent } from './policy-diff-viewer.component';
import { PolicyMergePreviewComponent } from './policy-merge-preview.component';
import { SimulationHistoryComponent } from './simulation-history.component';
import { ConflictDetectionComponent } from './conflict-detection.component';
import { BatchEvaluationComponent } from './batch-evaluation.component';
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
/**
* Main Policy Simulation Studio component with tabbed navigation.
* Provides access to Shadow Mode, Simulation Console, Coverage, Audit Log,
* Effective Policies, and Exceptions management.
*
* Rationalized from 13 tabs to 6:
* 1. Shadow Mode (default) - unchanged
* 2. Promotion Gate - unchanged
* 3. Test & Validate - merges Simulation Console + Coverage + Lint
* 4. Pre-Promotion Review - merges Diff Viewer + Merge Preview + Conflict Detection
* 5. Effective Policies - unchanged
* 6. Exceptions - unchanged (has badge)
*
* Removed from tabs:
* - Audit Log -> /evidence/audit-log?module=policy-simulation
* - History -> accessible from Shadow Mode via action
* - Batch Evaluation -> accessible from Simulation Console
*
* @sprint SPRINT_20251229_021b_FE
*/
const POLICY_SIM_TABS: readonly StellaPageTab[] = [
{ id: 'shadow', label: 'Shadow Mode', icon: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z|||M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0' },
{ id: 'simulation', label: 'Simulation Console', icon: 'M5 3l14 9-14 9V3z' },
{ id: 'coverage', label: 'Coverage', icon: 'M18 20V10|||M12 20V4|||M6 20v-6' },
{ id: 'lint', label: 'Lint', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
{ id: 'audit', label: 'Audit Log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
{ id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'testing', label: 'Test & Validate', icon: 'M5 3l14 9-14 9V3z' },
{ id: 'review', label: 'Pre-Promotion Review', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' },
{ id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'diff', label: 'Diff Viewer', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' },
{ id: 'merge', label: 'Merge Preview', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
{ id: 'history', label: 'History', icon: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0|||M12 6v6l4 2' },
{ id: 'conflicts', label: 'Conflicts', icon: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z|||M12 9v4|||M12 17h.01' },
{ id: 'batch', label: 'Batch Evaluation', icon: 'M18 20V10|||M12 20V4|||M6 20v-6' },
];
@Component({
@@ -43,18 +41,11 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
standalone: true,
imports: [
ShadowModeDashboardComponent,
SimulationConsoleComponent,
CoverageFixtureComponent,
PolicyAuditLogComponent,
PromotionGateComponent,
SimulationTestValidatePanelComponent,
SimulationPrePromotionPanelComponent,
EffectivePolicyViewerComponent,
PolicyExceptionComponent,
PolicyLintComponent,
PromotionGateComponent,
PolicyDiffViewerComponent,
PolicyMergePreviewComponent,
SimulationHistoryComponent,
ConflictDetectionComponent,
BatchEvaluationComponent,
StellaPageTabsComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -70,14 +61,14 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
@if (activeTab() === 'shadow') {
<app-shadow-mode-dashboard />
}
@if (activeTab() === 'simulation') {
<app-simulation-console />
@if (activeTab() === 'promotion') {
<app-promotion-gate />
}
@if (activeTab() === 'coverage') {
<app-coverage-fixture />
@if (activeTab() === 'testing') {
<app-simulation-test-validate-panel />
}
@if (activeTab() === 'audit') {
<app-policy-audit-log />
@if (activeTab() === 'review') {
<app-simulation-pre-promotion-panel />
}
@if (activeTab() === 'effective') {
<app-effective-policy-viewer />
@@ -85,27 +76,6 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
@if (activeTab() === 'exceptions') {
<app-policy-exception />
}
@if (activeTab() === 'lint') {
<app-policy-lint />
}
@if (activeTab() === 'promotion') {
<app-promotion-gate />
}
@if (activeTab() === 'diff') {
<app-policy-diff-viewer />
}
@if (activeTab() === 'merge') {
<app-policy-merge-preview />
}
@if (activeTab() === 'history') {
<app-simulation-history />
}
@if (activeTab() === 'conflicts') {
<app-conflict-detection />
}
@if (activeTab() === 'batch') {
<app-batch-evaluation />
}
</stella-page-tabs>
</div>
`,
@@ -131,4 +101,14 @@ export class PolicySimulationStudioComponent {
readonly activeTab = signal<string>('shadow');
readonly POLICY_SIM_TABS = POLICY_SIM_TABS;
/** Alias for spec compatibility. */
get tabs(): readonly StellaPageTab[] {
return this.POLICY_SIM_TABS;
}
/** Alias for spec compatibility. */
setActiveTab(tabId: string): void {
this.activeTab.set(tabId);
}
}

View File

@@ -2,36 +2,24 @@
* @file policy-simulation.routes.ts
* @sprint SPRINT_20251229_021b_FE
* @description Routes for Policy Simulation Studio at /policy/simulation
*
* Rationalized from 13 routes to 6 tab destinations:
* 1. Shadow Mode (default)
* 2. Promotion Gate
* 3. Test & Validate (merges console, coverage, lint)
* 4. Pre-Promotion Review (merges diff, merge, conflicts)
* 5. Effective Policies
* 6. Exceptions
*
* Legacy routes (console, coverage, lint, diff, merge, conflicts) redirect
* to the new merged tabs. Audit Log redirects to the central evidence page.
* History and Batch Evaluation remain accessible as non-tab routes.
*/
import { Routes } from '@angular/router';
import { requireAuthGuard } from '../../core/auth/auth.guard';
import { StellaOpsScopes } from '../../core/auth/scopes';
/**
* Policy Simulation Studio Routes
*
* Provides interfaces for policy testing and validation:
* - Dashboard (overview and quick actions)
* - Shadow Mode (A/B policy comparison) - MANDATORY visibility before production promotion
* - Simulation Console (run policy against test data)
* - Policy Lint (syntax and semantic validation)
* - Coverage (test coverage per rule)
* - Effective Policies (which policies apply where)
* - Audit Log (change history)
* - Policy Diff (version comparison)
* - Promotion Gate (checklist enforcement)
* - Exceptions (policy exception management)
* - Merge Preview (pack merge visualization)
*
* Promotion gates require:
* - Shadow duration: 7 days minimum
* - Coverage: 80%+ test coverage
* - Lint: Clean (no errors)
* - Compile: Success
* - Security review: Approved
* - Stakeholder approval: Signed off
*/
export const policySimulationRoutes: Routes = [
{
path: '',
@@ -42,6 +30,7 @@ export const policySimulationRoutes: Routes = [
(m) => m.SimulationDashboardComponent
),
children: [
// ── Default: Shadow Mode ──
{
path: '',
loadComponent: () =>
@@ -58,54 +47,8 @@ export const policySimulationRoutes: Routes = [
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'console',
loadComponent: () =>
import('./simulation-console.component').then(
(m) => m.SimulationConsoleComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] },
},
{
path: 'lint',
loadComponent: () =>
import('./policy-lint.component').then(
(m) => m.PolicyLintComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'coverage',
loadComponent: () =>
import('./coverage-fixture.component').then(
(m) => m.CoverageFixtureComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'effective',
loadComponent: () =>
import('./effective-policy-viewer.component').then(
(m) => m.EffectivePolicyViewerComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'audit',
loadComponent: () =>
import('./policy-audit-log.component').then(
(m) => m.PolicyAuditLogComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_AUDIT] },
},
{
path: 'diff/:policyPackId',
loadComponent: () =>
import('./policy-diff-viewer.component').then(
(m) => m.PolicyDiffViewerComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
// ── Promotion Gate ──
{
path: 'promotion',
loadComponent: () =>
@@ -114,6 +57,38 @@ export const policySimulationRoutes: Routes = [
),
data: { requiredScopes: [StellaOpsScopes.POLICY_APPROVE] },
},
// ── Test & Validate (merged: console + coverage + lint) ──
{
path: 'testing',
loadComponent: () =>
import('./simulation-test-validate-panel.component').then(
(m) => m.SimulationTestValidatePanelComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] },
},
// ── Pre-Promotion Review (merged: diff + merge + conflicts) ──
{
path: 'review',
loadComponent: () =>
import('./simulation-pre-promotion-panel.component').then(
(m) => m.SimulationPrePromotionPanelComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
// ── Effective Policies ──
{
path: 'effective',
loadComponent: () =>
import('./effective-policy-viewer.component').then(
(m) => m.EffectivePolicyViewerComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
// ── Exceptions ──
{
path: 'exceptions',
loadComponent: () =>
@@ -130,14 +105,20 @@ export const policySimulationRoutes: Routes = [
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'merge',
loadComponent: () =>
import('./policy-merge-preview.component').then(
(m) => m.PolicyMergePreviewComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
// ── Legacy redirects: merged into Test & Validate ──
{ path: 'console', redirectTo: 'testing', pathMatch: 'full' },
{ path: 'coverage', redirectTo: 'testing', pathMatch: 'full' },
{ path: 'lint', redirectTo: 'testing', pathMatch: 'full' },
// ── Legacy redirects: merged into Pre-Promotion Review ──
{ path: 'merge', redirectTo: 'review', pathMatch: 'full' },
{ path: 'conflicts', redirectTo: 'review', pathMatch: 'full' },
// ── Audit Log: redirect to central evidence page ──
{ path: 'audit', redirectTo: '/evidence/audit-log', pathMatch: 'full' },
// ── Non-tab routes (still accessible via actions) ──
{
path: 'history',
loadComponent: () =>
@@ -146,14 +127,6 @@ export const policySimulationRoutes: Routes = [
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'conflicts',
loadComponent: () =>
import('./conflict-detection.component').then(
(m) => m.ConflictDetectionComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
{
path: 'batch',
loadComponent: () =>
@@ -162,6 +135,14 @@ export const policySimulationRoutes: Routes = [
),
data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] },
},
{
path: 'diff/:policyPackId',
loadComponent: () =>
import('./policy-diff-viewer.component').then(
(m) => m.PolicyDiffViewerComponent
),
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
},
],
},
];

View File

@@ -5,11 +5,23 @@ import { RouterModule, Router } from '@angular/router';
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
import { ShadowModeStateService } from './shadow-mode-state.service';
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
import { StellaQuickLinksComponent, type StellaQuickLink } from '../../shared/components/stella-quick-links/stella-quick-links.component';
/**
* Main Policy Simulation Studio dashboard component with tabbed navigation.
* Provides access to Shadow Mode, Simulation Console, Coverage, Audit Log,
* Effective Policies, and Exceptions management.
*
* Rationalized from 9 tabs to 6:
* 1. Shadow Mode (default) - unchanged
* 2. Promotion Gate - unchanged
* 3. Test & Validate - merges Simulation Console + Coverage + Lint
* 4. Pre-Promotion Review - merges Diff Viewer + Merge Preview + Conflict Detection
* 5. Effective Policies - unchanged
* 6. Exceptions - unchanged (has badge)
*
* Removed from tabs:
* - Audit Log -> /evidence/audit-log?module=policy-simulation
* - History -> accessible from Shadow Mode via action
* - Batch Evaluation -> accessible from Simulation Console
*
* MANDATORY: Shadow mode indicator is visible on all policy views before production promotion.
*
@@ -17,19 +29,16 @@ import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/
*/
const SIMULATION_TABS: readonly StellaPageTab[] = [
{ id: 'shadow', label: 'Shadow Mode', icon: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z|||M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0' },
{ id: 'console', label: 'Simulation Console', icon: 'M5 3l14 9-14 9V3z' },
{ id: 'lint', label: 'Lint', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
{ id: 'coverage', label: 'Coverage', icon: 'M18 20V10|||M12 20V4|||M6 20v-6', badge: '72%', status: 'warn', statusHint: '72% coverage (80%+ required)' },
{ id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'audit', label: 'Audit Log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
{ id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', badge: 3 },
{ id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'merge', label: 'Merge Preview', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
{ id: 'testing', label: 'Test & Validate', icon: 'M5 3l14 9-14 9V3z' },
{ id: 'review', label: 'Pre-Promotion Review', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' },
{ id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', badge: 3 },
];
@Component({
selector: 'app-simulation-dashboard',
imports: [RouterModule, ShadowModeIndicatorComponent, StellaPageTabsComponent],
imports: [RouterModule, ShadowModeIndicatorComponent, StellaPageTabsComponent, StellaQuickLinksComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="simulation">
@@ -38,6 +47,9 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
<h1 class="simulation__title">Policy Simulation</h1>
<p class="simulation__subtitle">{{ activeSubtitle() }}</p>
</div>
<aside class="page-aside">
<stella-quick-links [links]="quickLinks" label="Quick Links" layout="strip" />
</aside>
</header>
<!-- MANDATORY: Shadow Mode Indicator - Visible on all policy views -->
@@ -192,6 +204,8 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
.simulation__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
.simulation__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
.page-aside { flex: 0 1 60%; min-width: 0; }
.simulation__shadow-banner {
margin-bottom: 1rem;
}
@@ -296,16 +310,20 @@ export class SimulationDashboardComponent implements OnInit {
protected readonly activeTab = signal<string>('shadow');
readonly quickLinks: readonly StellaQuickLink[] = [
{ label: 'Governance', route: '/ops/policy/governance', description: 'Risk budgets and compliance profiles' },
{ label: 'Policy Packs', route: '/ops/policy/packs', description: 'Author and manage policy pack rules' },
{ label: 'VEX & Exceptions', route: '/ops/policy/vex', description: 'Vulnerability exceptions and waivers' },
{ label: 'Audit Events', route: '/evidence/audit-log', description: 'Cross-module audit trail' },
];
private static readonly SUBTITLES: Record<string, string> = {
shadow: 'Compare shadow policy evaluations against active production policy.',
console: 'Run policies against test data and review evaluations.',
lint: 'Syntax and semantic validation for policy documents.',
coverage: 'Test coverage metrics per rule and policy pack.',
effective: 'Which policies apply to which environments and artifacts.',
audit: 'Simulation and policy change history.',
exceptions: 'Manage temporary policy exception waivers.',
promotion: 'Pre-promotion checklist and gate enforcement.',
merge: 'Preview pack merge results before applying.',
testing: 'Run policies against test data, check coverage, and validate syntax.',
review: 'Diff versions, preview merges, and detect conflicts before promotion.',
effective: 'Which policies apply to which environments and artifacts.',
exceptions: 'Manage temporary policy exception waivers.',
};
protected readonly activeSubtitle = computed(() =>
@@ -328,14 +346,11 @@ export class SimulationDashboardComponent implements OnInit {
private static readonly TAB_ROUTES: Record<string, string> = {
shadow: './shadow',
console: './console',
lint: './lint',
coverage: './coverage',
effective: './effective',
audit: './audit',
exceptions: './exceptions',
promotion: './promotion',
merge: './merge',
testing: './testing',
review: './review',
effective: './effective',
exceptions: './exceptions',
};
protected readonly SIMULATION_TABS: readonly StellaPageTab[] = SIMULATION_TABS;

View File

@@ -0,0 +1,128 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { PolicyDiffViewerComponent } from './policy-diff-viewer.component';
import { PolicyMergePreviewComponent } from './policy-merge-preview.component';
import { ConflictDetectionComponent } from './conflict-detection.component';
/**
* Wrapper panel that merges Diff Viewer, Merge Preview, and Conflict Detection
* into a single "Pre-Promotion Review" tab with toggle sub-sections.
*
* Replaces three separate top-level tabs with a single tab and
* toggle buttons to switch between sub-views.
*/
type ReviewSection = 'diff' | 'merge' | 'conflicts';
interface SectionOption {
id: ReviewSection;
label: string;
icon: string;
}
const SECTIONS: readonly SectionOption[] = [
{
id: 'diff',
label: 'Diff Viewer',
icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6',
},
{
id: 'merge',
label: 'Merge Preview',
icon: 'M6 3v12 M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M18 9a9 9 0 0 1-9 9',
},
{
id: 'conflicts',
label: 'Conflict Detection',
icon: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z M12 9v4 M12 17h.01',
},
] as const;
@Component({
selector: 'app-simulation-pre-promotion-panel',
standalone: true,
imports: [
PolicyDiffViewerComponent,
PolicyMergePreviewComponent,
ConflictDetectionComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="panel">
<div class="panel__toggles" role="group" aria-label="Pre-promotion review sections">
@for (section of sections; track section.id) {
<button
class="panel__toggle"
[class.panel__toggle--active]="activeSection() === section.id"
[attr.aria-pressed]="activeSection() === section.id"
(click)="activeSection.set(section.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@for (d of section.icon.split(' M'); track $index) {
<path [attr.d]="$index === 0 ? d : 'M' + d" />
}
</svg>
<span>{{ section.label }}</span>
</button>
}
</div>
<div class="panel__content">
@if (activeSection() === 'diff') {
<app-policy-diff-viewer />
}
@if (activeSection() === 'merge') {
<app-policy-merge-preview />
}
@if (activeSection() === 'conflicts') {
<app-conflict-detection />
}
</div>
</div>
`,
styles: [`
:host { display: block; }
.panel__toggles {
display: flex;
gap: 0.25rem;
padding: 0.75rem 1rem 0;
border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.06));
background: var(--color-surface-secondary, #1a1a2e);
}
.panel__toggle {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: none;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.8125rem;
font-weight: var(--font-weight-medium, 500);
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
white-space: nowrap;
}
.panel__toggle:hover {
color: var(--color-text-primary);
}
.panel__toggle--active {
color: var(--color-brand-primary, #7c5cfc);
border-bottom-color: var(--color-brand-primary, #7c5cfc);
}
.panel__content {
padding: 0;
}
`],
})
export class SimulationPrePromotionPanelComponent {
readonly sections = SECTIONS;
readonly activeSection = signal<ReviewSection>('diff');
}

View File

@@ -0,0 +1,128 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { SimulationConsoleComponent } from './simulation-console.component';
import { CoverageFixtureComponent } from './coverage-fixture.component';
import { PolicyLintComponent } from './policy-lint.component';
/**
* Wrapper panel that merges Simulation Console, Coverage, and Lint
* into a single "Test & Validate" tab with toggle sub-sections.
*
* Replaces three separate top-level tabs with a single tab and
* toggle buttons to switch between sub-views.
*/
type TestValidateSection = 'console' | 'coverage' | 'lint';
interface SectionOption {
id: TestValidateSection;
label: string;
icon: string;
}
const SECTIONS: readonly SectionOption[] = [
{
id: 'console',
label: 'Simulation Console',
icon: 'M5 3l14 9-14 9V3z',
},
{
id: 'coverage',
label: 'Coverage',
icon: 'M18 20V10 M12 20V4 M6 20v-6',
},
{
id: 'lint',
label: 'Lint',
icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8',
},
] as const;
@Component({
selector: 'app-simulation-test-validate-panel',
standalone: true,
imports: [
SimulationConsoleComponent,
CoverageFixtureComponent,
PolicyLintComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="panel">
<div class="panel__toggles" role="group" aria-label="Test and validate sections">
@for (section of sections; track section.id) {
<button
class="panel__toggle"
[class.panel__toggle--active]="activeSection() === section.id"
[attr.aria-pressed]="activeSection() === section.id"
(click)="activeSection.set(section.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@for (d of section.icon.split(' M'); track $index) {
<path [attr.d]="$index === 0 ? d : 'M' + d" />
}
</svg>
<span>{{ section.label }}</span>
</button>
}
</div>
<div class="panel__content">
@if (activeSection() === 'console') {
<app-simulation-console />
}
@if (activeSection() === 'coverage') {
<app-coverage-fixture />
}
@if (activeSection() === 'lint') {
<app-policy-lint />
}
</div>
</div>
`,
styles: [`
:host { display: block; }
.panel__toggles {
display: flex;
gap: 0.25rem;
padding: 0.75rem 1rem 0;
border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.06));
background: var(--color-surface-secondary, #1a1a2e);
}
.panel__toggle {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: none;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.8125rem;
font-weight: var(--font-weight-medium, 500);
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
white-space: nowrap;
}
.panel__toggle:hover {
color: var(--color-text-primary);
}
.panel__toggle--active {
color: var(--color-brand-primary, #7c5cfc);
border-bottom-color: var(--color-brand-primary, #7c5cfc);
}
.panel__content {
padding: 0;
}
`],
})
export class SimulationTestValidatePanelComponent {
readonly sections = SECTIONS;
readonly activeSection = signal<TestValidateSection>('console');
}

View File

@@ -6,7 +6,7 @@
[backLabel]="returnTo() ? 'Return to previous context' : null"
(backClick)="returnToSource()"
>
<a header-actions routerLink="/releases/runs" class="run-workspace__back-link">Back to release runs</a>
<a header-actions routerLink="/releases/deployments" class="run-workspace__back-link">Back to deployments</a>
</app-context-header>
<app-tabbed-nav