@devflow-tools/cli 0.16.12 → 0.16.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/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +224 -16
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +41 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/with-server.d.ts.map +1 -1
- package/dist/lib/with-server.js +12 -6
- package/dist/lib/with-server.js.map +1 -1
- package/dist/plugin-files/.claude-plugin/plugin.json +1 -1
- package/dist/plugin-files/dist/command-registry.json +5 -5
- package/dist/plugin-files/dist/hooks/hook-daemon.js +25 -25
- package/dist/plugin-files/dist/hooks/post-tool-use-failure.js +2 -2
- package/dist/plugin-files/dist/hooks/post-tool-use.js +4 -4
- package/dist/plugin-files/dist/hooks/pre-compact.js +3 -2
- package/dist/plugin-files/dist/hooks/pre-tool-use.js +3 -2
- package/dist/plugin-files/dist/hooks/server-bootstrap.js +22 -22
- package/dist/plugin-files/dist/hooks/session-end.js +4 -2
- package/dist/plugin-files/dist/hooks/stop.js +12 -10
- package/dist/plugin-files/dist/hooks/user-prompt-submit.js +9 -8
- package/dist/plugin-files/package.json +7 -7
- package/package.json +27 -27
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
|
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import { DOCTOR_SCHEMA_VERSION, checkMcpHealth, loadPluginsWithDiagnostics, readMcpInventory, redactMcpInventory, selectHostAdapter, } from "@devflow-tools/sdk";
|
|
9
|
+
import { DOCTOR_SCHEMA_VERSION, checkMcpHealth, loadPluginsWithDiagnostics, readMcpInventory, redactMcpInventory, readRuntimeEndpointState, listRuntimeHttpCircuits, selectHostAdapter, } from "@devflow-tools/sdk";
|
|
10
10
|
import { resolveActivePlugin, scanClaudeSkillShadowing } from "../lib/plugin-setup.js";
|
|
11
11
|
import { serverStatus } from "./server.js";
|
|
12
12
|
import { inspectSkillHealth } from "./skill-health.js";
|
|
@@ -132,6 +132,24 @@ async function checkRuntimeControlPlane(project, dependencies) {
|
|
|
132
132
|
severity: "warning",
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
|
+
const endpoint = readRuntimeEndpointState(home);
|
|
136
|
+
metrics.push({
|
|
137
|
+
key: 'runtime_endpoint_registry',
|
|
138
|
+
value: endpoint ?? 'not registered',
|
|
139
|
+
target: 'healthy while active, stopped or absent while idle',
|
|
140
|
+
ok: !endpoint || endpoint.status === 'healthy' || endpoint.status === 'stopped',
|
|
141
|
+
severity: endpoint?.status === 'degraded' ? 'error' : 'warning',
|
|
142
|
+
});
|
|
143
|
+
const now = (dependencies.now ?? Date.now)();
|
|
144
|
+
const openCircuits = listRuntimeHttpCircuits(home)
|
|
145
|
+
.filter(circuit => circuit.status === 'open' && (circuit.openUntil ?? 0) > now);
|
|
146
|
+
metrics.push({
|
|
147
|
+
key: 'runtime_http_circuits',
|
|
148
|
+
value: openCircuits,
|
|
149
|
+
target: [],
|
|
150
|
+
ok: openCircuits.length === 0,
|
|
151
|
+
severity: openCircuits.some(circuit => circuit.failures >= 2) ? 'error' : 'warning',
|
|
152
|
+
});
|
|
135
153
|
return { name: "Contract", status: sectionStatus(metrics), metrics };
|
|
136
154
|
}
|
|
137
155
|
function canonicalizeProject(project) {
|
|
@@ -575,7 +593,10 @@ function semanticToolOutcome(row) {
|
|
|
575
593
|
const data = doctorRecord(output?.data);
|
|
576
594
|
const status = typeof output?.status === "string" ? output.status : undefined;
|
|
577
595
|
const metricStatus = typeof row.metric_status === "string" ? row.metric_status : undefined;
|
|
578
|
-
const
|
|
596
|
+
const expectedPolicyBlock = Boolean(row.blocked)
|
|
597
|
+
&& typeof row.block_reason === "string"
|
|
598
|
+
&& row.block_reason.startsWith("DEVFLOW_");
|
|
599
|
+
const failed = !expectedPolicyBlock && (Boolean(row.error)
|
|
579
600
|
|| Boolean(row.failure_category)
|
|
580
601
|
|| output?.ok === false
|
|
581
602
|
|| output?.success === false
|
|
@@ -583,7 +604,7 @@ function semanticToolOutcome(row) {
|
|
|
583
604
|
|| Boolean(output?.error)
|
|
584
605
|
|| status === "failed"
|
|
585
606
|
|| status === "rejected"
|
|
586
|
-
|| metricStatus === "error";
|
|
607
|
+
|| metricStatus === "error");
|
|
587
608
|
const degraded = !failed && (output?.degraded === true
|
|
588
609
|
|| Boolean(output?.degradation)
|
|
589
610
|
|| data?.degraded === true
|
|
@@ -691,8 +712,21 @@ async function checkVectors(project, dependencies) {
|
|
|
691
712
|
metrics.push({ key: "knowledge_vector_schema", value: missing, target: [], ok: false, severity: "warning" });
|
|
692
713
|
}
|
|
693
714
|
else {
|
|
694
|
-
const
|
|
695
|
-
const
|
|
715
|
+
const docColumns = doctorTableColumns(db, "knowledge_docs");
|
|
716
|
+
const activeClause = docColumns.has("status") ? "AND d.status = 'active'" : "";
|
|
717
|
+
const chunks = doctorCount(db, `
|
|
718
|
+
SELECT COUNT(*) count
|
|
719
|
+
FROM knowledge_chunks c
|
|
720
|
+
JOIN knowledge_docs d ON d.id = c.doc_id
|
|
721
|
+
WHERE 1 = 1 ${activeClause}
|
|
722
|
+
`);
|
|
723
|
+
const vectors = doctorCount(db, `
|
|
724
|
+
SELECT COUNT(*) count
|
|
725
|
+
FROM knowledge_vec_rowids v
|
|
726
|
+
JOIN knowledge_chunks c ON c.id = v.id
|
|
727
|
+
JOIN knowledge_docs d ON d.id = c.doc_id
|
|
728
|
+
WHERE 1 = 1 ${activeClause}
|
|
729
|
+
`);
|
|
696
730
|
const value = coverage(chunks, vectors);
|
|
697
731
|
const rows = db.prepare(`
|
|
698
732
|
SELECT d.source source, COUNT(c.id) chunks,
|
|
@@ -700,6 +734,7 @@ async function checkVectors(project, dependencies) {
|
|
|
700
734
|
FROM knowledge_docs d
|
|
701
735
|
LEFT JOIN knowledge_chunks c ON c.doc_id = d.id
|
|
702
736
|
LEFT JOIN knowledge_vec_rowids v ON v.id = c.id
|
|
737
|
+
WHERE 1 = 1 ${activeClause}
|
|
703
738
|
GROUP BY d.source ORDER BY d.source
|
|
704
739
|
`).all();
|
|
705
740
|
const bySource = rows.map(row => {
|
|
@@ -779,7 +814,13 @@ async function checkVectors(project, dependencies) {
|
|
|
779
814
|
metrics.push({ key: "embedder_error_log_read", value: error.message, target: "readable error log", ok: false, severity: "error" });
|
|
780
815
|
}
|
|
781
816
|
}
|
|
782
|
-
metrics.push({
|
|
817
|
+
metrics.push({
|
|
818
|
+
key: "embedder_errors_24h",
|
|
819
|
+
value: recentErrors,
|
|
820
|
+
target: "trend; current readiness is gated by model state and integrity",
|
|
821
|
+
ok: true,
|
|
822
|
+
severity: "info",
|
|
823
|
+
});
|
|
783
824
|
try {
|
|
784
825
|
const model = await (dependencies.inspectEmbeddingModel ?? inspectEmbeddingModel)({
|
|
785
826
|
homeDir: dependencies.homePath,
|
|
@@ -1105,6 +1146,9 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
1105
1146
|
toolEventColumns.has("error") ? "event.error" : "NULL error",
|
|
1106
1147
|
toolEventColumns.has("failure_category") ? "event.failure_category" : "NULL failure_category",
|
|
1107
1148
|
toolEventColumns.has("output") ? "event.output" : "NULL output",
|
|
1149
|
+
toolEventColumns.has("blocked") ? "event.blocked" : "0 blocked",
|
|
1150
|
+
toolEventColumns.has("block_reason") ? "event.block_reason" : "NULL block_reason",
|
|
1151
|
+
canProjectFilterEvents ? "event_session.status session_status" : "NULL session_status",
|
|
1108
1152
|
canJoinMetrics ? "metric.status metric_status" : "NULL metric_status",
|
|
1109
1153
|
].join(", ");
|
|
1110
1154
|
const eventRows = db.prepare(`
|
|
@@ -1117,11 +1161,20 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
1117
1161
|
`).all(...(canProjectFilterEvents ? [cutoff, project] : [cutoff]));
|
|
1118
1162
|
const semantic = eventRows.map(semanticToolOutcome);
|
|
1119
1163
|
const semanticFailures = semantic.filter(outcome => outcome.failed).length;
|
|
1164
|
+
const unresolvedSemanticFailures = semantic.filter((outcome, index) => outcome.failed
|
|
1165
|
+
&& eventRows[index]?.session_status !== "completed").length;
|
|
1120
1166
|
const semanticDegradations = semantic.filter(outcome => outcome.degraded).length;
|
|
1121
1167
|
metrics.push({ key: "semantic_tool_calls_24h", value: eventRows.length, ok: true, severity: "info" });
|
|
1122
|
-
metrics.push({ key: "semantic_tool_failures_24h", value: semanticFailures, target:
|
|
1168
|
+
metrics.push({ key: "semantic_tool_failures_24h", value: semanticFailures, target: "trend", ok: true, severity: "info" });
|
|
1169
|
+
metrics.push({
|
|
1170
|
+
key: "unresolved_semantic_tool_failures_24h",
|
|
1171
|
+
value: unresolvedSemanticFailures,
|
|
1172
|
+
target: 0,
|
|
1173
|
+
ok: unresolvedSemanticFailures === 0,
|
|
1174
|
+
severity: strictSeverity,
|
|
1175
|
+
});
|
|
1123
1176
|
metrics.push({ key: "semantic_tool_degradations_24h", value: semanticDegradations, target: 0, ok: semanticDegradations === 0, severity: strictSeverity });
|
|
1124
|
-
metrics.push({ key: "semantic_tool_failure_rate_24h", value: rate(semanticFailures, eventRows.length), target:
|
|
1177
|
+
metrics.push({ key: "semantic_tool_failure_rate_24h", value: rate(semanticFailures, eventRows.length), target: "trend", ok: true, severity: "info" });
|
|
1125
1178
|
metrics.push({ key: "semantic_tool_degradation_rate_24h", value: rate(semanticDegradations, eventRows.length), target: 0, ok: semanticDegradations === 0, severity: strictSeverity });
|
|
1126
1179
|
metrics.push({
|
|
1127
1180
|
key: "semantic_tool_project_scope",
|
|
@@ -1137,8 +1190,11 @@ function checkTelemetry(project, strict, dependencies) {
|
|
|
1137
1190
|
continue;
|
|
1138
1191
|
distribution[row.failure_category] = (distribution[row.failure_category] ?? 0) + 1;
|
|
1139
1192
|
}
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1193
|
+
const unresolvedRows = eventRows.filter((row, index) => semantic[index]?.failed
|
|
1194
|
+
&& row.session_status !== "completed");
|
|
1195
|
+
const classifiedEvents = unresolvedRows.filter(row => typeof row.failure_category === "string"
|
|
1196
|
+
&& row.failure_category.trim().length > 0).length;
|
|
1197
|
+
const classificationCoverage = unresolvedRows.length === 0 ? 1 : classifiedEvents / unresolvedRows.length;
|
|
1142
1198
|
metrics.push({
|
|
1143
1199
|
key: "failure_categories_24h",
|
|
1144
1200
|
value: distribution,
|
|
@@ -1505,6 +1561,75 @@ function checkBenchmark(dependencies) {
|
|
|
1505
1561
|
}
|
|
1506
1562
|
return { name: "Benchmark", status: sectionStatus(metrics), metrics };
|
|
1507
1563
|
}
|
|
1564
|
+
function checkRetrievalQuality(project, dependencies) {
|
|
1565
|
+
const metrics = [];
|
|
1566
|
+
const home = dependencies.homePath ?? homedir();
|
|
1567
|
+
const path = getGlobalDevFlowDbPath(home);
|
|
1568
|
+
if (!existsSync(path)) {
|
|
1569
|
+
metrics.push({
|
|
1570
|
+
key: 'retrieval_quality_report',
|
|
1571
|
+
value: 'missing global database',
|
|
1572
|
+
target: 'passing retrieval-quality report',
|
|
1573
|
+
ok: false,
|
|
1574
|
+
severity: 'warning',
|
|
1575
|
+
});
|
|
1576
|
+
return { name: 'Retrieval quality', status: sectionStatus(metrics), metrics };
|
|
1577
|
+
}
|
|
1578
|
+
let db;
|
|
1579
|
+
try {
|
|
1580
|
+
db = doctorOpenDatabase(path, dependencies);
|
|
1581
|
+
if (!doctorTableExists(db, 'benchmark_reports'))
|
|
1582
|
+
throw new Error('benchmark_reports table missing');
|
|
1583
|
+
const row = db.prepare(`
|
|
1584
|
+
SELECT report_json, created_at
|
|
1585
|
+
FROM benchmark_reports
|
|
1586
|
+
WHERE suite_id = 'retrieval-quality' AND project_root = ?
|
|
1587
|
+
ORDER BY created_at DESC
|
|
1588
|
+
LIMIT 1
|
|
1589
|
+
`).get(project);
|
|
1590
|
+
if (!row?.report_json) {
|
|
1591
|
+
metrics.push({
|
|
1592
|
+
key: 'retrieval_quality_report',
|
|
1593
|
+
value: 'no project report',
|
|
1594
|
+
target: 'run retrieval-quality benchmark for this project',
|
|
1595
|
+
ok: false,
|
|
1596
|
+
severity: 'warning',
|
|
1597
|
+
});
|
|
1598
|
+
return { name: 'Retrieval quality', status: sectionStatus(metrics), metrics };
|
|
1599
|
+
}
|
|
1600
|
+
const report = JSON.parse(row.report_json);
|
|
1601
|
+
const ageMs = (dependencies.now ?? Date.now)() - Number(row.created_at ?? 0);
|
|
1602
|
+
metrics.push({
|
|
1603
|
+
key: 'retrieval_quality_report',
|
|
1604
|
+
value: { status: report.qualityStatus, ageMs, diagnostics: report.diagnostics ?? [] },
|
|
1605
|
+
target: { status: 'passed', maxAgeMs: BENCHMARK_MAX_AGE_MS },
|
|
1606
|
+
ok: report.qualityStatus === 'passed' && ageMs <= BENCHMARK_MAX_AGE_MS,
|
|
1607
|
+
severity: report.qualityStatus === 'failed' ? 'error' : 'warning',
|
|
1608
|
+
});
|
|
1609
|
+
for (const [key, passed] of Object.entries(report.gates ?? {})) {
|
|
1610
|
+
metrics.push({
|
|
1611
|
+
key: `retrieval_${key}`,
|
|
1612
|
+
value: report.metrics?.[key],
|
|
1613
|
+
target: report.thresholds?.[key],
|
|
1614
|
+
ok: passed,
|
|
1615
|
+
severity: 'error',
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
metrics.push({
|
|
1621
|
+
key: 'retrieval_quality_read',
|
|
1622
|
+
value: error.message,
|
|
1623
|
+
target: 'readable retrieval quality report',
|
|
1624
|
+
ok: false,
|
|
1625
|
+
severity: 'error',
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
finally {
|
|
1629
|
+
db?.close();
|
|
1630
|
+
}
|
|
1631
|
+
return { name: 'Retrieval quality', status: sectionStatus(metrics), metrics };
|
|
1632
|
+
}
|
|
1508
1633
|
function hookDebugFootprint(root) {
|
|
1509
1634
|
if (!existsSync(root))
|
|
1510
1635
|
return { bytes: 0, files: 0 };
|
|
@@ -4505,6 +4630,50 @@ function checkMemoryQuality(project, dependencies) {
|
|
|
4505
4630
|
ok: true,
|
|
4506
4631
|
severity: "info",
|
|
4507
4632
|
});
|
|
4633
|
+
if (doctorTableExists(db, 'retrieval_access_log')) {
|
|
4634
|
+
const attribution = db.prepare(`
|
|
4635
|
+
SELECT access_type, COUNT(*) count
|
|
4636
|
+
FROM retrieval_access_log
|
|
4637
|
+
GROUP BY access_type
|
|
4638
|
+
`).all();
|
|
4639
|
+
metrics.push({
|
|
4640
|
+
key: 'memory_retrieval_attribution',
|
|
4641
|
+
value: Object.fromEntries(attribution.map(row => [row.access_type, Number(row.count)])),
|
|
4642
|
+
target: 'selected/exposed are distinct from adopted',
|
|
4643
|
+
ok: true,
|
|
4644
|
+
severity: 'info',
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4647
|
+
if (doctorTableExists(db, 'observations')) {
|
|
4648
|
+
const observationColumns = doctorTableColumns(db, 'observations');
|
|
4649
|
+
if (!observationColumns.has('evidence_status')) {
|
|
4650
|
+
metrics.push({
|
|
4651
|
+
key: 'explicit_memory_evidence_schema',
|
|
4652
|
+
value: 'evidence_status missing',
|
|
4653
|
+
target: 'memory schema v8+',
|
|
4654
|
+
ok: false,
|
|
4655
|
+
severity: 'error',
|
|
4656
|
+
});
|
|
4657
|
+
}
|
|
4658
|
+
else {
|
|
4659
|
+
const evidence = db.prepare(`
|
|
4660
|
+
SELECT
|
|
4661
|
+
COUNT(*) total,
|
|
4662
|
+
SUM(CASE WHEN evidence_status = 'verified' THEN 1 ELSE 0 END) verified
|
|
4663
|
+
FROM observations
|
|
4664
|
+
WHERE source = 'explicit_intent' AND quality_status = 'accepted'
|
|
4665
|
+
`).get();
|
|
4666
|
+
const total = Number(evidence?.total ?? 0);
|
|
4667
|
+
const verified = Number(evidence?.verified ?? 0);
|
|
4668
|
+
metrics.push({
|
|
4669
|
+
key: 'explicit_memory_evidence_coverage',
|
|
4670
|
+
value: total > 0 ? verified / total : 1,
|
|
4671
|
+
target: 1,
|
|
4672
|
+
ok: total === 0 || verified === total,
|
|
4673
|
+
severity: 'warning',
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4508
4677
|
metrics.push({
|
|
4509
4678
|
key: "memory_access_distribution",
|
|
4510
4679
|
value: {
|
|
@@ -4538,13 +4707,41 @@ function checkKnowledgeQuality(project, dependencies) {
|
|
|
4538
4707
|
metrics.push({ key: "knowledge_schema", value: "knowledge_chunks missing", target: "Batch 3 knowledge schema", ok: false, severity: "warning" });
|
|
4539
4708
|
}
|
|
4540
4709
|
else {
|
|
4541
|
-
const
|
|
4710
|
+
const chunkColumns = doctorTableColumns(db, 'knowledge_chunks');
|
|
4711
|
+
const tokenExpression = chunkColumns.has('token_count')
|
|
4712
|
+
? 'COALESCE(token_count, CAST((length(content) + 3) / 4 AS INTEGER))'
|
|
4713
|
+
: 'CAST((length(content) + 3) / 4 AS INTEGER)';
|
|
4714
|
+
const lengths = db.prepare(`SELECT ${tokenExpression} length FROM knowledge_chunks ORDER BY ${tokenExpression}`).all().map(row => Number(row.length));
|
|
4542
4715
|
const p95Index = Math.max(0, Math.ceil(lengths.length * 0.95) - 1);
|
|
4543
4716
|
const p95 = lengths.length === 0 ? 0 : lengths[p95Index];
|
|
4544
|
-
metrics.push({ key: "knowledge_chunk_length_p95", value: p95, target: "<= 3000", ok: p95 <= 3000, severity: "
|
|
4717
|
+
metrics.push({ key: "knowledge_chunk_length_p95", value: p95, target: "<= 3000 tokens", ok: p95 <= 3000, severity: "error" });
|
|
4545
4718
|
const fts = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'knowledge_fts'").get();
|
|
4546
4719
|
const trigram = /tokenize\s*=\s*['\"]?trigram/i.test(fts?.sql ?? "");
|
|
4547
4720
|
metrics.push({ key: "knowledge_fts_tokenizer", value: trigram ? "trigram" : "missing/non-trigram", target: "trigram", ok: trigram, severity: "warning" });
|
|
4721
|
+
if (doctorTableExists(db, 'knowledge_source_readiness')) {
|
|
4722
|
+
const readiness = db.prepare(`
|
|
4723
|
+
SELECT source, expected_version, documents, chunks, vectors, coverage, status, failure_category
|
|
4724
|
+
FROM knowledge_source_readiness ORDER BY source
|
|
4725
|
+
`).all();
|
|
4726
|
+
const active = readiness.filter(row => Number(row.documents ?? 0) > 0 || Number(row.chunks ?? 0) > 0);
|
|
4727
|
+
const minimumCoverage = active.length > 0
|
|
4728
|
+
? Math.min(...active.map(row => Number(row.coverage ?? 0)))
|
|
4729
|
+
: 0;
|
|
4730
|
+
metrics.push({
|
|
4731
|
+
key: 'knowledge_source_readiness',
|
|
4732
|
+
value: readiness,
|
|
4733
|
+
target: 'all active sources current with >=95% vector coverage',
|
|
4734
|
+
ok: active.length > 0 && active.every(row => row.status === 'ready' && Number(row.coverage ?? 0) >= 0.95),
|
|
4735
|
+
severity: 'error',
|
|
4736
|
+
});
|
|
4737
|
+
metrics.push({
|
|
4738
|
+
key: 'knowledge_vector_coverage',
|
|
4739
|
+
value: minimumCoverage,
|
|
4740
|
+
target: '>= 0.95',
|
|
4741
|
+
ok: active.length > 0 && minimumCoverage >= 0.95,
|
|
4742
|
+
severity: 'error',
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4548
4745
|
}
|
|
4549
4746
|
}
|
|
4550
4747
|
catch (error) {
|
|
@@ -4596,8 +4793,19 @@ async function checkUiReachability(dependencies) {
|
|
|
4596
4793
|
const { manifest, metrics } = acceptanceManifestMetrics(dependencies);
|
|
4597
4794
|
const routes = manifest?.ui?.routes ?? [];
|
|
4598
4795
|
metrics.push({ key: "dashboard_routes", value: routes.length, target: ">= 13", ok: manifest?.ui?.status === "green" && routes.length >= 13, severity: "warning" });
|
|
4796
|
+
const endpoint = readRuntimeEndpointState(dependencies.homePath ?? homedir());
|
|
4797
|
+
if ((!endpoint || endpoint.status === "stopped") && !dependencies.httpGet) {
|
|
4798
|
+
metrics.push({
|
|
4799
|
+
key: "dashboard_server_health",
|
|
4800
|
+
value: "inactive",
|
|
4801
|
+
target: "HTTP 200 when the runtime endpoint is active",
|
|
4802
|
+
ok: true,
|
|
4803
|
+
severity: "info",
|
|
4804
|
+
});
|
|
4805
|
+
return { name: "UI reachability", status: sectionStatus(metrics), metrics };
|
|
4806
|
+
}
|
|
4599
4807
|
const port = dependencies.env?.PORT ?? process.env.PORT ?? "13337";
|
|
4600
|
-
const healthUrl = `http://127.0.0.1:${port}/api/health`;
|
|
4808
|
+
const healthUrl = `${endpoint?.url ?? `http://127.0.0.1:${port}`}/api/health`;
|
|
4601
4809
|
try {
|
|
4602
4810
|
const response = await (dependencies.httpGet ?? defaultHttpGet)(healthUrl);
|
|
4603
4811
|
metrics.push({ key: "dashboard_server_health", value: { url: healthUrl, status: response.status }, target: "HTTP 200", ok: response.status === 200, severity: "warning" });
|
|
@@ -4626,14 +4834,14 @@ export async function runStructuredDoctor(options = {}, dependencies = {}) {
|
|
|
4626
4834
|
contract,
|
|
4627
4835
|
checkDeadcode(dependencies),
|
|
4628
4836
|
checkBenchmark(dependencies),
|
|
4837
|
+
checkRetrievalQuality(canonical.path, dependencies),
|
|
4629
4838
|
checkDataFace(dependencies),
|
|
4630
4839
|
checkRuntimeSecurity(dependencies),
|
|
4631
4840
|
await checkUiReachability(dependencies),
|
|
4632
4841
|
checkHookPathHealth(dependencies),
|
|
4633
4842
|
];
|
|
4634
|
-
const exitCode =
|
|
4635
|
-
? 1
|
|
4636
|
-
: 0;
|
|
4843
|
+
const exitCode = sections.some(section => section.status === 'red'
|
|
4844
|
+
|| (options.strict && section.status === 'yellow')) ? 1 : 0;
|
|
4637
4845
|
return { schemaVersion: DOCTOR_SCHEMA_VERSION, sections, exitCode };
|
|
4638
4846
|
}
|
|
4639
4847
|
export function runDoctor(options, dependencies = {}) {
|