@skyhook-io/radar-app 1.14.2 → 1.14.4
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/package.json +2 -2
- package/src/api/client.ts +32 -0
- package/src/api/diagnose.ts +7 -1
- package/src/components/CloudConnectFlow.tsx +21 -7
- package/src/components/CloudFunnelButton.tsx +85 -8
- package/src/components/cloudConnectHandoff.test.ts +10 -1
- package/src/components/cloudConnectHandoff.ts +15 -1
- package/src/components/diagnose/AIMarkdown.tsx +87 -0
- package/src/components/diagnose/ActivityTurn.tsx +1244 -0
- package/src/components/diagnose/AgentControls.tsx +603 -0
- package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
- package/src/components/diagnose/AnalysisStory.tsx +13 -18
- package/src/components/diagnose/ApplyDialog.tsx +289 -0
- package/src/components/diagnose/AssessmentCard.tsx +1579 -0
- package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
- package/src/components/diagnose/EvidenceCard.tsx +611 -0
- package/src/components/diagnose/Home.test.tsx +1 -3
- package/src/components/diagnose/Home.tsx +2 -1
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
- package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
- package/src/components/diagnose/InvestigationView.tsx +39 -51
- package/src/components/diagnose/StoryExcerpt.tsx +118 -0
- package/src/components/diagnose/assessmentCopy.ts +169 -0
- package/src/components/diagnose/investigationCase.test.tsx +658 -2
- package/src/components/diagnose/investigationCase.ts +135 -26
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +682 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
- package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +136 -0
- package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
- package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
- package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
- package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
- package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
- package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
- package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
- package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
- package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
- package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
- package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
- package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
- package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
- package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
- package/src/components/diagnose/investigationEvidence/index.ts +1 -1
- package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +13 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
- package/src/components/diagnose/investigationEvidence/types.ts +97 -2
- package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
- package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
- package/src/components/diagnose/investigationState.test.ts +2 -2
- package/src/components/diagnose/investigationState.ts +2 -2
- package/src/components/diagnose/investigationStory.ts +1 -1
- package/src/components/diagnose/parts.test.tsx +15 -1
- package/src/components/diagnose/parts.tsx +26 -3904
- package/src/components/diagnose/target.ts +9 -3
- package/src/components/diagnose/toolCallLabel.test.ts +47 -1
- package/src/components/diagnose/toolCallLabel.ts +41 -2
- package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +27 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
- package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
- package/src/components/resources/PodFilePreview.tsx +1 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +20 -1
- package/src/components/ui/CommandPalette.tsx +12 -8
- package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
- package/src/components/ui/Omnibar.tsx +96 -60
- package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
- package/src/components/workload/WorkloadView.tsx +7 -1
- package/src/components/ui/Disclosure.tsx +0 -47
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
canInvestigateFurther,
|
|
44
44
|
} from "./investigationState";
|
|
45
45
|
import type { AssessmentExplanation } from "./parts";
|
|
46
|
-
import {
|
|
46
|
+
import { investigationRunningLabel } from "./toolCallLabel";
|
|
47
47
|
import { AGENT_ROLE_LABELS } from "./AgentCase";
|
|
48
48
|
import {
|
|
49
49
|
Fragment,
|
|
@@ -99,9 +99,9 @@ import {
|
|
|
99
99
|
import { resolveInvestigationCase } from "./investigationCase";
|
|
100
100
|
import {
|
|
101
101
|
InvestigationEvidencePane,
|
|
102
|
-
partitionInvestigationEvidence,
|
|
103
102
|
type InvestigationTimelineScope,
|
|
104
103
|
} from "./InvestigationEvidencePane";
|
|
104
|
+
import { partitionInvestigationEvidence } from "./investigationEvidencePartition";
|
|
105
105
|
import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
106
106
|
import { formatInvestigationTarget } from "./target";
|
|
107
107
|
import { parseContextName } from "../../utils/context-name";
|
|
@@ -176,19 +176,17 @@ export function InvestigationView({
|
|
|
176
176
|
onOpenTimeline?: (scope: InvestigationTimelineScope) => void;
|
|
177
177
|
}) {
|
|
178
178
|
const { kind, namespace, name } = run;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
agents.find((agent) => agent.name === run.agent),
|
|
191
|
-
);
|
|
179
|
+
const { refreshRuns, openInvestigation, startError, dismissError, agents } =
|
|
180
|
+
useDiagnose();
|
|
181
|
+
// Capabilities are the declared ones of the agent that ran this run, not
|
|
182
|
+
// the picker's: a reopened run keeps the backend it was made with.
|
|
183
|
+
const runAgent = agents.find((agent) => agent.name === run.agent);
|
|
184
|
+
const explanationEnabled = supportsAssessmentExplanation(runAgent);
|
|
185
|
+
const canApply = runAgent?.apply === true;
|
|
186
|
+
// Read through a ref by the stream callback, which lives as long as the
|
|
187
|
+
// run and would otherwise keep the value from before the agents loaded.
|
|
188
|
+
const verifiesAfterApplyRef = useRef(false);
|
|
189
|
+
verifiesAfterApplyRef.current = runAgent?.verification === true;
|
|
192
190
|
// Investigate again means look again, so it asks for a new session explicitly and only
|
|
193
191
|
// carries the issue forward — being handed the previous answer is the one
|
|
194
192
|
// thing someone clicking this doesn't want.
|
|
@@ -536,10 +534,13 @@ export function InvestigationView({
|
|
|
536
534
|
});
|
|
537
535
|
pendingApplyStartedLiveRef.current = false;
|
|
538
536
|
if (effects.refreshClusterState) refreshClusterState();
|
|
539
|
-
//
|
|
540
|
-
// durable event is the automatic
|
|
541
|
-
// the controls through that
|
|
542
|
-
|
|
537
|
+
// On a backend that verifies, a successful apply is one compound
|
|
538
|
+
// server-owned job whose next durable event is the automatic
|
|
539
|
+
// read-only verification turn; hold the controls through that
|
|
540
|
+
// adjacent event so there is no idle flash. A backend that
|
|
541
|
+
// declares no verification sends no such turn, so nothing waits.
|
|
542
|
+
if (effects.verificationPending && verifiesAfterApplyRef.current)
|
|
543
|
+
setVerificationPending(true);
|
|
543
544
|
}
|
|
544
545
|
if (live || (isApply && applyStartedLive)) refreshRuns();
|
|
545
546
|
break;
|
|
@@ -1299,13 +1300,15 @@ export function InvestigationView({
|
|
|
1299
1300
|
(rootCauseEvidenceResolution?.links.length ||
|
|
1300
1301
|
investigationCase?.items.length ||
|
|
1301
1302
|
currentAssessment.diagnosis.unlinkedEvidence ||
|
|
1302
|
-
currentAssessment.diagnosis.evidenceMalformed
|
|
1303
|
+
currentAssessment.diagnosis.evidenceMalformed ||
|
|
1304
|
+
currentAssessment.diagnosis.omittedEntries) ? (
|
|
1303
1305
|
<AssessmentSources
|
|
1304
1306
|
renderedGroupIds={visibleEvidenceGroupIds}
|
|
1305
1307
|
resolution={rootCauseEvidenceResolution}
|
|
1306
1308
|
investigationCase={investigationCase}
|
|
1307
1309
|
unlinkedEvidence={currentAssessment.diagnosis.unlinkedEvidence}
|
|
1308
1310
|
evidenceMalformed={currentAssessment.diagnosis.evidenceMalformed}
|
|
1311
|
+
omittedEntries={currentAssessment.diagnosis.omittedEntries}
|
|
1309
1312
|
onViewSource={viewActivitySource}
|
|
1310
1313
|
/>
|
|
1311
1314
|
) : undefined;
|
|
@@ -1313,7 +1316,8 @@ export function InvestigationView({
|
|
|
1313
1316
|
storyShape &&
|
|
1314
1317
|
(investigationCase?.items.some((item) => item.placement === "source") ||
|
|
1315
1318
|
currentAssessment?.diagnosis?.unlinkedEvidence ||
|
|
1316
|
-
currentAssessment?.diagnosis?.evidenceMalformed
|
|
1319
|
+
currentAssessment?.diagnosis?.evidenceMalformed ||
|
|
1320
|
+
currentAssessment?.diagnosis?.omittedEntries)
|
|
1317
1321
|
? assessmentSourcesNode
|
|
1318
1322
|
: undefined;
|
|
1319
1323
|
const assessmentLimits = useMemo(() => {
|
|
@@ -1345,8 +1349,7 @@ export function InvestigationView({
|
|
|
1345
1349
|
);
|
|
1346
1350
|
// While the first assessment is still running the pane keeps the story
|
|
1347
1351
|
// shape, so the page fills in rather than rearranging when the verdict lands.
|
|
1348
|
-
const storyShell =
|
|
1349
|
-
!hosted && !currentAssessment && lastTurn?.status === "running";
|
|
1352
|
+
const storyShell = !currentAssessment && lastTurn?.status === "running";
|
|
1350
1353
|
const currentAssessmentEvidenceConflict =
|
|
1351
1354
|
currentAssessment?.diagnosis?.healthy === true &&
|
|
1352
1355
|
investigationEvidenceConflictsWithHealthy(projection);
|
|
@@ -1458,37 +1461,20 @@ export function InvestigationView({
|
|
|
1458
1461
|
toolSeenRef.current = toolSignature;
|
|
1459
1462
|
toolChangedAtRef.current = Date.now();
|
|
1460
1463
|
}, [toolSignature]);
|
|
1461
|
-
const investigatingLabel = (
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
+
const investigatingLabel = investigationRunningLabel({
|
|
1465
|
+
lastTool: lastTool && lastTool.kind === "tool" ? lastTool : undefined,
|
|
1466
|
+
reads: (lastTurn?.timeline ?? []).filter((item) => item.kind === "tool")
|
|
1467
|
+
.length,
|
|
1464
1468
|
// The render that first shows a changed tool is not quiet yet; the effect
|
|
1465
1469
|
// that stamps the change runs after it.
|
|
1466
|
-
|
|
1470
|
+
quietFor:
|
|
1467
1471
|
toolSeenRef.current === toolSignature
|
|
1468
1472
|
? Date.now() - toolChangedAtRef.current
|
|
1469
|
-
: 0
|
|
1470
|
-
|
|
1471
|
-
if (lastTool && lastTool.kind === "tool" && lastTool.tool) {
|
|
1472
|
-
current =
|
|
1473
|
-
lastTool.status === "running" || quietFor < 8000
|
|
1474
|
-
? describeToolCall(lastTool.tool, lastTool.summary)
|
|
1475
|
-
: "Thinking";
|
|
1476
|
-
}
|
|
1477
|
-
const elapsed = runningSinceRef.current
|
|
1473
|
+
: 0,
|
|
1474
|
+
elapsedSeconds: runningSinceRef.current
|
|
1478
1475
|
? Math.round((Date.now() - runningSinceRef.current) / 1000)
|
|
1479
|
-
: 0
|
|
1480
|
-
|
|
1481
|
-
// the right so they never move with it.
|
|
1482
|
-
return {
|
|
1483
|
-
current: `${current}…`,
|
|
1484
|
-
meta: [
|
|
1485
|
-
reads > 0 ? `${reads} ${reads === 1 ? "read" : "reads"}` : undefined,
|
|
1486
|
-
elapsed >= 2 ? `${elapsed} s` : undefined,
|
|
1487
|
-
]
|
|
1488
|
-
.filter(Boolean)
|
|
1489
|
-
.join(" · "),
|
|
1490
|
-
};
|
|
1491
|
-
})();
|
|
1476
|
+
: 0,
|
|
1477
|
+
});
|
|
1492
1478
|
const showSplitWorkspace = maximized;
|
|
1493
1479
|
const splitGridClass = showSplitWorkspace
|
|
1494
1480
|
? "@min-[1000px]/investigation:grid-cols-[minmax(320px,min(30%,520px))_minmax(0,1fr)]"
|
|
@@ -1677,7 +1663,7 @@ export function InvestigationView({
|
|
|
1677
1663
|
lastApplyAttemptIdx,
|
|
1678
1664
|
localApplyAttemptAssessmentIdx,
|
|
1679
1665
|
interactionsBlocked,
|
|
1680
|
-
|
|
1666
|
+
canApply,
|
|
1681
1667
|
hasNewerEvidence: hasEvidenceCollectedAfterAssessment,
|
|
1682
1668
|
})
|
|
1683
1669
|
? requestApply
|
|
@@ -1983,7 +1969,8 @@ export function InvestigationView({
|
|
|
1983
1969
|
return turnCase.items.length ||
|
|
1984
1970
|
turnResolution?.links.length ||
|
|
1985
1971
|
turn.diagnosis.unlinkedEvidence ||
|
|
1986
|
-
turn.diagnosis.evidenceMalformed
|
|
1972
|
+
turn.diagnosis.evidenceMalformed ||
|
|
1973
|
+
turn.diagnosis.omittedEntries ? (
|
|
1987
1974
|
<AssessmentSources
|
|
1988
1975
|
renderedGroupIds={visibleEvidenceGroupIds}
|
|
1989
1976
|
resolution={turnResolution}
|
|
@@ -1994,6 +1981,7 @@ export function InvestigationView({
|
|
|
1994
1981
|
evidenceMalformed={
|
|
1995
1982
|
turn.diagnosis.evidenceMalformed
|
|
1996
1983
|
}
|
|
1984
|
+
omittedEntries={turn.diagnosis.omittedEntries}
|
|
1997
1985
|
readOnly
|
|
1998
1986
|
onViewSource={viewActivitySource}
|
|
1999
1987
|
/>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import {
|
|
3
|
+
Collapse,
|
|
4
|
+
TerminalBlock,
|
|
5
|
+
TerminalBlockLabel,
|
|
6
|
+
} from "@skyhook-io/k8s-ui";
|
|
7
|
+
import { InventoryRow } from "./investigationEvidence/bodies/inventory";
|
|
8
|
+
import { RankingRow } from "./investigationEvidence/bodies/ranking";
|
|
9
|
+
import { PostureFindingRow } from "./investigationEvidence/bodies/posture";
|
|
10
|
+
import { MetricsBody } from "./investigationEvidence/bodies/metrics";
|
|
11
|
+
import type { StoryExcerpt as Excerpt } from "./investigationEvidence/excerpt";
|
|
12
|
+
import type { InvestigationEvidenceData } from "./investigationEvidence";
|
|
13
|
+
import type { MetricsChangeCoverage } from "./investigationMetrics";
|
|
14
|
+
|
|
15
|
+
/** The inline slice of a placed card, rendered from Radar's record. */
|
|
16
|
+
export function StoryExcerpt({
|
|
17
|
+
excerpt,
|
|
18
|
+
data,
|
|
19
|
+
prominence,
|
|
20
|
+
open,
|
|
21
|
+
canExpand,
|
|
22
|
+
changeCoverage,
|
|
23
|
+
}: {
|
|
24
|
+
excerpt: Excerpt;
|
|
25
|
+
data: InvestigationEvidenceData;
|
|
26
|
+
prominence: "primary" | "supporting" | "secondary";
|
|
27
|
+
open: boolean;
|
|
28
|
+
canExpand: boolean;
|
|
29
|
+
changeCoverage?: MetricsChangeCoverage;
|
|
30
|
+
}) {
|
|
31
|
+
const padding = prominence === "primary" ? "px-3 pb-2.5" : "px-2.5 pb-2";
|
|
32
|
+
switch (excerpt.kind) {
|
|
33
|
+
case "lines":
|
|
34
|
+
return (
|
|
35
|
+
<div data-story-log-lines className={padding}>
|
|
36
|
+
<TerminalBlock
|
|
37
|
+
footer={
|
|
38
|
+
excerpt.rest.length > 0 ? (
|
|
39
|
+
<Collapse open={open && canExpand}>
|
|
40
|
+
<TerminalBlockLabel divider>Earlier lines</TerminalBlockLabel>
|
|
41
|
+
<pre className="overflow-x-auto whitespace-pre-wrap break-words px-3 pb-2.5 font-mono text-xs leading-relaxed text-[var(--terminal-text)]">
|
|
42
|
+
{excerpt.rest.join("\n")}
|
|
43
|
+
</pre>
|
|
44
|
+
</Collapse>
|
|
45
|
+
) : null
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
{excerpt.head.join("\n")}
|
|
49
|
+
</TerminalBlock>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
case "rows":
|
|
53
|
+
return (
|
|
54
|
+
<div data-story-inventory-rows className={padding}>
|
|
55
|
+
<div className="rounded-md border border-theme-border">
|
|
56
|
+
{excerpt.entries.map((entry, index) =>
|
|
57
|
+
entry.row ? (
|
|
58
|
+
<InventoryRow
|
|
59
|
+
key={entry.key}
|
|
60
|
+
resource={entry.row}
|
|
61
|
+
divider={index > 0}
|
|
62
|
+
/>
|
|
63
|
+
) : (
|
|
64
|
+
<p
|
|
65
|
+
key={entry.key}
|
|
66
|
+
data-story-inventory-absent={
|
|
67
|
+
entry.matches === 0 ? "" : undefined
|
|
68
|
+
}
|
|
69
|
+
className={clsx(
|
|
70
|
+
"px-2.5 py-1.5 font-mono text-xs text-theme-text-secondary",
|
|
71
|
+
index > 0 && "border-t border-theme-border/60",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{entry.matches === 0
|
|
75
|
+
? `Not in this listing: ${entry.label}`
|
|
76
|
+
: `${entry.label}: ${entry.matches} entries match in this listing`}
|
|
77
|
+
</p>
|
|
78
|
+
),
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
case "ranking":
|
|
84
|
+
return (
|
|
85
|
+
<div data-story-ranking-rows className={padding}>
|
|
86
|
+
<div className="rounded-md border border-theme-border">
|
|
87
|
+
{excerpt.rows.map((row, index) => (
|
|
88
|
+
<RankingRow
|
|
89
|
+
key={`${row.namespace ?? ""}/${row.name}`}
|
|
90
|
+
row={row}
|
|
91
|
+
divider={index > 0}
|
|
92
|
+
/>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
case "posture":
|
|
98
|
+
return (
|
|
99
|
+
<div data-story-posture-findings className={padding}>
|
|
100
|
+
<div className="rounded-md border border-theme-border">
|
|
101
|
+
{excerpt.findings.map((finding, index) => (
|
|
102
|
+
<PostureFindingRow
|
|
103
|
+
key={`${finding.name}-${finding.check}`}
|
|
104
|
+
finding={finding}
|
|
105
|
+
divider={index > 0}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
case "chart":
|
|
112
|
+
return data.type === "metrics" ? (
|
|
113
|
+
<div data-story-metrics className={padding}>
|
|
114
|
+
<MetricsBody data={data} changeCoverage={changeCoverage} />
|
|
115
|
+
</div>
|
|
116
|
+
) : null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Diagnosis } from "../../api/diagnose";
|
|
2
|
+
import { storyPlainText } from "./investigationStory";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The whole assessment as text: headline, certainty, what is unresolved, the
|
|
6
|
+
* technical cause, the story without its markers, and the steps. Copying only
|
|
7
|
+
* the headline would ship the persuasive half without its caveats.
|
|
8
|
+
*/
|
|
9
|
+
/** What Radar adds to the agent's verdict when the assessment is copied for a channel. */
|
|
10
|
+
export interface AssessmentCopyRadar {
|
|
11
|
+
/** Reads Radar could not complete, as shown in Still open. */
|
|
12
|
+
limits?: readonly string[];
|
|
13
|
+
/** Adverse Radar cards the agent explained, as shown in Still open. */
|
|
14
|
+
signals?: readonly string[];
|
|
15
|
+
/** Adverse Radar cards nothing explains, as shown above a healthy verdict. */
|
|
16
|
+
flags?: readonly string[];
|
|
17
|
+
/** Where and when: the header line a reader in a channel needs first. */
|
|
18
|
+
context?: {
|
|
19
|
+
target: string;
|
|
20
|
+
cluster?: string;
|
|
21
|
+
startedAt?: string;
|
|
22
|
+
agent?: string;
|
|
23
|
+
};
|
|
24
|
+
/** The placed cards, as short excerpts: the receipts behind the headline. */
|
|
25
|
+
receipts?: readonly {
|
|
26
|
+
title: string;
|
|
27
|
+
role?: string;
|
|
28
|
+
lines: readonly string[];
|
|
29
|
+
/** The agent's statement of what this result does not cover. */
|
|
30
|
+
gap?: string;
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The assessment as Markdown for a channel or a ticket: context, headline,
|
|
36
|
+
* cause, the receipts, what is still open, the next step. The full story and
|
|
37
|
+
* every captured result stay in Radar; the text says so.
|
|
38
|
+
*/
|
|
39
|
+
export function assessmentCopyText(
|
|
40
|
+
diagnosis: Diagnosis,
|
|
41
|
+
radar: AssessmentCopyRadar = {},
|
|
42
|
+
): string {
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
if (radar.context) {
|
|
45
|
+
const when = radar.context.startedAt
|
|
46
|
+
? new Date(radar.context.startedAt).toLocaleString(undefined, {
|
|
47
|
+
dateStyle: "medium",
|
|
48
|
+
timeStyle: "short",
|
|
49
|
+
})
|
|
50
|
+
: undefined;
|
|
51
|
+
parts.push(
|
|
52
|
+
[
|
|
53
|
+
`**${radar.context.target}**`,
|
|
54
|
+
radar.context.cluster,
|
|
55
|
+
when,
|
|
56
|
+
radar.context.agent ? `via ${radar.context.agent}` : undefined,
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(" · "),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (diagnosis.summary) {
|
|
63
|
+
const certainty = diagnosis.healthy
|
|
64
|
+
? "Healthy"
|
|
65
|
+
: diagnosis.certainty
|
|
66
|
+
? CERTAINTY_LABEL[diagnosis.certainty]
|
|
67
|
+
: undefined;
|
|
68
|
+
parts.push(
|
|
69
|
+
`**${diagnosis.summary.trim()}**${certainty ? ` — ${certainty} (agent's word)` : ""}`,
|
|
70
|
+
);
|
|
71
|
+
} else if (diagnosis.certainty) {
|
|
72
|
+
parts.push(`Certainty (agent): ${CERTAINTY_LABEL[diagnosis.certainty]}`);
|
|
73
|
+
}
|
|
74
|
+
// The pasted text must not read more confident than the screen: Radar's
|
|
75
|
+
// own qualifications travel with the agent's.
|
|
76
|
+
const flags = (radar.flags ?? []).filter((item) => item.trim());
|
|
77
|
+
if (flags.length > 0)
|
|
78
|
+
parts.push(
|
|
79
|
+
[
|
|
80
|
+
"Radar flagged:",
|
|
81
|
+
...flags.map((item) => `- ${item}`),
|
|
82
|
+
"Read those cards before treating this as an all-clear.",
|
|
83
|
+
].join("\n"),
|
|
84
|
+
);
|
|
85
|
+
const unresolved = [
|
|
86
|
+
...(diagnosis.unresolved ?? []),
|
|
87
|
+
...(radar.limits ?? []),
|
|
88
|
+
...(radar.signals ?? []),
|
|
89
|
+
].filter((item) => item.trim());
|
|
90
|
+
if (unresolved.length > 0)
|
|
91
|
+
parts.push(
|
|
92
|
+
["Still open:", ...unresolved.map((item) => `- ${item}`)].join("\n"),
|
|
93
|
+
);
|
|
94
|
+
if (diagnosis.rootCause) parts.push(storyPlainText(diagnosis.rootCause));
|
|
95
|
+
const receipts = (radar.receipts ?? []).filter((receipt) => receipt.title);
|
|
96
|
+
if (receipts.length > 0) {
|
|
97
|
+
parts.push(
|
|
98
|
+
[
|
|
99
|
+
"Evidence (Radar):",
|
|
100
|
+
...receipts.map((receipt) =>
|
|
101
|
+
[
|
|
102
|
+
`- **${receipt.title}**${receipt.role ? ` · ${receipt.role}` : ""}`,
|
|
103
|
+
...receipt.lines
|
|
104
|
+
.filter((line) => line.trim())
|
|
105
|
+
.map((line) => ` > ${line.trim()}`),
|
|
106
|
+
...(receipt.gap ? [` Not shown: ${receipt.gap}`] : []),
|
|
107
|
+
].join("\n"),
|
|
108
|
+
),
|
|
109
|
+
].join("\n"),
|
|
110
|
+
);
|
|
111
|
+
} else if (!radar.context) {
|
|
112
|
+
const story = storyPlainText(diagnosis.report ?? "");
|
|
113
|
+
if (story) parts.push(story);
|
|
114
|
+
}
|
|
115
|
+
const steps = diagnosis.steps?.length
|
|
116
|
+
? diagnosis.steps.map(
|
|
117
|
+
(step, index) =>
|
|
118
|
+
`${index + 1}. [${STEP_KIND_LABEL[step.kind]}] ${step.text}${step.precondition ? ` (only if ${step.precondition})` : ""}`,
|
|
119
|
+
)
|
|
120
|
+
: (diagnosis.remediation ?? []).map(
|
|
121
|
+
(text, index) => `${index + 1}. ${text}`,
|
|
122
|
+
);
|
|
123
|
+
if (steps.length > 0) {
|
|
124
|
+
if (radar.context) {
|
|
125
|
+
// A channel gets the recommended step; the rest wait in Radar.
|
|
126
|
+
const lead = diagnosis.recommendedIndex
|
|
127
|
+
? diagnosis.recommendedIndex - 1
|
|
128
|
+
: 0;
|
|
129
|
+
const first = steps[lead] ?? steps[0];
|
|
130
|
+
const rest = steps.length - 1;
|
|
131
|
+
parts.push(
|
|
132
|
+
[
|
|
133
|
+
`Next step: ${first.replace(/^\d+\. /, "")}`,
|
|
134
|
+
diagnosis.recommendedReason
|
|
135
|
+
? `Why: ${diagnosis.recommendedReason}`
|
|
136
|
+
: undefined,
|
|
137
|
+
rest > 0
|
|
138
|
+
? `${rest} more ${rest === 1 ? "step" : "steps"} in Radar.`
|
|
139
|
+
: undefined,
|
|
140
|
+
]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join("\n"),
|
|
143
|
+
);
|
|
144
|
+
} else parts.push(["Next steps:", ...steps].join("\n"));
|
|
145
|
+
}
|
|
146
|
+
if (radar.context)
|
|
147
|
+
parts.push(
|
|
148
|
+
"Full analysis and every captured result: open the investigation in Radar.",
|
|
149
|
+
);
|
|
150
|
+
return parts.join("\n\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const CERTAINTY_LABEL: Record<
|
|
154
|
+
NonNullable<Diagnosis["certainty"]>,
|
|
155
|
+
string
|
|
156
|
+
> = {
|
|
157
|
+
established: "Established",
|
|
158
|
+
likely: "Likely",
|
|
159
|
+
suspected: "Suspected",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const STEP_KIND_LABEL: Record<
|
|
163
|
+
NonNullable<Diagnosis["steps"]>[number]["kind"],
|
|
164
|
+
string
|
|
165
|
+
> = {
|
|
166
|
+
mitigate: "Mitigate",
|
|
167
|
+
verify: "Verify",
|
|
168
|
+
investigate: "Investigate",
|
|
169
|
+
};
|