@aiwg/cli 2026.9.20 → 2026.9.24
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/cli.js +23 -18
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
- package/dist/src/artifacts/query-engine.js +14 -4
- 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 +282 -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,316 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
8
|
+
import { admitEntry } from '../entry.js';
|
|
9
|
+
/** Pure D17 contract validation. Nothing here resolves credentials, calls an adapter or opens a transport. */
|
|
10
|
+
export class EnsembleContractError extends Error {
|
|
11
|
+
layer;
|
|
12
|
+
details;
|
|
13
|
+
constructor(message, layer = 'semantic', details = []) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.layer = layer;
|
|
16
|
+
this.details = details;
|
|
17
|
+
this.name = 'EnsembleContractError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const ENSEMBLE_SCHEMA_FILES = {
|
|
21
|
+
policy: 'DecisionEnsemblePolicy.v1.schema.json',
|
|
22
|
+
championChallenger: 'DecisionChampionChallenger.v1.schema.json',
|
|
23
|
+
driftResponse: 'DecisionDriftResponse.v1.schema.json',
|
|
24
|
+
aggregate: 'DecisionEnsembleAggregate.v1.schema.json',
|
|
25
|
+
integrityReport: 'DecisionEnsembleIntegrityReport.v1.schema.json',
|
|
26
|
+
};
|
|
27
|
+
/** Versioned algorithm and metric compatibility. An unknown version is rejected, never approximated. */
|
|
28
|
+
export const ENSEMBLE_AGGREGATION_PRIMITIVES = {
|
|
29
|
+
'majority-v1': ['choice', 'truth-probability'],
|
|
30
|
+
'mean-probability-v1': ['choice', 'truth-probability'],
|
|
31
|
+
'score-distribution-mean-v1': ['ordinal-score'],
|
|
32
|
+
'score-median-v1': ['ordinal-score'],
|
|
33
|
+
};
|
|
34
|
+
export const ENSEMBLE_DISAGREEMENT_PRIMITIVES = {
|
|
35
|
+
'vote-share-v1': ['choice', 'truth-probability'],
|
|
36
|
+
'normalized-entropy-v1': ['choice', 'truth-probability'],
|
|
37
|
+
'jensen-shannon-v1': ['choice', 'truth-probability'],
|
|
38
|
+
'score-dispersion-v1': ['ordinal-score'],
|
|
39
|
+
};
|
|
40
|
+
/** Algorithms/metrics that combine numeric uncertainty require one shared uncertainty profile. */
|
|
41
|
+
const UNCERTAINTY_COMBINING = new Set(['mean-probability-v1', 'jensen-shannon-v1']);
|
|
42
|
+
export const REQUIRED_PAIRED_METRICS = ['quality', 'calibration', 'risk-coverage', 'abstention', 'latency', 'tokens', 'cost', 'slice'];
|
|
43
|
+
const CEILING_FIELDS = ['members', 'attempts', 'deadlineMs', 'tokens', 'costMicros', 'concurrency', 'fallbackDepth'];
|
|
44
|
+
const GATE_RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
|
|
45
|
+
let validators = null;
|
|
46
|
+
function schemaValidator(kind) {
|
|
47
|
+
if (!validators) {
|
|
48
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
const dir = [resolve(here, '../../../schemas/decision'), resolve(here, '../../../../schemas/decision')]
|
|
50
|
+
.find(path => existsSync(resolve(path, ENSEMBLE_SCHEMA_FILES.policy)));
|
|
51
|
+
if (!dir)
|
|
52
|
+
throw new Error('Decision ensemble schema directory is unavailable');
|
|
53
|
+
const ajv = new Ajv2020({ strict: true, allErrors: true });
|
|
54
|
+
addFormats(ajv);
|
|
55
|
+
validators = new Map(Object.keys(ENSEMBLE_SCHEMA_FILES).map(name => [name,
|
|
56
|
+
ajv.compile(JSON.parse(readFileSync(resolve(dir, ENSEMBLE_SCHEMA_FILES[name]), 'utf8')))]));
|
|
57
|
+
}
|
|
58
|
+
return validators.get(kind);
|
|
59
|
+
}
|
|
60
|
+
/** Admission then JSON Schema; the semantic layer runs only on schema-valid values. */
|
|
61
|
+
export function checkEnsembleSchema(kind, value) {
|
|
62
|
+
try {
|
|
63
|
+
admitEntry(value);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new EnsembleContractError(`${kind} admission denied`, 'admission');
|
|
67
|
+
}
|
|
68
|
+
const check = schemaValidator(kind);
|
|
69
|
+
if (!check(value)) {
|
|
70
|
+
throw new EnsembleContractError(`${kind} does not match its v1 schema`, 'schema', (check.errors ?? []).map(error => `${error.instancePath || '/'} ${error.message ?? 'invalid'}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function ensembleContractDigest(value) {
|
|
74
|
+
return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
|
|
75
|
+
}
|
|
76
|
+
/** Order-independent digest a preregistration pins before any held-out access. */
|
|
77
|
+
export function pairedThresholdsDigest(metrics) {
|
|
78
|
+
return ensembleContractDigest([...metrics].sort((a, b) => compareEnsembleKeys(a.metric, b.metric)));
|
|
79
|
+
}
|
|
80
|
+
export function driftThresholdsDigest(rules) {
|
|
81
|
+
return ensembleContractDigest([...rules].sort((a, b) => compareEnsembleKeys(a.id, b.id)));
|
|
82
|
+
}
|
|
83
|
+
/** Locale-independent code-unit ordering; the canonical order for ties and member lists. */
|
|
84
|
+
export function compareEnsembleKeys(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
|
|
85
|
+
function semantic(problems, message) {
|
|
86
|
+
if (problems.length)
|
|
87
|
+
throw new EnsembleContractError(`${message}: ${problems[0]}`, 'semantic', problems);
|
|
88
|
+
}
|
|
89
|
+
const time = (value) => Date.parse(value);
|
|
90
|
+
/** Refuses incompatible members, primitives, uncertainty semantics, calibration, capabilities or budgets. */
|
|
91
|
+
export function validateEnsemblePolicy(value, options = {}) {
|
|
92
|
+
checkEnsembleSchema('policy', value);
|
|
93
|
+
const policy = value;
|
|
94
|
+
const problems = [];
|
|
95
|
+
const { algorithm } = policy.aggregation;
|
|
96
|
+
const { metric } = policy.disagreement;
|
|
97
|
+
if (!ENSEMBLE_AGGREGATION_PRIMITIVES[algorithm].includes(policy.primitive))
|
|
98
|
+
problems.push(`aggregation ${algorithm} is not defined for ${policy.primitive}`);
|
|
99
|
+
if (!ENSEMBLE_DISAGREEMENT_PRIMITIVES[metric].includes(policy.primitive))
|
|
100
|
+
problems.push(`disagreement ${metric} is not defined for ${policy.primitive}`);
|
|
101
|
+
const ids = new Set();
|
|
102
|
+
const identities = new Set();
|
|
103
|
+
const combining = UNCERTAINTY_COMBINING.has(algorithm) || UNCERTAINTY_COMBINING.has(metric);
|
|
104
|
+
for (const member of policy.members) {
|
|
105
|
+
const at = `member ${member.id}`;
|
|
106
|
+
if (ids.has(member.id))
|
|
107
|
+
problems.push(`${at} is duplicated`);
|
|
108
|
+
ids.add(member.id);
|
|
109
|
+
if (member.primitive !== policy.primitive)
|
|
110
|
+
problems.push(`${at} primitive mismatch: ${member.primitive} is not ${policy.primitive}`);
|
|
111
|
+
if (canonicalJson(member.definition) !== canonicalJson(policy.definition))
|
|
112
|
+
problems.push(`${at} definition pin mismatch`);
|
|
113
|
+
if (!policy.compatibleUncertaintyProfiles.includes(member.uncertaintyProfile))
|
|
114
|
+
problems.push(`${at} uncertainty profile ${member.uncertaintyProfile} is not compatible`);
|
|
115
|
+
if (combining && member.uncertaintyProfile !== policy.members[0].uncertaintyProfile)
|
|
116
|
+
problems.push(`${at} cannot combine different uncertainty semantics`);
|
|
117
|
+
for (const capability of [...policy.requiredCapabilities, ...member.requiredCapabilities]) {
|
|
118
|
+
if (!member.capabilities.includes(capability))
|
|
119
|
+
problems.push(`${at} lacks required capability ${capability}`);
|
|
120
|
+
}
|
|
121
|
+
if (member.memberType === 'repeated-sample' && member.samples < 2)
|
|
122
|
+
problems.push(`${at} repeated-sample member needs at least two samples`);
|
|
123
|
+
if (member.memberType === 'prompt-adapter' && !member.approvalReference)
|
|
124
|
+
problems.push(`${at} prompt/adapter variant requires explicit approval`);
|
|
125
|
+
if (member.estimate.attemptsPerSample < member.fallbackDepth + 1)
|
|
126
|
+
problems.push(`${at} attempt estimate does not cover its fallback depth`);
|
|
127
|
+
if (member.memberType !== 'repeated-sample') {
|
|
128
|
+
const identity = canonicalJson([member.binding.digest, member.adapter, member.model.provider, member.model.backend, member.model.pinnedVersion]);
|
|
129
|
+
if (identities.has(identity))
|
|
130
|
+
problems.push(`${at} duplicates another member identity; independence is not established`);
|
|
131
|
+
identities.add(identity);
|
|
132
|
+
}
|
|
133
|
+
if (policy.calibration.requirement === 'required' && !member.calibration)
|
|
134
|
+
problems.push(`${at} requires a pinned calibration artifact`);
|
|
135
|
+
const pin = options.calibrationPins?.[member.id];
|
|
136
|
+
if (options.calibrationPins && member.calibration) {
|
|
137
|
+
if (!pin)
|
|
138
|
+
problems.push(`${at} has no resolved calibration compatibility pin`);
|
|
139
|
+
else if (pin.action !== 'allow' || !['exact', 'approved-compatible'].includes(pin.state) || pin.artifactDigest !== member.calibration.artifactDigest || pin.artifactId !== member.calibration.artifactId) {
|
|
140
|
+
problems.push(`${at} calibration is incompatible (${pin.state}/${pin.action})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const totalSamples = policy.members.reduce((sum, member) => sum + member.samples, 0);
|
|
145
|
+
if (policy.acceptance.minimumSuccessfulMembers > totalSamples)
|
|
146
|
+
problems.push('minimum successful members exceeds planned samples');
|
|
147
|
+
semantic(problems, 'ensemble policy rejected');
|
|
148
|
+
const budget = computeBudget(policy, options.hostCeilings ?? []);
|
|
149
|
+
return { policy, digest: ensembleContractDigest(policy), budget };
|
|
150
|
+
}
|
|
151
|
+
/** Validation-time mirror of GraphBudgetLedger: effective limits are layer minima, estimates must be
|
|
152
|
+
* safe integers with at least one attempt, attempts are never refunded, unknown cost needs a trusted
|
|
153
|
+
* bound, and the whole plan is admitted all-or-nothing before any dispatch could start. */
|
|
154
|
+
export function planEnsembleBudget(value, hostCeilings = []) {
|
|
155
|
+
return validateEnsemblePolicy(value, { hostCeilings }).budget;
|
|
156
|
+
}
|
|
157
|
+
function computeBudget(policy, layers) {
|
|
158
|
+
const { members, attempts, deadlineMs, tokens, costMicros, concurrency, fallbackDepth } = policy.ceilings;
|
|
159
|
+
const effective = { members, attempts, deadlineMs, tokens, costMicros, concurrency, fallbackDepth };
|
|
160
|
+
for (const layer of layers) {
|
|
161
|
+
for (const field of CEILING_FIELDS) {
|
|
162
|
+
const limit = layer[field];
|
|
163
|
+
if (limit === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
if (!Number.isSafeInteger(limit) || limit < (field === 'fallbackDepth' ? 0 : 1))
|
|
166
|
+
throw new EnsembleContractError(`invalid host ceiling ${field}`, 'semantic');
|
|
167
|
+
effective[field] = Math.min(effective[field], limit);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const problems = [];
|
|
171
|
+
const reservations = [];
|
|
172
|
+
const unknown = policy.ceilings.unknownCost;
|
|
173
|
+
let slowestSample = 0;
|
|
174
|
+
for (const member of [...policy.members].sort((a, b) => compareEnsembleKeys(a.id, b.id))) {
|
|
175
|
+
const { attemptsPerSample, tokensPerAttempt, costMicrosPerAttempt, deadlineMsPerAttempt } = member.estimate;
|
|
176
|
+
const perAttemptCost = costMicrosPerAttempt ?? (unknown.rule === 'reserve-bound' ? unknown.boundMicrosPerAttempt : null);
|
|
177
|
+
if (perAttemptCost === null) {
|
|
178
|
+
problems.push(`member ${member.id} has unknown cost and the policy rejects unbounded cost`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
slowestSample = Math.max(slowestSample, attemptsPerSample * deadlineMsPerAttempt);
|
|
182
|
+
for (let sampleIndex = 0; sampleIndex < member.samples; sampleIndex++) {
|
|
183
|
+
reservations.push({ memberId: member.id, sampleIndex, attempts: attemptsPerSample,
|
|
184
|
+
tokens: attemptsPerSample * tokensPerAttempt, costMicros: attemptsPerSample * perAttemptCost });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const total = (field) => reservations.reduce((sum, item) => sum + item[field], 0);
|
|
188
|
+
const samples = policy.members.reduce((sum, member) => sum + member.samples, 0);
|
|
189
|
+
const demand = {
|
|
190
|
+
members: policy.members.length, attempts: total('attempts'), tokens: total('tokens'), costMicros: total('costMicros'),
|
|
191
|
+
concurrency: Math.min(samples, effective.concurrency),
|
|
192
|
+
deadlineMs: Math.ceil(samples / effective.concurrency) * slowestSample,
|
|
193
|
+
fallbackDepth: Math.max(...policy.members.map(member => member.fallbackDepth)),
|
|
194
|
+
};
|
|
195
|
+
for (const field of CEILING_FIELDS) {
|
|
196
|
+
if (!Number.isSafeInteger(demand[field]))
|
|
197
|
+
problems.push(`ensemble ${field} demand is not a safe integer`);
|
|
198
|
+
else if (demand[field] > effective[field])
|
|
199
|
+
problems.push(`ensemble ${field} demand ${demand[field]} exceeds ceiling ${effective[field]}`);
|
|
200
|
+
}
|
|
201
|
+
semantic(problems, 'ensemble budget rejected');
|
|
202
|
+
return { effective, demand, reservations };
|
|
203
|
+
}
|
|
204
|
+
/** Stable problem codes comparing a champion/challenger record with the D09 eligibility record. */
|
|
205
|
+
export function championChallengerEligibilityProblems(record, eligibility) {
|
|
206
|
+
const problems = [];
|
|
207
|
+
if (eligibility.id !== record.eligibilityId)
|
|
208
|
+
problems.push('eligibility-id-mismatch');
|
|
209
|
+
if (!eligibility.eligible)
|
|
210
|
+
problems.push('eligibility-not-eligible');
|
|
211
|
+
if (eligibility.alias !== record.alias)
|
|
212
|
+
problems.push('eligibility-alias-mismatch');
|
|
213
|
+
if (eligibility.candidateIdentityDigest !== record.challenger.identityDigest || eligibility.candidateActualModel !== record.challenger.actualModel)
|
|
214
|
+
problems.push('eligibility-candidate-mismatch');
|
|
215
|
+
if (eligibility.evaluationIntegrityReport.id !== record.evaluationIntegrityReport.id || eligibility.evaluationIntegrityReport.digest !== record.evaluationIntegrityReport.digest)
|
|
216
|
+
problems.push('eligibility-integrity-report-mismatch');
|
|
217
|
+
if (eligibility.approvalReference !== record.approval.reference)
|
|
218
|
+
problems.push('eligibility-approval-mismatch');
|
|
219
|
+
if (eligibility.rollbackTarget.aliasRevision !== record.rollbackTarget.aliasRevision || eligibility.rollbackTarget.identityDigest !== record.rollbackTarget.identityDigest)
|
|
220
|
+
problems.push('eligibility-rollback-target-mismatch');
|
|
221
|
+
return problems;
|
|
222
|
+
}
|
|
223
|
+
export function validateChampionChallenger(value, context = {}) {
|
|
224
|
+
checkEnsembleSchema('championChallenger', value);
|
|
225
|
+
const record = value;
|
|
226
|
+
const problems = [];
|
|
227
|
+
const metrics = record.pairedMetrics.map(item => item.metric);
|
|
228
|
+
for (const metric of REQUIRED_PAIRED_METRICS)
|
|
229
|
+
if (metrics.filter(item => item === metric).length !== 1)
|
|
230
|
+
problems.push(`paired metric ${metric} must appear exactly once`);
|
|
231
|
+
if (record.preregistration.thresholdsDigest !== pairedThresholdsDigest(record.pairedMetrics))
|
|
232
|
+
problems.push('paired thresholds do not match the preregistered digest');
|
|
233
|
+
if (record.champion.identityDigest === record.challenger.identityDigest)
|
|
234
|
+
problems.push('challenger must differ from the champion identity');
|
|
235
|
+
if (record.rollbackTarget.aliasRevision !== record.champion.aliasRevision || record.rollbackTarget.identityDigest !== record.champion.identityDigest) {
|
|
236
|
+
problems.push('rollback target must be the exact pinned champion revision');
|
|
237
|
+
}
|
|
238
|
+
const registered = time(record.preregistration.registeredAt);
|
|
239
|
+
if (record.preregistration.holdoutAccessedAt !== null && time(record.preregistration.holdoutAccessedAt) <= registered)
|
|
240
|
+
problems.push('thresholds must be preregistered before held-out access');
|
|
241
|
+
if (registered > time(record.approval.approvedAt))
|
|
242
|
+
problems.push('approval cannot precede threshold preregistration');
|
|
243
|
+
if (time(record.inputSet.frozenAt) > time(record.approval.approvedAt))
|
|
244
|
+
problems.push('approval cannot precede freezing the input set');
|
|
245
|
+
if (record.inputSet.itemCount < Math.max(...record.pairedMetrics.map(item => item.minimumPairs)))
|
|
246
|
+
problems.push('input set is smaller than the preregistered minimum pairs');
|
|
247
|
+
if (context.eligibility)
|
|
248
|
+
problems.push(...championChallengerEligibilityProblems(record, context.eligibility));
|
|
249
|
+
if (context.aliasHistory) {
|
|
250
|
+
const target = context.aliasHistory.find(event => event.revision === record.rollbackTarget.aliasRevision);
|
|
251
|
+
if (!target || target.actualIdentityDigest !== record.rollbackTarget.identityDigest || target.kind === 'retired')
|
|
252
|
+
problems.push('rollback target no longer matches immutable alias history');
|
|
253
|
+
}
|
|
254
|
+
semantic(problems, 'champion/challenger record rejected');
|
|
255
|
+
return { record, digest: ensembleContractDigest(record) };
|
|
256
|
+
}
|
|
257
|
+
export function validateDriftResponsePolicy(value) {
|
|
258
|
+
checkEnsembleSchema('driftResponse', value);
|
|
259
|
+
const policy = value;
|
|
260
|
+
const problems = [];
|
|
261
|
+
const ids = new Set();
|
|
262
|
+
const keys = new Set();
|
|
263
|
+
for (const rule of policy.rules) {
|
|
264
|
+
if (ids.has(rule.id))
|
|
265
|
+
problems.push(`drift rule ${rule.id} is duplicated`);
|
|
266
|
+
ids.add(rule.id);
|
|
267
|
+
const key = `${rule.source}/${rule.metric}`;
|
|
268
|
+
if (keys.has(key))
|
|
269
|
+
problems.push(`drift signal ${key} has more than one configured response`);
|
|
270
|
+
keys.add(key);
|
|
271
|
+
}
|
|
272
|
+
if (!policy.rules.some(rule => rule.source === 'alias-drift'))
|
|
273
|
+
problems.push('alias-drift events have no configured response');
|
|
274
|
+
if (policy.thresholds.digest !== driftThresholdsDigest(policy.rules))
|
|
275
|
+
problems.push('drift rules do not match the registered thresholds digest');
|
|
276
|
+
if (policy.window.kind === 'observations' && policy.window.minimumSamples > policy.window.size)
|
|
277
|
+
problems.push('minimum samples exceed the observation window');
|
|
278
|
+
semantic(problems, 'drift response policy rejected');
|
|
279
|
+
return policy;
|
|
280
|
+
}
|
|
281
|
+
export function validateEnsembleAggregate(value) {
|
|
282
|
+
checkEnsembleSchema('aggregate', value);
|
|
283
|
+
const aggregate = value;
|
|
284
|
+
const { digest, ...payload } = aggregate;
|
|
285
|
+
const problems = [];
|
|
286
|
+
if (digest !== ensembleContractDigest(payload))
|
|
287
|
+
problems.push('aggregate digest does not match its content');
|
|
288
|
+
const { counts } = aggregate;
|
|
289
|
+
if (counts.succeeded + counts.failed + counts.abstained !== counts.declared || aggregate.members.length !== counts.declared)
|
|
290
|
+
problems.push('aggregate counts do not reconcile');
|
|
291
|
+
if (aggregate.outcome.disposition !== 'accept' && aggregate.outcome.value !== null)
|
|
292
|
+
problems.push('a deferred aggregate cannot carry an outcome value');
|
|
293
|
+
if (!ENSEMBLE_AGGREGATION_PRIMITIVES[aggregate.algorithm.id]?.includes(aggregate.primitive))
|
|
294
|
+
problems.push('unknown or incompatible aggregation version');
|
|
295
|
+
semantic(problems, 'ensemble aggregate rejected');
|
|
296
|
+
return aggregate;
|
|
297
|
+
}
|
|
298
|
+
/** A D17 report may keep or tighten the upstream #2037/#2048 decision, never loosen it. */
|
|
299
|
+
export function validateEnsembleIntegrityReport(value) {
|
|
300
|
+
checkEnsembleSchema('integrityReport', value);
|
|
301
|
+
const report = value;
|
|
302
|
+
const { digest, ...payload } = report;
|
|
303
|
+
const problems = [];
|
|
304
|
+
if (report.upstreamDecision !== report.integrity.release_gate.decision)
|
|
305
|
+
problems.push('upstream decision must be the eval-integrity release gate decision');
|
|
306
|
+
if (GATE_RANK[report.decision] < GATE_RANK[report.upstreamDecision])
|
|
307
|
+
problems.push(`D17 cannot upgrade ${report.upstreamDecision} to ${report.decision}`);
|
|
308
|
+
if (report.decision === 'PROMOTE' && (report.findings.length > 0 || report.pairedDeltas.some(item => !item.passed)))
|
|
309
|
+
problems.push('PROMOTE requires every D17 check to pass');
|
|
310
|
+
if (digest !== ensembleContractDigest(payload))
|
|
311
|
+
problems.push('integrity report digest does not match its content');
|
|
312
|
+
semantic(problems, 'ensemble integrity report rejected');
|
|
313
|
+
return report;
|
|
314
|
+
}
|
|
315
|
+
export function integrityGateRank(decision) { return GATE_RANK[decision]; }
|
|
316
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { admitEntry } from '../entry.js';
|
|
2
|
+
import { EnsembleContractError, validateDriftResponsePolicy } from './contract.js';
|
|
3
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
4
|
+
/** Maps one drift signal to its exact configured response. It does not execute the response.
|
|
5
|
+
* A signal with no configured rule, a different alias, or evaluated against a different
|
|
6
|
+
* threshold version is rejected rather than falling back to a default or stale threshold. */
|
|
7
|
+
export function resolveDriftResponse(policyInput, signal) {
|
|
8
|
+
const policy = validateDriftResponsePolicy(policyInput);
|
|
9
|
+
try {
|
|
10
|
+
admitEntry(signal);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new EnsembleContractError('drift signal admission denied', 'admission');
|
|
14
|
+
}
|
|
15
|
+
const reject = (message) => { throw new EnsembleContractError(message, 'semantic', [message]); };
|
|
16
|
+
const base = { schemaVersion: 'decision-drift-response-decision/v1',
|
|
17
|
+
policy: { id: policy.id, version: policy.version, thresholdsVersion: policy.thresholds.version } };
|
|
18
|
+
if (signal.source === 'alias-drift') {
|
|
19
|
+
const event = signal.event;
|
|
20
|
+
if (!event?.id || event.alias !== policy.alias)
|
|
21
|
+
return reject('drift event alias does not match the drift response policy');
|
|
22
|
+
if (!DIGEST.test(event.previousIdentityDigest) || !DIGEST.test(event.observedIdentityDigest) || event.previousIdentityDigest === event.observedIdentityDigest) {
|
|
23
|
+
return reject('alias drift event must record two different pinned identities');
|
|
24
|
+
}
|
|
25
|
+
const rule = policy.rules.find(item => item.source === 'alias-drift');
|
|
26
|
+
if (!rule)
|
|
27
|
+
return reject('drift event has no configured response');
|
|
28
|
+
return { ...base, signalId: event.id, source: 'alias-drift', metric: 'identity-change', ruleId: rule.id,
|
|
29
|
+
state: 'breached', evidence: 'identity-change', response: rule.response };
|
|
30
|
+
}
|
|
31
|
+
if (signal.source !== 'output-distribution' && signal.source !== 'label-drift')
|
|
32
|
+
return reject('drift event has no configured response');
|
|
33
|
+
if (!signal.id || signal.alias !== policy.alias)
|
|
34
|
+
return reject('drift event alias does not match the drift response policy');
|
|
35
|
+
if (signal.thresholdsVersion !== policy.thresholds.version)
|
|
36
|
+
return reject('drift event was evaluated against a different threshold version');
|
|
37
|
+
if (!Number.isSafeInteger(signal.valueBps) || signal.valueBps < 0 || !Number.isSafeInteger(signal.sampleN) || signal.sampleN < 0) {
|
|
38
|
+
return reject('drift event value and sample count must be non-negative integers');
|
|
39
|
+
}
|
|
40
|
+
const rule = policy.rules.find(item => item.source === signal.source && item.metric === signal.metric);
|
|
41
|
+
if (!rule)
|
|
42
|
+
return reject('drift event has no configured response');
|
|
43
|
+
const evidence = signal.source === 'label-drift' ? 'labeled-quality' : 'unlabeled-distribution-warning';
|
|
44
|
+
const common = { ...base, signalId: signal.id, source: signal.source, metric: signal.metric, ruleId: rule.id, evidence };
|
|
45
|
+
if (signal.sampleN < policy.window.minimumSamples)
|
|
46
|
+
return { ...common, state: 'insufficient-samples', response: policy.insufficientSamplesResponse };
|
|
47
|
+
return signal.valueBps > rule.thresholdBps
|
|
48
|
+
? { ...common, state: 'breached', response: rule.response }
|
|
49
|
+
: { ...common, state: 'within-threshold', response: null };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=drift.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { championChallengerEligibilityProblems, ensembleContractDigest, EnsembleContractError, validateChampionChallenger, validateEnsembleIntegrityReport, } from './contract.js';
|
|
2
|
+
/** Extends the eval-integrity report with paired champion/challenger evidence. D17 findings can
|
|
3
|
+
* only hold or roll back: HOLD and ROLLBACK from the upstream gate are never upgraded. */
|
|
4
|
+
export function buildEnsembleIntegrityReport(input) {
|
|
5
|
+
const { record, digest } = validateChampionChallenger(input.record, input.aliasHistory ? { aliasHistory: input.aliasHistory } : {});
|
|
6
|
+
const integrity = input.integrity;
|
|
7
|
+
const findings = new Set();
|
|
8
|
+
if (!input.eligibility)
|
|
9
|
+
findings.add('d09-eligibility-missing');
|
|
10
|
+
else
|
|
11
|
+
for (const problem of championChallengerEligibilityProblems(record, input.eligibility))
|
|
12
|
+
findings.add(`d09-${problem}`);
|
|
13
|
+
if (integrity.integrity_state !== 'verified')
|
|
14
|
+
findings.add('integrity-not-verified');
|
|
15
|
+
if (integrity.integrity_mode === 'standard')
|
|
16
|
+
findings.add('integrity-mode-standard');
|
|
17
|
+
if (integrity.trusted_score_source === 'local-unverified')
|
|
18
|
+
findings.add('untrusted-score-source');
|
|
19
|
+
if (integrity.fresh_workspace_required && !integrity.fresh_workspace_verified)
|
|
20
|
+
findings.add('fresh-workspace-unverified');
|
|
21
|
+
if (integrity.uncertainty === null)
|
|
22
|
+
findings.add('uncertainty-missing');
|
|
23
|
+
if (integrity.paired_baseline === null)
|
|
24
|
+
findings.add('paired-baseline-missing');
|
|
25
|
+
if (integrity.weak_signal_reason !== null)
|
|
26
|
+
findings.add('weak-signal');
|
|
27
|
+
if (integrity.compromise_labels.length > 0)
|
|
28
|
+
findings.add('compromised');
|
|
29
|
+
if (integrity.sample_n < Math.max(...record.pairedMetrics.map(item => item.minimumPairs)))
|
|
30
|
+
findings.add('insufficient-samples');
|
|
31
|
+
const observed = new Map();
|
|
32
|
+
for (const item of input.pairedDeltas) {
|
|
33
|
+
if (observed.has(item.metric))
|
|
34
|
+
throw new EnsembleContractError(`paired delta ${item.metric} is duplicated`, 'semantic');
|
|
35
|
+
observed.set(item.metric, item);
|
|
36
|
+
}
|
|
37
|
+
const pairedDeltas = record.pairedMetrics.map(threshold => {
|
|
38
|
+
const item = observed.get(threshold.metric);
|
|
39
|
+
const delta = item?.delta ?? null;
|
|
40
|
+
const pairs = item?.pairs ?? 0;
|
|
41
|
+
let passed = true;
|
|
42
|
+
if (!item) {
|
|
43
|
+
findings.add(`paired-delta-missing:${threshold.metric}`);
|
|
44
|
+
passed = false;
|
|
45
|
+
}
|
|
46
|
+
else if (delta === null || !Number.isFinite(delta)) {
|
|
47
|
+
findings.add(`paired-delta-unknown:${threshold.metric}`);
|
|
48
|
+
passed = false;
|
|
49
|
+
}
|
|
50
|
+
else if (!Number.isSafeInteger(pairs) || pairs < threshold.minimumPairs) {
|
|
51
|
+
findings.add(`paired-delta-insufficient:${threshold.metric}`);
|
|
52
|
+
passed = false;
|
|
53
|
+
}
|
|
54
|
+
else if (threshold.comparison === 'delta-at-least' ? delta < threshold.bound : delta > threshold.bound) {
|
|
55
|
+
findings.add(`paired-delta-failed:${threshold.metric}`);
|
|
56
|
+
passed = false;
|
|
57
|
+
}
|
|
58
|
+
return { ...threshold, delta, pairs, passed };
|
|
59
|
+
}).sort((a, b) => a.metric < b.metric ? -1 : a.metric > b.metric ? 1 : 0);
|
|
60
|
+
const upstreamDecision = integrity.release_gate.decision;
|
|
61
|
+
const compromised = integrity.integrity_state === 'compromised' || integrity.compromise_labels.length > 0;
|
|
62
|
+
const decision = upstreamDecision === 'ROLLBACK' || compromised ? 'ROLLBACK'
|
|
63
|
+
: upstreamDecision === 'HOLD' || findings.size > 0 ? 'HOLD' : 'PROMOTE';
|
|
64
|
+
const payload = {
|
|
65
|
+
schemaVersion: 'decision-ensemble-integrity-report/v1',
|
|
66
|
+
subject: { kind: 'champion-challenger', id: record.id, digest, eligibilityId: record.eligibilityId },
|
|
67
|
+
integrity, pairedDeltas, findings: [...findings].sort(), upstreamDecision, decision,
|
|
68
|
+
};
|
|
69
|
+
return validateEnsembleIntegrityReport({ ...payload, digest: ensembleContractDigest(payload) });
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=report.js.map
|