agentic-orchestrator 0.1.25 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentic/orchestrator/defaults/policy.defaults.yaml +1 -0
- package/agentic/orchestrator/policy.yaml +1 -0
- package/agentic/orchestrator/schemas/policy.schema.json +4 -0
- package/agentic/orchestrator/schemas/policy.user.schema.json +4 -0
- package/apps/control-plane/src/application/services/activity-monitor-service.ts +42 -0
- package/apps/control-plane/src/application/services/collision-queue-service.ts +48 -0
- package/apps/control-plane/src/application/services/cost-tracking-service.ts +80 -0
- package/apps/control-plane/src/application/services/feature-deletion-service.ts +57 -0
- package/apps/control-plane/src/application/services/feature-lifecycle-service.ts +65 -0
- package/apps/control-plane/src/application/services/feature-state-service.ts +87 -0
- package/apps/control-plane/src/application/services/gate-selection-service.ts +28 -0
- package/apps/control-plane/src/application/services/gate-service.ts +148 -0
- package/apps/control-plane/src/application/services/issue-tracker-service.ts +28 -0
- package/apps/control-plane/src/application/services/lock-service.ts +88 -0
- package/apps/control-plane/src/application/services/merge-service.ts +63 -0
- package/apps/control-plane/src/application/services/notifier-service.ts +70 -0
- package/apps/control-plane/src/application/services/patch-service.ts +95 -0
- package/apps/control-plane/src/application/services/performance-analytics-service.ts +90 -0
- package/apps/control-plane/src/application/services/plan-service.ts +184 -0
- package/apps/control-plane/src/application/services/pr-monitor-service.ts +58 -0
- package/apps/control-plane/src/application/services/qa-index-service.ts +68 -0
- package/apps/control-plane/src/application/services/reactions-service.ts +77 -0
- package/apps/control-plane/src/application/services/reporting-service.ts +79 -0
- package/apps/control-plane/src/application/services/run-lease-service.ts +79 -0
- package/apps/control-plane/src/application/tools/tool-router.ts +76 -0
- package/apps/control-plane/src/mcp/tool-runtime.ts +4 -0
- package/apps/control-plane/src/providers/providers.ts +96 -0
- package/apps/control-plane/src/providers/worker-provider-factory.ts +4 -0
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +4 -0
- package/apps/control-plane/src/supervisor/planning-wave-executor.ts +4 -0
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +4 -0
- package/apps/control-plane/src/supervisor/run-coordinator.ts +4 -0
- package/apps/control-plane/src/supervisor/runtime.ts +4 -0
- package/apps/control-plane/src/supervisor/session-orchestrator.ts +4 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +310 -1
- package/apps/control-plane/test/dashboard-client.spec.ts +136 -1
- package/apps/control-plane/test/dashboard-live-feed.spec.ts +153 -0
- package/apps/control-plane/test/dashboard-ui-utils.spec.ts +132 -0
- package/apps/control-plane/test/mcp.spec.ts +96 -0
- package/apps/control-plane/test/tool-runtime.spec.ts +68 -0
- package/config/agentic/orchestrator/policy.yaml +1 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +42 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js +42 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/collision-queue-service.d.ts +48 -0
- package/dist/apps/control-plane/application/services/collision-queue-service.js +48 -0
- package/dist/apps/control-plane/application/services/collision-queue-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +79 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js +76 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +57 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.js +57 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/feature-lifecycle-service.d.ts +64 -0
- package/dist/apps/control-plane/application/services/feature-lifecycle-service.js +61 -0
- package/dist/apps/control-plane/application/services/feature-lifecycle-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/feature-state-service.d.ts +86 -0
- package/dist/apps/control-plane/application/services/feature-state-service.js +83 -0
- package/dist/apps/control-plane/application/services/feature-state-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/gate-selection-service.d.ts +28 -0
- package/dist/apps/control-plane/application/services/gate-selection-service.js +28 -0
- package/dist/apps/control-plane/application/services/gate-selection-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/gate-service.d.ts +148 -0
- package/dist/apps/control-plane/application/services/gate-service.js +120 -0
- package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +28 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js +28 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/lock-service.d.ts +88 -0
- package/dist/apps/control-plane/application/services/lock-service.js +64 -0
- package/dist/apps/control-plane/application/services/lock-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/merge-service.d.ts +62 -0
- package/dist/apps/control-plane/application/services/merge-service.js +59 -0
- package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/notifier-service.d.ts +70 -0
- package/dist/apps/control-plane/application/services/notifier-service.js +70 -0
- package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/patch-service.d.ts +94 -0
- package/dist/apps/control-plane/application/services/patch-service.js +91 -0
- package/dist/apps/control-plane/application/services/patch-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +89 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js +86 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/plan-service.d.ts +287 -0
- package/dist/apps/control-plane/application/services/plan-service.js +207 -0
- package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +58 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js +58 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/qa-index-service.d.ts +67 -0
- package/dist/apps/control-plane/application/services/qa-index-service.js +64 -0
- package/dist/apps/control-plane/application/services/qa-index-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/reactions-service.d.ts +76 -0
- package/dist/apps/control-plane/application/services/reactions-service.js +73 -0
- package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/reporting-service.d.ts +78 -0
- package/dist/apps/control-plane/application/services/reporting-service.js +75 -0
- package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/run-lease-service.d.ts +79 -0
- package/dist/apps/control-plane/application/services/run-lease-service.js +79 -0
- package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
- package/dist/apps/control-plane/application/tools/tool-router.d.ts +76 -0
- package/dist/apps/control-plane/application/tools/tool-router.js +62 -0
- package/dist/apps/control-plane/application/tools/tool-router.js.map +1 -1
- package/dist/apps/control-plane/mcp/tool-runtime.d.ts +3 -0
- package/dist/apps/control-plane/mcp/tool-runtime.js +3 -0
- package/dist/apps/control-plane/mcp/tool-runtime.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +88 -0
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +3 -0
- package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -1
- package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +3 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js +3 -0
- package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +3 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +3 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/runtime.js +3 -0
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/session-orchestrator.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/session-orchestrator.js +3 -0
- package/dist/apps/control-plane/supervisor/session-orchestrator.js.map +1 -1
- package/package.json +1 -1
- package/packages/web-dashboard/src/app/analytics/page.tsx +451 -0
- package/packages/web-dashboard/src/app/api/analytics/route.ts +62 -0
- package/packages/web-dashboard/src/app/api/collisions/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/cost/route.ts +29 -0
- package/packages/web-dashboard/src/app/api/features/[id]/review-brief/route.ts +31 -0
- package/packages/web-dashboard/src/app/api/features/[id]/route.ts +5 -8
- package/packages/web-dashboard/src/app/api/features/[id]/test-index/route.ts +31 -0
- package/packages/web-dashboard/src/app/api/flaky/route.ts +20 -0
- package/packages/web-dashboard/src/app/api/policy/budget/route.ts +31 -0
- package/packages/web-dashboard/src/app/api/run/route.ts +162 -0
- package/packages/web-dashboard/src/app/api/status/route.ts +2 -5
- package/packages/web-dashboard/src/app/feature/[id]/page.tsx +112 -0
- package/packages/web-dashboard/src/app/page.tsx +379 -77
- package/packages/web-dashboard/src/components/agent-pipeline-stepper.tsx +100 -0
- package/packages/web-dashboard/src/components/dependency-chains.tsx +58 -0
- package/packages/web-dashboard/src/components/detail-panel.tsx +215 -5
- package/packages/web-dashboard/src/components/feature-card.tsx +5 -0
- package/packages/web-dashboard/src/components/feature-cost-panel.tsx +66 -0
- package/packages/web-dashboard/src/components/feature-list-view.tsx +336 -0
- package/packages/web-dashboard/src/components/gate-step-drilldown.tsx +92 -0
- package/packages/web-dashboard/src/components/kanban-board.tsx +3 -0
- package/packages/web-dashboard/src/components/live-event-feed.tsx +60 -0
- package/packages/web-dashboard/src/components/lock-resource-map.tsx +73 -0
- package/packages/web-dashboard/src/components/plan-revision-timeline.tsx +55 -0
- package/packages/web-dashboard/src/components/plan-risk-annotations.tsx +63 -0
- package/packages/web-dashboard/src/components/plan-scope-tree.tsx +285 -0
- package/packages/web-dashboard/src/components/qa-coverage-map.tsx +174 -0
- package/packages/web-dashboard/src/components/quick-launch-panel.tsx +70 -0
- package/packages/web-dashboard/src/components/review-brief-panel.tsx +75 -0
- package/packages/web-dashboard/src/components/run-health-panel.tsx +85 -0
- package/packages/web-dashboard/src/components/summary-bar.tsx +59 -2
- package/packages/web-dashboard/src/components/verification-signals.tsx +167 -0
- package/packages/web-dashboard/src/lib/analytics-utils.ts +3 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +510 -11
- package/packages/web-dashboard/src/lib/api-envelope.ts +48 -0
- package/packages/web-dashboard/src/lib/authz-adapter.ts +33 -0
- package/packages/web-dashboard/src/lib/dashboard-utils.ts +304 -0
- package/packages/web-dashboard/src/lib/live-feed.ts +218 -0
- package/packages/web-dashboard/src/lib/policy-reader.ts +22 -0
- package/packages/web-dashboard/src/lib/types.ts +114 -0
- package/packages/web-dashboard/src/styles/dashboard.module.css +341 -1
- package/spec-files/{outstanding → completed}/agentic_orchestrator_dashboard_advanced_ux_spec.md +1 -1
- package/spec-files/outstanding/agentic_orchestrator_evidence_integrity_doctor_spec.md +561 -0
- package/spec-files/outstanding/agentic_orchestrator_kernel_simplification_spec.md +834 -0
- package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +12 -5
- package/spec-files/outstanding/agentic_orchestrator_observability_replay_spec.md +442 -0
- package/spec-files/progress.md +174 -1
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
import { getProjectByName, readMultiProjectConfig } from '@/lib/multi-project-config.js';
|
|
5
5
|
import type {
|
|
6
|
+
AgentPipelineStatus,
|
|
7
|
+
CostSummary,
|
|
6
8
|
DashboardStatusPayload,
|
|
7
9
|
EvidenceArtifact,
|
|
8
10
|
FeatureDetail,
|
|
@@ -10,6 +12,12 @@ import type {
|
|
|
10
12
|
FeatureLogEntry,
|
|
11
13
|
FeaturesIndex,
|
|
12
14
|
FeatureSummary,
|
|
15
|
+
GateRunEvidence,
|
|
16
|
+
LockLease,
|
|
17
|
+
QaTestIndex,
|
|
18
|
+
ReviewBrief,
|
|
19
|
+
RoleStatus,
|
|
20
|
+
RuntimeSession,
|
|
13
21
|
} from '@/lib/types.js';
|
|
14
22
|
|
|
15
23
|
const AOP_ROOT = process.env.AOP_ROOT ?? process.cwd();
|
|
@@ -48,18 +56,39 @@ function isActivityState(value: unknown): value is NonNullable<FeatureSummary['a
|
|
|
48
56
|
);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
function
|
|
59
|
+
function normalizeRoleStatus(value: unknown): RoleStatus {
|
|
60
|
+
if (typeof value !== 'string') {
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
const normalized = value.trim().toLowerCase();
|
|
64
|
+
if (normalized === 'running' || normalized === 'in_progress' || normalized === 'active') {
|
|
65
|
+
return 'running';
|
|
66
|
+
}
|
|
67
|
+
if (normalized === 'blocked' || normalized === 'waiting') {
|
|
68
|
+
return 'blocked';
|
|
69
|
+
}
|
|
70
|
+
if (normalized === 'done' || normalized === 'complete' || normalized === 'completed') {
|
|
71
|
+
return 'done';
|
|
72
|
+
}
|
|
73
|
+
if (normalized === 'ready' || normalized === 'pending' || normalized === 'queued') {
|
|
74
|
+
return 'ready';
|
|
75
|
+
}
|
|
76
|
+
return 'unknown';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseRoleStatus(frontMatter: Record<string, unknown>): AgentPipelineStatus | null {
|
|
52
80
|
const raw = frontMatter.role_status;
|
|
53
81
|
if (!raw || typeof raw !== 'object') {
|
|
54
82
|
return null;
|
|
55
83
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
);
|
|
59
|
-
|
|
84
|
+
const record = raw as Record<string, unknown>;
|
|
85
|
+
const planner = normalizeRoleStatus(record.planner);
|
|
86
|
+
const builder = normalizeRoleStatus(record.builder);
|
|
87
|
+
const qa = normalizeRoleStatus(record.qa);
|
|
88
|
+
if (planner === 'unknown' && builder === 'unknown' && qa === 'unknown') {
|
|
60
89
|
return null;
|
|
61
90
|
}
|
|
62
|
-
return
|
|
91
|
+
return { planner, builder, qa };
|
|
63
92
|
}
|
|
64
93
|
|
|
65
94
|
function firstValidTimestamp(frontMatter: Record<string, unknown>): number | null {
|
|
@@ -133,6 +162,99 @@ function parseFrontMatter(raw: string): ParsedFrontMatter {
|
|
|
133
162
|
return { frontMatter, body: normalized.slice(match[0].length) };
|
|
134
163
|
}
|
|
135
164
|
|
|
165
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
166
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
170
|
+
try {
|
|
171
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
172
|
+
return JSON.parse(raw) as T;
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeRuntimeSession(value: unknown): RuntimeSession | undefined {
|
|
179
|
+
const record = asRecord(value);
|
|
180
|
+
if (!record) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
run_id: typeof record.run_id === 'string' ? record.run_id : undefined,
|
|
185
|
+
owner_instance_id:
|
|
186
|
+
typeof record.owner_instance_id === 'string' ? record.owner_instance_id : undefined,
|
|
187
|
+
provider: typeof record.provider === 'string' ? record.provider : null,
|
|
188
|
+
model: typeof record.model === 'string' ? record.model : null,
|
|
189
|
+
started_at: typeof record.started_at === 'string' ? record.started_at : null,
|
|
190
|
+
last_heartbeat_at:
|
|
191
|
+
typeof record.last_heartbeat_at === 'string' ? record.last_heartbeat_at : null,
|
|
192
|
+
lease_expires_at: typeof record.lease_expires_at === 'string' ? record.lease_expires_at : null,
|
|
193
|
+
orchestrator_epoch:
|
|
194
|
+
typeof record.orchestrator_epoch === 'number' ? record.orchestrator_epoch : undefined,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeLockMap(index: FeaturesIndex): LockLease[] {
|
|
199
|
+
const lockLeases = asRecord(index.lock_leases);
|
|
200
|
+
if (!lockLeases) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
return Object.entries(lockLeases)
|
|
205
|
+
.map(([resource, lease]) => {
|
|
206
|
+
const record = asRecord(lease);
|
|
207
|
+
if (!record) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const holder = typeof record.holder === 'string' ? record.holder : null;
|
|
211
|
+
const expiresAt =
|
|
212
|
+
typeof record.expires_at === 'string'
|
|
213
|
+
? record.expires_at
|
|
214
|
+
: typeof record.lease_expires_at === 'string'
|
|
215
|
+
? record.lease_expires_at
|
|
216
|
+
: null;
|
|
217
|
+
const parsedExpiry = expiresAt ? Date.parse(expiresAt) : Number.NaN;
|
|
218
|
+
const stale = Number.isNaN(parsedExpiry) ? false : parsedExpiry < now;
|
|
219
|
+
return {
|
|
220
|
+
resource,
|
|
221
|
+
holder,
|
|
222
|
+
expires_at: expiresAt,
|
|
223
|
+
is_stale: stale,
|
|
224
|
+
} satisfies LockLease;
|
|
225
|
+
})
|
|
226
|
+
.filter((entry): entry is LockLease => entry != null)
|
|
227
|
+
.sort((left, right) => left.resource.localeCompare(right.resource));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeDepBlocked(index: FeaturesIndex): Array<{
|
|
231
|
+
feature_id: string;
|
|
232
|
+
depends_on_unresolved: string[];
|
|
233
|
+
}> {
|
|
234
|
+
if (!Array.isArray(index.dep_blocked)) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
return index.dep_blocked
|
|
238
|
+
.map((entry) => {
|
|
239
|
+
const featureId = typeof entry?.feature_id === 'string' ? entry.feature_id : null;
|
|
240
|
+
const deps = Array.isArray(entry?.depends_on_unresolved)
|
|
241
|
+
? entry.depends_on_unresolved.filter((item): item is string => typeof item === 'string')
|
|
242
|
+
: [];
|
|
243
|
+
if (!featureId) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { feature_id: featureId, depends_on_unresolved: deps };
|
|
247
|
+
})
|
|
248
|
+
.filter(
|
|
249
|
+
(
|
|
250
|
+
entry,
|
|
251
|
+
): entry is {
|
|
252
|
+
feature_id: string;
|
|
253
|
+
depends_on_unresolved: string[];
|
|
254
|
+
} => entry != null,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
136
258
|
function normalizeFeatureSummary(
|
|
137
259
|
featureId: string,
|
|
138
260
|
frontMatter: Record<string, unknown>,
|
|
@@ -172,14 +294,47 @@ function normalizeFeatureSummary(
|
|
|
172
294
|
: undefined,
|
|
173
295
|
}
|
|
174
296
|
: undefined;
|
|
297
|
+
const roleStatus = parseRoleStatus(frontMatter);
|
|
298
|
+
const rawLocks =
|
|
299
|
+
frontMatter.locks && typeof frontMatter.locks === 'object'
|
|
300
|
+
? (frontMatter.locks as Record<string, unknown>)
|
|
301
|
+
: null;
|
|
302
|
+
const locksHeldRaw = rawLocks?.held;
|
|
303
|
+
const locksHeld = Array.isArray(locksHeldRaw)
|
|
304
|
+
? locksHeldRaw.filter((item): item is string => typeof item === 'string')
|
|
305
|
+
: Array.isArray(frontMatter.locks)
|
|
306
|
+
? frontMatter.locks.filter((item): item is string => typeof item === 'string')
|
|
307
|
+
: undefined;
|
|
308
|
+
|
|
309
|
+
const planVersionRaw = frontMatter.plan_version;
|
|
310
|
+
const planVersion =
|
|
311
|
+
typeof planVersionRaw === 'number'
|
|
312
|
+
? planVersionRaw
|
|
313
|
+
: typeof planVersionRaw === 'string'
|
|
314
|
+
? Number.parseInt(planVersionRaw, 10)
|
|
315
|
+
: undefined;
|
|
316
|
+
|
|
317
|
+
const revisionOfRaw = frontMatter.revision_of;
|
|
318
|
+
const revisionOf =
|
|
319
|
+
typeof revisionOfRaw === 'number'
|
|
320
|
+
? revisionOfRaw
|
|
321
|
+
: typeof revisionOfRaw === 'string'
|
|
322
|
+
? Number.parseInt(revisionOfRaw, 10)
|
|
323
|
+
: null;
|
|
175
324
|
return {
|
|
176
325
|
id: featureId,
|
|
177
326
|
feature_id: featureId,
|
|
178
327
|
status,
|
|
179
328
|
phase,
|
|
180
|
-
plan_version:
|
|
181
|
-
|
|
182
|
-
|
|
329
|
+
plan_version: Number.isNaN(planVersion) ? undefined : planVersion,
|
|
330
|
+
revision_of: Number.isNaN(revisionOf) ? null : revisionOf,
|
|
331
|
+
revision_reason:
|
|
332
|
+
typeof frontMatter.revision_reason === 'string' ? frontMatter.revision_reason : null,
|
|
333
|
+
role_status: roleStatus ?? undefined,
|
|
334
|
+
gate_retry_count:
|
|
335
|
+
typeof frontMatter.gate_retry_count === 'number' ? frontMatter.gate_retry_count : undefined,
|
|
336
|
+
last_retry_at: typeof frontMatter.last_retry_at === 'string' ? frontMatter.last_retry_at : null,
|
|
337
|
+
locks_held: locksHeld,
|
|
183
338
|
gates,
|
|
184
339
|
gate_profile:
|
|
185
340
|
typeof frontMatter.gate_profile === 'string' ? frontMatter.gate_profile : undefined,
|
|
@@ -234,7 +389,62 @@ export async function readFeaturesIndex(repoRoot = AOP_ROOT): Promise<FeaturesIn
|
|
|
234
389
|
const indexPath = path.join(repoRoot, '.aop', 'features', 'index.json');
|
|
235
390
|
try {
|
|
236
391
|
const raw = await readFile(indexPath, 'utf-8');
|
|
237
|
-
|
|
392
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
393
|
+
const normalized: FeaturesIndex = {
|
|
394
|
+
active: Array.isArray(parsed.active)
|
|
395
|
+
? parsed.active.filter((item): item is string => typeof item === 'string')
|
|
396
|
+
: [],
|
|
397
|
+
blocked: Array.isArray(parsed.blocked)
|
|
398
|
+
? parsed.blocked.filter((item): item is string => typeof item === 'string')
|
|
399
|
+
: [],
|
|
400
|
+
merged: Array.isArray(parsed.merged)
|
|
401
|
+
? parsed.merged.filter((item): item is string => typeof item === 'string')
|
|
402
|
+
: [],
|
|
403
|
+
blocked_queue: Array.isArray(parsed.blocked_queue)
|
|
404
|
+
? parsed.blocked_queue
|
|
405
|
+
.map((entry) => {
|
|
406
|
+
const record = asRecord(entry);
|
|
407
|
+
const featureId =
|
|
408
|
+
record && typeof record.feature_id === 'string' ? record.feature_id : null;
|
|
409
|
+
const detectedAt =
|
|
410
|
+
record && typeof record.detected_at === 'string' ? record.detected_at : null;
|
|
411
|
+
const collisionFingerprint =
|
|
412
|
+
record && typeof record.collision_fingerprint === 'string'
|
|
413
|
+
? record.collision_fingerprint
|
|
414
|
+
: null;
|
|
415
|
+
if (!featureId || !detectedAt || !collisionFingerprint) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
feature_id: featureId,
|
|
420
|
+
detected_at: detectedAt,
|
|
421
|
+
collision_fingerprint: collisionFingerprint,
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
.filter(
|
|
425
|
+
(
|
|
426
|
+
entry,
|
|
427
|
+
): entry is {
|
|
428
|
+
feature_id: string;
|
|
429
|
+
detected_at: string;
|
|
430
|
+
collision_fingerprint: string;
|
|
431
|
+
} => entry != null,
|
|
432
|
+
)
|
|
433
|
+
: [],
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const lockLeases = asRecord(parsed.lock_leases);
|
|
437
|
+
if (lockLeases) {
|
|
438
|
+
normalized.lock_leases = lockLeases as FeaturesIndex['lock_leases'];
|
|
439
|
+
}
|
|
440
|
+
if (Array.isArray(parsed.dep_blocked)) {
|
|
441
|
+
normalized.dep_blocked = parsed.dep_blocked as FeaturesIndex['dep_blocked'];
|
|
442
|
+
}
|
|
443
|
+
const runtimeSessions = normalizeRuntimeSession(parsed.runtime_sessions);
|
|
444
|
+
if (runtimeSessions) {
|
|
445
|
+
normalized.runtime_sessions = runtimeSessions;
|
|
446
|
+
}
|
|
447
|
+
return normalized;
|
|
238
448
|
} catch {
|
|
239
449
|
return { active: [], blocked: [], merged: [], blocked_queue: [] };
|
|
240
450
|
}
|
|
@@ -272,6 +482,137 @@ export async function readFeaturePlan(
|
|
|
272
482
|
}
|
|
273
483
|
}
|
|
274
484
|
|
|
485
|
+
export async function readFeatureCost(
|
|
486
|
+
featureId: string,
|
|
487
|
+
repoRoot = AOP_ROOT,
|
|
488
|
+
): Promise<CostSummary | null> {
|
|
489
|
+
const costPath = path.join(getFeatureRoot(featureId, repoRoot), 'cost.json');
|
|
490
|
+
const parsed = await readJsonFile<Record<string, unknown>>(costPath);
|
|
491
|
+
if (!parsed) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
feature_id: featureId,
|
|
496
|
+
tokens_used: typeof parsed.tokens_used === 'number' ? parsed.tokens_used : 0,
|
|
497
|
+
estimated_cost_usd:
|
|
498
|
+
typeof parsed.estimated_cost_usd === 'number' ? parsed.estimated_cost_usd : 0,
|
|
499
|
+
recorded_at:
|
|
500
|
+
typeof parsed.recorded_at === 'string'
|
|
501
|
+
? parsed.recorded_at
|
|
502
|
+
: typeof parsed.last_updated === 'string'
|
|
503
|
+
? parsed.last_updated
|
|
504
|
+
: null,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function readFeatureQaTestIndex(
|
|
509
|
+
featureId: string,
|
|
510
|
+
repoRoot = AOP_ROOT,
|
|
511
|
+
): Promise<QaTestIndex | null> {
|
|
512
|
+
const indexPath = path.join(getFeatureRoot(featureId, repoRoot), 'qa_test_index.json');
|
|
513
|
+
const parsed = await readJsonFile<Record<string, unknown>>(indexPath);
|
|
514
|
+
if (!parsed || !Array.isArray(parsed.items)) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const items = parsed.items
|
|
519
|
+
.map((item) => {
|
|
520
|
+
const record = asRecord(item);
|
|
521
|
+
if (!record || typeof record.path !== 'string' || typeof record.status !== 'string') {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const status = record.status.toLowerCase();
|
|
525
|
+
if (!['pending', 'running', 'passed', 'failed', 'waived'].includes(status)) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const normalizedStatus = status as QaTestIndex['items'][number]['status'];
|
|
529
|
+
const nextItem: QaTestIndex['items'][number] = {
|
|
530
|
+
path: record.path,
|
|
531
|
+
status: normalizedStatus,
|
|
532
|
+
required_tests: Array.isArray(record.required_tests)
|
|
533
|
+
? record.required_tests.filter((test): test is string => typeof test === 'string')
|
|
534
|
+
: [],
|
|
535
|
+
};
|
|
536
|
+
if (typeof record.last_run_at === 'string') {
|
|
537
|
+
nextItem.last_run_at = record.last_run_at;
|
|
538
|
+
}
|
|
539
|
+
return nextItem;
|
|
540
|
+
})
|
|
541
|
+
.filter((entry): entry is QaTestIndex['items'][number] => entry != null);
|
|
542
|
+
|
|
543
|
+
const version =
|
|
544
|
+
typeof parsed.version === 'number'
|
|
545
|
+
? parsed.version
|
|
546
|
+
: typeof parsed.version === 'string'
|
|
547
|
+
? Number.parseInt(parsed.version, 10)
|
|
548
|
+
: 1;
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
feature_id: typeof parsed.feature_id === 'string' ? parsed.feature_id : featureId,
|
|
552
|
+
version: Number.isNaN(version) ? 1 : version,
|
|
553
|
+
items,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export async function readFeatureReviewBrief(
|
|
558
|
+
featureId: string,
|
|
559
|
+
repoRoot = AOP_ROOT,
|
|
560
|
+
): Promise<ReviewBrief | null> {
|
|
561
|
+
const briefPath = path.join(getFeatureRoot(featureId, repoRoot), 'review_brief.json');
|
|
562
|
+
const parsed = await readJsonFile<Record<string, unknown>>(briefPath);
|
|
563
|
+
if (!parsed) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const intent =
|
|
568
|
+
typeof parsed.intent_summary === 'string'
|
|
569
|
+
? parsed.intent_summary
|
|
570
|
+
: typeof parsed.intent === 'string'
|
|
571
|
+
? parsed.intent
|
|
572
|
+
: '';
|
|
573
|
+
const scope =
|
|
574
|
+
typeof parsed.scope_summary === 'string'
|
|
575
|
+
? parsed.scope_summary
|
|
576
|
+
: typeof parsed.scope === 'string'
|
|
577
|
+
? parsed.scope
|
|
578
|
+
: '';
|
|
579
|
+
const contractRisk =
|
|
580
|
+
typeof parsed.contract_risk_summary === 'string'
|
|
581
|
+
? parsed.contract_risk_summary
|
|
582
|
+
: typeof parsed.contract_risk === 'string'
|
|
583
|
+
? parsed.contract_risk
|
|
584
|
+
: '';
|
|
585
|
+
const generatedAt =
|
|
586
|
+
typeof parsed.generated_at === 'string' ? parsed.generated_at : new Date().toISOString();
|
|
587
|
+
|
|
588
|
+
if (intent.length === 0 && scope.length === 0 && contractRisk.length === 0) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
intent_summary: intent,
|
|
594
|
+
scope_summary: scope,
|
|
595
|
+
contract_risk_summary: contractRisk,
|
|
596
|
+
feasibility_score:
|
|
597
|
+
typeof parsed.feasibility_score === 'number' ? parsed.feasibility_score : undefined,
|
|
598
|
+
feasibility_breakdown:
|
|
599
|
+
parsed.feasibility_breakdown && typeof parsed.feasibility_breakdown === 'object'
|
|
600
|
+
? (parsed.feasibility_breakdown as Record<string, number>)
|
|
601
|
+
: undefined,
|
|
602
|
+
gate_matrix:
|
|
603
|
+
parsed.gate_matrix && typeof parsed.gate_matrix === 'object'
|
|
604
|
+
? (parsed.gate_matrix as Record<string, string>)
|
|
605
|
+
: {},
|
|
606
|
+
unresolved_questions: Array.isArray(parsed.unresolved_questions)
|
|
607
|
+
? parsed.unresolved_questions.filter((item): item is string => typeof item === 'string')
|
|
608
|
+
: [],
|
|
609
|
+
evidence_refs: Array.isArray(parsed.evidence_refs)
|
|
610
|
+
? parsed.evidence_refs.filter((item): item is string => typeof item === 'string')
|
|
611
|
+
: [],
|
|
612
|
+
generated_at: generatedAt,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
275
616
|
export async function readFeatureDiff(featureId: string, repoRoot = AOP_ROOT): Promise<string> {
|
|
276
617
|
const diffPath = path.join(getFeatureRoot(featureId, repoRoot), 'evidence', 'diff.patch');
|
|
277
618
|
try {
|
|
@@ -307,6 +648,99 @@ export async function listEvidenceArtifacts(
|
|
|
307
648
|
}
|
|
308
649
|
}
|
|
309
650
|
|
|
651
|
+
async function readOutputTail(logPath: unknown, maxLines = 50): Promise<string[] | undefined> {
|
|
652
|
+
if (typeof logPath !== 'string' || logPath.length === 0) {
|
|
653
|
+
return undefined;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const raw = await readFile(logPath, 'utf-8');
|
|
657
|
+
const lines = raw.replace(/\r\n/g, '\n').split('\n');
|
|
658
|
+
return lines.slice(Math.max(0, lines.length - maxLines));
|
|
659
|
+
} catch {
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export async function readLatestGateEvidence(
|
|
665
|
+
featureId: string,
|
|
666
|
+
repoRoot = AOP_ROOT,
|
|
667
|
+
): Promise<GateRunEvidence | null> {
|
|
668
|
+
const evidenceDir = path.join(getFeatureRoot(featureId, repoRoot), 'evidence');
|
|
669
|
+
try {
|
|
670
|
+
const files = await readdir(evidenceDir);
|
|
671
|
+
const gateJsons = files.filter(
|
|
672
|
+
(name) =>
|
|
673
|
+
/^latest-(fast|full|merge)\.json$/.test(name) || /^gate-(fast|full|merge)-/.test(name),
|
|
674
|
+
);
|
|
675
|
+
if (gateJsons.length === 0) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let latest: { file: string; finishedAt: number; raw: Record<string, unknown> } | null = null;
|
|
680
|
+
for (const file of gateJsons) {
|
|
681
|
+
const fullPath = path.join(evidenceDir, file);
|
|
682
|
+
const parsed = await readJsonFile<Record<string, unknown>>(fullPath);
|
|
683
|
+
if (!parsed) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const finishedAt =
|
|
687
|
+
typeof parsed.finished_at === 'string' ? Date.parse(parsed.finished_at) : Number.NaN;
|
|
688
|
+
const statInfo = await stat(fullPath);
|
|
689
|
+
const score = Number.isNaN(finishedAt) ? statInfo.mtime.getTime() : finishedAt;
|
|
690
|
+
if (!latest || score > latest.finishedAt) {
|
|
691
|
+
latest = { file, finishedAt: score, raw: parsed };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!latest) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const rawSteps = Array.isArray(latest.raw.step_results) ? latest.raw.step_results : [];
|
|
700
|
+
const steps = await Promise.all(
|
|
701
|
+
rawSteps.map(async (step) => {
|
|
702
|
+
const record = asRecord(step) ?? {};
|
|
703
|
+
const startedAt =
|
|
704
|
+
typeof record.started_at === 'string' ? Date.parse(record.started_at) : NaN;
|
|
705
|
+
const finishedAt =
|
|
706
|
+
typeof record.finished_at === 'string' ? Date.parse(record.finished_at) : NaN;
|
|
707
|
+
const duration =
|
|
708
|
+
Number.isNaN(startedAt) || Number.isNaN(finishedAt)
|
|
709
|
+
? null
|
|
710
|
+
: Math.max(0, finishedAt - startedAt);
|
|
711
|
+
const exitCode = typeof record.exit_code === 'number' ? record.exit_code : null;
|
|
712
|
+
const timeout = record.timeout === true;
|
|
713
|
+
const status: 'pass' | 'fail' | 'unknown' =
|
|
714
|
+
exitCode == null ? 'unknown' : exitCode === 0 && !timeout ? 'pass' : 'fail';
|
|
715
|
+
return {
|
|
716
|
+
name: typeof record.name === 'string' ? record.name : 'step',
|
|
717
|
+
command: Array.isArray(record.cmd)
|
|
718
|
+
? record.cmd.filter((token): token is string => typeof token === 'string').join(' ')
|
|
719
|
+
: typeof record.command === 'string'
|
|
720
|
+
? record.command
|
|
721
|
+
: '',
|
|
722
|
+
status,
|
|
723
|
+
exit_code: exitCode,
|
|
724
|
+
duration_ms: duration,
|
|
725
|
+
log_path: typeof record.log_path === 'string' ? record.log_path : null,
|
|
726
|
+
output_tail: await readOutputTail(record.log_path),
|
|
727
|
+
};
|
|
728
|
+
}),
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
mode: typeof latest.raw.mode === 'string' ? latest.raw.mode : 'unknown',
|
|
733
|
+
profile: typeof latest.raw.profile === 'string' ? latest.raw.profile : null,
|
|
734
|
+
overall: typeof latest.raw.overall === 'string' ? latest.raw.overall : 'unknown',
|
|
735
|
+
finished_at: typeof latest.raw.finished_at === 'string' ? latest.raw.finished_at : null,
|
|
736
|
+
steps,
|
|
737
|
+
raw: latest.raw,
|
|
738
|
+
};
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
310
744
|
export async function readEvidenceArtifact(
|
|
311
745
|
featureId: string,
|
|
312
746
|
artifactName: string,
|
|
@@ -399,6 +833,49 @@ export async function readFeatureLog(featureId: string, repoRoot = AOP_ROOT): Pr
|
|
|
399
833
|
};
|
|
400
834
|
}
|
|
401
835
|
|
|
836
|
+
function buildMergeHistogram14d(features: FeatureSummary[]): number[] {
|
|
837
|
+
const days = 14;
|
|
838
|
+
const buckets = new Array<number>(days).fill(0);
|
|
839
|
+
const now = Date.now();
|
|
840
|
+
for (const feature of features) {
|
|
841
|
+
if (feature.phase !== 'merged' || typeof feature.last_updated !== 'string') {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const stamp = Date.parse(feature.last_updated);
|
|
845
|
+
if (Number.isNaN(stamp) || stamp > now) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const ageDays = Math.floor((now - stamp) / 86_400_000);
|
|
849
|
+
if (ageDays < 0 || ageDays >= days) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
buckets[days - 1 - ageDays] += 1;
|
|
853
|
+
}
|
|
854
|
+
return buckets;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function readFlakySuspects(repoRoot = AOP_ROOT): Promise<string[]> {
|
|
858
|
+
const candidates = [
|
|
859
|
+
path.join(repoRoot, '.aop', 'runtime', 'flaky.json'),
|
|
860
|
+
path.join(repoRoot, '.aop', 'analytics', 'flaky.json'),
|
|
861
|
+
];
|
|
862
|
+
|
|
863
|
+
for (const candidate of candidates) {
|
|
864
|
+
const parsed = await readJsonFile<Record<string, unknown>>(candidate);
|
|
865
|
+
if (!parsed) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (Array.isArray(parsed.suspects)) {
|
|
869
|
+
return parsed.suspects.filter((item): item is string => typeof item === 'string');
|
|
870
|
+
}
|
|
871
|
+
if (Array.isArray(parsed.tests)) {
|
|
872
|
+
return parsed.tests.filter((item): item is string => typeof item === 'string');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return [];
|
|
877
|
+
}
|
|
878
|
+
|
|
402
879
|
export async function readDashboardStatus(repoRoot = AOP_ROOT): Promise<DashboardStatusPayload> {
|
|
403
880
|
const index = await readFeaturesIndex(repoRoot);
|
|
404
881
|
const allFeatureIds = new Set<string>([
|
|
@@ -409,6 +886,7 @@ export async function readDashboardStatus(repoRoot = AOP_ROOT): Promise<Dashboar
|
|
|
409
886
|
]);
|
|
410
887
|
|
|
411
888
|
const features: FeatureSummary[] = [];
|
|
889
|
+
let totalCostUsd = 0;
|
|
412
890
|
for (const featureId of [...allFeatureIds].sort((a, b) => a.localeCompare(b))) {
|
|
413
891
|
const statePath = path.join(getFeatureRoot(featureId, repoRoot), 'state.md');
|
|
414
892
|
try {
|
|
@@ -439,9 +917,26 @@ export async function readDashboardStatus(repoRoot = AOP_ROOT): Promise<Dashboar
|
|
|
439
917
|
pr: null,
|
|
440
918
|
});
|
|
441
919
|
}
|
|
920
|
+
|
|
921
|
+
const cost = await readFeatureCost(featureId, repoRoot);
|
|
922
|
+
if (cost) {
|
|
923
|
+
totalCostUsd += cost.estimated_cost_usd;
|
|
924
|
+
}
|
|
442
925
|
}
|
|
443
926
|
|
|
444
|
-
|
|
927
|
+
const depBlocked = normalizeDepBlocked(index);
|
|
928
|
+
return {
|
|
929
|
+
index,
|
|
930
|
+
features,
|
|
931
|
+
runtime: index.runtime_sessions,
|
|
932
|
+
lock_map: normalizeLockMap(index),
|
|
933
|
+
dep_blocked: depBlocked,
|
|
934
|
+
flaky_suspects: await readFlakySuspects(repoRoot),
|
|
935
|
+
metrics: {
|
|
936
|
+
total_cost_today_usd: Number(totalCostUsd.toFixed(4)),
|
|
937
|
+
merge_histogram_14d: buildMergeHistogram14d(features),
|
|
938
|
+
},
|
|
939
|
+
};
|
|
445
940
|
}
|
|
446
941
|
|
|
447
942
|
export async function readFeatureDetail(
|
|
@@ -457,5 +952,9 @@ export async function readFeatureDetail(
|
|
|
457
952
|
plan: await readFeaturePlan(featureId, repoRoot),
|
|
458
953
|
diff: await readFeatureDiff(featureId, repoRoot),
|
|
459
954
|
evidence: await listEvidenceArtifacts(featureId, repoRoot),
|
|
955
|
+
gate_evidence: await readLatestGateEvidence(featureId, repoRoot),
|
|
956
|
+
cost: await readFeatureCost(featureId, repoRoot),
|
|
957
|
+
qa_test_index: await readFeatureQaTestIndex(featureId, repoRoot),
|
|
958
|
+
review_brief: await readFeatureReviewBrief(featureId, repoRoot),
|
|
460
959
|
};
|
|
461
960
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface ApiSuccessEnvelope<TData> {
|
|
2
|
+
ok: true;
|
|
3
|
+
data: TData;
|
|
4
|
+
meta: {
|
|
5
|
+
stale: boolean;
|
|
6
|
+
source: 'tool' | 'artifact' | 'derived';
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ApiErrorEnvelope {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: {
|
|
13
|
+
code: string;
|
|
14
|
+
message: string;
|
|
15
|
+
retryable: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ApiEnvelope<TData> = ApiSuccessEnvelope<TData> | ApiErrorEnvelope;
|
|
20
|
+
|
|
21
|
+
export function okEnvelope<TData>(
|
|
22
|
+
data: TData,
|
|
23
|
+
options: { stale?: boolean; source?: 'tool' | 'artifact' | 'derived' } = {},
|
|
24
|
+
): ApiSuccessEnvelope<TData> {
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
data,
|
|
28
|
+
meta: {
|
|
29
|
+
stale: options.stale ?? false,
|
|
30
|
+
source: options.source ?? 'artifact',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function errorEnvelope(
|
|
36
|
+
code: string,
|
|
37
|
+
message: string,
|
|
38
|
+
options: { retryable?: boolean } = {},
|
|
39
|
+
): ApiErrorEnvelope {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code,
|
|
44
|
+
message,
|
|
45
|
+
retryable: options.retryable ?? false,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface DashboardAuthorizationContext {
|
|
2
|
+
action: string;
|
|
3
|
+
project: string | null;
|
|
4
|
+
request: Request;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface DashboardAuthorizationResult {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
code?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DashboardAuthorizationAdapter {
|
|
14
|
+
authorize(context: DashboardAuthorizationContext): Promise<DashboardAuthorizationResult>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class AllowAllAuthorizationAdapter implements DashboardAuthorizationAdapter {
|
|
18
|
+
async authorize(): Promise<DashboardAuthorizationResult> {
|
|
19
|
+
return { allowed: true };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let adapter: DashboardAuthorizationAdapter = new AllowAllAuthorizationAdapter();
|
|
24
|
+
|
|
25
|
+
export function setDashboardAuthorizationAdapter(nextAdapter: DashboardAuthorizationAdapter): void {
|
|
26
|
+
adapter = nextAdapter;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function authorizeDashboardAction(
|
|
30
|
+
context: DashboardAuthorizationContext,
|
|
31
|
+
): Promise<DashboardAuthorizationResult> {
|
|
32
|
+
return await adapter.authorize(context);
|
|
33
|
+
}
|