ds4-context-core 0.1.2 → 0.2.0-beta.2

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.
Files changed (125) hide show
  1. package/README.md +20 -4
  2. package/dist/adapter/conformance.d.ts +35 -0
  3. package/dist/adapter/conformance.d.ts.map +1 -0
  4. package/dist/adapter/conformance.js +260 -0
  5. package/dist/adapter/conformance.js.map +1 -0
  6. package/dist/adapter/local-kv.d.ts +125 -0
  7. package/dist/adapter/local-kv.d.ts.map +1 -0
  8. package/dist/adapter/local-kv.js +276 -0
  9. package/dist/adapter/local-kv.js.map +1 -0
  10. package/dist/adapter/runtime-adapter.d.ts +124 -0
  11. package/dist/adapter/runtime-adapter.d.ts.map +1 -0
  12. package/dist/adapter/runtime-adapter.js +289 -0
  13. package/dist/adapter/runtime-adapter.js.map +1 -0
  14. package/dist/config/config-loader.d.ts +1 -0
  15. package/dist/config/config-loader.d.ts.map +1 -1
  16. package/dist/config/config-loader.js +110 -1
  17. package/dist/config/config-loader.js.map +1 -1
  18. package/dist/config/config.d.ts +50 -0
  19. package/dist/config/config.d.ts.map +1 -1
  20. package/dist/config/config.js +31 -0
  21. package/dist/config/config.js.map +1 -1
  22. package/dist/core/canonical-message.d.ts +2 -1
  23. package/dist/core/canonical-message.d.ts.map +1 -1
  24. package/dist/index.d.ts +13 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +13 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/manifest/context-manifest.d.ts +12 -0
  29. package/dist/manifest/context-manifest.d.ts.map +1 -1
  30. package/dist/memory/memory-manager.d.ts +7 -1
  31. package/dist/memory/memory-manager.d.ts.map +1 -1
  32. package/dist/memory/memory-manager.js +100 -3
  33. package/dist/memory/memory-manager.js.map +1 -1
  34. package/dist/memory/memory-types.d.ts +43 -0
  35. package/dist/memory/memory-types.d.ts.map +1 -1
  36. package/dist/memory/memory-types.js.map +1 -1
  37. package/dist/persistence/migrations.d.ts +2 -1
  38. package/dist/persistence/migrations.d.ts.map +1 -1
  39. package/dist/persistence/migrations.js +155 -25
  40. package/dist/persistence/migrations.js.map +1 -1
  41. package/dist/persistence/repositories/artifact-repository.d.ts +3 -1
  42. package/dist/persistence/repositories/artifact-repository.d.ts.map +1 -1
  43. package/dist/persistence/repositories/artifact-repository.js +16 -27
  44. package/dist/persistence/repositories/artifact-repository.js.map +1 -1
  45. package/dist/persistence/repositories/context-manifest-repository.d.ts +3 -1
  46. package/dist/persistence/repositories/context-manifest-repository.d.ts.map +1 -1
  47. package/dist/persistence/repositories/context-manifest-repository.js +10 -18
  48. package/dist/persistence/repositories/context-manifest-repository.js.map +1 -1
  49. package/dist/persistence/repositories/context-quality-repository.d.ts +21 -0
  50. package/dist/persistence/repositories/context-quality-repository.d.ts.map +1 -0
  51. package/dist/persistence/repositories/context-quality-repository.js +83 -0
  52. package/dist/persistence/repositories/context-quality-repository.js.map +1 -0
  53. package/dist/persistence/repositories/embedding-repository.d.ts +37 -0
  54. package/dist/persistence/repositories/embedding-repository.d.ts.map +1 -0
  55. package/dist/persistence/repositories/embedding-repository.js +142 -0
  56. package/dist/persistence/repositories/embedding-repository.js.map +1 -0
  57. package/dist/persistence/repositories/lease-repository.d.ts +22 -0
  58. package/dist/persistence/repositories/lease-repository.d.ts.map +1 -0
  59. package/dist/persistence/repositories/lease-repository.js +113 -0
  60. package/dist/persistence/repositories/lease-repository.js.map +1 -0
  61. package/dist/persistence/repositories/memory-repository.d.ts +34 -3
  62. package/dist/persistence/repositories/memory-repository.d.ts.map +1 -1
  63. package/dist/persistence/repositories/memory-repository.js +426 -64
  64. package/dist/persistence/repositories/memory-repository.js.map +1 -1
  65. package/dist/persistence/repositories/project-knowledge-repository.d.ts +28 -5
  66. package/dist/persistence/repositories/project-knowledge-repository.d.ts.map +1 -1
  67. package/dist/persistence/repositories/project-knowledge-repository.js +123 -53
  68. package/dist/persistence/repositories/project-knowledge-repository.js.map +1 -1
  69. package/dist/persistence/repositories/session-index-repository.d.ts +9 -1
  70. package/dist/persistence/repositories/session-index-repository.d.ts.map +1 -1
  71. package/dist/persistence/repositories/session-index-repository.js +38 -17
  72. package/dist/persistence/repositories/session-index-repository.js.map +1 -1
  73. package/dist/persistence/repositories/summary-repository.d.ts +3 -1
  74. package/dist/persistence/repositories/summary-repository.d.ts.map +1 -1
  75. package/dist/persistence/repositories/summary-repository.js +35 -31
  76. package/dist/persistence/repositories/summary-repository.js.map +1 -1
  77. package/dist/persistence/sqlite.d.ts +10 -0
  78. package/dist/persistence/sqlite.d.ts.map +1 -1
  79. package/dist/persistence/sqlite.js +41 -12
  80. package/dist/persistence/sqlite.js.map +1 -1
  81. package/dist/persistence/write-coordinator.d.ts +37 -0
  82. package/dist/persistence/write-coordinator.d.ts.map +1 -0
  83. package/dist/persistence/write-coordinator.js +120 -0
  84. package/dist/persistence/write-coordinator.js.map +1 -0
  85. package/dist/planner/context-planner.d.ts +5 -0
  86. package/dist/planner/context-planner.d.ts.map +1 -1
  87. package/dist/planner/context-planner.js +41 -62
  88. package/dist/planner/context-planner.js.map +1 -1
  89. package/dist/project/file-indexer.d.ts +5 -3
  90. package/dist/project/file-indexer.d.ts.map +1 -1
  91. package/dist/project/file-indexer.js +83 -38
  92. package/dist/project/file-indexer.js.map +1 -1
  93. package/dist/project/project-knowledge.d.ts +15 -3
  94. package/dist/project/project-knowledge.d.ts.map +1 -1
  95. package/dist/project/project-knowledge.js +168 -42
  96. package/dist/project/project-knowledge.js.map +1 -1
  97. package/dist/project/symbol-parser.d.ts +48 -0
  98. package/dist/project/symbol-parser.d.ts.map +1 -0
  99. package/dist/project/symbol-parser.js +427 -0
  100. package/dist/project/symbol-parser.js.map +1 -0
  101. package/dist/quality/context-quality.d.ts +172 -0
  102. package/dist/quality/context-quality.d.ts.map +1 -0
  103. package/dist/quality/context-quality.js +624 -0
  104. package/dist/quality/context-quality.js.map +1 -0
  105. package/dist/ranking/learned-ranker.d.ts +174 -0
  106. package/dist/ranking/learned-ranker.d.ts.map +1 -0
  107. package/dist/ranking/learned-ranker.js +782 -0
  108. package/dist/ranking/learned-ranker.js.map +1 -0
  109. package/dist/retrieval/embedding.d.ts +92 -0
  110. package/dist/retrieval/embedding.d.ts.map +1 -0
  111. package/dist/retrieval/embedding.js +56 -0
  112. package/dist/retrieval/embedding.js.map +1 -0
  113. package/dist/retrieval/retrieval-engine.d.ts +9 -1
  114. package/dist/retrieval/retrieval-engine.d.ts.map +1 -1
  115. package/dist/retrieval/retrieval-engine.js +105 -7
  116. package/dist/retrieval/retrieval-engine.js.map +1 -1
  117. package/dist/retrieval/semantic-index.d.ts +30 -0
  118. package/dist/retrieval/semantic-index.d.ts.map +1 -0
  119. package/dist/retrieval/semantic-index.js +284 -0
  120. package/dist/retrieval/semantic-index.js.map +1 -0
  121. package/dist/retrieval/semantic-quality.d.ts +52 -0
  122. package/dist/retrieval/semantic-quality.d.ts.map +1 -0
  123. package/dist/retrieval/semantic-quality.js +117 -0
  124. package/dist/retrieval/semantic-quality.js.map +1 -0
  125. package/package.json +1 -1
