ds4-context-core 0.1.1 → 0.2.0-beta.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/README.md +17 -4
- package/dist/adapter/conformance.d.ts +35 -0
- package/dist/adapter/conformance.d.ts.map +1 -0
- package/dist/adapter/conformance.js +260 -0
- package/dist/adapter/conformance.js.map +1 -0
- package/dist/adapter/runtime-adapter.d.ts +119 -0
- package/dist/adapter/runtime-adapter.d.ts.map +1 -0
- package/dist/adapter/runtime-adapter.js +289 -0
- package/dist/adapter/runtime-adapter.js.map +1 -0
- package/dist/config/config-loader.d.ts +1 -0
- package/dist/config/config-loader.d.ts.map +1 -1
- package/dist/config/config-loader.js +107 -1
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config.d.ts +45 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +28 -0
- package/dist/config/config.js.map +1 -1
- package/dist/core/canonical-message.d.ts +2 -1
- package/dist/core/canonical-message.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest/context-manifest.d.ts +12 -0
- package/dist/manifest/context-manifest.d.ts.map +1 -1
- package/dist/memory/memory-manager.d.ts +7 -1
- package/dist/memory/memory-manager.d.ts.map +1 -1
- package/dist/memory/memory-manager.js +100 -3
- package/dist/memory/memory-manager.js.map +1 -1
- package/dist/memory/memory-types.d.ts +43 -0
- package/dist/memory/memory-types.d.ts.map +1 -1
- package/dist/memory/memory-types.js.map +1 -1
- package/dist/persistence/migrations.d.ts +2 -1
- package/dist/persistence/migrations.d.ts.map +1 -1
- package/dist/persistence/migrations.js +155 -25
- package/dist/persistence/migrations.js.map +1 -1
- package/dist/persistence/repositories/artifact-repository.d.ts +3 -1
- package/dist/persistence/repositories/artifact-repository.d.ts.map +1 -1
- package/dist/persistence/repositories/artifact-repository.js +16 -27
- package/dist/persistence/repositories/artifact-repository.js.map +1 -1
- package/dist/persistence/repositories/context-manifest-repository.d.ts +3 -1
- package/dist/persistence/repositories/context-manifest-repository.d.ts.map +1 -1
- package/dist/persistence/repositories/context-manifest-repository.js +10 -18
- package/dist/persistence/repositories/context-manifest-repository.js.map +1 -1
- package/dist/persistence/repositories/context-quality-repository.d.ts +21 -0
- package/dist/persistence/repositories/context-quality-repository.d.ts.map +1 -0
- package/dist/persistence/repositories/context-quality-repository.js +83 -0
- package/dist/persistence/repositories/context-quality-repository.js.map +1 -0
- package/dist/persistence/repositories/embedding-repository.d.ts +37 -0
- package/dist/persistence/repositories/embedding-repository.d.ts.map +1 -0
- package/dist/persistence/repositories/embedding-repository.js +142 -0
- package/dist/persistence/repositories/embedding-repository.js.map +1 -0
- package/dist/persistence/repositories/lease-repository.d.ts +22 -0
- package/dist/persistence/repositories/lease-repository.d.ts.map +1 -0
- package/dist/persistence/repositories/lease-repository.js +113 -0
- package/dist/persistence/repositories/lease-repository.js.map +1 -0
- package/dist/persistence/repositories/memory-repository.d.ts +34 -3
- package/dist/persistence/repositories/memory-repository.d.ts.map +1 -1
- package/dist/persistence/repositories/memory-repository.js +426 -64
- package/dist/persistence/repositories/memory-repository.js.map +1 -1
- package/dist/persistence/repositories/project-knowledge-repository.d.ts +28 -5
- package/dist/persistence/repositories/project-knowledge-repository.d.ts.map +1 -1
- package/dist/persistence/repositories/project-knowledge-repository.js +123 -53
- package/dist/persistence/repositories/project-knowledge-repository.js.map +1 -1
- package/dist/persistence/repositories/session-index-repository.d.ts +9 -1
- package/dist/persistence/repositories/session-index-repository.d.ts.map +1 -1
- package/dist/persistence/repositories/session-index-repository.js +38 -17
- package/dist/persistence/repositories/session-index-repository.js.map +1 -1
- package/dist/persistence/repositories/summary-repository.d.ts +3 -1
- package/dist/persistence/repositories/summary-repository.d.ts.map +1 -1
- package/dist/persistence/repositories/summary-repository.js +35 -31
- package/dist/persistence/repositories/summary-repository.js.map +1 -1
- package/dist/persistence/sqlite.d.ts +10 -0
- package/dist/persistence/sqlite.d.ts.map +1 -1
- package/dist/persistence/sqlite.js +41 -12
- package/dist/persistence/sqlite.js.map +1 -1
- package/dist/persistence/write-coordinator.d.ts +37 -0
- package/dist/persistence/write-coordinator.d.ts.map +1 -0
- package/dist/persistence/write-coordinator.js +120 -0
- package/dist/persistence/write-coordinator.js.map +1 -0
- package/dist/planner/context-planner.d.ts +5 -0
- package/dist/planner/context-planner.d.ts.map +1 -1
- package/dist/planner/context-planner.js +41 -62
- package/dist/planner/context-planner.js.map +1 -1
- package/dist/project/file-indexer.d.ts +5 -3
- package/dist/project/file-indexer.d.ts.map +1 -1
- package/dist/project/file-indexer.js +108 -42
- package/dist/project/file-indexer.js.map +1 -1
- package/dist/project/project-knowledge.d.ts +15 -3
- package/dist/project/project-knowledge.d.ts.map +1 -1
- package/dist/project/project-knowledge.js +168 -42
- package/dist/project/project-knowledge.js.map +1 -1
- package/dist/project/symbol-parser.d.ts +48 -0
- package/dist/project/symbol-parser.d.ts.map +1 -0
- package/dist/project/symbol-parser.js +427 -0
- package/dist/project/symbol-parser.js.map +1 -0
- package/dist/quality/context-quality.d.ts +172 -0
- package/dist/quality/context-quality.d.ts.map +1 -0
- package/dist/quality/context-quality.js +624 -0
- package/dist/quality/context-quality.js.map +1 -0
- package/dist/ranking/learned-ranker.d.ts +174 -0
- package/dist/ranking/learned-ranker.d.ts.map +1 -0
- package/dist/ranking/learned-ranker.js +782 -0
- package/dist/ranking/learned-ranker.js.map +1 -0
- package/dist/retrieval/embedding.d.ts +92 -0
- package/dist/retrieval/embedding.d.ts.map +1 -0
- package/dist/retrieval/embedding.js +56 -0
- package/dist/retrieval/embedding.js.map +1 -0
- package/dist/retrieval/retrieval-engine.d.ts +9 -1
- package/dist/retrieval/retrieval-engine.d.ts.map +1 -1
- package/dist/retrieval/retrieval-engine.js +105 -7
- package/dist/retrieval/retrieval-engine.js.map +1 -1
- package/dist/retrieval/semantic-index.d.ts +30 -0
- package/dist/retrieval/semantic-index.d.ts.map +1 -0
- package/dist/retrieval/semantic-index.js +284 -0
- package/dist/retrieval/semantic-index.js.map +1 -0
- package/dist/retrieval/semantic-quality.d.ts +52 -0
- package/dist/retrieval/semantic-quality.d.ts.map +1 -0
- package/dist/retrieval/semantic-quality.js +117 -0
- package/dist/retrieval/semantic-quality.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { sha256 } from "../shared/hash.js";
|
|
2
|
+
export const CONTEXT_QUALITY_SCHEMA_VERSION = 1;
|
|
3
|
+
export const CONTEXT_QUALITY_METRICS_VERSION = "context-quality-v1";
|
|
4
|
+
export const LIVE_QUALITY_CORPUS_VERSION = "live-observation-v1";
|
|
5
|
+
export const STATIC_RANKING_STRATEGY_ID = "static-ranking-v0.1";
|
|
6
|
+
export const CANDIDATE_RANKING_STRATEGY_ID = "task-weighted-v0.2-candidate";
|
|
7
|
+
const RATE_SCALE = 1_000_000;
|
|
8
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u;
|
|
9
|
+
const REASON_PATTERN = /^[a-z0-9][a-z0-9.-]{0,63}$/u;
|
|
10
|
+
function rounded(value) {
|
|
11
|
+
return Math.round(value * RATE_SCALE) / RATE_SCALE;
|
|
12
|
+
}
|
|
13
|
+
function countRate(numerator, denominator) {
|
|
14
|
+
return {
|
|
15
|
+
numerator,
|
|
16
|
+
denominator,
|
|
17
|
+
rate: denominator > 0 ? rounded(numerator / denominator) : null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function sortedUnique(values) {
|
|
21
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
22
|
+
}
|
|
23
|
+
function increment(counts, key, amount = 1) {
|
|
24
|
+
counts[key] = (counts[key] ?? 0) + amount;
|
|
25
|
+
}
|
|
26
|
+
function sortedCounts(counts) {
|
|
27
|
+
return Object.fromEntries(Object.entries(counts)
|
|
28
|
+
.filter(([, value]) => Number.isSafeInteger(value) && value >= 0)
|
|
29
|
+
.sort(([left], [right]) => left.localeCompare(right)));
|
|
30
|
+
}
|
|
31
|
+
function sortedKinds(values) {
|
|
32
|
+
return Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right)));
|
|
33
|
+
}
|
|
34
|
+
function metricRate(metric, defaultValue = 1) {
|
|
35
|
+
return metric.rate ?? defaultValue;
|
|
36
|
+
}
|
|
37
|
+
function qualityScore(input) {
|
|
38
|
+
const evidence = metricRate(input.evidenceRecall);
|
|
39
|
+
const relevance = 1 - metricRate(input.irrelevantTokenRatio, 0);
|
|
40
|
+
const provenance = metricRate(input.provenanceCoverage);
|
|
41
|
+
const current = metricRate(input.currentRequestRetention);
|
|
42
|
+
const atomic = metricRate(input.atomicGroupValidity);
|
|
43
|
+
const reliability = Math.max(0, 1 - Math.max(input.overflowRate, input.fallbackRate));
|
|
44
|
+
return rounded(evidence * 0.35
|
|
45
|
+
+ relevance * 0.2
|
|
46
|
+
+ provenance * 0.15
|
|
47
|
+
+ current * 0.15
|
|
48
|
+
+ atomic * 0.1
|
|
49
|
+
+ reliability * 0.05);
|
|
50
|
+
}
|
|
51
|
+
function validateIdentifier(value, name) {
|
|
52
|
+
if (!IDENTIFIER_PATTERN.test(value))
|
|
53
|
+
throw new Error(`${name} is not a safe quality identifier`);
|
|
54
|
+
}
|
|
55
|
+
export function validateQualityFixture(fixture) {
|
|
56
|
+
if (fixture.schemaVersion !== CONTEXT_QUALITY_SCHEMA_VERSION) {
|
|
57
|
+
throw new Error("Unsupported quality fixture schema version");
|
|
58
|
+
}
|
|
59
|
+
for (const [name, value] of [
|
|
60
|
+
["corpusVersion", fixture.corpusVersion],
|
|
61
|
+
["sampleId", fixture.sampleId],
|
|
62
|
+
["plannerVersion", fixture.plannerVersion],
|
|
63
|
+
["profileKey", fixture.profileKey],
|
|
64
|
+
["task.taskId", fixture.task.taskId],
|
|
65
|
+
["task.taskType", fixture.task.taskType],
|
|
66
|
+
])
|
|
67
|
+
validateIdentifier(value, name);
|
|
68
|
+
if (!Number.isSafeInteger(fixture.totalBudgetTokens) || fixture.totalBudgetTokens < 0) {
|
|
69
|
+
throw new Error("Quality fixture totalBudgetTokens must be a non-negative integer");
|
|
70
|
+
}
|
|
71
|
+
const candidateIds = new Set();
|
|
72
|
+
for (const candidate of fixture.candidates) {
|
|
73
|
+
validateIdentifier(candidate.id, "candidate.id");
|
|
74
|
+
validateIdentifier(candidate.groupId, "candidate.groupId");
|
|
75
|
+
if (candidateIds.has(candidate.id))
|
|
76
|
+
throw new Error(`Duplicate quality candidate ID: ${candidate.id}`);
|
|
77
|
+
candidateIds.add(candidate.id);
|
|
78
|
+
if (!Number.isSafeInteger(candidate.tokens) || candidate.tokens < 0) {
|
|
79
|
+
throw new Error(`Quality candidate ${candidate.id} has invalid tokens`);
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isFinite(candidate.staticScore)
|
|
82
|
+
|| (candidate.candidateScore !== undefined && !Number.isFinite(candidate.candidateScore))) {
|
|
83
|
+
throw new Error(`Quality candidate ${candidate.id} has invalid score metadata`);
|
|
84
|
+
}
|
|
85
|
+
for (const sourceId of candidate.sourceIds)
|
|
86
|
+
validateIdentifier(sourceId, "candidate.sourceId");
|
|
87
|
+
}
|
|
88
|
+
for (const sourceId of fixture.expectedEvidenceSourceIds)
|
|
89
|
+
validateIdentifier(sourceId, "expectedEvidenceSourceId");
|
|
90
|
+
for (const tag of fixture.task.queryTags)
|
|
91
|
+
validateIdentifier(tag, "task.queryTag");
|
|
92
|
+
for (const label of fixture.outcomeLabels ?? [])
|
|
93
|
+
validateIdentifier(label, "outcomeLabel");
|
|
94
|
+
for (const [kind, limit] of Object.entries(fixture.categoryBudgets)) {
|
|
95
|
+
if (!Number.isSafeInteger(limit) || limit < 0)
|
|
96
|
+
throw new Error(`Invalid category budget for ${kind}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function planWithScore(fixture, strategyId, scoreFor) {
|
|
100
|
+
validateQualityFixture(fixture);
|
|
101
|
+
const groups = new Map();
|
|
102
|
+
for (const candidate of fixture.candidates) {
|
|
103
|
+
const group = groups.get(candidate.groupId) ?? [];
|
|
104
|
+
group.push(candidate);
|
|
105
|
+
groups.set(candidate.groupId, group);
|
|
106
|
+
}
|
|
107
|
+
const orderedGroups = [...groups.entries()]
|
|
108
|
+
.map(([groupId, candidates]) => ({
|
|
109
|
+
groupId,
|
|
110
|
+
candidates: [...candidates].sort((left, right) => left.id.localeCompare(right.id)),
|
|
111
|
+
mandatory: candidates.some((candidate) => candidate.currentRequest === true),
|
|
112
|
+
score: Math.max(...candidates.map(scoreFor)),
|
|
113
|
+
tokens: candidates.reduce((total, candidate) => total + candidate.tokens, 0),
|
|
114
|
+
}))
|
|
115
|
+
.sort((left, right) => Number(right.mandatory) - Number(left.mandatory)
|
|
116
|
+
|| right.score - left.score
|
|
117
|
+
|| left.groupId.localeCompare(right.groupId));
|
|
118
|
+
const selected = new Set();
|
|
119
|
+
const selectedByKind = {};
|
|
120
|
+
let selectedTokens = 0;
|
|
121
|
+
let fallback = false;
|
|
122
|
+
for (const group of orderedGroups) {
|
|
123
|
+
const categoryAdditions = {};
|
|
124
|
+
for (const candidate of group.candidates) {
|
|
125
|
+
categoryAdditions[candidate.kind] = (categoryAdditions[candidate.kind] ?? 0) + candidate.tokens;
|
|
126
|
+
}
|
|
127
|
+
const fitsTotal = selectedTokens + group.tokens <= fixture.totalBudgetTokens;
|
|
128
|
+
const fitsCategories = Object.entries(categoryAdditions).every(([kind, tokens]) => {
|
|
129
|
+
const limit = fixture.categoryBudgets[kind];
|
|
130
|
+
return limit === undefined || (selectedByKind[kind] ?? 0) + tokens <= limit;
|
|
131
|
+
});
|
|
132
|
+
if (!fitsTotal || !fitsCategories) {
|
|
133
|
+
if (group.mandatory)
|
|
134
|
+
fallback = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const candidate of group.candidates) {
|
|
138
|
+
selected.add(candidate.id);
|
|
139
|
+
selectedByKind[candidate.kind] = (selectedByKind[candidate.kind] ?? 0) + candidate.tokens;
|
|
140
|
+
}
|
|
141
|
+
selectedTokens += group.tokens;
|
|
142
|
+
}
|
|
143
|
+
if (fallback) {
|
|
144
|
+
for (const candidate of fixture.candidates)
|
|
145
|
+
selected.add(candidate.id);
|
|
146
|
+
selectedTokens = fixture.candidates.reduce((total, candidate) => total + candidate.tokens, 0);
|
|
147
|
+
}
|
|
148
|
+
const selectionReasons = {};
|
|
149
|
+
const dropReasons = {};
|
|
150
|
+
for (const candidate of fixture.candidates) {
|
|
151
|
+
if (selected.has(candidate.id)) {
|
|
152
|
+
selectionReasons[candidate.id] = fallback
|
|
153
|
+
? "fail-open-retained"
|
|
154
|
+
: candidate.currentRequest
|
|
155
|
+
? "mandatory-current-request"
|
|
156
|
+
: `${strategyId}-selected`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
dropReasons[candidate.id] = "strategy-or-budget-drop";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
selectedCandidateIds: [...selected].sort((left, right) => left.localeCompare(right)),
|
|
164
|
+
selectionReasons,
|
|
165
|
+
dropReasons,
|
|
166
|
+
fallback,
|
|
167
|
+
overflow: selectedTokens > fixture.totalBudgetTokens,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function staticRankingStrategy() {
|
|
171
|
+
return {
|
|
172
|
+
id: STATIC_RANKING_STRATEGY_ID,
|
|
173
|
+
plan: (fixture) => planWithScore(fixture, STATIC_RANKING_STRATEGY_ID, (candidate) => candidate.staticScore),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function taskWeightedCandidateStrategy() {
|
|
177
|
+
return {
|
|
178
|
+
id: CANDIDATE_RANKING_STRATEGY_ID,
|
|
179
|
+
plan: (fixture) => planWithScore(fixture, CANDIDATE_RANKING_STRATEGY_ID, (candidate) => candidate.candidateScore ?? candidate.staticScore),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function evaluateQualityPlan(fixture, strategyId, plan, planningDurationMs) {
|
|
183
|
+
validateQualityFixture(fixture);
|
|
184
|
+
validateIdentifier(strategyId, "strategyId");
|
|
185
|
+
const byId = new Map(fixture.candidates.map((candidate) => [candidate.id, candidate]));
|
|
186
|
+
const selectedIds = new Set(plan.selectedCandidateIds);
|
|
187
|
+
for (const id of selectedIds) {
|
|
188
|
+
if (!byId.has(id))
|
|
189
|
+
throw new Error(`Quality plan selected unknown candidate: ${id}`);
|
|
190
|
+
}
|
|
191
|
+
const selected = fixture.candidates.filter((candidate) => selectedIds.has(candidate.id));
|
|
192
|
+
const dropped = fixture.candidates.filter((candidate) => !selectedIds.has(candidate.id));
|
|
193
|
+
const expected = new Set(fixture.expectedEvidenceSourceIds);
|
|
194
|
+
const currentSources = new Set(fixture.candidates.flatMap((candidate) => candidate.currentRequest ? candidate.sourceIds : []));
|
|
195
|
+
const relevant = new Set([...expected, ...currentSources]);
|
|
196
|
+
const selectedSources = new Set(selected.flatMap((candidate) => candidate.sourceIds));
|
|
197
|
+
const evidenceSelected = [...expected].filter((sourceId) => selectedSources.has(sourceId)).length;
|
|
198
|
+
const selectedTokens = selected.reduce((total, candidate) => total + candidate.tokens, 0);
|
|
199
|
+
const droppedTokens = dropped.reduce((total, candidate) => total + candidate.tokens, 0);
|
|
200
|
+
const irrelevantTokens = expected.size === 0
|
|
201
|
+
? 0
|
|
202
|
+
: selected.reduce((total, candidate) => candidate.currentRequest || candidate.sourceIds.some((sourceId) => relevant.has(sourceId))
|
|
203
|
+
? total
|
|
204
|
+
: total + candidate.tokens, 0);
|
|
205
|
+
const sourceReferences = selected.flatMap((candidate) => candidate.sourceIds);
|
|
206
|
+
const duplicateReferences = sourceReferences.length - new Set(sourceReferences).size;
|
|
207
|
+
const provenanceEligible = selected.filter((candidate) => candidate.provenanceRequired !== false);
|
|
208
|
+
const provenanceCovered = provenanceEligible.filter((candidate) => candidate.sourceIds.length > 0).length;
|
|
209
|
+
const currentCandidates = fixture.candidates.filter((candidate) => candidate.currentRequest === true);
|
|
210
|
+
const retainedCurrent = currentCandidates.filter((candidate) => selectedIds.has(candidate.id)).length;
|
|
211
|
+
const atomicGroups = new Map();
|
|
212
|
+
for (const candidate of fixture.candidates) {
|
|
213
|
+
const group = atomicGroups.get(candidate.groupId) ?? [];
|
|
214
|
+
group.push(candidate);
|
|
215
|
+
atomicGroups.set(candidate.groupId, group);
|
|
216
|
+
}
|
|
217
|
+
const multiCandidateGroups = [...atomicGroups.values()].filter((group) => group.length > 1);
|
|
218
|
+
const validAtomicGroups = multiCandidateGroups.filter((group) => {
|
|
219
|
+
const selectedCount = group.filter((candidate) => selectedIds.has(candidate.id)).length;
|
|
220
|
+
return selectedCount === 0 || selectedCount === group.length;
|
|
221
|
+
}).length;
|
|
222
|
+
const sourceKindCounts = {};
|
|
223
|
+
for (const candidate of fixture.candidates) {
|
|
224
|
+
const counts = sourceKindCounts[candidate.kind] ?? { available: 0, selected: 0, dropped: 0 };
|
|
225
|
+
counts.available++;
|
|
226
|
+
if (selectedIds.has(candidate.id))
|
|
227
|
+
counts.selected++;
|
|
228
|
+
else
|
|
229
|
+
counts.dropped++;
|
|
230
|
+
sourceKindCounts[candidate.kind] = counts;
|
|
231
|
+
}
|
|
232
|
+
const budgetUtilization = {};
|
|
233
|
+
for (const [kindValue, limitTokens] of Object.entries(fixture.categoryBudgets)) {
|
|
234
|
+
const kind = kindValue;
|
|
235
|
+
const categorySelected = selected
|
|
236
|
+
.filter((candidate) => candidate.kind === kind)
|
|
237
|
+
.reduce((total, candidate) => total + candidate.tokens, 0);
|
|
238
|
+
const categoryDropped = dropped
|
|
239
|
+
.filter((candidate) => candidate.kind === kind)
|
|
240
|
+
.reduce((total, candidate) => total + candidate.tokens, 0);
|
|
241
|
+
budgetUtilization[kind] = {
|
|
242
|
+
limitTokens,
|
|
243
|
+
selectedTokens: categorySelected,
|
|
244
|
+
droppedTokens: categoryDropped,
|
|
245
|
+
utilization: limitTokens > 0 ? rounded(categorySelected / limitTokens) : categorySelected > 0 ? 1 : 0,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const selectionReasonCounts = {};
|
|
249
|
+
const dropReasonCounts = {};
|
|
250
|
+
for (const candidate of selected) {
|
|
251
|
+
const reason = plan.selectionReasons[candidate.id] ?? "selected-unspecified";
|
|
252
|
+
increment(selectionReasonCounts, REASON_PATTERN.test(reason) ? reason : "selected-unspecified");
|
|
253
|
+
}
|
|
254
|
+
for (const candidate of dropped) {
|
|
255
|
+
const reason = plan.dropReasons[candidate.id] ?? "dropped-unspecified";
|
|
256
|
+
increment(dropReasonCounts, REASON_PATTERN.test(reason) ? reason : "dropped-unspecified");
|
|
257
|
+
}
|
|
258
|
+
const evidenceRecall = countRate(evidenceSelected, expected.size);
|
|
259
|
+
const irrelevantTokenRatio = countRate(irrelevantTokens, expected.size > 0 ? selectedTokens : 0);
|
|
260
|
+
const provenanceCoverage = countRate(provenanceCovered, provenanceEligible.length);
|
|
261
|
+
const currentRequestRetention = countRate(retainedCurrent, currentCandidates.length);
|
|
262
|
+
const atomicGroupValidity = countRate(validAtomicGroups, multiCandidateGroups.length);
|
|
263
|
+
const overflow = plan.overflow || selectedTokens > fixture.totalBudgetTokens;
|
|
264
|
+
const fallback = plan.fallback;
|
|
265
|
+
const metrics = {
|
|
266
|
+
evidenceRecall,
|
|
267
|
+
irrelevantTokenRatio,
|
|
268
|
+
duplicateEvidence: { duplicateReferences, selectedReferences: sourceReferences.length },
|
|
269
|
+
provenanceCoverage,
|
|
270
|
+
currentRequestRetention,
|
|
271
|
+
atomicGroupValidity,
|
|
272
|
+
overflow,
|
|
273
|
+
fallback,
|
|
274
|
+
qualityScore: qualityScore({
|
|
275
|
+
evidenceRecall,
|
|
276
|
+
irrelevantTokenRatio,
|
|
277
|
+
provenanceCoverage,
|
|
278
|
+
currentRequestRetention,
|
|
279
|
+
atomicGroupValidity,
|
|
280
|
+
overflowRate: overflow ? 1 : 0,
|
|
281
|
+
fallbackRate: fallback ? 1 : 0,
|
|
282
|
+
}),
|
|
283
|
+
selectedTokens,
|
|
284
|
+
droppedTokens,
|
|
285
|
+
sourceKindCounts: sortedKinds(sourceKindCounts),
|
|
286
|
+
budgetUtilization: sortedKinds(budgetUtilization),
|
|
287
|
+
selectionReasons: sortedCounts(selectionReasonCounts),
|
|
288
|
+
dropReasons: sortedCounts(dropReasonCounts),
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
schemaVersion: CONTEXT_QUALITY_SCHEMA_VERSION,
|
|
292
|
+
metricsVersion: CONTEXT_QUALITY_METRICS_VERSION,
|
|
293
|
+
sampleId: fixture.sampleId,
|
|
294
|
+
corpusVersion: fixture.corpusVersion,
|
|
295
|
+
strategyId,
|
|
296
|
+
plannerVersion: fixture.plannerVersion,
|
|
297
|
+
profileKey: fixture.profileKey,
|
|
298
|
+
outcomeLabels: sortedUnique(fixture.outcomeLabels ?? []),
|
|
299
|
+
metrics,
|
|
300
|
+
...(planningDurationMs !== undefined && Number.isFinite(planningDurationMs) && planningDurationMs >= 0
|
|
301
|
+
? { timing: { planningDurationMs: rounded(planningDurationMs) } }
|
|
302
|
+
: {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
export function aggregateContextQuality(samples) {
|
|
306
|
+
const valid = samples.filter(isContextQualitySample);
|
|
307
|
+
const evidenceNumerator = valid.reduce((total, sample) => total + sample.metrics.evidenceRecall.numerator, 0);
|
|
308
|
+
const evidenceDenominator = valid.reduce((total, sample) => total + sample.metrics.evidenceRecall.denominator, 0);
|
|
309
|
+
const irrelevantNumerator = valid.reduce((total, sample) => total + sample.metrics.irrelevantTokenRatio.numerator, 0);
|
|
310
|
+
const irrelevantDenominator = valid.reduce((total, sample) => total + sample.metrics.irrelevantTokenRatio.denominator, 0);
|
|
311
|
+
const provenanceNumerator = valid.reduce((total, sample) => total + sample.metrics.provenanceCoverage.numerator, 0);
|
|
312
|
+
const provenanceDenominator = valid.reduce((total, sample) => total + sample.metrics.provenanceCoverage.denominator, 0);
|
|
313
|
+
const currentNumerator = valid.reduce((total, sample) => total + sample.metrics.currentRequestRetention.numerator, 0);
|
|
314
|
+
const currentDenominator = valid.reduce((total, sample) => total + sample.metrics.currentRequestRetention.denominator, 0);
|
|
315
|
+
const atomicNumerator = valid.reduce((total, sample) => total + sample.metrics.atomicGroupValidity.numerator, 0);
|
|
316
|
+
const atomicDenominator = valid.reduce((total, sample) => total + sample.metrics.atomicGroupValidity.denominator, 0);
|
|
317
|
+
const evidenceRecall = countRate(evidenceNumerator, evidenceDenominator);
|
|
318
|
+
const irrelevantTokenRatio = countRate(irrelevantNumerator, irrelevantDenominator);
|
|
319
|
+
const provenanceCoverage = countRate(provenanceNumerator, provenanceDenominator);
|
|
320
|
+
const currentRequestRetention = countRate(currentNumerator, currentDenominator);
|
|
321
|
+
const atomicGroupValidity = countRate(atomicNumerator, atomicDenominator);
|
|
322
|
+
const overflowRate = countRate(valid.filter((sample) => sample.metrics.overflow).length, valid.length);
|
|
323
|
+
const fallbackRate = countRate(valid.filter((sample) => sample.metrics.fallback).length, valid.length);
|
|
324
|
+
const sourceKindCounts = {};
|
|
325
|
+
const budgetTotals = {};
|
|
326
|
+
const selectionReasons = {};
|
|
327
|
+
const dropReasons = {};
|
|
328
|
+
const outcomeLabels = {};
|
|
329
|
+
for (const sample of valid) {
|
|
330
|
+
for (const [kindValue, counts] of Object.entries(sample.metrics.sourceKindCounts)) {
|
|
331
|
+
const kind = kindValue;
|
|
332
|
+
const aggregate = sourceKindCounts[kind] ?? { available: 0, selected: 0, dropped: 0 };
|
|
333
|
+
aggregate.available += counts.available;
|
|
334
|
+
aggregate.selected += counts.selected;
|
|
335
|
+
aggregate.dropped += counts.dropped;
|
|
336
|
+
sourceKindCounts[kind] = aggregate;
|
|
337
|
+
}
|
|
338
|
+
for (const [kindValue, utilization] of Object.entries(sample.metrics.budgetUtilization)) {
|
|
339
|
+
const kind = kindValue;
|
|
340
|
+
const aggregate = budgetTotals[kind] ?? { limitTokens: 0, selectedTokens: 0, droppedTokens: 0 };
|
|
341
|
+
aggregate.limitTokens += utilization.limitTokens;
|
|
342
|
+
aggregate.selectedTokens += utilization.selectedTokens;
|
|
343
|
+
aggregate.droppedTokens += utilization.droppedTokens;
|
|
344
|
+
budgetTotals[kind] = aggregate;
|
|
345
|
+
}
|
|
346
|
+
for (const [reason, count] of Object.entries(sample.metrics.selectionReasons))
|
|
347
|
+
increment(selectionReasons, reason, count);
|
|
348
|
+
for (const [reason, count] of Object.entries(sample.metrics.dropReasons))
|
|
349
|
+
increment(dropReasons, reason, count);
|
|
350
|
+
for (const label of sample.outcomeLabels)
|
|
351
|
+
increment(outcomeLabels, label);
|
|
352
|
+
}
|
|
353
|
+
const budgetUtilization = {};
|
|
354
|
+
for (const [kindValue, totals] of Object.entries(budgetTotals)) {
|
|
355
|
+
const kind = kindValue;
|
|
356
|
+
budgetUtilization[kind] = {
|
|
357
|
+
...totals,
|
|
358
|
+
utilization: totals.limitTokens > 0
|
|
359
|
+
? rounded(totals.selectedTokens / totals.limitTokens)
|
|
360
|
+
: totals.selectedTokens > 0 ? 1 : 0,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const duplicateReferences = valid.reduce((total, sample) => total + sample.metrics.duplicateEvidence.duplicateReferences, 0);
|
|
364
|
+
const selectedReferences = valid.reduce((total, sample) => total + sample.metrics.duplicateEvidence.selectedReferences, 0);
|
|
365
|
+
return {
|
|
366
|
+
schemaVersion: CONTEXT_QUALITY_SCHEMA_VERSION,
|
|
367
|
+
metricsVersion: CONTEXT_QUALITY_METRICS_VERSION,
|
|
368
|
+
sampleCount: valid.length,
|
|
369
|
+
labeledSampleCount: valid.filter((sample) => sample.metrics.evidenceRecall.denominator > 0).length,
|
|
370
|
+
strategyIds: sortedUnique(valid.map((sample) => sample.strategyId)),
|
|
371
|
+
plannerVersions: sortedUnique(valid.map((sample) => sample.plannerVersion)),
|
|
372
|
+
profileKeys: sortedUnique(valid.map((sample) => sample.profileKey)),
|
|
373
|
+
outcomeLabels: sortedCounts(outcomeLabels),
|
|
374
|
+
evidenceRecall,
|
|
375
|
+
irrelevantTokenRatio,
|
|
376
|
+
duplicateEvidence: { duplicateReferences, selectedReferences },
|
|
377
|
+
provenanceCoverage,
|
|
378
|
+
currentRequestRetention,
|
|
379
|
+
atomicGroupValidity,
|
|
380
|
+
overflowRate,
|
|
381
|
+
fallbackRate,
|
|
382
|
+
qualityScore: valid.length > 0 && evidenceDenominator > 0
|
|
383
|
+
? qualityScore({
|
|
384
|
+
evidenceRecall,
|
|
385
|
+
irrelevantTokenRatio,
|
|
386
|
+
provenanceCoverage,
|
|
387
|
+
currentRequestRetention,
|
|
388
|
+
atomicGroupValidity,
|
|
389
|
+
overflowRate: overflowRate.rate ?? 0,
|
|
390
|
+
fallbackRate: fallbackRate.rate ?? 0,
|
|
391
|
+
})
|
|
392
|
+
: 0,
|
|
393
|
+
selectedTokens: valid.reduce((total, sample) => total + sample.metrics.selectedTokens, 0),
|
|
394
|
+
droppedTokens: valid.reduce((total, sample) => total + sample.metrics.droppedTokens, 0),
|
|
395
|
+
sourceKindCounts: sortedKinds(sourceKindCounts),
|
|
396
|
+
budgetUtilization: sortedKinds(budgetUtilization),
|
|
397
|
+
selectionReasons: sortedCounts(selectionReasons),
|
|
398
|
+
dropReasons: sortedCounts(dropReasons),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
export function compareQualityStrategies(corpus, strategies) {
|
|
402
|
+
if (corpus.schemaVersion !== CONTEXT_QUALITY_SCHEMA_VERSION)
|
|
403
|
+
throw new Error("Unsupported quality corpus schema");
|
|
404
|
+
validateIdentifier(corpus.corpusVersion, "corpusVersion");
|
|
405
|
+
if (strategies.length === 0)
|
|
406
|
+
throw new Error("At least one quality strategy is required");
|
|
407
|
+
const strategyReports = strategies.map((strategy) => {
|
|
408
|
+
validateIdentifier(strategy.id, "strategy.id");
|
|
409
|
+
const samples = corpus.fixtures.map((fixture) => {
|
|
410
|
+
if (fixture.corpusVersion !== corpus.corpusVersion) {
|
|
411
|
+
throw new Error(`Fixture ${fixture.sampleId} has a mismatched corpus version`);
|
|
412
|
+
}
|
|
413
|
+
return evaluateQualityPlan(fixture, strategy.id, strategy.plan(fixture));
|
|
414
|
+
});
|
|
415
|
+
return { strategyId: strategy.id, aggregate: aggregateContextQuality(samples) };
|
|
416
|
+
});
|
|
417
|
+
const baseline = strategyReports[0];
|
|
418
|
+
if (!baseline)
|
|
419
|
+
throw new Error("Quality baseline is unavailable");
|
|
420
|
+
const rate = (value) => value ?? 0;
|
|
421
|
+
return {
|
|
422
|
+
schemaVersion: CONTEXT_QUALITY_SCHEMA_VERSION,
|
|
423
|
+
metricsVersion: CONTEXT_QUALITY_METRICS_VERSION,
|
|
424
|
+
corpusVersion: corpus.corpusVersion,
|
|
425
|
+
fixtureCount: corpus.fixtures.length,
|
|
426
|
+
strategies: strategyReports,
|
|
427
|
+
deltasFromBaseline: strategyReports.slice(1).map((report) => ({
|
|
428
|
+
strategyId: report.strategyId,
|
|
429
|
+
qualityScore: rounded(report.aggregate.qualityScore - baseline.aggregate.qualityScore),
|
|
430
|
+
evidenceRecall: rounded(rate(report.aggregate.evidenceRecall.rate) - rate(baseline.aggregate.evidenceRecall.rate)),
|
|
431
|
+
irrelevantTokenRatio: rounded(rate(report.aggregate.irrelevantTokenRatio.rate) - rate(baseline.aggregate.irrelevantTokenRatio.rate)),
|
|
432
|
+
fallbackRate: rounded(rate(report.aggregate.fallbackRate.rate) - rate(baseline.aggregate.fallbackRate.rate)),
|
|
433
|
+
})),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function safeLiveIdentifier(value, prefix) {
|
|
437
|
+
return IDENTIFIER_PATTERN.test(value) ? value : `${prefix}:${sha256(value).slice(0, 24)}`;
|
|
438
|
+
}
|
|
439
|
+
function liveReason(item, selected) {
|
|
440
|
+
if (selected) {
|
|
441
|
+
if (item.kind === "current")
|
|
442
|
+
return "mandatory-current-request";
|
|
443
|
+
if (item.kind === "system")
|
|
444
|
+
return "effective-system-prompt";
|
|
445
|
+
if (item.kind === "tool")
|
|
446
|
+
return "active-tool-definition";
|
|
447
|
+
return `selected-${item.kind}`;
|
|
448
|
+
}
|
|
449
|
+
if (item.reason.toLowerCase().includes("privacy"))
|
|
450
|
+
return "privacy-policy-drop";
|
|
451
|
+
return `budget-or-policy-drop-${item.kind}`;
|
|
452
|
+
}
|
|
453
|
+
export function evaluateManifestQuality(manifest, categoryBudgets) {
|
|
454
|
+
const included = manifest.included.map((item, index) => ({
|
|
455
|
+
id: `included:${index}`,
|
|
456
|
+
kind: item.kind,
|
|
457
|
+
tokens: Math.max(0, Math.floor(item.tokens)),
|
|
458
|
+
groupId: item.groupId ? `group:${sha256(item.groupId).slice(0, 24)}` : `included:${index}`,
|
|
459
|
+
sourceIds: item.sourceId ? [`source:${sha256(item.sourceId).slice(0, 24)}`] : [],
|
|
460
|
+
staticScore: item.score ?? 0,
|
|
461
|
+
currentRequest: item.kind === "current",
|
|
462
|
+
provenanceRequired: item.kind !== "system" && item.kind !== "tool",
|
|
463
|
+
}));
|
|
464
|
+
const excluded = manifest.excluded.map((item, index) => ({
|
|
465
|
+
id: `excluded:${index}`,
|
|
466
|
+
kind: item.kind,
|
|
467
|
+
tokens: Math.max(0, Math.floor(item.tokens)),
|
|
468
|
+
groupId: item.groupId ? `group:${sha256(item.groupId).slice(0, 24)}` : `excluded:${index}`,
|
|
469
|
+
sourceIds: item.sourceId ? [`source:${sha256(item.sourceId).slice(0, 24)}`] : [],
|
|
470
|
+
staticScore: item.score ?? 0,
|
|
471
|
+
currentRequest: item.kind === "current",
|
|
472
|
+
provenanceRequired: item.kind !== "system" && item.kind !== "tool",
|
|
473
|
+
}));
|
|
474
|
+
const fixture = {
|
|
475
|
+
schemaVersion: CONTEXT_QUALITY_SCHEMA_VERSION,
|
|
476
|
+
corpusVersion: LIVE_QUALITY_CORPUS_VERSION,
|
|
477
|
+
sampleId: `manifest:${sha256(manifest.id).slice(0, 24)}`,
|
|
478
|
+
plannerVersion: safeLiveIdentifier(manifest.plannerVersion, "planner"),
|
|
479
|
+
profileKey: safeLiveIdentifier(manifest.modelAwareness?.profileKey ?? `model:${manifest.provider}/${manifest.model}`, "profile"),
|
|
480
|
+
task: { taskId: "live:unlabeled", taskType: "live-observation", queryTags: [] },
|
|
481
|
+
expectedEvidenceSourceIds: [],
|
|
482
|
+
candidates: [...included, ...excluded],
|
|
483
|
+
totalBudgetTokens: manifest.hardInputLimit,
|
|
484
|
+
categoryBudgets,
|
|
485
|
+
outcomeLabels: [
|
|
486
|
+
`mode:${manifest.planning?.mode ?? "observer"}`,
|
|
487
|
+
...(manifest.privacy?.enabled ? ["privacy:enabled"] : []),
|
|
488
|
+
],
|
|
489
|
+
};
|
|
490
|
+
const selectionReasons = Object.fromEntries(manifest.included.map((item, index) => [`included:${index}`, liveReason(item, true)]));
|
|
491
|
+
const dropReasons = Object.fromEntries(manifest.excluded.map((item, index) => [`excluded:${index}`, liveReason(item, false)]));
|
|
492
|
+
return evaluateQualityPlan(fixture, safeLiveIdentifier(`planner:${manifest.plannerVersion}`, "strategy"), {
|
|
493
|
+
selectedCandidateIds: included.map((candidate) => candidate.id),
|
|
494
|
+
selectionReasons,
|
|
495
|
+
dropReasons,
|
|
496
|
+
fallback: manifest.planning?.mode === "fallback",
|
|
497
|
+
overflow: manifest.estimatedInputTokens > manifest.hardInputLimit,
|
|
498
|
+
}, manifest.planning?.durationMs);
|
|
499
|
+
}
|
|
500
|
+
function validCount(value) {
|
|
501
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
502
|
+
}
|
|
503
|
+
function validCountRate(value) {
|
|
504
|
+
if (!value || typeof value !== "object")
|
|
505
|
+
return false;
|
|
506
|
+
const metric = value;
|
|
507
|
+
return validCount(metric.numerator)
|
|
508
|
+
&& validCount(metric.denominator)
|
|
509
|
+
&& metric.numerator <= metric.denominator
|
|
510
|
+
&& (metric.rate === null || (typeof metric.rate === "number" && metric.rate >= 0 && metric.rate <= 1));
|
|
511
|
+
}
|
|
512
|
+
function validReasonCounts(value) {
|
|
513
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
514
|
+
return false;
|
|
515
|
+
return Object.entries(value).every(([reason, count]) => REASON_PATTERN.test(reason) && validCount(count));
|
|
516
|
+
}
|
|
517
|
+
function validSourceKindCounts(value) {
|
|
518
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
519
|
+
return false;
|
|
520
|
+
return Object.values(value).every((counts) => {
|
|
521
|
+
if (!counts || typeof counts !== "object")
|
|
522
|
+
return false;
|
|
523
|
+
const item = counts;
|
|
524
|
+
return validCount(item.available)
|
|
525
|
+
&& validCount(item.selected)
|
|
526
|
+
&& validCount(item.dropped)
|
|
527
|
+
&& item.selected + item.dropped === item.available;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function validBudgetUtilization(value) {
|
|
531
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
532
|
+
return false;
|
|
533
|
+
return Object.values(value).every((budget) => {
|
|
534
|
+
if (!budget || typeof budget !== "object")
|
|
535
|
+
return false;
|
|
536
|
+
const item = budget;
|
|
537
|
+
return validCount(item.limitTokens)
|
|
538
|
+
&& validCount(item.selectedTokens)
|
|
539
|
+
&& validCount(item.droppedTokens)
|
|
540
|
+
&& typeof item.utilization === "number"
|
|
541
|
+
&& Number.isFinite(item.utilization)
|
|
542
|
+
&& item.utilization >= 0;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
export function isContextQualitySample(value) {
|
|
546
|
+
if (!value || typeof value !== "object")
|
|
547
|
+
return false;
|
|
548
|
+
const sample = value;
|
|
549
|
+
if (sample.schemaVersion !== CONTEXT_QUALITY_SCHEMA_VERSION
|
|
550
|
+
|| sample.metricsVersion !== CONTEXT_QUALITY_METRICS_VERSION
|
|
551
|
+
|| typeof sample.sampleId !== "string"
|
|
552
|
+
|| typeof sample.corpusVersion !== "string"
|
|
553
|
+
|| typeof sample.strategyId !== "string"
|
|
554
|
+
|| typeof sample.plannerVersion !== "string"
|
|
555
|
+
|| typeof sample.profileKey !== "string"
|
|
556
|
+
|| !Array.isArray(sample.outcomeLabels)
|
|
557
|
+
|| !sample.outcomeLabels.every((label) => typeof label === "string" && IDENTIFIER_PATTERN.test(label)))
|
|
558
|
+
return false;
|
|
559
|
+
try {
|
|
560
|
+
validateIdentifier(sample.sampleId, "sampleId");
|
|
561
|
+
validateIdentifier(sample.corpusVersion, "corpusVersion");
|
|
562
|
+
validateIdentifier(sample.strategyId, "strategyId");
|
|
563
|
+
validateIdentifier(sample.plannerVersion, "plannerVersion");
|
|
564
|
+
validateIdentifier(sample.profileKey, "profileKey");
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
const metrics = sample.metrics;
|
|
570
|
+
if (!metrics || typeof metrics !== "object")
|
|
571
|
+
return false;
|
|
572
|
+
if (!validCountRate(metrics.evidenceRecall)
|
|
573
|
+
|| !validCountRate(metrics.irrelevantTokenRatio)
|
|
574
|
+
|| !validCountRate(metrics.provenanceCoverage)
|
|
575
|
+
|| !validCountRate(metrics.currentRequestRetention)
|
|
576
|
+
|| !validCountRate(metrics.atomicGroupValidity)
|
|
577
|
+
|| typeof metrics.overflow !== "boolean"
|
|
578
|
+
|| typeof metrics.fallback !== "boolean"
|
|
579
|
+
|| typeof metrics.qualityScore !== "number"
|
|
580
|
+
|| metrics.qualityScore < 0
|
|
581
|
+
|| metrics.qualityScore > 1
|
|
582
|
+
|| !validCount(metrics.selectedTokens)
|
|
583
|
+
|| !validCount(metrics.droppedTokens)
|
|
584
|
+
|| !metrics.duplicateEvidence
|
|
585
|
+
|| !validCount(metrics.duplicateEvidence.duplicateReferences)
|
|
586
|
+
|| !validCount(metrics.duplicateEvidence.selectedReferences)
|
|
587
|
+
|| metrics.duplicateEvidence.duplicateReferences > metrics.duplicateEvidence.selectedReferences
|
|
588
|
+
|| !validSourceKindCounts(metrics.sourceKindCounts)
|
|
589
|
+
|| !validBudgetUtilization(metrics.budgetUtilization)
|
|
590
|
+
|| !validReasonCounts(metrics.selectionReasons)
|
|
591
|
+
|| !validReasonCounts(metrics.dropReasons))
|
|
592
|
+
return false;
|
|
593
|
+
return sample.timing === undefined
|
|
594
|
+
|| (typeof sample.timing.planningDurationMs === "number"
|
|
595
|
+
&& Number.isFinite(sample.timing.planningDurationMs)
|
|
596
|
+
&& sample.timing.planningDurationMs >= 0);
|
|
597
|
+
}
|
|
598
|
+
export function qualityTiming(samples) {
|
|
599
|
+
const durations = samples
|
|
600
|
+
.flatMap((sample) => sample.timing ? [sample.timing.planningDurationMs] : [])
|
|
601
|
+
.filter((duration) => Number.isFinite(duration) && duration >= 0)
|
|
602
|
+
.sort((left, right) => left - right);
|
|
603
|
+
if (durations.length === 0)
|
|
604
|
+
return { timedSamples: 0 };
|
|
605
|
+
const total = durations.reduce((sum, duration) => sum + duration, 0);
|
|
606
|
+
const p95Index = Math.max(0, Math.ceil(durations.length * 0.95) - 1);
|
|
607
|
+
return {
|
|
608
|
+
timedSamples: durations.length,
|
|
609
|
+
meanPlanningDurationMs: rounded(total / durations.length),
|
|
610
|
+
p95PlanningDurationMs: rounded(durations[p95Index] ?? 0),
|
|
611
|
+
latestPlanningDurationMs: rounded(samples.findLast((sample) => sample.timing)?.timing?.planningDurationMs ?? 0),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
export function disabledContextQualityDiagnostics() {
|
|
615
|
+
return {
|
|
616
|
+
enabled: false,
|
|
617
|
+
metricsVersion: CONTEXT_QUALITY_METRICS_VERSION,
|
|
618
|
+
storedSamples: 0,
|
|
619
|
+
ignoredSamples: 0,
|
|
620
|
+
aggregate: aggregateContextQuality([]),
|
|
621
|
+
timing: { timedSamples: 0 },
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
//# sourceMappingURL=context-quality.js.map
|