kibi-cli 0.17.0 → 0.18.1
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/cli-json-command.d.ts.map +1 -1
- package/dist/cli-json-command.js +87 -11
- package/dist/cli-register-maintenance.d.ts.map +1 -1
- package/dist/cli-register-maintenance.js +8 -0
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/commands/aggregated-checks.d.ts.map +1 -1
- package/dist/commands/aggregated-checks.js +1 -0
- package/dist/commands/coverage.d.ts +5 -0
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/coverage.js +5 -0
- package/dist/commands/discovery-table.d.ts.map +1 -1
- package/dist/commands/discovery-table.js +96 -5
- package/dist/commands/sync/cache.d.ts +2 -0
- package/dist/commands/sync/cache.d.ts.map +1 -1
- package/dist/commands/sync/cache.js +8 -0
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +58 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +72 -15
- package/dist/commands/usage-metrics.d.ts +66 -2
- package/dist/commands/usage-metrics.d.ts.map +1 -1
- package/dist/commands/usage-metrics.js +34 -24
- package/dist/commands/usage-remediation.d.ts +9 -0
- package/dist/commands/usage-remediation.d.ts.map +1 -0
- package/dist/commands/usage-remediation.js +52 -0
- package/dist/extractors/markdown.d.ts +8 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +69 -2
- package/dist/operations/modeling/logic-modeling.d.ts.map +1 -1
- package/dist/operations/modeling/logic-modeling.js +14 -5
- package/dist/operations/modeling/model-requirement.d.ts.map +1 -1
- package/dist/operations/modeling/model-requirement.js +27 -2
- package/dist/operations/modeling/predicate-applyplan.d.ts +1 -1
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +18 -6
- package/dist/operations/modeling/predicate-inference.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-inference.js +5 -17
- package/dist/operations/modeling/predicate-loader.js +1 -1
- package/dist/operations/modeling/predicate-types.d.ts +9 -1
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +35 -11
- package/dist/operations/mutation/audit.js +1 -1
- package/dist/operations/mutation/contradictions.d.ts +1 -1
- package/dist/operations/mutation/contradictions.d.ts.map +1 -1
- package/dist/operations/mutation/contradictions.js +23 -18
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +6 -3
- package/dist/operations/mutation/upsert.d.ts +3 -1
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +51 -12
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +18 -2
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +8 -0
- package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analysis-receipt.js +7 -1
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +76 -61
- package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +7 -7
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts +18 -0
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -0
- package/dist/operations/semantic-advisor/ingestion-boundary.js +167 -0
- package/dist/operations/semantic-advisor/shared.d.ts +8 -0
- package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/shared.js +37 -3
- package/dist/operations/semantic-advisor/types.d.ts +5 -0
- package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
- package/dist/prolog/codec.d.ts.map +1 -1
- package/dist/prolog/codec.js +9 -2
- package/dist/prolog.d.ts +6 -0
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +240 -61
- package/dist/public/diagnostic-usage.d.ts +17 -0
- package/dist/public/diagnostic-usage.d.ts.map +1 -0
- package/dist/public/diagnostic-usage.js +152 -0
- package/dist/public/distribution-parity.d.ts +90 -0
- package/dist/public/distribution-parity.d.ts.map +1 -0
- package/dist/public/distribution-parity.js +367 -0
- package/dist/public/impact/full-kb-quality.d.ts +2 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
- package/dist/public/impact/full-kb-quality.js +53 -1
- package/dist/public/impact-diagnostics.d.ts +2 -0
- package/dist/public/impact-diagnostics.d.ts.map +1 -1
- package/dist/public/impact-diagnostics.js +1 -0
- package/dist/public/operations/catalog.d.ts +151 -6
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +4 -0
- package/dist/public/operations/discovery-executors.d.ts +6 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +18 -2
- package/dist/public/operations/index.d.ts +4 -0
- package/dist/public/operations/index.d.ts.map +1 -1
- package/dist/public/operations/index.js +4 -0
- package/dist/public/operations/legacy-migration-plan.d.ts +113 -0
- package/dist/public/operations/legacy-migration-plan.d.ts.map +1 -0
- package/dist/public/operations/legacy-migration-plan.js +409 -0
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +90 -0
- package/dist/public/operations/repair-plan.d.ts +184 -0
- package/dist/public/operations/repair-plan.d.ts.map +1 -0
- package/dist/public/operations/repair-plan.js +397 -0
- package/dist/public/operations/runtime-types.d.ts +7 -0
- package/dist/public/operations/runtime-types.d.ts.map +1 -1
- package/dist/public/operations/specs/check.d.ts +2 -2
- package/dist/public/operations/specs/check.js +2 -2
- package/dist/public/operations/specs/discovery.d.ts +1 -1
- package/dist/public/operations/specs/discovery.js +1 -1
- package/dist/public/operations/specs/modeling.d.ts +19 -1
- package/dist/public/operations/specs/modeling.d.ts.map +1 -1
- package/dist/public/operations/specs/modeling.js +16 -1
- package/dist/public/operations/specs/mutation-schemas.d.ts +96 -1
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +40 -1
- package/dist/public/operations/specs/reporting.d.ts +42 -1
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +70 -6
- package/dist/public/operations/specs/semantic.js +1 -1
- package/dist/public/operations/specs/upsert.d.ts +96 -1
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/operations/workspace-snapshot.d.ts +10 -0
- package/dist/public/operations/workspace-snapshot.d.ts.map +1 -0
- package/dist/public/operations/workspace-snapshot.js +20 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +43 -1
- package/dist/public/skills/kibi-traceability/SKILL.md +7 -1
- package/dist/public/skills/kibi-usage/SKILL.md +10 -4
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +1 -1
- package/dist/public/skills/kibi-usage/resources/logic-ir.md +1 -1
- package/dist/public/skills/kibi-usage/resources/workflows.md +19 -2
- package/dist/public/telemetry-acceptance.d.ts +89 -0
- package/dist/public/telemetry-acceptance.d.ts.map +1 -0
- package/dist/public/telemetry-acceptance.js +597 -0
- package/dist/public/telemetry-remediation.d.ts +33 -0
- package/dist/public/telemetry-remediation.d.ts.map +1 -0
- package/dist/public/telemetry-remediation.js +257 -0
- package/dist/public/verification-receipt.d.ts +75 -0
- package/dist/public/verification-receipt.d.ts.map +1 -0
- package/dist/public/verification-receipt.js +129 -0
- package/dist/schemas/entity.schema.json +96 -2
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +36 -3
- package/dist/types/entities.d.ts +1 -0
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/rule-registry.d.ts +2 -0
- package/dist/utils/rule-registry.d.ts.map +1 -1
- package/package.json +30 -2
- package/src/public/diagnostic-usage.ts +206 -0
- package/src/public/distribution-parity.ts +580 -0
- package/src/public/impact/full-kb-quality.ts +74 -1
- package/src/public/impact-diagnostics.ts +16 -0
- package/src/public/operations/check-executor.ts +4 -0
- package/src/public/operations/discovery-executors.ts +24 -2
- package/src/public/operations/index.ts +4 -0
- package/src/public/operations/legacy-migration-plan.ts +688 -0
- package/src/public/operations/node-ports.ts +109 -0
- package/src/public/operations/repair-plan.ts +562 -0
- package/src/public/operations/runtime-types.ts +8 -0
- package/src/public/operations/specs/check.ts +2 -2
- package/src/public/operations/specs/discovery.ts +1 -1
- package/src/public/operations/specs/modeling.ts +19 -1
- package/src/public/operations/specs/mutation-schemas.ts +47 -1
- package/src/public/operations/specs/reporting.ts +91 -6
- package/src/public/operations/specs/semantic.ts +1 -1
- package/src/public/operations/workspace-snapshot.ts +28 -0
- package/src/public/schemas/entity.ts +44 -1
- package/src/public/skills/kibi-traceability/SKILL.md +7 -1
- package/src/public/skills/kibi-usage/SKILL.md +10 -4
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +1 -1
- package/src/public/skills/kibi-usage/resources/logic-ir.md +1 -1
- package/src/public/skills/kibi-usage/resources/workflows.md +19 -2
- package/src/public/telemetry-acceptance.ts +859 -0
- package/src/public/telemetry-remediation.ts +462 -0
- package/src/public/verification-receipt.ts +170 -0
- package/src/schemas/entity.schema.json +96 -2
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import type { QualityDiagnostic } from "./impact/types.js";
|
|
2
|
+
|
|
3
|
+
export const TELEMETRY_ACCEPTANCE_VERSION =
|
|
4
|
+
"kibi.telemetry-acceptance.v1" as const;
|
|
5
|
+
|
|
6
|
+
export type TelemetryMetricStatus =
|
|
7
|
+
| "passed"
|
|
8
|
+
| "failed"
|
|
9
|
+
| "insufficient_evidence"
|
|
10
|
+
| "not_applicable";
|
|
11
|
+
|
|
12
|
+
export type TelemetryAcceptanceStatus =
|
|
13
|
+
| "passed"
|
|
14
|
+
| "failed"
|
|
15
|
+
| "insufficient_evidence";
|
|
16
|
+
|
|
17
|
+
export type TelemetryMetricId =
|
|
18
|
+
| "telemetry_completeness"
|
|
19
|
+
| "advisor_before_requirement_write"
|
|
20
|
+
| "validation_before_upsert"
|
|
21
|
+
| "source_lookup_zero_result_rate"
|
|
22
|
+
| "proof_gap_recovery"
|
|
23
|
+
| "e2e_receipt_freshness"
|
|
24
|
+
| "repeated_mutation_failures";
|
|
25
|
+
|
|
26
|
+
export interface TelemetryUsageEvent {
|
|
27
|
+
readonly timestamp?: string;
|
|
28
|
+
readonly request_id?: string;
|
|
29
|
+
readonly session_id?: string;
|
|
30
|
+
readonly actor_id?: string;
|
|
31
|
+
readonly tool?: string;
|
|
32
|
+
readonly status?: string;
|
|
33
|
+
readonly success?: boolean;
|
|
34
|
+
readonly telemetry?: unknown;
|
|
35
|
+
readonly telemetry_status?: string;
|
|
36
|
+
readonly business_args?: Readonly<Record<string, unknown>>;
|
|
37
|
+
readonly args?: Readonly<Record<string, unknown>>;
|
|
38
|
+
readonly mutation_fingerprint?: string;
|
|
39
|
+
readonly mutation_target?: string;
|
|
40
|
+
readonly validation_valid?: boolean;
|
|
41
|
+
readonly semantic_source_hash?: string;
|
|
42
|
+
readonly coverage_by?: string;
|
|
43
|
+
readonly coverage_scope_complete?: boolean;
|
|
44
|
+
readonly coverage_proof_gap_count?: number;
|
|
45
|
+
readonly coverage_receipt_gap_count?: number;
|
|
46
|
+
readonly zero_results?: boolean;
|
|
47
|
+
readonly result_count?: number;
|
|
48
|
+
readonly error_category?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TelemetryAcceptancePolicy {
|
|
52
|
+
readonly eventLimit: number;
|
|
53
|
+
readonly minimumEvents: number;
|
|
54
|
+
readonly maxEvidenceAgeSeconds: number;
|
|
55
|
+
readonly maxFutureSkewSeconds: number;
|
|
56
|
+
readonly telemetryCompletenessMinimum: number;
|
|
57
|
+
readonly validationBeforeUpsertMinimum: number;
|
|
58
|
+
readonly advisorBeforeRequirementWriteMinimum: number;
|
|
59
|
+
readonly sourceLookupZeroResultMaximum: number;
|
|
60
|
+
readonly preflightMaxAgeSeconds: number;
|
|
61
|
+
readonly advisorMaxAgeSeconds: number;
|
|
62
|
+
readonly repeatedMutationFailureThreshold: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TelemetryAcceptanceMetric {
|
|
66
|
+
readonly id: TelemetryMetricId;
|
|
67
|
+
readonly status: TelemetryMetricStatus;
|
|
68
|
+
readonly numerator: number;
|
|
69
|
+
readonly denominator: number;
|
|
70
|
+
readonly rate?: number;
|
|
71
|
+
readonly threshold?: Readonly<{
|
|
72
|
+
operator: ">=" | "<=" | "<";
|
|
73
|
+
value: number;
|
|
74
|
+
}>;
|
|
75
|
+
readonly message: string;
|
|
76
|
+
readonly evidence?: Readonly<Record<string, unknown>>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TelemetryAcceptanceReport {
|
|
80
|
+
readonly version: typeof TELEMETRY_ACCEPTANCE_VERSION;
|
|
81
|
+
readonly status: TelemetryAcceptanceStatus;
|
|
82
|
+
readonly evaluatedAt: string;
|
|
83
|
+
readonly policy: TelemetryAcceptancePolicy;
|
|
84
|
+
readonly scope: Readonly<{
|
|
85
|
+
totalEvents: number;
|
|
86
|
+
evaluatedEvents: number;
|
|
87
|
+
truncated: boolean;
|
|
88
|
+
firstTimestamp: string | null;
|
|
89
|
+
lastTimestamp: string | null;
|
|
90
|
+
evidenceAgeSeconds: number | null;
|
|
91
|
+
fresh: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
readonly metrics: readonly TelemetryAcceptanceMetric[];
|
|
94
|
+
readonly diagnostics: readonly string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const DEFAULT_TELEMETRY_ACCEPTANCE_POLICY = {
|
|
98
|
+
eventLimit: 200,
|
|
99
|
+
minimumEvents: 20,
|
|
100
|
+
maxEvidenceAgeSeconds: 7 * 24 * 60 * 60,
|
|
101
|
+
maxFutureSkewSeconds: 5 * 60,
|
|
102
|
+
telemetryCompletenessMinimum: 0.95,
|
|
103
|
+
validationBeforeUpsertMinimum: 1,
|
|
104
|
+
advisorBeforeRequirementWriteMinimum: 1,
|
|
105
|
+
sourceLookupZeroResultMaximum: 0.2,
|
|
106
|
+
preflightMaxAgeSeconds: 60 * 60,
|
|
107
|
+
advisorMaxAgeSeconds: 24 * 60 * 60,
|
|
108
|
+
repeatedMutationFailureThreshold: 3,
|
|
109
|
+
} as const satisfies TelemetryAcceptancePolicy;
|
|
110
|
+
|
|
111
|
+
type MutableMetric = Omit<TelemetryAcceptanceMetric, "evidence"> & {
|
|
112
|
+
evidence?: Record<string, unknown>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type TimedEvent = {
|
|
116
|
+
readonly event: TelemetryUsageEvent;
|
|
117
|
+
readonly index: number;
|
|
118
|
+
readonly time: number | null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
122
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseTelemetryUsageLog(
|
|
126
|
+
contents: string,
|
|
127
|
+
): TelemetryUsageEvent[] {
|
|
128
|
+
const events: TelemetryUsageEvent[] = [];
|
|
129
|
+
for (const [index, line] of contents.split(/\r?\n/).entries()) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed) continue;
|
|
132
|
+
let parsed: unknown;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(trimmed);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Failed to parse .kb/usage.log line ${index + 1}: ${message}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!isRecord(parsed)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Failed to parse .kb/usage.log line ${index + 1}: expected object`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
events.push(parsed as TelemetryUsageEvent);
|
|
147
|
+
}
|
|
148
|
+
return events;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function eventArgs(
|
|
152
|
+
event: TelemetryUsageEvent,
|
|
153
|
+
): Readonly<Record<string, unknown>> {
|
|
154
|
+
return event.business_args ?? event.args ?? {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function eventSucceeded(event: TelemetryUsageEvent): boolean {
|
|
158
|
+
return event.status === "success" || event.success === true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function eventFailed(event: TelemetryUsageEvent): boolean {
|
|
162
|
+
return event.status === "error" || event.success === false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function telemetryProvided(event: TelemetryUsageEvent): boolean {
|
|
166
|
+
if (event.telemetry_status === "provided") return true;
|
|
167
|
+
if (event.telemetry_status === "missing") return false;
|
|
168
|
+
return event.telemetry !== null && event.telemetry !== undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function timestampMillis(timestamp: string | undefined): number | null {
|
|
172
|
+
if (timestamp === undefined) return null;
|
|
173
|
+
const parsed = Date.parse(timestamp);
|
|
174
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function canonicalValue(value: unknown): unknown {
|
|
178
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
179
|
+
if (!isRecord(value)) return value;
|
|
180
|
+
return Object.fromEntries(
|
|
181
|
+
Object.entries(value)
|
|
182
|
+
.filter(([key]) => key !== "_diagnostic_telemetry")
|
|
183
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
184
|
+
.map(([key, child]) => [key, canonicalValue(child)]),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function mutationFingerprint(event: TelemetryUsageEvent): string {
|
|
189
|
+
return (
|
|
190
|
+
event.mutation_fingerprint ??
|
|
191
|
+
JSON.stringify(canonicalValue(eventArgs(event)))
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function mutationTarget(event: TelemetryUsageEvent): string {
|
|
196
|
+
if (event.mutation_target) return event.mutation_target;
|
|
197
|
+
const args = eventArgs(event);
|
|
198
|
+
const type = typeof args.type === "string" ? args.type : "unknown";
|
|
199
|
+
const id = typeof args.id === "string" ? args.id : mutationFingerprint(event);
|
|
200
|
+
return `${type}:${id}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function rate(numerator: number, denominator: number): number | undefined {
|
|
204
|
+
return denominator === 0 ? undefined : numerator / denominator;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function thresholdStatus(
|
|
208
|
+
actual: number | undefined,
|
|
209
|
+
operator: ">=" | "<=" | "<",
|
|
210
|
+
expected: number,
|
|
211
|
+
): TelemetryMetricStatus {
|
|
212
|
+
if (actual === undefined) return "not_applicable";
|
|
213
|
+
if (operator === ">=") return actual >= expected ? "passed" : "failed";
|
|
214
|
+
if (operator === "<=") return actual <= expected ? "passed" : "failed";
|
|
215
|
+
return actual < expected ? "passed" : "failed";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function withinAge(
|
|
219
|
+
earlier: number | null,
|
|
220
|
+
later: number | null,
|
|
221
|
+
maxAgeSeconds: number,
|
|
222
|
+
): boolean {
|
|
223
|
+
if (earlier === null || later === null || earlier > later) return false;
|
|
224
|
+
return later - earlier <= maxAgeSeconds * 1_000;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function telemetryCorrelationValue(
|
|
228
|
+
event: TelemetryUsageEvent,
|
|
229
|
+
key: "session_id" | "actor_id",
|
|
230
|
+
): string | undefined {
|
|
231
|
+
if (typeof event[key] === "string" && event[key].length > 0) {
|
|
232
|
+
return event[key];
|
|
233
|
+
}
|
|
234
|
+
return isRecord(event.telemetry) && typeof event.telemetry[key] === "string"
|
|
235
|
+
? event.telemetry[key]
|
|
236
|
+
: undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function correlationCompatible(
|
|
240
|
+
evidence: TelemetryUsageEvent,
|
|
241
|
+
action: TelemetryUsageEvent,
|
|
242
|
+
): boolean {
|
|
243
|
+
for (const key of ["session_id", "actor_id"] as const) {
|
|
244
|
+
const evidenceValue = telemetryCorrelationValue(evidence, key);
|
|
245
|
+
const actionValue = telemetryCorrelationValue(action, key);
|
|
246
|
+
if (
|
|
247
|
+
evidenceValue !== undefined &&
|
|
248
|
+
actionValue !== undefined &&
|
|
249
|
+
evidenceValue !== actionValue
|
|
250
|
+
) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function telemetryCompletenessMetric(
|
|
258
|
+
recent: readonly TimedEvent[],
|
|
259
|
+
policy: TelemetryAcceptancePolicy,
|
|
260
|
+
): MutableMetric {
|
|
261
|
+
const complete = recent.filter(({ event }) =>
|
|
262
|
+
telemetryProvided(event),
|
|
263
|
+
).length;
|
|
264
|
+
const actual = rate(complete, recent.length);
|
|
265
|
+
const enoughEvidence = recent.length >= policy.minimumEvents;
|
|
266
|
+
return {
|
|
267
|
+
id: "telemetry_completeness",
|
|
268
|
+
status: enoughEvidence
|
|
269
|
+
? thresholdStatus(actual, ">=", policy.telemetryCompletenessMinimum)
|
|
270
|
+
: "insufficient_evidence",
|
|
271
|
+
numerator: complete,
|
|
272
|
+
denominator: recent.length,
|
|
273
|
+
...(actual !== undefined ? { rate: actual } : {}),
|
|
274
|
+
threshold: {
|
|
275
|
+
operator: ">=",
|
|
276
|
+
value: policy.telemetryCompletenessMinimum,
|
|
277
|
+
},
|
|
278
|
+
message: enoughEvidence
|
|
279
|
+
? `${complete}/${recent.length} recent events include diagnostic telemetry.`
|
|
280
|
+
: `Only ${recent.length}/${policy.minimumEvents} required recent events are available.`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function validationBeforeUpsertMetric(
|
|
285
|
+
all: readonly TimedEvent[],
|
|
286
|
+
recentStart: number,
|
|
287
|
+
policy: TelemetryAcceptancePolicy,
|
|
288
|
+
): MutableMetric {
|
|
289
|
+
const validPreflights = new Map<string, TimedEvent[]>();
|
|
290
|
+
let attempts = 0;
|
|
291
|
+
let validated = 0;
|
|
292
|
+
const unvalidatedTargets = new Set<string>();
|
|
293
|
+
|
|
294
|
+
for (const current of all) {
|
|
295
|
+
if (
|
|
296
|
+
current.event.tool === "kb_validate_upsert" &&
|
|
297
|
+
eventSucceeded(current.event) &&
|
|
298
|
+
current.event.validation_valid !== false
|
|
299
|
+
) {
|
|
300
|
+
const fingerprint = mutationFingerprint(current.event);
|
|
301
|
+
const matches = validPreflights.get(fingerprint) ?? [];
|
|
302
|
+
matches.push(current);
|
|
303
|
+
validPreflights.set(fingerprint, matches);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (current.event.tool !== "kb_upsert" || current.index < recentStart) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
attempts += 1;
|
|
310
|
+
const fingerprint = mutationFingerprint(current.event);
|
|
311
|
+
const match = (validPreflights.get(fingerprint) ?? []).findLast(
|
|
312
|
+
(candidate) =>
|
|
313
|
+
correlationCompatible(candidate.event, current.event) &&
|
|
314
|
+
withinAge(candidate.time, current.time, policy.preflightMaxAgeSeconds),
|
|
315
|
+
);
|
|
316
|
+
if (match !== undefined) {
|
|
317
|
+
validated += 1;
|
|
318
|
+
} else {
|
|
319
|
+
unvalidatedTargets.add(mutationTarget(current.event));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const actual = rate(validated, attempts);
|
|
324
|
+
return {
|
|
325
|
+
id: "validation_before_upsert",
|
|
326
|
+
status: thresholdStatus(actual, ">=", policy.validationBeforeUpsertMinimum),
|
|
327
|
+
numerator: validated,
|
|
328
|
+
denominator: attempts,
|
|
329
|
+
...(actual !== undefined ? { rate: actual } : {}),
|
|
330
|
+
threshold: {
|
|
331
|
+
operator: ">=",
|
|
332
|
+
value: policy.validationBeforeUpsertMinimum,
|
|
333
|
+
},
|
|
334
|
+
message:
|
|
335
|
+
attempts === 0
|
|
336
|
+
? "No upsert attempts occurred in the evaluated window."
|
|
337
|
+
: `${validated}/${attempts} upsert attempts had a recent successful preflight for the exact payload.`,
|
|
338
|
+
evidence: {
|
|
339
|
+
unvalidatedTargets: [...unvalidatedTargets].sort().slice(0, 20),
|
|
340
|
+
preflightMaxAgeSeconds: policy.preflightMaxAgeSeconds,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function advisorBeforeRequirementWriteMetric(
|
|
346
|
+
all: readonly TimedEvent[],
|
|
347
|
+
recentStart: number,
|
|
348
|
+
policy: TelemetryAcceptancePolicy,
|
|
349
|
+
): MutableMetric {
|
|
350
|
+
const advisors = new Map<
|
|
351
|
+
string,
|
|
352
|
+
Array<{
|
|
353
|
+
readonly entry: TimedEvent;
|
|
354
|
+
readonly sourceHash?: string;
|
|
355
|
+
}>
|
|
356
|
+
>();
|
|
357
|
+
let attempts = 0;
|
|
358
|
+
let advised = 0;
|
|
359
|
+
const unadvisedRequirements = new Set<string>();
|
|
360
|
+
|
|
361
|
+
for (const current of all) {
|
|
362
|
+
const args = eventArgs(current.event);
|
|
363
|
+
const id =
|
|
364
|
+
typeof args.id === "string"
|
|
365
|
+
? args.id
|
|
366
|
+
: typeof args.requirementId === "string"
|
|
367
|
+
? args.requirementId
|
|
368
|
+
: undefined;
|
|
369
|
+
if (
|
|
370
|
+
current.event.tool === "kb_semantic_advisor" &&
|
|
371
|
+
eventSucceeded(current.event) &&
|
|
372
|
+
id !== undefined
|
|
373
|
+
) {
|
|
374
|
+
const matches = advisors.get(id) ?? [];
|
|
375
|
+
matches.push({
|
|
376
|
+
entry: current,
|
|
377
|
+
...(current.event.semantic_source_hash !== undefined
|
|
378
|
+
? { sourceHash: current.event.semantic_source_hash }
|
|
379
|
+
: {}),
|
|
380
|
+
});
|
|
381
|
+
advisors.set(id, matches);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (
|
|
385
|
+
current.event.tool !== "kb_upsert" ||
|
|
386
|
+
current.index < recentStart ||
|
|
387
|
+
args.type !== "req"
|
|
388
|
+
) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
attempts += 1;
|
|
392
|
+
const requirementId = id ?? mutationTarget(current.event);
|
|
393
|
+
const advisor = (advisors.get(requirementId) ?? []).findLast(
|
|
394
|
+
(candidate) =>
|
|
395
|
+
(candidate.sourceHash === undefined ||
|
|
396
|
+
current.event.semantic_source_hash === undefined ||
|
|
397
|
+
candidate.sourceHash === current.event.semantic_source_hash) &&
|
|
398
|
+
correlationCompatible(candidate.entry.event, current.event) &&
|
|
399
|
+
withinAge(
|
|
400
|
+
candidate.entry.time,
|
|
401
|
+
current.time,
|
|
402
|
+
policy.advisorMaxAgeSeconds,
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
if (advisor !== undefined) {
|
|
406
|
+
advised += 1;
|
|
407
|
+
} else {
|
|
408
|
+
unadvisedRequirements.add(requirementId);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const actual = rate(advised, attempts);
|
|
413
|
+
return {
|
|
414
|
+
id: "advisor_before_requirement_write",
|
|
415
|
+
status: thresholdStatus(
|
|
416
|
+
actual,
|
|
417
|
+
">=",
|
|
418
|
+
policy.advisorBeforeRequirementWriteMinimum,
|
|
419
|
+
),
|
|
420
|
+
numerator: advised,
|
|
421
|
+
denominator: attempts,
|
|
422
|
+
...(actual !== undefined ? { rate: actual } : {}),
|
|
423
|
+
threshold: {
|
|
424
|
+
operator: ">=",
|
|
425
|
+
value: policy.advisorBeforeRequirementWriteMinimum,
|
|
426
|
+
},
|
|
427
|
+
message:
|
|
428
|
+
attempts === 0
|
|
429
|
+
? "No requirement upsert attempts occurred in the evaluated window."
|
|
430
|
+
: `${advised}/${attempts} requirement upserts followed a recent semantic-advisor pass for the same requirement and source hash when observable.`,
|
|
431
|
+
evidence: {
|
|
432
|
+
unadvisedRequirements: [...unadvisedRequirements].sort().slice(0, 20),
|
|
433
|
+
advisorMaxAgeSeconds: policy.advisorMaxAgeSeconds,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function sourceLookupMetric(
|
|
439
|
+
recent: readonly TimedEvent[],
|
|
440
|
+
policy: TelemetryAcceptancePolicy,
|
|
441
|
+
): MutableMetric {
|
|
442
|
+
const lookups = recent.filter(({ event }) => {
|
|
443
|
+
if (
|
|
444
|
+
(event.tool !== "kb_query" && event.tool !== "kb_search") ||
|
|
445
|
+
!eventSucceeded(event)
|
|
446
|
+
) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return typeof eventArgs(event).sourceFile === "string";
|
|
450
|
+
});
|
|
451
|
+
const zeroResults = lookups.filter(
|
|
452
|
+
({ event }) => event.zero_results === true || event.result_count === 0,
|
|
453
|
+
);
|
|
454
|
+
const actual = rate(zeroResults.length, lookups.length);
|
|
455
|
+
const sourceCounts = new Map<string, number>();
|
|
456
|
+
for (const { event } of zeroResults) {
|
|
457
|
+
const sourceFile = String(eventArgs(event).sourceFile);
|
|
458
|
+
sourceCounts.set(sourceFile, (sourceCounts.get(sourceFile) ?? 0) + 1);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
id: "source_lookup_zero_result_rate",
|
|
462
|
+
status: thresholdStatus(actual, "<=", policy.sourceLookupZeroResultMaximum),
|
|
463
|
+
numerator: zeroResults.length,
|
|
464
|
+
denominator: lookups.length,
|
|
465
|
+
...(actual !== undefined ? { rate: actual } : {}),
|
|
466
|
+
threshold: {
|
|
467
|
+
operator: "<=",
|
|
468
|
+
value: policy.sourceLookupZeroResultMaximum,
|
|
469
|
+
},
|
|
470
|
+
message:
|
|
471
|
+
lookups.length === 0
|
|
472
|
+
? "No source-linked query or search calls occurred in the evaluated window."
|
|
473
|
+
: `${zeroResults.length}/${lookups.length} source-linked lookups returned zero results.`,
|
|
474
|
+
evidence: {
|
|
475
|
+
zeroResultSourceFiles: [...sourceCounts.entries()]
|
|
476
|
+
.sort(
|
|
477
|
+
([leftFile, leftCount], [rightFile, rightCount]) =>
|
|
478
|
+
rightCount - leftCount || leftFile.localeCompare(rightFile),
|
|
479
|
+
)
|
|
480
|
+
.slice(0, 20)
|
|
481
|
+
.map(([sourceFile, count]) => ({ sourceFile, count })),
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function completeRequirementCoverageEvents(
|
|
487
|
+
recent: readonly TimedEvent[],
|
|
488
|
+
): readonly TimedEvent[] {
|
|
489
|
+
return recent.filter(
|
|
490
|
+
({ event }) =>
|
|
491
|
+
event.tool === "kb_coverage" &&
|
|
492
|
+
eventSucceeded(event) &&
|
|
493
|
+
(event.coverage_by ?? "req") === "req" &&
|
|
494
|
+
event.coverage_scope_complete === true &&
|
|
495
|
+
typeof event.coverage_proof_gap_count === "number",
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function proofGapRecoveryMetric(recent: readonly TimedEvent[]): MutableMetric {
|
|
500
|
+
const coverage = completeRequirementCoverageEvents(recent);
|
|
501
|
+
const latest = coverage.at(-1)?.event.coverage_proof_gap_count;
|
|
502
|
+
const previous = coverage.at(-2)?.event.coverage_proof_gap_count;
|
|
503
|
+
if (latest === undefined) {
|
|
504
|
+
return {
|
|
505
|
+
id: "proof_gap_recovery",
|
|
506
|
+
status: "insufficient_evidence",
|
|
507
|
+
numerator: 0,
|
|
508
|
+
denominator: 0,
|
|
509
|
+
message:
|
|
510
|
+
"No complete requirement coverage event with proof-gap telemetry is available.",
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (latest === 0) {
|
|
514
|
+
return {
|
|
515
|
+
id: "proof_gap_recovery",
|
|
516
|
+
status: "passed",
|
|
517
|
+
numerator: previous === undefined ? 0 : Math.max(0, previous - latest),
|
|
518
|
+
denominator: previous ?? latest,
|
|
519
|
+
message:
|
|
520
|
+
"The latest complete requirement coverage event has zero proof gaps.",
|
|
521
|
+
evidence: { previousGapCount: previous ?? null, latestGapCount: latest },
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
if (previous === undefined) {
|
|
525
|
+
return {
|
|
526
|
+
id: "proof_gap_recovery",
|
|
527
|
+
status: "insufficient_evidence",
|
|
528
|
+
numerator: 0,
|
|
529
|
+
denominator: latest,
|
|
530
|
+
message: `The latest complete coverage event reports ${latest} proof gaps, but no comparable earlier event is available.`,
|
|
531
|
+
evidence: { previousGapCount: null, latestGapCount: latest },
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const recovered = Math.max(0, previous - latest);
|
|
535
|
+
return {
|
|
536
|
+
id: "proof_gap_recovery",
|
|
537
|
+
status: latest < previous ? "passed" : "failed",
|
|
538
|
+
numerator: recovered,
|
|
539
|
+
denominator: previous,
|
|
540
|
+
rate: previous === 0 ? 0 : recovered / previous,
|
|
541
|
+
threshold: { operator: "<", value: previous },
|
|
542
|
+
message:
|
|
543
|
+
latest < previous
|
|
544
|
+
? `Proof gaps decreased from ${previous} to ${latest}.`
|
|
545
|
+
: `Proof gaps did not recover: the previous complete report had ${previous} and the latest has ${latest}.`,
|
|
546
|
+
evidence: { previousGapCount: previous, latestGapCount: latest },
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function receiptFreshnessMetric(recent: readonly TimedEvent[]): MutableMetric {
|
|
551
|
+
const latest = completeRequirementCoverageEvents(recent).at(-1)?.event;
|
|
552
|
+
if (latest === undefined || latest.coverage_receipt_gap_count === undefined) {
|
|
553
|
+
return {
|
|
554
|
+
id: "e2e_receipt_freshness",
|
|
555
|
+
status: "insufficient_evidence",
|
|
556
|
+
numerator: 0,
|
|
557
|
+
denominator: 0,
|
|
558
|
+
message:
|
|
559
|
+
"No complete requirement coverage event with receipt-freshness telemetry is available.",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const gapCount = latest.coverage_receipt_gap_count;
|
|
563
|
+
return {
|
|
564
|
+
id: "e2e_receipt_freshness",
|
|
565
|
+
status: gapCount === 0 ? "passed" : "failed",
|
|
566
|
+
numerator: gapCount,
|
|
567
|
+
denominator: Number(latest.coverage_proof_gap_count ?? gapCount),
|
|
568
|
+
threshold: { operator: "<=", value: 0 },
|
|
569
|
+
message:
|
|
570
|
+
gapCount === 0
|
|
571
|
+
? "The latest complete requirement coverage event has no receipt-specific gaps."
|
|
572
|
+
: `The latest complete requirement coverage event has ${gapCount} missing, stale, failed, invalid, or uncheckable receipt gaps.`,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function repeatedMutationFailuresMetric(
|
|
577
|
+
recent: readonly TimedEvent[],
|
|
578
|
+
policy: TelemetryAcceptancePolicy,
|
|
579
|
+
): MutableMetric {
|
|
580
|
+
const streaks = new Map<string, number>();
|
|
581
|
+
const maxima = new Map<string, number>();
|
|
582
|
+
const categories = new Map<string, Set<string>>();
|
|
583
|
+
let attempts = 0;
|
|
584
|
+
|
|
585
|
+
for (const { event } of recent) {
|
|
586
|
+
if (event.tool !== "kb_upsert") continue;
|
|
587
|
+
attempts += 1;
|
|
588
|
+
const target = mutationTarget(event);
|
|
589
|
+
if (eventFailed(event)) {
|
|
590
|
+
const next = (streaks.get(target) ?? 0) + 1;
|
|
591
|
+
streaks.set(target, next);
|
|
592
|
+
maxima.set(target, Math.max(maxima.get(target) ?? 0, next));
|
|
593
|
+
if (event.error_category) {
|
|
594
|
+
const targetCategories = categories.get(target) ?? new Set<string>();
|
|
595
|
+
targetCategories.add(event.error_category);
|
|
596
|
+
categories.set(target, targetCategories);
|
|
597
|
+
}
|
|
598
|
+
} else if (eventSucceeded(event)) {
|
|
599
|
+
streaks.set(target, 0);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const repeated = [...maxima.entries()]
|
|
604
|
+
.filter(([, count]) => count >= policy.repeatedMutationFailureThreshold)
|
|
605
|
+
.sort(
|
|
606
|
+
([leftTarget, leftCount], [rightTarget, rightCount]) =>
|
|
607
|
+
rightCount - leftCount || leftTarget.localeCompare(rightTarget),
|
|
608
|
+
);
|
|
609
|
+
return {
|
|
610
|
+
id: "repeated_mutation_failures",
|
|
611
|
+
status:
|
|
612
|
+
attempts === 0
|
|
613
|
+
? "not_applicable"
|
|
614
|
+
: repeated.length === 0
|
|
615
|
+
? "passed"
|
|
616
|
+
: "failed",
|
|
617
|
+
numerator: repeated.length,
|
|
618
|
+
denominator: attempts,
|
|
619
|
+
threshold: {
|
|
620
|
+
operator: "<",
|
|
621
|
+
value: policy.repeatedMutationFailureThreshold,
|
|
622
|
+
},
|
|
623
|
+
message:
|
|
624
|
+
attempts === 0
|
|
625
|
+
? "No upsert attempts occurred in the evaluated window."
|
|
626
|
+
: repeated.length === 0
|
|
627
|
+
? `No target reached ${policy.repeatedMutationFailureThreshold} consecutive mutation failures.`
|
|
628
|
+
: `${repeated.length} target(s) reached at least ${policy.repeatedMutationFailureThreshold} consecutive mutation failures.`,
|
|
629
|
+
evidence: {
|
|
630
|
+
targets: repeated.slice(0, 20).map(([target, count]) => ({
|
|
631
|
+
target,
|
|
632
|
+
consecutiveFailures: count,
|
|
633
|
+
errorCategories: [...(categories.get(target) ?? [])].sort(),
|
|
634
|
+
})),
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function reportStatus(
|
|
640
|
+
metrics: readonly TelemetryAcceptanceMetric[],
|
|
641
|
+
fresh: boolean,
|
|
642
|
+
): TelemetryAcceptanceStatus {
|
|
643
|
+
if (!fresh) return "insufficient_evidence";
|
|
644
|
+
if (metrics.some((metric) => metric.status === "failed")) return "failed";
|
|
645
|
+
if (metrics.some((metric) => metric.status === "insufficient_evidence")) {
|
|
646
|
+
return "insufficient_evidence";
|
|
647
|
+
}
|
|
648
|
+
return "passed";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function analyzeTelemetryAcceptance(
|
|
652
|
+
events: readonly TelemetryUsageEvent[],
|
|
653
|
+
now: Date = new Date(),
|
|
654
|
+
policy: TelemetryAcceptancePolicy = DEFAULT_TELEMETRY_ACCEPTANCE_POLICY,
|
|
655
|
+
): TelemetryAcceptanceReport {
|
|
656
|
+
const all = events.map((event, index) => ({
|
|
657
|
+
event,
|
|
658
|
+
index,
|
|
659
|
+
time: timestampMillis(event.timestamp),
|
|
660
|
+
}));
|
|
661
|
+
const recentStart = Math.max(0, all.length - policy.eventLimit);
|
|
662
|
+
const recent = all.slice(recentStart);
|
|
663
|
+
const validTimes = recent
|
|
664
|
+
.map((entry) => entry.time)
|
|
665
|
+
.filter((value): value is number => value !== null)
|
|
666
|
+
.sort((left, right) => left - right);
|
|
667
|
+
const firstTime = validTimes[0] ?? null;
|
|
668
|
+
const lastTime = validTimes.at(-1) ?? null;
|
|
669
|
+
const ageSeconds =
|
|
670
|
+
lastTime === null ? null : Math.round((now.getTime() - lastTime) / 1_000);
|
|
671
|
+
const fresh =
|
|
672
|
+
ageSeconds !== null &&
|
|
673
|
+
ageSeconds <= policy.maxEvidenceAgeSeconds &&
|
|
674
|
+
ageSeconds >= -policy.maxFutureSkewSeconds;
|
|
675
|
+
|
|
676
|
+
const metrics: readonly TelemetryAcceptanceMetric[] = [
|
|
677
|
+
telemetryCompletenessMetric(recent, policy),
|
|
678
|
+
advisorBeforeRequirementWriteMetric(all, recentStart, policy),
|
|
679
|
+
validationBeforeUpsertMetric(all, recentStart, policy),
|
|
680
|
+
sourceLookupMetric(recent, policy),
|
|
681
|
+
proofGapRecoveryMetric(recent),
|
|
682
|
+
receiptFreshnessMetric(recent),
|
|
683
|
+
repeatedMutationFailuresMetric(recent, policy),
|
|
684
|
+
];
|
|
685
|
+
const diagnostics: string[] = [];
|
|
686
|
+
if (events.length === 0) diagnostics.push("usage_log_empty");
|
|
687
|
+
if (lastTime === null && events.length > 0) {
|
|
688
|
+
diagnostics.push("usage_log_timestamps_unavailable");
|
|
689
|
+
} else if (!fresh) {
|
|
690
|
+
diagnostics.push(
|
|
691
|
+
ageSeconds !== null && ageSeconds < -policy.maxFutureSkewSeconds
|
|
692
|
+
? "usage_log_future_dated"
|
|
693
|
+
: "usage_log_stale",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
version: TELEMETRY_ACCEPTANCE_VERSION,
|
|
699
|
+
status: reportStatus(metrics, fresh),
|
|
700
|
+
evaluatedAt: now.toISOString(),
|
|
701
|
+
policy,
|
|
702
|
+
scope: {
|
|
703
|
+
totalEvents: events.length,
|
|
704
|
+
evaluatedEvents: recent.length,
|
|
705
|
+
truncated: events.length > recent.length,
|
|
706
|
+
firstTimestamp:
|
|
707
|
+
firstTime === null ? null : new Date(firstTime).toISOString(),
|
|
708
|
+
lastTimestamp:
|
|
709
|
+
lastTime === null ? null : new Date(lastTime).toISOString(),
|
|
710
|
+
evidenceAgeSeconds: ageSeconds,
|
|
711
|
+
fresh,
|
|
712
|
+
},
|
|
713
|
+
metrics,
|
|
714
|
+
diagnostics,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
type DiagnosticDefinition = {
|
|
719
|
+
readonly id: string;
|
|
720
|
+
readonly severity: "warning" | "review";
|
|
721
|
+
readonly rank: number;
|
|
722
|
+
readonly suggestion: string;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const METRIC_DIAGNOSTICS: Readonly<
|
|
726
|
+
Record<TelemetryMetricId, DiagnosticDefinition>
|
|
727
|
+
> = {
|
|
728
|
+
telemetry_completeness: {
|
|
729
|
+
id: "telemetry_completeness_low",
|
|
730
|
+
severity: "warning",
|
|
731
|
+
rank: 70,
|
|
732
|
+
suggestion:
|
|
733
|
+
"Enable diagnostic mode and supply complete _diagnostic_telemetry on every MCP operation, then rerun the workflow.",
|
|
734
|
+
},
|
|
735
|
+
advisor_before_requirement_write: {
|
|
736
|
+
id: "semantic_advisor_bypassed",
|
|
737
|
+
severity: "warning",
|
|
738
|
+
rank: 30,
|
|
739
|
+
suggestion:
|
|
740
|
+
"Run kb_semantic_advisor on the complete current requirement prose before its next upsert, preserve the returned source hash, and recheck telemetry.",
|
|
741
|
+
},
|
|
742
|
+
validation_before_upsert: {
|
|
743
|
+
id: "mutation_validation_bypassed",
|
|
744
|
+
severity: "warning",
|
|
745
|
+
rank: 20,
|
|
746
|
+
suggestion:
|
|
747
|
+
"Run kb_validate_upsert for the exact payload no more than one hour before each sequential kb_upsert attempt.",
|
|
748
|
+
},
|
|
749
|
+
source_lookup_zero_result_rate: {
|
|
750
|
+
id: "source_lookup_zero_result_rate_high",
|
|
751
|
+
severity: "warning",
|
|
752
|
+
rank: 60,
|
|
753
|
+
suggestion:
|
|
754
|
+
"Inspect the cited source files, refresh symbol/source links when stale, and repeat focused kb_search then exact kb_query lookups.",
|
|
755
|
+
},
|
|
756
|
+
proof_gap_recovery: {
|
|
757
|
+
id: "proof_gap_recovery_stalled",
|
|
758
|
+
severity: "warning",
|
|
759
|
+
rank: 50,
|
|
760
|
+
suggestion:
|
|
761
|
+
"Run complete requirement coverage, apply one reviewed ready repair batch per requirement, and rerun coverage to demonstrate a smaller gap count.",
|
|
762
|
+
},
|
|
763
|
+
e2e_receipt_freshness: {
|
|
764
|
+
id: "e2e_receipt_freshness_low",
|
|
765
|
+
severity: "warning",
|
|
766
|
+
rank: 40,
|
|
767
|
+
suggestion:
|
|
768
|
+
"Run each scenario-backed E2E test against the live snapshot, append a passing kibi.verification-receipt.v1, sync, and rerun complete coverage.",
|
|
769
|
+
},
|
|
770
|
+
repeated_mutation_failures: {
|
|
771
|
+
id: "repeated_mutation_failures",
|
|
772
|
+
severity: "warning",
|
|
773
|
+
rank: 10,
|
|
774
|
+
suggestion:
|
|
775
|
+
"Stop retrying the failing target; query current endpoints, preflight the exact reduced payload, repair runtime health if needed, and retry once.",
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
export function createTelemetryAcceptanceDiagnostics(
|
|
780
|
+
report: TelemetryAcceptanceReport,
|
|
781
|
+
): readonly QualityDiagnostic[] {
|
|
782
|
+
const diagnostics: Array<QualityDiagnostic & { readonly rank: number }> = [];
|
|
783
|
+
if (!report.scope.fresh) {
|
|
784
|
+
diagnostics.push({
|
|
785
|
+
id: "telemetry_evidence_stale",
|
|
786
|
+
severity: "review",
|
|
787
|
+
blocking: false,
|
|
788
|
+
category: "telemetry",
|
|
789
|
+
source: ".kb/usage.log",
|
|
790
|
+
message:
|
|
791
|
+
report.scope.lastTimestamp === null
|
|
792
|
+
? "Telemetry acceptance has no valid timestamped evidence, so current workflow quality is unresolved."
|
|
793
|
+
: `Telemetry evidence is not current (latest ${report.scope.lastTimestamp}); historical metrics cannot prove current workflow quality.`,
|
|
794
|
+
suggestion:
|
|
795
|
+
"Run the current Kibi workflow in diagnostic mode, then rerun kibi usage-metrics --require-acceptance and an unfiltered kb_check.",
|
|
796
|
+
evidence: {
|
|
797
|
+
version: report.version,
|
|
798
|
+
rank: 5,
|
|
799
|
+
scope: report.scope,
|
|
800
|
+
diagnostics: report.diagnostics,
|
|
801
|
+
},
|
|
802
|
+
rank: 5,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const insufficient = report.metrics.filter(
|
|
807
|
+
(metric) => metric.status === "insufficient_evidence",
|
|
808
|
+
);
|
|
809
|
+
if (insufficient.length > 0) {
|
|
810
|
+
diagnostics.push({
|
|
811
|
+
id: "telemetry_acceptance_incomplete",
|
|
812
|
+
severity: "review",
|
|
813
|
+
blocking: false,
|
|
814
|
+
category: "telemetry",
|
|
815
|
+
source: ".kb/usage.log",
|
|
816
|
+
message: `Telemetry cannot evaluate ${
|
|
817
|
+
insufficient.length
|
|
818
|
+
} required metric(s): ${insufficient
|
|
819
|
+
.map((metric) => metric.id)
|
|
820
|
+
.join(", ")}.`,
|
|
821
|
+
suggestion:
|
|
822
|
+
"Capture complete diagnostic events, including a complete requirement coverage run, then rerun the telemetry acceptance report.",
|
|
823
|
+
evidence: {
|
|
824
|
+
version: report.version,
|
|
825
|
+
rank: 80,
|
|
826
|
+
metrics: insufficient,
|
|
827
|
+
},
|
|
828
|
+
rank: 80,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const metric of report.metrics) {
|
|
833
|
+
if (metric.status !== "failed") continue;
|
|
834
|
+
const definition = METRIC_DIAGNOSTICS[metric.id];
|
|
835
|
+
diagnostics.push({
|
|
836
|
+
id: definition.id,
|
|
837
|
+
severity: definition.severity,
|
|
838
|
+
blocking: false,
|
|
839
|
+
category: "telemetry",
|
|
840
|
+
source: ".kb/usage.log",
|
|
841
|
+
message: metric.message,
|
|
842
|
+
suggestion: definition.suggestion,
|
|
843
|
+
evidence: {
|
|
844
|
+
version: report.version,
|
|
845
|
+
rank: definition.rank,
|
|
846
|
+
metric,
|
|
847
|
+
scope: report.scope,
|
|
848
|
+
},
|
|
849
|
+
rank: definition.rank,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return diagnostics
|
|
854
|
+
.sort(
|
|
855
|
+
(left, right) =>
|
|
856
|
+
left.rank - right.rank || left.id.localeCompare(right.id),
|
|
857
|
+
)
|
|
858
|
+
.map(({ rank: _rank, ...diagnostic }) => diagnostic);
|
|
859
|
+
}
|