@harperfast/harper 5.3.0 → 5.3.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/bin/copyDb.ts +5 -1
- package/bin/deployRenderer.ts +3 -0
- package/components/Application.ts +66 -22
- package/components/DESIGN.md +89 -36
- package/components/componentLoader.ts +2 -0
- package/components/deploymentRecorder.ts +18 -1
- package/components/installFingerprint.ts +123 -0
- package/components/mcp/DESIGN.md +4 -4
- package/components/operations.js +29 -0
- package/config/DESIGN.md +5 -5
- package/dataLayer/DESIGN.md +37 -22
- package/dataLayer/backupRepository.ts +248 -0
- package/dataLayer/restoreMarker.ts +30 -75
- package/dataLayer/rocksdbBackup.ts +253 -110
- package/dist/bin/copyDb.js +3 -0
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deployRenderer.js +3 -0
- package/dist/bin/deployRenderer.js.map +1 -1
- package/dist/components/Application.d.ts +5 -0
- package/dist/components/Application.js +51 -19
- package/dist/components/Application.js.map +1 -1
- package/dist/components/componentLoader.js +2 -0
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentRecorder.js +18 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/installFingerprint.d.ts +39 -0
- package/dist/components/installFingerprint.js +113 -0
- package/dist/components/installFingerprint.js.map +1 -0
- package/dist/components/operations.d.ts +2 -0
- package/dist/components/operations.js +31 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/dataLayer/backupRepository.d.ts +33 -0
- package/dist/dataLayer/backupRepository.js +232 -0
- package/dist/dataLayer/backupRepository.js.map +1 -0
- package/dist/dataLayer/restoreMarker.d.ts +4 -1
- package/dist/dataLayer/restoreMarker.js +29 -68
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.js +221 -112
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +14 -0
- package/dist/resources/DatabaseTransaction.d.ts +140 -11
- package/dist/resources/DatabaseTransaction.js +1055 -464
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/IterableEventQueue.d.ts +2 -1
- package/dist/resources/IterableEventQueue.js +16 -8
- package/dist/resources/IterableEventQueue.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +13 -2
- package/dist/resources/LMDBTransaction.js +219 -93
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/RecordEncoder.js +2 -3
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +5 -6
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +3 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +2 -1
- package/dist/resources/RocksIndexStore.js +3 -0
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/Table.js +116 -12
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -2
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +71 -17
- package/dist/resources/auditStore.js +248 -64
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/branchDatabase.js +2 -0
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databases.js +8 -2
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +6 -2
- package/dist/resources/indexes/fullTextNativeBinding.js +12 -2
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
- package/dist/resources/indexes/fullTextQueryIndex.d.ts +4 -0
- package/dist/resources/indexes/fullTextQueryIndex.js +207 -10
- package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -0
- package/dist/resources/models/Models.js +95 -4
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +8 -0
- package/dist/resources/models/backendRegistry.js +15 -1
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +2 -0
- package/dist/resources/models/bootstrap.js +6 -0
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/calibration.d.ts +88 -0
- package/dist/resources/models/calibration.js +244 -0
- package/dist/resources/models/calibration.js.map +1 -0
- package/dist/resources/models/calibrationStore.d.ts +163 -0
- package/dist/resources/models/calibrationStore.js +986 -0
- package/dist/resources/models/calibrationStore.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +24 -0
- package/dist/resources/models/decisionStore.js +45 -0
- package/dist/resources/models/decisionStore.js.map +1 -1
- package/dist/resources/models/generativeDecision.js +44 -15
- package/dist/resources/models/generativeDecision.js.map +1 -1
- package/dist/resources/models/types.d.ts +7 -0
- package/dist/resources/recordLock.d.ts +8 -5
- package/dist/resources/recordLock.js +6 -4
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/replayLogs.js +8 -1
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/scheduler/engine.d.ts +4 -0
- package/dist/resources/scheduler/engine.js +30 -2
- package/dist/resources/scheduler/engine.js.map +1 -1
- package/dist/resources/search.d.ts +16 -4
- package/dist/resources/search.js +117 -13
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transaction.js +85 -5
- package/dist/resources/transaction.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +8 -0
- package/dist/resources/transactionBroadcast.js +65 -0
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/keys.d.ts +1 -1
- package/dist/security/keys.js +170 -120
- package/dist/security/keys.js.map +1 -1
- package/dist/server/http.js +10 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Request.js +11 -2
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +8 -3
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +38 -7
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +3 -3
- package/dist/server/threads/manageThreads.js +15 -13
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +3 -0
- package/dist/server/threads/threadServer.js +24 -6
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +9 -3
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/durableFile.d.ts +36 -0
- package/dist/utility/durableFile.js +106 -0
- package/dist/utility/durableFile.js.map +1 -0
- package/dist/utility/errors/hdbError.d.ts +4 -0
- package/dist/utility/errors/hdbError.js +10 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +1 -0
- package/dist/utility/hdbTerms.js +1 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/validation/configValidator.js +29 -0
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +7 -0
- package/json/systemSchema.json +14 -0
- package/npm-shrinkwrap.json +22 -22
- package/package.json +2 -2
- package/resources/DESIGN.md +57 -272
- package/resources/DatabaseTransaction.ts +708 -87
- package/resources/FULL_TEXT.md +6 -2
- package/resources/IterableEventQueue.ts +14 -7
- package/resources/LMDBTransaction.ts +212 -84
- package/resources/RecordEncoder.ts +2 -3
- package/resources/Resource.ts +5 -6
- package/resources/ResourceInterface.ts +3 -1
- package/resources/RocksIndexStore.ts +5 -0
- package/resources/Table.ts +118 -15
- package/resources/analytics/write.ts +6 -2
- package/resources/audit-retention.md +203 -0
- package/resources/auditStore.ts +306 -84
- package/resources/branchDatabase.ts +2 -0
- package/resources/databases.ts +6 -2
- package/resources/indexes/DESIGN.md +120 -177
- package/resources/indexes/fullTextNativeBinding.ts +22 -3
- package/resources/indexes/fullTextQueryIndex.ts +274 -11
- package/resources/models/DESIGN.md +11 -3
- package/resources/models/Models.ts +127 -4
- package/resources/models/backendRegistry.ts +17 -0
- package/resources/models/bootstrap.ts +9 -1
- package/resources/models/calibration.ts +298 -0
- package/resources/models/calibrationStore.ts +1167 -0
- package/resources/models/decisionStore.ts +57 -0
- package/resources/models/generativeDecision.ts +54 -16
- package/resources/models/types.ts +6 -0
- package/resources/record-locks.md +155 -141
- package/resources/recordLock.ts +12 -6
- package/resources/replayLogs.ts +9 -2
- package/resources/scheduler/DESIGN.md +4 -3
- package/resources/scheduler/engine.ts +35 -2
- package/resources/search.ts +154 -16
- package/resources/transaction.ts +50 -5
- package/resources/transactionBroadcast.ts +69 -0
- package/security/DESIGN.md +9 -30
- package/security/keys.ts +175 -127
- package/server/DESIGN.md +70 -36
- package/server/http.ts +10 -0
- package/server/serverHelpers/Request.ts +10 -2
- package/server/serverHelpers/contentTypes.ts +5 -3
- package/server/serverHelpers/uwsServer.ts +34 -7
- package/server/threads/manageThreads.js +16 -14
- package/server/threads/threadServer.js +24 -6
- package/studio/web/assets/{Chat-CNEKMllJ.js → Chat-4LMxI0yB.js} +1 -1
- package/studio/web/assets/{FloatingChat-CEnWhHeR.js → FloatingChat-kWHApEtF.js} +3 -3
- package/studio/web/assets/{SearchDialog-CwhJdEdY.js → SearchDialog-B7ymna97.js} +1 -1
- package/studio/web/assets/{apiToken-DC5vsaaO.js → apiToken-CE3AAt9f.js} +1 -1
- package/studio/web/assets/{applications-DQZxDy0x.js → applications-DdN7CqsB.js} +1 -1
- package/studio/web/assets/{cssMode-Dz2DxF4e.js → cssMode-BpySZUSt.js} +1 -1
- package/studio/web/assets/{editor-B3mjd3tz.js → editor-myLCVv9C.js} +1 -1
- package/studio/web/assets/{html-BsRGM7tp.js → html-DGNYO55O.js} +1 -1
- package/studio/web/assets/{htmlMode-tUKJaA3o.js → htmlMode-BUgpQr41.js} +1 -1
- package/studio/web/assets/{index-BuqFjN5K.js → index-FN4KC9va.js} +39 -39
- package/studio/web/assets/index-QLZw6xrU.css +1 -0
- package/studio/web/assets/{index.lazy-CcpfQFY_.js → index.lazy-DZFuaumG.js} +1 -1
- package/studio/web/assets/{javascript-BZ_uRGHl.js → javascript-DiBzh4pj.js} +1 -1
- package/studio/web/assets/{jsonMode-DgB-aFMj.js → jsonMode-Bm312nmu.js} +1 -1
- package/studio/web/assets/{languageServices-BleuBw17.js → languageServices-Dz2K9rZN.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-ByVYLSsQ.js → lspLanguageFeatures-CNu1L5xn.js} +1 -1
- package/studio/web/assets/{notifications-TuzN0IMi.js → notifications--nwFLGmM.js} +1 -1
- package/studio/web/assets/{notifications-D1Nf-Edn.js → notifications-BelKw5bR.js} +1 -1
- package/studio/web/assets/{profile-D7C7Lupz.js → profile-AU5FjOBx.js} +1 -1
- package/studio/web/assets/{regions-nGQCm-v8.js → regions-C0sTSEHH.js} +1 -1
- package/studio/web/assets/{register-C3x9Ze6P.js → register-DyNR2rGk.js} +2 -2
- package/studio/web/assets/{setComponentFile-DpjrRqeG.js → setComponentFile-BkWx8AN3.js} +1 -1
- package/studio/web/assets/{setup-CGiKLZ9e.js → setup-D6LZS78d.js} +2 -2
- package/studio/web/assets/{status-CGJwrNPH.js → status-BCWnn70o.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-C-Ne6P7T.js → toggleHighContrast-pyeGJ6W1.js} +1 -1
- package/studio/web/assets/{tsMode-CSreYTys.js → tsMode-CGV3iaWC.js} +1 -1
- package/studio/web/assets/{typescript-MnxBk89W.js → typescript-Ci7Khvwo.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-TNrh77-s.js → useEntityRestURL-DNiz3aZ0.js} +1 -1
- package/studio/web/assets/{workers-BfZlUNVI.js → workers-byzxjbqy.js} +1 -1
- package/studio/web/assets/{xml-BHd6qGH7.js → xml-DGnHQ3CX.js} +1 -1
- package/studio/web/assets/{yaml-C53R8OQq.js → yaml-BUoTnPWb.js} +1 -1
- package/studio/web/index.html +2 -2
- package/upgrade/directives/5-3-0.ts +10 -3
- package/utility/durableFile.ts +96 -0
- package/utility/errors/hdbError.ts +9 -0
- package/utility/hdbTerms.ts +1 -0
- package/validation/configValidator.ts +31 -0
- package/studio/web/assets/index-72cEH474.css +0 -1
|
@@ -3,9 +3,25 @@ import { contextStorage } from '../transaction.ts';
|
|
|
3
3
|
import {
|
|
4
4
|
defineBackend,
|
|
5
5
|
getBackend,
|
|
6
|
+
getBackendSource,
|
|
6
7
|
ModelBackendNotFoundError,
|
|
7
8
|
registerBackend as registerBackendImpl,
|
|
9
|
+
SERVED_SOURCE,
|
|
10
|
+
type ServedSourceHook,
|
|
8
11
|
} from './backendRegistry.ts';
|
|
12
|
+
import {
|
|
13
|
+
type AppliedCalibration,
|
|
14
|
+
applyFits,
|
|
15
|
+
type CalibrationBudgets,
|
|
16
|
+
type CalibrationRunResult,
|
|
17
|
+
type CalibrationSummary,
|
|
18
|
+
isCalibrationEnabled,
|
|
19
|
+
listCalibrations,
|
|
20
|
+
runCalibration,
|
|
21
|
+
} from './calibrationStore.ts';
|
|
22
|
+
import { populationKey, populationRank } from './calibration.ts';
|
|
23
|
+
import { safeErrorMessage } from '../scheduler/engine.ts';
|
|
24
|
+
import harperLogger from '../../utility/logging/harper_logger.ts';
|
|
9
25
|
import { getRouter, registerRouter as registerRouterImpl } from './routing.ts';
|
|
10
26
|
import { getModelCallAnalyticsWriter, type ModelCallAnalyticsWriter, type ModelCallRecord } from './analyticsTable.ts';
|
|
11
27
|
import { recordAction } from '../analytics/write.ts';
|
|
@@ -214,6 +230,7 @@ export class Models implements ModelsContract {
|
|
|
214
230
|
let firstError: unknown = undefined;
|
|
215
231
|
let hasError = false;
|
|
216
232
|
for (const backend of resolved.candidates) {
|
|
233
|
+
let completed: GenerateResult | undefined;
|
|
217
234
|
// Don't spend a backend call on an already-cancelled request — between
|
|
218
235
|
// candidates the caller may have aborted (and at entry it may already be).
|
|
219
236
|
signal?.throwIfAborted();
|
|
@@ -227,7 +244,7 @@ export class Models implements ModelsContract {
|
|
|
227
244
|
// Propagate usage onto the returned GenerateResult so callers (notably the
|
|
228
245
|
// `toolMode: 'auto'` loop's budget tracker) can read cumulative tokens without
|
|
229
246
|
// re-querying analytics. Pure pass-through — backend usage is the source of truth.
|
|
230
|
-
|
|
247
|
+
completed = result.usage ? { ...result.output, usage: result.usage } : result.output;
|
|
231
248
|
} catch (err) {
|
|
232
249
|
this.#recordFailure(backend, 'generate', opts.model, accounting, opts, attemptStart, err);
|
|
233
250
|
if (!hasError) {
|
|
@@ -236,6 +253,10 @@ export class Models implements ModelsContract {
|
|
|
236
253
|
}
|
|
237
254
|
if (signal?.aborted) throw err; // caller cancelled — stop, surface the abort
|
|
238
255
|
}
|
|
256
|
+
if (completed) {
|
|
257
|
+
reportServed(opts, backend);
|
|
258
|
+
return completed;
|
|
259
|
+
}
|
|
239
260
|
}
|
|
240
261
|
throw firstError;
|
|
241
262
|
}
|
|
@@ -389,6 +410,17 @@ export class Models implements ModelsContract {
|
|
|
389
410
|
continue;
|
|
390
411
|
}
|
|
391
412
|
const callId = this.#record(backend, 'decide', callOpts.model, accounting, undefined, result, attemptStart);
|
|
413
|
+
const calibration = calibrate(
|
|
414
|
+
call,
|
|
415
|
+
decision,
|
|
416
|
+
backend,
|
|
417
|
+
callOpts.model,
|
|
418
|
+
instructions,
|
|
419
|
+
accounting,
|
|
420
|
+
result.output,
|
|
421
|
+
identity
|
|
422
|
+
);
|
|
423
|
+
if (calibration.applied) decision = calibration.applied.decision;
|
|
392
424
|
if (!identity) return result.usage ? { ...decision, usage: result.usage } : decision;
|
|
393
425
|
const id = await this.#persistDecision(
|
|
394
426
|
callId,
|
|
@@ -398,7 +430,8 @@ export class Models implements ModelsContract {
|
|
|
398
430
|
accounting,
|
|
399
431
|
result.output,
|
|
400
432
|
identity,
|
|
401
|
-
decision
|
|
433
|
+
decision,
|
|
434
|
+
calibration
|
|
402
435
|
);
|
|
403
436
|
return result.usage ? { id, ...decision, usage: result.usage } : { id, ...decision };
|
|
404
437
|
}
|
|
@@ -437,6 +470,7 @@ export class Models implements ModelsContract {
|
|
|
437
470
|
let firstFailure: unknown = undefined;
|
|
438
471
|
let hasFailure = false;
|
|
439
472
|
for (const backend of resolved.candidates) {
|
|
473
|
+
let completed: (ChoiceScores & { usage?: TokenUsage }) | undefined;
|
|
440
474
|
signal?.throwIfAborted();
|
|
441
475
|
const attemptStart = performance.now();
|
|
442
476
|
try {
|
|
@@ -462,7 +496,7 @@ export class Models implements ModelsContract {
|
|
|
462
496
|
`Backend '${backend.name}' did not return one finite log-likelihood per choice (${choices.length})`
|
|
463
497
|
);
|
|
464
498
|
this.#record(backend, 'scoreChoices', opts.model, accounting, undefined, result, attemptStart);
|
|
465
|
-
|
|
499
|
+
completed = result.usage ? { logLikelihoods, usage: result.usage } : { logLikelihoods };
|
|
466
500
|
} catch (err) {
|
|
467
501
|
// Only this attempt's own decline bills its tokens: the error may travel on as an abort
|
|
468
502
|
// reason or through the decision adapter, and those rows must not count it again.
|
|
@@ -486,6 +520,10 @@ export class Models implements ModelsContract {
|
|
|
486
520
|
}
|
|
487
521
|
if (signal?.aborted) throw err;
|
|
488
522
|
}
|
|
523
|
+
if (completed) {
|
|
524
|
+
reportServed(opts, backend);
|
|
525
|
+
return completed;
|
|
526
|
+
}
|
|
489
527
|
}
|
|
490
528
|
throw hasFailure ? firstFailure : firstError;
|
|
491
529
|
}
|
|
@@ -496,6 +534,20 @@ export class Models implements ModelsContract {
|
|
|
496
534
|
DecisionRecord<T> | undefined;
|
|
497
535
|
}
|
|
498
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Fit calibrations from recorded outcomes now, as the periodic job does, over every tenant's decisions:
|
|
539
|
+
* a maintenance operation for trusted code. `budgets` may narrow this run; the fit policy always comes
|
|
540
|
+
* from configuration. Resolves with the run's result; a data fault is reported there, never thrown.
|
|
541
|
+
*/
|
|
542
|
+
calibrate(budgets?: CalibrationBudgets): Promise<CalibrationRunResult> {
|
|
543
|
+
return runCalibration(budgets);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The newest calibration of each field of each population the caller's tenant owns. */
|
|
547
|
+
getCalibrations(filter?: { model?: string }): Promise<CalibrationSummary[]> {
|
|
548
|
+
return listCalibrations(resolveCallContext().accounting.tenantId, filter);
|
|
549
|
+
}
|
|
550
|
+
|
|
499
551
|
async recordOutcome<T = unknown>(id: string, outcome: OutcomeReport): Promise<DecisionRecord<T>> {
|
|
500
552
|
checkDecisionId(id);
|
|
501
553
|
return (await this.#decisionStore.recordOutcome(
|
|
@@ -513,7 +565,8 @@ export class Models implements ModelsContract {
|
|
|
513
565
|
accounting: AccountingContext,
|
|
514
566
|
output: DecisionOutput<unknown>,
|
|
515
567
|
identity: CallIdentity,
|
|
516
|
-
decision: Omit<Decision<T>, 'id' | 'usage'
|
|
568
|
+
decision: Omit<Decision<T>, 'id' | 'usage'>,
|
|
569
|
+
calibration: Calibration<T>
|
|
517
570
|
): Promise<string> {
|
|
518
571
|
const id = newDecisionId();
|
|
519
572
|
const at = Date.now();
|
|
@@ -538,6 +591,16 @@ export class Models implements ModelsContract {
|
|
|
538
591
|
calibrated: decision.calibrated,
|
|
539
592
|
};
|
|
540
593
|
if (decision.noMatch !== undefined) row.noMatch = decision.noMatch;
|
|
594
|
+
if (calibration.entry !== undefined) row.entry = calibration.entry;
|
|
595
|
+
if (calibration.population !== undefined) {
|
|
596
|
+
row.population = calibration.population;
|
|
597
|
+
row.populationRank = populationRank(calibration.population, row.expiresAt, row.id);
|
|
598
|
+
}
|
|
599
|
+
if (calibration.applied) {
|
|
600
|
+
row.rawDistribution = calibration.applied.rawDistribution;
|
|
601
|
+
row.rawFields = calibration.applied.rawFields;
|
|
602
|
+
row.calibration = calibration.applied.snapshot;
|
|
603
|
+
}
|
|
541
604
|
try {
|
|
542
605
|
await this.#decisionStore.persist(row);
|
|
543
606
|
} catch (err) {
|
|
@@ -714,9 +777,69 @@ function toBackendOpts<TOpts extends { model?: string; signal?: AbortSignal }>(
|
|
|
714
777
|
): BackendOpts<TOpts> {
|
|
715
778
|
const backendOpts = { ...opts, signal, accounting };
|
|
716
779
|
delete backendOpts.model;
|
|
780
|
+
if (SERVED_SOURCE in backendOpts) delete (backendOpts as Record<symbol, unknown>)[SERVED_SOURCE];
|
|
717
781
|
return backendOpts;
|
|
718
782
|
}
|
|
719
783
|
|
|
784
|
+
const calibrationLog = harperLogger.forComponent('models').conditional;
|
|
785
|
+
let lastCalibrationFaultLog = 0;
|
|
786
|
+
|
|
787
|
+
function logCalibrationFault(what: string, err: unknown): void {
|
|
788
|
+
const now = Date.now();
|
|
789
|
+
if (now - lastCalibrationFaultLog < 60_000) return;
|
|
790
|
+
lastCalibrationFaultLog = now;
|
|
791
|
+
calibrationLog.warn?.(`models: ${what}: ${safeErrorMessage(err)}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function reportServed(opts: object, backend: ModelBackend): void {
|
|
795
|
+
const hook = (opts as Record<symbol, unknown>)[SERVED_SOURCE];
|
|
796
|
+
if (typeof hook !== 'function') return;
|
|
797
|
+
try {
|
|
798
|
+
(hook as ServedSourceHook)(getBackendSource(backend));
|
|
799
|
+
} catch (err) {
|
|
800
|
+
logCalibrationFault('a served-source hook failed', err);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
interface Calibration<T> {
|
|
805
|
+
entry?: string;
|
|
806
|
+
population?: string;
|
|
807
|
+
applied?: AppliedCalibration<T>;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const NOT_CALIBRATING: Calibration<never> = {};
|
|
811
|
+
|
|
812
|
+
function calibrate<T>(
|
|
813
|
+
schema: DecisionSchema,
|
|
814
|
+
decision: Omit<Decision<T>, 'id' | 'usage'>,
|
|
815
|
+
backend: ModelBackend,
|
|
816
|
+
model: string | undefined,
|
|
817
|
+
instructions: string | undefined,
|
|
818
|
+
accounting: AccountingContext,
|
|
819
|
+
output: DecisionOutput<unknown>,
|
|
820
|
+
identity: CallIdentity | undefined
|
|
821
|
+
): Calibration<T> {
|
|
822
|
+
if (!isCalibrationEnabled()) return NOT_CALIBRATING as Calibration<T>;
|
|
823
|
+
try {
|
|
824
|
+
const signature = typeof output.signature === 'string' && output.signature ? output.signature : undefined;
|
|
825
|
+
const entry = getBackendSource(backend) ?? `registered:${backend.name}`;
|
|
826
|
+
if (!signature) return { entry };
|
|
827
|
+
const population = populationKey({
|
|
828
|
+
tenant: accounting.tenantId,
|
|
829
|
+
model: model ?? 'default',
|
|
830
|
+
entry,
|
|
831
|
+
signature,
|
|
832
|
+
instructionsHash: instructions ? hashText(instructions) : undefined,
|
|
833
|
+
schemaHash: identity?.hash ?? hashSchema(schema),
|
|
834
|
+
});
|
|
835
|
+
if (decision.calibrated) return { entry, population };
|
|
836
|
+
return { entry, population, applied: applyFits(schema, decision, population) };
|
|
837
|
+
} catch (err) {
|
|
838
|
+
logCalibrationFault('calibration was not applied', err);
|
|
839
|
+
return NOT_CALIBRATING as Calibration<T>;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
720
843
|
function resolveCandidates(kind: ModelKind, model: string | undefined, requires: Capability[]): Resolution {
|
|
721
844
|
const logicalName = model ?? 'default';
|
|
722
845
|
const candidates = getRouter().route({ kind, logicalName, requires: [...requires] });
|
|
@@ -51,6 +51,23 @@ interface CaptureSlot {
|
|
|
51
51
|
active: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const sources = new WeakMap<ModelBackend, string>();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An internal option key: the decision adapter passes a hook under it on inner calls, and the facade
|
|
58
|
+
* reports each successful attempt's source through it. A symbol, so it is never part of the public options.
|
|
59
|
+
*/
|
|
60
|
+
export const SERVED_SOURCE = Symbol('models.servedSource');
|
|
61
|
+
export type ServedSourceHook = (source: string | undefined) => void;
|
|
62
|
+
|
|
63
|
+
export function setBackendSource(backend: ModelBackend, fingerprint: string): void {
|
|
64
|
+
sources.set(backend, fingerprint);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getBackendSource(backend: ModelBackend): string | undefined {
|
|
68
|
+
return sources.get(backend);
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
// Async-context scoped: a module-global slot would divert unrelated registrations and collide
|
|
55
72
|
// concurrent constructions.
|
|
56
73
|
const captureScope = new AsyncLocalStorage<CaptureSlot>();
|
|
@@ -34,7 +34,8 @@ import { registerOllamaBackend, type OllamaBackendConfig } from '../../component
|
|
|
34
34
|
import { registerOpenAIBackend, type OpenAIBackendConfig } from '../../components/openai/index.ts';
|
|
35
35
|
import { registerAnthropicBackend, type AnthropicBackendConfig } from '../../components/anthropic/index.ts';
|
|
36
36
|
import { registerBedrockBackend, type BedrockBackendConfig } from '../../components/bedrock/index.ts';
|
|
37
|
-
import { setModelsConfigHash } from './decisionStore.ts';
|
|
37
|
+
import { setModelsConfigHash, sourceFingerprint } from './decisionStore.ts';
|
|
38
|
+
import { type CalibrationConfig, configureCalibration } from './calibrationStore.ts';
|
|
38
39
|
import { isAbsolute, resolve as resolvePath } from 'node:path';
|
|
39
40
|
import { pathToFileURL } from 'node:url';
|
|
40
41
|
import { createRequire } from 'node:module';
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
getBackend,
|
|
46
47
|
removeIfCurrent,
|
|
47
48
|
replaceIfCurrent,
|
|
49
|
+
setBackendSource,
|
|
48
50
|
type CapturedInstall,
|
|
49
51
|
} from './backendRegistry.ts';
|
|
50
52
|
import { getSharedRootConfigWatcher, RootConfigWatcher } from '../../config/RootConfigWatcher.ts';
|
|
@@ -86,6 +88,7 @@ interface ModelsConfig {
|
|
|
86
88
|
embedding?: Record<string, ModelEntry>;
|
|
87
89
|
generative?: Record<string, ModelEntry>;
|
|
88
90
|
decision?: Record<string, ModelEntry>;
|
|
91
|
+
calibration?: CalibrationConfig | null;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
interface RootConfig {
|
|
@@ -288,6 +291,10 @@ function publishEntry(
|
|
|
288
291
|
): void {
|
|
289
292
|
const { kind, logicalName, entry, entryJson } = desiredEntry;
|
|
290
293
|
const key = slotKey(kind, logicalName);
|
|
294
|
+
const sourceConfig = desiredEntry.configJson ? JSON.parse(desiredEntry.configJson) : entry;
|
|
295
|
+
setBackendSource(backend, sourceFingerprint(kind, logicalName, sourceConfig));
|
|
296
|
+
for (const extra of extras)
|
|
297
|
+
setBackendSource(extra.backend, sourceFingerprint(extra.kind, extra.logicalName, sourceConfig));
|
|
291
298
|
// Boot overwrites occupants (the documented contract); a reload replaces only what this
|
|
292
299
|
// projection installed — or a helper the projection installed under this name, which a config
|
|
293
300
|
// entry outranks — so genuine application overrides survive.
|
|
@@ -503,6 +510,7 @@ async function applyModels(block: ModelsConfig | null | undefined, isBoot: boole
|
|
|
503
510
|
}
|
|
504
511
|
}
|
|
505
512
|
setModelsConfigHash(installedConfiguration());
|
|
513
|
+
configureCalibration(block?.calibration);
|
|
506
514
|
}
|
|
507
515
|
|
|
508
516
|
function installedConfiguration(): Record<string, unknown> | undefined {
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
3
|
+
import { canonicalJson } from './decision.ts';
|
|
4
|
+
import type { DecisionOutcome } from './types.ts';
|
|
5
|
+
|
|
6
|
+
export const SELECTIVE_THRESHOLDS = [0.5, 0.6, 0.7, 0.8, 0.9, 0.95] as const;
|
|
7
|
+
const ECE_BINS = 10;
|
|
8
|
+
const SCORED_EPSILON = 1e-9;
|
|
9
|
+
const DEFAULT_VOTE_SAMPLES = 5;
|
|
10
|
+
const WILSON_Z = 1.96;
|
|
11
|
+
const GOLDEN_ITERATIONS = 60;
|
|
12
|
+
export const MIN_TEMPERATURE = 0.05;
|
|
13
|
+
export const MAX_TEMPERATURE = 20;
|
|
14
|
+
/** The `truth` of an example whose recorded truth is `noMatch`: counted by operational risk only. */
|
|
15
|
+
export const NO_MATCH_TRUTH = -1;
|
|
16
|
+
|
|
17
|
+
export interface CalibrationParams {
|
|
18
|
+
t: number;
|
|
19
|
+
epsilon: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Example {
|
|
23
|
+
probabilities: number[];
|
|
24
|
+
truth: number;
|
|
25
|
+
/** Index of the value the decision returned; among tied scores it need not be the first. */
|
|
26
|
+
chosen?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SelectivePoint {
|
|
30
|
+
threshold: number;
|
|
31
|
+
count: number;
|
|
32
|
+
coverage: number;
|
|
33
|
+
risk: number | null;
|
|
34
|
+
riskUpper: number | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReliabilityBin {
|
|
38
|
+
count: number;
|
|
39
|
+
confidence: number | null;
|
|
40
|
+
accuracy: number | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Reliability {
|
|
44
|
+
ece: number;
|
|
45
|
+
nll: number;
|
|
46
|
+
bins: ReliabilityBin[];
|
|
47
|
+
/** Among examples whose truth is an allowed value: the population a fit is trained on. */
|
|
48
|
+
conditional: SelectivePoint[];
|
|
49
|
+
/** Among all examples, where a `noMatch` truth above the threshold counts as an error. */
|
|
50
|
+
operational: SelectivePoint[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What produced a decision's scores and whose outcomes it answers to; any change is a new population. */
|
|
54
|
+
export interface Population {
|
|
55
|
+
tenant?: string;
|
|
56
|
+
app?: string;
|
|
57
|
+
model: string;
|
|
58
|
+
entry: string;
|
|
59
|
+
signature: string;
|
|
60
|
+
instructionsHash?: string;
|
|
61
|
+
schemaHash: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FitPolicy {
|
|
65
|
+
minReport: number;
|
|
66
|
+
minTrain: number;
|
|
67
|
+
minHeldOut: number;
|
|
68
|
+
heldOutShare: number;
|
|
69
|
+
eceMargin: number;
|
|
70
|
+
maxAgeMs: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function digest(value: unknown): string {
|
|
74
|
+
return createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function populationKey(population: Population): string {
|
|
78
|
+
return digest({
|
|
79
|
+
tenant: population.tenant ?? null,
|
|
80
|
+
app: population.app ?? null,
|
|
81
|
+
model: population.model,
|
|
82
|
+
entry: population.entry,
|
|
83
|
+
signature: population.signature,
|
|
84
|
+
instructionsHash: population.instructionsHash ?? null,
|
|
85
|
+
schemaHash: population.schemaHash,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Orders a population's decisions newest first under one indexed prefix, so a run reads only the newest it needs. */
|
|
90
|
+
export function populationRank(population: string, expiresAt: number, id: string): string {
|
|
91
|
+
return `${population}|${String(Math.max(0, Math.floor(expiresAt))).padStart(16, '0')}|${id}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function calibrationKey(population: string, field: string | undefined): string {
|
|
95
|
+
return digest({ population, field: field ?? null });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Identical inputs, policy and result give an identical id, so a repeated run writes nothing new. */
|
|
99
|
+
export function fitId(key: string, inputDigest: string, policy: FitPolicy, result: object): string {
|
|
100
|
+
return digest({ key, inputDigest, policy, result });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function policyDigest(policy: FitPolicy): string {
|
|
104
|
+
return digest(policy);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function inputDigester(): { add(part: unknown): void; digest(): string } {
|
|
108
|
+
const hash = createHash('sha256');
|
|
109
|
+
return {
|
|
110
|
+
add(part: unknown) {
|
|
111
|
+
hash.update(canonicalJson(part));
|
|
112
|
+
hash.update('\n');
|
|
113
|
+
},
|
|
114
|
+
digest: () => hash.digest('hex'),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Smoothing mass for a distribution: half a vote per value for a voted distribution, so a value no
|
|
120
|
+
* sample chose keeps a finite log; a numerical guard otherwise.
|
|
121
|
+
*/
|
|
122
|
+
export function smoothingEpsilon(signature: string | undefined): number {
|
|
123
|
+
if (!signature || !/(^|;)mode=vote(;|$)/.test(signature)) return SCORED_EPSILON;
|
|
124
|
+
const samples = Number(/(?:^|;)samples=(\d+)(?:;|$)/.exec(signature)?.[1]);
|
|
125
|
+
return 0.5 / (Number.isInteger(samples) && samples > 0 ? samples : DEFAULT_VOTE_SAMPLES);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A replicated row is not trusted to be well formed. */
|
|
129
|
+
export function isValidParams(params: unknown): params is CalibrationParams {
|
|
130
|
+
if (!params || typeof params !== 'object') return false;
|
|
131
|
+
const { t, epsilon } = params as { t?: unknown; epsilon?: unknown };
|
|
132
|
+
return (
|
|
133
|
+
typeof t === 'number' &&
|
|
134
|
+
Number.isFinite(t) &&
|
|
135
|
+
t >= MIN_TEMPERATURE &&
|
|
136
|
+
t <= MAX_TEMPERATURE &&
|
|
137
|
+
typeof epsilon === 'number' &&
|
|
138
|
+
Number.isFinite(epsilon) &&
|
|
139
|
+
epsilon > 0 &&
|
|
140
|
+
epsilon < 1
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Strictly increasing in each value's raw score, so ties stay tied and the winner never moves. */
|
|
145
|
+
export function applyCalibration(probabilities: number[], params: CalibrationParams): number[] {
|
|
146
|
+
const k = probabilities.length;
|
|
147
|
+
const inverse = 1 / params.t;
|
|
148
|
+
const logs = new Array<number>(k);
|
|
149
|
+
let max = -Infinity;
|
|
150
|
+
for (let i = 0; i < k; i++) {
|
|
151
|
+
logs[i] = Math.log((Math.max(0, probabilities[i]) + params.epsilon) / (1 + k * params.epsilon)) * inverse;
|
|
152
|
+
if (logs[i] > max) max = logs[i];
|
|
153
|
+
}
|
|
154
|
+
let total = 0;
|
|
155
|
+
for (let i = 0; i < k; i++) {
|
|
156
|
+
logs[i] = Math.exp(logs[i] - max);
|
|
157
|
+
total += logs[i];
|
|
158
|
+
}
|
|
159
|
+
for (let i = 0; i < k; i++) logs[i] /= total;
|
|
160
|
+
return logs;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function meanNll(examples: Example[], params: CalibrationParams): number {
|
|
164
|
+
let total = 0;
|
|
165
|
+
for (const example of examples) {
|
|
166
|
+
total -= Math.log(Math.max(applyCalibration(example.probabilities, params)[example.truth], 1e-15));
|
|
167
|
+
}
|
|
168
|
+
return total / examples.length;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The temperature minimizing negative log-likelihood on `train` (examples with an allowed-value truth),
|
|
173
|
+
* by golden-section search over `log t`. Yields between iterations; undefined when `deadline` passes first.
|
|
174
|
+
*/
|
|
175
|
+
export async function fitCalibration(
|
|
176
|
+
train: Example[],
|
|
177
|
+
epsilon: number,
|
|
178
|
+
deadline: number,
|
|
179
|
+
now: () => number = Date.now
|
|
180
|
+
): Promise<CalibrationParams | undefined> {
|
|
181
|
+
const f = (x: number) => meanNll(train, { t: Math.exp(x), epsilon });
|
|
182
|
+
const ratio = (Math.sqrt(5) - 1) / 2;
|
|
183
|
+
let a = Math.log(MIN_TEMPERATURE);
|
|
184
|
+
let b = Math.log(MAX_TEMPERATURE);
|
|
185
|
+
let c = b - ratio * (b - a);
|
|
186
|
+
let d = a + ratio * (b - a);
|
|
187
|
+
let fc = f(c);
|
|
188
|
+
let fd = f(d);
|
|
189
|
+
for (let i = 0; i < GOLDEN_ITERATIONS; i++) {
|
|
190
|
+
await yieldToEventLoop();
|
|
191
|
+
if (now() >= deadline) return undefined;
|
|
192
|
+
if (fc < fd) {
|
|
193
|
+
b = d;
|
|
194
|
+
d = c;
|
|
195
|
+
fd = fc;
|
|
196
|
+
c = b - ratio * (b - a);
|
|
197
|
+
fc = f(c);
|
|
198
|
+
} else {
|
|
199
|
+
a = c;
|
|
200
|
+
c = d;
|
|
201
|
+
fc = fd;
|
|
202
|
+
d = a + ratio * (b - a);
|
|
203
|
+
fd = f(d);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { t: Math.exp((a + b) / 2), epsilon };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function argmax(p: number[]): number {
|
|
210
|
+
let top = 0;
|
|
211
|
+
for (let i = 1; i < p.length; i++) if (p[i] > p[top]) top = i;
|
|
212
|
+
return top;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function wilsonUpper(errors: number, count: number): number | null {
|
|
216
|
+
if (count === 0) return null;
|
|
217
|
+
const p = errors / count;
|
|
218
|
+
const z2 = WILSON_Z * WILSON_Z;
|
|
219
|
+
const centre = p + z2 / (2 * count);
|
|
220
|
+
const margin = WILSON_Z * Math.sqrt((p * (1 - p)) / count + z2 / (4 * count * count));
|
|
221
|
+
return Math.min(1, (centre + margin) / (1 + z2 / count));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function selective(tops: Array<{ confidence: number; correct: boolean }>, total: number): SelectivePoint[] {
|
|
225
|
+
return SELECTIVE_THRESHOLDS.map((threshold) => {
|
|
226
|
+
let count = 0;
|
|
227
|
+
let errors = 0;
|
|
228
|
+
for (const top of tops) {
|
|
229
|
+
if (top.confidence < threshold) continue;
|
|
230
|
+
count++;
|
|
231
|
+
if (!top.correct) errors++;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
threshold,
|
|
235
|
+
count,
|
|
236
|
+
coverage: total ? count / total : 0,
|
|
237
|
+
risk: count ? errors / count : null,
|
|
238
|
+
riskUpper: wilsonUpper(errors, count),
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function reliability(examples: Example[], transform: (p: number[]) => number[]): Reliability {
|
|
244
|
+
const sums = Array.from({ length: ECE_BINS }, () => ({ count: 0, confidence: 0, correct: 0 }));
|
|
245
|
+
const inSet: Array<{ confidence: number; correct: boolean }> = [];
|
|
246
|
+
const all: Array<{ confidence: number; correct: boolean }> = [];
|
|
247
|
+
let nll = 0;
|
|
248
|
+
for (const example of examples) {
|
|
249
|
+
const p = transform(example.probabilities);
|
|
250
|
+
const top = example.chosen ?? argmax(p);
|
|
251
|
+
const confidence = p[top];
|
|
252
|
+
if (example.truth === NO_MATCH_TRUTH) {
|
|
253
|
+
all.push({ confidence, correct: false });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const correct = top === example.truth;
|
|
257
|
+
const bin = sums[Math.min(ECE_BINS - 1, Math.floor(confidence * ECE_BINS))];
|
|
258
|
+
bin.count++;
|
|
259
|
+
bin.confidence += confidence;
|
|
260
|
+
bin.correct += correct ? 1 : 0;
|
|
261
|
+
inSet.push({ confidence, correct });
|
|
262
|
+
all.push({ confidence, correct });
|
|
263
|
+
nll -= Math.log(Math.max(p[example.truth], 1e-15));
|
|
264
|
+
}
|
|
265
|
+
const n = inSet.length;
|
|
266
|
+
let ece = 0;
|
|
267
|
+
for (const bin of sums) if (bin.count && n) ece += Math.abs(bin.confidence - bin.correct) / n;
|
|
268
|
+
return {
|
|
269
|
+
ece,
|
|
270
|
+
nll: n ? nll / n : 0,
|
|
271
|
+
bins: sums.map(({ count, confidence, correct }) => ({
|
|
272
|
+
count,
|
|
273
|
+
confidence: count ? confidence / count : null,
|
|
274
|
+
accuracy: count ? correct / count : null,
|
|
275
|
+
})),
|
|
276
|
+
conditional: selective(inSet, n),
|
|
277
|
+
operational: selective(all, all.length),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Oldest first; the newest `heldOutShare` is held out, so a refit is judged on decisions newer than any it trained on. */
|
|
282
|
+
export function splitByTime<E>(ordered: E[], heldOutShare: number): { train: E[]; heldOut: E[] } {
|
|
283
|
+
const heldOutCount = Math.floor(ordered.length * heldOutShare);
|
|
284
|
+
return {
|
|
285
|
+
train: ordered.slice(0, ordered.length - heldOutCount),
|
|
286
|
+
heldOut: ordered.slice(ordered.length - heldOutCount),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function toVector(
|
|
291
|
+
distribution: DecisionOutcome[] | undefined,
|
|
292
|
+
values: readonly unknown[]
|
|
293
|
+
): number[] | undefined {
|
|
294
|
+
if (!Array.isArray(distribution)) return undefined;
|
|
295
|
+
const byValue = new Map(distribution.map((entry) => [canonicalJson(entry.value), entry.probability]));
|
|
296
|
+
const vector = values.map((value) => byValue.get(canonicalJson(value)));
|
|
297
|
+
return vector.every((p) => typeof p === 'number' && Number.isFinite(p)) ? (vector as number[]) : undefined;
|
|
298
|
+
}
|