@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
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/cliOperations.ts +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ClientError, ServerError } from '../../utility/errors/hdbError.ts';
|
|
3
|
+
import type {
|
|
4
|
+
DecideInput,
|
|
5
|
+
Decision,
|
|
6
|
+
DecisionLeaf,
|
|
7
|
+
DecisionOutcome,
|
|
8
|
+
DecisionOutput,
|
|
9
|
+
DecisionSchema,
|
|
10
|
+
FieldDecision,
|
|
11
|
+
} from './types.ts';
|
|
12
|
+
|
|
13
|
+
export const MAX_LEAF_VALUES = 255;
|
|
14
|
+
export const MAX_OBJECT_FIELDS = 32;
|
|
15
|
+
/** Aggregate allowed values across an object schema; also the ceiling strict structured-output providers place on enum values per request. */
|
|
16
|
+
export const MAX_SCHEMA_VALUES = 500;
|
|
17
|
+
const PROBABILITY_SUM_TOLERANCE = 1e-3;
|
|
18
|
+
const UNSAFE_FIELD_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
19
|
+
const BOOLEAN_VALUES: readonly boolean[] = Object.freeze([false, true]);
|
|
20
|
+
|
|
21
|
+
export type ObjectDecisionSchema = Extract<DecisionSchema, { type: 'object' }>;
|
|
22
|
+
|
|
23
|
+
export function isObjectSchema(schema: DecisionSchema): schema is ObjectDecisionSchema {
|
|
24
|
+
return (schema as { type?: unknown }).type === 'object';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class DecisionSchemaError extends ClientError {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(`Invalid decision schema: ${message}`, 400);
|
|
30
|
+
this.name = 'DecisionSchemaError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class DecisionInputError extends ClientError {
|
|
35
|
+
constructor(message: string) {
|
|
36
|
+
super(`Invalid decision input: ${message}`, 400);
|
|
37
|
+
this.name = 'DecisionInputError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A backend's output violated the decision contract. A backend error: the facade records it and tries the next candidate. */
|
|
42
|
+
export class DecisionContractError extends ServerError {
|
|
43
|
+
constructor(backendName: string, message: string) {
|
|
44
|
+
super(`Backend '${backendName}' returned an invalid decision: ${message}`);
|
|
45
|
+
this.name = 'DecisionContractError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateDecisionSchema(schema: unknown): asserts schema is DecisionSchema {
|
|
50
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
51
|
+
throw new DecisionSchemaError('schema must be an object');
|
|
52
|
+
const candidate = schema as Record<string, unknown>;
|
|
53
|
+
if (candidate.type === 'object') {
|
|
54
|
+
const properties = candidate.properties;
|
|
55
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties))
|
|
56
|
+
throw new DecisionSchemaError('an object schema needs a properties map');
|
|
57
|
+
if (candidate.description !== undefined && typeof candidate.description !== 'string')
|
|
58
|
+
throw new DecisionSchemaError('description must be a string');
|
|
59
|
+
if (candidate.noMatch !== undefined)
|
|
60
|
+
throw new DecisionSchemaError('noMatch belongs on a property, not on an object schema');
|
|
61
|
+
const names = Object.keys(properties);
|
|
62
|
+
if (names.length === 0) throw new DecisionSchemaError('an object schema needs at least one property');
|
|
63
|
+
if (names.length > MAX_OBJECT_FIELDS)
|
|
64
|
+
throw new DecisionSchemaError(
|
|
65
|
+
`an object schema has at most ${MAX_OBJECT_FIELDS} properties, got ${names.length}`
|
|
66
|
+
);
|
|
67
|
+
let total = 0;
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
if (name.length === 0 || UNSAFE_FIELD_NAMES.has(name))
|
|
70
|
+
throw new DecisionSchemaError(`property name '${name}' is not allowed`);
|
|
71
|
+
total += validateLeaf((properties as Record<string, unknown>)[name], `property '${name}'`);
|
|
72
|
+
}
|
|
73
|
+
if (total > MAX_SCHEMA_VALUES)
|
|
74
|
+
throw new DecisionSchemaError(
|
|
75
|
+
`an object schema spans at most ${MAX_SCHEMA_VALUES} values across its properties, got ${total}`
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
validateLeaf(schema, 'schema');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateLeaf(leaf: unknown, label: string): number {
|
|
83
|
+
if (!leaf || typeof leaf !== 'object' || Array.isArray(leaf))
|
|
84
|
+
throw new DecisionSchemaError(`${label} must be an object`);
|
|
85
|
+
const candidate = leaf as Record<string, unknown>;
|
|
86
|
+
if (candidate.description !== undefined && typeof candidate.description !== 'string')
|
|
87
|
+
throw new DecisionSchemaError(`${label}: description must be a string`);
|
|
88
|
+
if (candidate.noMatch !== undefined && typeof candidate.noMatch !== 'boolean')
|
|
89
|
+
throw new DecisionSchemaError(`${label}: noMatch must be a boolean`);
|
|
90
|
+
if (Array.isArray(candidate.enum)) {
|
|
91
|
+
if (candidate.enum.length < 2 || candidate.enum.length > MAX_LEAF_VALUES)
|
|
92
|
+
throw new DecisionSchemaError(`${label}: enum needs 2..${MAX_LEAF_VALUES} values, got ${candidate.enum.length}`);
|
|
93
|
+
const type = typeof candidate.enum[0];
|
|
94
|
+
if (type !== 'string' && type !== 'number' && type !== 'boolean')
|
|
95
|
+
throw new DecisionSchemaError(`${label}: enum values must be strings, finite numbers or booleans`);
|
|
96
|
+
const seen = new Set<unknown>();
|
|
97
|
+
for (const value of candidate.enum) {
|
|
98
|
+
if (typeof value !== type)
|
|
99
|
+
throw new DecisionSchemaError(`${label}: enum values must all be of one type (strings, numbers or booleans)`);
|
|
100
|
+
if (type === 'number' && !Number.isFinite(value))
|
|
101
|
+
throw new DecisionSchemaError(`${label}: enum values must be strings, finite numbers or booleans`);
|
|
102
|
+
if (seen.has(value)) throw new DecisionSchemaError(`${label}: enum values must be distinct`);
|
|
103
|
+
seen.add(value);
|
|
104
|
+
}
|
|
105
|
+
return candidate.enum.length;
|
|
106
|
+
}
|
|
107
|
+
if (candidate.type === 'boolean') return BOOLEAN_VALUES.length;
|
|
108
|
+
if (candidate.type === 'integer') {
|
|
109
|
+
const { minimum, maximum } = candidate;
|
|
110
|
+
// Safe integers only: past 2^53 `value++` no longer advances, so a range could never end.
|
|
111
|
+
if (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximum))
|
|
112
|
+
throw new DecisionSchemaError(`${label}: integer minimum and maximum must be safe integers`);
|
|
113
|
+
const span = (maximum as number) - (minimum as number) + 1;
|
|
114
|
+
if (span < 2 || span > MAX_LEAF_VALUES)
|
|
115
|
+
throw new DecisionSchemaError(`${label}: an integer range spans 2..${MAX_LEAF_VALUES} values, got ${span}`);
|
|
116
|
+
return span;
|
|
117
|
+
}
|
|
118
|
+
throw new DecisionSchemaError(`${label} must be an enum, a boolean, or a bounded integer`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function integerRange(minimum: number, maximum: number): number[] {
|
|
122
|
+
const span = maximum - minimum + 1;
|
|
123
|
+
const values: number[] = new Array(span);
|
|
124
|
+
for (let i = 0; i < span; i++) values[i] = minimum + i;
|
|
125
|
+
return values;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** In the order ties resolve; assumes a validated leaf. */
|
|
129
|
+
export function allowedValues(leaf: DecisionLeaf): readonly unknown[] {
|
|
130
|
+
if ('enum' in leaf) return leaf.enum;
|
|
131
|
+
if (leaf.type === 'boolean') return BOOLEAN_VALUES;
|
|
132
|
+
return integerRange(leaf.minimum, leaf.maximum);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Assumes a validated leaf. */
|
|
136
|
+
export function isAllowedValue(leaf: DecisionLeaf, raw: unknown): boolean {
|
|
137
|
+
if ('enum' in leaf) return (leaf.enum as readonly unknown[]).includes(raw);
|
|
138
|
+
if (leaf.type === 'boolean') return typeof raw === 'boolean';
|
|
139
|
+
return Number.isInteger(raw) && (raw as number) >= leaf.minimum && (raw as number) <= leaf.maximum;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function wantsNoMatch(schema: DecisionSchema): boolean {
|
|
143
|
+
if (!isObjectSchema(schema)) return schema.noMatch === true;
|
|
144
|
+
for (const name in schema.properties)
|
|
145
|
+
if (Object.hasOwn(schema.properties, name) && schema.properties[name].noMatch === true) return true;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The text form of a `decide` state, or a `DecisionInputError` for a value no backend could serialize. */
|
|
150
|
+
export function stateToText(state: DecideInput): string {
|
|
151
|
+
if (typeof state === 'string') return state;
|
|
152
|
+
if (!state || typeof state !== 'object') throw new DecisionInputError('state must be a string or an object');
|
|
153
|
+
let text: string | undefined;
|
|
154
|
+
try {
|
|
155
|
+
text = JSON.stringify(state);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new DecisionInputError(`state is not JSON-serializable (${(err as Error)?.message ?? err})`);
|
|
158
|
+
}
|
|
159
|
+
if (typeof text !== 'string') throw new DecisionInputError('state is not JSON-serializable');
|
|
160
|
+
return text;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check a backend's output against the schema and shape it for the caller: complete distributions
|
|
165
|
+
* sorted descending (ties keep schema order), the argmax as `value`, per-field marginals for object
|
|
166
|
+
* schemas. `calibrated` falls back to the backend's capability when the output does not say.
|
|
167
|
+
* Messages never echo backend-supplied values, because they can reach an error response.
|
|
168
|
+
*/
|
|
169
|
+
export function normalizeDecision<T>(
|
|
170
|
+
schema: DecisionSchema,
|
|
171
|
+
output: DecisionOutput<unknown>,
|
|
172
|
+
backendName: string,
|
|
173
|
+
calibratedDefault: boolean,
|
|
174
|
+
calibratedNoMatch = false
|
|
175
|
+
): Omit<Decision<T>, 'id' | 'usage'> {
|
|
176
|
+
if (!output || typeof output !== 'object') throw new DecisionContractError(backendName, 'output must be an object');
|
|
177
|
+
const reported = typeof output.calibrated === 'boolean' ? output.calibrated : calibratedDefault;
|
|
178
|
+
const calibrated = reported && (calibratedNoMatch || !wantsNoMatch(schema));
|
|
179
|
+
if (isObjectSchema(schema)) {
|
|
180
|
+
const fields = output.fields;
|
|
181
|
+
if (!fields || typeof fields !== 'object')
|
|
182
|
+
throw new DecisionContractError(backendName, 'an object schema needs a fields map');
|
|
183
|
+
const marginals: Record<string, FieldDecision> = {};
|
|
184
|
+
const value: Record<string, unknown> = {};
|
|
185
|
+
for (const name of Object.keys(schema.properties)) {
|
|
186
|
+
if (!Object.hasOwn(fields, name)) throw new DecisionContractError(backendName, `missing field '${name}'`);
|
|
187
|
+
const marginal = normalizeLeaf(schema.properties[name], fields[name], backendName, `field '${name}'`);
|
|
188
|
+
marginals[name] = marginal;
|
|
189
|
+
value[name] = marginal.value;
|
|
190
|
+
}
|
|
191
|
+
for (const name of Object.keys(fields)) {
|
|
192
|
+
if (!Object.hasOwn(schema.properties, name))
|
|
193
|
+
throw new DecisionContractError(backendName, `unexpected field '${name}'`);
|
|
194
|
+
}
|
|
195
|
+
return { value: value as T, fields: marginals, calibrated };
|
|
196
|
+
}
|
|
197
|
+
const leaf = normalizeLeaf(schema, output, backendName, 'output');
|
|
198
|
+
const decision: Omit<Decision<T>, 'id' | 'usage'> = {
|
|
199
|
+
value: leaf.value as T,
|
|
200
|
+
probability: leaf.probability,
|
|
201
|
+
distribution: leaf.distribution,
|
|
202
|
+
calibrated,
|
|
203
|
+
};
|
|
204
|
+
if (leaf.noMatch !== undefined) decision.noMatch = leaf.noMatch;
|
|
205
|
+
return decision;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeLeaf(
|
|
209
|
+
leaf: DecisionLeaf,
|
|
210
|
+
output: DecisionOutput<unknown> | undefined,
|
|
211
|
+
backendName: string,
|
|
212
|
+
label: string
|
|
213
|
+
): FieldDecision {
|
|
214
|
+
const allowed = allowedValues(leaf);
|
|
215
|
+
const distribution = output?.distribution;
|
|
216
|
+
if (!Array.isArray(distribution)) throw new DecisionContractError(backendName, `${label}: distribution is required`);
|
|
217
|
+
if (distribution.length !== allowed.length)
|
|
218
|
+
throw new DecisionContractError(
|
|
219
|
+
backendName,
|
|
220
|
+
`${label}: distribution needs one entry per allowed value (${allowed.length}), got ${distribution.length}`
|
|
221
|
+
);
|
|
222
|
+
const byValue = new Map<unknown, number>();
|
|
223
|
+
let sum = 0;
|
|
224
|
+
for (let i = 0; i < distribution.length; i++) {
|
|
225
|
+
const entry = distribution[i];
|
|
226
|
+
if (!entry || typeof entry !== 'object')
|
|
227
|
+
throw new DecisionContractError(backendName, `${label}: distribution entry ${i} is not an object`);
|
|
228
|
+
const { value, probability } = entry;
|
|
229
|
+
if (!isAllowedValue(leaf, value))
|
|
230
|
+
throw new DecisionContractError(backendName, `${label}: distribution entry ${i} is not an allowed value`);
|
|
231
|
+
if (typeof probability !== 'number' || !Number.isFinite(probability) || probability < 0 || probability > 1)
|
|
232
|
+
throw new DecisionContractError(
|
|
233
|
+
backendName,
|
|
234
|
+
`${label}: the probability of distribution entry ${i} must be a finite number in [0, 1]`
|
|
235
|
+
);
|
|
236
|
+
if (byValue.has(value))
|
|
237
|
+
throw new DecisionContractError(backendName, `${label}: distribution entry ${i} duplicates an earlier entry`);
|
|
238
|
+
byValue.set(value, probability);
|
|
239
|
+
sum += probability;
|
|
240
|
+
}
|
|
241
|
+
if (Math.abs(sum - 1) > PROBABILITY_SUM_TOLERANCE)
|
|
242
|
+
throw new DecisionContractError(backendName, `${label}: probabilities sum to ${sum}, expected 1`);
|
|
243
|
+
const sorted: DecisionOutcome[] = allowed.map((value) => ({ value, probability: byValue.get(value)! }));
|
|
244
|
+
sorted.sort((a, b) => b.probability - a.probability);
|
|
245
|
+
const top = sorted[0].probability;
|
|
246
|
+
const chosen = output!.value;
|
|
247
|
+
if (chosen !== undefined && chosen !== sorted[0].value) {
|
|
248
|
+
if (byValue.get(chosen) !== top)
|
|
249
|
+
throw new DecisionContractError(backendName, `${label}: value is not a most-probable outcome`);
|
|
250
|
+
const index = sorted.findIndex((entry) => entry.value === chosen);
|
|
251
|
+
sorted.unshift(...sorted.splice(index, 1));
|
|
252
|
+
}
|
|
253
|
+
const marginal: FieldDecision = { value: sorted[0].value, probability: top, distribution: sorted };
|
|
254
|
+
if (leaf.noMatch === true) {
|
|
255
|
+
const score = output!.noMatch;
|
|
256
|
+
if (typeof score !== 'number' || !Number.isFinite(score) || score < 0 || score > 1)
|
|
257
|
+
throw new DecisionContractError(backendName, `${label}: noMatch must be a finite number in [0, 1]`);
|
|
258
|
+
marginal.noMatch = score;
|
|
259
|
+
}
|
|
260
|
+
return marginal;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The JSON Schema a generative backend is asked to honor for one sample: an object at the root
|
|
265
|
+
* (leaf answers wrapped as `{ value }`), every property required, no additional properties, and
|
|
266
|
+
* bounded integers as an explicit enum — the shape strict structured-output modes accept.
|
|
267
|
+
*/
|
|
268
|
+
export function toResponseSchema(schema: DecisionSchema): object {
|
|
269
|
+
if (isObjectSchema(schema)) {
|
|
270
|
+
const properties: Record<string, object> = {};
|
|
271
|
+
for (const [name, leaf] of Object.entries(schema.properties))
|
|
272
|
+
properties[name] = leaf.noMatch === true ? pairJsonSchema(leafJsonSchema(leaf)) : leafJsonSchema(leaf);
|
|
273
|
+
const out: Record<string, unknown> = {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties,
|
|
276
|
+
required: Object.keys(schema.properties),
|
|
277
|
+
additionalProperties: false,
|
|
278
|
+
};
|
|
279
|
+
if (schema.description) out.description = schema.description;
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
if (schema.noMatch === true) return pairJsonSchema(leafJsonSchema(schema));
|
|
283
|
+
return {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: { value: leafJsonSchema(schema) },
|
|
286
|
+
required: ['value'],
|
|
287
|
+
additionalProperties: false,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function pairJsonSchema(value: object): object {
|
|
292
|
+
return {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: { value, noMatch: { type: 'boolean' } },
|
|
295
|
+
required: ['value', 'noMatch'],
|
|
296
|
+
additionalProperties: false,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function leafJsonSchema(leaf: DecisionLeaf): object {
|
|
301
|
+
const out: Record<string, unknown> = {};
|
|
302
|
+
if ('enum' in leaf) {
|
|
303
|
+
out.type = typeof leaf.enum[0];
|
|
304
|
+
out.enum = [...leaf.enum];
|
|
305
|
+
} else if (leaf.type === 'boolean') {
|
|
306
|
+
out.type = 'boolean';
|
|
307
|
+
} else {
|
|
308
|
+
out.type = 'integer';
|
|
309
|
+
out.enum = integerRange(leaf.minimum, leaf.maximum);
|
|
310
|
+
}
|
|
311
|
+
if (leaf.description) out.description = leaf.description;
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Parse one generative sample against the schema: the leaf value, or a `{ [property]: value }`
|
|
317
|
+
* map for object schemas. Throws a plain `Error` naming what did not fit, never quoting the
|
|
318
|
+
* sample itself; the caller wraps it.
|
|
319
|
+
*/
|
|
320
|
+
export function parseDecisionSample(schema: DecisionSchema, content: unknown): unknown {
|
|
321
|
+
if (typeof content !== 'string') throw new Error('the sample has no text');
|
|
322
|
+
let answer: unknown;
|
|
323
|
+
let answerKey: string | undefined;
|
|
324
|
+
let lastError: Error | undefined;
|
|
325
|
+
for (const candidate of jsonObjectSpans(content)) {
|
|
326
|
+
let values: unknown;
|
|
327
|
+
try {
|
|
328
|
+
values = extractSampleValues(schema, candidate);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
lastError = err as Error;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const key = JSON.stringify(values);
|
|
334
|
+
if (answerKey === undefined) {
|
|
335
|
+
answer = values;
|
|
336
|
+
answerKey = key;
|
|
337
|
+
} else if (key !== answerKey) {
|
|
338
|
+
throw new Error('the sample contains more than one answer');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (answerKey !== undefined) return answer;
|
|
342
|
+
throw lastError ?? new Error('the sample contains no JSON object');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function extractSampleValues(schema: DecisionSchema, sample: Record<string, unknown>): unknown {
|
|
346
|
+
if (isObjectSchema(schema)) {
|
|
347
|
+
const values: Record<string, unknown> = {};
|
|
348
|
+
for (const [name, leaf] of Object.entries(schema.properties)) {
|
|
349
|
+
if (!Object.hasOwn(sample, name)) throw new Error(`the sample has no '${name}'`);
|
|
350
|
+
values[name] =
|
|
351
|
+
leaf.noMatch === true
|
|
352
|
+
? checkSamplePair(leaf, sample[name], `'${name}'`)
|
|
353
|
+
: checkSampleValue(leaf, sample[name], `'${name}'`);
|
|
354
|
+
}
|
|
355
|
+
return values;
|
|
356
|
+
}
|
|
357
|
+
if (schema.noMatch === true) return checkSamplePair(schema, sample, 'the sample');
|
|
358
|
+
return checkSampleValue(schema, sample.value, "'value'");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function checkSamplePair(leaf: DecisionLeaf, raw: unknown, label: string): SamplePair {
|
|
362
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`${label} needs { value, noMatch }`);
|
|
363
|
+
const pair = raw as Record<string, unknown>;
|
|
364
|
+
if (typeof pair.noMatch !== 'boolean') throw new Error(`${label} has no boolean noMatch`);
|
|
365
|
+
return { value: checkSampleValue(leaf, pair.value, `${label} value`), noMatch: pair.noMatch };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface SamplePair {
|
|
369
|
+
value: unknown;
|
|
370
|
+
noMatch: boolean;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const MAX_OBJECT_SPANS = 64;
|
|
374
|
+
const MAX_SCAN_STEPS_PER_CHAR = 256;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Every balanced `{`…`}` span that parses to a JSON object. A backend that ignores
|
|
378
|
+
* `responseFormat` (Anthropic, Bedrock) answers from the prompt alone and may wrap the object in
|
|
379
|
+
* code fences or prose with braces and stray quotes of its own; the schema check on each candidate
|
|
380
|
+
* is what tells the answer from the rest, so no span is preferred over another. String tracking
|
|
381
|
+
* starts at each opening brace, so quotes in the prose before an object cannot hide it, while
|
|
382
|
+
* braces inside the object's own strings are text and are never tried as starts once that object
|
|
383
|
+
* has parsed. Those string ranges are recorded once, from the outermost object that parses, so
|
|
384
|
+
* they stay in text order and the skip is one forward pass over them. The scan fails loudly, never
|
|
385
|
+
* partially: past `MAX_OBJECT_SPANS` balanced spans, or once the character steps exceed
|
|
386
|
+
* `MAX_SCAN_STEPS_PER_CHAR` times the reply's length.
|
|
387
|
+
*/
|
|
388
|
+
function* jsonObjectSpans(text: string): Generator<Record<string, unknown>> {
|
|
389
|
+
const work = { left: MAX_SCAN_STEPS_PER_CHAR * text.length };
|
|
390
|
+
const stringRanges: Array<[number, number]> = [];
|
|
391
|
+
let range = 0;
|
|
392
|
+
let recordedEnd = -1;
|
|
393
|
+
let spans = 0;
|
|
394
|
+
for (let start = text.indexOf('{'); start >= 0; start = text.indexOf('{', start + 1)) {
|
|
395
|
+
while (range < stringRanges.length && stringRanges[range][1] < start) range++;
|
|
396
|
+
if (range < stringRanges.length && stringRanges[range][0] < start) continue;
|
|
397
|
+
const strings: Array<[number, number]> = [];
|
|
398
|
+
const end = balancedClose(text, start, work, strings);
|
|
399
|
+
if (end < 0) continue;
|
|
400
|
+
if (++spans > MAX_OBJECT_SPANS) throw new Error('the sample has too many objects');
|
|
401
|
+
let parsed: unknown;
|
|
402
|
+
try {
|
|
403
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
404
|
+
} catch {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (start > recordedEnd) {
|
|
408
|
+
stringRanges.push(...strings);
|
|
409
|
+
recordedEnd = end;
|
|
410
|
+
}
|
|
411
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) yield parsed as Record<string, unknown>;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Index of the `}` that balances the `{` at `start`, reading JSON strings as text, or -1. The
|
|
417
|
+
* `[from, to]` index range of every string read on the way is appended to `strings`.
|
|
418
|
+
*/
|
|
419
|
+
function balancedClose(text: string, start: number, work: { left: number }, strings: Array<[number, number]>): number {
|
|
420
|
+
let depth = 0;
|
|
421
|
+
let stringStart = -1;
|
|
422
|
+
let escaped = false;
|
|
423
|
+
for (let i = start; i < text.length; i++) {
|
|
424
|
+
if (--work.left < 0) throw new Error('the sample is too long to scan');
|
|
425
|
+
const ch = text[i];
|
|
426
|
+
if (stringStart >= 0) {
|
|
427
|
+
if (escaped) escaped = false;
|
|
428
|
+
else if (ch === '\\') escaped = true;
|
|
429
|
+
else if (ch === '"') {
|
|
430
|
+
strings.push([stringStart, i]);
|
|
431
|
+
stringStart = -1;
|
|
432
|
+
}
|
|
433
|
+
} else if (ch === '"') stringStart = i;
|
|
434
|
+
else if (ch === '{') depth++;
|
|
435
|
+
else if (ch === '}' && --depth === 0) return i;
|
|
436
|
+
}
|
|
437
|
+
return -1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function checkSampleValue(leaf: DecisionLeaf, raw: unknown, label: string): unknown {
|
|
441
|
+
if (isAllowedValue(leaf, raw)) return raw;
|
|
442
|
+
throw new Error(`${label} is not an allowed value`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** JSON with object keys sorted at every level; array order is kept because enum order is meaning. */
|
|
446
|
+
export function canonicalJson(value: unknown): string {
|
|
447
|
+
return JSON.stringify(sortKeys(value));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sortKeys(value: unknown): unknown {
|
|
451
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
452
|
+
if (!value || typeof value !== 'object') return value;
|
|
453
|
+
const sorted: Record<string, unknown> = {};
|
|
454
|
+
for (const key of Object.keys(value as object).sort())
|
|
455
|
+
sorted[key] = sortKeys((value as Record<string, unknown>)[key]);
|
|
456
|
+
return sorted;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Identity of the schema as the model saw it: the allowed values and the descriptions, nothing else. */
|
|
460
|
+
export function hashSchema(schema: DecisionSchema): string {
|
|
461
|
+
return createHash('sha256')
|
|
462
|
+
.update(canonicalJson(knownSchema(schema, true)))
|
|
463
|
+
.digest('hex');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The allowed values alone, for validating outcomes against. Descriptions are hashed, never stored,
|
|
468
|
+
* because they are free text that can carry request data; unknown keys are dropped from both.
|
|
469
|
+
*/
|
|
470
|
+
export function scoringSchema(schema: DecisionSchema): DecisionSchema {
|
|
471
|
+
return knownSchema(schema, false);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** A copy of a validated schema with only its known keys, so a call and its record see one schema whatever the caller mutates afterwards. */
|
|
475
|
+
export function snapshotSchema(schema: DecisionSchema): DecisionSchema {
|
|
476
|
+
return knownSchema(schema, true);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** sha256 of a caller's per-call text, so decisions made under different instructions do not share an identity. */
|
|
480
|
+
export function hashText(text: string): string {
|
|
481
|
+
return createHash('sha256').update(text).digest('hex');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function knownSchema(schema: DecisionSchema, withDescriptions: boolean): DecisionSchema {
|
|
485
|
+
if (isObjectSchema(schema)) {
|
|
486
|
+
const properties: Record<string, DecisionLeaf> = {};
|
|
487
|
+
for (const [name, leaf] of Object.entries(schema.properties)) properties[name] = knownLeaf(leaf, withDescriptions);
|
|
488
|
+
const known: Record<string, unknown> = { type: 'object', properties };
|
|
489
|
+
if (withDescriptions && schema.description !== undefined) known.description = schema.description;
|
|
490
|
+
return known as DecisionSchema;
|
|
491
|
+
}
|
|
492
|
+
return knownLeaf(schema, withDescriptions);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function knownLeaf(leaf: DecisionLeaf, withDescriptions: boolean): DecisionLeaf {
|
|
496
|
+
const known: Record<string, unknown> =
|
|
497
|
+
'enum' in leaf
|
|
498
|
+
? { enum: Array.from(leaf.enum as readonly unknown[]) }
|
|
499
|
+
: leaf.type === 'boolean'
|
|
500
|
+
? { type: 'boolean' }
|
|
501
|
+
: { type: 'integer', minimum: leaf.minimum, maximum: leaf.maximum };
|
|
502
|
+
if (leaf.noMatch === true) known.noMatch = true;
|
|
503
|
+
if (withDescriptions && leaf.description !== undefined) known.description = leaf.description;
|
|
504
|
+
return known as DecisionLeaf;
|
|
505
|
+
}
|