@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,12 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './manifest.js';
|
|
3
|
+
export * from './gates.js';
|
|
4
|
+
export * from './gate-evidence.js';
|
|
5
|
+
export * from './runner.js';
|
|
6
|
+
export * from './quality.js';
|
|
7
|
+
export * from './drift.js';
|
|
8
|
+
export * from './privacy.js';
|
|
9
|
+
export * from './capture.js';
|
|
10
|
+
export * from './release.js';
|
|
11
|
+
export * from './load.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
3
|
+
import { AdmissionError, DecisionAdmissionRegistry } from '../admission.js';
|
|
4
|
+
export function decisionLoadManifestDigest(bytes) {
|
|
5
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
6
|
+
}
|
|
7
|
+
/** mulberry32: small, seedable, and stable across Node versions. */
|
|
8
|
+
function seeded(seed) {
|
|
9
|
+
let state = seed >>> 0;
|
|
10
|
+
return () => {
|
|
11
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
12
|
+
let value = Math.imul(state ^ (state >>> 15), 1 | state);
|
|
13
|
+
value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
|
|
14
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Seeded bursty Poisson arrivals for the manifest's load, spike, and soak phases. */
|
|
18
|
+
export function decisionLoadArrivals(manifest, windowSeconds) {
|
|
19
|
+
const model = manifest.arrivalModel;
|
|
20
|
+
const random = seeded(model.seed);
|
|
21
|
+
const total = manifest.duration.loadSeconds + manifest.duration.spikeSeconds + manifest.duration.soakSeconds;
|
|
22
|
+
const endMs = Math.min(total, windowSeconds ?? total) * 1000;
|
|
23
|
+
const spikeStart = manifest.duration.loadSeconds * 1000;
|
|
24
|
+
const spikeEnd = spikeStart + model.spikeDurationSeconds * 1000;
|
|
25
|
+
const weights = manifest.principals.reduce((sum, principal) => sum + principal.weight, 0);
|
|
26
|
+
const providers = Object.keys(manifest.providers).sort();
|
|
27
|
+
const arrivals = [];
|
|
28
|
+
let at = 0;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const rate = (at >= spikeStart && at < spikeEnd ? model.spikeMultiplier : 1) * model.averageRequestsPerSecond / 1000;
|
|
31
|
+
at += -Math.log(1 - random()) / rate;
|
|
32
|
+
if (at >= endMs)
|
|
33
|
+
break;
|
|
34
|
+
let pick = random() * weights;
|
|
35
|
+
const principal = manifest.principals.find(candidate => (pick -= candidate.weight) < 0) ?? manifest.principals.at(-1);
|
|
36
|
+
const provider = providers[Math.floor(random() * providers.length)];
|
|
37
|
+
const huge = random() < model.hugeTokenShare;
|
|
38
|
+
const service = model.serviceTimeMs.minimum + Math.floor(random() * (model.serviceTimeMs.maximum - model.serviceTimeMs.minimum + 1));
|
|
39
|
+
const fails = random() < model.retryableFailureRate;
|
|
40
|
+
const cancels = random() < model.cancellationRate;
|
|
41
|
+
const atMs = Math.round(at);
|
|
42
|
+
arrivals.push({ ordinal: arrivals.length, atMs, principalId: principal.id, principalClass: principal.class,
|
|
43
|
+
providerId: provider, tokens: huge ? model.hugeTokens : model.smallTokens, serviceMs: service,
|
|
44
|
+
failsFirstAttempt: fails, cancelAtMs: cancels ? atMs + model.cancelAfterMs : null });
|
|
45
|
+
}
|
|
46
|
+
return arrivals;
|
|
47
|
+
}
|
|
48
|
+
/** Scheduler policy the manifest declares for one principal. */
|
|
49
|
+
export function decisionLoadPolicy(manifest, workspaceId, principalId) {
|
|
50
|
+
const principal = manifest.principals.find(candidate => candidate.id === principalId);
|
|
51
|
+
return {
|
|
52
|
+
enabled: true, profileVersion: manifest.profileVersion,
|
|
53
|
+
workspace: { id: workspaceId, limits: structuredClone(manifest.workspace) },
|
|
54
|
+
principal: { id: principal.id, limits: structuredClone(principal.limits) },
|
|
55
|
+
providers: structuredClone(manifest.providers),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Drives each arrival straight through a registry-owned `DecisionAdmissionController`. */
|
|
59
|
+
export function admissionControllerLoadDriver(manifest, clock, registry = new DecisionAdmissionRegistry()) {
|
|
60
|
+
const workspaceId = 'load-harness';
|
|
61
|
+
for (const principal of manifest.principals)
|
|
62
|
+
registry.register(decisionLoadPolicy(manifest, workspaceId, principal.id), clock.now);
|
|
63
|
+
const controller = registry.controller(workspaceId);
|
|
64
|
+
const driver = async (arrival, signal, context) => {
|
|
65
|
+
const queueDelaysMs = [];
|
|
66
|
+
let attempts = 0;
|
|
67
|
+
for (let retry = 0; retry <= manifest.arrivalModel.maxRetries; retry += 1) {
|
|
68
|
+
attempts += 1;
|
|
69
|
+
let lease;
|
|
70
|
+
try {
|
|
71
|
+
lease = await controller.acquire({ budgetId: `load-${arrival.ordinal}`, principalId: arrival.principalId,
|
|
72
|
+
workspaceId, providerId: arrival.providerId, signal,
|
|
73
|
+
estimate: { tokens: arrival.tokens, attempts: 1, batchSize: 1, items: 1, costUsd: 0, retainedWork: 1 },
|
|
74
|
+
deadlineEpochMs: clock.now() + manifest.workspace.maxQueueWaitMs + arrival.serviceMs });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof AdmissionError))
|
|
78
|
+
throw error;
|
|
79
|
+
controller.releaseBudget(`load-${arrival.ordinal}`);
|
|
80
|
+
return { attempts, outcome: error.evidence.reason === 'cancelled' ? 'cancelled' : 'shed',
|
|
81
|
+
admissionReason: error.evidence.reason, queueDelaysMs, settledAtMs: clock.now() };
|
|
82
|
+
}
|
|
83
|
+
queueDelaysMs.push(lease.evidence.queueDelayMs);
|
|
84
|
+
const dispatched = await context.dispatch(arrival.serviceMs, signal);
|
|
85
|
+
const failed = arrival.failsFirstAttempt && retry === 0;
|
|
86
|
+
lease.release({ success: dispatched === 'completed' && !failed });
|
|
87
|
+
if (dispatched === 'aborted') {
|
|
88
|
+
controller.releaseBudget(`load-${arrival.ordinal}`);
|
|
89
|
+
return { attempts, outcome: 'cancelled', queueDelaysMs, settledAtMs: clock.now() };
|
|
90
|
+
}
|
|
91
|
+
if (!failed) {
|
|
92
|
+
controller.releaseBudget(`load-${arrival.ordinal}`);
|
|
93
|
+
return { attempts, outcome: 'completed', queueDelaysMs, settledAtMs: clock.now() };
|
|
94
|
+
}
|
|
95
|
+
if (retry < manifest.arrivalModel.maxRetries) {
|
|
96
|
+
await new Promise(done => setTimeout(done, manifest.arrivalModel.retryBackoffMs));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
controller.releaseBudget(`load-${arrival.ordinal}`);
|
|
100
|
+
return { attempts, outcome: 'failed', queueDelaysMs, settledAtMs: clock.now() };
|
|
101
|
+
};
|
|
102
|
+
return { driver, sample: () => controller.snapshot() };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Offline load harness. Arrivals are released on a virtual timeline supplied by
|
|
106
|
+
* the caller's fake clock. Deterministic observations are separated from
|
|
107
|
+
* host-dependent memory and CPU measurements.
|
|
108
|
+
*/
|
|
109
|
+
export async function runDecisionLoadHarness(options) {
|
|
110
|
+
const { manifest, clock, driver, sample } = options;
|
|
111
|
+
const arrivals = decisionLoadArrivals(manifest, options.windowSeconds);
|
|
112
|
+
const start = clock.now();
|
|
113
|
+
const threadCpu = process.threadCpuUsage?.bind(process);
|
|
114
|
+
const cpuStart = threadCpu?.();
|
|
115
|
+
const wallStart = process.hrtime.bigint();
|
|
116
|
+
let rss = process.memoryUsage().rss;
|
|
117
|
+
let heap = process.memoryUsage().heapUsed;
|
|
118
|
+
const sampleMemory = () => {
|
|
119
|
+
const usage = process.memoryUsage();
|
|
120
|
+
rss = Math.max(rss, usage.rss);
|
|
121
|
+
heap = Math.max(heap, usage.heapUsed);
|
|
122
|
+
};
|
|
123
|
+
let active = 0;
|
|
124
|
+
let maximumActive = 0;
|
|
125
|
+
let maximumQueued = 0;
|
|
126
|
+
const observe = () => {
|
|
127
|
+
const snapshot = sample();
|
|
128
|
+
maximumQueued = Math.max(maximumQueued, snapshot.queued);
|
|
129
|
+
maximumActive = Math.max(maximumActive, snapshot.active, active);
|
|
130
|
+
};
|
|
131
|
+
const context = {
|
|
132
|
+
clock, manifest,
|
|
133
|
+
dispatch: (serviceMs, signal) => new Promise(resolve => {
|
|
134
|
+
active += 1;
|
|
135
|
+
observe();
|
|
136
|
+
const finish = (result) => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
signal.removeEventListener('abort', aborted);
|
|
139
|
+
active -= 1;
|
|
140
|
+
resolve(result);
|
|
141
|
+
};
|
|
142
|
+
const aborted = () => finish('aborted');
|
|
143
|
+
const timer = setTimeout(() => finish('completed'), serviceMs);
|
|
144
|
+
if (signal.aborted)
|
|
145
|
+
aborted();
|
|
146
|
+
else
|
|
147
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
const outcomes = [];
|
|
151
|
+
for (const arrival of arrivals) {
|
|
152
|
+
const due = start + arrival.atMs - clock.now();
|
|
153
|
+
if (due > 0)
|
|
154
|
+
await clock.advance(due);
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const cancellation = { abortedAtMs: null };
|
|
157
|
+
if (arrival.cancelAtMs !== null) {
|
|
158
|
+
setTimeout(() => { cancellation.abortedAtMs = clock.now(); controller.abort(); }, arrival.cancelAtMs - arrival.atMs);
|
|
159
|
+
}
|
|
160
|
+
outcomes.push(driver(arrival, controller.signal, context)
|
|
161
|
+
.then(result => ({ arrival, result, abortedAtMs: cancellation.abortedAtMs })));
|
|
162
|
+
observe();
|
|
163
|
+
if (arrival.ordinal % 500 === 0)
|
|
164
|
+
sampleMemory();
|
|
165
|
+
}
|
|
166
|
+
let settled = false;
|
|
167
|
+
const all = Promise.all(outcomes).then(values => { settled = true; return values; });
|
|
168
|
+
const drainStep = manifest.arrivalModel.serviceTimeMs.maximum + manifest.arrivalModel.retryBackoffMs;
|
|
169
|
+
while (!settled)
|
|
170
|
+
await clock.advance(drainStep);
|
|
171
|
+
const results = await all;
|
|
172
|
+
sampleMemory();
|
|
173
|
+
const threadUsage = cpuStart && threadCpu ? threadCpu(cpuStart) : undefined;
|
|
174
|
+
const cpuMicros = threadUsage ? threadUsage.user + threadUsage.system : Number(process.hrtime.bigint() - wallStart) / 1000;
|
|
175
|
+
const virtualDurationMs = Math.max(1, clock.now() - start);
|
|
176
|
+
const shedReasons = {};
|
|
177
|
+
let attempts = 0;
|
|
178
|
+
let completed = 0;
|
|
179
|
+
let failed = 0;
|
|
180
|
+
let shed = 0;
|
|
181
|
+
let cancelled = 0;
|
|
182
|
+
let laneWait = 0;
|
|
183
|
+
let cancellationLatency = 0;
|
|
184
|
+
const admitted = { quiet: 0, noisy: 0 };
|
|
185
|
+
const submitted = { quiet: 0, noisy: 0 };
|
|
186
|
+
for (const { arrival, result, abortedAtMs } of results) {
|
|
187
|
+
attempts += result.attempts;
|
|
188
|
+
submitted[arrival.principalClass] += 1;
|
|
189
|
+
if (result.queueDelaysMs.length)
|
|
190
|
+
admitted[arrival.principalClass] += 1;
|
|
191
|
+
for (const delay of result.queueDelaysMs)
|
|
192
|
+
laneWait = Math.max(laneWait, delay);
|
|
193
|
+
if (result.outcome === 'completed')
|
|
194
|
+
completed += 1;
|
|
195
|
+
else if (result.outcome === 'failed')
|
|
196
|
+
failed += 1;
|
|
197
|
+
else if (result.outcome === 'cancelled')
|
|
198
|
+
cancelled += 1;
|
|
199
|
+
else {
|
|
200
|
+
shed += 1;
|
|
201
|
+
shedReasons[result.admissionReason ?? 'unknown'] = (shedReasons[result.admissionReason ?? 'unknown'] ?? 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
if (result.outcome === 'cancelled' && abortedAtMs !== null) {
|
|
204
|
+
cancellationLatency = Math.max(cancellationLatency, result.settledAtMs - abortedAtMs);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const ratio = (value, total) => total ? Number((value / total).toFixed(6)) : 1;
|
|
208
|
+
return {
|
|
209
|
+
observations: {
|
|
210
|
+
arrivals: arrivals.length, attempts, completed, failed, shed, cancelled,
|
|
211
|
+
shedReasons: Object.fromEntries(Object.entries(shedReasons).sort(([left], [right]) => left.localeCompare(right))),
|
|
212
|
+
maximumActiveCalls: maximumActive, maximumQueuedCalls: maximumQueued, maximumEligibleLaneWaitMs: laneWait,
|
|
213
|
+
retryAmplificationRatio: ratio(attempts, arrivals.length), maximumCancellationLatencyMs: cancellationLatency,
|
|
214
|
+
quietPrincipalAdmissionRatio: ratio(admitted.quiet, submitted.quiet),
|
|
215
|
+
noisyPrincipalAdmissionRatio: ratio(admitted.noisy, submitted.noisy),
|
|
216
|
+
virtualDurationMs,
|
|
217
|
+
},
|
|
218
|
+
resources: {
|
|
219
|
+
heapUsedMiB: Number((heap / 1_048_576).toFixed(1)),
|
|
220
|
+
residentMemoryMiB: Number((rss / 1_048_576).toFixed(1)),
|
|
221
|
+
cpuPercentOneCore: Number(((cpuMicros / 1000) / virtualDurationMs * 100).toFixed(3)),
|
|
222
|
+
cpuSource: threadUsage ? 'thread' : 'wall-clock-upper-bound',
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function compareDecisionLoadBounds(manifest, observations, resources) {
|
|
227
|
+
const bounds = manifest.bounds;
|
|
228
|
+
const atMost = (bound, observed) => ({ bound, limit: bounds[bound], observed, pass: observed <= bounds[bound] });
|
|
229
|
+
return [
|
|
230
|
+
atMost('maximumActiveCalls', observations.maximumActiveCalls),
|
|
231
|
+
atMost('maximumQueuedCalls', observations.maximumQueuedCalls),
|
|
232
|
+
atMost('maximumHeapUsedMiB', resources.heapUsedMiB),
|
|
233
|
+
atMost('maximumCpuPercentOneCore', resources.cpuPercentOneCore),
|
|
234
|
+
atMost('maximumEligibleLaneWaitMs', observations.maximumEligibleLaneWaitMs),
|
|
235
|
+
atMost('maximumRetryAmplificationRatio', observations.retryAmplificationRatio),
|
|
236
|
+
atMost('maximumCancellationLatencyMs', observations.maximumCancellationLatencyMs),
|
|
237
|
+
{ bound: 'minimumQuietPrincipalAdmissionRatio', limit: bounds.minimumQuietPrincipalAdmissionRatio,
|
|
238
|
+
observed: observations.quietPrincipalAdmissionRatio,
|
|
239
|
+
pass: observations.quietPrincipalAdmissionRatio >= bounds.minimumQuietPrincipalAdmissionRatio },
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
/** Build a digest-bound result record. The manifest digest must be computed from its exact bytes. */
|
|
243
|
+
export function decisionLoadResultRecord(input) {
|
|
244
|
+
const comparisons = compareDecisionLoadBounds(input.manifest, input.observations, input.resources);
|
|
245
|
+
const body = {
|
|
246
|
+
schema: 'decision-load-result/v1',
|
|
247
|
+
manifest: { schema: input.manifest.schema, version: input.manifest.version, digest: input.manifestDigest },
|
|
248
|
+
mode: input.manifest.mode, target: input.target, window: { seconds: input.windowSeconds },
|
|
249
|
+
environment: { node: process.version, platform: process.platform, arch: process.arch },
|
|
250
|
+
observations: input.observations, resources: input.resources, comparisons,
|
|
251
|
+
passed: comparisons.every(comparison => comparison.pass),
|
|
252
|
+
};
|
|
253
|
+
return { ...body, digest: decisionLoadManifestDigest(canonicalJson(body)) };
|
|
254
|
+
}
|
|
255
|
+
export function verifyDecisionLoadResultRecord(record, manifestDigest) {
|
|
256
|
+
const { digest, ...body } = record;
|
|
257
|
+
return record.schema === 'decision-load-result/v1' && record.manifest.digest === manifestDigest
|
|
258
|
+
&& digest === decisionLoadManifestDigest(canonicalJson(body));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* G5 evidence flags. Offline fake-provider records can prove the harness and
|
|
262
|
+
* the admission path, but only a passing staged-provider record bound to its
|
|
263
|
+
* manifest may set `load-manifest-qualified`.
|
|
264
|
+
*/
|
|
265
|
+
export function decisionLoadEvidenceFlags(records, manifestDigest) {
|
|
266
|
+
const bound = records.filter(record => verifyDecisionLoadResultRecord(record, manifestDigest));
|
|
267
|
+
const offline = bound.filter(record => record.mode === 'offline-fake-provider');
|
|
268
|
+
return {
|
|
269
|
+
'load-manifest-offline-passed': offline.length > 0 && offline.length === records.filter(record => record.mode === 'offline-fake-provider').length
|
|
270
|
+
&& offline.every(record => record.passed),
|
|
271
|
+
'load-manifest-qualified': bound.some(record => record.mode === 'staged-provider' && record.passed)
|
|
272
|
+
&& bound.length === records.length && bound.every(record => record.passed),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const ids = (prefix, count) => Array.from({ length: count }, (_, index) => `${prefix}${String(index + 1).padStart(2, '0')}`);
|
|
3
|
+
export const REQUIRED_BASELINE_CASE_IDS = ids('C', 42);
|
|
4
|
+
export const REQUIRED_VENDOR_CASE_IDS = ids('TV', 25);
|
|
5
|
+
export function expectedQualificationCaseIds() {
|
|
6
|
+
return [...REQUIRED_BASELINE_CASE_IDS, ...REQUIRED_VENDOR_CASE_IDS];
|
|
7
|
+
}
|
|
8
|
+
export function validateCaseInventory(cases) {
|
|
9
|
+
const actual = new Set();
|
|
10
|
+
const errors = [];
|
|
11
|
+
for (const item of cases) {
|
|
12
|
+
if (actual.has(item.id))
|
|
13
|
+
errors.push(`duplicate:${item.id}`);
|
|
14
|
+
actual.add(item.id);
|
|
15
|
+
}
|
|
16
|
+
for (const id of expectedQualificationCaseIds()) {
|
|
17
|
+
if (!actual.has(id))
|
|
18
|
+
errors.push(`missing:${id}`);
|
|
19
|
+
}
|
|
20
|
+
for (const id of actual) {
|
|
21
|
+
if (!expectedQualificationCaseIds().includes(id))
|
|
22
|
+
errors.push(`unexpected:${id}`);
|
|
23
|
+
}
|
|
24
|
+
return errors.sort();
|
|
25
|
+
}
|
|
26
|
+
export function stableManifestDigest(manifest) {
|
|
27
|
+
const canonical = JSON.stringify({
|
|
28
|
+
...manifest,
|
|
29
|
+
cases: [...manifest.cases].sort((a, b) => a.id.localeCompare(b.id)),
|
|
30
|
+
evidence: [...manifest.evidence].sort((a, b) => a.caseId.localeCompare(b.caseId)),
|
|
31
|
+
evidenceFlags: Object.fromEntries(Object.entries(manifest.evidenceFlags).sort(([a], [b]) => a.localeCompare(b))),
|
|
32
|
+
...(manifest.gateArtifacts ? { gateArtifacts: Object.fromEntries(Object.entries(manifest.gateArtifacts)
|
|
33
|
+
.sort(([a], [b]) => a.localeCompare(b))) } : {}),
|
|
34
|
+
});
|
|
35
|
+
return `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Qualification canary scan of sanitized text captures. Never put the canary value in diagnostics. */
|
|
2
|
+
export const QUALIFICATION_PRIVACY_SURFACES = [
|
|
3
|
+
'stdout', 'stderr', 'test-report', 'trace', 'receipt', 'snapshot', 'export', 'thrown-error',
|
|
4
|
+
// Host-visible runtime activity: evidence callbacks, admission/prefix evidence and audit records.
|
|
5
|
+
'activity-record',
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* Scans raw and JSON-escaped spellings on each observed surface. An omitted
|
|
9
|
+
* surface is not assumed empty, and an empty canary list cannot prove privacy.
|
|
10
|
+
* The caller must collect streams/errors at their actual source; this function
|
|
11
|
+
* cannot infer that a supplied capture covers the whole process lifetime.
|
|
12
|
+
*/
|
|
13
|
+
export function scanQualificationPrivacy(captures, canaries) {
|
|
14
|
+
const missing = [];
|
|
15
|
+
const affected = [];
|
|
16
|
+
if (!canaries.length || canaries.some(value => typeof value !== 'string' || !value.length)) {
|
|
17
|
+
throw new Error('qualification privacy scan requires nonempty synthetic canaries');
|
|
18
|
+
}
|
|
19
|
+
const known = new Set(QUALIFICATION_PRIVACY_SURFACES);
|
|
20
|
+
const bySurface = new Map();
|
|
21
|
+
for (const item of captures) {
|
|
22
|
+
if (!known.has(item.surface) || bySurface.has(item.surface)) {
|
|
23
|
+
throw new Error('qualification privacy capture has unknown or duplicate surface');
|
|
24
|
+
}
|
|
25
|
+
if (typeof item.content !== 'string' && !(item.content instanceof Uint8Array)) {
|
|
26
|
+
throw new Error('qualification privacy capture requires text or bytes');
|
|
27
|
+
}
|
|
28
|
+
bySurface.set(item.surface, typeof item.content === 'string' ? item.content : new TextDecoder().decode(item.content));
|
|
29
|
+
}
|
|
30
|
+
for (const surface of QUALIFICATION_PRIVACY_SURFACES) {
|
|
31
|
+
const content = bySurface.get(surface);
|
|
32
|
+
if (content === undefined) {
|
|
33
|
+
missing.push(surface);
|
|
34
|
+
}
|
|
35
|
+
else if (canaries.some(canary => content.includes(canary) || content.includes(JSON.stringify(canary).slice(1, -1)))) {
|
|
36
|
+
affected.push(surface);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { clean: !missing.length && !affected.length, missing, affected };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=privacy.js.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const sha256 = (value) => `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
|
|
3
|
+
/** Freeze thresholds and all three label sets before acquiring test predictions. */
|
|
4
|
+
export function freezeBinaryBenchmarkPlan(splits, labels, limits) {
|
|
5
|
+
verifyQualificationSplits(splits);
|
|
6
|
+
const ids = splits.flatMap(split => split.ids).sort();
|
|
7
|
+
if (labels.length !== ids.length || new Set(labels.map(label => label.id)).size !== ids.length
|
|
8
|
+
|| labels.some(label => !ids.includes(label.id) || ![0, 1].includes(label.label) || !label.slice?.trim())
|
|
9
|
+
|| !Number.isSafeInteger(limits.minimumOverallN) || limits.minimumOverallN < 1
|
|
10
|
+
|| !Number.isSafeInteger(limits.minimumSliceN) || limits.minimumSliceN < 1
|
|
11
|
+
|| [limits.maximumSelectiveRisk, limits.maximumReviewRate, limits.maximumBrier]
|
|
12
|
+
.some(value => !Number.isFinite(value) || value < 0 || value > 1)) {
|
|
13
|
+
throw new Error('invalid preregistered benchmark plan');
|
|
14
|
+
}
|
|
15
|
+
const fields = {
|
|
16
|
+
schemaVersion: 'decision-binary-benchmark-plan/v1',
|
|
17
|
+
splits: ['tuning', 'calibration', 'test'].map(name => splits.find(split => split.name === name)),
|
|
18
|
+
datasetDigest: sha256([...labels].sort((a, b) => a.id.localeCompare(b.id))),
|
|
19
|
+
...limits,
|
|
20
|
+
};
|
|
21
|
+
return { ...fields, digest: sha256(fields) };
|
|
22
|
+
}
|
|
23
|
+
/** A separately anchored digest is required: a caller-created plan cannot attest its own preregistration. */
|
|
24
|
+
export function evaluatePreregisteredBinaryBenchmark(plan, trustedPlanDigest, labels, samples) {
|
|
25
|
+
const { digest, ...fields } = plan;
|
|
26
|
+
if (digest !== trustedPlanDigest || digest !== sha256(fields)
|
|
27
|
+
|| freezeBinaryBenchmarkPlan(plan.splits, labels, {
|
|
28
|
+
minimumOverallN: plan.minimumOverallN, minimumSliceN: plan.minimumSliceN,
|
|
29
|
+
maximumSelectiveRisk: plan.maximumSelectiveRisk, maximumReviewRate: plan.maximumReviewRate,
|
|
30
|
+
maximumBrier: plan.maximumBrier,
|
|
31
|
+
}).digest !== digest)
|
|
32
|
+
throw new Error('benchmark preregistration or dataset mismatch');
|
|
33
|
+
const testLabels = new Map(labels.filter(label => plan.splits.find(split => split.name === 'test').ids.includes(label.id))
|
|
34
|
+
.map(label => [label.id, label]));
|
|
35
|
+
if (samples.some(sample => sample.label !== testLabels.get(sample.id)?.label || sample.slice !== testLabels.get(sample.id)?.slice)) {
|
|
36
|
+
throw new Error('held-out label or slice mismatch');
|
|
37
|
+
}
|
|
38
|
+
const metrics = evaluateBinaryHeldout(plan.splits, samples);
|
|
39
|
+
const insufficient = metrics.overall.sampleN < plan.minimumOverallN
|
|
40
|
+
|| Object.values(metrics.slices).some(slice => slice.sampleN < plan.minimumSliceN || slice.selectiveRisk === null);
|
|
41
|
+
const reasons = [];
|
|
42
|
+
if (insufficient)
|
|
43
|
+
reasons.push('insufficient-heldout-evidence');
|
|
44
|
+
if (metrics.overall.selectiveRisk === null)
|
|
45
|
+
reasons.push('zero-accepted-samples');
|
|
46
|
+
if (metrics.overall.selectiveRisk !== null && metrics.overall.selectiveRisk > plan.maximumSelectiveRisk)
|
|
47
|
+
reasons.push('selective-risk');
|
|
48
|
+
if (metrics.overall.reviewRate > plan.maximumReviewRate)
|
|
49
|
+
reasons.push('review-rate');
|
|
50
|
+
if (metrics.overall.brier > plan.maximumBrier)
|
|
51
|
+
reasons.push('brier');
|
|
52
|
+
const exceeded = reasons.some(reason => reason === 'selective-risk' || reason === 'review-rate' || reason === 'brier');
|
|
53
|
+
return { decision: exceeded ? 'fail' : insufficient || metrics.overall.selectiveRisk === null ? 'insufficient-evidence'
|
|
54
|
+
: 'pass', reasons, metrics };
|
|
55
|
+
}
|
|
56
|
+
function digestIds(ids) {
|
|
57
|
+
return `sha256:${createHash('sha256').update(JSON.stringify([...ids].sort())).digest('hex')}`;
|
|
58
|
+
}
|
|
59
|
+
/** Hash the exact membership set; duplicate, empty and overlapping IDs fail closed. */
|
|
60
|
+
export function freezeQualificationSplit(name, ids) {
|
|
61
|
+
if (!ids.length || ids.some(id => !id.trim()) || new Set(ids).size !== ids.length) {
|
|
62
|
+
throw new Error('qualification split requires unique nonempty IDs');
|
|
63
|
+
}
|
|
64
|
+
return { name, ids: [...ids].sort(), digest: digestIds(ids) };
|
|
65
|
+
}
|
|
66
|
+
export function verifyQualificationSplits(splits) {
|
|
67
|
+
if (splits.length !== 3 || new Set(splits.map(split => split.name)).size !== 3
|
|
68
|
+
|| splits.some(split => !['tuning', 'calibration', 'test'].includes(split.name))) {
|
|
69
|
+
throw new Error('qualification requires tuning, calibration and test splits');
|
|
70
|
+
}
|
|
71
|
+
const all = new Set();
|
|
72
|
+
for (const split of splits) {
|
|
73
|
+
if (!split.ids.length || split.ids.some(id => typeof id !== 'string' || !id.trim())
|
|
74
|
+
|| new Set(split.ids).size !== split.ids.length || split.digest !== digestIds(split.ids)) {
|
|
75
|
+
throw new Error('qualification split digest or membership mismatch');
|
|
76
|
+
}
|
|
77
|
+
for (const id of split.ids) {
|
|
78
|
+
if (all.has(id))
|
|
79
|
+
throw new Error('qualification splits overlap');
|
|
80
|
+
all.add(id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Refuses to score unregistered or non-held-out samples. */
|
|
85
|
+
export function evaluateBinaryHeldout(splits, samples) {
|
|
86
|
+
verifyQualificationSplits(splits);
|
|
87
|
+
const test = splits.find(split => split.name === 'test');
|
|
88
|
+
if (samples.length !== test.ids.length || new Set(samples.map(sample => sample.id)).size !== samples.length
|
|
89
|
+
|| samples.some(sample => !test.ids.includes(sample.id))) {
|
|
90
|
+
throw new Error('held-out sample membership mismatch');
|
|
91
|
+
}
|
|
92
|
+
for (const sample of samples) {
|
|
93
|
+
if (typeof sample.slice !== 'string' || !sample.slice.trim() || typeof sample.accepted !== 'boolean'
|
|
94
|
+
|| ![0, 1].includes(sample.label)
|
|
95
|
+
|| !Number.isFinite(sample.probability) || sample.probability < 0 || sample.probability > 1
|
|
96
|
+
|| !Number.isFinite(sample.latencyMs) || sample.latencyMs < 0
|
|
97
|
+
|| [sample.calls, sample.retries, sample.fallbacks].some(n => !Number.isSafeInteger(n) || n < 0)
|
|
98
|
+
|| [sample.inputTokens, sample.outputTokens].some(n => n !== null && (!Number.isSafeInteger(n) || n < 0))
|
|
99
|
+
|| (sample.costUsd !== null && (!Number.isFinite(sample.costUsd) || sample.costUsd < 0))) {
|
|
100
|
+
throw new Error('invalid held-out sample');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const groups = new Map();
|
|
104
|
+
for (const sample of samples)
|
|
105
|
+
groups.set(sample.slice, [...(groups.get(sample.slice) ?? []), sample]);
|
|
106
|
+
return {
|
|
107
|
+
overall: scoreBinary(samples),
|
|
108
|
+
slices: Object.fromEntries([...groups].sort(([a], [b]) => a.localeCompare(b)).map(([key, group]) => [key, scoreBinary(group)])),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Mean absolute level error and exact match, scored on a frozen held-out split. */
|
|
112
|
+
export function evaluateOrdinalHeldout(splits, samples) {
|
|
113
|
+
verifyQualificationSplits(splits);
|
|
114
|
+
const testIds = splits.find(split => split.name === 'test').ids;
|
|
115
|
+
if (samples.length !== testIds.length || new Set(samples.map(s => s.id)).size !== samples.length
|
|
116
|
+
|| samples.some(s => !testIds.includes(s.id)))
|
|
117
|
+
throw new Error('held-out ordinal membership mismatch');
|
|
118
|
+
if (samples.some(s => !Number.isSafeInteger(s.levels) || s.levels < 2
|
|
119
|
+
|| !Number.isSafeInteger(s.trueLevel) || !Number.isSafeInteger(s.predictedLevel)
|
|
120
|
+
|| s.trueLevel < 0 || s.trueLevel >= s.levels || s.predictedLevel < 0 || s.predictedLevel >= s.levels)) {
|
|
121
|
+
throw new Error('invalid held-out ordinal sample');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
sampleN: samples.length,
|
|
125
|
+
exactRate: samples.filter(s => s.trueLevel === s.predictedLevel).length / samples.length,
|
|
126
|
+
meanAbsoluteError: samples.reduce((sum, s) => sum + Math.abs(s.trueLevel - s.predictedLevel), 0) / samples.length,
|
|
127
|
+
normalizedAbsoluteError: samples.reduce((sum, s) => sum + Math.abs(s.trueLevel - s.predictedLevel) / (s.levels - 1), 0) / samples.length,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Counts gold-comparable option pairs; ties in the prediction are errors, not wins. */
|
|
131
|
+
export function evaluateRankingHeldout(splits, samples) {
|
|
132
|
+
verifyQualificationSplits(splits);
|
|
133
|
+
const testIds = splits.find(split => split.name === 'test').ids;
|
|
134
|
+
if (samples.length !== testIds.length || new Set(samples.map(s => s.id)).size !== samples.length
|
|
135
|
+
|| samples.some(s => !testIds.includes(s.id)))
|
|
136
|
+
throw new Error('held-out ranking membership mismatch');
|
|
137
|
+
let pairs = 0;
|
|
138
|
+
let correct = 0;
|
|
139
|
+
for (const sample of samples) {
|
|
140
|
+
const options = Object.keys(sample.gold).sort();
|
|
141
|
+
if (options.length < 2 || options.length !== Object.keys(sample.predicted).length
|
|
142
|
+
|| options.some(option => !Object.hasOwn(sample.predicted, option))) {
|
|
143
|
+
throw new Error('ranking option mismatch');
|
|
144
|
+
}
|
|
145
|
+
for (const option of options) {
|
|
146
|
+
if (!Number.isFinite(sample.gold[option]) || !Number.isFinite(sample.predicted[option])) {
|
|
147
|
+
throw new Error('invalid ranking score');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (let i = 0; i < options.length; i++)
|
|
151
|
+
for (let j = i + 1; j < options.length; j++) {
|
|
152
|
+
const goldDifference = sample.gold[options[i]] - sample.gold[options[j]];
|
|
153
|
+
if (goldDifference === 0)
|
|
154
|
+
continue;
|
|
155
|
+
pairs++;
|
|
156
|
+
const predictedDifference = sample.predicted[options[i]] - sample.predicted[options[j]];
|
|
157
|
+
if (Math.sign(predictedDifference) === Math.sign(goldDifference))
|
|
158
|
+
correct++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { sampleN: samples.length, comparablePairs: pairs, concordance: pairs ? correct / pairs : null };
|
|
162
|
+
}
|
|
163
|
+
/** Measures categorical movement without claiming that either output is correct. */
|
|
164
|
+
export function measurePairedMovement(pairs) {
|
|
165
|
+
if (!pairs.length || new Set(pairs.map(pair => pair.id)).size !== pairs.length
|
|
166
|
+
|| pairs.some(pair => !pair.id.trim() || !pair.control.trim() || !pair.observed.trim())) {
|
|
167
|
+
throw new Error('paired qualification requires unique nonempty IDs and outcomes');
|
|
168
|
+
}
|
|
169
|
+
const changedN = pairs.filter(pair => pair.control !== pair.observed).length;
|
|
170
|
+
const [low, high] = wilson95(changedN, pairs.length);
|
|
171
|
+
return { sampleN: pairs.length, changedN, changedRate: changedN / pairs.length, changedWilson95: [low, high] };
|
|
172
|
+
}
|
|
173
|
+
function wilson95(errors, n) {
|
|
174
|
+
// 95% normal quantile; finite-sample Wilson interval for binomial events.
|
|
175
|
+
const z = 1.959963984540054;
|
|
176
|
+
const rate = errors / n;
|
|
177
|
+
const denominator = 1 + z * z / n;
|
|
178
|
+
const center = (rate + z * z / (2 * n)) / denominator;
|
|
179
|
+
const margin = z * Math.sqrt(rate * (1 - rate) / n + z * z / (4 * n * n)) / denominator;
|
|
180
|
+
return [Math.max(0, center - margin), Math.min(1, center + margin)];
|
|
181
|
+
}
|
|
182
|
+
function scoreBinary(samples) {
|
|
183
|
+
const n = samples.length;
|
|
184
|
+
const errors = samples.filter(s => (s.probability >= 0.5 ? 1 : 0) !== s.label).length;
|
|
185
|
+
const accepted = samples.filter(s => s.accepted);
|
|
186
|
+
const acceptedErrors = accepted.filter(s => (s.probability >= 0.5 ? 1 : 0) !== s.label).length;
|
|
187
|
+
const rate = errors / n;
|
|
188
|
+
const sorted = samples.map(s => s.latencyMs).sort((a, b) => a - b);
|
|
189
|
+
const quantile = (q) => sorted[Math.ceil(q * n) - 1];
|
|
190
|
+
// Deciles are a reporting convention, not a calibrated decision threshold.
|
|
191
|
+
const buckets = Array.from({ length: 10 }, () => []);
|
|
192
|
+
for (const sample of samples)
|
|
193
|
+
buckets[Math.min(9, Math.floor(sample.probability * 10))].push(sample);
|
|
194
|
+
const sumKnown = (key) => samples.some(s => s[key] === null) ? null : samples.reduce((sum, s) => sum + (s[key] ?? 0), 0);
|
|
195
|
+
return {
|
|
196
|
+
sampleN: n, errorRate: rate,
|
|
197
|
+
brier: samples.reduce((sum, s) => sum + (s.probability - s.label) ** 2, 0) / n,
|
|
198
|
+
logLoss: samples.reduce((sum, s) => sum - (s.label ? Math.log(Math.max(s.probability, Number.EPSILON))
|
|
199
|
+
: Math.log(Math.max(1 - s.probability, Number.EPSILON))), 0) / n,
|
|
200
|
+
expectedCalibrationError: buckets.reduce((sum, bucket) => bucket.length ? sum + bucket.length / n
|
|
201
|
+
* Math.abs(bucket.reduce((a, s) => a + s.probability, 0) / bucket.length
|
|
202
|
+
- bucket.reduce((a, s) => a + s.label, 0) / bucket.length) : sum, 0),
|
|
203
|
+
coverage: accepted.length / n, selectiveRisk: accepted.length ? acceptedErrors / accepted.length : null,
|
|
204
|
+
reviewRate: (n - accepted.length) / n,
|
|
205
|
+
errorWilson95: wilson95(errors, n),
|
|
206
|
+
latencyMs: { p50: quantile(0.5), p95: quantile(0.95), p99: quantile(0.99) },
|
|
207
|
+
inputTokens: sumKnown('inputTokens'), outputTokens: sumKnown('outputTokens'), costUsd: sumKnown('costUsd'),
|
|
208
|
+
calls: samples.reduce((sum, s) => sum + s.calls, 0),
|
|
209
|
+
retries: samples.reduce((sum, s) => sum + s.retries, 0),
|
|
210
|
+
fallbacks: samples.reduce((sum, s) => sum + s.fallbacks, 0),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=quality.js.map
|