@aiwg/cli 2026.9.24 → 2026.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentic/code/providers/capability-matrix.yaml +2 -1
- package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
- package/dist/src/artifacts/discovery-shadow.js +195 -0
- package/dist/src/artifacts/index-builder.js +47 -9
- package/dist/src/artifacts/query-engine.js +94 -1
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/decision.js +177 -0
- package/dist/src/cli/handlers/gates.js +154 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +10 -7
- package/dist/src/cli/handlers/utilities.js +20 -0
- package/dist/src/config/aiwg-config.js +15 -2
- package/dist/src/decision/acceptance.js +25 -0
- package/dist/src/decision/adapters/jev.js +29 -3
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/calibration/registry.js +9 -0
- package/dist/src/decision/compose.js +74 -15
- package/dist/src/decision/context-live-promotion.js +344 -0
- package/dist/src/decision/context-live-qualification.js +591 -0
- package/dist/src/decision/context-pruning.js +847 -0
- package/dist/src/decision/driver.js +432 -0
- package/dist/src/decision/egress-live-qualification.js +874 -0
- package/dist/src/decision/ensemble/contract.js +64 -1
- package/dist/src/decision/ensemble/index.js +1 -0
- package/dist/src/decision/ensemble/report.js +8 -42
- package/dist/src/decision/ensemble/runtime.js +571 -0
- package/dist/src/decision/ensemble-study/artifacts.js +85 -0
- package/dist/src/decision/ensemble-study/calibration.js +443 -0
- package/dist/src/decision/ensemble-study/corpus.js +229 -0
- package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
- package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
- package/dist/src/decision/ensemble-study/multifact.js +241 -0
- package/dist/src/decision/ensemble-study/protocol.js +77 -0
- package/dist/src/decision/ensemble-study/score.js +462 -0
- package/dist/src/decision/ensemble-study/staged.js +87 -0
- package/dist/src/decision/ensemble-study/statistics.js +84 -0
- package/dist/src/decision/evaluate.js +227 -14
- package/dist/src/decision/export.js +4 -0
- package/dist/src/decision/feature-export.js +1074 -0
- package/dist/src/decision/graph-index.js +16 -0
- package/dist/src/decision/graph-live-qualification.js +1041 -0
- package/dist/src/decision/graph-live-workload.js +245 -0
- package/dist/src/decision/graph-skill-bridge.js +21 -3
- package/dist/src/decision/heldout/calibration.js +140 -0
- package/dist/src/decision/heldout/collector.js +430 -0
- package/dist/src/decision/heldout/contract.js +309 -0
- package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
- package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
- package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
- package/dist/src/decision/heldout/d29-pools.js +288 -0
- package/dist/src/decision/heldout/d29-v6.js +169 -0
- package/dist/src/decision/heldout/d29-v7.js +202 -0
- package/dist/src/decision/heldout/d29-v8.js +586 -0
- package/dist/src/decision/heldout/generator-registry.js +67 -0
- package/dist/src/decision/heldout/generators.js +696 -0
- package/dist/src/decision/heldout/index.js +8 -0
- package/dist/src/decision/heldout/journal.js +346 -0
- package/dist/src/decision/heldout/study-generators.js +64 -0
- package/dist/src/decision/heldout/types.js +2 -0
- package/dist/src/decision/host-policy.js +61 -0
- package/dist/src/decision/index.js +13 -0
- package/dist/src/decision/interop-profile.v1.json +112 -0
- package/dist/src/decision/interop.js +653 -0
- package/dist/src/decision/issue-triage/index.js +3 -0
- package/dist/src/decision/issue-triage/runtime.js +1069 -0
- package/dist/src/decision/issue-triage/types.js +2 -0
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/patterns/catalog.js +28 -8
- package/dist/src/decision/patterns/playground.js +28 -11
- package/dist/src/decision/patterns/specs.js +19 -10
- package/dist/src/decision/preprocessed-evidence.js +724 -0
- package/dist/src/decision/qualification/capture.js +8 -2
- package/dist/src/decision/qualification/gate-evidence.js +19 -4
- package/dist/src/decision/qualification/quality.js +57 -49
- package/dist/src/decision/qualification/release.js +76 -3
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/routing/contract.js +175 -0
- package/dist/src/decision/routing/index.js +5 -0
- package/dist/src/decision/routing/report.js +216 -0
- package/dist/src/decision/routing/runtime.js +685 -0
- package/dist/src/decision/routing/types.js +2 -0
- package/dist/src/decision/sdlc-screening.js +1157 -0
- package/dist/src/decision/sensitivity/comparative.js +154 -0
- package/dist/src/decision/sensitivity/contract.js +218 -0
- package/dist/src/decision/sensitivity/index.js +5 -0
- package/dist/src/decision/sensitivity/runtime.js +912 -0
- package/dist/src/decision/sensitivity/types.js +2 -0
- package/dist/src/decision/validate.js +26 -0
- package/dist/src/extensions/commands/definitions.js +38 -0
- package/dist/src/extensions/manifest.js +44 -0
- package/dist/src/gates/discovery.js +315 -0
- package/dist/src/gates/driver.js +388 -0
- package/dist/src/gates/evaluate.js +719 -0
- package/dist/src/gates/floors.js +255 -0
- package/dist/src/gates/index.js +17 -0
- package/dist/src/gates/providers/evidence.js +38 -0
- package/dist/src/gates/providers/index.js +21 -0
- package/dist/src/gates/providers/isolated-child-source.js +446 -0
- package/dist/src/gates/providers/loader.js +1069 -0
- package/dist/src/gates/providers/paired.js +52 -0
- package/dist/src/gates/providers/proportion.js +60 -0
- package/dist/src/gates/providers/registry.js +40 -0
- package/dist/src/gates/providers/scalar.js +41 -0
- package/dist/src/gates/providers/screening.js +53 -0
- package/dist/src/gates/providers/types.js +25 -0
- package/dist/src/gates/registry.js +1003 -0
- package/dist/src/gates/report.js +102 -0
- package/dist/src/gates/schema.js +56 -0
- package/dist/src/gates/stats/binomial.js +26 -0
- package/dist/src/gates/stats/bootstrap.js +62 -0
- package/dist/src/gates/stats/clopper-pearson.js +95 -0
- package/dist/src/gates/stats/digest.js +26 -0
- package/dist/src/gates/stats/error.js +5 -0
- package/dist/src/gates/stats/index.js +9 -0
- package/dist/src/gates/stats/normal.js +35 -0
- package/dist/src/gates/stats/paired.js +120 -0
- package/dist/src/gates/stats/splits.js +30 -0
- package/dist/src/gates/types.js +8 -0
- package/dist/src/governance/redaction.js +14 -4
- package/dist/src/mcp/muse-native-config.mjs +123 -0
- package/dist/src/mcp/tools/decision.mjs +228 -0
- package/dist/src/mcp/tools/subsystems.mjs +2 -0
- package/dist/src/providers/capability-matrix.yaml +2 -1
- package/dist/src/providers/provider-definitions.js +2 -1
- package/dist/src/sessions/adapters/muse.js +336 -39
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/workspace-discovery.js +13 -2
- package/package.json +1 -1
- package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
- package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
- package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
- package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
- package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
- package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
- package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
- package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
- package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
- package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
- package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
- package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
- package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
- package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
- package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
- package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
- package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
- package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
- package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
- package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
- package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
- package/schemas/decision/D29Study.v1.schema.json +1903 -0
- package/schemas/decision/D29Study.v2.schema.json +1344 -0
- package/schemas/decision/D29Study.v3.schema.json +609 -0
- package/schemas/decision/D29Study.v4.schema.json +1133 -0
- package/schemas/decision/D29Study.v6.schema.json +1205 -0
- package/schemas/decision/D29Study.v7.schema.json +1471 -0
- package/schemas/decision/D29Study.v8.schema.json +671 -0
- package/schemas/decision/D29Study.v9.schema.json +778 -0
- package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
- package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
- package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
- package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
- package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
- package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
- package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
- package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
- package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
- package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
- package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
- package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
- package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
- package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
- package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
- package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
- package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
- package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
- package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
- package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
- package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
- package/schemas/decision/DecisionRuleset.schema.json +8 -2
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
- package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
- package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
- package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
- package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
- package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
- package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
- package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
- package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
- package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
- package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
- package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
- package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
- package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
- package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
- package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
- package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
- package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
- package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
- package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
- package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
- package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
- package/tools/agents/deploy-agents.mjs +1 -1
- package/tools/agents/providers/base.mjs +40 -18
- package/tools/agents/providers/factory.mjs +10 -2
- package/tools/agents/providers/muse-hooks.mjs +1 -29
- package/tools/agents/providers/muse.mjs +6 -3
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D12 live paired qualification (#2686): flat single-call FlowGraph baseline versus the
|
|
3
|
+
* compiled dependent decision graph, on the preregistered synthetic workload.
|
|
4
|
+
*
|
|
5
|
+
* Collection-only. Dry-run planning never resolves a credential or opens a connection; a
|
|
6
|
+
* live run additionally needs an operator approval bound to the workload and
|
|
7
|
+
* preregistration digests, a clean exact source commit and the explicit
|
|
8
|
+
* AIWG_DECISION_DAG_LIVE=1 gate. The result is evidence for a human promote-or-hold
|
|
9
|
+
* decision; nothing here promotes the experimental graph runtime.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { appendFile, mkdir, readdir, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
16
|
+
import { admitEntry, DEFAULT_ENTRY_LIMITS } from './entry.js';
|
|
17
|
+
import { CanonicalJsonByteEstimator } from './context-plan.js';
|
|
18
|
+
import { assertContextLiveSource } from './context-live-qualification.js';
|
|
19
|
+
import { compileJevQuestion, JevDecisionAdapter, JEV_ENDPOINT } from './adapters/jev.js';
|
|
20
|
+
import { evaluateDecisionRuleset } from './evaluate.js';
|
|
21
|
+
import { artifactPin, validateBinding, validateRuleset } from './validate.js';
|
|
22
|
+
import { DecisionGraphError, planDecisionGraph } from './graph.js';
|
|
23
|
+
import { shortlistRerankTemplate, taxonomyBeamTemplate, extractorVerifierFallbackTemplate } from './graph-templates.js';
|
|
24
|
+
import { decisionGraphToFlow } from './graph-flow.js';
|
|
25
|
+
import { graphBeamFlowInvoker } from './graph-beam.js';
|
|
26
|
+
import { GraphBudgetLedger } from './graph-budget.js';
|
|
27
|
+
import { admittedDecisionFlowAdapter } from './graph-flow-adapter.js';
|
|
28
|
+
import { assertDecisionFlowPins, decisionFlowNode, decisionFlowResponse } from './graph-decision-bridge.js';
|
|
29
|
+
import { finalizeDecisionGraphRun } from './graph-run.js';
|
|
30
|
+
import { pairedBinaryDifferenceInterval, pairedMeanDifferenceBootstrap, pairedNonInferiority, wilsonScoreInterval } from './qualification/quality.js';
|
|
31
|
+
import { executeQualificationPlan, writeQualificationEvidenceManifest } from './qualification/runner.js';
|
|
32
|
+
import { loadManifestDigest, qualificationOutcomesDigest } from './qualification/gate-evidence.js';
|
|
33
|
+
import { DAG_LIVE_ANALYSIS, DAG_LIVE_PATTERNS, DAG_LIVE_TASKS_PER_PATTERN, dagLiveDefinitions, dagLiveDigest, dagLivePreregistration, generateDagLiveWorkload } from './graph-live-workload.js';
|
|
34
|
+
export { dagLiveDigest };
|
|
35
|
+
/** Hard ceiling from the #2686 assessment; an approval cannot raise it. */
|
|
36
|
+
export const DAG_LIVE_HARD_CAP_USD = 2;
|
|
37
|
+
export const DAG_LIVE_ENV_GATE = 'AIWG_DECISION_DAG_LIVE';
|
|
38
|
+
/** Worst-case provider calls per task: flat baseline is always one. */
|
|
39
|
+
export const DAG_LIVE_MAX_CANDIDATE_CALLS = {
|
|
40
|
+
'shortlist-rerank': 2, 'taxonomy-beam': 3, 'extractor-verifier-fallback': 3,
|
|
41
|
+
};
|
|
42
|
+
const HOST_LOCAL = new Set(['taxonomy', 'select']);
|
|
43
|
+
/** Hierarchical search keeps one branch: the caller ceiling narrows the authored beam width. */
|
|
44
|
+
const NARROW = [{ beamWidth: 1 }];
|
|
45
|
+
const estimator = new CanonicalJsonByteEstimator();
|
|
46
|
+
const REQUEST_ENVELOPE_TOKENS = 32;
|
|
47
|
+
const WORKLOAD_LIMITS = { ...DEFAULT_ENTRY_LIMITS, serializedBytes: 8_388_608, properties: 100_000, entries: 200_000, memoryBytes: 67_108_864 };
|
|
48
|
+
/** Hard-coded floor. It never replaces the attestation: a higher attested price always wins. */
|
|
49
|
+
export const DAG_LIVE_RESERVE_FLOOR_USD_PER_MTOK = 0.1;
|
|
50
|
+
const rate = (attested) => Math.max(attested, DAG_LIVE_RESERVE_FLOOR_USD_PER_MTOK);
|
|
51
|
+
const perRequestMicros = (bound) => bound.perRequestUsd === undefined ? 0 : Math.ceil(bound.perRequestUsd * 1_000_000);
|
|
52
|
+
/** Worst-case reservation for one call: every token at the dearest rate, never below the per-request price. */
|
|
53
|
+
export function dagLiveReservationMicros(bound, perCallTokenBound) {
|
|
54
|
+
return Math.max(Math.ceil(perCallTokenBound * Math.max(rate(bound.inputUsdPerMTok), rate(bound.outputUsdPerMTok))), perRequestMicros(bound));
|
|
55
|
+
}
|
|
56
|
+
/** Charge for reported usage at the reservation rates, never below the per-request price. */
|
|
57
|
+
export function dagLiveChargeMicros(bound, inputTokens, outputTokens) {
|
|
58
|
+
return Math.max(Math.ceil(inputTokens * rate(bound.inputUsdPerMTok) + outputTokens * rate(bound.outputUsdPerMTok)), perRequestMicros(bound));
|
|
59
|
+
}
|
|
60
|
+
function validSecretService(pin) {
|
|
61
|
+
if (!pin || typeof pin !== 'object' || Object.keys(pin).sort().join(',') !== 'origin,secretPathDigest')
|
|
62
|
+
return false;
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(pin.origin);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return url.protocol === 'https:' && url.origin === pin.origin && !url.username && !url.password
|
|
71
|
+
&& /^sha256:[a-f0-9]{64}$/.test(pin.secretPathDigest);
|
|
72
|
+
}
|
|
73
|
+
/** Digest that pins a KV secret path: SHA-256 of its UTF-8 bytes (the resolver computes the same). */
|
|
74
|
+
export const dagLiveSecretPathDigest = (path) => `sha256:${createHash('sha256').update(path).digest('hex')}`;
|
|
75
|
+
const attestedText = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 512 && !value.trim().startsWith('<');
|
|
76
|
+
function validPriceBound(bound) {
|
|
77
|
+
if (!bound || typeof bound !== 'object' || Array.isArray(bound))
|
|
78
|
+
return false;
|
|
79
|
+
const allowed = ['inputUsdPerMTok', 'outputUsdPerMTok', 'perRequestUsd', 'evidenceReferences', 'approvalReference'];
|
|
80
|
+
return Object.keys(bound).every(key => allowed.includes(key)) && allowed.filter(key => key !== 'perRequestUsd').every(key => Object.hasOwn(bound, key))
|
|
81
|
+
&& [bound.inputUsdPerMTok, bound.outputUsdPerMTok].every(n => typeof n === 'number' && Number.isFinite(n) && n >= 0 && n <= 1_000)
|
|
82
|
+
&& (bound.perRequestUsd === undefined || (typeof bound.perRequestUsd === 'number' && Number.isFinite(bound.perRequestUsd) && bound.perRequestUsd > 0 && bound.perRequestUsd <= DAG_LIVE_HARD_CAP_USD))
|
|
83
|
+
&& Array.isArray(bound.evidenceReferences) && bound.evidenceReferences.length >= 1 && bound.evidenceReferences.length <= 8
|
|
84
|
+
&& bound.evidenceReferences.every(attestedText) && attestedText(bound.approvalReference);
|
|
85
|
+
}
|
|
86
|
+
const APPROVAL_KEYS = ['schemaVersion', 'approved', 'reviewer', 'stagingHost', 'runId', 'sourceCommit', 'exactHeadCi', 'model', 'apiRevision',
|
|
87
|
+
'region', 'secretServiceReference', 'credentialResolverDigest', 'workloadDigest', 'preregistrationDigest', 'budget', 'priceBound', 'priorSpendUsd', 'secretService', 'taskDeadlineMs'];
|
|
88
|
+
const LIMIT_KEYS = 'calls,tokens,usd,wallClockMs';
|
|
89
|
+
const validLimits = (limits) => !!limits && typeof limits === 'object'
|
|
90
|
+
&& Object.keys(limits).filter(key => key !== 'perPattern').sort().join(',') === LIMIT_KEYS
|
|
91
|
+
&& [limits.calls, limits.tokens, limits.wallClockMs].every(n => Number.isSafeInteger(n) && n > 0)
|
|
92
|
+
&& Number.isFinite(limits.usd) && limits.usd > 0 && limits.usd <= DAG_LIVE_HARD_CAP_USD;
|
|
93
|
+
/** Fails closed on any unknown field, missing value, digest mismatch or cap violation. */
|
|
94
|
+
export function validateDagLiveApproval(approval, workload, preregistration) {
|
|
95
|
+
admitEntry(approval);
|
|
96
|
+
admitEntry(workload, WORKLOAD_LIMITS);
|
|
97
|
+
admitEntry(preregistration);
|
|
98
|
+
const a = approval;
|
|
99
|
+
if (!a || typeof a !== 'object' || Object.keys(a).some(key => !APPROVAL_KEYS.includes(key)) || APPROVAL_KEYS.some(key => !Object.hasOwn(a, key))) {
|
|
100
|
+
throw new Error('Unknown or missing D12 approval field');
|
|
101
|
+
}
|
|
102
|
+
if (!validPriceBound(a.priceBound))
|
|
103
|
+
throw new Error('D12 approval lacks a valid operator-attested price bound');
|
|
104
|
+
if (!validSecretService(a.secretService))
|
|
105
|
+
throw new Error('D12 approval lacks a valid pinned secret service');
|
|
106
|
+
if (a.reviewer !== preregistration?.promotionOwner)
|
|
107
|
+
throw new Error('D12 approval must come from the preregistered promotion owner');
|
|
108
|
+
const { budget } = a;
|
|
109
|
+
if (a.schemaVersion !== 'dag-live-approval/v1' || a.approved !== true || a.apiRevision !== 'v1'
|
|
110
|
+
|| ![a.reviewer, a.stagingHost, a.runId, a.exactHeadCi, a.model, a.region, a.secretServiceReference].every(v => typeof v === 'string' && v.trim())
|
|
111
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(a.runId) || !/^[a-f0-9]{40}$/.test(a.sourceCommit)
|
|
112
|
+
|| !/^sha256:[a-f0-9]{64}$/.test(a.credentialResolverDigest) || /latest|unknown/i.test(a.model) || a.region === 'unknown'
|
|
113
|
+
|| !validLimits(budget) || !validLimits(budget.perPattern)
|
|
114
|
+
|| ['calls', 'tokens', 'usd', 'wallClockMs'].some(key => budget.perPattern[key] > budget[key])
|
|
115
|
+
|| typeof a.priorSpendUsd !== 'number' || !Number.isFinite(a.priorSpendUsd) || a.priorSpendUsd < 0 || a.priorSpendUsd >= DAG_LIVE_HARD_CAP_USD
|
|
116
|
+
|| !Number.isSafeInteger(a.taskDeadlineMs) || a.taskDeadlineMs < 1_000 || a.taskDeadlineMs > budget.perPattern.wallClockMs) {
|
|
117
|
+
throw new Error('Incomplete or invalid D12 approval');
|
|
118
|
+
}
|
|
119
|
+
validateDagLiveInputs(workload, preregistration);
|
|
120
|
+
if (a.workloadDigest !== dagLiveDigest(workload) || a.preregistrationDigest !== dagLiveDigest(preregistration)) {
|
|
121
|
+
throw new Error('D12 approval does not bind this workload and preregistration');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** The preregistration must be exactly the one generated from this workload. */
|
|
125
|
+
export function validateDagLiveInputs(workload, preregistration) {
|
|
126
|
+
if (workload?.schemaVersion !== 'dag-live-workload/v1' || workload.syntheticOnly !== true || !Array.isArray(workload.tasks)
|
|
127
|
+
|| new Set(workload.tasks.map(task => task.id)).size !== workload.tasks.length
|
|
128
|
+
|| DAG_LIVE_PATTERNS.some(pattern => workload.tasks.filter(task => task.pattern === pattern).length !== workload.tasksPerPattern)
|
|
129
|
+
|| dagLiveDigest(generateDagLiveWorkload(workload.seed, workload.tasksPerPattern)) !== dagLiveDigest(workload)) {
|
|
130
|
+
throw new Error('D12 workload is not the seeded synthetic workload');
|
|
131
|
+
}
|
|
132
|
+
if (dagLiveDigest(preregistration) !== dagLiveDigest(dagLivePreregistration(workload, preregistration?.promotionOwner))) {
|
|
133
|
+
throw new Error('D12 preregistration does not match its workload');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export class DagLiveStop extends Error {
|
|
137
|
+
reason;
|
|
138
|
+
constructor(reason) {
|
|
139
|
+
super(`D12 live qualification stopped: ${reason}`);
|
|
140
|
+
this.reason = reason;
|
|
141
|
+
this.name = 'DagLiveStop';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Pure accounting guard. Every call reserves the full per-call token bound at the larger of
|
|
146
|
+
* the attested price and the floor before dispatch, and is refused once any global or
|
|
147
|
+
* per-pattern dimension would pass the stop fraction. Settlement replaces the reservation
|
|
148
|
+
* with the reported usage, including usage above the reservation. A call reporting more
|
|
149
|
+
* than its per-call bound records a budget breach: its observed spend is still charged in
|
|
150
|
+
* full, and every later reservation is refused. A call with missing usage
|
|
151
|
+
* keeps at least its full reservation. Calls are never refunded.
|
|
152
|
+
*/
|
|
153
|
+
export class DagLiveBudget {
|
|
154
|
+
approval;
|
|
155
|
+
bound;
|
|
156
|
+
stopFraction;
|
|
157
|
+
started;
|
|
158
|
+
now;
|
|
159
|
+
total = { calls: 0, tokens: 0, usdMicros: 0 };
|
|
160
|
+
patterns = new Map();
|
|
161
|
+
boundMicros;
|
|
162
|
+
/** Set when a settled call reports more than its approved per-call bound. */
|
|
163
|
+
breached = false;
|
|
164
|
+
constructor(approval, bound, stopFraction, started, now = Date.now) {
|
|
165
|
+
this.approval = approval;
|
|
166
|
+
this.bound = bound;
|
|
167
|
+
this.stopFraction = stopFraction;
|
|
168
|
+
this.started = started;
|
|
169
|
+
this.now = now;
|
|
170
|
+
if (!Number.isSafeInteger(bound) || bound < 1 || !(stopFraction > 0 && stopFraction <= 1))
|
|
171
|
+
throw new Error('invalid D12 budget');
|
|
172
|
+
this.boundMicros = dagLiveReservationMicros(approval.priceBound, bound);
|
|
173
|
+
}
|
|
174
|
+
usd(totals = this.total) { return totals.usdMicros / 1_000_000; }
|
|
175
|
+
pattern(pattern) { const { calls, tokens, usdMicros } = this.patterns.get(pattern) ?? { calls: 0, tokens: 0, usdMicros: 0 }; return { calls, tokens, usdMicros }; }
|
|
176
|
+
exceeds(spent, limits, elapsed) {
|
|
177
|
+
const f = this.stopFraction;
|
|
178
|
+
if (spent.calls + 1 > Math.floor(limits.calls * f))
|
|
179
|
+
return 'calls';
|
|
180
|
+
if (spent.tokens + this.bound > Math.floor(limits.tokens * f))
|
|
181
|
+
return 'tokens';
|
|
182
|
+
if (spent.usdMicros + this.boundMicros > Math.floor(limits.usd * 1_000_000 * f))
|
|
183
|
+
return 'usd';
|
|
184
|
+
if (elapsed >= limits.wallClockMs * f)
|
|
185
|
+
return 'wall-clock';
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
reserve(pattern) {
|
|
189
|
+
if (this.breached)
|
|
190
|
+
throw new DagLiveStop('budget-run-usage-bound');
|
|
191
|
+
const now = this.now();
|
|
192
|
+
const current = this.patterns.get(pattern) ?? { calls: 0, tokens: 0, usdMicros: 0, started: now };
|
|
193
|
+
const global = this.exceeds(this.total, this.approval.budget, now - this.started);
|
|
194
|
+
const local = this.exceeds(current, this.approval.budget.perPattern, now - current.started);
|
|
195
|
+
if (global || local)
|
|
196
|
+
throw new DagLiveStop(`budget-${global ? 'run' : 'pattern'}-${global ?? local}`);
|
|
197
|
+
for (const target of [this.total, current]) {
|
|
198
|
+
target.calls++;
|
|
199
|
+
target.tokens += this.bound;
|
|
200
|
+
target.usdMicros += this.boundMicros;
|
|
201
|
+
}
|
|
202
|
+
this.patterns.set(pattern, current);
|
|
203
|
+
let settled = false;
|
|
204
|
+
return usage => {
|
|
205
|
+
if (settled)
|
|
206
|
+
throw new Error('D12 reservation already settled');
|
|
207
|
+
settled = true;
|
|
208
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
209
|
+
const input = count(usage?.inputTokens), output = count(usage?.outputTokens);
|
|
210
|
+
// Observed spend above the per-call bound is a breach, never a clamp: the full
|
|
211
|
+
// amount is charged below, and later reservations are refused. Settlement itself
|
|
212
|
+
// never throws, so the completed call is always recorded.
|
|
213
|
+
if ((input ?? 0) + (output ?? 0) > this.bound)
|
|
214
|
+
this.breached = true;
|
|
215
|
+
const complete = input !== null && output !== null;
|
|
216
|
+
const known = (input ?? 0) + (output ?? 0);
|
|
217
|
+
const charged = complete ? { tokens: known, usdMicros: dagLiveChargeMicros(this.approval.priceBound, input, output) }
|
|
218
|
+
: { tokens: Math.max(this.bound, known), usdMicros: Math.max(this.boundMicros, dagLiveChargeMicros(this.approval.priceBound, input ?? 0, output ?? 0)) };
|
|
219
|
+
for (const target of [this.total, current]) {
|
|
220
|
+
target.tokens += charged.tokens - this.bound;
|
|
221
|
+
target.usdMicros += charged.usdMicros - this.boundMicros;
|
|
222
|
+
}
|
|
223
|
+
return charged;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
class RunControl {
|
|
228
|
+
stopped = null;
|
|
229
|
+
/** The provider call whose outcome stopped the run, with its reported usage. */
|
|
230
|
+
stoppingCall = null;
|
|
231
|
+
/** Set when a call exhausts its retry: the current task is a measurement failure, not a run stop. */
|
|
232
|
+
taskFailure = null;
|
|
233
|
+
stop(reason) { this.stopped ??= reason; }
|
|
234
|
+
}
|
|
235
|
+
const RETRYABLE = new Set(DAG_LIVE_ANALYSIS.providerFailurePolicy.retryableOutcomes);
|
|
236
|
+
/**
|
|
237
|
+
* Budget and identity guard around the real Jev adapter. Reservation precedes credential
|
|
238
|
+
* resolution and transport; any failure, missing usage or served-model mismatch stops the run.
|
|
239
|
+
*/
|
|
240
|
+
class MeteredJevAdapter {
|
|
241
|
+
inner;
|
|
242
|
+
budget;
|
|
243
|
+
model;
|
|
244
|
+
bound;
|
|
245
|
+
control;
|
|
246
|
+
now;
|
|
247
|
+
record;
|
|
248
|
+
id = 'jev';
|
|
249
|
+
version;
|
|
250
|
+
context = null;
|
|
251
|
+
inflight = Promise.resolve();
|
|
252
|
+
/** The evaluator may return on a deadline before this adapter finishes; callers wait for the accounting to settle. */
|
|
253
|
+
async idle() { await this.inflight.catch(() => undefined); }
|
|
254
|
+
constructor(inner, budget, model, bound, control, now, record) {
|
|
255
|
+
this.inner = inner;
|
|
256
|
+
this.budget = budget;
|
|
257
|
+
this.model = model;
|
|
258
|
+
this.bound = bound;
|
|
259
|
+
this.control = control;
|
|
260
|
+
this.now = now;
|
|
261
|
+
this.record = record;
|
|
262
|
+
this.version = inner.version;
|
|
263
|
+
}
|
|
264
|
+
capabilities() { return this.inner.capabilities(); }
|
|
265
|
+
compile(request) { return this.inner.compile(request); }
|
|
266
|
+
evaluate(request) {
|
|
267
|
+
const result = this.evaluateMetered(request);
|
|
268
|
+
this.inflight = result;
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
async evaluateMetered(request) {
|
|
272
|
+
const notSent = (reason) => ({ status: 'error', reason, uncertainty: null, actualModel: null,
|
|
273
|
+
usage: { inputTokens: null, outputTokens: null, costUsd: null }, requestId: null, dispatchCertainty: 'not-sent' });
|
|
274
|
+
const context = this.context;
|
|
275
|
+
if (!context || this.control.stopped) {
|
|
276
|
+
this.control.stop(this.control.stopped ?? 'unscoped-call');
|
|
277
|
+
return notSent('cancelled');
|
|
278
|
+
}
|
|
279
|
+
if (request.target.model !== this.model) {
|
|
280
|
+
this.control.stop('model-mismatch');
|
|
281
|
+
return notSent('invalid-request');
|
|
282
|
+
}
|
|
283
|
+
const retries = DAG_LIVE_ANALYSIS.providerFailurePolicy.retriesPerCall;
|
|
284
|
+
for (let attempt = 1;; attempt++) {
|
|
285
|
+
// Every attempt, including the retry, is reserved before dispatch and charged.
|
|
286
|
+
let settle;
|
|
287
|
+
try {
|
|
288
|
+
settle = this.budget.reserve(context.pattern);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
this.control.stop(error instanceof DagLiveStop ? error.reason : 'budget');
|
|
292
|
+
return notSent('budget-exhausted');
|
|
293
|
+
}
|
|
294
|
+
const started = this.now();
|
|
295
|
+
let observation = null;
|
|
296
|
+
try {
|
|
297
|
+
observation = await this.inner.evaluate(request);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
observation = null;
|
|
301
|
+
}
|
|
302
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
303
|
+
const inputTokens = count(observation?.usage.inputTokens), outputTokens = count(observation?.usage.outputTokens);
|
|
304
|
+
const charged = settle({ inputTokens, outputTokens });
|
|
305
|
+
const outcome = !observation ? 'adapter-exception'
|
|
306
|
+
// Only the per-task deadline cancels a call while the run is still live: that is a provider timeout.
|
|
307
|
+
: observation.status !== 'success' ? (observation.reason === 'cancelled' && !this.control.stopped ? 'provider-timeout' : `provider-${observation.reason}`)
|
|
308
|
+
: observation.actualModel !== this.model ? 'served-model-mismatch'
|
|
309
|
+
: !observation.requestId ? 'missing-request-id'
|
|
310
|
+
: inputTokens === null || outputTokens === null ? 'unknown-usage'
|
|
311
|
+
: inputTokens + outputTokens > this.bound ? 'usage-exceeded-reservation' : 'ok';
|
|
312
|
+
const retryable = RETRYABLE.has(outcome);
|
|
313
|
+
// Reported usage is recorded and charged for every attempt, including the one that stops the run.
|
|
314
|
+
const call = { ...context, ordinal: this.budget.total.calls, status: observation?.status ?? 'error',
|
|
315
|
+
reason: observation?.reason ?? 'adapter-exception', requestId: observation?.requestId ?? null,
|
|
316
|
+
servedModel: observation?.actualModel === this.model ? this.model : null, inputTokens, outputTokens,
|
|
317
|
+
latencyMs: Math.max(0, this.now() - started), chargedTokens: charged.tokens, chargedUsdMicros: charged.usdMicros, attempt, outcome };
|
|
318
|
+
if (outcome !== 'ok' && !retryable) {
|
|
319
|
+
this.control.stop(outcome);
|
|
320
|
+
this.control.stoppingCall ??= call;
|
|
321
|
+
}
|
|
322
|
+
await this.record(call);
|
|
323
|
+
if (!observation)
|
|
324
|
+
throw new DecisionGraphError('D12 adapter failed');
|
|
325
|
+
if (outcome === 'ok' || !retryable)
|
|
326
|
+
return observation;
|
|
327
|
+
if (attempt > retries || this.control.stopped) {
|
|
328
|
+
this.control.taskFailure ??= { arm: context.arm, node: context.node, reason: outcome };
|
|
329
|
+
return observation.status === 'success' ? { ...observation, status: 'error', reason: 'invalid-output', value: undefined } : observation;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const SKILL_TYPES = { type: ['object', 'array', 'string', 'number', 'boolean', 'null'] };
|
|
335
|
+
function modelLabel(model) {
|
|
336
|
+
return /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(model) ? model : `m${dagLiveDigest(model).slice(7, 23)}`;
|
|
337
|
+
}
|
|
338
|
+
function policy(model, region) {
|
|
339
|
+
const origin = new URL(JEV_ENDPOINT).origin;
|
|
340
|
+
const field = (name, source) => ({ pointer: `/${name}`, output: name, source, subject: 'synthetic-d12', trust: 'untrusted',
|
|
341
|
+
sensitivity: 'public', purpose: 'd12-qualification', retentionClass: 'qualification', accessScopes: ['d12-reviewer'],
|
|
342
|
+
exportPolicy: 'denied', deletionPolicy: 'erase', backupPolicy: 'not-persisted',
|
|
343
|
+
allowedProviders: ['jev'], allowedModels: [model], allowedOrigins: [origin], allowedRegions: [region] });
|
|
344
|
+
return { version: 'd12-live-v1', provider: 'jev', model, region, origin, purpose: 'd12-qualification', allowIncompleteContext: false,
|
|
345
|
+
fields: [field('subject', 'synthetic-workload'), field('evidence', 'graph-predecessor-projection')] };
|
|
346
|
+
}
|
|
347
|
+
function nodeArtifacts(nodeId, definition, target, timeoutMs) {
|
|
348
|
+
const ruleset = { apiVersion: 'decision.aiwg.io/v1alpha1', kind: 'DecisionRuleset',
|
|
349
|
+
metadata: { id: `${definition.metadata.id}-${nodeId}`.slice(0, 100), version: '1.0.0', description: 'D12 live qualification node (#2686)' },
|
|
350
|
+
spec: { purpose: 'Record one synthetic answer; no action is authorized.', inputSchema: definition.spec.inputSchema,
|
|
351
|
+
evaluations: [{ alias: 'answer', decision: artifactPin(definition), inputPointer: '' }],
|
|
352
|
+
rules: [{ id: 'answered', priority: 1, when: { op: 'eq', left: { source: 'decision', alias: 'answer', pointer: '/status' }, right: 'success' }, outcome: 'answered' }],
|
|
353
|
+
composition: 'first-match', conflict: 'review', defaultOutcome: 'unanswered', failureOutcome: 'unanswered', outputSchema: { enum: ['answered', 'unanswered'] } } };
|
|
354
|
+
const binding = { apiVersion: 'decision.aiwg.io/v1alpha1', kind: 'DecisionBinding',
|
|
355
|
+
metadata: { id: `${ruleset.metadata.id}-binding`.slice(0, 100), version: '1.0.0', description: 'D12 live qualification binding (#2686)' },
|
|
356
|
+
spec: { ruleset: artifactPin(ruleset), totalTimeoutMs: timeoutMs, maxAttempts: 1, concurrency: 1,
|
|
357
|
+
evaluations: { answer: { targets: [target], fallbackOn: [] } } } };
|
|
358
|
+
validateRuleset(ruleset);
|
|
359
|
+
validateBinding(binding, ruleset);
|
|
360
|
+
return { ruleset, binding, definitions: { [definition.metadata.id]: definition } };
|
|
361
|
+
}
|
|
362
|
+
const HOST_LOCAL_PIN = artifactPin({ metadata: { id: 'dag-live-host-local', version: '1.0.0' },
|
|
363
|
+
spec: { purpose: 'Deterministic host-local stage: fixed taxonomy expansion or beam selection. No inference.' } });
|
|
364
|
+
/** Deterministic host projectors. Model output only selects among host-declared options. */
|
|
365
|
+
function project(task, nodeId, value, input) {
|
|
366
|
+
const text = typeof value === 'string' ? value : null;
|
|
367
|
+
if (nodeId === 'flat' || nodeId === 'fallback' || nodeId.startsWith('detail-'))
|
|
368
|
+
return { result: text ?? fail() };
|
|
369
|
+
if (nodeId === 'shortlist') {
|
|
370
|
+
const sections = task.subject.sections;
|
|
371
|
+
const index = sections.findIndex(section => section.id === text);
|
|
372
|
+
if (text !== 'none' && index < 0)
|
|
373
|
+
fail();
|
|
374
|
+
return { 'has-candidates': index >= 0, candidates: index < 0 ? { sectionIndex: null, items: [] }
|
|
375
|
+
: { sectionIndex: index, section: sections[index].name, items: sections[index].items.map(item => item.name) } };
|
|
376
|
+
}
|
|
377
|
+
if (nodeId === 'rerank') {
|
|
378
|
+
const sections = task.subject.sections;
|
|
379
|
+
const candidates = input.candidates;
|
|
380
|
+
const position = /^position-([1-9])$/.exec(text ?? '');
|
|
381
|
+
const item = position ? sections[candidates?.sectionIndex]?.items[Number(position[1]) - 1] : undefined;
|
|
382
|
+
return { result: item?.id ?? fail() };
|
|
383
|
+
}
|
|
384
|
+
if (nodeId === 'branch-a' || nodeId === 'branch-b')
|
|
385
|
+
return { score: typeof value === 'number' ? value : fail() };
|
|
386
|
+
if (nodeId === 'extractor')
|
|
387
|
+
return { evidence: { status: text ?? fail() } };
|
|
388
|
+
if (nodeId === 'verifier') {
|
|
389
|
+
const prior = input.evidence?.status;
|
|
390
|
+
if (typeof prior !== 'string' || !['supported', 'not-supported'].includes(text ?? ''))
|
|
391
|
+
fail();
|
|
392
|
+
return { result: prior, 'needs-fallback': text === 'not-supported' };
|
|
393
|
+
}
|
|
394
|
+
return fail();
|
|
395
|
+
}
|
|
396
|
+
function fail() { throw new DecisionGraphError('D12 projection rejected an out-of-contract answer'); }
|
|
397
|
+
/** Builds both arms for one task. Pins are resolved from the frozen task before any dispatch. */
|
|
398
|
+
export function buildDagLiveArms(task, options) {
|
|
399
|
+
const definitions = dagLiveDefinitions(task);
|
|
400
|
+
const timeoutMs = Math.min(30_000, options.taskDeadlineMs);
|
|
401
|
+
const target = { adapter: 'jev', adapterVersion: '1.0.0', model: options.model, credentialRef: options.secretServiceReference,
|
|
402
|
+
requiredCapabilities: [], acceptance: { mode: 'typed-value' }, timeoutMs, retry: { maxRetries: 0, initialDelayMs: 0, maxDelayMs: 0 } };
|
|
403
|
+
const requests = new Map(Object.entries(definitions).map(([node, definition]) => [node, nodeArtifacts(node, definition, target, timeoutMs)]));
|
|
404
|
+
const resolvedPins = new Set([HOST_LOCAL_PIN.digest]);
|
|
405
|
+
for (const item of requests.values()) {
|
|
406
|
+
resolvedPins.add(artifactPin(item.binding).digest);
|
|
407
|
+
Object.values(item.definitions).forEach(d => resolvedPins.add(artifactPin(d).digest));
|
|
408
|
+
}
|
|
409
|
+
const stateDigest = dagLiveDigest(task.subject);
|
|
410
|
+
const label = modelLabel(options.model);
|
|
411
|
+
const config = (node) => {
|
|
412
|
+
const item = requests.get(node);
|
|
413
|
+
return { subject: 'synthetic-d12', target: HOST_LOCAL.has(node) ? (node === 'select' ? 'beam' : 'host') : 'jev', model: label, egress: 'jev-network', stateDigest,
|
|
414
|
+
definition: item ? artifactPin(Object.values(item.definitions)[0]) : HOST_LOCAL_PIN, binding: item ? artifactPin(item.binding) : HOST_LOCAL_PIN };
|
|
415
|
+
};
|
|
416
|
+
const jevNodes = task.pattern === 'shortlist-rerank' ? 2 : 3;
|
|
417
|
+
const localNodes = task.pattern === 'taxonomy-beam' ? 2 : 0;
|
|
418
|
+
const boundMicros = options.reservationMicros;
|
|
419
|
+
// One spare call of slack: Flow treats a realized resource equal to its ceiling as exceeded.
|
|
420
|
+
const budget = { attempts: jevNodes + localNodes + 1, deadlineMs: options.taskDeadlineMs, tokens: (jevNodes + 1) * options.perCallTokenBound + localNodes,
|
|
421
|
+
costMicros: (jevNodes + 1) * boundMicros + localNodes, fanOut: 2, beamWidth: 2, depth: 4, concurrency: 1 };
|
|
422
|
+
const base = { id: `d12-${task.id}`, budget, resolvedPins };
|
|
423
|
+
let template = task.pattern === 'shortlist-rerank' ? shortlistRerankTemplate({ ...base, shortlist: config('shortlist'), rerank: config('rerank') })
|
|
424
|
+
: task.pattern === 'taxonomy-beam' ? taxonomyBeamTemplate({ ...base, taxonomy: config('taxonomy'),
|
|
425
|
+
branches: [{ id: 'branch-a', config: config('branch-a') }, { id: 'branch-b', config: config('branch-b') }], select: config('select'),
|
|
426
|
+
details: config('detail-branch-a') })
|
|
427
|
+
: extractorVerifierFallbackTemplate({ ...base, extractor: config('extractor'), verifier: config('verifier'), fallback: config('fallback') });
|
|
428
|
+
if (task.pattern === 'taxonomy-beam') {
|
|
429
|
+
// Each detail node asks its own branch's leaves; the template shares one config, so pin per node and replan.
|
|
430
|
+
for (const node of template.graph.nodes)
|
|
431
|
+
if (node.id.startsWith('detail-'))
|
|
432
|
+
Object.assign(node, { definition: config(node.id).definition, binding: config(node.id).binding });
|
|
433
|
+
template = { graph: template.graph, plan: planDecisionGraph(template.graph, resolvedPins) };
|
|
434
|
+
}
|
|
435
|
+
const graph = template.graph;
|
|
436
|
+
const flow = decisionGraphToFlow(graph, { resolvedPins, decisionSkillId: options.skillId, terminal: graph.terminals[0], ceilings: NARROW });
|
|
437
|
+
const flatGraph = { schemaVersion: 'decision-graph/v1', id: `d12-flat-${task.id}`, pattern: 'custom', entry: 'flat', terminals: ['flat'],
|
|
438
|
+
nodes: [{ id: 'flat', stage: 0, ...config('flat'), input: [], output: ['result'] }], edges: [],
|
|
439
|
+
budget: { attempts: 2, deadlineMs: options.taskDeadlineMs, tokens: 2 * options.perCallTokenBound, costMicros: 2 * boundMicros, fanOut: 1, beamWidth: 1, depth: 1, concurrency: 1 } };
|
|
440
|
+
return { resolvedPins, baseline: { graph: flatGraph, flow: flatBaselineFlow(options.skillId, options.perCallTokenBound, boundMicros, options.taskDeadlineMs), requests },
|
|
441
|
+
candidate: { graph, flow, requests, template } };
|
|
442
|
+
}
|
|
443
|
+
/** Independently authored explicit FlowGraph: one skill node answers the whole task. */
|
|
444
|
+
function flatBaselineFlow(skill, bound, boundMicros, deadlineMs) {
|
|
445
|
+
return { apiVersion: 'flow.aiwg.io/v1alpha1', kind: 'FlowGraph', metadata: { name: 'd12-flat-baseline' }, spec: {
|
|
446
|
+
entry: ['flat'], candidates: [{ id: skill, kind: 'skill' }], state: { fields: [] }, permissions: [], capabilities: [],
|
|
447
|
+
ceilings: { activations: 2, tokens: 2 * bound, costUsd: (2 * boundMicros) / 1_000_000, timeMs: deadlineMs, concurrency: 1 },
|
|
448
|
+
nodes: [{ id: 'flat', kind: 'skill', ref: skill, phase: 'stage-0', inputs: [], outputs: [{ name: 'result', schema: SKILL_TYPES }],
|
|
449
|
+
capabilities: [], permissions: [], sideEffectMode: 'none', retry: { limit: 0, backoff: 'none', on: ['failure'] } }],
|
|
450
|
+
routes: [], joins: [], failure: { onNodeFailure: 'fail', maxFailures: 0 },
|
|
451
|
+
output: { mode: 'final-only', from: 'flat.result', schema: SKILL_TYPES }, trace: { level: 'metadata', redact: [] }
|
|
452
|
+
} };
|
|
453
|
+
}
|
|
454
|
+
/** Stop rule: no arm may contain a node able to request an action or permission. */
|
|
455
|
+
export function speculativeActionViolation(flow) {
|
|
456
|
+
const spec = flow?.spec;
|
|
457
|
+
return !spec || !Array.isArray(spec.nodes) || (spec.permissions?.length ?? 0) > 0 || spec.nodes.some(node => !['skill', 'gate'].includes(node.kind ?? '')
|
|
458
|
+
|| node.sideEffectMode !== 'none' || (node.permissions?.length ?? 0) > 0 || (node.capabilities?.length ?? 0) > 0);
|
|
459
|
+
}
|
|
460
|
+
/** Stop rule: a pattern's cumulative candidate calls exceed the multiple of its Flow baseline calls. */
|
|
461
|
+
export function patternCallRatioExceeded(pairs, pattern, maxRatio) {
|
|
462
|
+
const rows = pairs.filter(row => row.pattern === pattern && !row.measurementFailure);
|
|
463
|
+
const baseline = rows.reduce((sum, row) => sum + row.baseline.calls, 0), candidate = rows.reduce((sum, row) => sum + row.candidate.calls, 0);
|
|
464
|
+
return candidate > maxRatio * baseline;
|
|
465
|
+
}
|
|
466
|
+
/** Operator-facing defaults for the dry run and approval template (USD 0.10/1M ceiling, USD 2.00 cap). */
|
|
467
|
+
export const DAG_LIVE_DEFAULT_LIMITS = {
|
|
468
|
+
// Room for the worst case including one retry per call (2200 calls, 8.8M tokens, USD 0.88 reserved) under the 80% stop.
|
|
469
|
+
budget: { usd: 2, calls: 3_000, tokens: 12_000_000, wallClockMs: 14_400_000,
|
|
470
|
+
perPattern: { usd: 0.5, calls: 1_000, tokens: 4_000_000, wallClockMs: 5_400_000 } },
|
|
471
|
+
priceBound: { inputUsdPerMTok: 0.042, outputUsdPerMTok: 0, evidenceReferences: [
|
|
472
|
+
'https://www.eesel.ai/blog/typesafe-jev-pricing', 'https://www.mindstudio.ai/blog/jev-pricing-cost-per-token',
|
|
473
|
+
'roctinam/aiwg#2613 comment 153093: live smoke jev-1.13.0, 369 input / 38 output tokens'
|
|
474
|
+
],
|
|
475
|
+
approvalReference: '<operator attestation reference>' },
|
|
476
|
+
priorSpendUsd: 0,
|
|
477
|
+
taskDeadlineMs: 120_000,
|
|
478
|
+
};
|
|
479
|
+
/** Unsigned approval skeleton: every `<...>` value must be filled in and reviewed by the promotion owner. */
|
|
480
|
+
export function dagLiveApprovalTemplate(workload, preregistration) {
|
|
481
|
+
return { schemaVersion: 'dag-live-approval/v1', approved: '<true once reviewed>', reviewer: preregistration.promotionOwner, stagingHost: 'titan',
|
|
482
|
+
runId: '<unique-run-id>', sourceCommit: '<exact 40-hex commit>', exactHeadCi: '<green CI run URL for that commit>', model: '<pinned Jev model id>',
|
|
483
|
+
apiRevision: 'v1', region: '<recorded deployment region>', secretServiceReference: 'openbao.typesafe.jev.api-key',
|
|
484
|
+
credentialResolverDigest: '<sha256 of tools/decision/jev-openbao-credential.mjs>', workloadDigest: dagLiveDigest(workload),
|
|
485
|
+
preregistrationDigest: dagLiveDigest(preregistration), ...structuredClone(DAG_LIVE_DEFAULT_LIMITS),
|
|
486
|
+
secretService: { origin: 'https://rca-g2.s9.internal:8200', secretPathDigest: dagLiveSecretPathDigest('kv_internal/data/typesafe/jev/api-key') } };
|
|
487
|
+
}
|
|
488
|
+
function invoker(context, arm, setup) {
|
|
489
|
+
const { task, approval, adapter, control } = context;
|
|
490
|
+
const unknownCostBoundUsd = dagLiveReservationMicros(approval.priceBound, context.perCallTokenBound) / 1_000_000;
|
|
491
|
+
const projection = policy(approval.model, approval.region);
|
|
492
|
+
return async (flow) => {
|
|
493
|
+
if (control.taskFailure)
|
|
494
|
+
throw new DecisionGraphError('D12 task is a measurement failure');
|
|
495
|
+
if (control.stopped || context.signal.aborted) {
|
|
496
|
+
control.stop(control.stopped ?? 'deadline');
|
|
497
|
+
throw new DecisionGraphError('D12 run stopped');
|
|
498
|
+
}
|
|
499
|
+
const { node, input } = decisionFlowNode(setup.graph, flow);
|
|
500
|
+
if (HOST_LOCAL.has(node.id)) {
|
|
501
|
+
if (node.id !== 'taxonomy')
|
|
502
|
+
throw new DecisionGraphError('unexpected host-local node');
|
|
503
|
+
// Caller-authored fixed children: no model can create or rename a branch.
|
|
504
|
+
return { outputs: { children: ['branch-a', 'branch-b'] }, attempts: 1, usage: { tokens: 0, costUsd: 0, timeMs: 0 } };
|
|
505
|
+
}
|
|
506
|
+
const resolved = setup.requests.get(node.id);
|
|
507
|
+
if (!resolved)
|
|
508
|
+
throw new DecisionGraphError('unresolved D12 node');
|
|
509
|
+
assertDecisionFlowPins(node, resolved.binding, Object.values(resolved.definitions));
|
|
510
|
+
adapter.context = { pattern: task.pattern, taskId: task.id, arm, node: node.id };
|
|
511
|
+
let result;
|
|
512
|
+
try {
|
|
513
|
+
result = await evaluateDecisionRuleset({ ...resolved, input: { subject: structuredClone(task.subject), evidence: input },
|
|
514
|
+
runId: flow.runId, invocationId: flow.invocationKey, adapters: { jev: adapter },
|
|
515
|
+
resolveCredential: async (reference) => {
|
|
516
|
+
if (reference !== approval.secretServiceReference || control.stopped)
|
|
517
|
+
throw new Error('D12 credential access denied');
|
|
518
|
+
return context.host.resolveCredential(reference);
|
|
519
|
+
},
|
|
520
|
+
projection: { resolve: () => structuredClone(projection) }, signal: context.signal });
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
adapter.context = null;
|
|
524
|
+
}
|
|
525
|
+
const evaluation = result.spec.evaluations.answer;
|
|
526
|
+
if (!evaluation || evaluation.spec.status !== 'success' || evaluation.spec.value === undefined) {
|
|
527
|
+
await adapter.idle();
|
|
528
|
+
// The per-task deadline (not a run stop) is a provider timeout for this task.
|
|
529
|
+
if (!control.taskFailure && !control.stopped && context.signal.aborted)
|
|
530
|
+
control.taskFailure = { arm, node: node.id, reason: 'provider-timeout' };
|
|
531
|
+
// An exhausted retry is a measurement failure for this task; anything else stops the run.
|
|
532
|
+
if (!control.taskFailure)
|
|
533
|
+
control.stop(control.stopped ?? `evaluation-${evaluation?.spec.reason ?? result.spec.reason}`);
|
|
534
|
+
throw new DecisionGraphError('D12 evaluation failed');
|
|
535
|
+
}
|
|
536
|
+
const value = evaluation.spec.value;
|
|
537
|
+
return decisionFlowResponse(setup.graph, node, result, { unknownCostBoundUsd, project: () => {
|
|
538
|
+
try {
|
|
539
|
+
return project(task, node.id, value, input);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
control.stop('out-of-contract-answer');
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
} });
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function armCalls(calls, taskId, arm) {
|
|
549
|
+
const rows = calls.filter(call => call.taskId === taskId && call.arm === arm);
|
|
550
|
+
// Calls are first attempts (the graph's shape); retries are counted separately. Tokens include every attempt.
|
|
551
|
+
return { calls: rows.filter(row => row.attempt === 1).length, retries: rows.filter(row => row.attempt > 1).length, inputTokens: rows.reduce((sum, row) => sum + (row.inputTokens ?? 0), 0),
|
|
552
|
+
outputTokens: rows.reduce((sum, row) => sum + (row.outputTokens ?? 0), 0), requestIds: rows.flatMap(row => row.requestId ? [row.requestId] : []), rows };
|
|
553
|
+
}
|
|
554
|
+
async function runBaseline(context, arms) {
|
|
555
|
+
const started = context.now();
|
|
556
|
+
const report = await context.executeFlow(arms.baseline.flow, { validation: { catalogIds: new Set([context.skillId]) },
|
|
557
|
+
invokeNode: invoker(context, 'baseline', arms.baseline), runId: `d12-flat-${context.task.id}` });
|
|
558
|
+
const latencyMs = Math.max(0, context.now() - started);
|
|
559
|
+
const usage = armCalls(context.calls, context.task.id, 'baseline');
|
|
560
|
+
const raw = report.status === 'completed' ? report.results.flat?.outputs.result : undefined;
|
|
561
|
+
if (typeof raw !== 'string' && !context.control.taskFailure)
|
|
562
|
+
context.control.stop(context.control.stopped ?? `baseline-${report.status}`);
|
|
563
|
+
const answer = typeof raw === 'string' ? raw : null;
|
|
564
|
+
const { rows: _rows, ...totals } = usage;
|
|
565
|
+
return { answer, correct: answer === context.task.label, ...totals, latencyMs };
|
|
566
|
+
}
|
|
567
|
+
async function runCandidate(context, arms) {
|
|
568
|
+
const { graph, template, flow } = arms.candidate;
|
|
569
|
+
const records = [];
|
|
570
|
+
const perCallMicros = dagLiveReservationMicros(context.approval.priceBound, context.perCallTokenBound);
|
|
571
|
+
const started = context.now();
|
|
572
|
+
const adapter = admittedDecisionFlowAdapter(new GraphBudgetLedger(graph, template.plan, NARROW), request => HOST_LOCAL.has(request.node.id) ? { attempts: 1, tokens: 1, costMicros: 1 } : { attempts: 1, tokens: context.perCallTokenBound, costMicros: perCallMicros }, graphBeamFlowInvoker(graph, invoker(context, 'candidate', arms.candidate), NARROW), context.signal, record => records.push(structuredClone(record)));
|
|
573
|
+
const report = await context.executeFlow(flow, { validation: { catalogIds: new Set([context.skillId]) }, invokeNode: adapter, runId: `d12-dag-${context.task.id}` });
|
|
574
|
+
const latencyMs = Math.max(0, context.now() - started);
|
|
575
|
+
const usage = armCalls(context.calls, context.task.id, 'candidate');
|
|
576
|
+
const { rows, ...totals } = usage;
|
|
577
|
+
let outcome = 'error';
|
|
578
|
+
let receiptDigest = '';
|
|
579
|
+
let answer = null;
|
|
580
|
+
let unusedCalls = 0;
|
|
581
|
+
let unusedTokens = 0;
|
|
582
|
+
if (context.control.taskFailure)
|
|
583
|
+
outcome = 'measurement-failure';
|
|
584
|
+
else if (!context.control.stopped) {
|
|
585
|
+
const receipt = finalizeDecisionGraphRun(graph, template.plan, report, records, NARROW);
|
|
586
|
+
outcome = receipt.outcome;
|
|
587
|
+
receiptDigest = receipt.receiptDigest;
|
|
588
|
+
const unused = new Set(receipt.evidence.stages.flatMap(stage => stage.nodes.filter(node => node.status === 'ok' && !node.used).map(node => node.id)));
|
|
589
|
+
// Speculative work outside the selected evidence stays in the totals and is reported here.
|
|
590
|
+
for (const row of rows)
|
|
591
|
+
if (unused.has(row.node)) {
|
|
592
|
+
unusedCalls++;
|
|
593
|
+
unusedTokens += (row.inputTokens ?? 0) + (row.outputTokens ?? 0);
|
|
594
|
+
}
|
|
595
|
+
if (outcome === 'complete' && receipt.value && typeof receipt.value.result === 'string')
|
|
596
|
+
answer = receipt.value.result;
|
|
597
|
+
else if (outcome === 'empty-shortlist' && context.task.pattern === 'shortlist-rerank')
|
|
598
|
+
answer = 'none';
|
|
599
|
+
else
|
|
600
|
+
context.control.stop(`candidate-outcome-${outcome}`);
|
|
601
|
+
}
|
|
602
|
+
return { answer, correct: answer !== null && answer === context.task.label, ...totals, latencyMs, outcome, receiptDigest, unusedCalls, unusedTokens };
|
|
603
|
+
}
|
|
604
|
+
const quantile = (values, q) => {
|
|
605
|
+
if (!values.length)
|
|
606
|
+
return null;
|
|
607
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
608
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* Paired analysis with the shared helpers at the preregistered level and margin. A pattern is
|
|
612
|
+
* eligible for the reviewer only with complete pairs, a non-inferior lower bound and every
|
|
613
|
+
* preregistered economics bound met. Unknown values fail closed.
|
|
614
|
+
*/
|
|
615
|
+
export function analyzeDagLivePattern(pattern, pairs, preregistration, price) {
|
|
616
|
+
const { nonInferiority: ni, economics: econ } = preregistration.analysis;
|
|
617
|
+
const all = pairs.filter(pair => pair.pattern === pattern);
|
|
618
|
+
const failures = all.filter(pair => pair.measurementFailure);
|
|
619
|
+
const rows = all.filter(pair => !pair.measurementFailure);
|
|
620
|
+
const n = rows.length;
|
|
621
|
+
const policy = preregistration.analysis.providerFailurePolicy;
|
|
622
|
+
const tolerance = Math.floor(policy.maxMeasurementFailureFractionPerPattern * preregistration.tasksPerPattern);
|
|
623
|
+
const table = { both: 0, candidateOnly: 0, baselineOnly: 0, neither: 0 };
|
|
624
|
+
const slices = {};
|
|
625
|
+
for (const row of rows) {
|
|
626
|
+
const key = row.candidate.correct ? (row.baseline.correct ? 'both' : 'candidateOnly') : (row.baseline.correct ? 'baselineOnly' : 'neither');
|
|
627
|
+
table[key]++;
|
|
628
|
+
const slice = slices[row.slice] ??= { n: 0, baselineCorrect: 0, candidateCorrect: 0 };
|
|
629
|
+
slice.n++;
|
|
630
|
+
slice.baselineCorrect += row.baseline.correct ? 1 : 0;
|
|
631
|
+
slice.candidateCorrect += row.candidate.correct ? 1 : 0;
|
|
632
|
+
}
|
|
633
|
+
const baselineCorrect = table.both + table.baselineOnly, candidateCorrect = table.both + table.candidateOnly;
|
|
634
|
+
const difference = n ? pairedBinaryDifferenceInterval({ counts: table, levelBps: ni.levelBps, method: ni.method }) : null;
|
|
635
|
+
const decision = difference ? pairedNonInferiority({ interval: difference, marginBps: ni.marginBps }) : { decision: 'insufficient', reason: 'no-pairs' };
|
|
636
|
+
const arm = (key) => {
|
|
637
|
+
const inputTokens = rows.reduce((sum, row) => sum + row[key].inputTokens, 0), outputTokens = rows.reduce((sum, row) => sum + row[key].outputTokens, 0);
|
|
638
|
+
return { calls: rows.reduce((sum, row) => sum + row[key].calls, 0), inputTokens, outputTokens,
|
|
639
|
+
billableUsd: (inputTokens * price.inputUsdPerMTok + outputTokens * price.outputUsdPerMTok) / 1_000_000,
|
|
640
|
+
p50LatencyMs: quantile(rows.map(row => row[key].latencyMs), 0.5), p95LatencyMs: quantile(rows.map(row => row[key].latencyMs), 0.95) };
|
|
641
|
+
};
|
|
642
|
+
const baseline = arm('baseline'), candidate = arm('candidate');
|
|
643
|
+
const callRatio = baseline.calls > 0 ? candidate.calls / baseline.calls : null;
|
|
644
|
+
const p95LatencyRatio = baseline.p95LatencyMs && candidate.p95LatencyMs !== null ? candidate.p95LatencyMs / baseline.p95LatencyMs : null;
|
|
645
|
+
// Per-task extra tokens scaled into [-1, 1] by the worst-case candidate tokens per task.
|
|
646
|
+
const scale = DAG_LIVE_MAX_CANDIDATE_CALLS[pattern] * preregistration.analysis.perCallTokenBound;
|
|
647
|
+
const differences = rows.map(row => (row.candidate.inputTokens + row.candidate.outputTokens - row.baseline.inputTokens - row.baseline.outputTokens) / scale);
|
|
648
|
+
let extraTokensPerTask = null;
|
|
649
|
+
if (differences.length >= 2 && differences.every(d => d >= -1 && d <= 1)) {
|
|
650
|
+
extraTokensPerTask = pairedMeanDifferenceBootstrap({ differences, levelBps: econ.bootstrap.levelBps, seed: econ.bootstrap.seed,
|
|
651
|
+
resamples: econ.bootstrap.resamples, bounds: [-1, 1] });
|
|
652
|
+
}
|
|
653
|
+
const extraTokensPerTaskUpper = extraTokensPerTask ? Math.ceil(extraTokensPerTask.upperBps / 10_000 * scale) : null;
|
|
654
|
+
const checks = { callRatio: callRatio !== null && callRatio <= econ.maxCallRatio,
|
|
655
|
+
p95LatencyRatio: p95LatencyRatio !== null && Number.isFinite(p95LatencyRatio) && p95LatencyRatio <= econ.maxP95LatencyRatio,
|
|
656
|
+
extraTokensPerTask: extraTokensPerTaskUpper !== null && extraTokensPerTaskUpper <= econ.maxExtraTokensPerTaskUpper };
|
|
657
|
+
const outcomes = {};
|
|
658
|
+
for (const row of rows)
|
|
659
|
+
outcomes[row.candidate.outcome] = (outcomes[row.candidate.outcome] ?? 0) + 1;
|
|
660
|
+
const complete = all.length === preregistration.tasksPerPattern && new Set(all.map(row => row.taskId)).size === all.length;
|
|
661
|
+
const withinTolerance = failures.length <= tolerance;
|
|
662
|
+
const qualifies = complete && withinTolerance && decision.decision === 'non-inferior';
|
|
663
|
+
const economicsPass = checks.callRatio && checks.p95LatencyRatio && checks.extraTokensPerTask;
|
|
664
|
+
const armSpend = (row, key) => ({ calls: row[key].calls, retries: row[key].retries ?? 0, tokens: row[key].inputTokens + row[key].outputTokens });
|
|
665
|
+
return { pattern, n, attempted: all.length, measurementFailures: failures.length, measurementFailureTolerance: tolerance, complete,
|
|
666
|
+
verdict: !withinTolerance || (!complete && failures.length > 0) ? 'insufficient-evidence' : qualifies && economicsPass ? 'eligible' : 'not-eligible',
|
|
667
|
+
quality: { baselineCorrect, candidateCorrect, table,
|
|
668
|
+
baselineAccuracyInterval: n ? wilsonScoreInterval({ events: baselineCorrect, n, levelBps: ni.levelBps }) : null,
|
|
669
|
+
candidateAccuracyInterval: n ? wilsonScoreInterval({ events: candidateCorrect, n, levelBps: ni.levelBps }) : null,
|
|
670
|
+
difference, nonInferiority: decision, slices },
|
|
671
|
+
economics: { baseline, candidate, net: { calls: candidate.calls - baseline.calls,
|
|
672
|
+
tokens: candidate.inputTokens + candidate.outputTokens - baseline.inputTokens - baseline.outputTokens,
|
|
673
|
+
billableUsd: candidate.billableUsd - baseline.billableUsd },
|
|
674
|
+
callRatio, p95LatencyRatio, extraTokensPerTask, extraTokensPerTaskUpper,
|
|
675
|
+
speculative: { unusedCalls: rows.reduce((sum, row) => sum + row.candidate.unusedCalls, 0), unusedTokens: rows.reduce((sum, row) => sum + row.candidate.unusedTokens, 0) },
|
|
676
|
+
outcomes, checks, pass: economicsPass,
|
|
677
|
+
retries: { baseline: rows.reduce((sum, row) => sum + (row.baseline.retries ?? 0), 0), candidate: rows.reduce((sum, row) => sum + (row.candidate.retries ?? 0), 0) },
|
|
678
|
+
measurementFailureSpend: failures.flatMap(row => [armSpend(row, 'baseline'), armSpend(row, 'candidate')])
|
|
679
|
+
.reduce((sum, item) => ({ calls: sum.calls + item.calls, retries: sum.retries + item.retries, tokens: sum.tokens + item.tokens }), { calls: 0, retries: 0, tokens: 0 }) },
|
|
680
|
+
eligible: qualifies && economicsPass };
|
|
681
|
+
}
|
|
682
|
+
/** Offline, credential-free worst-case plan against the approval limits. */
|
|
683
|
+
export function planDagLiveQualification(workload, preregistration, limits) {
|
|
684
|
+
validateDagLiveInputs(workload, preregistration);
|
|
685
|
+
const bound = preregistration.analysis.perCallTokenBound;
|
|
686
|
+
const fraction = preregistration.analysis.stopRules.budgetStopFraction;
|
|
687
|
+
const perCallMicros = dagLiveReservationMicros(limits.priceBound, bound);
|
|
688
|
+
const patterns = DAG_LIVE_PATTERNS.map(pattern => {
|
|
689
|
+
const tasks = workload.tasks.filter(task => task.pattern === pattern);
|
|
690
|
+
let expectedTokens = 0;
|
|
691
|
+
let largestCall = 0;
|
|
692
|
+
for (const task of tasks) {
|
|
693
|
+
for (const [node, definition] of Object.entries(dagLiveDefinitions(task))) {
|
|
694
|
+
const evidence = node === 'rerank' ? { candidates: { sectionIndex: 0, section: 'x'.repeat(24), items: Array(4).fill('x'.repeat(24)) } }
|
|
695
|
+
: node === 'verifier' || node === 'fallback' ? { evidence: { status: 'awaiting-pickup' } } : node.startsWith('branch-') ? { children: ['branch-a', 'branch-b'] } : {};
|
|
696
|
+
const tokens = estimator.estimate({ state: { untrusted: { subject: task.subject, evidence } }, question: JSON.parse(compileJevQuestion(definition).question) }).tokens + REQUEST_ENVELOPE_TOKENS;
|
|
697
|
+
largestCall = Math.max(largestCall, tokens);
|
|
698
|
+
expectedTokens += tokens;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const baselineCalls = tasks.length, candidateCalls = tasks.length * DAG_LIVE_MAX_CANDIDATE_CALLS[pattern];
|
|
702
|
+
const firstAttemptCalls = baselineCalls + candidateCalls;
|
|
703
|
+
// Every call may take the preregistered retry, and each retry is reserved like a call.
|
|
704
|
+
const retryCalls = firstAttemptCalls * preregistration.analysis.providerFailurePolicy.retriesPerCall;
|
|
705
|
+
const calls = firstAttemptCalls + retryCalls;
|
|
706
|
+
return { pattern, tasks: tasks.length, worstCase: { baselineCalls, candidateCalls, firstAttemptCalls, retryCalls, calls, tokens: calls * bound,
|
|
707
|
+
reservedUsd: calls * perCallMicros / 1_000_000 },
|
|
708
|
+
estimatedVisibleTokens: expectedTokens, largestEstimatedCallTokens: largestCall,
|
|
709
|
+
withinPatternStop: calls <= Math.floor(limits.budget.perPattern.calls * fraction) && calls * bound <= Math.floor(limits.budget.perPattern.tokens * fraction)
|
|
710
|
+
&& calls * perCallMicros <= Math.floor(limits.budget.perPattern.usd * 1_000_000 * fraction) };
|
|
711
|
+
});
|
|
712
|
+
const calls = patterns.reduce((sum, p) => sum + p.worstCase.calls, 0);
|
|
713
|
+
const firstAttempts = patterns.reduce((sum, p) => sum + p.worstCase.firstAttemptCalls, 0);
|
|
714
|
+
const reservedUsd = patterns.reduce((sum, p) => sum + p.worstCase.reservedUsd, 0);
|
|
715
|
+
const visible = patterns.reduce((sum, p) => sum + p.estimatedVisibleTokens, 0);
|
|
716
|
+
return { schemaVersion: 'dag-live-plan/v1', providerCalls: 0, workloadDigest: dagLiveDigest(workload), preregistrationDigest: dagLiveDigest(preregistration),
|
|
717
|
+
perCallTokenBound: bound, stopFraction: fraction, patterns,
|
|
718
|
+
totals: { worstCaseFirstAttemptCalls: firstAttempts, worstCaseCalls: calls, worstCaseTokens: calls * bound, worstCaseReservedUsd: Math.round(reservedUsd * 1e6) / 1e6,
|
|
719
|
+
estimatedVisibleTokens: visible, estimatedBillableUsdAtAttestedInputPrice: Math.round(visible * limits.priceBound.inputUsdPerMTok) / 1e6,
|
|
720
|
+
reservationUsdPerCall: perCallMicros / 1e6,
|
|
721
|
+
hardCapUsd: DAG_LIVE_HARD_CAP_USD },
|
|
722
|
+
boundCoversLargestEstimate: patterns.every(p => p.largestEstimatedCallTokens <= bound),
|
|
723
|
+
withinBudget: patterns.every(p => p.withinPatternStop) && calls <= Math.floor(limits.budget.calls * fraction)
|
|
724
|
+
&& calls * bound <= Math.floor(limits.budget.tokens * fraction) && reservedUsd <= limits.budget.usd * fraction };
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Spend of every earlier D12 run anywhere below the artifact root (depth-limited, symlinks not
|
|
728
|
+
* followed). A run with a summary counts its charged USD. A run with an approval but no summary
|
|
729
|
+
* (crashed or killed) counts each logged call at its charged amount (the reservation when a line
|
|
730
|
+
* carries none) plus one call in flight at the reservation. Directories that are not D12 runs are ignored.
|
|
731
|
+
*/
|
|
732
|
+
export async function dagLivePriorSpend(artifactRoot) {
|
|
733
|
+
let micros = 0;
|
|
734
|
+
const runs = [];
|
|
735
|
+
const readJson = async (path) => { try {
|
|
736
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return undefined;
|
|
740
|
+
} };
|
|
741
|
+
const directories = [];
|
|
742
|
+
const walk = async (relative, depth) => {
|
|
743
|
+
let entries;
|
|
744
|
+
try {
|
|
745
|
+
entries = await readdir(join(artifactRoot, relative), { withFileTypes: true });
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
if (!relative)
|
|
749
|
+
throw new Error('D12 artifact root is unreadable; prior spend cannot be established');
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
for (const entry of entries.filter(item => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
753
|
+
const child = relative ? `${relative}/${entry.name}` : entry.name;
|
|
754
|
+
directories.push(child);
|
|
755
|
+
if (depth < 8)
|
|
756
|
+
await walk(child, depth + 1);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
await walk('', 1);
|
|
760
|
+
for (const name of directories.sort()) {
|
|
761
|
+
const approval = await readJson(join(artifactRoot, name, 'approval.json'));
|
|
762
|
+
if (approval?.schemaVersion !== 'dag-live-approval/v1')
|
|
763
|
+
continue;
|
|
764
|
+
runs.push(name);
|
|
765
|
+
const summary = await readJson(join(artifactRoot, name, 'summary.json'));
|
|
766
|
+
const reported = [summary?.spend?.chargedUsd, summary?.reserved?.usd].filter(n => typeof n === 'number' && Number.isFinite(n) && n >= 0);
|
|
767
|
+
if (summary?.schemaVersion === 'dag-live-summary/v1' && reported.length) {
|
|
768
|
+
micros += Math.round(Math.max(...reported) * 1_000_000);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const bound = validPriceBound(approval.priceBound) ? approval.priceBound
|
|
772
|
+
: { inputUsdPerMTok: 0, outputUsdPerMTok: 0, evidenceReferences: [], approvalReference: '' };
|
|
773
|
+
const reservation = dagLiveReservationMicros(bound, DAG_LIVE_ANALYSIS.perCallTokenBound);
|
|
774
|
+
let lines = [];
|
|
775
|
+
try {
|
|
776
|
+
lines = (await readFile(join(artifactRoot, name, 'calls.jsonl'), 'utf8')).split('\n').filter(line => line.trim());
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
lines = [];
|
|
780
|
+
}
|
|
781
|
+
for (const line of lines) {
|
|
782
|
+
let charged;
|
|
783
|
+
try {
|
|
784
|
+
charged = JSON.parse(line)?.chargedUsdMicros;
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
charged = undefined;
|
|
788
|
+
}
|
|
789
|
+
micros += Number.isSafeInteger(charged) && charged >= 0 ? charged : reservation;
|
|
790
|
+
}
|
|
791
|
+
micros += reservation;
|
|
792
|
+
}
|
|
793
|
+
return { usd: micros / 1_000_000, runs };
|
|
794
|
+
}
|
|
795
|
+
/** The artifact root must BE the canonical AIWG artifact root, never a subdirectory of it. */
|
|
796
|
+
export async function assertDagLiveArtifactRoot(sourceRoot, artifactRoot) {
|
|
797
|
+
let route;
|
|
798
|
+
try {
|
|
799
|
+
route = JSON.parse(execFileSync('aiwg', ['artifacts', 'path', '--json', '--check-write'], { cwd: sourceRoot, encoding: 'utf8', timeout: 30_000 }));
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
throw new Error('D12 requires the canonical artifact root');
|
|
803
|
+
}
|
|
804
|
+
if (typeof route.artifact_root !== 'string' || await realpath(route.artifact_root) !== await realpath(artifactRoot)) {
|
|
805
|
+
throw new Error('D12 evidence must use the canonical artifact root itself');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/** Reads the resolver file once, checks that exact content against both pins, then imports those bytes. */
|
|
809
|
+
export async function loadDagLiveResolver(path, expectedDigest, approvedDigest) {
|
|
810
|
+
const bytes = await readFile(path);
|
|
811
|
+
const digest = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
812
|
+
if (digest !== expectedDigest || digest !== approvedDigest)
|
|
813
|
+
throw new Error('resolver-pin');
|
|
814
|
+
return import(`data:text/javascript;base64,${bytes.toString('base64')}`);
|
|
815
|
+
}
|
|
816
|
+
const write = (path, value) => writeFile(path, canonicalJson(value), { flag: 'wx', mode: 0o600 });
|
|
817
|
+
const fileDigest = async (path) => `sha256:${createHash('sha256').update(await readFile(path)).digest('hex')}`;
|
|
818
|
+
const liveGateOpen = () => process.env[DAG_LIVE_ENV_GATE] === '1';
|
|
819
|
+
/**
|
|
820
|
+
* Runs the approved paired collection and writes digest-bound evidence under
|
|
821
|
+
* `<artifactRoot>/<runId>/`. With `transport` the run is labelled synthetic and never
|
|
822
|
+
* counts as live evidence. Without it the explicit env gate and the full preregistered
|
|
823
|
+
* workload are required.
|
|
824
|
+
*/
|
|
825
|
+
export async function runDagLiveQualification(options) {
|
|
826
|
+
const approval = structuredClone(options.approval), workload = structuredClone(options.workload), preregistration = structuredClone(options.preregistration);
|
|
827
|
+
validateDagLiveApproval(approval, workload, preregistration);
|
|
828
|
+
const synthetic = options.transport !== undefined;
|
|
829
|
+
if (!synthetic && (!liveGateOpen() || workload.tasksPerPattern !== DAG_LIVE_TASKS_PER_PATTERN))
|
|
830
|
+
throw new Error('D12 live run requires the env gate and the full preregistered workload');
|
|
831
|
+
await assertContextLiveSource(options.sourceRoot, approval.sourceCommit);
|
|
832
|
+
await assertDagLiveArtifactRoot(options.sourceRoot, options.artifactRoot);
|
|
833
|
+
const { analysis } = preregistration;
|
|
834
|
+
// The USD 2.00 cap spans reruns: earlier runs on disk count, and the operator value is a floor.
|
|
835
|
+
const scanned = await dagLivePriorSpend(options.artifactRoot);
|
|
836
|
+
const priorMicros = Math.max(Math.round(approval.priorSpendUsd * 1_000_000), Math.round(scanned.usd * 1_000_000));
|
|
837
|
+
const capMicros = Math.min(Math.round(approval.budget.usd * 1_000_000), DAG_LIVE_HARD_CAP_USD * 1_000_000 - priorMicros);
|
|
838
|
+
if (capMicros * analysis.stopRules.budgetStopFraction < dagLiveReservationMicros(approval.priceBound, analysis.perCallTokenBound)) {
|
|
839
|
+
throw new Error('D12 prior spend leaves no budget for one call under the USD 2.00 cap');
|
|
840
|
+
}
|
|
841
|
+
const effectiveBudget = { ...approval.budget, usd: capMicros / 1_000_000 };
|
|
842
|
+
const now = options.now ?? Date.now;
|
|
843
|
+
const directory = resolve(options.artifactRoot, approval.runId);
|
|
844
|
+
await mkdir(directory, { recursive: false, mode: 0o700 });
|
|
845
|
+
// Frozen inputs are persisted before any credential or provider operation.
|
|
846
|
+
await write(join(directory, 'preregistration.json'), preregistration);
|
|
847
|
+
await write(join(directory, 'workload.json'), workload);
|
|
848
|
+
await write(join(directory, 'approval.json'), approval);
|
|
849
|
+
const callsPath = join(directory, 'calls.jsonl'), pairsPath = join(directory, 'pairs.jsonl');
|
|
850
|
+
await writeFile(callsPath, '', { flag: 'wx', mode: 0o600 });
|
|
851
|
+
await writeFile(pairsPath, '', { flag: 'wx', mode: 0o600 });
|
|
852
|
+
const started = now();
|
|
853
|
+
const budget = new DagLiveBudget({ budget: effectiveBudget, priceBound: approval.priceBound }, analysis.perCallTokenBound, analysis.stopRules.budgetStopFraction, started, now);
|
|
854
|
+
const control = new RunControl();
|
|
855
|
+
const calls = [];
|
|
856
|
+
const adapter = new MeteredJevAdapter(new JevDecisionAdapter({ region: approval.region, ...(synthetic ? { fetch: options.transport } : {}) }), budget, approval.model, analysis.perCallTokenBound, control, now, async (call) => { calls.push(call); await appendFile(callsPath, `${canonicalJson(call)}\n`); });
|
|
857
|
+
const controller = new AbortController();
|
|
858
|
+
const timer = setTimeout(() => { control.stop('budget-run-wall-clock'); controller.abort(); }, Math.min(2_147_483_647, Math.floor(approval.budget.wallClockMs * analysis.stopRules.budgetStopFraction)));
|
|
859
|
+
const pairs = [];
|
|
860
|
+
const abandonedPatterns = [];
|
|
861
|
+
// One secret-service read per run; each call gets a copy the adapter zeroes after use.
|
|
862
|
+
const vault = { secret: null };
|
|
863
|
+
const host = { resolveCredential: async (reference) => {
|
|
864
|
+
if (!vault.secret) {
|
|
865
|
+
const issued = await options.host.resolveCredential(reference);
|
|
866
|
+
try {
|
|
867
|
+
vault.secret = new Uint8Array(issued);
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
issued.fill(0);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return new Uint8Array(vault.secret);
|
|
874
|
+
} };
|
|
875
|
+
try {
|
|
876
|
+
for (const pattern of DAG_LIVE_PATTERNS) {
|
|
877
|
+
const tasks = workload.tasks.filter(task => task.pattern === pattern);
|
|
878
|
+
for (const [index, task] of tasks.entries()) {
|
|
879
|
+
if (control.stopped)
|
|
880
|
+
break;
|
|
881
|
+
const arms = buildDagLiveArms(task, { model: approval.model, secretServiceReference: approval.secretServiceReference, skillId: options.skillId,
|
|
882
|
+
perCallTokenBound: analysis.perCallTokenBound, reservationMicros: dagLiveReservationMicros(approval.priceBound, analysis.perCallTokenBound), taskDeadlineMs: approval.taskDeadlineMs });
|
|
883
|
+
if (speculativeActionViolation(arms.baseline.flow) || speculativeActionViolation(arms.candidate.flow)) {
|
|
884
|
+
control.stop('speculative-action');
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(approval.taskDeadlineMs)]);
|
|
888
|
+
const context = { task, index, approval, adapter, control, calls, host, executeFlow: options.executeFlow,
|
|
889
|
+
skillId: options.skillId, perCallTokenBound: analysis.perCallTokenBound, now, signal };
|
|
890
|
+
const order = index % 2 === 0 ? 'baseline-first' : 'candidate-first';
|
|
891
|
+
let baseline, candidate;
|
|
892
|
+
control.taskFailure = null;
|
|
893
|
+
const proceed = () => !control.stopped && !control.taskFailure;
|
|
894
|
+
try {
|
|
895
|
+
if (order === 'baseline-first') {
|
|
896
|
+
baseline = await runBaseline(context, arms);
|
|
897
|
+
if (proceed())
|
|
898
|
+
candidate = await runCandidate(context, arms);
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
candidate = await runCandidate(context, arms);
|
|
902
|
+
if (proceed())
|
|
903
|
+
baseline = await runBaseline(context, arms);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
if (!control.taskFailure)
|
|
908
|
+
control.stop(control.stopped ?? 'pair-exception');
|
|
909
|
+
}
|
|
910
|
+
// Settle any call the evaluator abandoned on a deadline before accounting for this task.
|
|
911
|
+
await adapter.idle();
|
|
912
|
+
if (control.stopped)
|
|
913
|
+
break;
|
|
914
|
+
const failure = control.taskFailure;
|
|
915
|
+
if (!failure && (!baseline || !candidate)) {
|
|
916
|
+
control.stop('pair-incomplete');
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
// A measurement failure is recorded for both arms and excluded from the paired analysis.
|
|
920
|
+
const emptyArm = (arm) => {
|
|
921
|
+
const { rows: _rows, ...usage } = armCalls(calls, task.id, arm);
|
|
922
|
+
return { answer: null, correct: false, ...usage, latencyMs: 0 };
|
|
923
|
+
};
|
|
924
|
+
const pair = { pattern, taskId: task.id, slice: task.slice, index, order, label: task.label,
|
|
925
|
+
baseline: failure ? { ...(baseline ?? emptyArm('baseline')), answer: null, correct: false } : baseline,
|
|
926
|
+
candidate: failure ? { ...(candidate ?? { ...emptyArm('candidate'), receiptDigest: '', unusedCalls: 0, unusedTokens: 0 }),
|
|
927
|
+
answer: null, correct: false, outcome: 'measurement-failure' } : candidate,
|
|
928
|
+
...(failure ? { measurementFailure: failure } : {}) };
|
|
929
|
+
pairs.push(pair);
|
|
930
|
+
await appendFile(pairsPath, `${canonicalJson(pair)}\n`);
|
|
931
|
+
if (patternCallRatioExceeded(pairs, pattern, analysis.stopRules.maxPatternCallRatio))
|
|
932
|
+
control.stop('pattern-call-ratio');
|
|
933
|
+
// Beyond the tolerance the pattern cannot qualify: stop spending on it and move to the next one.
|
|
934
|
+
const tolerance = Math.floor(analysis.providerFailurePolicy.maxMeasurementFailureFractionPerPattern * preregistration.tasksPerPattern);
|
|
935
|
+
if (pairs.filter(row => row.pattern === pattern && row.measurementFailure).length > tolerance) {
|
|
936
|
+
abandonedPatterns.push(pattern);
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (control.stopped)
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
clearTimeout(timer);
|
|
946
|
+
vault.secret?.fill(0);
|
|
947
|
+
}
|
|
948
|
+
await assertContextLiveSource(options.sourceRoot, approval.sourceCommit);
|
|
949
|
+
const analyses = DAG_LIVE_PATTERNS.map(pattern => analyzeDagLivePattern(pattern, pairs, preregistration, approval.priceBound));
|
|
950
|
+
const stopped = control.stopped;
|
|
951
|
+
const recommendation = !stopped && !synthetic && analyses.every(item => item.eligible) ? 'eligible-for-promotion-review' : 'hold';
|
|
952
|
+
const elapsedMs = Math.max(0, now() - started);
|
|
953
|
+
const summary = { schemaVersion: 'dag-live-summary/v1', issue: '#2686', runId: approval.runId, sourceCommit: approval.sourceCommit,
|
|
954
|
+
source: synthetic ? 'synthetic' : 'provider', liveEvidence: !synthetic, stagingHost: approval.stagingHost, model: approval.model,
|
|
955
|
+
workloadDigest: approval.workloadDigest, preregistrationDigest: approval.preregistrationDigest, approvalDigest: dagLiveDigest(approval),
|
|
956
|
+
callsDigest: await fileDigest(callsPath), pairsDigest: await fileDigest(pairsPath), stopped, abandonedPatterns,
|
|
957
|
+
measurementFailures: pairs.filter(pair => pair.measurementFailure).length, pairs: pairs.length, providerCalls: calls.length,
|
|
958
|
+
reserved: { calls: budget.total.calls, tokens: budget.total.tokens, usd: budget.usd() }, elapsedMs,
|
|
959
|
+
// Every provider call, including the one that stopped the run, as reported and as charged.
|
|
960
|
+
spend: { providerCalls: calls.length, inputTokens: calls.reduce((sum, call) => sum + (call.inputTokens ?? 0), 0),
|
|
961
|
+
outputTokens: calls.reduce((sum, call) => sum + (call.outputTokens ?? 0), 0),
|
|
962
|
+
attestedBillableUsd: calls.reduce((sum, call) => sum + (call.inputTokens ?? 0) * approval.priceBound.inputUsdPerMTok
|
|
963
|
+
+ (call.outputTokens ?? 0) * approval.priceBound.outputUsdPerMTok, 0) / 1_000_000,
|
|
964
|
+
chargedUsd: budget.usd() },
|
|
965
|
+
stoppingCall: control.stoppingCall, priorSpendUsd: priorMicros / 1_000_000, priorRuns: scanned.runs, effectiveRunUsdCap: effectiveBudget.usd,
|
|
966
|
+
priceBound: approval.priceBound,
|
|
967
|
+
analyses, recommendation, promotionDecision: 'pending-reviewer', reviewer: approval.reviewer, automaticPromotion: false };
|
|
968
|
+
await write(join(directory, 'summary.json'), summary);
|
|
969
|
+
// G5 input: observed resources against the approved bounds, pinned by manifest digest.
|
|
970
|
+
const loadManifest = { schema: 'decision-load-manifest/v1', mode: synthetic ? 'synthetic-transport' : 'staged-provider', bounds: {
|
|
971
|
+
calls: approval.budget.calls, tokens: approval.budget.tokens, reservedUsdMicros: capMicros, wallClockMs: approval.budget.wallClockMs,
|
|
972
|
+
...Object.fromEntries(DAG_LIVE_PATTERNS.map(p => [`${p}-call-ratio-milli`, analysis.stopRules.maxPatternCallRatio * 1000]))
|
|
973
|
+
} };
|
|
974
|
+
const loadPath = join(directory, 'g5-load-result.json');
|
|
975
|
+
await write(loadPath, { schemaVersion: 'decision-load-result/v1', manifestDigest: loadManifestDigest(loadManifest), manifest: loadManifest, mode: loadManifest.mode,
|
|
976
|
+
observations: { calls: budget.total.calls, tokens: budget.total.tokens, reservedUsdMicros: budget.total.usdMicros, wallClockMs: elapsedMs,
|
|
977
|
+
...Object.fromEntries(analyses.map(item => [`${item.pattern}-call-ratio-milli`, item.economics.callRatio === null ? -1 : Math.ceil(item.economics.callRatio * 1000)])) },
|
|
978
|
+
recordedAt: new Date().toISOString() });
|
|
979
|
+
const summaryDigest = await fileDigest(join(directory, 'summary.json'));
|
|
980
|
+
// D11 replays the recorded collection: each case re-reads the digest-bound pairs and recomputes its analysis.
|
|
981
|
+
const run = await executeQualificationPlan({ artifactRoot: options.artifactRoot, concurrency: 1, timeoutMs: 600_000,
|
|
982
|
+
manifest: { schemaVersion: 'decision-qualification-run/v1', mode: synthetic ? 'offline' : 'recorded', runId: approval.runId,
|
|
983
|
+
generatedAt: new Date().toISOString(), sourceCommit: approval.sourceCommit, dirty: false,
|
|
984
|
+
cases: DAG_LIVE_PATTERNS.map(pattern => ({ id: `d12-${pattern}`, kind: 'baseline', mandatory: true, candidateTests: [],
|
|
985
|
+
evidenceIds: [`BCH-D12-${pattern.toUpperCase()}`] })) },
|
|
986
|
+
executors: Object.fromEntries(DAG_LIVE_PATTERNS.map(pattern => [`d12-${pattern}`, async () => {
|
|
987
|
+
if (await fileDigest(pairsPath) !== summary.pairsDigest)
|
|
988
|
+
return { outcome: 'fail', details: { reason: 'pairs-digest-mismatch' } };
|
|
989
|
+
const text = await readFile(pairsPath, 'utf8');
|
|
990
|
+
const recorded = text.split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
991
|
+
const replay = analyzeDagLivePattern(pattern, recorded, preregistration, approval.priceBound);
|
|
992
|
+
const matches = canonicalJson(replay) === canonicalJson(analyses.find(item => item.pattern === pattern));
|
|
993
|
+
return { outcome: matches && replay.eligible && !stopped && !synthetic ? 'pass' : 'fail',
|
|
994
|
+
details: { summaryDigest, pairsDigest: summary.pairsDigest, replayMatches: matches, stopped, source: summary.source, analysis: replay } };
|
|
995
|
+
}])),
|
|
996
|
+
// Only an unstopped live run may offer its load result as the G5 gate artifact.
|
|
997
|
+
...(!synthetic && !stopped ? { gateArtifacts: { 'load-manifest-qualified': loadPath } } : {}), sanitizeDetails: details => details });
|
|
998
|
+
await write(join(directory, 'run-manifest.json'), run);
|
|
999
|
+
const sources = options.sourceGoldens ?? DAG_LIVE_SOURCE_GOLDENS;
|
|
1000
|
+
const evidence = await writeQualificationEvidenceManifest(run, options.artifactRoot, options.sourceRoot, Object.fromEntries(DAG_LIVE_PATTERNS.map(pattern => [`d12-${pattern}`, [...sources]])));
|
|
1001
|
+
// G6 input: the reviewer signs this outcome digest with --record-decision.
|
|
1002
|
+
await write(join(directory, 'g6-review-request.json'), { schemaVersion: 'dag-live-review-request/v1', runId: approval.runId, sourceCommit: approval.sourceCommit,
|
|
1003
|
+
outcomesDigest: qualificationOutcomesDigest(run), summaryDigest, evidenceManifestDigest: evidence.digest, recommendation, reviewer: approval.reviewer });
|
|
1004
|
+
return { ...summary, summaryDigest, evidenceManifest: evidence.artifact, evidenceDigest: evidence.digest,
|
|
1005
|
+
g5LoadResult: `${approval.runId}/g5-load-result.json`, g6ReviewRequest: `${approval.runId}/g6-review-request.json` };
|
|
1006
|
+
}
|
|
1007
|
+
export const DAG_LIVE_SOURCE_GOLDENS = ['src/decision/graph-live-qualification.ts', 'src/decision/graph-live-workload.ts',
|
|
1008
|
+
'docs/decision/evidence/dag-live-v1/workload.json', 'docs/decision/evidence/dag-live-v1/preregistration.json'];
|
|
1009
|
+
/**
|
|
1010
|
+
* Records the named reviewer's promote-or-hold decision against a finished run directory.
|
|
1011
|
+
* Promotion is refused unless the recorded run was live, unstopped and eligible for every pattern.
|
|
1012
|
+
*/
|
|
1013
|
+
export async function recordDagLiveDecision(runDirectory, decision, reviewer, rationale, at = new Date()) {
|
|
1014
|
+
if (!['promote', 'hold'].includes(decision) || !reviewer?.trim() || !rationale?.trim())
|
|
1015
|
+
throw new Error('D12 decision requires promote|hold, reviewer and rationale');
|
|
1016
|
+
const summaryPath = join(runDirectory, 'summary.json'), requestPath = join(runDirectory, 'g6-review-request.json');
|
|
1017
|
+
const summary = JSON.parse(await readFile(summaryPath, 'utf8'));
|
|
1018
|
+
const request = JSON.parse(await readFile(requestPath, 'utf8'));
|
|
1019
|
+
if (request.summaryDigest !== await fileDigest(summaryPath) || request.runId !== summary.runId)
|
|
1020
|
+
throw new Error('D12 review request does not bind this summary');
|
|
1021
|
+
if (reviewer !== summary.reviewer)
|
|
1022
|
+
throw new Error('D12 decision must come from the approved promotion owner');
|
|
1023
|
+
// The owner comes from the run's preregistration, which must still be the one the summary binds.
|
|
1024
|
+
const preregistration = JSON.parse(await readFile(join(runDirectory, 'preregistration.json'), 'utf8'));
|
|
1025
|
+
const bound = summary.preregistrationDigest;
|
|
1026
|
+
if (dagLiveDigest(preregistration) !== bound || preregistration.promotionOwner !== reviewer) {
|
|
1027
|
+
throw new Error('D12 decision does not match the preregistration promotion owner');
|
|
1028
|
+
}
|
|
1029
|
+
if (decision === 'promote' && (summary.recommendation !== 'eligible-for-promotion-review' || !summary.liveEvidence || summary.stopped)) {
|
|
1030
|
+
throw new Error('D12 promotion refused: evidence is not live, complete and eligible');
|
|
1031
|
+
}
|
|
1032
|
+
const recordedAt = at.toISOString();
|
|
1033
|
+
const review = { schemaVersion: 'decision-qualification-review/v1', runId: summary.runId, sourceCommit: summary.sourceCommit,
|
|
1034
|
+
outcomesDigest: request.outcomesDigest, reviewer, decision: decision === 'promote' ? 'approve' : 'reject', recordedAt };
|
|
1035
|
+
const record = { schemaVersion: 'dag-live-promotion-decision/v1', issue: '#2686', runId: summary.runId, sourceCommit: summary.sourceCommit,
|
|
1036
|
+
summaryDigest: request.summaryDigest, decision, reviewer, rationale, recordedAt, reviewRecordDigest: dagLiveDigest(review) };
|
|
1037
|
+
await write(join(runDirectory, 'g6-review-record.json'), review);
|
|
1038
|
+
await write(join(runDirectory, 'promotion-decision.json'), record);
|
|
1039
|
+
return record;
|
|
1040
|
+
}
|
|
1041
|
+
//# sourceMappingURL=graph-live-qualification.js.map
|