@aiwg/cli 2026.9.20 → 2026.9.21
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/agentic/code/providers/capability-matrix.yaml +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export class CalibrationGovernanceReceiptError extends Error {
|
|
5
|
+
}
|
|
6
|
+
function canonical(value) {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
9
|
+
if (value && typeof value === 'object')
|
|
10
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(',')}}`;
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
function digest(value) {
|
|
14
|
+
return `sha256:${createHash('sha256').update(canonical(value)).digest('hex')}`;
|
|
15
|
+
}
|
|
16
|
+
function validDigest(value) { return /^sha256:[0-9a-f]{64}$/.test(value); }
|
|
17
|
+
function immutable(value) {
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
for (const item of Object.values(value))
|
|
20
|
+
immutable(item);
|
|
21
|
+
Object.freeze(value);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function nonEmpty(value, name) {
|
|
26
|
+
if (!value.trim())
|
|
27
|
+
throw new CalibrationGovernanceReceiptError(`${name} must not be empty`);
|
|
28
|
+
}
|
|
29
|
+
export function calibrationGovernanceReceiptDigest(receipt) {
|
|
30
|
+
return digest(receipt);
|
|
31
|
+
}
|
|
32
|
+
export function createCalibrationGovernanceReceipt(draft) {
|
|
33
|
+
const payload = {
|
|
34
|
+
schemaVersion: 'decision-calibration-governance-receipt/v1', ...structuredClone(draft),
|
|
35
|
+
};
|
|
36
|
+
const receipt = { ...payload, digest: calibrationGovernanceReceiptDigest(payload) };
|
|
37
|
+
validateCalibrationGovernanceReceipt(receipt);
|
|
38
|
+
return immutable(receipt);
|
|
39
|
+
}
|
|
40
|
+
export function validateCalibrationGovernanceReceipt(receipt) {
|
|
41
|
+
if (receipt.schemaVersion !== 'decision-calibration-governance-receipt/v1')
|
|
42
|
+
throw new CalibrationGovernanceReceiptError('unsupported governance receipt schema');
|
|
43
|
+
for (const [name, value] of Object.entries({ receiptId: receipt.receiptId, alias: receipt.alias,
|
|
44
|
+
approvalReference: receipt.approvalReference, evaluationReportId: receipt.reviewedEvidence.evaluationIntegrityReport.id,
|
|
45
|
+
calibrationArtifactId: receipt.reviewedEvidence.calibrationArtifact.id }))
|
|
46
|
+
nonEmpty(value, name);
|
|
47
|
+
if (!Number.isSafeInteger(receipt.sequence) || receipt.sequence < 1)
|
|
48
|
+
throw new CalibrationGovernanceReceiptError('sequence must be a positive safe integer');
|
|
49
|
+
if (!Number.isSafeInteger(receipt.from.aliasRevision) || receipt.from.aliasRevision < 1
|
|
50
|
+
|| !Number.isSafeInteger(receipt.to.aliasRevision) || receipt.to.aliasRevision < 1)
|
|
51
|
+
throw new CalibrationGovernanceReceiptError('alias revisions must be positive safe integers');
|
|
52
|
+
if ((receipt.kind === 'promotion' ? 'promote' : 'rollback') !== receipt.action)
|
|
53
|
+
throw new CalibrationGovernanceReceiptError('receipt kind and action must agree');
|
|
54
|
+
if (!receipt.reasons.length || receipt.reasons.some(reason => !reason.trim()))
|
|
55
|
+
throw new CalibrationGovernanceReceiptError('receipt reasons must be explicit');
|
|
56
|
+
if (!Number.isFinite(Date.parse(receipt.recordedAt)))
|
|
57
|
+
throw new CalibrationGovernanceReceiptError('recordedAt must be a valid date-time');
|
|
58
|
+
const digests = [receipt.from.identityDigest, receipt.to.identityDigest,
|
|
59
|
+
receipt.reviewedEvidence.evaluationIntegrityReport.digest, receipt.reviewedEvidence.calibrationArtifact.digest,
|
|
60
|
+
receipt.reviewedEvidence.compatibilityRelation?.digest, receipt.previousReceiptDigest, receipt.digest].filter((item) => item !== null && item !== undefined);
|
|
61
|
+
if (digests.some(item => !validDigest(item)))
|
|
62
|
+
throw new CalibrationGovernanceReceiptError('receipt digests must be lowercase sha256 values');
|
|
63
|
+
const { digest: claimed, ...payload } = receipt;
|
|
64
|
+
if (claimed !== calibrationGovernanceReceiptDigest(payload))
|
|
65
|
+
throw new CalibrationGovernanceReceiptError('governance receipt digest mismatch');
|
|
66
|
+
}
|
|
67
|
+
/** Append-only, fsync-backed receipt ledger. A competing sequence writer fails closed. */
|
|
68
|
+
export class FileCalibrationGovernanceReceiptStore {
|
|
69
|
+
directory;
|
|
70
|
+
constructor(directory) {
|
|
71
|
+
this.directory = directory;
|
|
72
|
+
}
|
|
73
|
+
async readHistory() {
|
|
74
|
+
let names;
|
|
75
|
+
try {
|
|
76
|
+
names = await readdir(this.directory);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === 'ENOENT')
|
|
80
|
+
return [];
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const receiptNames = names.filter(name => /^\d{12}\.json$/.test(name)).sort();
|
|
84
|
+
const history = [];
|
|
85
|
+
for (const [index, name] of receiptNames.entries()) {
|
|
86
|
+
const receipt = JSON.parse(await readFile(join(this.directory, name), 'utf8'));
|
|
87
|
+
validateCalibrationGovernanceReceipt(receipt);
|
|
88
|
+
if (receipt.sequence !== index + 1 || name !== `${String(receipt.sequence).padStart(12, '0')}.json`)
|
|
89
|
+
throw new CalibrationGovernanceReceiptError('governance receipt sequence gap');
|
|
90
|
+
const previous = history.at(-1);
|
|
91
|
+
if (receipt.previousReceiptDigest !== (previous?.digest ?? null))
|
|
92
|
+
throw new CalibrationGovernanceReceiptError('governance receipt hash chain mismatch');
|
|
93
|
+
history.push(receipt);
|
|
94
|
+
}
|
|
95
|
+
return structuredClone(history);
|
|
96
|
+
}
|
|
97
|
+
async append(receipt) {
|
|
98
|
+
validateCalibrationGovernanceReceipt(receipt);
|
|
99
|
+
const history = await this.readHistory();
|
|
100
|
+
if (receipt.sequence !== history.length + 1 || receipt.previousReceiptDigest !== (history.at(-1)?.digest ?? null)) {
|
|
101
|
+
throw new CalibrationGovernanceReceiptError('receipt does not extend the current ledger head');
|
|
102
|
+
}
|
|
103
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
104
|
+
const target = join(this.directory, `${String(receipt.sequence).padStart(12, '0')}.json`);
|
|
105
|
+
let file;
|
|
106
|
+
try {
|
|
107
|
+
file = await open(target, 'wx', 0o600);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error.code === 'EEXIST')
|
|
111
|
+
throw new CalibrationGovernanceReceiptError('receipt sequence was concurrently claimed');
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await file.writeFile(`${JSON.stringify(receipt)}\n`);
|
|
116
|
+
await file.sync();
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await file.close();
|
|
120
|
+
}
|
|
121
|
+
const directory = await open(this.directory, 'r');
|
|
122
|
+
try {
|
|
123
|
+
await directory.sync();
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await directory.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=receipts.js.map
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export class CalibrationRegistryError extends Error {
|
|
3
|
+
}
|
|
4
|
+
function canonical(value) {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
7
|
+
if (value && typeof value === 'object')
|
|
8
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(',')}}`;
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
export function calibrationIdentityDigest(identity) {
|
|
12
|
+
return `sha256:${createHash('sha256').update(canonical(identity)).digest('hex')}`;
|
|
13
|
+
}
|
|
14
|
+
export function calibrationArtifactDigest(artifact) {
|
|
15
|
+
return `sha256:${createHash('sha256').update(canonical(artifact)).digest('hex')}`;
|
|
16
|
+
}
|
|
17
|
+
function immutable(value) { return Object.freeze(structuredClone(value)); }
|
|
18
|
+
function validDate(value) { const time = Date.parse(value); if (!Number.isFinite(time))
|
|
19
|
+
throw new CalibrationRegistryError(`invalid date: ${value}`); return time; }
|
|
20
|
+
function nonEmpty(value, name) { if (!value.trim())
|
|
21
|
+
throw new CalibrationRegistryError(`${name} must not be empty`); }
|
|
22
|
+
export class CalibrationRegistry {
|
|
23
|
+
artifacts = new Map();
|
|
24
|
+
artifactDigests = new Map();
|
|
25
|
+
relations = [];
|
|
26
|
+
aliases = new Map();
|
|
27
|
+
drift = [];
|
|
28
|
+
eligibility = new Map();
|
|
29
|
+
pins = new Map();
|
|
30
|
+
registerArtifact(input) {
|
|
31
|
+
validateArtifact(input);
|
|
32
|
+
const existing = this.artifacts.get(input.id);
|
|
33
|
+
if (existing && canonical(existing) !== canonical(input))
|
|
34
|
+
throw new CalibrationRegistryError(`artifact '${input.id}' is immutable`);
|
|
35
|
+
const digestOwner = this.artifactDigests.get(input.digest);
|
|
36
|
+
if (digestOwner && digestOwner !== input.id)
|
|
37
|
+
throw new CalibrationRegistryError(`artifact digest already belongs to '${digestOwner}'`);
|
|
38
|
+
const artifact = immutable(input);
|
|
39
|
+
this.artifacts.set(artifact.id, artifact);
|
|
40
|
+
this.artifactDigests.set(artifact.digest, artifact.id);
|
|
41
|
+
return structuredClone(artifact);
|
|
42
|
+
}
|
|
43
|
+
registerRelation(input) {
|
|
44
|
+
if (input.state === 'approved-compatible' && !input.approvalReference)
|
|
45
|
+
throw new CalibrationRegistryError('approved compatibility requires an approval reference');
|
|
46
|
+
validDate(input.effectiveAt);
|
|
47
|
+
if (input.expiresAt)
|
|
48
|
+
validDate(input.expiresAt);
|
|
49
|
+
if (this.relations.some(item => item.id === input.id && canonical(item) !== canonical(input)))
|
|
50
|
+
throw new CalibrationRegistryError(`relation '${input.id}' is immutable`);
|
|
51
|
+
if (!this.relations.some(item => item.id === input.id))
|
|
52
|
+
this.relations.push(immutable(input));
|
|
53
|
+
return structuredClone(input);
|
|
54
|
+
}
|
|
55
|
+
observeAlias(alias, identity, at) {
|
|
56
|
+
nonEmpty(alias, 'alias');
|
|
57
|
+
validDate(at);
|
|
58
|
+
const digest = calibrationIdentityDigest(identity);
|
|
59
|
+
const history = this.aliases.get(alias) ?? [];
|
|
60
|
+
const previous = history.at(-1);
|
|
61
|
+
if (previous?.actualIdentityDigest === digest && previous.kind !== 'retired')
|
|
62
|
+
return structuredClone(previous);
|
|
63
|
+
if (previous && previous.kind !== 'retired')
|
|
64
|
+
this.drift.push(immutable({
|
|
65
|
+
id: `${alias}:drift:${history.length + 1}`, alias, previousIdentityDigest: previous.actualIdentityDigest,
|
|
66
|
+
observedIdentityDigest: digest, previousActualModel: previous.actualModel, observedActualModel: identity.actualModel, detectedAt: at,
|
|
67
|
+
}));
|
|
68
|
+
const event = immutable({ revision: history.length + 1, alias, actualIdentityDigest: digest,
|
|
69
|
+
actualModel: identity.actualModel, recordedAt: at, kind: 'observed', promotionEligibilityId: null });
|
|
70
|
+
history.push(event);
|
|
71
|
+
this.aliases.set(alias, history);
|
|
72
|
+
return structuredClone(event);
|
|
73
|
+
}
|
|
74
|
+
recordPromotionEligibility(input) {
|
|
75
|
+
validateEligibility(input, this.aliases.get(input.alias) ?? []);
|
|
76
|
+
const existing = this.eligibility.get(input.id);
|
|
77
|
+
if (existing && canonical(existing) !== canonical(input))
|
|
78
|
+
throw new CalibrationRegistryError(`promotion eligibility '${input.id}' is immutable`);
|
|
79
|
+
const record = immutable(input);
|
|
80
|
+
this.eligibility.set(record.id, record);
|
|
81
|
+
return structuredClone(record);
|
|
82
|
+
}
|
|
83
|
+
promoteAlias(eligibilityId, at) {
|
|
84
|
+
const eligibility = this.eligibility.get(eligibilityId);
|
|
85
|
+
if (!eligibility?.eligible)
|
|
86
|
+
throw new CalibrationRegistryError('alias movement requires an eligible promotion record');
|
|
87
|
+
const history = this.aliases.get(eligibility.alias) ?? [];
|
|
88
|
+
const rollback = history.find(event => event.revision === eligibility.rollbackTarget.aliasRevision);
|
|
89
|
+
if (!rollback || rollback.actualIdentityDigest !== eligibility.rollbackTarget.identityDigest)
|
|
90
|
+
throw new CalibrationRegistryError('rollback target no longer matches immutable alias history');
|
|
91
|
+
const event = immutable({ revision: history.length + 1, alias: eligibility.alias,
|
|
92
|
+
actualIdentityDigest: eligibility.candidateIdentityDigest, actualModel: eligibility.candidateActualModel, recordedAt: new Date(validDate(at)).toISOString(),
|
|
93
|
+
kind: 'promoted', promotionEligibilityId: eligibility.id });
|
|
94
|
+
history.push(event);
|
|
95
|
+
this.aliases.set(event.alias, history);
|
|
96
|
+
return structuredClone(event);
|
|
97
|
+
}
|
|
98
|
+
rollbackAlias(alias, targetRevision, approvalReference, at) {
|
|
99
|
+
nonEmpty(approvalReference, 'rollback approval reference');
|
|
100
|
+
const history = this.aliases.get(alias) ?? [];
|
|
101
|
+
const target = history.find(event => event.revision === targetRevision);
|
|
102
|
+
if (!target)
|
|
103
|
+
throw new CalibrationRegistryError('rollback target must be an exact alias-history revision');
|
|
104
|
+
const event = immutable({ ...target, revision: history.length + 1, recordedAt: new Date(validDate(at)).toISOString(),
|
|
105
|
+
kind: 'rolled-back', promotionEligibilityId: `rollback-approval:${approvalReference}` });
|
|
106
|
+
history.push(event);
|
|
107
|
+
return structuredClone(event);
|
|
108
|
+
}
|
|
109
|
+
retireAlias(alias, approvalReference, at) {
|
|
110
|
+
nonEmpty(approvalReference, 'retirement approval reference');
|
|
111
|
+
const history = this.aliases.get(alias) ?? [];
|
|
112
|
+
const current = history.at(-1);
|
|
113
|
+
if (!current)
|
|
114
|
+
throw new CalibrationRegistryError(`unknown alias '${alias}'`);
|
|
115
|
+
const event = immutable({ ...current, revision: history.length + 1, recordedAt: new Date(validDate(at)).toISOString(),
|
|
116
|
+
kind: 'retired', promotionEligibilityId: `retirement-approval:${approvalReference}` });
|
|
117
|
+
history.push(event);
|
|
118
|
+
return structuredClone(event);
|
|
119
|
+
}
|
|
120
|
+
resolve(request, policy) {
|
|
121
|
+
const existing = this.pins.get(request.runId);
|
|
122
|
+
if (existing)
|
|
123
|
+
return structuredClone(existing);
|
|
124
|
+
validDate(request.at);
|
|
125
|
+
const actualDigest = calibrationIdentityDigest(request.actualIdentity);
|
|
126
|
+
const history = this.aliases.get(request.requestedAlias) ?? [];
|
|
127
|
+
const alias = history.at(-1) ?? null;
|
|
128
|
+
if (alias && alias.kind !== 'retired' && alias.actualIdentityDigest !== actualDigest)
|
|
129
|
+
this.observeAlias(request.requestedAlias, request.actualIdentity, request.at);
|
|
130
|
+
const artifact = request.calibrationArtifactId ? this.artifacts.get(request.calibrationArtifactId) : findExactArtifact(this.artifacts, actualDigest);
|
|
131
|
+
const artifactIdentity = artifact ? calibrationIdentityDigest(artifact.identity) : null;
|
|
132
|
+
let state = artifactIdentity === actualDigest ? 'exact' : 'unknown';
|
|
133
|
+
const relation = artifactIdentity ? this.relations.filter(item => item.fromIdentityDigest === artifactIdentity && item.toIdentityDigest === actualDigest
|
|
134
|
+
&& validDate(item.effectiveAt) <= validDate(request.at) && (!item.expiresAt || validDate(item.expiresAt) > validDate(request.at))).at(-1) : undefined;
|
|
135
|
+
if (state !== 'exact' && relation)
|
|
136
|
+
state = relation.state;
|
|
137
|
+
const reasons = [];
|
|
138
|
+
if (!artifact)
|
|
139
|
+
reasons.push('calibration-missing');
|
|
140
|
+
const usable = artifact ? calibrationUsability(artifact, request.at) : [];
|
|
141
|
+
reasons.push(...usable);
|
|
142
|
+
if (alias?.kind === 'retired')
|
|
143
|
+
reasons.push('alias-retired');
|
|
144
|
+
if (alias && alias.actualIdentityDigest !== actualDigest)
|
|
145
|
+
reasons.push('alias-drift');
|
|
146
|
+
let action;
|
|
147
|
+
const unusable = !artifact || usable.length > 0 || alias?.kind === 'retired';
|
|
148
|
+
if (unusable)
|
|
149
|
+
action = policy.unusableCalibration;
|
|
150
|
+
else if (state === 'exact' || state === 'approved-compatible')
|
|
151
|
+
action = 'allow';
|
|
152
|
+
else if (state === 'shadow-required')
|
|
153
|
+
action = policy.shadowRequired;
|
|
154
|
+
else if (state === 'incompatible')
|
|
155
|
+
action = policy.incompatible;
|
|
156
|
+
else
|
|
157
|
+
action = policy.unknown;
|
|
158
|
+
const currentAlias = (this.aliases.get(request.requestedAlias) ?? []).at(-1) ?? alias;
|
|
159
|
+
const pin = immutable({ schemaVersion: 'decision-calibration-compatibility/v1', pinId: `${request.runId}:${actualDigest}`,
|
|
160
|
+
runId: request.runId, requestedAlias: request.requestedAlias, actualModel: request.actualIdentity.actualModel,
|
|
161
|
+
aliasRevision: currentAlias?.revision ?? null, artifactId: artifact?.id ?? null, artifactDigest: artifact?.digest ?? null,
|
|
162
|
+
state, action, reasons, decidedAt: request.at });
|
|
163
|
+
this.pins.set(request.runId, pin);
|
|
164
|
+
return structuredClone(pin);
|
|
165
|
+
}
|
|
166
|
+
evidence(pin, raw, calibrated) {
|
|
167
|
+
if (calibrated && (calibrated.calibrationArtifactId !== pin.artifactId || calibrated.calibrationArtifactDigest !== pin.artifactDigest)) {
|
|
168
|
+
throw new CalibrationRegistryError('calibrated evidence must cite the pinned calibration artifact');
|
|
169
|
+
}
|
|
170
|
+
return immutable({ raw, calibrated, compatibilityPin: pin });
|
|
171
|
+
}
|
|
172
|
+
aliasHistory(alias) { return structuredClone(this.aliases.get(alias) ?? []); }
|
|
173
|
+
driftEvents(alias) { return structuredClone(alias ? this.drift.filter(event => event.alias === alias) : this.drift); }
|
|
174
|
+
promotionHistory() { return structuredClone([...this.eligibility.values()]); }
|
|
175
|
+
artifactHistory() { return structuredClone([...this.artifacts.values()]); }
|
|
176
|
+
compatibilityHistory() { return structuredClone(this.relations); }
|
|
177
|
+
}
|
|
178
|
+
function findExactArtifact(artifacts, digest) {
|
|
179
|
+
return [...artifacts.values()].find(artifact => calibrationIdentityDigest(artifact.identity) === digest);
|
|
180
|
+
}
|
|
181
|
+
function calibrationUsability(artifact, at) {
|
|
182
|
+
const reasons = [];
|
|
183
|
+
if (artifact.approval.state !== 'approved' || !artifact.approval.reference)
|
|
184
|
+
reasons.push(`calibration-${artifact.approval.state}`);
|
|
185
|
+
if (artifact.metrics.totalSamples < artifact.profile.minimumTotalSamples)
|
|
186
|
+
reasons.push('insufficient-total-samples');
|
|
187
|
+
if (artifact.metrics.perSliceSamples < artifact.profile.minimumPerSliceSamples)
|
|
188
|
+
reasons.push('insufficient-slice-samples');
|
|
189
|
+
if (artifact.metrics.calibrationError > artifact.profile.maximumCalibrationError)
|
|
190
|
+
reasons.push('calibration-bound-exceeded');
|
|
191
|
+
if (artifact.metrics.selectiveRisk > artifact.profile.maximumSelectiveRisk)
|
|
192
|
+
reasons.push('selective-risk-bound-exceeded');
|
|
193
|
+
if (validDate(at) >= validDate(artifact.effectiveAt) + artifact.profile.expiresAfterDays * 86_400_000)
|
|
194
|
+
reasons.push('calibration-expired');
|
|
195
|
+
return reasons;
|
|
196
|
+
}
|
|
197
|
+
function validateArtifact(artifact) {
|
|
198
|
+
nonEmpty(artifact.id, 'artifact id');
|
|
199
|
+
validDate(artifact.effectiveAt);
|
|
200
|
+
if (artifact.schemaVersion !== 'decision-calibration-artifact/v1')
|
|
201
|
+
throw new CalibrationRegistryError('unsupported calibration artifact schema');
|
|
202
|
+
validateIdentity(artifact.identity);
|
|
203
|
+
const { digest, ...payload } = artifact;
|
|
204
|
+
if (digest !== calibrationArtifactDigest(payload))
|
|
205
|
+
throw new CalibrationRegistryError('calibration artifact digest does not match its immutable content');
|
|
206
|
+
if (!artifact.limitations.length)
|
|
207
|
+
throw new CalibrationRegistryError('calibration limitations must be explicit');
|
|
208
|
+
const profile = artifact.profile;
|
|
209
|
+
if (!Number.isInteger(profile.minimumTotalSamples) || profile.minimumTotalSamples < 1 || !Number.isInteger(profile.minimumPerSliceSamples) || profile.minimumPerSliceSamples < 1)
|
|
210
|
+
throw new CalibrationRegistryError('sample minima must be positive integers');
|
|
211
|
+
if (!(profile.confidenceInterval.level > 0 && profile.confidenceInterval.level < 1) || !profile.confidenceInterval.method)
|
|
212
|
+
throw new CalibrationRegistryError('confidence interval method and level must be pinned');
|
|
213
|
+
if (profile.maximumCalibrationError < 0 || profile.maximumCalibrationError > 1 || profile.maximumSelectiveRisk < 0 || profile.maximumSelectiveRisk > 1)
|
|
214
|
+
throw new CalibrationRegistryError('risk bounds must be in [0,1]');
|
|
215
|
+
if (!(profile.expiresAfterDays > 0))
|
|
216
|
+
throw new CalibrationRegistryError('expiry rule must be positive');
|
|
217
|
+
if (!Number.isInteger(artifact.metrics.totalSamples) || artifact.metrics.totalSamples < 0 || !Number.isInteger(artifact.metrics.perSliceSamples) || artifact.metrics.perSliceSamples < 0)
|
|
218
|
+
throw new CalibrationRegistryError('sample observations must be non-negative integers');
|
|
219
|
+
const intervals = Object.entries(artifact.metrics.confidenceIntervals);
|
|
220
|
+
if (!intervals.length)
|
|
221
|
+
throw new CalibrationRegistryError('at least one confidence interval is required');
|
|
222
|
+
for (const [name, interval] of intervals) {
|
|
223
|
+
nonEmpty(name, 'confidence interval name');
|
|
224
|
+
if (!Number.isFinite(interval.lower) || !Number.isFinite(interval.upper)
|
|
225
|
+
|| interval.lower < 0 || interval.upper > 1 || interval.lower > interval.upper) {
|
|
226
|
+
throw new CalibrationRegistryError(`confidence interval '${name}' must have ordered finite bounds in [0,1]`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (artifact.approval.state === 'approved' && !artifact.approval.reference)
|
|
230
|
+
throw new CalibrationRegistryError('approved calibration requires an approval reference');
|
|
231
|
+
if (artifact.splitProvenance.holdoutAccessedAt && validDate(artifact.splitProvenance.holdoutAccessedAt) < validDate(artifact.effectiveAt))
|
|
232
|
+
throw new CalibrationRegistryError('profile must be preregistered before holdout access');
|
|
233
|
+
}
|
|
234
|
+
function validateIdentity(identity) {
|
|
235
|
+
for (const [name, value] of Object.entries({ provider: identity.provider, backend: identity.backend, actualModel: identity.actualModel,
|
|
236
|
+
primitive: identity.primitive, adapterVersion: identity.adapterVersion, datasetId: identity.dataset.id, sliceId: identity.slice.id,
|
|
237
|
+
calibratorId: identity.calibrator.id, calibratorVersion: identity.calibrator.version }))
|
|
238
|
+
nonEmpty(value, name);
|
|
239
|
+
for (const digest of [identity.definitionDigest, identity.dataset.hash, identity.slice.hash, identity.calibrator.parametersDigest]) {
|
|
240
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(digest))
|
|
241
|
+
throw new CalibrationRegistryError('identity digests must be lowercase sha256 values');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function validateEligibility(record, history) {
|
|
245
|
+
nonEmpty(record.approvalReference, 'promotion approval reference');
|
|
246
|
+
nonEmpty(record.candidateActualModel, 'candidate actual model');
|
|
247
|
+
validDate(record.recordedAt);
|
|
248
|
+
if (!record.evaluationIntegrityReport.id || !record.evaluationIntegrityReport.digest)
|
|
249
|
+
throw new CalibrationRegistryError('promotion requires a pinned evaluation-integrity report');
|
|
250
|
+
const target = history.find(event => event.revision === record.rollbackTarget.aliasRevision);
|
|
251
|
+
if (!target || target.actualIdentityDigest !== record.rollbackTarget.identityDigest)
|
|
252
|
+
throw new CalibrationRegistryError('promotion requires an exact rollback target');
|
|
253
|
+
if (record.eligible && record.reasons.length)
|
|
254
|
+
throw new CalibrationRegistryError('eligible promotion records cannot contain denial reasons');
|
|
255
|
+
if (!record.eligible && !record.reasons.length)
|
|
256
|
+
throw new CalibrationRegistryError('ineligible promotion records must explain the denial');
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function cacheBenchmarkReport(configurationDigest, warmupCalls, minimumBenefitTargetBps, confidenceInterval, samples) {
|
|
2
|
+
const disabledCount = samples.filter(value => value.mode === 'cache-disabled').length;
|
|
3
|
+
const enabledCount = samples.filter(value => value.mode === 'cache-enabled').length;
|
|
4
|
+
if (!disabledCount || disabledCount !== enabledCount || !Number.isSafeInteger(warmupCalls) || warmupCalls < 0 ||
|
|
5
|
+
!Number.isSafeInteger(minimumBenefitTargetBps) || minimumBenefitTargetBps < 0 || minimumBenefitTargetBps > 10_000 ||
|
|
6
|
+
samples.some(value => !Number.isFinite(value.preparationLatencyMs) || value.preparationLatencyMs < 0 ||
|
|
7
|
+
!Number.isSafeInteger(value.memoryBytes) || value.memoryBytes < 0 ||
|
|
8
|
+
!Number.isSafeInteger(value.storageBytes) || value.storageBytes < 0 ||
|
|
9
|
+
[value.inputTokens, value.outputTokens, value.cachedInputTokens].some(tokens => tokens != null && (!Number.isSafeInteger(tokens) || tokens < 0)) ||
|
|
10
|
+
(value.costUsd !== null && (!Number.isFinite(value.costUsd) || value.costUsd < 0)))) {
|
|
11
|
+
throw new Error('paired cache benchmark requires equal valid enabled and disabled samples');
|
|
12
|
+
}
|
|
13
|
+
return { schemaVersion: 'decision-cache-benchmark/v1', configurationDigest, warmupCalls,
|
|
14
|
+
measuredCalls: samples.length, minimumBenefitTargetBps, confidenceInterval,
|
|
15
|
+
disabled: summarize(samples.filter(value => value.mode === 'cache-disabled')),
|
|
16
|
+
enabled: summarize(samples.filter(value => value.mode === 'cache-enabled')) };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Enforce the preregistered minimum benefit as a pass/fail result. Only
|
|
20
|
+
* measured preparation latency is judged; unknown provider economics never
|
|
21
|
+
* contribute to a pass.
|
|
22
|
+
*/
|
|
23
|
+
export function evaluateCacheBenchmarkTarget(report) {
|
|
24
|
+
const baseline = report.disabled.averagePreparationLatencyMs;
|
|
25
|
+
const observedBenefitBps = baseline > 0
|
|
26
|
+
? Math.round((baseline - report.enabled.averagePreparationLatencyMs) * 10_000 / baseline) : null;
|
|
27
|
+
return { metric: 'preparation-latency', targetBps: report.minimumBenefitTargetBps, observedBenefitBps,
|
|
28
|
+
outcome: observedBenefitBps === null ? 'unknown' : observedBenefitBps >= report.minimumBenefitTargetBps ? 'pass' : 'fail' };
|
|
29
|
+
}
|
|
30
|
+
/** Deterministic paired bootstrap interval; positive differences favor enabled caching. */
|
|
31
|
+
export function pairedPreparationLatencyInterval(disabled, enabled, iterations = 233) {
|
|
32
|
+
if (disabled.length !== enabled.length || disabled.length < 2 ||
|
|
33
|
+
!Number.isSafeInteger(iterations) || iterations < 2 ||
|
|
34
|
+
[...disabled, ...enabled].some(value => !Number.isFinite(value) || value < 0)) {
|
|
35
|
+
throw new Error('paired latency interval requires equal non-negative measured samples');
|
|
36
|
+
}
|
|
37
|
+
const differences = disabled.map((value, index) => value - enabled[index]);
|
|
38
|
+
let seed = 0x2603;
|
|
39
|
+
const means = Array.from({ length: iterations }, () => {
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (let index = 0; index < differences.length; index++) {
|
|
42
|
+
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
|
43
|
+
total += differences[seed % differences.length];
|
|
44
|
+
}
|
|
45
|
+
return total / differences.length;
|
|
46
|
+
}).sort((left, right) => left - right);
|
|
47
|
+
return `95% paired bootstrap CI [${means[Math.floor(iterations * 0.025)].toFixed(3)}, ${means[Math.ceil(iterations * 0.975) - 1].toFixed(3)}] ms`;
|
|
48
|
+
}
|
|
49
|
+
function summarize(samples) {
|
|
50
|
+
const average = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
51
|
+
const nullableAverage = (values) => values.some(value => value === null) ? null : average(values);
|
|
52
|
+
return { calls: samples.length, averagePreparationLatencyMs: average(samples.map(value => value.preparationLatencyMs)),
|
|
53
|
+
averageInputTokens: nullableAverage(samples.map(value => value.inputTokens)),
|
|
54
|
+
averageTotalTaskTokens: nullableAverage(samples.map(value => value.inputTokens === null || value.outputTokens == null ? null : value.inputTokens + value.outputTokens)),
|
|
55
|
+
averageCachedInputTokens: nullableAverage(samples.map(value => value.cachedInputTokens)),
|
|
56
|
+
averageCostUsd: nullableAverage(samples.map(value => value.costUsd)),
|
|
57
|
+
peakMemoryBytes: Math.max(...samples.map(value => value.memoryBytes)),
|
|
58
|
+
peakStorageBytes: Math.max(...samples.map(value => value.storageBytes)),
|
|
59
|
+
hitRateBps: Math.round(samples.filter(value => value.outcome === 'hit').length * 10_000 / samples.length),
|
|
60
|
+
invalidationRateBps: Math.round(samples.filter(value => value.invalidated).length * 10_000 / samples.length) };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=benchmark.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
2
|
+
import { providerPrefixKey } from './identity.js';
|
|
3
|
+
const PREFIX_DIMENSIONS = [
|
|
4
|
+
'schemaVersion', 'orderedPrefixDigest', 'provider', 'backend', 'requestedModel', 'actualModel', 'apiRevision', 'policy',
|
|
5
|
+
'tenantId', 'workspaceId', 'dataClass', 'region', 'egressPolicyDigest',
|
|
6
|
+
];
|
|
7
|
+
const TRANSFERABLE = new Set(['requestedModel', 'actualModel', 'apiRevision', 'backend', 'policy']);
|
|
8
|
+
/** Dimensions whose canonical values differ between two prefix identities. */
|
|
9
|
+
export function changedPrefixDimensions(left, right) {
|
|
10
|
+
return PREFIX_DIMENSIONS.filter(dimension => canonicalJson(left[dimension]) !== canonicalJson(right[dimension]));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Decide whether a previously established provider prefix may be reused.
|
|
14
|
+
* TTL expiry always invalidates. Any identity change invalidates unless one
|
|
15
|
+
* unexpired pinned record names exactly this from/to pair and permits every
|
|
16
|
+
* changed dimension; scope, data class, region, egress, provider and the
|
|
17
|
+
* prefix bytes themselves can never transfer.
|
|
18
|
+
*/
|
|
19
|
+
export async function evaluateProviderPrefixReuse(input) {
|
|
20
|
+
const changed = changedPrefixDimensions(input.cached, input.next);
|
|
21
|
+
const decision = (reusable, reason) => ({ reusable, reason, changedDimensions: changed });
|
|
22
|
+
if (!Number.isSafeInteger(input.cachedAtEpochMs) || !Number.isSafeInteger(input.nowEpochMs)
|
|
23
|
+
|| input.nowEpochMs < input.cachedAtEpochMs
|
|
24
|
+
|| input.nowEpochMs - input.cachedAtEpochMs >= Math.min(input.cached.policy.ttlMs, input.next.policy.ttlMs)) {
|
|
25
|
+
return decision(false, 'ttl-expired');
|
|
26
|
+
}
|
|
27
|
+
if (!changed.length)
|
|
28
|
+
return decision(true, 'identical');
|
|
29
|
+
if (changed.some(dimension => !TRANSFERABLE.has(dimension)))
|
|
30
|
+
return decision(false, 'non-transferable-dimension');
|
|
31
|
+
const from = providerPrefixKey(input.cached);
|
|
32
|
+
const to = providerPrefixKey(input.next);
|
|
33
|
+
const candidates = (input.records ?? []).filter(record => record.schemaVersion === 'decision-provider-prefix-compatibility/v1'
|
|
34
|
+
&& record.fromIdentityDigest === from && record.toIdentityDigest === to);
|
|
35
|
+
if (!candidates.length)
|
|
36
|
+
return decision(false, 'identity-changed');
|
|
37
|
+
const live = candidates.filter(record => Number.isSafeInteger(record.expiresAtEpochMs) && record.expiresAtEpochMs > input.nowEpochMs
|
|
38
|
+
&& !!record.approvedBy);
|
|
39
|
+
if (!live.length)
|
|
40
|
+
return decision(false, 'record-expired');
|
|
41
|
+
const permitting = live.filter(record => changed.every(dimension => record.permits.includes(dimension)));
|
|
42
|
+
if (!permitting.length)
|
|
43
|
+
return decision(false, 'record-mismatch');
|
|
44
|
+
const modelMoved = changed.includes('requestedModel') || changed.includes('actualModel');
|
|
45
|
+
for (const record of permitting) {
|
|
46
|
+
if (!modelMoved)
|
|
47
|
+
return decision(true, 'pinned-compatibility');
|
|
48
|
+
const snapshot = record.aliasSnapshot;
|
|
49
|
+
if (!snapshot || snapshot.alias !== input.next.requestedModel || snapshot.validUntilEpochMs <= input.nowEpochMs
|
|
50
|
+
|| input.next.actualModel === null || !snapshot.approvedActualModels.includes(input.next.actualModel)
|
|
51
|
+
|| !input.verifyAliasSnapshot)
|
|
52
|
+
continue;
|
|
53
|
+
let verified = false;
|
|
54
|
+
try {
|
|
55
|
+
verified = await input.verifyAliasSnapshot(structuredClone(snapshot)) === true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
verified = false;
|
|
59
|
+
}
|
|
60
|
+
if (verified)
|
|
61
|
+
return decision(true, 'pinned-compatibility');
|
|
62
|
+
}
|
|
63
|
+
return decision(false, 'alias-snapshot-unverified');
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=compatibility.js.map
|