@@ -0,0 +1,782 @@
1
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { isPrivacyClassification } from "../privacy/privacy-policy.js";
4
+ import { sha256 } from "../shared/hash.js";
5
+ import { stableStringify } from "../shared/stable-json.js";
6
+ export const RANKING_FEATURE_VERSION = "ranking-features-v1";
7
+ export const RANKING_MODEL_SCHEMA_VERSION = 1;
8
+ export const RANKING_FEEDBACK_SCHEMA_VERSION = 1;
9
+ export const RANKING_FEEDBACK_CUSTOM_ENTRY_TYPE = "ds4-context-ranking-feedback-v1";
10
+ export const RANKING_ALGORITHM = "bounded-linear-centroid-v1";
11
+ const NUMERIC_FEATURES = [
12
+ "staticScore",
13
+ "exactScore",
14
+ "ftsScore",
15
+ "vectorScore",
16
+ "recency",
17
+ "branchRelation",
18
+ "symbolRelation",
19
+ "classificationEligible",
20
+ "tokenCost",
21
+ "priorSelected",
22
+ ];
23
+ const SOURCE_KINDS = ["memory", "retrieval", "project"];
24
+ const HASH_PATTERN = /^[a-f0-9]{64}$/u;
25
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u;
26
+ const SCORE_SCALE = 1_000_000_000;
27
+ const MAX_MODEL_WEIGHT = 16;
28
+ const MAX_RANKING_MODEL_BYTES = 1_000_000;
29
+ function isRecord(value) {
30
+ return value !== null && typeof value === "object" && !Array.isArray(value);
31
+ }
32
+ function hasOnlyKeys(value, allowed) {
33
+ const keys = new Set(allowed);
34
+ return Object.keys(value).every((key) => keys.has(key));
35
+ }
36
+ function rounded(value) {
37
+ return Math.round(value * SCORE_SCALE) / SCORE_SCALE;
38
+ }
39
+ function clamp01(value) {
40
+ if (value === undefined || !Number.isFinite(value))
41
+ return 0;
42
+ return rounded(Math.max(0, Math.min(1, value)));
43
+ }
44
+ function normalizedSourceKind(value) {
45
+ return typeof value === "string" && SOURCE_KINDS.includes(value)
46
+ ? value
47
+ : undefined;
48
+ }
49
+ export function createRankingFeatures(input) {
50
+ return {
51
+ sourceKind: input.sourceKind,
52
+ staticScore: clamp01(input.staticScore),
53
+ exactScore: clamp01(input.exactScore),
54
+ ftsScore: clamp01(input.ftsScore),
55
+ vectorScore: clamp01(input.vectorScore),
56
+ recency: clamp01(input.recency),
57
+ branchRelation: clamp01(input.branchRelation),
58
+ symbolRelation: clamp01(input.symbolRelation),
59
+ classificationEligible: clamp01(input.classificationEligible),
60
+ tokenCost: clamp01(input.tokenCost),
61
+ priorSelected: clamp01(input.priorSelected),
62
+ };
63
+ }
64
+ export function parseRankingFeatures(value) {
65
+ if (!isRecord(value) || !hasOnlyKeys(value, ["sourceKind", ...NUMERIC_FEATURES]))
66
+ return undefined;
67
+ const sourceKind = normalizedSourceKind(value.sourceKind);
68
+ if (!sourceKind)
69
+ return undefined;
70
+ const result = { sourceKind };
71
+ for (const feature of NUMERIC_FEATURES) {
72
+ const featureValue = value[feature];
73
+ if (typeof featureValue !== "number" || !Number.isFinite(featureValue)
74
+ || featureValue < 0 || featureValue > 1)
75
+ return undefined;
76
+ result[feature] = rounded(featureValue);
77
+ }
78
+ return result;
79
+ }
80
+ export function hashRankingCandidateKey(candidateKey) {
81
+ return sha256(candidateKey);
82
+ }
83
+ export function createRankingFeedback(input) {
84
+ if (!ID_PATTERN.test(input.feedbackId))
85
+ throw new Error("Ranking feedback ID is invalid");
86
+ if (!Number.isSafeInteger(input.createdAt) || input.createdAt < 0) {
87
+ throw new Error("Ranking feedback timestamp is invalid");
88
+ }
89
+ const features = parseRankingFeatures(input.features);
90
+ if (!features)
91
+ throw new Error("Ranking feedback features are invalid");
92
+ if (!input.repositoryIdentity || !input.candidateKey) {
93
+ throw new Error("Ranking feedback repository and candidate identities are required");
94
+ }
95
+ if ((input.labelSource !== undefined && input.labelSource !== "feedback" && input.labelSource !== "replay")
96
+ || (input.label !== "useful" && input.label !== "irrelevant")
97
+ || !isPrivacyClassification(input.classification)) {
98
+ throw new Error("Ranking feedback label metadata is invalid");
99
+ }
100
+ return {
101
+ schemaVersion: RANKING_FEEDBACK_SCHEMA_VERSION,
102
+ feedbackId: input.feedbackId,
103
+ createdAt: input.createdAt,
104
+ repositoryHash: sha256(input.repositoryIdentity),
105
+ candidateKeyHash: hashRankingCandidateKey(input.candidateKey),
106
+ labelSource: input.labelSource ?? "feedback",
107
+ label: input.label,
108
+ classification: input.classification,
109
+ features,
110
+ };
111
+ }
112
+ export function parseRankingFeedback(value) {
113
+ if (!isRecord(value)
114
+ || !hasOnlyKeys(value, [
115
+ "schemaVersion",
116
+ "feedbackId",
117
+ "createdAt",
118
+ "repositoryHash",
119
+ "candidateKeyHash",
120
+ "labelSource",
121
+ "label",
122
+ "classification",
123
+ "features",
124
+ ])
125
+ || value.schemaVersion !== RANKING_FEEDBACK_SCHEMA_VERSION)
126
+ return undefined;
127
+ if (typeof value.feedbackId !== "string" || !ID_PATTERN.test(value.feedbackId))
128
+ return undefined;
129
+ if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt < 0) {
130
+ return undefined;
131
+ }
132
+ if (typeof value.repositoryHash !== "string" || !HASH_PATTERN.test(value.repositoryHash))
133
+ return undefined;
134
+ if (typeof value.candidateKeyHash !== "string" || !HASH_PATTERN.test(value.candidateKeyHash))
135
+ return undefined;
136
+ if (value.labelSource !== "feedback" && value.labelSource !== "replay")
137
+ return undefined;
138
+ if (value.label !== "useful" && value.label !== "irrelevant")
139
+ return undefined;
140
+ if (typeof value.classification !== "string" || !isPrivacyClassification(value.classification))
141
+ return undefined;
142
+ const features = parseRankingFeatures(value.features);
143
+ if (!features)
144
+ return undefined;
145
+ return {
146
+ schemaVersion: RANKING_FEEDBACK_SCHEMA_VERSION,
147
+ feedbackId: value.feedbackId,
148
+ createdAt: value.createdAt,
149
+ repositoryHash: value.repositoryHash,
150
+ candidateKeyHash: value.candidateKeyHash,
151
+ labelSource: value.labelSource,
152
+ label: value.label,
153
+ classification: value.classification,
154
+ features,
155
+ };
156
+ }
157
+ export function rankingTrainingSample(entry) {
158
+ return {
159
+ sampleId: entry.feedbackId,
160
+ repositoryHash: entry.repositoryHash,
161
+ candidateKeyHash: entry.candidateKeyHash,
162
+ labelSource: entry.labelSource,
163
+ label: entry.label,
164
+ classification: entry.classification,
165
+ features: { ...entry.features },
166
+ };
167
+ }
168
+ function emptyFeatureWeights() {
169
+ return Object.fromEntries(NUMERIC_FEATURES.map((feature) => [feature, 0]));
170
+ }
171
+ function emptySourceWeights() {
172
+ return { memory: 0, retrieval: 0, project: 0 };
173
+ }
174
+ function boundedWeight(value) {
175
+ return rounded(Math.max(-MAX_MODEL_WEIGHT, Math.min(MAX_MODEL_WEIGHT, value)));
176
+ }
177
+ function modelPayload(model) {
178
+ return {
179
+ schemaVersion: model.schemaVersion,
180
+ featureVersion: model.featureVersion,
181
+ algorithm: model.algorithm,
182
+ modelId: model.modelId,
183
+ createdAt: model.createdAt,
184
+ training: { ...model.training },
185
+ weights: {
186
+ bias: model.weights.bias,
187
+ features: { ...model.weights.features },
188
+ sourceKinds: { ...model.weights.sourceKinds },
189
+ },
190
+ ...(model.promotion ? { promotion: structuredClone(model.promotion) } : {}),
191
+ };
192
+ }
193
+ function sealModel(input) {
194
+ const identityPayload = { ...input, modelId: "" };
195
+ const modelId = `ranking-${sha256(stableStringify(identityPayload)).slice(0, 16)}`;
196
+ const payload = modelPayload({ ...input, modelId });
197
+ return { ...payload, checksum: sha256(stableStringify(payload)) };
198
+ }
199
+ function assertFiniteWeight(value) {
200
+ return typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= MAX_MODEL_WEIGHT;
201
+ }
202
+ export function parseLearnedRankingModel(value) {
203
+ if (!isRecord(value)
204
+ || !hasOnlyKeys(value, [
205
+ "schemaVersion",
206
+ "featureVersion",
207
+ "algorithm",
208
+ "modelId",
209
+ "createdAt",
210
+ "training",
211
+ "weights",
212
+ "promotion",
213
+ "checksum",
214
+ ])
215
+ || value.schemaVersion !== RANKING_MODEL_SCHEMA_VERSION
216
+ || value.featureVersion !== RANKING_FEATURE_VERSION
217
+ || value.algorithm !== RANKING_ALGORITHM
218
+ || typeof value.modelId !== "string"
219
+ || !/^ranking-[a-f0-9]{16}$/u.test(value.modelId)
220
+ || typeof value.createdAt !== "number"
221
+ || !Number.isSafeInteger(value.createdAt)
222
+ || value.createdAt < 0
223
+ || typeof value.checksum !== "string"
224
+ || !HASH_PATTERN.test(value.checksum)
225
+ || !isRecord(value.training)
226
+ || !isRecord(value.weights)
227
+ || !isRecord(value.weights.features)
228
+ || !isRecord(value.weights.sourceKinds)
229
+ || !hasOnlyKeys(value.training, ["sampleCount", "positiveSamples", "negativeSamples", "repositoryCount"])
230
+ || !hasOnlyKeys(value.weights, ["bias", "features", "sourceKinds"])
231
+ || !hasOnlyKeys(value.weights.features, NUMERIC_FEATURES)
232
+ || !hasOnlyKeys(value.weights.sourceKinds, SOURCE_KINDS)
233
+ || !assertFiniteWeight(value.weights.bias))
234
+ return undefined;
235
+ const trainingKeys = ["sampleCount", "positiveSamples", "negativeSamples", "repositoryCount"];
236
+ for (const key of trainingKeys) {
237
+ const count = value.training[key];
238
+ if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)
239
+ return undefined;
240
+ }
241
+ const featureWeights = emptyFeatureWeights();
242
+ for (const feature of NUMERIC_FEATURES) {
243
+ const weight = value.weights.features[feature];
244
+ if (!assertFiniteWeight(weight))
245
+ return undefined;
246
+ featureWeights[feature] = rounded(weight);
247
+ }
248
+ const sourceWeights = emptySourceWeights();
249
+ for (const kind of SOURCE_KINDS) {
250
+ const weight = value.weights.sourceKinds[kind];
251
+ if (!assertFiniteWeight(weight))
252
+ return undefined;
253
+ sourceWeights[kind] = rounded(weight);
254
+ }
255
+ let promotion;
256
+ if (value.promotion !== undefined) {
257
+ if (!isRecord(value.promotion))
258
+ return undefined;
259
+ const parsedPromotion = parsePromotionReport(value.promotion);
260
+ if (!parsedPromotion)
261
+ return undefined;
262
+ promotion = parsedPromotion;
263
+ }
264
+ const parsedWithoutChecksum = {
265
+ schemaVersion: RANKING_MODEL_SCHEMA_VERSION,
266
+ featureVersion: RANKING_FEATURE_VERSION,
267
+ algorithm: RANKING_ALGORITHM,
268
+ modelId: value.modelId,
269
+ createdAt: value.createdAt,
270
+ training: {
271
+ sampleCount: value.training.sampleCount,
272
+ positiveSamples: value.training.positiveSamples,
273
+ negativeSamples: value.training.negativeSamples,
274
+ repositoryCount: value.training.repositoryCount,
275
+ },
276
+ weights: { bias: rounded(value.weights.bias), features: featureWeights, sourceKinds: sourceWeights },
277
+ ...(promotion ? { promotion } : {}),
278
+ };
279
+ const expectedChecksum = sha256(stableStringify(modelPayload(parsedWithoutChecksum)));
280
+ if (expectedChecksum !== value.checksum)
281
+ return undefined;
282
+ const expectedId = sealModel({
283
+ schemaVersion: parsedWithoutChecksum.schemaVersion,
284
+ featureVersion: parsedWithoutChecksum.featureVersion,
285
+ algorithm: parsedWithoutChecksum.algorithm,
286
+ createdAt: parsedWithoutChecksum.createdAt,
287
+ training: parsedWithoutChecksum.training,
288
+ weights: parsedWithoutChecksum.weights,
289
+ ...(promotion ? { promotion } : {}),
290
+ }).modelId;
291
+ if (expectedId !== value.modelId)
292
+ return undefined;
293
+ return { ...parsedWithoutChecksum, checksum: value.checksum };
294
+ }
295
+ function parsePromotionReport(value) {
296
+ if (!hasOnlyKeys(value, [
297
+ "reportVersion",
298
+ "fixtureCount",
299
+ "heldOutRepositories",
300
+ "minimumHeldOutRepositories",
301
+ "baselineQualityScore",
302
+ "learnedQualityScore",
303
+ "qualityScoreDelta",
304
+ "baselineExactIdentifierRecall",
305
+ "learnedExactIdentifierRecall",
306
+ "exactIdentifierRecallDelta",
307
+ "privacyViolationDelta",
308
+ "atomicityFailureDelta",
309
+ "overflowDelta",
310
+ "measuredP95LatencyMs",
311
+ "latencyBudgetMs",
312
+ "deterministic",
313
+ "eligible",
314
+ "reasons",
315
+ ])
316
+ || value.reportVersion !== "ranking-promotion-v1"
317
+ || typeof value.deterministic !== "boolean"
318
+ || typeof value.eligible !== "boolean"
319
+ || !Array.isArray(value.reasons)
320
+ || value.reasons.some((reason) => typeof reason !== "string" || reason.length > 300))
321
+ return undefined;
322
+ const integerKeys = ["fixtureCount", "heldOutRepositories", "minimumHeldOutRepositories"];
323
+ for (const key of integerKeys) {
324
+ if (typeof value[key] !== "number" || !Number.isSafeInteger(value[key]) || value[key] < 0)
325
+ return undefined;
326
+ }
327
+ const numericKeys = [
328
+ "baselineQualityScore",
329
+ "learnedQualityScore",
330
+ "qualityScoreDelta",
331
+ "baselineExactIdentifierRecall",
332
+ "learnedExactIdentifierRecall",
333
+ "exactIdentifierRecallDelta",
334
+ "privacyViolationDelta",
335
+ "atomicityFailureDelta",
336
+ "overflowDelta",
337
+ "measuredP95LatencyMs",
338
+ "latencyBudgetMs",
339
+ ];
340
+ for (const key of numericKeys) {
341
+ if (typeof value[key] !== "number" || !Number.isFinite(value[key]))
342
+ return undefined;
343
+ }
344
+ if (value.minimumHeldOutRepositories < 1
345
+ || value.baselineQualityScore < 0 || value.baselineQualityScore > 1
346
+ || value.learnedQualityScore < 0 || value.learnedQualityScore > 1
347
+ || value.baselineExactIdentifierRecall < 0
348
+ || value.baselineExactIdentifierRecall > 1
349
+ || value.learnedExactIdentifierRecall < 0
350
+ || value.learnedExactIdentifierRecall > 1
351
+ || value.measuredP95LatencyMs < 0
352
+ || value.latencyBudgetMs <= 0)
353
+ return undefined;
354
+ const parsed = value;
355
+ return promotionReportPasses(parsed) === parsed.eligible ? parsed : undefined;
356
+ }
357
+ function promotionReportPasses(report) {
358
+ return report.reasons.length === 0
359
+ && report.fixtureCount > 0
360
+ && report.heldOutRepositories >= report.minimumHeldOutRepositories
361
+ && report.minimumHeldOutRepositories >= 1
362
+ && report.qualityScoreDelta > 0
363
+ && report.exactIdentifierRecallDelta >= 0
364
+ && report.privacyViolationDelta <= 0
365
+ && report.atomicityFailureDelta <= 0
366
+ && report.overflowDelta <= 0
367
+ && report.measuredP95LatencyMs <= report.latencyBudgetMs
368
+ && report.deterministic;
369
+ }
370
+ function normalizedTrainingSamples(samples) {
371
+ const unique = new Map();
372
+ for (const sample of samples) {
373
+ if (!ID_PATTERN.test(sample.sampleId)
374
+ || !HASH_PATTERN.test(sample.repositoryHash)
375
+ || !HASH_PATTERN.test(sample.candidateKeyHash)
376
+ || (sample.labelSource !== "feedback" && sample.labelSource !== "replay")
377
+ || (sample.label !== "useful" && sample.label !== "irrelevant")
378
+ || !isPrivacyClassification(sample.classification)) {
379
+ throw new Error(`Invalid ranking training sample ${sample.sampleId || "<unknown>"}`);
380
+ }
381
+ const features = parseRankingFeatures(sample.features);
382
+ if (!features)
383
+ throw new Error(`Invalid ranking features for sample ${sample.sampleId}`);
384
+ if (!unique.has(sample.sampleId))
385
+ unique.set(sample.sampleId, { ...sample, features });
386
+ }
387
+ return [...unique.values()].sort((left, right) => left.sampleId.localeCompare(right.sampleId));
388
+ }
389
+ export function trainLearnedRankingModel(samples, options) {
390
+ const normalized = normalizedTrainingSamples(samples);
391
+ const minimumSamples = options.minimumSamples ?? 20;
392
+ if (!Number.isSafeInteger(minimumSamples) || minimumSamples < 2) {
393
+ throw new Error("Ranking minimumSamples must be an integer of at least 2");
394
+ }
395
+ if (normalized.length < minimumSamples) {
396
+ throw new Error(`Ranking training requires at least ${minimumSamples} unique samples`);
397
+ }
398
+ if (!Number.isSafeInteger(options.createdAt) || options.createdAt < 0) {
399
+ throw new Error("Ranking model timestamp is invalid");
400
+ }
401
+ const positives = normalized.filter((sample) => sample.label === "useful");
402
+ const negatives = normalized.filter((sample) => sample.label === "irrelevant");
403
+ if (positives.length === 0 || negatives.length === 0) {
404
+ throw new Error("Ranking training requires both useful and irrelevant labels");
405
+ }
406
+ const featureWeights = emptyFeatureWeights();
407
+ for (const feature of NUMERIC_FEATURES) {
408
+ const positiveMean = positives.reduce((sum, sample) => sum + sample.features[feature], 0) / positives.length;
409
+ const negativeMean = negatives.reduce((sum, sample) => sum + sample.features[feature], 0) / negatives.length;
410
+ featureWeights[feature] = boundedWeight((positiveMean - negativeMean) * 4);
411
+ }
412
+ const sourceWeights = emptySourceWeights();
413
+ for (const kind of SOURCE_KINDS) {
414
+ const positiveRate = positives.filter((sample) => sample.features.sourceKind === kind).length / positives.length;
415
+ const negativeRate = negatives.filter((sample) => sample.features.sourceKind === kind).length / negatives.length;
416
+ sourceWeights[kind] = boundedWeight((positiveRate - negativeRate) * 2);
417
+ }
418
+ const positiveRate = positives.length / normalized.length;
419
+ const bias = boundedWeight((positiveRate - 0.5) * 2);
420
+ return sealModel({
421
+ schemaVersion: RANKING_MODEL_SCHEMA_VERSION,
422
+ featureVersion: RANKING_FEATURE_VERSION,
423
+ algorithm: RANKING_ALGORITHM,
424
+ createdAt: options.createdAt,
425
+ training: {
426
+ sampleCount: normalized.length,
427
+ positiveSamples: positives.length,
428
+ negativeSamples: negatives.length,
429
+ repositoryCount: new Set(normalized.map((sample) => sample.repositoryHash)).size,
430
+ },
431
+ weights: { bias, features: featureWeights, sourceKinds: sourceWeights },
432
+ });
433
+ }
434
+ function scoreValidatedRankingCandidate(model, features) {
435
+ let score = model.weights.bias + model.weights.sourceKinds[features.sourceKind];
436
+ for (const feature of NUMERIC_FEATURES)
437
+ score += model.weights.features[feature] * features[feature];
438
+ return rounded(score);
439
+ }
440
+ export function scoreLearnedRankingCandidate(model, features) {
441
+ const parsedModel = parseLearnedRankingModel(model);
442
+ const parsedFeatures = parseRankingFeatures(features);
443
+ if (!parsedModel || !parsedFeatures)
444
+ throw new Error("Learned ranking model or features are invalid");
445
+ return scoreValidatedRankingCandidate(parsedModel, parsedFeatures);
446
+ }
447
+ function staticOrder(candidates) {
448
+ return [...candidates].sort((left, right) => right.staticScore - left.staticScore || left.id.localeCompare(right.id));
449
+ }
450
+ function rankShift(staticRanks, learnedRanks) {
451
+ const ids = [...staticRanks.keys()];
452
+ let disagreements = 0;
453
+ for (let leftIndex = 0; leftIndex < ids.length; leftIndex++) {
454
+ for (let rightIndex = leftIndex + 1; rightIndex < ids.length; rightIndex++) {
455
+ const left = ids[leftIndex];
456
+ const right = ids[rightIndex];
457
+ if (!left || !right)
458
+ continue;
459
+ const staticDirection = (staticRanks.get(left) ?? 0) - (staticRanks.get(right) ?? 0);
460
+ const learnedDirection = (learnedRanks.get(left) ?? 0) - (learnedRanks.get(right) ?? 0);
461
+ if (Math.sign(staticDirection) !== Math.sign(learnedDirection))
462
+ disagreements++;
463
+ }
464
+ }
465
+ const totalShift = ids.reduce((sum, id) => sum + Math.abs((staticRanks.get(id) ?? 0) - (learnedRanks.get(id) ?? 0)), 0);
466
+ return { disagreements, meanShift: ids.length > 0 ? rounded(totalShift / ids.length) : 0 };
467
+ }
468
+ function fallbackResult(candidates, mode, startedAt, now, reason, modelId) {
469
+ const ordered = staticOrder(candidates);
470
+ return {
471
+ ranked: ordered.map((candidate, index) => ({
472
+ ...candidate,
473
+ staticRank: index,
474
+ learnedRank: index,
475
+ learnedScore: candidate.staticScore,
476
+ effectiveScore: candidate.staticScore,
477
+ })),
478
+ diagnostics: {
479
+ mode,
480
+ status: mode === "off" ? "off" : "fallback",
481
+ featureVersion: RANKING_FEATURE_VERSION,
482
+ ...(modelId ? { modelId } : {}),
483
+ candidateCount: candidates.length,
484
+ topChanged: false,
485
+ pairwiseDisagreements: 0,
486
+ meanRankShift: 0,
487
+ durationMs: Math.max(0, now() - startedAt),
488
+ ...(mode === "off" ? {} : { fallbackReason: reason }),
489
+ },
490
+ };
491
+ }
492
+ export function rankCandidates(candidates, options) {
493
+ const now = options.now ?? (() => performance.now());
494
+ const startedAt = now();
495
+ if (options.mode === "off")
496
+ return fallbackResult(candidates, "off", startedAt, now, "ranking disabled");
497
+ if (!options.model)
498
+ return fallbackResult(candidates, options.mode, startedAt, now, "ranking model is missing");
499
+ const model = parseLearnedRankingModel(options.model);
500
+ if (!model)
501
+ return fallbackResult(candidates, options.mode, startedAt, now, "ranking model is corrupt or incompatible");
502
+ if (options.mode === "active" && (!model.promotion || !promotionReportPasses(model.promotion))) {
503
+ return fallbackResult(candidates, options.mode, startedAt, now, "ranking model has not passed the promotion gate", model.modelId);
504
+ }
505
+ if (options.mode === "active" && options.maxLatencyMs !== undefined
506
+ && (!Number.isFinite(options.maxLatencyMs) || options.maxLatencyMs <= 0
507
+ || (model.promotion?.measuredP95LatencyMs ?? Number.POSITIVE_INFINITY) > options.maxLatencyMs)) {
508
+ return fallbackResult(candidates, options.mode, startedAt, now, "ranking model exceeds the configured latency gate", model.modelId);
509
+ }
510
+ const candidateIds = new Set();
511
+ if (candidates.some((candidate) => {
512
+ const invalid = !candidate.id
513
+ || candidateIds.has(candidate.id)
514
+ || !Number.isFinite(candidate.staticScore)
515
+ || !parseRankingFeatures(candidate.features);
516
+ candidateIds.add(candidate.id);
517
+ return invalid;
518
+ })) {
519
+ return fallbackResult(candidates, options.mode, startedAt, now, "ranking candidate features or IDs are invalid", model.modelId);
520
+ }
521
+ const staticallyRanked = staticOrder(candidates);
522
+ const learned = candidates.map((candidate) => ({
523
+ candidate,
524
+ learnedScore: scoreValidatedRankingCandidate(model, candidate.features),
525
+ })).sort((left, right) => right.learnedScore - left.learnedScore
526
+ || right.candidate.staticScore - left.candidate.staticScore
527
+ || left.candidate.id.localeCompare(right.candidate.id));
528
+ const staticRanks = new Map(staticallyRanked.map((candidate, index) => [candidate.id, index]));
529
+ const learnedRanks = new Map(learned.map((item, index) => [item.candidate.id, index]));
530
+ const learnedScores = new Map(learned.map((item) => [item.candidate.id, item.learnedScore]));
531
+ const order = options.mode === "active" ? learned.map((item) => item.candidate) : staticallyRanked;
532
+ const shifts = rankShift(staticRanks, learnedRanks);
533
+ const durationMs = Math.max(0, now() - startedAt);
534
+ if (options.mode === "active" && options.maxLatencyMs !== undefined && durationMs > options.maxLatencyMs) {
535
+ return fallbackResult(candidates, options.mode, startedAt, now, "learned inference exceeded the configured latency budget", model.modelId);
536
+ }
537
+ return {
538
+ ranked: order.map((candidate) => ({
539
+ ...candidate,
540
+ staticRank: staticRanks.get(candidate.id) ?? 0,
541
+ learnedRank: learnedRanks.get(candidate.id) ?? 0,
542
+ learnedScore: learnedScores.get(candidate.id) ?? candidate.staticScore,
543
+ effectiveScore: options.mode === "active"
544
+ ? learnedScores.get(candidate.id) ?? candidate.staticScore
545
+ : candidate.staticScore,
546
+ })),
547
+ diagnostics: {
548
+ mode: options.mode,
549
+ status: options.mode,
550
+ featureVersion: RANKING_FEATURE_VERSION,
551
+ modelId: model.modelId,
552
+ candidateCount: candidates.length,
553
+ topChanged: staticallyRanked[0]?.id !== learned[0]?.candidate.id,
554
+ pairwiseDisagreements: shifts.disagreements,
555
+ meanRankShift: shifts.meanShift,
556
+ durationMs,
557
+ },
558
+ };
559
+ }
560
+ function promotionPlan(fixture, ordered) {
561
+ const groups = new Map();
562
+ for (const candidate of ordered) {
563
+ const group = groups.get(candidate.groupId) ?? [];
564
+ group.push(candidate);
565
+ groups.set(candidate.groupId, group);
566
+ }
567
+ const selected = [];
568
+ let tokens = 0;
569
+ for (const candidate of ordered) {
570
+ if (selected.some((item) => item.groupId === candidate.groupId))
571
+ continue;
572
+ const group = groups.get(candidate.groupId) ?? [candidate];
573
+ const groupTokens = group.reduce((sum, item) => sum + item.tokens, 0);
574
+ if (selected.length + group.length > fixture.maxResults || tokens + groupTokens > fixture.maxTokens)
575
+ continue;
576
+ selected.push(...group);
577
+ tokens += groupTokens;
578
+ }
579
+ const selectedGroups = new Map();
580
+ for (const candidate of selected) {
581
+ selectedGroups.set(candidate.groupId, (selectedGroups.get(candidate.groupId) ?? 0) + 1);
582
+ }
583
+ const atomicityFailures = [...selectedGroups].filter(([groupId, count]) => count !== (groups.get(groupId)?.length ?? 0)).length;
584
+ return {
585
+ selected,
586
+ overflow: selected.length > fixture.maxResults || tokens > fixture.maxTokens,
587
+ atomicityFailures,
588
+ privacyViolations: selected.filter((candidate) => candidate.features.classificationEligible < 1).length,
589
+ };
590
+ }
591
+ function promotionMetrics(fixtures, model) {
592
+ let relevantAvailable = 0;
593
+ let relevantSelected = 0;
594
+ let irrelevantTokens = 0;
595
+ let selectedTokens = 0;
596
+ let exactAvailable = 0;
597
+ let exactSelected = 0;
598
+ let privacyViolations = 0;
599
+ let atomicityFailures = 0;
600
+ let overflow = 0;
601
+ const signatures = [];
602
+ for (const fixture of fixtures) {
603
+ const ordered = model
604
+ ? rankCandidates(fixture.candidates.map((candidate) => ({
605
+ id: candidate.id,
606
+ staticScore: candidate.staticScore,
607
+ features: candidate.features,
608
+ value: candidate,
609
+ })), { mode: "shadow", model, now: () => 0 }).ranked
610
+ .sort((left, right) => left.learnedRank - right.learnedRank)
611
+ .map((candidate) => candidate.value)
612
+ : [...fixture.candidates].sort((left, right) => right.staticScore - left.staticScore || left.id.localeCompare(right.id));
613
+ const plan = promotionPlan(fixture, ordered);
614
+ signatures.push(plan.selected.map((candidate) => candidate.id).join(","));
615
+ relevantAvailable += fixture.candidates.filter((candidate) => candidate.relevant).length;
616
+ relevantSelected += plan.selected.filter((candidate) => candidate.relevant).length;
617
+ exactAvailable += fixture.candidates.filter((candidate) => candidate.relevant && candidate.exactIdentifier).length;
618
+ exactSelected += plan.selected.filter((candidate) => candidate.relevant && candidate.exactIdentifier).length;
619
+ selectedTokens += plan.selected.reduce((sum, candidate) => sum + candidate.tokens, 0);
620
+ irrelevantTokens += plan.selected
621
+ .filter((candidate) => !candidate.relevant)
622
+ .reduce((sum, candidate) => sum + candidate.tokens, 0);
623
+ privacyViolations += plan.privacyViolations;
624
+ atomicityFailures += plan.atomicityFailures;
625
+ overflow += Number(plan.overflow);
626
+ }
627
+ const recall = relevantAvailable > 0 ? relevantSelected / relevantAvailable : 1;
628
+ const precisionByTokens = selectedTokens > 0 ? 1 - irrelevantTokens / selectedTokens : 1;
629
+ const atomicity = atomicityFailures === 0 ? 1 : 0;
630
+ const noOverflow = overflow === 0 ? 1 : 0;
631
+ return {
632
+ qualityScore: rounded(recall * 0.6 + precisionByTokens * 0.2 + atomicity * 0.1 + noOverflow * 0.1),
633
+ exactRecall: exactAvailable > 0 ? rounded(exactSelected / exactAvailable) : 1,
634
+ privacyViolations,
635
+ atomicityFailures,
636
+ overflow,
637
+ signatures,
638
+ };
639
+ }
640
+ function validatePromotionFixture(fixture) {
641
+ if (!ID_PATTERN.test(fixture.fixtureId) || !HASH_PATTERN.test(fixture.repositoryHash)) {
642
+ throw new Error(`Invalid ranking promotion fixture ${fixture.fixtureId || "<unknown>"}`);
643
+ }
644
+ if (!Number.isSafeInteger(fixture.maxResults) || fixture.maxResults < 1
645
+ || !Number.isSafeInteger(fixture.maxTokens) || fixture.maxTokens < 1) {
646
+ throw new Error(`Invalid ranking promotion budget for ${fixture.fixtureId}`);
647
+ }
648
+ const ids = new Set();
649
+ for (const candidate of fixture.candidates) {
650
+ if (!ID_PATTERN.test(candidate.id) || !ID_PATTERN.test(candidate.groupId) || ids.has(candidate.id)
651
+ || !Number.isSafeInteger(candidate.tokens) || candidate.tokens < 0
652
+ || !Number.isFinite(candidate.staticScore)
653
+ || !parseRankingFeatures(candidate.features)) {
654
+ throw new Error(`Invalid ranking promotion candidate in ${fixture.fixtureId}`);
655
+ }
656
+ ids.add(candidate.id);
657
+ }
658
+ }
659
+ export function evaluateRankingPromotion(modelInput, fixtures, options) {
660
+ const model = parseLearnedRankingModel(modelInput);
661
+ if (!model)
662
+ throw new Error("Cannot evaluate an invalid ranking model");
663
+ if (fixtures.length === 0)
664
+ throw new Error("Ranking promotion requires held-out fixtures");
665
+ fixtures.forEach(validatePromotionFixture);
666
+ if (!Number.isFinite(options.measuredP95LatencyMs) || options.measuredP95LatencyMs < 0
667
+ || !Number.isFinite(options.latencyBudgetMs) || options.latencyBudgetMs <= 0) {
668
+ throw new Error("Ranking promotion latency values are invalid");
669
+ }
670
+ const minimumHeldOutRepositories = options.minimumHeldOutRepositories ?? 2;
671
+ if (!Number.isSafeInteger(minimumHeldOutRepositories) || minimumHeldOutRepositories < 1) {
672
+ throw new Error("Ranking promotion minimumHeldOutRepositories must be a positive integer");
673
+ }
674
+ const baseline = promotionMetrics(fixtures);
675
+ const learned = promotionMetrics(fixtures, model);
676
+ const repeated = promotionMetrics(fixtures, model);
677
+ const heldOutRepositories = new Set(fixtures.map((fixture) => fixture.repositoryHash)).size;
678
+ const deterministic = stableStringify(learned.signatures) === stableStringify(repeated.signatures);
679
+ const qualityScoreDelta = rounded(learned.qualityScore - baseline.qualityScore);
680
+ const exactIdentifierRecallDelta = rounded(learned.exactRecall - baseline.exactRecall);
681
+ const privacyViolationDelta = learned.privacyViolations - baseline.privacyViolations;
682
+ const atomicityFailureDelta = learned.atomicityFailures - baseline.atomicityFailures;
683
+ const overflowDelta = learned.overflow - baseline.overflow;
684
+ const reasons = [
685
+ ...(qualityScoreDelta > 0 ? [] : ["primary quality score did not improve"]),
686
+ ...(exactIdentifierRecallDelta >= 0 ? [] : ["exact-identifier recall regressed"]),
687
+ ...(privacyViolationDelta <= 0 ? [] : ["privacy violations increased"]),
688
+ ...(atomicityFailureDelta <= 0 ? [] : ["atomicity failures increased"]),
689
+ ...(overflowDelta <= 0 ? [] : ["overflow increased"]),
690
+ ...(options.measuredP95LatencyMs <= options.latencyBudgetMs ? [] : ["planner latency budget exceeded"]),
691
+ ...(deterministic ? [] : ["inference ordering was not deterministic"]),
692
+ ...(heldOutRepositories >= minimumHeldOutRepositories
693
+ ? []
694
+ : [`fewer than ${minimumHeldOutRepositories} held-out repositories`]),
695
+ ];
696
+ return {
697
+ reportVersion: "ranking-promotion-v1",
698
+ fixtureCount: fixtures.length,
699
+ heldOutRepositories,
700
+ minimumHeldOutRepositories,
701
+ baselineQualityScore: baseline.qualityScore,
702
+ learnedQualityScore: learned.qualityScore,
703
+ qualityScoreDelta,
704
+ baselineExactIdentifierRecall: baseline.exactRecall,
705
+ learnedExactIdentifierRecall: learned.exactRecall,
706
+ exactIdentifierRecallDelta,
707
+ privacyViolationDelta,
708
+ atomicityFailureDelta,
709
+ overflowDelta,
710
+ measuredP95LatencyMs: rounded(options.measuredP95LatencyMs),
711
+ latencyBudgetMs: rounded(options.latencyBudgetMs),
712
+ deterministic,
713
+ eligible: reasons.length === 0,
714
+ reasons,
715
+ };
716
+ }
717
+ export function withRankingPromotion(modelInput, promotion) {
718
+ const model = parseLearnedRankingModel(modelInput);
719
+ const parsedPromotion = parsePromotionReport(promotion);
720
+ if (!model || !parsedPromotion)
721
+ throw new Error("Cannot promote an invalid ranking model or report");
722
+ return sealModel({
723
+ schemaVersion: model.schemaVersion,
724
+ featureVersion: model.featureVersion,
725
+ algorithm: model.algorithm,
726
+ createdAt: model.createdAt,
727
+ training: { ...model.training },
728
+ weights: {
729
+ bias: model.weights.bias,
730
+ features: { ...model.weights.features },
731
+ sourceKinds: { ...model.weights.sourceKinds },
732
+ },
733
+ promotion: parsedPromotion,
734
+ });
735
+ }
736
+ export function loadLearnedRankingModel(path) {
737
+ try {
738
+ if (statSync(path).size > MAX_RANKING_MODEL_BYTES) {
739
+ return { error: `ranking model exceeds ${MAX_RANKING_MODEL_BYTES} bytes` };
740
+ }
741
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
742
+ const model = parseLearnedRankingModel(parsed);
743
+ return model ? { model } : { error: "ranking model is corrupt or incompatible" };
744
+ }
745
+ catch (error) {
746
+ return { error: error instanceof Error ? error.message : String(error) };
747
+ }
748
+ }
749
+ export function saveLearnedRankingModel(path, modelInput) {
750
+ const model = parseLearnedRankingModel(modelInput);
751
+ if (!model)
752
+ throw new Error("Cannot save an invalid ranking model");
753
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
754
+ const temporary = `${path}.${process.pid}.tmp`;
755
+ try {
756
+ writeFileSync(temporary, `${stableStringify(model)}\n`, { encoding: "utf8", mode: 0o600 });
757
+ renameSync(temporary, path);
758
+ }
759
+ finally {
760
+ rmSync(temporary, { force: true });
761
+ }
762
+ try {
763
+ chmodSync(path, 0o600);
764
+ }
765
+ catch {
766
+ // Best effort on filesystems without POSIX permissions.
767
+ }
768
+ }
769
+ export function disabledRankingDiagnostics(mode = "off") {
770
+ return {
771
+ mode,
772
+ status: mode === "off" ? "off" : "fallback",
773
+ featureVersion: RANKING_FEATURE_VERSION,
774
+ candidateCount: 0,
775
+ topChanged: false,
776
+ pairwiseDisagreements: 0,
777
+ meanRankShift: 0,
778
+ durationMs: 0,
779
+ ...(mode === "off" ? {} : { fallbackReason: "ranking model has not been loaded" }),
780
+ };
781
+ }
782
+ //# sourceMappingURL=learned-ranker.js.map