@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
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CALIBRATION_ATTRIBUTES = exports.CALIBRATIONS_TABLE = void 0;
|
|
7
|
+
exports.getCalibrationsTable = getCalibrationsTable;
|
|
8
|
+
exports.resetCalibrationsTable = resetCalibrationsTable;
|
|
9
|
+
exports.declareCalibrationsTableAtBoot = declareCalibrationsTableAtBoot;
|
|
10
|
+
exports.isCalibrationEnabled = isCalibrationEnabled;
|
|
11
|
+
exports.configureCalibration = configureCalibration;
|
|
12
|
+
exports.calibrationJob = calibrationJob;
|
|
13
|
+
exports.resetCalibrationCache = resetCalibrationCache;
|
|
14
|
+
exports.outstandingCalibrationReads = outstandingCalibrationReads;
|
|
15
|
+
exports.setCalibrationReadForTests = setCalibrationReadForTests;
|
|
16
|
+
exports.applyFits = applyFits;
|
|
17
|
+
exports.runCalibration = runCalibration;
|
|
18
|
+
exports.processingOrder = processingOrder;
|
|
19
|
+
exports.listCalibrations = listCalibrations;
|
|
20
|
+
const databases_ts_1 = require("../databases.js");
|
|
21
|
+
const transaction_ts_1 = require("../transaction.js");
|
|
22
|
+
const harper_logger_ts_1 = __importDefault(require("../../utility/logging/harper_logger.js"));
|
|
23
|
+
const manageThreads_js_1 = require("../../server/threads/manageThreads.js");
|
|
24
|
+
const write_ts_1 = require("../analytics/write.js");
|
|
25
|
+
const engine_ts_1 = require("../scheduler/engine.js");
|
|
26
|
+
const decision_ts_1 = require("./decision.js");
|
|
27
|
+
const decisionStore_ts_1 = require("./decisionStore.js");
|
|
28
|
+
const calibration_ts_1 = require("./calibration.js");
|
|
29
|
+
const log = harper_logger_ts_1.default.forComponent('models').conditional;
|
|
30
|
+
exports.CALIBRATIONS_TABLE = 'hdb_model_calibrations';
|
|
31
|
+
const JOB_NAME = 'models-calibration';
|
|
32
|
+
const CACHE_LIMIT = 1024;
|
|
33
|
+
const CACHE_FRESH_MS = 60_000;
|
|
34
|
+
const LOAD_TIMEOUT_MS = 2_000;
|
|
35
|
+
const FAULT_LOG_INTERVAL_MS = 60_000;
|
|
36
|
+
const SCAN_PAGE = 500;
|
|
37
|
+
const YIELD_EVERY = 500;
|
|
38
|
+
const EXAMPLE_OVERHEAD_BYTES = 200;
|
|
39
|
+
const POPULATION_OVERHEAD_BYTES = 1_000;
|
|
40
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
41
|
+
const RANK_WIDTH = 16;
|
|
42
|
+
const CURSOR_ID = 'discovery/cursor';
|
|
43
|
+
const LIST_BATCH = 16;
|
|
44
|
+
const BUDGET_KEYS = ['maxDecisions', 'maxPopulations', 'maxExamplesPerKey', 'maxBytes', 'maxRunMs'];
|
|
45
|
+
function budgetsOnly(budgets, configured) {
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const key of BUDGET_KEYS) {
|
|
48
|
+
const value = budgets?.[key];
|
|
49
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0)
|
|
50
|
+
out[key] = Math.min(value, configured[key]);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
const DEFAULTS = {
|
|
55
|
+
interval: DAY_MS,
|
|
56
|
+
maxDecisions: 100_000,
|
|
57
|
+
maxPopulations: 1_000,
|
|
58
|
+
maxExamplesPerKey: 5_000,
|
|
59
|
+
maxBytes: 64 * 1024 * 1024,
|
|
60
|
+
maxLoads: 8,
|
|
61
|
+
maxRunMs: 60_000,
|
|
62
|
+
minReport: 20,
|
|
63
|
+
minTrain: 100,
|
|
64
|
+
minHeldOut: 100,
|
|
65
|
+
heldOutShare: 0.3,
|
|
66
|
+
eceMargin: 0.01,
|
|
67
|
+
maxAgeMs: 30 * DAY_MS,
|
|
68
|
+
};
|
|
69
|
+
exports.CALIBRATION_ATTRIBUTES = [
|
|
70
|
+
{ name: 'id', isPrimaryKey: true },
|
|
71
|
+
{ name: 'kind', type: 'string', indexed: true },
|
|
72
|
+
{ name: 'key', type: 'string' },
|
|
73
|
+
{ name: 'rank', type: 'string', indexed: true },
|
|
74
|
+
{ name: 'population', type: 'string' },
|
|
75
|
+
{ name: 'field', type: 'string' },
|
|
76
|
+
{ name: 'tenant', type: 'string' },
|
|
77
|
+
{ name: 'app', type: 'string' },
|
|
78
|
+
{ name: 'model', type: 'string' },
|
|
79
|
+
{ name: 'entry', type: 'string' },
|
|
80
|
+
{ name: 'signature', type: 'string' },
|
|
81
|
+
{ name: 'instructionsHash', type: 'string' },
|
|
82
|
+
{ name: 'schemaHash', type: 'string' },
|
|
83
|
+
{ name: 'schema' },
|
|
84
|
+
{ name: 't', type: 'number' },
|
|
85
|
+
{ name: 'epsilon', type: 'number' },
|
|
86
|
+
{ name: 'cutoff', type: 'number' },
|
|
87
|
+
{ name: 'evidenceAt', type: 'number' },
|
|
88
|
+
{ name: 'fittedAt', type: 'number' },
|
|
89
|
+
{ name: 'lastFittedAt', type: 'number', indexed: true },
|
|
90
|
+
{ name: 'owner', type: 'string', indexed: true },
|
|
91
|
+
{ name: 'boundary' },
|
|
92
|
+
{ name: 'before', type: 'number' },
|
|
93
|
+
{ name: 'applyUntil', type: 'number' },
|
|
94
|
+
{ name: 'expiresAt', expiresAt: true, indexed: true },
|
|
95
|
+
{ name: 'eligible', type: 'boolean' },
|
|
96
|
+
{ name: 'reason', type: 'string' },
|
|
97
|
+
{ name: 'policy' },
|
|
98
|
+
{ name: 'policyDigest', type: 'string' },
|
|
99
|
+
{ name: 'inputDigest', type: 'string' },
|
|
100
|
+
{ name: 'decisions', type: 'number' },
|
|
101
|
+
{ name: 'labelled', type: 'number' },
|
|
102
|
+
{ name: 'report' },
|
|
103
|
+
];
|
|
104
|
+
let calibrations;
|
|
105
|
+
/** Declared like the decision tables: a read-only node takes what the catalog holds and declares nothing. */
|
|
106
|
+
function getCalibrationsTable(readOnly = (0, databases_ts_1.isReadOnlyMode)()) {
|
|
107
|
+
if (calibrations)
|
|
108
|
+
return calibrations;
|
|
109
|
+
const handle = readOnly
|
|
110
|
+
? (0, databases_ts_1.getDatabases)().system?.[exports.CALIBRATIONS_TABLE]
|
|
111
|
+
: (0, databases_ts_1.table)({
|
|
112
|
+
table: exports.CALIBRATIONS_TABLE,
|
|
113
|
+
database: 'system',
|
|
114
|
+
// Declared positively, like the decision tables: every node must apply the same versions.
|
|
115
|
+
replicate: true,
|
|
116
|
+
audit: true,
|
|
117
|
+
attributes: exports.CALIBRATION_ATTRIBUTES,
|
|
118
|
+
});
|
|
119
|
+
if (handle) {
|
|
120
|
+
handle.loadAsInstance = false;
|
|
121
|
+
calibrations = handle;
|
|
122
|
+
}
|
|
123
|
+
return handle;
|
|
124
|
+
}
|
|
125
|
+
function resetCalibrationsTable() {
|
|
126
|
+
calibrations = undefined;
|
|
127
|
+
resetCalibrationCache();
|
|
128
|
+
}
|
|
129
|
+
function declareCalibrationsTableAtBoot() {
|
|
130
|
+
if ((0, databases_ts_1.isReadOnlyMode)())
|
|
131
|
+
return;
|
|
132
|
+
try {
|
|
133
|
+
getCalibrationsTable();
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
log.warn?.(`models: calibration table could not be declared at boot: ${(0, engine_ts_1.safeErrorMessage)(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function freshContext() {
|
|
140
|
+
const user = transaction_ts_1.contextStorage.getStore()?.user;
|
|
141
|
+
return (user ? { user } : {});
|
|
142
|
+
}
|
|
143
|
+
let settings;
|
|
144
|
+
let activePolicyDigest;
|
|
145
|
+
let registeredBlock;
|
|
146
|
+
function isCalibrationEnabled() {
|
|
147
|
+
return settings !== undefined;
|
|
148
|
+
}
|
|
149
|
+
function policyOf(config) {
|
|
150
|
+
return {
|
|
151
|
+
minReport: config.minReport,
|
|
152
|
+
minTrain: config.minTrain,
|
|
153
|
+
minHeldOut: config.minHeldOut,
|
|
154
|
+
heldOutShare: config.heldOutShare,
|
|
155
|
+
eceMargin: config.eceMargin,
|
|
156
|
+
maxAgeMs: config.maxAgeMs,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Apply the `models.calibration` block. Present: fits apply on every worker, and the application primary
|
|
161
|
+
* worker registers the cluster-once job. Absent: nothing applies and the job is removed.
|
|
162
|
+
*/
|
|
163
|
+
function configureCalibration(block, primary = (0, manageThreads_js_1.isApplicationPrimaryWorker)(undefined)) {
|
|
164
|
+
if (!block) {
|
|
165
|
+
resetCalibrationCache();
|
|
166
|
+
settings = undefined;
|
|
167
|
+
activePolicyDigest = undefined;
|
|
168
|
+
registeredBlock = undefined;
|
|
169
|
+
if (primary)
|
|
170
|
+
(0, engine_ts_1.unregisterInternalJobs)(JOB_NAME);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const next = { ...DEFAULTS, ...definedOnly(block) };
|
|
174
|
+
const digest = (0, decision_ts_1.canonicalJson)(next);
|
|
175
|
+
if (settings && (0, decision_ts_1.canonicalJson)(settings) === digest && (registeredBlock === digest || !primary))
|
|
176
|
+
return;
|
|
177
|
+
resetCalibrationCache();
|
|
178
|
+
settings = next;
|
|
179
|
+
activePolicyDigest = (0, calibration_ts_1.policyDigest)(policyOf(settings));
|
|
180
|
+
if (!primary || (0, databases_ts_1.isReadOnlyMode)())
|
|
181
|
+
return;
|
|
182
|
+
registeredBlock = digest;
|
|
183
|
+
(0, engine_ts_1.registerInternalJobs)(JOB_NAME, [
|
|
184
|
+
{
|
|
185
|
+
name: 'calibrate',
|
|
186
|
+
componentName: (0, engine_ts_1.internalJobOwner)(JOB_NAME),
|
|
187
|
+
intervalMs: settings.interval,
|
|
188
|
+
handler: calibrationJob,
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
(0, engine_ts_1.startSchedulerEngine)();
|
|
192
|
+
}
|
|
193
|
+
async function calibrationJob() {
|
|
194
|
+
const run = await runCalibration();
|
|
195
|
+
if (run.status === 'failed')
|
|
196
|
+
throw new Error(`calibration run failed: ${run.error}`);
|
|
197
|
+
}
|
|
198
|
+
function definedOnly(block) {
|
|
199
|
+
return Object.fromEntries(Object.entries(block).filter(([, value]) => value !== undefined));
|
|
200
|
+
}
|
|
201
|
+
const cache = new Map();
|
|
202
|
+
const loading = new Map();
|
|
203
|
+
let generation = 0;
|
|
204
|
+
let outstandingReads = 0;
|
|
205
|
+
let lastFaultLog = 0;
|
|
206
|
+
function resetCalibrationCache() {
|
|
207
|
+
generation++;
|
|
208
|
+
cache.clear();
|
|
209
|
+
loading.clear();
|
|
210
|
+
}
|
|
211
|
+
function invalidate(keys) {
|
|
212
|
+
if (keys.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
generation++;
|
|
215
|
+
for (const key of keys) {
|
|
216
|
+
cache.delete(key);
|
|
217
|
+
loading.delete(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function outstandingCalibrationReads() {
|
|
221
|
+
return outstandingReads;
|
|
222
|
+
}
|
|
223
|
+
function remember(key, fit) {
|
|
224
|
+
cache.delete(key);
|
|
225
|
+
cache.set(key, { fit, loadedAt: Date.now() });
|
|
226
|
+
while (cache.size > CACHE_LIMIT)
|
|
227
|
+
cache.delete(cache.keys().next().value);
|
|
228
|
+
}
|
|
229
|
+
function logFault(what, err) {
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
if (now - lastFaultLog < FAULT_LOG_INTERVAL_MS)
|
|
232
|
+
return;
|
|
233
|
+
lastFaultLog = now;
|
|
234
|
+
log.warn?.(`models: ${what}: ${(0, engine_ts_1.safeErrorMessage)(err)}`);
|
|
235
|
+
}
|
|
236
|
+
function schemaBytes(schema) {
|
|
237
|
+
try {
|
|
238
|
+
return ((0, decision_ts_1.canonicalJson)(schema ?? null) ?? '').length;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function ownerOf(tenant) {
|
|
245
|
+
return tenant == null ? 'none' : `t:${tenant}`;
|
|
246
|
+
}
|
|
247
|
+
function pad(n) {
|
|
248
|
+
return String(Math.max(0, Math.floor(n))).padStart(RANK_WIDTH, '0');
|
|
249
|
+
}
|
|
250
|
+
function rankOf(key, evidenceAt, fittedAt, id) {
|
|
251
|
+
return `${key}|${pad(evidenceAt)}|${pad(fittedAt)}|${id}`;
|
|
252
|
+
}
|
|
253
|
+
/** The newest version of a key: the greatest rank within the key's prefix, one descending index read. */
|
|
254
|
+
async function readNewest(key) {
|
|
255
|
+
const rows = getCalibrationsTable();
|
|
256
|
+
if (!rows)
|
|
257
|
+
return null;
|
|
258
|
+
return (0, transaction_ts_1.transaction)(freshContext(), async () => {
|
|
259
|
+
for await (const row of rows.search({
|
|
260
|
+
conditions: [{ attribute: 'rank', comparator: 'starts_with', value: `${key}|`, descending: true }],
|
|
261
|
+
limit: 1,
|
|
262
|
+
})) {
|
|
263
|
+
return row;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function scheduleLoad(key, read = readNewest) {
|
|
269
|
+
if (loading.has(key) || !settings || outstandingReads >= settings.maxLoads)
|
|
270
|
+
return;
|
|
271
|
+
const token = {};
|
|
272
|
+
const startedAt = generation;
|
|
273
|
+
loading.set(key, token);
|
|
274
|
+
outstandingReads++;
|
|
275
|
+
const owns = () => loading.get(key) === token && generation === startedAt;
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
if (!owns())
|
|
278
|
+
return;
|
|
279
|
+
remember(key, null);
|
|
280
|
+
loading.delete(key);
|
|
281
|
+
}, LOAD_TIMEOUT_MS);
|
|
282
|
+
timer.unref?.();
|
|
283
|
+
let pending;
|
|
284
|
+
try {
|
|
285
|
+
pending = read(key);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
pending = Promise.reject(err);
|
|
289
|
+
}
|
|
290
|
+
pending
|
|
291
|
+
.then((row) => {
|
|
292
|
+
if (owns())
|
|
293
|
+
remember(key, row);
|
|
294
|
+
}, (err) => {
|
|
295
|
+
logFault('calibration lookup failed', err);
|
|
296
|
+
if (owns())
|
|
297
|
+
remember(key, null);
|
|
298
|
+
})
|
|
299
|
+
.catch(() => { })
|
|
300
|
+
.finally(() => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
outstandingReads--;
|
|
303
|
+
if (loading.get(key) === token)
|
|
304
|
+
loading.delete(key);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** @internal — testing only: replace the storage read the cache loads through. */
|
|
308
|
+
let readForLoads;
|
|
309
|
+
function setCalibrationReadForTests(read) {
|
|
310
|
+
readForLoads = read;
|
|
311
|
+
}
|
|
312
|
+
function isUsable(row, now, key) {
|
|
313
|
+
return (!!row &&
|
|
314
|
+
row.kind === 'fit' &&
|
|
315
|
+
row.key === key &&
|
|
316
|
+
typeof row.rank === 'string' &&
|
|
317
|
+
row.rank.startsWith(`${key}|`) &&
|
|
318
|
+
typeof row.population === 'string' &&
|
|
319
|
+
(0, calibration_ts_1.calibrationKey)(row.population, row.field) === key &&
|
|
320
|
+
row.eligible === true &&
|
|
321
|
+
row.applyUntil > now &&
|
|
322
|
+
row.policyDigest === activePolicyDigest &&
|
|
323
|
+
(0, calibration_ts_1.isValidParams)(row));
|
|
324
|
+
}
|
|
325
|
+
function cachedFit(key, now) {
|
|
326
|
+
const entry = cache.get(key);
|
|
327
|
+
if (!entry) {
|
|
328
|
+
scheduleLoad(key, readForLoads);
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
cache.delete(key);
|
|
332
|
+
cache.set(key, entry);
|
|
333
|
+
if (now - entry.loadedAt >= CACHE_FRESH_MS)
|
|
334
|
+
scheduleLoad(key, readForLoads);
|
|
335
|
+
return isUsable(entry.fit, now, key) ? entry.fit : undefined;
|
|
336
|
+
}
|
|
337
|
+
function hasNoMatchLeaf(schema) {
|
|
338
|
+
if ((0, decision_ts_1.isObjectSchema)(schema))
|
|
339
|
+
return Object.values(schema.properties).some((leaf) => leaf.noMatch === true);
|
|
340
|
+
return schema.noMatch === true;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The decision with every distribution replaced by its population's fit, or undefined when any field lacks
|
|
344
|
+
* a cached usable fit or the order would change: all fields or none. Never waits on storage. A schema with
|
|
345
|
+
* a no-match leaf is never fit-calibrated, because its no-match score would stay raw under `calibrated: true`.
|
|
346
|
+
*/
|
|
347
|
+
function applyFits(schema, decision, population, now = Date.now()) {
|
|
348
|
+
if (hasNoMatchLeaf(schema))
|
|
349
|
+
return undefined;
|
|
350
|
+
if ((0, decision_ts_1.isObjectSchema)(schema)) {
|
|
351
|
+
const fields = decision.fields;
|
|
352
|
+
if (!fields)
|
|
353
|
+
return undefined;
|
|
354
|
+
const names = Object.keys(schema.properties);
|
|
355
|
+
const fits = names.map((name) => cachedFit((0, calibration_ts_1.calibrationKey)(population, name), now));
|
|
356
|
+
if (fits.some((fit) => !fit))
|
|
357
|
+
return undefined;
|
|
358
|
+
const calibrated = {};
|
|
359
|
+
const snapshot = [];
|
|
360
|
+
for (let i = 0; i < names.length; i++) {
|
|
361
|
+
const fit = fits[i];
|
|
362
|
+
const marginal = calibrateMarginal(schema.properties[names[i]], fields[names[i]], fit);
|
|
363
|
+
if (!marginal)
|
|
364
|
+
return undefined;
|
|
365
|
+
calibrated[names[i]] = marginal;
|
|
366
|
+
snapshot.push({ field: names[i], fitId: fit.id, t: fit.t, epsilon: fit.epsilon });
|
|
367
|
+
}
|
|
368
|
+
return { decision: { ...decision, fields: calibrated, calibrated: true }, rawFields: fields, snapshot };
|
|
369
|
+
}
|
|
370
|
+
const fit = cachedFit((0, calibration_ts_1.calibrationKey)(population, undefined), now);
|
|
371
|
+
if (!fit || !decision.distribution)
|
|
372
|
+
return undefined;
|
|
373
|
+
const marginal = calibrateMarginal(schema, { value: decision.value, probability: decision.probability, distribution: decision.distribution }, fit);
|
|
374
|
+
if (!marginal)
|
|
375
|
+
return undefined;
|
|
376
|
+
return {
|
|
377
|
+
decision: { ...decision, probability: marginal.probability, distribution: marginal.distribution, calibrated: true },
|
|
378
|
+
rawDistribution: decision.distribution,
|
|
379
|
+
snapshot: [{ fitId: fit.id, t: fit.t, epsilon: fit.epsilon }],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function calibrateMarginal(leaf, marginal, fit) {
|
|
383
|
+
if (!marginal)
|
|
384
|
+
return undefined;
|
|
385
|
+
const values = (0, decision_ts_1.allowedValues)(leaf);
|
|
386
|
+
const vector = (0, calibration_ts_1.toVector)(marginal.distribution, values);
|
|
387
|
+
if (!vector)
|
|
388
|
+
return undefined;
|
|
389
|
+
const out = (0, calibration_ts_1.applyCalibration)(vector, fit);
|
|
390
|
+
if (!out.every(Number.isFinite))
|
|
391
|
+
return undefined;
|
|
392
|
+
const indexByValue = new Map(values.map((value, i) => [(0, decision_ts_1.canonicalJson)(value), i]));
|
|
393
|
+
const distribution = marginal.distribution.map((entry) => ({
|
|
394
|
+
value: entry.value,
|
|
395
|
+
probability: out[indexByValue.get((0, decision_ts_1.canonicalJson)(entry.value))],
|
|
396
|
+
}));
|
|
397
|
+
for (let i = 1; i < distribution.length; i++)
|
|
398
|
+
if (distribution[i].probability > distribution[i - 1].probability)
|
|
399
|
+
return undefined;
|
|
400
|
+
if ((0, decision_ts_1.canonicalJson)(distribution[0].value) !== (0, decision_ts_1.canonicalJson)(marginal.value))
|
|
401
|
+
return undefined;
|
|
402
|
+
return { value: marginal.value, probability: distribution[0].probability, distribution };
|
|
403
|
+
}
|
|
404
|
+
let queue = Promise.resolve();
|
|
405
|
+
/** One run at a time per process. Resolves with the run's result, an unexpected fault included as a failed run. */
|
|
406
|
+
function runCalibration(budgets = {}, deps = {}) {
|
|
407
|
+
const next = queue.then(() => runOnce(budgets, deps)).catch((err) => failedRun(err));
|
|
408
|
+
queue = next;
|
|
409
|
+
return next;
|
|
410
|
+
}
|
|
411
|
+
function failedRun(err) {
|
|
412
|
+
const error = `unexpected fault: ${(0, engine_ts_1.safeErrorMessage)(err)}`;
|
|
413
|
+
log.error?.(`models: calibration run failed ${error}`);
|
|
414
|
+
return {
|
|
415
|
+
status: 'failed',
|
|
416
|
+
error,
|
|
417
|
+
scanned: 0,
|
|
418
|
+
read: 0,
|
|
419
|
+
discovered: 0,
|
|
420
|
+
processed: 0,
|
|
421
|
+
pending: 0,
|
|
422
|
+
written: 0,
|
|
423
|
+
eligible: 0,
|
|
424
|
+
skipped: 0,
|
|
425
|
+
failed: 0,
|
|
426
|
+
durationMs: 0,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async function runOnce(budgets, deps) {
|
|
430
|
+
const now = deps.now ?? Date.now;
|
|
431
|
+
const configured = { ...DEFAULTS, ...settings };
|
|
432
|
+
const config = { ...configured, ...budgetsOnly(budgets, configured) };
|
|
433
|
+
const policy = policyOf(config);
|
|
434
|
+
const digestOfPolicy = (0, calibration_ts_1.policyDigest)(policy);
|
|
435
|
+
const started = now();
|
|
436
|
+
const deadline = started + config.maxRunMs;
|
|
437
|
+
const result = {
|
|
438
|
+
status: 'completed',
|
|
439
|
+
scanned: 0,
|
|
440
|
+
read: 0,
|
|
441
|
+
discovered: 0,
|
|
442
|
+
processed: 0,
|
|
443
|
+
pending: 0,
|
|
444
|
+
written: 0,
|
|
445
|
+
eligible: 0,
|
|
446
|
+
skipped: 0,
|
|
447
|
+
failed: 0,
|
|
448
|
+
durationMs: 0,
|
|
449
|
+
};
|
|
450
|
+
const { decisions, outcomes } = (0, decisionStore_ts_1.getDecisionTables)();
|
|
451
|
+
const store = getCalibrationsTable();
|
|
452
|
+
if (!decisions || !outcomes || !store)
|
|
453
|
+
return finish();
|
|
454
|
+
const populations = new Map();
|
|
455
|
+
const writtenKeys = [];
|
|
456
|
+
let bytes = 0;
|
|
457
|
+
await discover();
|
|
458
|
+
try {
|
|
459
|
+
const discovered = [...populations.values()];
|
|
460
|
+
const known = await (0, transaction_ts_1.transaction)(freshContext(), async () => {
|
|
461
|
+
for (const found of discovered) {
|
|
462
|
+
const head = await store.get(found.head.id);
|
|
463
|
+
if (head?.lastFittedAt !== undefined)
|
|
464
|
+
found.lastFittedAt = head.lastFittedAt;
|
|
465
|
+
else
|
|
466
|
+
await store.put({ ...found.head, lastFittedAt: 0, expiresAt: found.expiresAt ?? now() + policy.maxAgeMs });
|
|
467
|
+
}
|
|
468
|
+
const out = [];
|
|
469
|
+
for await (const row of store.search({
|
|
470
|
+
conditions: [{ attribute: 'kind', value: 'population' }],
|
|
471
|
+
sort: { attribute: 'lastFittedAt' },
|
|
472
|
+
limit: config.maxPopulations,
|
|
473
|
+
}))
|
|
474
|
+
out.push(row);
|
|
475
|
+
return out;
|
|
476
|
+
});
|
|
477
|
+
for (const head of known) {
|
|
478
|
+
if (populations.has(head.population))
|
|
479
|
+
continue;
|
|
480
|
+
const size = POPULATION_OVERHEAD_BYTES + schemaBytes(head.schema);
|
|
481
|
+
if (bytes + size > config.maxBytes) {
|
|
482
|
+
result.stoppedBy ??= 'maxBytes';
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
bytes += size;
|
|
486
|
+
const { lastFittedAt, expiresAt, ...rest } = head;
|
|
487
|
+
populations.set(head.population, { head: rest, lastFittedAt, expiresAt, bytes: size });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
fail('reading known populations', err);
|
|
492
|
+
}
|
|
493
|
+
result.discovered = populations.size;
|
|
494
|
+
const ordered = processingOrder([...populations.values()]);
|
|
495
|
+
const order = ordered.slice(0, config.maxPopulations);
|
|
496
|
+
if (ordered.length > order.length) {
|
|
497
|
+
result.stoppedBy ??= 'maxPopulations';
|
|
498
|
+
result.pending = ordered.length - order.length;
|
|
499
|
+
}
|
|
500
|
+
let allowance = config.maxDecisions - result.scanned;
|
|
501
|
+
for (let i = 0; i < order.length; i++) {
|
|
502
|
+
if (allowance <= 0) {
|
|
503
|
+
result.stoppedBy ??= 'maxDecisions';
|
|
504
|
+
result.pending += order.length - i;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (now() >= deadline) {
|
|
508
|
+
result.stoppedBy ??= 'maxRunMs';
|
|
509
|
+
result.pending += order.length - i;
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
const outcome = await processPopulation(order[i]);
|
|
513
|
+
if (outcome === 'deferred') {
|
|
514
|
+
await rotate(order[i]);
|
|
515
|
+
result.stoppedBy ??= 'maxDecisions';
|
|
516
|
+
result.pending += order.length - i;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
if (outcome === 'budget') {
|
|
520
|
+
await rotate(order[i]);
|
|
521
|
+
result.stoppedBy ??= 'maxBytes';
|
|
522
|
+
result.pending += order.length - i;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
if (outcome === 'deadline') {
|
|
526
|
+
await rotate(order[i]);
|
|
527
|
+
result.stoppedBy ??= 'maxRunMs';
|
|
528
|
+
result.pending += order.length - i;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
result.processed++;
|
|
532
|
+
}
|
|
533
|
+
invalidate(writtenKeys);
|
|
534
|
+
return finish();
|
|
535
|
+
async function discover() {
|
|
536
|
+
const decisionBudget = Math.max(1, Math.floor(config.maxDecisions / 2));
|
|
537
|
+
const newest = await scan(undefined, [], Math.floor(decisionBudget / 2), Math.floor(config.maxPopulations / 2));
|
|
538
|
+
if (newest.fault || newest.stopped)
|
|
539
|
+
return;
|
|
540
|
+
if (newest.end)
|
|
541
|
+
return saveCursor(undefined, []);
|
|
542
|
+
let cursor;
|
|
543
|
+
try {
|
|
544
|
+
cursor = await (0, transaction_ts_1.transaction)(freshContext(), () => store.get(CURSOR_ID));
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
logFault('calibration could not read its discovery cursor', err);
|
|
548
|
+
}
|
|
549
|
+
const saved = typeof cursor?.before === 'number' ? cursor.before : undefined;
|
|
550
|
+
const savedSeen = Array.isArray(cursor?.boundary) ? cursor.boundary : [];
|
|
551
|
+
const resume = saved !== undefined && (newest.before === undefined || saved <= newest.before);
|
|
552
|
+
const from = resume ? saved : newest.before;
|
|
553
|
+
const seen = !resume
|
|
554
|
+
? newest.boundary
|
|
555
|
+
: saved === newest.before
|
|
556
|
+
? [...new Set([...savedSeen, ...newest.boundary])]
|
|
557
|
+
: savedSeen;
|
|
558
|
+
const rest = await scan(from, seen, decisionBudget - result.scanned, config.maxPopulations - populations.size);
|
|
559
|
+
if (rest.fault)
|
|
560
|
+
return;
|
|
561
|
+
if (rest.capped)
|
|
562
|
+
result.stoppedBy ??= rest.capped;
|
|
563
|
+
else if (!rest.end && !rest.stopped)
|
|
564
|
+
result.stoppedBy ??= 'maxDecisions';
|
|
565
|
+
return rest.end ? saveCursor(undefined, []) : saveCursor(rest.before, rest.boundary);
|
|
566
|
+
}
|
|
567
|
+
async function saveCursor(before, boundary) {
|
|
568
|
+
try {
|
|
569
|
+
await (0, transaction_ts_1.transaction)(freshContext(), () => store.put({ id: CURSOR_ID, kind: 'cursor', before, boundary, expiresAt: now() + decisionStore_ts_1.DECISION_RETENTION_MS }));
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
logFault('calibration could not save its discovery cursor', err);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function scan(start, startSeen, budget, populationBudget) {
|
|
576
|
+
let before = start;
|
|
577
|
+
let taken = 0;
|
|
578
|
+
let added = 0;
|
|
579
|
+
const seenAtBoundary = new Set(start === undefined ? [] : startSeen);
|
|
580
|
+
while (true) {
|
|
581
|
+
if (taken >= budget)
|
|
582
|
+
return { before, boundary: [...seenAtBoundary], end: false, stopped: false };
|
|
583
|
+
let page;
|
|
584
|
+
const requested = Math.min(SCAN_PAGE, budget - taken) + seenAtBoundary.size;
|
|
585
|
+
let returned = 0;
|
|
586
|
+
try {
|
|
587
|
+
page = await (0, transaction_ts_1.transaction)(freshContext(), async () => {
|
|
588
|
+
const rows = [];
|
|
589
|
+
const conditions = before === undefined
|
|
590
|
+
? [{ attribute: 'expiresAt', comparator: 'greater_than', value: 0, descending: true }]
|
|
591
|
+
: [{ attribute: 'expiresAt', comparator: 'less_than_equal', value: before, descending: true }];
|
|
592
|
+
for await (const row of decisions.search({
|
|
593
|
+
conditions,
|
|
594
|
+
limit: requested,
|
|
595
|
+
})) {
|
|
596
|
+
returned++;
|
|
597
|
+
if (!seenAtBoundary.has(row.id))
|
|
598
|
+
rows.push(row);
|
|
599
|
+
}
|
|
600
|
+
return rows;
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
fail('discovering populations', err);
|
|
605
|
+
return { before, boundary: [...seenAtBoundary], end: false, stopped: true, fault: true };
|
|
606
|
+
}
|
|
607
|
+
if (page.length === 0)
|
|
608
|
+
return { before, boundary: [...seenAtBoundary], end: true, stopped: false };
|
|
609
|
+
for (const row of page) {
|
|
610
|
+
if (taken >= budget)
|
|
611
|
+
return { before, boundary: [...seenAtBoundary], end: false, stopped: false };
|
|
612
|
+
if (now() >= deadline) {
|
|
613
|
+
result.stoppedBy ??= 'maxRunMs';
|
|
614
|
+
return { before, boundary: [...seenAtBoundary], end: false, stopped: true };
|
|
615
|
+
}
|
|
616
|
+
const isNew = Boolean(row.population && row.signature) && !populations.has(row.population);
|
|
617
|
+
let size = 0;
|
|
618
|
+
if (isNew) {
|
|
619
|
+
if (added >= populationBudget)
|
|
620
|
+
return { before, boundary: [...seenAtBoundary], end: false, stopped: false, capped: 'maxPopulations' };
|
|
621
|
+
size = POPULATION_OVERHEAD_BYTES + schemaBytes(row.schema);
|
|
622
|
+
if (bytes + size > config.maxBytes)
|
|
623
|
+
return { before, boundary: [...seenAtBoundary], end: false, stopped: false, capped: 'maxBytes' };
|
|
624
|
+
}
|
|
625
|
+
taken++;
|
|
626
|
+
result.scanned++;
|
|
627
|
+
result.reachedAt = Math.min(result.reachedAt ?? row.at, row.at);
|
|
628
|
+
if (row.expiresAt !== before)
|
|
629
|
+
seenAtBoundary.clear();
|
|
630
|
+
before = row.expiresAt;
|
|
631
|
+
seenAtBoundary.add(row.id);
|
|
632
|
+
if (result.scanned % YIELD_EVERY === 0)
|
|
633
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
634
|
+
if (!isNew)
|
|
635
|
+
continue;
|
|
636
|
+
added++;
|
|
637
|
+
bytes += size;
|
|
638
|
+
populations.set(row.population, {
|
|
639
|
+
bytes: size,
|
|
640
|
+
expiresAt: row.expiresAt,
|
|
641
|
+
head: {
|
|
642
|
+
id: `population/${row.population}`,
|
|
643
|
+
kind: 'population',
|
|
644
|
+
population: row.population,
|
|
645
|
+
owner: ownerOf(row.tenant),
|
|
646
|
+
tenant: row.tenant,
|
|
647
|
+
model: row.model,
|
|
648
|
+
entry: row.entry ?? `registered:${row.backend}`,
|
|
649
|
+
signature: row.signature,
|
|
650
|
+
instructionsHash: row.instructionsHash,
|
|
651
|
+
schemaHash: row.schemaHash,
|
|
652
|
+
schema: row.schema,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (returned < requested)
|
|
657
|
+
return { before, boundary: [...seenAtBoundary], end: true, stopped: false };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async function processPopulation(found) {
|
|
661
|
+
const { head } = found;
|
|
662
|
+
const schema = head.schema;
|
|
663
|
+
let perKey;
|
|
664
|
+
let noMatchSchema;
|
|
665
|
+
try {
|
|
666
|
+
const leaves = (0, decision_ts_1.isObjectSchema)(schema)
|
|
667
|
+
? Object.entries(schema.properties)
|
|
668
|
+
: [[undefined, schema]];
|
|
669
|
+
perKey = leaves.map(([field, leaf]) => ({
|
|
670
|
+
field,
|
|
671
|
+
values: (0, decision_ts_1.allowedValues)(leaf),
|
|
672
|
+
examples: [],
|
|
673
|
+
digester: (0, calibration_ts_1.inputDigester)(),
|
|
674
|
+
evidenceAt: 0,
|
|
675
|
+
cutoff: 0,
|
|
676
|
+
}));
|
|
677
|
+
noMatchSchema = hasNoMatchLeaf(schema);
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
result.failed++;
|
|
681
|
+
logFault('calibration found a malformed population', err);
|
|
682
|
+
try {
|
|
683
|
+
const touchedAt = now();
|
|
684
|
+
await (0, transaction_ts_1.transaction)(freshContext(), () => store.put({ ...head, lastFittedAt: touchedAt, expiresAt: found.expiresAt ?? touchedAt + policy.maxAgeMs }));
|
|
685
|
+
}
|
|
686
|
+
catch { }
|
|
687
|
+
return 'failed';
|
|
688
|
+
}
|
|
689
|
+
const rowBytes = perKey.reduce((sum, key) => sum + EXAMPLE_OVERHEAD_BYTES + key.values.length * 8, 0);
|
|
690
|
+
const sample = Math.min(config.maxExamplesPerKey, Math.max(1, Math.floor((configured.maxBytes - found.bytes) / rowBytes)));
|
|
691
|
+
const limit = Math.min(sample, allowance + 1);
|
|
692
|
+
let rows;
|
|
693
|
+
let used = found.bytes;
|
|
694
|
+
let newestExpiry = 0;
|
|
695
|
+
try {
|
|
696
|
+
rows = await (0, transaction_ts_1.transaction)(freshContext(), async () => {
|
|
697
|
+
const out = [];
|
|
698
|
+
for await (const row of decisions.search({
|
|
699
|
+
conditions: [
|
|
700
|
+
{ attribute: 'populationRank', comparator: 'starts_with', value: `${head.population}|`, descending: true },
|
|
701
|
+
],
|
|
702
|
+
limit,
|
|
703
|
+
})) {
|
|
704
|
+
out.push(row);
|
|
705
|
+
}
|
|
706
|
+
return out;
|
|
707
|
+
});
|
|
708
|
+
const available = allowance;
|
|
709
|
+
allowance -= rows.length;
|
|
710
|
+
result.read += rows.length;
|
|
711
|
+
if (rows.length > available && available < sample)
|
|
712
|
+
return 'deferred';
|
|
713
|
+
rows.sort((a, b) => a.at - b.at || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
714
|
+
for (let start = 0; start < rows.length; start += YIELD_EVERY) {
|
|
715
|
+
if (start > 0) {
|
|
716
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
717
|
+
if (now() >= deadline)
|
|
718
|
+
return 'deadline';
|
|
719
|
+
}
|
|
720
|
+
const chunk = rows.slice(start, start + YIELD_EVERY);
|
|
721
|
+
const chunkFacts = await (0, transaction_ts_1.transaction)(freshContext(), () => Promise.all(chunk.flatMap((row) => perKey.map((key) => outcomes.get((0, decisionStore_ts_1.truthKey)(row.id, key.field))))));
|
|
722
|
+
for (let r = 0; r < chunk.length; r++) {
|
|
723
|
+
const row = chunk[r];
|
|
724
|
+
if (row.expiresAt > newestExpiry)
|
|
725
|
+
newestExpiry = row.expiresAt;
|
|
726
|
+
const facts = chunkFacts.slice(r * perKey.length, (r + 1) * perKey.length);
|
|
727
|
+
for (let i = 0; i < perKey.length; i++) {
|
|
728
|
+
const key = perKey[i];
|
|
729
|
+
const distribution = key.field === undefined
|
|
730
|
+
? (row.rawDistribution ?? row.distribution)
|
|
731
|
+
: (row.rawFields ?? row.fields)?.[key.field]?.distribution;
|
|
732
|
+
const probabilities = (0, calibration_ts_1.toVector)(distribution, key.values);
|
|
733
|
+
const fact = facts[i];
|
|
734
|
+
key.digester.add([row.id, probabilities ?? null, fact?.state ?? null, fact?.at ?? null]);
|
|
735
|
+
key.evidenceAt = Math.max(key.evidenceAt, row.at, fact?.at ?? 0);
|
|
736
|
+
if (!probabilities || !fact?.state || fact.state.kind === 'unknown')
|
|
737
|
+
continue;
|
|
738
|
+
let truth;
|
|
739
|
+
if (fact.state.kind === 'noMatch')
|
|
740
|
+
truth = calibration_ts_1.NO_MATCH_TRUTH;
|
|
741
|
+
else {
|
|
742
|
+
const expected = (0, decision_ts_1.canonicalJson)(fact.state.value);
|
|
743
|
+
truth = key.values.findIndex((value) => (0, decision_ts_1.canonicalJson)(value) === expected);
|
|
744
|
+
if (truth < 0)
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
used += EXAMPLE_OVERHEAD_BYTES + probabilities.length * 8;
|
|
748
|
+
if (used > config.maxBytes)
|
|
749
|
+
return 'budget';
|
|
750
|
+
const returned = (0, decision_ts_1.canonicalJson)(key.field === undefined ? row.value : row.value?.[key.field]);
|
|
751
|
+
const chosen = key.values.findIndex((value) => (0, decision_ts_1.canonicalJson)(value) === returned);
|
|
752
|
+
key.examples.push({ probabilities, truth, at: row.at, chosen: chosen >= 0 ? chosen : undefined });
|
|
753
|
+
key.cutoff = row.at;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
result.failed++;
|
|
760
|
+
logFault('calibration could not read a population', err);
|
|
761
|
+
return 'failed';
|
|
762
|
+
}
|
|
763
|
+
const epsilon = (0, calibration_ts_1.smoothingEpsilon)(head.signature);
|
|
764
|
+
const fittedAt = now();
|
|
765
|
+
const written = [];
|
|
766
|
+
try {
|
|
767
|
+
for (const key of perKey) {
|
|
768
|
+
const keyId = (0, calibration_ts_1.calibrationKey)(head.population, key.field);
|
|
769
|
+
const inputDigest = key.digester.digest();
|
|
770
|
+
const previous = await readNewest(keyId);
|
|
771
|
+
if (previous && previous.inputDigest === inputDigest && previous.policyDigest === digestOfPolicy) {
|
|
772
|
+
result.skipped++;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const labelled = key.examples.length;
|
|
776
|
+
if (!previous && labelled < policy.minReport) {
|
|
777
|
+
result.skipped++;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const inSet = key.examples.filter((example) => example.truth !== calibration_ts_1.NO_MATCH_TRUTH);
|
|
781
|
+
const { train, heldOut } = (0, calibration_ts_1.splitByTime)(key.examples, policy.heldOutShare);
|
|
782
|
+
const trainInSet = train.filter((example) => example.truth !== calibration_ts_1.NO_MATCH_TRUTH);
|
|
783
|
+
const heldOutInSet = heldOut.filter((example) => example.truth !== calibration_ts_1.NO_MATCH_TRUTH);
|
|
784
|
+
const enough = trainInSet.length >= policy.minTrain && heldOutInSet.length >= policy.minHeldOut;
|
|
785
|
+
let params;
|
|
786
|
+
if (enough && !noMatchSchema) {
|
|
787
|
+
params = await (0, calibration_ts_1.fitCalibration)(trainInSet, epsilon, deadline, now);
|
|
788
|
+
if (!params)
|
|
789
|
+
return 'deadline';
|
|
790
|
+
}
|
|
791
|
+
const identity = (p) => p;
|
|
792
|
+
let report;
|
|
793
|
+
let improves = false;
|
|
794
|
+
if (params) {
|
|
795
|
+
const fitted = params;
|
|
796
|
+
const raw = (0, calibration_ts_1.reliability)(heldOut, identity);
|
|
797
|
+
const calibrated = (0, calibration_ts_1.reliability)(heldOut, (p) => (0, calibration_ts_1.applyCalibration)(p, fitted));
|
|
798
|
+
improves = calibrated.ece <= raw.ece - policy.eceMargin && calibrated.nll <= raw.nll;
|
|
799
|
+
report = {
|
|
800
|
+
window: 'heldOut',
|
|
801
|
+
labelled,
|
|
802
|
+
noMatchCount: labelled - inSet.length,
|
|
803
|
+
trainCount: trainInSet.length,
|
|
804
|
+
heldOutCount: heldOutInSet.length,
|
|
805
|
+
raw,
|
|
806
|
+
calibrated,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
else if (labelled >= policy.minReport) {
|
|
810
|
+
report = {
|
|
811
|
+
window: 'all',
|
|
812
|
+
labelled,
|
|
813
|
+
noMatchCount: labelled - inSet.length,
|
|
814
|
+
trainCount: 0,
|
|
815
|
+
heldOutCount: 0,
|
|
816
|
+
raw: (0, calibration_ts_1.reliability)(key.examples, identity),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const eligible = Boolean(params) && improves;
|
|
820
|
+
const reason = noMatchSchema
|
|
821
|
+
? 'no-match-schema'
|
|
822
|
+
: !params
|
|
823
|
+
? 'too-few-labels'
|
|
824
|
+
: improves
|
|
825
|
+
? undefined
|
|
826
|
+
: 'no-improvement';
|
|
827
|
+
const t = params?.t ?? 1;
|
|
828
|
+
const id = (0, calibration_ts_1.fitId)(keyId, inputDigest, policy, { t, epsilon, eligible, reason: reason ?? null });
|
|
829
|
+
if (previous?.id === id) {
|
|
830
|
+
result.skipped++;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const applyUntil = fittedAt + policy.maxAgeMs;
|
|
834
|
+
const evidenceAt = Math.max(key.evidenceAt, (previous?.evidenceAt ?? 0) + 1);
|
|
835
|
+
const row = {
|
|
836
|
+
id,
|
|
837
|
+
kind: 'fit',
|
|
838
|
+
key: keyId,
|
|
839
|
+
rank: rankOf(keyId, evidenceAt, fittedAt, id),
|
|
840
|
+
population: head.population,
|
|
841
|
+
field: key.field,
|
|
842
|
+
tenant: head.tenant,
|
|
843
|
+
model: head.model,
|
|
844
|
+
entry: head.entry,
|
|
845
|
+
signature: head.signature,
|
|
846
|
+
instructionsHash: head.instructionsHash,
|
|
847
|
+
schemaHash: head.schemaHash,
|
|
848
|
+
t,
|
|
849
|
+
epsilon,
|
|
850
|
+
cutoff: key.cutoff,
|
|
851
|
+
evidenceAt,
|
|
852
|
+
fittedAt,
|
|
853
|
+
applyUntil,
|
|
854
|
+
expiresAt: applyUntil + decisionStore_ts_1.DECISION_RETENTION_MS,
|
|
855
|
+
eligible,
|
|
856
|
+
reason,
|
|
857
|
+
policy,
|
|
858
|
+
policyDigest: digestOfPolicy,
|
|
859
|
+
inputDigest,
|
|
860
|
+
decisions: rows.length,
|
|
861
|
+
labelled,
|
|
862
|
+
report,
|
|
863
|
+
};
|
|
864
|
+
written.push(row);
|
|
865
|
+
}
|
|
866
|
+
const headRow = {
|
|
867
|
+
...head,
|
|
868
|
+
owner: ownerOf(head.tenant),
|
|
869
|
+
lastFittedAt: fittedAt,
|
|
870
|
+
expiresAt: rows.length
|
|
871
|
+
? Math.max(newestExpiry, fittedAt + policy.maxAgeMs)
|
|
872
|
+
: (found.expiresAt ?? fittedAt + policy.maxAgeMs),
|
|
873
|
+
};
|
|
874
|
+
await (0, transaction_ts_1.transaction)(freshContext(), async () => {
|
|
875
|
+
for (const row of written)
|
|
876
|
+
await store.put(row);
|
|
877
|
+
await store.put(headRow);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
catch (err) {
|
|
881
|
+
result.failed++;
|
|
882
|
+
logFault('calibration could not fit a population', err);
|
|
883
|
+
return 'failed';
|
|
884
|
+
}
|
|
885
|
+
for (const row of written)
|
|
886
|
+
writtenKeys.push(row.key);
|
|
887
|
+
result.written += written.length;
|
|
888
|
+
result.eligible += written.filter((row) => row.eligible).length;
|
|
889
|
+
return 'done';
|
|
890
|
+
}
|
|
891
|
+
/** A population this run could not fit goes to the back of the queue, so it cannot hold up the ones behind it. */
|
|
892
|
+
async function rotate(found) {
|
|
893
|
+
try {
|
|
894
|
+
const at = now();
|
|
895
|
+
await (0, transaction_ts_1.transaction)(freshContext(), () => store.put({ ...found.head, lastFittedAt: at, expiresAt: Math.max(found.expiresAt ?? 0, at + policy.maxAgeMs) }));
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
logFault('calibration could not requeue a population', err);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function fail(what, err) {
|
|
902
|
+
result.status = 'failed';
|
|
903
|
+
result.error = `${what}: ${(0, engine_ts_1.safeErrorMessage)(err)}`;
|
|
904
|
+
log.error?.(`models: calibration run failed ${result.error}`);
|
|
905
|
+
}
|
|
906
|
+
function finish() {
|
|
907
|
+
if (result.failed > 0 && result.status === 'completed') {
|
|
908
|
+
result.status = 'failed';
|
|
909
|
+
result.error = `${result.failed} population(s) could not be processed`;
|
|
910
|
+
}
|
|
911
|
+
result.durationMs = now() - started;
|
|
912
|
+
try {
|
|
913
|
+
(0, write_ts_1.recordAction)(result.durationMs, 'model-calibrate', undefined, result.status);
|
|
914
|
+
}
|
|
915
|
+
catch { }
|
|
916
|
+
log.info?.(`models: calibration run ${result.status}: scanned ${result.scanned} decisions, read ${result.read} for fitting, ${result.discovered} populations, processed ${result.processed}, pending ${result.pending}, wrote ${result.written} (${result.eligible} eligible), skipped ${result.skipped}, failed ${result.failed}${result.stoppedBy ? `, stopped by ${result.stoppedBy}` : ''} in ${result.durationMs} ms`);
|
|
917
|
+
return result;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function processingOrder(populations) {
|
|
921
|
+
return [...populations].sort((a, b) => (a.lastFittedAt ?? -1) - (b.lastFittedAt ?? -1) || a.head.population.localeCompare(b.head.population));
|
|
922
|
+
}
|
|
923
|
+
async function listCalibrations(tenant, filter = {}) {
|
|
924
|
+
const store = getCalibrationsTable();
|
|
925
|
+
if (!store)
|
|
926
|
+
return [];
|
|
927
|
+
const now = Date.now();
|
|
928
|
+
const out = [];
|
|
929
|
+
const owned = await (0, transaction_ts_1.transaction)(freshContext(), async () => {
|
|
930
|
+
const rows = [];
|
|
931
|
+
for await (const row of store.search({
|
|
932
|
+
conditions: [
|
|
933
|
+
{ attribute: 'owner', value: ownerOf(tenant) },
|
|
934
|
+
{ attribute: 'kind', value: 'population' },
|
|
935
|
+
...(filter.model === undefined ? [] : [{ attribute: 'model', value: filter.model }]),
|
|
936
|
+
],
|
|
937
|
+
}))
|
|
938
|
+
rows.push(row);
|
|
939
|
+
return rows;
|
|
940
|
+
});
|
|
941
|
+
const keys = [];
|
|
942
|
+
for (const head of owned) {
|
|
943
|
+
if ((head.tenant ?? null) !== (tenant ?? null))
|
|
944
|
+
continue;
|
|
945
|
+
if (filter.model !== undefined && head.model !== filter.model)
|
|
946
|
+
continue;
|
|
947
|
+
try {
|
|
948
|
+
const fields = (0, decision_ts_1.isObjectSchema)(head.schema)
|
|
949
|
+
? Object.keys(head.schema.properties)
|
|
950
|
+
: [undefined];
|
|
951
|
+
for (const field of fields)
|
|
952
|
+
keys.push((0, calibration_ts_1.calibrationKey)(head.population, field));
|
|
953
|
+
}
|
|
954
|
+
catch { }
|
|
955
|
+
}
|
|
956
|
+
const newest = [];
|
|
957
|
+
for (let i = 0; i < keys.length; i += LIST_BATCH) {
|
|
958
|
+
newest.push(...(await Promise.all(keys.slice(i, i + LIST_BATCH).map((key) => readNewest(key)))));
|
|
959
|
+
}
|
|
960
|
+
{
|
|
961
|
+
for (let i = 0; i < newest.length; i++) {
|
|
962
|
+
const row = newest[i];
|
|
963
|
+
if (!row)
|
|
964
|
+
continue;
|
|
965
|
+
out.push({
|
|
966
|
+
model: row.model,
|
|
967
|
+
field: row.field,
|
|
968
|
+
population: row.population,
|
|
969
|
+
signature: row.signature,
|
|
970
|
+
instructionsHash: row.instructionsHash,
|
|
971
|
+
schemaHash: row.schemaHash,
|
|
972
|
+
fittedAt: row.fittedAt,
|
|
973
|
+
applyUntil: row.applyUntil,
|
|
974
|
+
eligible: row.eligible,
|
|
975
|
+
applied: isUsable(row, now, keys[i]),
|
|
976
|
+
reason: row.reason,
|
|
977
|
+
decisions: row.decisions,
|
|
978
|
+
labelled: row.labelled,
|
|
979
|
+
t: row.t,
|
|
980
|
+
report: row.report,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return out.sort((a, b) => b.fittedAt - a.fittedAt);
|
|
985
|
+
}
|
|
986
|
+
//# sourceMappingURL=calibrationStore.js.map
|