@swarmclawai/swarmclaw 1.9.12 → 1.9.13
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/README.md +9 -0
- package/package.json +2 -2
- package/src/app/api/quality/architecture-health/route.ts +16 -0
- package/src/app/api/quality/release-readiness/route.ts +6 -1
- package/src/cli/index.js +1 -0
- package/src/components/quality/quality-workspace.tsx +155 -1
- package/src/lib/quality/architecture-health.test.ts +79 -0
- package/src/lib/quality/architecture-health.ts +451 -0
- package/src/lib/quality/release-readiness.test.ts +13 -0
- package/src/lib/quality/release-readiness.ts +36 -0
package/README.md
CHANGED
|
@@ -399,6 +399,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.9.13 Highlights
|
|
403
|
+
|
|
404
|
+
Architecture health release: SwarmClaw now turns runtime ownership, dispatch, memory, startup, and quality evidence into a scored operator report.
|
|
405
|
+
|
|
406
|
+
- **Architecture Health report.** `/api/quality/architecture-health` returns a structured inventory of runtime domains, surfaces, owners, guardrails, tests, score, risks, warnings, and next actions.
|
|
407
|
+
- **Quality Center visibility.** `/quality` now shows a Runtime Ownership Map beside release readiness so operators can inspect dispatch, memory, startup, and quality coverage before shipping.
|
|
408
|
+
- **Release gate integration.** Release readiness includes architecture health when scoring the ship gate report, blocking or warning when ownership evidence is incomplete.
|
|
409
|
+
- **CLI access.** `swarmclaw operations architecture-health` exposes the same report for automation and release scripts.
|
|
410
|
+
|
|
402
411
|
### v1.9.12 Highlights
|
|
403
412
|
|
|
404
413
|
Local file-queue connector release: operators can bridge SwarmClaw to filesystem inbox, outbox, archive, and error folders without a hosted message bus.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.13",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
90
90
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { buildArchitectureHealthReport } from '@/lib/quality/architecture-health'
|
|
3
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
try {
|
|
9
|
+
return NextResponse.json(buildArchitectureHealthReport())
|
|
10
|
+
} catch (err: unknown) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: errorMessage(err) },
|
|
13
|
+
{ status: 500 },
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { evaluateEvalGate } from '@/lib/server/eval/baseline'
|
|
3
3
|
import { getOperationPulse, normalizeOperationPulseRange } from '@/lib/server/operations/operation-pulse'
|
|
4
|
+
import { buildArchitectureHealthReport } from '@/lib/quality/architecture-health'
|
|
4
5
|
import { buildReleaseReadinessReport } from '@/lib/quality/release-readiness'
|
|
5
6
|
import { errorMessage } from '@/lib/shared-utils'
|
|
6
7
|
|
|
@@ -28,7 +29,11 @@ export async function GET(req: Request) {
|
|
|
28
29
|
})
|
|
29
30
|
: null
|
|
30
31
|
|
|
31
|
-
return NextResponse.json(buildReleaseReadinessReport({
|
|
32
|
+
return NextResponse.json(buildReleaseReadinessReport({
|
|
33
|
+
pulse,
|
|
34
|
+
evalGate,
|
|
35
|
+
architectureHealth: buildArchitectureHealthReport(),
|
|
36
|
+
}))
|
|
32
37
|
} catch (err: unknown) {
|
|
33
38
|
return NextResponse.json(
|
|
34
39
|
{ error: errorMessage(err) },
|
package/src/cli/index.js
CHANGED
|
@@ -211,6 +211,7 @@ const COMMAND_GROUPS = [
|
|
|
211
211
|
commands: [
|
|
212
212
|
cmd('pulse', 'GET', '/operations/pulse', 'Get Operations Pulse summary (use --query range=24h or --query range=7d)'),
|
|
213
213
|
cmd('readiness', 'GET', '/quality/release-readiness', 'Get release readiness report (use --query agentId=... and --query suite=core for eval gate coverage)'),
|
|
214
|
+
cmd('architecture-health', 'GET', '/quality/architecture-health', 'Get architecture health inventory and drift report'),
|
|
214
215
|
],
|
|
215
216
|
},
|
|
216
217
|
{
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
summarizeEvalRuns,
|
|
16
16
|
summarizeRunHealth,
|
|
17
17
|
} from '@/lib/quality/quality-summary'
|
|
18
|
+
import type { ArchitectureHealthReport, ArchitectureHealthStatus } from '@/lib/quality/architecture-health'
|
|
18
19
|
import type { ReleaseReadinessReport, ReleaseReadinessStatus } from '@/lib/quality/release-readiness'
|
|
19
20
|
import { cn } from '@/lib/utils'
|
|
20
21
|
import { useAppStore } from '@/stores/use-app-store'
|
|
@@ -142,6 +143,18 @@ function readinessScoreTone(status: ReleaseReadinessStatus): string {
|
|
|
142
143
|
return 'text-rose-300'
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
function architectureStatusClass(status: ArchitectureHealthStatus): string {
|
|
147
|
+
if (status === 'healthy') return 'border-emerald-500/25 bg-emerald-500/10 text-emerald-200'
|
|
148
|
+
if (status === 'watch') return 'border-amber-500/25 bg-amber-500/10 text-amber-200'
|
|
149
|
+
return 'border-rose-500/25 bg-rose-500/10 text-rose-200'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function architectureScoreTone(status: ArchitectureHealthStatus): string {
|
|
153
|
+
if (status === 'healthy') return 'text-emerald-300'
|
|
154
|
+
if (status === 'watch') return 'text-amber-300'
|
|
155
|
+
return 'text-rose-300'
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
function ReleaseReadinessPanel({
|
|
146
159
|
report,
|
|
147
160
|
loading,
|
|
@@ -254,6 +267,118 @@ function ReleaseReadinessPanel({
|
|
|
254
267
|
)
|
|
255
268
|
}
|
|
256
269
|
|
|
270
|
+
function ArchitectureHealthPanel({
|
|
271
|
+
report,
|
|
272
|
+
loading,
|
|
273
|
+
onRefresh,
|
|
274
|
+
onOpenHref,
|
|
275
|
+
}: {
|
|
276
|
+
report: ArchitectureHealthReport | null
|
|
277
|
+
loading: boolean
|
|
278
|
+
onRefresh: () => void
|
|
279
|
+
onOpenHref: (href: string) => void
|
|
280
|
+
}) {
|
|
281
|
+
return (
|
|
282
|
+
<section className="rounded-[16px] border border-white/[0.06] bg-white/[0.025] p-4">
|
|
283
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
284
|
+
<div>
|
|
285
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-accent-bright/70">Architecture Health</div>
|
|
286
|
+
<h2 className="mt-1 font-display text-[17px] font-700 text-text">Runtime ownership map</h2>
|
|
287
|
+
<p className="mt-1 max-w-[680px] text-[12px] leading-relaxed text-text-3/65">
|
|
288
|
+
Inventories dispatch, memory, startup, and quality surfaces with owners, guardrails, and test evidence.
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
onClick={onRefresh}
|
|
294
|
+
disabled={loading}
|
|
295
|
+
className="shrink-0 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-800 text-text-2 transition-colors hover:bg-white/[0.08] disabled:opacity-40"
|
|
296
|
+
>
|
|
297
|
+
{loading ? 'Checking' : 'Refresh map'}
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{!report ? (
|
|
302
|
+
<div className="mt-4 rounded-[12px] border border-dashed border-white/[0.08] bg-white/[0.02] px-4 py-5 text-[12px] text-text-3/65">
|
|
303
|
+
{loading ? 'Building architecture health report...' : 'No architecture health report is available yet.'}
|
|
304
|
+
</div>
|
|
305
|
+
) : (
|
|
306
|
+
<div className="mt-4 grid gap-4 xl:grid-cols-[260px_1fr]">
|
|
307
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.025] p-4">
|
|
308
|
+
<span className={cn('inline-flex rounded-full border px-2.5 py-1 text-[10px] font-800 uppercase tracking-[0.1em]', architectureStatusClass(report.status))}>
|
|
309
|
+
{report.status}
|
|
310
|
+
</span>
|
|
311
|
+
<div className={cn('mt-4 font-display text-[42px] font-700 tracking-[-0.04em]', architectureScoreTone(report.status))}>{report.score}</div>
|
|
312
|
+
<div className="mt-1 text-[12px] text-text-3/65">health score</div>
|
|
313
|
+
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
314
|
+
<div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
|
|
315
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Surfaces</div>
|
|
316
|
+
<div className="mt-1 text-[18px] font-800 text-text">{report.surfaceCount}</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="rounded-[10px] bg-white/[0.035] px-3 py-2">
|
|
319
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/50">Guardrails</div>
|
|
320
|
+
<div className="mt-1 text-[18px] font-800 text-text">{report.guardrailCount}</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div className="grid gap-3 lg:grid-cols-2">
|
|
326
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
|
|
327
|
+
<div className="text-[12px] font-800 text-text">Domains</div>
|
|
328
|
+
<div className="mt-3 grid gap-2">
|
|
329
|
+
{report.domains.map((domain) => (
|
|
330
|
+
<div key={domain.id} className="rounded-[10px] border border-white/[0.06] bg-white/[0.025] px-3 py-2">
|
|
331
|
+
<div className="flex items-center justify-between gap-2">
|
|
332
|
+
<div className="text-[12px] font-800 text-text">{domain.title}</div>
|
|
333
|
+
<span className={cn('rounded-full border px-2 py-0.5 text-[9px] font-800 uppercase tracking-[0.08em]', architectureStatusClass(domain.status))}>
|
|
334
|
+
{domain.status}
|
|
335
|
+
</span>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="mt-1 text-[11px] leading-relaxed text-text-3/65">{domain.surfaces.length} surfaces, {domain.testPaths.length} evidence paths</div>
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] p-3">
|
|
344
|
+
<div className="text-[12px] font-800 text-text">Checks</div>
|
|
345
|
+
<div className="mt-3 flex flex-col gap-2">
|
|
346
|
+
{report.nextActions.length === 0 ? (
|
|
347
|
+
report.checks.filter((check) => check.status === 'healthy').slice(0, 4).map((check) => (
|
|
348
|
+
<button
|
|
349
|
+
key={check.code}
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={() => check.href && onOpenHref(check.href)}
|
|
352
|
+
className="rounded-[10px] border border-emerald-500/20 bg-emerald-500/[0.05] px-3 py-2 text-left text-emerald-200 transition-colors hover:bg-emerald-500/[0.08]"
|
|
353
|
+
>
|
|
354
|
+
<div className="text-[11px] font-800 uppercase tracking-[0.08em]">{check.status}</div>
|
|
355
|
+
<div className="mt-1 text-[12px] font-700 text-text">{check.title}</div>
|
|
356
|
+
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{check.summary}</div>
|
|
357
|
+
</button>
|
|
358
|
+
))
|
|
359
|
+
) : (
|
|
360
|
+
report.nextActions.slice(0, 5).map((action) => (
|
|
361
|
+
<button
|
|
362
|
+
key={action.id}
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => onOpenHref(action.href)}
|
|
365
|
+
className={cn('rounded-[10px] border px-3 py-2 text-left transition-colors hover:bg-white/[0.08]', architectureStatusClass(action.severity))}
|
|
366
|
+
>
|
|
367
|
+
<div className="text-[11px] font-800 uppercase tracking-[0.08em]">{action.severity}</div>
|
|
368
|
+
<div className="mt-1 text-[12px] font-700 text-text">{action.title}</div>
|
|
369
|
+
<div className="mt-0.5 text-[11px] leading-relaxed text-text-3/70">{action.summary}</div>
|
|
370
|
+
</button>
|
|
371
|
+
))
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</section>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
257
382
|
function EvalEnvironmentPanel({ plan, loading, onRefresh }: {
|
|
258
383
|
plan: EvalEnvironmentPlan | null
|
|
259
384
|
loading: boolean
|
|
@@ -471,6 +596,8 @@ export function QualityWorkspace() {
|
|
|
471
596
|
const [evalBaselineBusy, setEvalBaselineBusy] = useState(false)
|
|
472
597
|
const [releaseReadiness, setReleaseReadiness] = useState<ReleaseReadinessReport | null>(null)
|
|
473
598
|
const [releaseReadinessLoading, setReleaseReadinessLoading] = useState(false)
|
|
599
|
+
const [architectureHealth, setArchitectureHealth] = useState<ArchitectureHealthReport | null>(null)
|
|
600
|
+
const [architectureHealthLoading, setArchitectureHealthLoading] = useState(false)
|
|
474
601
|
const [approvalBusy, setApprovalBusy] = useState<string | null>(null)
|
|
475
602
|
|
|
476
603
|
useEffect(() => {
|
|
@@ -578,10 +705,27 @@ export function QualityWorkspace() {
|
|
|
578
705
|
}
|
|
579
706
|
}, [evalGateScope, selectedAgentId, selectedScenarioId, selectedSuite])
|
|
580
707
|
|
|
708
|
+
const loadArchitectureHealth = useCallback(async () => {
|
|
709
|
+
setArchitectureHealthLoading(true)
|
|
710
|
+
try {
|
|
711
|
+
const report = await api<ArchitectureHealthReport>('GET', '/quality/architecture-health')
|
|
712
|
+
setArchitectureHealth(report)
|
|
713
|
+
} catch (err) {
|
|
714
|
+
setArchitectureHealth(null)
|
|
715
|
+
toast.error(err instanceof Error ? err.message : 'Unable to check architecture health')
|
|
716
|
+
} finally {
|
|
717
|
+
setArchitectureHealthLoading(false)
|
|
718
|
+
}
|
|
719
|
+
}, [])
|
|
720
|
+
|
|
581
721
|
useEffect(() => {
|
|
582
722
|
void loadQualityData()
|
|
583
723
|
}, [loadQualityData])
|
|
584
724
|
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
void loadArchitectureHealth()
|
|
727
|
+
}, [loadArchitectureHealth])
|
|
728
|
+
|
|
585
729
|
useWs('runs', () => { void loadQualityData({ silent: true }) }, 5000)
|
|
586
730
|
|
|
587
731
|
useEffect(() => {
|
|
@@ -746,7 +890,11 @@ export function QualityWorkspace() {
|
|
|
746
890
|
{refreshing && <span className="text-[11px] text-text-3/60">Refreshing...</span>}
|
|
747
891
|
<button
|
|
748
892
|
type="button"
|
|
749
|
-
onClick={() =>
|
|
893
|
+
onClick={() => {
|
|
894
|
+
void loadQualityData({ silent: true })
|
|
895
|
+
void loadArchitectureHealth()
|
|
896
|
+
void loadReleaseReadiness()
|
|
897
|
+
}}
|
|
750
898
|
className="inline-flex items-center gap-2 rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-700 text-text-2 transition-colors hover:bg-white/[0.08]"
|
|
751
899
|
>
|
|
752
900
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
@@ -790,6 +938,12 @@ export function QualityWorkspace() {
|
|
|
790
938
|
onRefresh={() => void loadReleaseReadiness()}
|
|
791
939
|
onOpenHref={(href) => router.push(href)}
|
|
792
940
|
/>
|
|
941
|
+
<ArchitectureHealthPanel
|
|
942
|
+
report={architectureHealth}
|
|
943
|
+
loading={architectureHealthLoading}
|
|
944
|
+
onRefresh={() => void loadArchitectureHealth()}
|
|
945
|
+
onOpenHref={(href) => router.push(href)}
|
|
946
|
+
/>
|
|
793
947
|
|
|
794
948
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
795
949
|
<StatTile
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildArchitectureHealthReport,
|
|
6
|
+
DEFAULT_ARCHITECTURE_HEALTH_INVENTORY,
|
|
7
|
+
} from './architecture-health'
|
|
8
|
+
|
|
9
|
+
describe('architecture health report', () => {
|
|
10
|
+
it('summarizes the default runtime architecture inventory', () => {
|
|
11
|
+
const report = buildArchitectureHealthReport({
|
|
12
|
+
generatedAt: 100_000,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
assert.equal(report.generatedAt, 100_000)
|
|
16
|
+
assert.equal(report.status, 'healthy')
|
|
17
|
+
assert.equal(report.score, 100)
|
|
18
|
+
assert.equal(report.domainCount, DEFAULT_ARCHITECTURE_HEALTH_INVENTORY.length)
|
|
19
|
+
assert.ok(report.surfaceCount >= 10)
|
|
20
|
+
assert.ok(report.guardrailCount >= 8)
|
|
21
|
+
assert.ok(report.checks.some((check) => check.code === 'dispatch_guardrail_coverage'))
|
|
22
|
+
assert.ok(report.checks.some((check) => check.code === 'memory_authority'))
|
|
23
|
+
assert.ok(report.checks.some((check) => check.code === 'startup_surface_inventory'))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('warns when a domain has an unguarded surface', () => {
|
|
27
|
+
const report = buildArchitectureHealthReport({
|
|
28
|
+
generatedAt: 100_000,
|
|
29
|
+
inventory: [{
|
|
30
|
+
id: 'dispatch',
|
|
31
|
+
title: 'Dispatch',
|
|
32
|
+
summary: 'Test dispatch surface',
|
|
33
|
+
owner: 'runtime',
|
|
34
|
+
surfaces: [{
|
|
35
|
+
id: 'direct',
|
|
36
|
+
title: 'Direct run',
|
|
37
|
+
kind: 'dispatch',
|
|
38
|
+
path: 'src/lib/server/test.ts',
|
|
39
|
+
description: 'A dispatch path without a guardrail.',
|
|
40
|
+
guardrails: [],
|
|
41
|
+
evidence: ['No policy attached'],
|
|
42
|
+
}],
|
|
43
|
+
testPaths: ['src/lib/server/test.test.ts'],
|
|
44
|
+
}],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
assert.equal(report.status, 'watch')
|
|
48
|
+
assert.ok(report.score < 100)
|
|
49
|
+
assert.equal(report.warningCount, 1)
|
|
50
|
+
assert.ok(report.checks.some((check) => check.code === 'dispatch_unguarded_surface'))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('marks missing test coverage as an architecture risk', () => {
|
|
54
|
+
const report = buildArchitectureHealthReport({
|
|
55
|
+
generatedAt: 100_000,
|
|
56
|
+
inventory: [{
|
|
57
|
+
id: 'startup',
|
|
58
|
+
title: 'Startup',
|
|
59
|
+
summary: 'Startup entry points',
|
|
60
|
+
owner: 'runtime',
|
|
61
|
+
surfaces: [{
|
|
62
|
+
id: 'cli',
|
|
63
|
+
title: 'CLI',
|
|
64
|
+
kind: 'startup',
|
|
65
|
+
path: 'src/cli/index.js',
|
|
66
|
+
description: 'CLI startup surface.',
|
|
67
|
+
guardrails: ['route coverage'],
|
|
68
|
+
evidence: ['CLI starts the server'],
|
|
69
|
+
}],
|
|
70
|
+
testPaths: [],
|
|
71
|
+
}],
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
assert.equal(report.status, 'risk')
|
|
75
|
+
assert.ok(report.score <= 70)
|
|
76
|
+
assert.equal(report.riskCount, 1)
|
|
77
|
+
assert.ok(report.checks.some((check) => check.code === 'startup_missing_tests'))
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
export type ArchitectureHealthStatus = 'healthy' | 'watch' | 'risk'
|
|
2
|
+
|
|
3
|
+
export type ArchitectureSurfaceKind =
|
|
4
|
+
| 'dispatch'
|
|
5
|
+
| 'memory'
|
|
6
|
+
| 'startup'
|
|
7
|
+
| 'quality'
|
|
8
|
+
|
|
9
|
+
export interface ArchitectureHealthSurface {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
kind: ArchitectureSurfaceKind
|
|
13
|
+
path: string
|
|
14
|
+
description: string
|
|
15
|
+
guardrails: string[]
|
|
16
|
+
evidence: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ArchitectureHealthDomainInput {
|
|
20
|
+
id: string
|
|
21
|
+
title: string
|
|
22
|
+
summary: string
|
|
23
|
+
owner: string
|
|
24
|
+
surfaces: ArchitectureHealthSurface[]
|
|
25
|
+
testPaths: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ArchitectureHealthCheck {
|
|
29
|
+
code: string
|
|
30
|
+
status: ArchitectureHealthStatus
|
|
31
|
+
title: string
|
|
32
|
+
summary: string
|
|
33
|
+
evidence?: string[]
|
|
34
|
+
href?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ArchitectureHealthDomain extends ArchitectureHealthDomainInput {
|
|
38
|
+
status: ArchitectureHealthStatus
|
|
39
|
+
score: number
|
|
40
|
+
checkCodes: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ArchitectureHealthAction {
|
|
44
|
+
id: string
|
|
45
|
+
severity: Exclude<ArchitectureHealthStatus, 'healthy'>
|
|
46
|
+
title: string
|
|
47
|
+
summary: string
|
|
48
|
+
href: string
|
|
49
|
+
evidence: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ArchitectureHealthReport {
|
|
53
|
+
generatedAt: number
|
|
54
|
+
status: ArchitectureHealthStatus
|
|
55
|
+
score: number
|
|
56
|
+
domainCount: number
|
|
57
|
+
surfaceCount: number
|
|
58
|
+
guardrailCount: number
|
|
59
|
+
riskCount: number
|
|
60
|
+
warningCount: number
|
|
61
|
+
domains: ArchitectureHealthDomain[]
|
|
62
|
+
checks: ArchitectureHealthCheck[]
|
|
63
|
+
nextActions: ArchitectureHealthAction[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const WATCH_PENALTY = 10
|
|
67
|
+
const RISK_PENALTY = 30
|
|
68
|
+
|
|
69
|
+
export const DEFAULT_ARCHITECTURE_HEALTH_INVENTORY: ArchitectureHealthDomainInput[] = [
|
|
70
|
+
{
|
|
71
|
+
id: 'dispatch',
|
|
72
|
+
title: 'Dispatch Boundaries',
|
|
73
|
+
summary: 'Agent, task, protocol, connector, and tool execution paths that can start model or tool work.',
|
|
74
|
+
owner: 'runtime',
|
|
75
|
+
surfaces: [
|
|
76
|
+
{
|
|
77
|
+
id: 'agent-loop',
|
|
78
|
+
title: 'Agent loop dispatch',
|
|
79
|
+
kind: 'dispatch',
|
|
80
|
+
path: 'src/lib/server/agents/main-agent-loop.ts',
|
|
81
|
+
description: 'Main chat and autonomous run loop for agent turns.',
|
|
82
|
+
guardrails: ['tool capability policy', 'approval hooks', 'mission budgets', 'structured internal payload stripping'],
|
|
83
|
+
evidence: ['WorkingStatePatchSchema', 'MessageClassificationSchema', 'ResponseCompletenessSchema'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'protocol-runs',
|
|
87
|
+
title: 'Protocol run dispatch',
|
|
88
|
+
kind: 'dispatch',
|
|
89
|
+
path: 'src/lib/server/protocols/protocol-service.ts',
|
|
90
|
+
description: 'Visual protocol runner, DAG lifecycle, and step processors.',
|
|
91
|
+
guardrails: ['DAG validation', 'run lifecycle repository', 'step output contracts'],
|
|
92
|
+
evidence: ['protocol-service.test.ts', 'protocol-normalization.test.ts', 'protocol-foreach.test.ts'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'task-execution',
|
|
96
|
+
title: 'Task execution dispatch',
|
|
97
|
+
kind: 'dispatch',
|
|
98
|
+
path: 'src/lib/server/tasks/task-service.ts',
|
|
99
|
+
description: 'Task creation, execution workspace, liveness, handoff, and quality gates.',
|
|
100
|
+
guardrails: ['task execution policy', 'task quality gate', 'handoff packet readiness checks'],
|
|
101
|
+
evidence: ['task-execution-policy.test.ts', 'task-validation.test.ts', 'task-handoff.test.ts'],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'connector-ingress',
|
|
105
|
+
title: 'Connector ingress dispatch',
|
|
106
|
+
kind: 'dispatch',
|
|
107
|
+
path: 'src/lib/server/connectors/connector-service.ts',
|
|
108
|
+
description: 'Inbound connector messages routed into sessions or rooms.',
|
|
109
|
+
guardrails: ['connector schema validation', 'readiness checks', 'routing tests'],
|
|
110
|
+
evidence: ['connector-routing.test.ts', 'email.test.ts', 'filequeue.test.ts'],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'session-tools',
|
|
114
|
+
title: 'Session tool dispatch',
|
|
115
|
+
kind: 'dispatch',
|
|
116
|
+
path: 'src/lib/server/session-tools.ts',
|
|
117
|
+
description: 'Tool registry, session tool execution, and managed tool surfaces.',
|
|
118
|
+
guardrails: ['zod tool schemas', 'capability router', 'approval matching'],
|
|
119
|
+
evidence: ['tool-capability-policy.test.ts', 'universal-tool-access.test.ts', 'manage-tasks.test.ts'],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
testPaths: [
|
|
123
|
+
'src/lib/server/agents/agent-runtime-config.test.ts',
|
|
124
|
+
'src/lib/server/protocols/protocol-service.test.ts',
|
|
125
|
+
'src/lib/server/tasks/task-execution-policy.test.ts',
|
|
126
|
+
'src/lib/server/connectors/connector-routing.test.ts',
|
|
127
|
+
'src/lib/server/tool-capability-policy.test.ts',
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'memory',
|
|
132
|
+
title: 'Memory Ownership',
|
|
133
|
+
summary: 'Authoritative working state, long-term memory, graph retrieval, and archive surfaces.',
|
|
134
|
+
owner: 'memory',
|
|
135
|
+
surfaces: [
|
|
136
|
+
{
|
|
137
|
+
id: 'working-state',
|
|
138
|
+
title: 'Working state service',
|
|
139
|
+
kind: 'memory',
|
|
140
|
+
path: 'src/lib/server/working-state/service.ts',
|
|
141
|
+
description: 'Structured short-term state and fact extraction for active sessions.',
|
|
142
|
+
guardrails: ['zod schemas', 'normalization', 'repository boundary'],
|
|
143
|
+
evidence: ['working-state/service.test.ts', 'working-state/extraction.ts'],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'memory-policy',
|
|
147
|
+
title: 'Memory policy',
|
|
148
|
+
kind: 'memory',
|
|
149
|
+
path: 'src/lib/server/memory/memory-policy.ts',
|
|
150
|
+
description: 'Controls what can be written, retained, consolidated, and recalled.',
|
|
151
|
+
guardrails: ['policy tests', 'session memory scope', 'temporal decay'],
|
|
152
|
+
evidence: ['memory-policy.test.ts', 'session-memory-scope.test.ts'],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'memory-graph',
|
|
156
|
+
title: 'Memory graph',
|
|
157
|
+
kind: 'memory',
|
|
158
|
+
path: 'src/lib/server/memory/memory-graph.ts',
|
|
159
|
+
description: 'Graph relationships and retrieval context for long-running work.',
|
|
160
|
+
guardrails: ['graph tests', 'memory retrieval tests', 'MMR ranking'],
|
|
161
|
+
evidence: ['memory-graph.test.ts', 'memory-retrieval.test.ts'],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'session-archive',
|
|
165
|
+
title: 'Session archive memory',
|
|
166
|
+
kind: 'memory',
|
|
167
|
+
path: 'src/lib/server/memory/session-archive-memory.ts',
|
|
168
|
+
description: 'Archived session memory used after compaction or long-running autonomous work.',
|
|
169
|
+
guardrails: ['archive tests', 'freshness boundaries', 'session ownership'],
|
|
170
|
+
evidence: ['session-archive-memory.test.ts', 'memory-consolidation.test.ts'],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
testPaths: [
|
|
174
|
+
'src/lib/server/working-state/service.test.ts',
|
|
175
|
+
'src/lib/server/memory/memory-policy.test.ts',
|
|
176
|
+
'src/lib/server/memory/memory-graph.test.ts',
|
|
177
|
+
'src/lib/server/memory/session-archive-memory.test.ts',
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'startup',
|
|
182
|
+
title: 'Startup Entry Points',
|
|
183
|
+
summary: 'CLI, web, desktop, daemon, and packaging paths that bootstrap the runtime.',
|
|
184
|
+
owner: 'platform',
|
|
185
|
+
surfaces: [
|
|
186
|
+
{
|
|
187
|
+
id: 'cli-server',
|
|
188
|
+
title: 'CLI server entry',
|
|
189
|
+
kind: 'startup',
|
|
190
|
+
path: 'src/cli/index.ts',
|
|
191
|
+
description: 'Package CLI, command routing, server start, and API command coverage.',
|
|
192
|
+
guardrails: ['API route coverage guard', 'binary router tests', 'pack dry run'],
|
|
193
|
+
evidence: ['src/cli/index.test.js', 'bin/swarmclaw.js'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'next-app',
|
|
197
|
+
title: 'Next app runtime',
|
|
198
|
+
kind: 'startup',
|
|
199
|
+
path: 'src/app',
|
|
200
|
+
description: 'Self-hosted web UI and API routes.',
|
|
201
|
+
guardrails: ['health route', 'browser smoke', 'type-check'],
|
|
202
|
+
evidence: ['healthz/route.test.ts', 'scripts/browser-e2e-smoke.ts'],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'desktop-wrapper',
|
|
206
|
+
title: 'Desktop wrapper',
|
|
207
|
+
kind: 'startup',
|
|
208
|
+
path: 'electron/main.ts',
|
|
209
|
+
description: 'Electron wrapper around the standalone server with app-owned data directories.',
|
|
210
|
+
guardrails: ['local-only bind host', 'userData home root', 'native module rebuild smoke'],
|
|
211
|
+
evidence: ['scripts/build-electron.mjs', 'scripts/electron-after-pack.test.mjs'],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'daemon',
|
|
215
|
+
title: 'Daemon lifecycle',
|
|
216
|
+
kind: 'startup',
|
|
217
|
+
path: 'src/app/api/daemon/route.ts',
|
|
218
|
+
description: 'Runtime daemon start, stop, health checks, and status paths.',
|
|
219
|
+
guardrails: ['daemon health check', 'safe action schema', 'CLI mapping'],
|
|
220
|
+
evidence: ['src/cli/index.test.js', 'src/app/api/daemon/health-check/route.ts'],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
testPaths: [
|
|
224
|
+
'src/cli/index.test.js',
|
|
225
|
+
'src/app/api/healthz/route.test.ts',
|
|
226
|
+
'scripts/electron-after-pack.test.mjs',
|
|
227
|
+
'scripts/browser-e2e-smoke.ts',
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'quality',
|
|
232
|
+
title: 'Quality Evidence',
|
|
233
|
+
summary: 'Operator evidence surfaces that turn runtime state into release decisions.',
|
|
234
|
+
owner: 'quality',
|
|
235
|
+
surfaces: [
|
|
236
|
+
{
|
|
237
|
+
id: 'release-readiness',
|
|
238
|
+
title: 'Release readiness',
|
|
239
|
+
kind: 'quality',
|
|
240
|
+
path: 'src/lib/quality/release-readiness.ts',
|
|
241
|
+
description: 'Combines eval gates, operations pulse, approvals, budgets, and runtime readiness.',
|
|
242
|
+
guardrails: ['scored report', 'blocker and warning counts', 'next actions'],
|
|
243
|
+
evidence: ['release-readiness.test.ts', '/api/quality/release-readiness'],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'operations-pulse',
|
|
247
|
+
title: 'Operations pulse',
|
|
248
|
+
kind: 'quality',
|
|
249
|
+
path: 'src/lib/server/operations/operation-pulse.ts',
|
|
250
|
+
description: 'Shared triage queue for failed runs, approvals, connectors, gateways, and budgets.',
|
|
251
|
+
guardrails: ['range normalization', 'severity ranking', 'operator hrefs'],
|
|
252
|
+
evidence: ['operation-pulse.test.ts', '/api/operations/pulse'],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'eval-gates',
|
|
256
|
+
title: 'Eval regression gates',
|
|
257
|
+
kind: 'quality',
|
|
258
|
+
path: 'src/lib/server/eval/baseline.ts',
|
|
259
|
+
description: 'Compares latest eval evidence against thresholds and approved baselines.',
|
|
260
|
+
guardrails: ['baseline scope', 'regression thresholds', 'CLI commands'],
|
|
261
|
+
evidence: ['baseline.test.ts', '/api/eval/gate'],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
testPaths: [
|
|
265
|
+
'src/lib/quality/release-readiness.test.ts',
|
|
266
|
+
'src/lib/server/operations/operation-pulse.test.ts',
|
|
267
|
+
'src/lib/server/eval/baseline.test.ts',
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
function worstStatus(statuses: ArchitectureHealthStatus[]): ArchitectureHealthStatus {
|
|
273
|
+
if (statuses.includes('risk')) return 'risk'
|
|
274
|
+
if (statuses.includes('watch')) return 'watch'
|
|
275
|
+
return 'healthy'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function statusPenalty(status: ArchitectureHealthStatus): number {
|
|
279
|
+
if (status === 'risk') return RISK_PENALTY
|
|
280
|
+
if (status === 'watch') return WATCH_PENALTY
|
|
281
|
+
return 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function scoreFromChecks(checks: ArchitectureHealthCheck[]): number {
|
|
285
|
+
const penalty = checks.reduce((sum, check) => sum + statusPenalty(check.status), 0)
|
|
286
|
+
return Math.max(0, 100 - penalty)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function domainScore(checks: ArchitectureHealthCheck[]): number {
|
|
290
|
+
const actionable = checks.filter((check) => check.status !== 'healthy')
|
|
291
|
+
return scoreFromChecks(actionable)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function addCheck(checks: ArchitectureHealthCheck[], check: ArchitectureHealthCheck): void {
|
|
295
|
+
checks.push(check)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function plural(count: number, singular: string, pluralLabel = `${singular}s`): string {
|
|
299
|
+
return `${count} ${count === 1 ? singular : pluralLabel}`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildDomainChecks(domain: ArchitectureHealthDomainInput): ArchitectureHealthCheck[] {
|
|
303
|
+
const checks: ArchitectureHealthCheck[] = []
|
|
304
|
+
|
|
305
|
+
if (!domain.owner.trim()) {
|
|
306
|
+
addCheck(checks, {
|
|
307
|
+
code: `${domain.id}_missing_owner`,
|
|
308
|
+
status: 'risk',
|
|
309
|
+
title: `${domain.title} needs an owner`,
|
|
310
|
+
summary: 'This architecture domain does not declare an owner.',
|
|
311
|
+
evidence: [domain.id],
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (domain.surfaces.length === 0) {
|
|
316
|
+
addCheck(checks, {
|
|
317
|
+
code: `${domain.id}_missing_surfaces`,
|
|
318
|
+
status: 'risk',
|
|
319
|
+
title: `${domain.title} has no inventoried surfaces`,
|
|
320
|
+
summary: 'A quality report cannot reason about this domain until its runtime surfaces are listed.',
|
|
321
|
+
evidence: [domain.id],
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (domain.testPaths.length === 0) {
|
|
326
|
+
addCheck(checks, {
|
|
327
|
+
code: `${domain.id}_missing_tests`,
|
|
328
|
+
status: 'risk',
|
|
329
|
+
title: `${domain.title} has no mapped test evidence`,
|
|
330
|
+
summary: 'This domain needs at least one concrete test or verifier path before it can be treated as release-ready.',
|
|
331
|
+
evidence: [domain.id],
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const unguarded = domain.surfaces.filter((surface) => surface.guardrails.length === 0)
|
|
336
|
+
if (unguarded.length > 0) {
|
|
337
|
+
addCheck(checks, {
|
|
338
|
+
code: `${domain.id}_unguarded_surface`,
|
|
339
|
+
status: 'watch',
|
|
340
|
+
title: `${domain.title} has unguarded surfaces`,
|
|
341
|
+
summary: `${plural(unguarded.length, 'surface')} missing explicit guardrails.`,
|
|
342
|
+
evidence: unguarded.map((surface) => `${surface.title}: ${surface.path}`),
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return checks
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildCrossDomainChecks(inventory: ArchitectureHealthDomainInput[]): ArchitectureHealthCheck[] {
|
|
350
|
+
const checks: ArchitectureHealthCheck[] = []
|
|
351
|
+
const surfaces = inventory.flatMap((domain) => domain.surfaces)
|
|
352
|
+
const dispatchSurfaces = surfaces.filter((surface) => surface.kind === 'dispatch')
|
|
353
|
+
const memoryDomain = inventory.find((domain) => domain.id === 'memory')
|
|
354
|
+
const startupDomain = inventory.find((domain) => domain.id === 'startup')
|
|
355
|
+
|
|
356
|
+
if (dispatchSurfaces.length > 0 && dispatchSurfaces.every((surface) => surface.guardrails.length > 0)) {
|
|
357
|
+
addCheck(checks, {
|
|
358
|
+
code: 'dispatch_guardrail_coverage',
|
|
359
|
+
status: 'healthy',
|
|
360
|
+
title: 'Dispatch surfaces declare guardrails',
|
|
361
|
+
summary: `${plural(dispatchSurfaces.length, 'dispatch surface')} mapped to policy, approval, schema, or lifecycle controls.`,
|
|
362
|
+
evidence: dispatchSurfaces.map((surface) => `${surface.title}: ${surface.guardrails.join(', ')}`),
|
|
363
|
+
href: '/quality',
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (memoryDomain && memoryDomain.surfaces.length >= 3 && memoryDomain.testPaths.length > 0) {
|
|
368
|
+
addCheck(checks, {
|
|
369
|
+
code: 'memory_authority',
|
|
370
|
+
status: 'healthy',
|
|
371
|
+
title: 'Memory ownership is explicit',
|
|
372
|
+
summary: 'Working state, policy, graph, and archive surfaces are inventoried with test evidence.',
|
|
373
|
+
evidence: memoryDomain.surfaces.map((surface) => surface.path),
|
|
374
|
+
href: '/memory',
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (startupDomain && startupDomain.surfaces.length >= 3 && startupDomain.testPaths.length > 0) {
|
|
379
|
+
addCheck(checks, {
|
|
380
|
+
code: 'startup_surface_inventory',
|
|
381
|
+
status: 'healthy',
|
|
382
|
+
title: 'Startup surfaces are inventoried',
|
|
383
|
+
summary: 'CLI, web, desktop, and daemon entry points are tracked with smoke or route evidence.',
|
|
384
|
+
evidence: startupDomain.surfaces.map((surface) => surface.path),
|
|
385
|
+
href: '/settings',
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return checks
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function makeAction(check: ArchitectureHealthCheck): ArchitectureHealthAction | null {
|
|
393
|
+
if (check.status === 'healthy') return null
|
|
394
|
+
return {
|
|
395
|
+
id: check.code,
|
|
396
|
+
severity: check.status,
|
|
397
|
+
title: check.title,
|
|
398
|
+
summary: check.summary,
|
|
399
|
+
href: check.href || '/quality',
|
|
400
|
+
evidence: check.evidence || [],
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function buildArchitectureHealthReport(input: {
|
|
405
|
+
generatedAt?: number
|
|
406
|
+
inventory?: ArchitectureHealthDomainInput[]
|
|
407
|
+
} = {}): ArchitectureHealthReport {
|
|
408
|
+
const generatedAt = input.generatedAt ?? Date.now()
|
|
409
|
+
const inventory = input.inventory ?? DEFAULT_ARCHITECTURE_HEALTH_INVENTORY
|
|
410
|
+
const domainChecks = new Map<string, ArchitectureHealthCheck[]>()
|
|
411
|
+
const checks: ArchitectureHealthCheck[] = []
|
|
412
|
+
|
|
413
|
+
for (const domain of inventory) {
|
|
414
|
+
const nextChecks = buildDomainChecks(domain)
|
|
415
|
+
domainChecks.set(domain.id, nextChecks)
|
|
416
|
+
checks.push(...nextChecks)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
checks.push(...buildCrossDomainChecks(inventory))
|
|
420
|
+
|
|
421
|
+
const domains: ArchitectureHealthDomain[] = inventory.map((domain) => {
|
|
422
|
+
const actionableChecks = domainChecks.get(domain.id) ?? []
|
|
423
|
+
return {
|
|
424
|
+
...domain,
|
|
425
|
+
status: worstStatus(actionableChecks.map((check) => check.status)),
|
|
426
|
+
score: domainScore(actionableChecks),
|
|
427
|
+
checkCodes: actionableChecks.map((check) => check.code),
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
const actionableChecks = checks.filter((check) => check.status !== 'healthy')
|
|
432
|
+
const reportStatus = worstStatus(actionableChecks.map((check) => check.status))
|
|
433
|
+
const warningCount = actionableChecks.filter((check) => check.status === 'watch').length
|
|
434
|
+
const riskCount = actionableChecks.filter((check) => check.status === 'risk').length
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
generatedAt,
|
|
438
|
+
status: reportStatus,
|
|
439
|
+
score: scoreFromChecks(actionableChecks),
|
|
440
|
+
domainCount: inventory.length,
|
|
441
|
+
surfaceCount: inventory.reduce((sum, domain) => sum + domain.surfaces.length, 0),
|
|
442
|
+
guardrailCount: inventory.reduce((sum, domain) => (
|
|
443
|
+
sum + domain.surfaces.reduce((surfaceSum, surface) => surfaceSum + surface.guardrails.length, 0)
|
|
444
|
+
), 0),
|
|
445
|
+
riskCount,
|
|
446
|
+
warningCount,
|
|
447
|
+
domains,
|
|
448
|
+
checks,
|
|
449
|
+
nextActions: actionableChecks.map(makeAction).filter((action): action is ArchitectureHealthAction => action !== null),
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { describe, it } from 'node:test'
|
|
3
3
|
|
|
4
|
+
import { buildArchitectureHealthReport } from './architecture-health'
|
|
4
5
|
import { buildReleaseReadinessReport } from './release-readiness'
|
|
5
6
|
import type { EvalGateResult } from '@/lib/server/eval/types'
|
|
6
7
|
import type { OperationPulse } from '@/types'
|
|
@@ -126,4 +127,16 @@ describe('release readiness report', () => {
|
|
|
126
127
|
assert.ok(report.checks.some((check) => check.code === 'failed_runs_present'))
|
|
127
128
|
assert.ok(report.checks.some((check) => check.code === 'pending_approvals_present'))
|
|
128
129
|
})
|
|
130
|
+
|
|
131
|
+
it('includes architecture health when supplied', () => {
|
|
132
|
+
const report = buildReleaseReadinessReport({
|
|
133
|
+
pulse: pulse(),
|
|
134
|
+
evalGate: evalGate(),
|
|
135
|
+
architectureHealth: buildArchitectureHealthReport({ generatedAt: now }),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
assert.equal(report.status, 'ready')
|
|
139
|
+
assert.equal(report.architectureHealth?.status, 'healthy')
|
|
140
|
+
assert.ok(report.checks.some((check) => check.code === 'architecture_health_passed'))
|
|
141
|
+
})
|
|
129
142
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EvalGateResult } from '@/lib/server/eval/types'
|
|
2
|
+
import type { ArchitectureHealthReport } from '@/lib/quality/architecture-health'
|
|
2
3
|
import type { OperationPulse, OperationPulseAction, OperationPulseRange } from '@/types'
|
|
3
4
|
|
|
4
5
|
export type ReleaseReadinessStatus = 'ready' | 'warning' | 'blocked'
|
|
@@ -21,6 +22,7 @@ export interface ReleaseReadinessReport {
|
|
|
21
22
|
warningCount: number
|
|
22
23
|
pulse: OperationPulse
|
|
23
24
|
evalGate: EvalGateResult | null
|
|
25
|
+
architectureHealth: ArchitectureHealthReport | null
|
|
24
26
|
checks: ReleaseReadinessCheck[]
|
|
25
27
|
nextActions: OperationPulseAction[]
|
|
26
28
|
}
|
|
@@ -54,9 +56,11 @@ function addCheck(checks: ReleaseReadinessCheck[], check: ReleaseReadinessCheck)
|
|
|
54
56
|
export function buildReleaseReadinessReport(input: {
|
|
55
57
|
pulse: OperationPulse
|
|
56
58
|
evalGate?: EvalGateResult | null
|
|
59
|
+
architectureHealth?: ArchitectureHealthReport | null
|
|
57
60
|
}): ReleaseReadinessReport {
|
|
58
61
|
const checks: ReleaseReadinessCheck[] = []
|
|
59
62
|
const evalGate = input.evalGate ?? null
|
|
63
|
+
const architectureHealth = input.architectureHealth ?? null
|
|
60
64
|
|
|
61
65
|
if (!evalGate) {
|
|
62
66
|
addCheck(checks, {
|
|
@@ -169,6 +173,37 @@ export function buildReleaseReadinessReport(input: {
|
|
|
169
173
|
})
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
if (architectureHealth) {
|
|
177
|
+
if (architectureHealth.status === 'risk') {
|
|
178
|
+
addCheck(checks, {
|
|
179
|
+
code: 'architecture_health_risk',
|
|
180
|
+
status: 'blocked',
|
|
181
|
+
title: 'Architecture health has risks',
|
|
182
|
+
summary: `${plural(architectureHealth.riskCount, 'architecture risk')} need review before release.`,
|
|
183
|
+
href: '/quality',
|
|
184
|
+
evidence: architectureHealth.nextActions.map((action) => action.summary),
|
|
185
|
+
})
|
|
186
|
+
} else if (architectureHealth.status === 'watch') {
|
|
187
|
+
addCheck(checks, {
|
|
188
|
+
code: 'architecture_health_watch',
|
|
189
|
+
status: 'warning',
|
|
190
|
+
title: 'Architecture health needs review',
|
|
191
|
+
summary: `${plural(architectureHealth.warningCount, 'architecture warning')} found in runtime ownership checks.`,
|
|
192
|
+
href: '/quality',
|
|
193
|
+
evidence: architectureHealth.nextActions.map((action) => action.summary),
|
|
194
|
+
})
|
|
195
|
+
} else {
|
|
196
|
+
addCheck(checks, {
|
|
197
|
+
code: 'architecture_health_passed',
|
|
198
|
+
status: 'ready',
|
|
199
|
+
title: 'Architecture health passed',
|
|
200
|
+
summary: 'Dispatch, memory, startup, and quality surfaces have mapped owners, guardrails, and test evidence.',
|
|
201
|
+
href: '/quality',
|
|
202
|
+
evidence: [`${architectureHealth.score} health score`],
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
172
207
|
const blockerCount = checks.filter((check) => check.status === 'blocked').length
|
|
173
208
|
const warningCount = checks.filter((check) => check.status === 'warning').length
|
|
174
209
|
|
|
@@ -181,6 +216,7 @@ export function buildReleaseReadinessReport(input: {
|
|
|
181
216
|
warningCount,
|
|
182
217
|
pulse: input.pulse,
|
|
183
218
|
evalGate,
|
|
219
|
+
architectureHealth,
|
|
184
220
|
checks,
|
|
185
221
|
nextActions: input.pulse.actions.slice(0, 8),
|
|
186
222
|
}
|