diff --git a/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts
index f486254ec..b9559e65f 100644
--- a/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts
+++ b/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts
@@ -1,79 +1,18 @@
-import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
- DestroyRef,
- computed,
- inject,
- signal,
} from '@angular/core';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
- ActivatedRoute,
- ActivatedRouteSnapshot,
- NavigationEnd,
- Router,
- RouterLink,
RouterOutlet,
} from '@angular/router';
-import { filter, startWith } from 'rxjs';
-
-import {
- ContextHeaderComponent,
-} from '../../shared/ui';
-import {
- buildContextRouteParams,
-} from '../../shared/ui/context-route-state/context-route-state';
-
-type DecisioningContextKind =
- | 'global'
- | 'pack'
- | 'release'
- | 'approval'
- | 'workflow'
- | 'evidence';
-
-interface DecisioningShellState {
- readonly kind: DecisioningContextKind;
- readonly packId: string | null;
- readonly releaseId: string | null;
- readonly approvalId: string | null;
- readonly environment: string | null;
- readonly artifact: string | null;
- readonly returnTo: string | null;
- readonly workflowId: string | null;
- readonly evidenceId: string | null;
-}
@Component({
selector: 'app-policy-decisioning-shell',
imports: [
- CommonModule,
- RouterLink,
RouterOutlet,
- ContextHeaderComponent,
],
template: `
`,
@@ -85,245 +24,9 @@ interface DecisioningShellState {
.policy-decisioning-shell {
display: grid;
- gap: 1rem;
- padding: 1.25rem;
- }
-
- .shell-action {
- display: inline-flex;
- align-items: center;
- border: 1px solid var(--color-border-primary);
- border-radius: 0.75rem;
- background: var(--color-surface-secondary);
- color: var(--color-text-primary);
- font-weight: 600;
- padding: 0.6rem 0.9rem;
- text-decoration: none;
+ gap: 0;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class PolicyDecisioningShellComponent {
- private readonly route = inject(ActivatedRoute);
- private readonly router = inject(Router);
- private readonly destroyRef = inject(DestroyRef);
-
- readonly shellState = signal(this.readShellState());
-
- readonly headerTitle = computed(() => {
- const state = this.shellState();
-
- switch (state.kind) {
- case 'pack':
- return `Policy Pack ${state.packId}`;
- case 'release':
- return `Release ${state.releaseId} Decisioning`;
- case 'approval':
- return `Approval ${state.approvalId} Decisioning`;
- case 'workflow':
- return `Workflow ${state.workflowId} Decisioning`;
- case 'evidence':
- return `Evidence ${state.evidenceId} Decisioning`;
- default:
- return 'Policy Decisioning Studio';
- }
- });
-
- readonly headerSubtitle = computed(() => {
- const state = this.shellState();
-
- switch (state.kind) {
- case 'pack':
- return 'Author, validate, approve, and explain a pack from the canonical policy shell.';
- case 'release':
- case 'approval':
- return 'Review gates, simulation, VEX, and exceptions for a live release decision without leaving the shared shell.';
- case 'workflow':
- return 'Keep workflow gate logic and release policy inspection in one decisioning workspace.';
- case 'evidence':
- return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.';
- default:
- return 'Policy packs, governance, simulation, VEX, exceptions, and audit.';
- }
- });
-
- readonly headerNote = computed(() => {
- const state = this.shellState();
-
- switch (state.kind) {
- case 'release':
- return 'Release-context mode keeps gate review and operator actions inside the shared policy shell.';
- case 'approval':
- return 'Approval-context mode preserves policy and VEX actions while allowing a direct return to the approval flow.';
- case 'pack':
- return 'Pack mode exposes authoring, YAML, rules, approvals, simulation, and explainability for the selected pack.';
- case 'workflow':
- return 'Workflow context is carried as a non-owning deep link for release pipeline editing.';
- case 'evidence':
- return 'Evidence context is non-owning: Decisioning Studio stays focused on gates, policy, and VEX actions.';
- default:
- return null;
- }
- });
-
- readonly headerChips = computed(() => {
- const state = this.shellState();
- const chips: string[] = [];
-
- if (state.packId) {
- chips.push(`Pack ${state.packId}`);
- }
- if (state.releaseId) {
- chips.push(`Release ${state.releaseId}`);
- }
- if (state.approvalId) {
- chips.push(`Approval ${state.approvalId}`);
- }
- if (state.environment) {
- chips.push(`Env ${state.environment}`);
- }
- if (state.artifact) {
- chips.push(`Artifact ${truncateValue(state.artifact)}`);
- }
- if (state.workflowId) {
- chips.push(`Workflow ${state.workflowId}`);
- }
- if (state.evidenceId) {
- chips.push(`Evidence ${state.evidenceId}`);
- }
-
- return chips;
- });
-
- constructor() {
- this.router.events
- .pipe(
- filter((event): event is NavigationEnd => event instanceof NavigationEnd),
- startWith(null),
- takeUntilDestroyed(this.destroyRef),
- )
- .subscribe(() => {
- this.shellState.set(this.readShellState());
- });
- }
-
- overviewRoute(): readonly unknown[] {
- return ['/ops/policy/overview'];
- }
-
- gatesRoute(): readonly unknown[] {
- const state = this.shellState();
-
- if (state.approvalId) {
- return ['/ops/policy/gates/approvals', state.approvalId];
- }
- if (state.releaseId) {
- return ['/ops/policy/gates/releases', state.releaseId];
- }
- if (state.environment) {
- return ['/ops/policy/gates/environments', state.environment];
- }
-
- return ['/ops/policy/gates'];
- }
-
- contextQueryParams(): Record {
- const state = this.shellState();
-
- return buildContextRouteParams({
- releaseId: state.releaseId,
- approvalId: state.approvalId,
- environment: state.environment,
- artifact: state.artifact,
- returnTo: state.returnTo,
- workflowId: state.workflowId,
- evidenceId: state.evidenceId,
- });
- }
-
- returnToSource(): void {
- const returnTo = this.shellState().returnTo;
- if (!returnTo) {
- return;
- }
-
- void this.router.navigateByUrl(returnTo);
- }
-
- private readShellState(): DecisioningShellState {
- const params = collectRouteParams(this.route.snapshot.root);
- const queryParams = this.route.snapshot.root.queryParams ?? {};
- const releaseId = coerceString(params['releaseId']) ?? coerceString(queryParams['releaseId']);
- const approvalId = coerceString(params['approvalId']) ?? coerceString(queryParams['approvalId']);
- const packId = coerceString(params['packId']) ?? coerceString(queryParams['packId']);
- const environment = coerceString(params['environment']) ?? coerceString(queryParams['environment']);
- const artifact =
- coerceString(queryParams['artifact'])
- ?? coerceString(queryParams['artifactDigest'])
- ?? coerceString(queryParams['bundleDigest']);
- const workflowId = coerceString(queryParams['workflowId']);
- const evidenceId =
- coerceString(queryParams['evidenceId'])
- ?? coerceString(queryParams['packetId']);
-
- let kind: DecisioningContextKind = 'global';
-
- if (approvalId) {
- kind = 'approval';
- } else if (releaseId) {
- kind = 'release';
- } else if (packId) {
- kind = 'pack';
- } else if (workflowId) {
- kind = 'workflow';
- } else if (evidenceId) {
- kind = 'evidence';
- }
-
- return {
- kind,
- packId,
- releaseId,
- approvalId,
- environment,
- artifact,
- returnTo: coerceString(queryParams['returnTo']),
- workflowId,
- evidenceId,
- };
- }
-}
-
-function collectRouteParams(snapshot: ActivatedRouteSnapshot | null): Record {
- const params: Record = {};
-
- if (!snapshot) {
- return params;
- }
-
- const stack: ActivatedRouteSnapshot[] = [snapshot];
- while (stack.length > 0) {
- const current = stack.pop()!;
- for (const [key, value] of Object.entries(current.params ?? {})) {
- if (typeof value === 'string' && value.length > 0) {
- params[key] = value;
- }
- }
- stack.push(...current.children);
- }
-
- return params;
-}
-
-function coerceString(value: unknown): string | null {
- if (typeof value !== 'string') {
- return null;
- }
-
- const trimmed = value.trim();
- return trimmed.length > 0 ? trimmed : null;
-}
-
-function truncateValue(value: string): string {
- return value.length > 20 ? `${value.slice(0, 12)}...${value.slice(-6)}` : value;
-}
+export class PolicyDecisioningShellComponent {}