@aiwg/cli 2026.9.24 → 2026.10.0

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.
Files changed (207) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  3. package/dist/src/artifacts/discovery-shadow.js +195 -0
  4. package/dist/src/artifacts/index-builder.js +47 -9
  5. package/dist/src/artifacts/query-engine.js +94 -1
  6. package/dist/src/artifacts/types.js +1 -0
  7. package/dist/src/cli/handlers/decision.js +88 -0
  8. package/dist/src/cli/handlers/gates.js +154 -0
  9. package/dist/src/cli/handlers/index.js +5 -1
  10. package/dist/src/cli/handlers/sessions.js +10 -7
  11. package/dist/src/cli/handlers/utilities.js +20 -0
  12. package/dist/src/config/aiwg-config.js +15 -2
  13. package/dist/src/decision/acceptance.js +25 -0
  14. package/dist/src/decision/adapters/jev.js +24 -2
  15. package/dist/src/decision/calibration/registry.js +9 -0
  16. package/dist/src/decision/compose.js +74 -15
  17. package/dist/src/decision/context-live-promotion.js +344 -0
  18. package/dist/src/decision/context-live-qualification.js +591 -0
  19. package/dist/src/decision/context-pruning.js +847 -0
  20. package/dist/src/decision/driver.js +430 -0
  21. package/dist/src/decision/egress-live-qualification.js +874 -0
  22. package/dist/src/decision/ensemble/contract.js +64 -1
  23. package/dist/src/decision/ensemble/index.js +1 -0
  24. package/dist/src/decision/ensemble/report.js +8 -42
  25. package/dist/src/decision/ensemble/runtime.js +571 -0
  26. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  27. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  28. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  29. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  30. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  31. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  32. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  33. package/dist/src/decision/ensemble-study/score.js +462 -0
  34. package/dist/src/decision/ensemble-study/staged.js +87 -0
  35. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  36. package/dist/src/decision/evaluate.js +227 -14
  37. package/dist/src/decision/export.js +4 -0
  38. package/dist/src/decision/feature-export.js +1074 -0
  39. package/dist/src/decision/graph-index.js +16 -0
  40. package/dist/src/decision/graph-live-qualification.js +1041 -0
  41. package/dist/src/decision/graph-live-workload.js +245 -0
  42. package/dist/src/decision/graph-skill-bridge.js +21 -3
  43. package/dist/src/decision/heldout/calibration.js +140 -0
  44. package/dist/src/decision/heldout/collector.js +430 -0
  45. package/dist/src/decision/heldout/contract.js +309 -0
  46. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  47. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  48. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  50. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  51. package/dist/src/decision/heldout/d29-pools.js +288 -0
  52. package/dist/src/decision/heldout/d29-v6.js +169 -0
  53. package/dist/src/decision/heldout/d29-v7.js +202 -0
  54. package/dist/src/decision/heldout/d29-v8.js +586 -0
  55. package/dist/src/decision/heldout/generator-registry.js +67 -0
  56. package/dist/src/decision/heldout/generators.js +696 -0
  57. package/dist/src/decision/heldout/index.js +8 -0
  58. package/dist/src/decision/heldout/journal.js +346 -0
  59. package/dist/src/decision/heldout/study-generators.js +64 -0
  60. package/dist/src/decision/heldout/types.js +2 -0
  61. package/dist/src/decision/host-policy.js +61 -0
  62. package/dist/src/decision/index.js +11 -0
  63. package/dist/src/decision/interop-profile.v1.json +112 -0
  64. package/dist/src/decision/interop.js +653 -0
  65. package/dist/src/decision/issue-triage/index.js +3 -0
  66. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  67. package/dist/src/decision/issue-triage/types.js +2 -0
  68. package/dist/src/decision/patterns/catalog.js +28 -8
  69. package/dist/src/decision/patterns/playground.js +28 -11
  70. package/dist/src/decision/patterns/specs.js +19 -10
  71. package/dist/src/decision/preprocessed-evidence.js +724 -0
  72. package/dist/src/decision/qualification/capture.js +8 -2
  73. package/dist/src/decision/qualification/gate-evidence.js +19 -4
  74. package/dist/src/decision/qualification/quality.js +57 -49
  75. package/dist/src/decision/qualification/release.js +76 -3
  76. package/dist/src/decision/qualification/runner.js +5 -2
  77. package/dist/src/decision/routing/contract.js +175 -0
  78. package/dist/src/decision/routing/index.js +5 -0
  79. package/dist/src/decision/routing/report.js +216 -0
  80. package/dist/src/decision/routing/runtime.js +685 -0
  81. package/dist/src/decision/routing/types.js +2 -0
  82. package/dist/src/decision/sdlc-screening.js +1157 -0
  83. package/dist/src/decision/sensitivity/comparative.js +154 -0
  84. package/dist/src/decision/sensitivity/contract.js +218 -0
  85. package/dist/src/decision/sensitivity/index.js +5 -0
  86. package/dist/src/decision/sensitivity/runtime.js +912 -0
  87. package/dist/src/decision/sensitivity/types.js +2 -0
  88. package/dist/src/decision/validate.js +26 -0
  89. package/dist/src/extensions/commands/definitions.js +38 -0
  90. package/dist/src/extensions/manifest.js +44 -0
  91. package/dist/src/gates/discovery.js +315 -0
  92. package/dist/src/gates/driver.js +388 -0
  93. package/dist/src/gates/evaluate.js +719 -0
  94. package/dist/src/gates/floors.js +255 -0
  95. package/dist/src/gates/index.js +17 -0
  96. package/dist/src/gates/providers/evidence.js +38 -0
  97. package/dist/src/gates/providers/index.js +21 -0
  98. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  99. package/dist/src/gates/providers/loader.js +1069 -0
  100. package/dist/src/gates/providers/paired.js +52 -0
  101. package/dist/src/gates/providers/proportion.js +60 -0
  102. package/dist/src/gates/providers/registry.js +40 -0
  103. package/dist/src/gates/providers/scalar.js +41 -0
  104. package/dist/src/gates/providers/screening.js +53 -0
  105. package/dist/src/gates/providers/types.js +25 -0
  106. package/dist/src/gates/registry.js +1003 -0
  107. package/dist/src/gates/report.js +102 -0
  108. package/dist/src/gates/schema.js +56 -0
  109. package/dist/src/gates/stats/binomial.js +26 -0
  110. package/dist/src/gates/stats/bootstrap.js +62 -0
  111. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  112. package/dist/src/gates/stats/digest.js +26 -0
  113. package/dist/src/gates/stats/error.js +5 -0
  114. package/dist/src/gates/stats/index.js +9 -0
  115. package/dist/src/gates/stats/normal.js +35 -0
  116. package/dist/src/gates/stats/paired.js +120 -0
  117. package/dist/src/gates/stats/splits.js +30 -0
  118. package/dist/src/gates/types.js +8 -0
  119. package/dist/src/governance/redaction.js +14 -4
  120. package/dist/src/mcp/muse-native-config.mjs +123 -0
  121. package/dist/src/mcp/tools/decision.mjs +188 -0
  122. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  123. package/dist/src/providers/capability-matrix.yaml +2 -1
  124. package/dist/src/providers/provider-definitions.js +2 -1
  125. package/dist/src/sessions/adapters/muse.js +336 -39
  126. package/dist/src/sessions/batch-import.js +7 -0
  127. package/dist/src/sessions/workspace-discovery.js +13 -2
  128. package/package.json +1 -1
  129. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  130. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  131. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  132. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  133. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  134. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  135. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  136. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  137. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  138. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  139. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  140. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  141. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  142. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  143. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  144. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  145. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  146. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  147. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  148. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  149. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  150. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  151. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  152. package/schemas/decision/D29Study.v3.schema.json +609 -0
  153. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  154. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  155. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  156. package/schemas/decision/D29Study.v8.schema.json +671 -0
  157. package/schemas/decision/D29Study.v9.schema.json +778 -0
  158. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  159. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  160. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  161. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  162. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  163. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  164. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  165. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  166. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  167. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  168. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  169. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  170. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  171. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  172. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  173. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  174. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  175. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  176. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  177. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  178. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  179. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  180. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  181. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  182. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  183. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  184. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  185. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  186. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  187. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  188. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  189. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  190. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  191. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  192. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  193. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  194. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  195. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  196. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  197. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  198. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  199. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  200. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  201. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  202. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  203. package/tools/agents/deploy-agents.mjs +1 -1
  204. package/tools/agents/providers/base.mjs +26 -7
  205. package/tools/agents/providers/factory.mjs +10 -2
  206. package/tools/agents/providers/muse-hooks.mjs +1 -29
  207. package/tools/agents/providers/muse.mjs +6 -3
@@ -0,0 +1,1069 @@
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 { redactText } from '../../governance/redaction.js';
8
+ import { canonicalJson } from '../../security/artifact-trust.js';
9
+ import { correlateAtomicBatch } from '../batch.js';
10
+ import { admitEntry } from '../entry.js';
11
+ import { projectDecisionState } from '../projection.js';
12
+ import { evaluateBinaryHeldout, evaluateOrdinalHeldout, evaluateRankingHeldout, freezeQualificationSplit, pairedBinaryDifferenceInterval, pairedNonInferiority, verifyQualificationSplits, wilsonScoreInterval, } from '../qualification/quality.js';
13
+ import { artifactDigest } from '../validate.js';
14
+ export class IssueTriagePilotError extends Error {
15
+ layer;
16
+ details;
17
+ constructor(message, layer = 'semantic', details = []) {
18
+ super(message);
19
+ this.layer = layer;
20
+ this.details = details;
21
+ this.name = 'IssueTriagePilotError';
22
+ }
23
+ }
24
+ const SCHEMA_FILES = {
25
+ pilotPack: 'DecisionIssueTriagePilotPack.v1.schema.json',
26
+ manifest: 'DecisionIssueTriageEvaluationManifest.v1.schema.json',
27
+ report: 'DecisionIssueTriageEvaluationReport.v1.schema.json',
28
+ };
29
+ const GATE_RANK = { PROMOTE: 0, HOLD: 1, ROLLBACK: 2 };
30
+ const FINAL_FIELDS = ['finalLabels', 'finalDuplicateOf', 'resolution', 'closedAt'];
31
+ const RESPONSE_FIELDS = ['issueId', 'issueType', 'area', 'urgency', 'completeness', 'clarificationNeed', 'duplicate',
32
+ 'uncertaintyProfile', 'accepted', 'actualModel', 'requestedModel', 'latencyMs', 'usage', 'usageReceipts', 'calls', 'retries', 'fallbacks',
33
+ 'cacheHit'];
34
+ const DUPLICATE_RESPONSE_FIELDS = ['issueId', 'rank'];
35
+ const METADATA_ALLOWLIST = ['component', 'provider', 'framework', 'source', 'environment', 'reproduction'];
36
+ const CANDIDATE_FIELDS = ['id', 'repository', 'createdAt', 'revisions'];
37
+ const REVISION_FIELDS = ['observedAt', 'title', 'body'];
38
+ const RECEIPT_FIELDS = ['kind', 'requestId', 'inputTokens', 'outputTokens', 'costUsd'];
39
+ // Formats the shared governance redactor does not cover on its own.
40
+ const ISSUE_TRIAGE_REDACTION_PATTERNS = [
41
+ { id: 'jwt', pattern: '\\beyJ[A-Za-z0-9_-]{4,}\\.[A-Za-z0-9_-]{4,}\\.[A-Za-z0-9_-]{4,}' },
42
+ { id: 'aws-access-key', pattern: '\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b' },
43
+ ];
44
+ let validators = null;
45
+ function schemaRoot() {
46
+ const here = dirname(fileURLToPath(import.meta.url));
47
+ const candidates = [resolve(here, '../../../schemas/decision'), resolve(here, '../../../../schemas/decision')];
48
+ const found = candidates.find(candidate => existsSync(resolve(candidate, SCHEMA_FILES.pilotPack)));
49
+ if (!found)
50
+ throw new IssueTriagePilotError('Issue triage schema directory is unavailable');
51
+ return found;
52
+ }
53
+ function schemaValidator(kind) {
54
+ if (!validators) {
55
+ const ajv = new Ajv2020({ strict: true, allErrors: true, validateFormats: false });
56
+ addFormats(ajv);
57
+ validators = new Map(Object.keys(SCHEMA_FILES).map(name => [
58
+ name,
59
+ ajv.compile(JSON.parse(readFileSync(resolve(schemaRoot(), SCHEMA_FILES[name]), 'utf8'))),
60
+ ]));
61
+ }
62
+ return validators.get(kind);
63
+ }
64
+ function checkSchema(kind, value) {
65
+ try {
66
+ admitEntry(value);
67
+ }
68
+ catch {
69
+ throw new IssueTriagePilotError(`${kind} admission denied`, 'admission');
70
+ }
71
+ const check = schemaValidator(kind);
72
+ if (!check(value)) {
73
+ const details = (check.errors ?? []).map(error => `${error.instancePath || '/'} ${error.message ?? 'invalid'}`);
74
+ throw new IssueTriagePilotError(`${kind} does not match its v1 schema: ${details[0] ?? 'invalid'}`, 'schema', details);
75
+ }
76
+ }
77
+ function digest(value) {
78
+ return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
79
+ }
80
+ function semantic(problems, message) {
81
+ if (problems.length)
82
+ throw new IssueTriagePilotError(`${message}: ${problems[0]}`, 'semantic', problems);
83
+ }
84
+ export function validateIssueTriagePilotPack(value) {
85
+ checkSchema('pilotPack', value);
86
+ const pack = value;
87
+ const problems = [];
88
+ const ordinals = new Set();
89
+ const urgencyIds = new Set();
90
+ for (const level of pack.urgencyRubric) {
91
+ if (ordinals.has(level.ordinal))
92
+ problems.push(`urgency ordinal ${level.ordinal} is duplicated`);
93
+ if (urgencyIds.has(level.id))
94
+ problems.push(`urgency id ${level.id} is duplicated`);
95
+ ordinals.add(level.ordinal);
96
+ urgencyIds.add(level.id);
97
+ }
98
+ for (let index = 0; index < pack.urgencyRubric.length; index++) {
99
+ if (!ordinals.has(index))
100
+ problems.push('urgency ordinals must be contiguous from zero');
101
+ }
102
+ if (!pack.taxonomies.stateCompleteness.includes('complete')
103
+ || !pack.taxonomies.stateCompleteness.includes('partial')
104
+ || !pack.taxonomies.stateCompleteness.includes('missing'))
105
+ problems.push('state completeness taxonomy is incomplete');
106
+ if (!pack.taxonomies.clarificationNeed.includes('needed')
107
+ || !pack.taxonomies.clarificationNeed.includes('not-needed'))
108
+ problems.push('clarification taxonomy is incomplete');
109
+ semantic(problems, 'issue triage pilot pack rejected');
110
+ return pack;
111
+ }
112
+ export function validateIssueTriageEvaluationManifest(value) {
113
+ checkSchema('manifest', value);
114
+ const manifest = value;
115
+ const problems = [];
116
+ for (const dimension of ['issue-type', 'area', 'state-completeness', 'adversarial-authority', 'new-or-unseen-category']) {
117
+ if (!manifest.slices.some(slice => slice.dimension === dimension))
118
+ problems.push(`slice dimension ${dimension} is not preregistered`);
119
+ }
120
+ if (manifest.thresholds.minimumTotalSamples < manifest.dataset.minimumTotal)
121
+ problems.push('threshold minimum total is below dataset minimum');
122
+ if (manifest.thresholds.minimumPerSliceSamples < manifest.dataset.minimumPerSupportedSlice)
123
+ problems.push('threshold per-slice minimum is below dataset minimum');
124
+ const marginBps = manifest.thresholds.qualityNonInferiorityMargin * 10000;
125
+ if (manifest.thresholds.qualityNonInferiorityMargin > 0) {
126
+ problems.push('thresholds.qualityNonInferiorityMargin must be <= 0; a positive margin is a superiority test, which is not supported');
127
+ }
128
+ else if (Math.abs(marginBps - Math.round(marginBps)) > 1e-6) {
129
+ problems.push('thresholds.qualityNonInferiorityMargin must be a whole number of basis points');
130
+ }
131
+ const sliceIds = new Set();
132
+ for (const slice of manifest.slices) {
133
+ if (sliceIds.has(slice.id))
134
+ problems.push(`slice id ${slice.id} is duplicated`);
135
+ sliceIds.add(slice.id);
136
+ }
137
+ const splitDigests = Object.values(manifest.dataset.splitDigests);
138
+ if (new Set(splitDigests).size !== splitDigests.length)
139
+ problems.push('split digests must be distinct');
140
+ const pinParts = [manifest.calibration.registryDigest, manifest.calibration.policyDigest, manifest.calibration.requestRule];
141
+ if (pinParts.some(part => part === null) && pinParts.some(part => part !== null)) {
142
+ problems.push('calibration pin must pin registry, policy and request rule together, or none of them');
143
+ }
144
+ const rule = manifest.calibration.requestRule;
145
+ if (rule && (!rule.requestedAlias.trim() || !rule.runIdPrefix.trim() || !Number.isFinite(Date.parse(rule.notBefore)))) {
146
+ problems.push('calibration request rule must pin an alias, run id prefix and notBefore instant');
147
+ }
148
+ if (manifest.holdout.holdoutAccessedAt !== null
149
+ && Date.parse(manifest.holdout.holdoutAccessedAt) <= Date.parse(manifest.holdout.thresholdsRegisteredAt)) {
150
+ problems.push('thresholds must be preregistered before holdout access');
151
+ }
152
+ semantic(problems, 'issue triage evaluation manifest rejected');
153
+ return manifest;
154
+ }
155
+ function checkIssueTriageEvaluationReportShape(value) {
156
+ checkSchema('report', value);
157
+ const report = value;
158
+ const problems = [];
159
+ if (GATE_RANK[report.decision] < GATE_RANK[report.integrity.upstreamDecision]) {
160
+ problems.push(`issue triage report cannot upgrade ${report.integrity.upstreamDecision} to ${report.decision}`);
161
+ }
162
+ if (report.decision === 'PROMOTE' && report.integrity.findings.length > 0)
163
+ problems.push('PROMOTE requires no findings');
164
+ if (report.decision === 'PROMOTE' && report.baselineComparison.qualityNonInferiority?.decision !== 'non-inferior') {
165
+ problems.push('PROMOTE requires a non-inferior paired quality interval');
166
+ }
167
+ semantic(problems, 'issue triage evaluation report rejected');
168
+ return report;
169
+ }
170
+ /**
171
+ * A report is accepted only if it is exactly what this module rebuilds from the same inputs, and only if
172
+ * those inputs use the manifest whose digest was preregistered out of band. A spread-copied or edited
173
+ * report, or one built from a relaxed manifest, is rejected.
174
+ */
175
+ export function validateIssueTriageEvaluationReport(value, binding) {
176
+ const report = checkIssueTriageEvaluationReportShape(value);
177
+ if (!binding || digest(binding.inputs.manifest) !== binding.trustedManifestDigest
178
+ || report.manifest.digest !== binding.trustedManifestDigest) {
179
+ throw new IssueTriagePilotError('issue triage report manifest does not match the trusted manifest digest');
180
+ }
181
+ verifyPilotPackPin(validateIssueTriagePilotPack(binding.inputs.pack), validateIssueTriageEvaluationManifest(binding.inputs.manifest));
182
+ if (canonicalJson(report.pilotPack) !== canonicalJson(binding.inputs.manifest.pilotPack)) {
183
+ throw new IssueTriagePilotError('issue triage pilot pack does not match the manifest pin');
184
+ }
185
+ const rebuilt = buildIssueTriageEvaluationReport(binding.inputs);
186
+ if (canonicalJson(rebuilt) !== canonicalJson(report)) {
187
+ throw new IssueTriagePilotError('issue triage report does not match a rebuild from its inputs');
188
+ }
189
+ return report;
190
+ }
191
+ export function defaultIssueTriagePilotPack() {
192
+ return validateIssueTriagePilotPack({
193
+ schemaVersion: 'decision-issue-triage-pilot-pack/v1',
194
+ id: 'aiwg-issue-triage-jev-shadow',
195
+ version: '2026.9.29',
196
+ mode: 'disabled',
197
+ taxonomies: {
198
+ issueTypes: ['bug', 'feature', 'documentation', 'operations', 'security', 'question', 'other'],
199
+ areas: ['decision-engine', 'composition-engine', 'aiwg-cli', 'provider-adapters', 'docs', 'release', 'unknown'],
200
+ stateCompleteness: ['complete', 'partial', 'missing'],
201
+ clarificationNeed: ['needed', 'not-needed'],
202
+ },
203
+ urgencyRubric: [
204
+ { id: 'low', ordinal: 0, description: 'Cosmetic, documentation-only, or low operational impact.' },
205
+ { id: 'normal', ordinal: 1, description: 'User-visible issue with workaround or bounded impact.' },
206
+ { id: 'high', ordinal: 2, description: 'Important workflow failure, no easy workaround, or time-sensitive release risk.' },
207
+ { id: 'critical', ordinal: 3, description: 'Security, data loss, production outage, or release-blocking failure.' },
208
+ ],
209
+ candidatePolicy: { generator: 'deterministic-token-overlap-v1', maxCandidates: 5, includeNone: true },
210
+ acceptance: {
211
+ uncertaintyProfiles: ['typesafe-distribution-v1', 'typesafe-truth-v1'],
212
+ calibration: 'advisory',
213
+ acceptedScoring: 'disabled-on-incompatibility',
214
+ },
215
+ trackerMutationPolicy: {
216
+ create: 'forbidden',
217
+ edit: 'forbidden',
218
+ label: 'forbidden',
219
+ assign: 'forbidden',
220
+ comment: 'forbidden',
221
+ close: 'forbidden',
222
+ merge: 'forbidden',
223
+ },
224
+ rollback: { disableRestoresPriorWorkflow: true, preserveReceiptsAndLabels: true },
225
+ });
226
+ }
227
+ function tokens(value) {
228
+ return new Set(value.toLowerCase().match(/[a-z0-9_]+/g)?.filter(token => token.length > 2) ?? []);
229
+ }
230
+ function issueText(issue) {
231
+ return `${issue.title}\n${issue.body}`;
232
+ }
233
+ /**
234
+ * The candidate revision visible as of `asOf`: the latest revision observed at or before it. Candidates without
235
+ * such a revision are excluded, so a later title, label or state edit can never reach the replayed issue.
236
+ */
237
+ function candidateAsOf(candidate, asOf) {
238
+ for (const key of Object.keys(candidate)) {
239
+ if (!CANDIDATE_FIELDS.includes(key)) {
240
+ throw new IssueTriagePilotError(`candidate ${candidate.id} carries mutable current-state field ${key}; supply point-in-time revisions`);
241
+ }
242
+ }
243
+ if (!Array.isArray(candidate.revisions))
244
+ throw new IssueTriagePilotError(`candidate ${candidate.id} requires point-in-time revisions`);
245
+ let visible = null;
246
+ for (const revision of candidate.revisions) {
247
+ if (Object.keys(revision).some(key => !REVISION_FIELDS.includes(key))
248
+ || typeof revision.title !== 'string' || typeof revision.body !== 'string') {
249
+ throw new IssueTriagePilotError(`candidate ${candidate.id} revision must contain only observedAt, title and body`);
250
+ }
251
+ const at = Date.parse(revision.observedAt);
252
+ if (!Number.isFinite(at))
253
+ throw new IssueTriagePilotError(`candidate ${candidate.id} revision has an invalid observedAt`);
254
+ if (at <= asOf && (visible === null || at >= visible.at))
255
+ visible = { at, title: revision.title, body: revision.body };
256
+ }
257
+ return visible && { title: visible.title, body: visible.body };
258
+ }
259
+ export function deterministicIssueDuplicateCandidates(issue, corpus, pack = defaultIssueTriagePilotPack()) {
260
+ const query = tokens(issueText(issue));
261
+ const issueCreatedAt = Date.parse(issue.createdAt);
262
+ if (!Number.isFinite(issueCreatedAt))
263
+ throw new IssueTriagePilotError('triaged issue createdAt is invalid');
264
+ const scored = corpus
265
+ .filter(candidate => candidate.id !== issue.id && candidate.repository === issue.repository
266
+ && Date.parse(candidate.createdAt) <= issueCreatedAt)
267
+ .flatMap(candidate => {
268
+ const snapshot = candidateAsOf(candidate, issueCreatedAt);
269
+ if (!snapshot)
270
+ return [];
271
+ const candidateTokens = tokens(issueText(snapshot));
272
+ const overlap = [...query].filter(token => candidateTokens.has(token)).length;
273
+ const union = new Set([...query, ...candidateTokens]).size || 1;
274
+ return [{
275
+ issueId: candidate.id,
276
+ repository: candidate.repository,
277
+ title: redactCredentialMaterial(snapshot.title),
278
+ rank: 0,
279
+ score: overlap / union,
280
+ sourceDigest: digest({ id: candidate.id, repository: candidate.repository, createdAt: candidate.createdAt, ...snapshot }),
281
+ }];
282
+ })
283
+ .filter(candidate => candidate.score > 0)
284
+ .sort((left, right) => right.score - left.score || left.issueId.localeCompare(right.issueId))
285
+ .slice(0, pack.candidatePolicy.maxCandidates)
286
+ .map((candidate, index) => ({ ...candidate, rank: index + 1 }));
287
+ return { generator: 'deterministic-token-overlap-v1', queryDigest: digest({ id: issue.id, text: issueText(issue) }), candidates: scored, noneOutcome: 'none' };
288
+ }
289
+ /** Shared governance redactor plus JWT and AWS STS key formats; fails closed on oversized input. */
290
+ function redactCredentialMaterial(value) {
291
+ return redactText(value, { organizationPatterns: ISSUE_TRIAGE_REDACTION_PATTERNS, includeLength: false }).text;
292
+ }
293
+ function allowedMetadata(metadata) {
294
+ if (!metadata)
295
+ return undefined;
296
+ const entries = Object.entries(metadata)
297
+ .filter(([key]) => METADATA_ALLOWLIST.includes(key))
298
+ .map(([key, value]) => {
299
+ // Structured values could smuggle unredacted text; only scalars are model-visible.
300
+ if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
301
+ throw new IssueTriagePilotError(`metadata ${key} must be a string, number, boolean or null`);
302
+ }
303
+ return [key, typeof value === 'string' ? redactCredentialMaterial(value) : value];
304
+ });
305
+ return entries.length ? Object.fromEntries(entries) : undefined;
306
+ }
307
+ function projectionField(field, subject) {
308
+ return {
309
+ ...field,
310
+ source: 'issue-triage-shadow',
311
+ subject,
312
+ purpose: 'issue-triage',
313
+ retentionClass: 'decision-shadow-evaluation',
314
+ accessScopes: ['decision:evaluate'],
315
+ exportPolicy: 'sanitized',
316
+ deletionPolicy: 'erase',
317
+ backupPolicy: 'not-persisted',
318
+ allowedProviders: ['jev'],
319
+ allowedModels: ['jev-shadow'],
320
+ allowedOrigins: ['https://typesafe.ai'],
321
+ allowedRegions: ['us'],
322
+ };
323
+ }
324
+ export async function projectIssueTriageModelState(packInput, issue, lineage) {
325
+ const pack = validateIssueTriagePilotPack(packInput);
326
+ const excludedReplayFields = Object.fromEntries(FINAL_FIELDS.map(field => [field, issue[field] !== undefined]));
327
+ const metadata = allowedMetadata(issue.metadata);
328
+ const input = {
329
+ issue: {
330
+ id: issue.id,
331
+ repository: issue.repository,
332
+ title: redactCredentialMaterial(issue.title),
333
+ body: redactCredentialMaterial(issue.body),
334
+ createdAt: issue.createdAt,
335
+ ...(issue.author === undefined ? {} : { author: redactCredentialMaterial(issue.author) }),
336
+ ...(metadata === undefined ? {} : { metadata }),
337
+ },
338
+ duplicateCandidates: lineage.candidates.map(item => ({ ...item, title: redactCredentialMaterial(item.title) })),
339
+ allowed: {
340
+ issueTypes: pack.taxonomies.issueTypes,
341
+ areas: pack.taxonomies.areas,
342
+ urgency: [...pack.urgencyRubric].sort((left, right) => left.ordinal - right.ordinal).map(level => level.id),
343
+ stateCompleteness: pack.taxonomies.stateCompleteness,
344
+ clarificationNeed: pack.taxonomies.clarificationNeed,
345
+ duplicateOutcomes: [...lineage.candidates.map(candidate => candidate.issueId), 'none'],
346
+ },
347
+ };
348
+ const policy = {
349
+ version: 'issue-triage-shadow-d10/v1',
350
+ provider: 'jev',
351
+ model: 'jev-shadow',
352
+ origin: 'https://typesafe.ai',
353
+ region: 'us',
354
+ purpose: 'issue-triage',
355
+ allowIncompleteContext: false,
356
+ maxSensitivity: 'confidential',
357
+ fields: [
358
+ projectionField({ pointer: '/issue', output: 'issue', trust: 'untrusted', sensitivity: 'confidential' }, issue.id),
359
+ projectionField({ pointer: '/duplicateCandidates', output: 'duplicateCandidates', trust: 'verified', sensitivity: 'internal' }, issue.id),
360
+ projectionField({ pointer: '/allowed', output: 'allowed', trust: 'verified', sensitivity: 'internal' }, issue.id),
361
+ ],
362
+ };
363
+ const projected = await projectDecisionState(input, policy);
364
+ return { modelState: projected.state, lineage, excludedReplayFields,
365
+ projectionEvidence: projected.evidence, stateDigest: projected.evidence.projectedDigest };
366
+ }
367
+ export function validateIssueTriageBatchSubject(questions) {
368
+ if (!questions.length)
369
+ throw new IssueTriagePilotError('issue triage batch requires at least one question');
370
+ const issueIds = new Set(questions.map(question => question.issueId));
371
+ if (issueIds.size !== 1)
372
+ throw new IssueTriagePilotError('issue triage batch cannot contain more than one issue subject');
373
+ const ids = new Set();
374
+ for (const question of questions) {
375
+ if (!question.questionId.trim())
376
+ throw new IssueTriagePilotError('issue triage question id is empty');
377
+ if (ids.has(question.questionId))
378
+ throw new IssueTriagePilotError('issue triage question id is duplicated');
379
+ ids.add(question.questionId);
380
+ }
381
+ correlateAtomicBatch([...ids], questions.map(question => ({ questionId: question.questionId, value: question.alias })));
382
+ return questions[0].issueId;
383
+ }
384
+ /** Structural, taxonomy and lineage problems of one response for one subject; usage reconciliation is separate. */
385
+ function responseProblems(pack, subjectId, lineage, response) {
386
+ const problems = [];
387
+ for (const key of Object.keys(response)) {
388
+ if (!RESPONSE_FIELDS.includes(key))
389
+ problems.push(`model response contains unauthorized field ${key}`);
390
+ }
391
+ for (const key of Object.keys(response.duplicate)) {
392
+ if (!DUPLICATE_RESPONSE_FIELDS.includes(key))
393
+ problems.push(`duplicate response contains unauthorized field ${key}`);
394
+ }
395
+ if (response.issueId !== subjectId)
396
+ problems.push('response issue id does not match the projected subject');
397
+ if (!pack.taxonomies.issueTypes.includes(response.issueType))
398
+ problems.push(`issue type ${response.issueType} is not in the governed taxonomy`);
399
+ if (!pack.taxonomies.areas.includes(response.area))
400
+ problems.push(`area ${response.area} is not in the governed taxonomy`);
401
+ if (!pack.urgencyRubric.some(level => level.id === response.urgency))
402
+ problems.push(`urgency ${response.urgency} is not in the governed rubric`);
403
+ if (!pack.taxonomies.stateCompleteness.includes(response.completeness))
404
+ problems.push(`completeness ${response.completeness} is not in the governed taxonomy`);
405
+ if (!pack.taxonomies.clarificationNeed.includes(response.clarificationNeed))
406
+ problems.push(`clarification ${response.clarificationNeed} is not in the governed taxonomy`);
407
+ if (response.duplicate.issueId !== 'none' && !lineage.candidates.some(candidate => candidate.issueId === response.duplicate.issueId)) {
408
+ problems.push(`duplicate candidate ${response.duplicate.issueId} was not deterministically generated`);
409
+ }
410
+ const deterministicRank = response.duplicate.issueId === 'none'
411
+ ? null
412
+ : lineage.candidates.find(candidate => candidate.issueId === response.duplicate.issueId)?.rank ?? null;
413
+ if (response.duplicate.issueId === 'none' && response.duplicate.rank !== null)
414
+ problems.push('none duplicate outcome must not carry a rank');
415
+ if (response.duplicate.issueId !== 'none' && response.duplicate.rank !== deterministicRank) {
416
+ problems.push(`duplicate rank for ${response.duplicate.issueId} does not match deterministic rank ${deterministicRank}`);
417
+ }
418
+ if (typeof response.accepted !== 'boolean')
419
+ problems.push('accepted must be a boolean');
420
+ if (!Number.isFinite(response.latencyMs) || response.latencyMs < 0)
421
+ problems.push('latency must be finite and nonnegative');
422
+ if ([response.calls.jev, response.calls.fallbackModel, response.retries, response.fallbacks].some(value => !Number.isSafeInteger(value) || value < 0)) {
423
+ problems.push('call, retry and fallback counts must be nonnegative integers');
424
+ }
425
+ return problems;
426
+ }
427
+ /**
428
+ * The single acceptance rule for shadow scoring. Compatibility comes only from resolving the registry
429
+ * request; with calibration: required, no registry resolution means no accepted scoring.
430
+ */
431
+ function acceptanceFor(pack, response, calibration) {
432
+ const compatibility = calibration.registry
433
+ ? calibration.registry.registry.resolve(calibration.registry.request, calibration.registry.policy)
434
+ : null;
435
+ const profileCompatible = pack.acceptance.uncertaintyProfiles.includes(calibration.uncertaintyProfile)
436
+ && calibration.uncertaintyProfile === response.uncertaintyProfile;
437
+ const aliasCompatible = calibration.requestedModel === response.requestedModel
438
+ && calibration.actualModel === response.actualModel
439
+ && calibration.compatibleActualModels.includes(response.actualModel);
440
+ // The registry pin must describe this response's alias and served model; drift or an unknown identity is not "allow".
441
+ const registryDrift = compatibility !== null && (compatibility.requestedAlias !== response.requestedModel
442
+ || compatibility.actualModel !== response.actualModel || compatibility.reasons.includes('alias-drift') || compatibility.state === 'unknown');
443
+ const registryCompatible = compatibility === null
444
+ ? pack.acceptance.calibration !== 'required'
445
+ : compatibility.action === 'allow' && !registryDrift;
446
+ const modelCompatible = aliasCompatible && !registryDrift;
447
+ const acceptedScoring = response.accepted === true && profileCompatible && modelCompatible && registryCompatible;
448
+ const reason = acceptedScoring ? 'compatible' : response.accepted !== true ? 'defer-response-rejected'
449
+ : modelCompatible ? 'defer-calibration-incompatible' : 'defer-drift';
450
+ const driftEvent = modelCompatible ? null : `model-drift:${response.requestedModel}->${response.actualModel}`;
451
+ return { acceptedScoring, reason, driftEvent, compatibility };
452
+ }
453
+ export function validateIssueTriageModelResponse(packInput, projection, response, calibration) {
454
+ const pack = validateIssueTriagePilotPack(packInput);
455
+ const problems = [
456
+ ...responseProblems(pack, projection.modelState.issue.id, projection.lineage, response),
457
+ ...usageReconciliationProblems(response),
458
+ ];
459
+ semantic(problems, 'issue triage model response rejected');
460
+ return { ...response, acceptance: acceptanceFor(pack, response, calibration) };
461
+ }
462
+ /**
463
+ * Caller totals are derived values; the provider-reported receipts are authoritative. Totals, per-kind call
464
+ * counts and the cache flag must all agree with the receipts, and a null receipt value makes the total null.
465
+ */
466
+ function usageReconciliationProblems(response) {
467
+ const problems = [];
468
+ const receipts = response.usageReceipts;
469
+ if (!Array.isArray(receipts))
470
+ return ['usage receipts are required'];
471
+ const ids = new Set();
472
+ for (const receipt of receipts) {
473
+ if (!receipt || typeof receipt !== 'object' || Object.keys(receipt).some(key => !RECEIPT_FIELDS.includes(key))
474
+ || !['jev', 'fallback-model', 'cache'].includes(receipt.kind) || typeof receipt.requestId !== 'string' || !receipt.requestId.trim()
475
+ || [receipt.inputTokens, receipt.outputTokens].some(value => value !== null && (!Number.isSafeInteger(value) || value < 0))
476
+ || (receipt.costUsd !== null && (!Number.isFinite(receipt.costUsd) || receipt.costUsd < 0))) {
477
+ problems.push('usage receipt is malformed');
478
+ continue;
479
+ }
480
+ if (ids.has(receipt.requestId))
481
+ problems.push(`usage receipt ${receipt.requestId} is duplicated`);
482
+ ids.add(receipt.requestId);
483
+ }
484
+ if (problems.length)
485
+ return problems;
486
+ const count = (kind) => receipts.filter(receipt => receipt.kind === kind).length;
487
+ if (count('jev') !== response.calls.jev)
488
+ problems.push('Jev call count does not match provider receipts');
489
+ if (count('fallback-model') !== response.calls.fallbackModel)
490
+ problems.push('fallback call count does not match provider receipts');
491
+ if ((count('cache') > 0) !== response.cacheHit)
492
+ problems.push('cache flag does not match cache receipts');
493
+ for (const key of ['inputTokens', 'outputTokens', 'costUsd']) {
494
+ const total = receiptTotal(receipts, key);
495
+ const claimed = response.usage[key];
496
+ if (total === null ? claimed !== null : claimed === null || Math.abs(claimed - total) > 1e-9) {
497
+ problems.push(`usage ${key} does not match provider receipts`);
498
+ }
499
+ }
500
+ return problems;
501
+ }
502
+ function receiptTotal(receipts, key) {
503
+ if (receipts.some(receipt => receipt[key] === null))
504
+ return null;
505
+ return receipts.reduce((sum, receipt) => sum + receipt[key], 0);
506
+ }
507
+ export async function runIssueTriageShadow(pack, issue, corpus, response, calibration) {
508
+ const active = validateIssueTriagePilotPack(pack);
509
+ if (active.mode !== 'offline-shadow')
510
+ throw new IssueTriagePilotError('issue triage shadow run requires offline-shadow mode');
511
+ const lineage = deterministicIssueDuplicateCandidates(issue, corpus, active);
512
+ const projection = await projectIssueTriageModelState(active, issue, lineage);
513
+ const validated = validateIssueTriageModelResponse(active, projection, response, calibration);
514
+ return {
515
+ schemaVersion: 'decision-issue-triage-shadow-artifact/v1',
516
+ mode: 'offline-shadow',
517
+ subject: { issueId: issue.id, stateDigest: projection.stateDigest },
518
+ lineage,
519
+ response: validated,
520
+ actionAuthorization: 'not-authorized',
521
+ trackerMutations: 0,
522
+ };
523
+ }
524
+ function failureArtifact(error) {
525
+ return {
526
+ schemaVersion: 'decision-issue-triage-shadow-failure/v1',
527
+ mode: 'offline-shadow',
528
+ actionAuthorization: 'not-authorized',
529
+ trackerMutations: 0,
530
+ error: { name: error instanceof Error ? error.name : 'Error', message: error instanceof Error ? error.message : String(error) },
531
+ };
532
+ }
533
+ /**
534
+ * Records without ever throwing or leaving a rejected promise. A recorder failure on a success artifact is
535
+ * retried once as a failure artifact; a failing failure record is dropped, because the shadow path must not
536
+ * change the host workflow.
537
+ */
538
+ function containedRecord(record, artifact, retry) {
539
+ const onError = (error) => { if (retry)
540
+ containedRecord(record, failureArtifact(error), false); };
541
+ try {
542
+ const pending = record(artifact);
543
+ if (pending && typeof pending.then === 'function')
544
+ pending.then(undefined, onError);
545
+ }
546
+ catch (error) {
547
+ onError(error);
548
+ }
549
+ }
550
+ export function applyIssueTriagePilot(enabled, previousWorkflowResult, shadow, recordArtifact = () => { }) {
551
+ if (!enabled)
552
+ return previousWorkflowResult;
553
+ try {
554
+ const artifact = shadow();
555
+ if (artifact && typeof artifact.then === 'function') {
556
+ artifact.then(resolved => containedRecord(recordArtifact, resolved, true), error => containedRecord(recordArtifact, failureArtifact(error), false)).then(undefined, () => { });
557
+ }
558
+ else {
559
+ containedRecord(recordArtifact, artifact, true);
560
+ }
561
+ }
562
+ catch (error) {
563
+ containedRecord(recordArtifact, failureArtifact(error), false);
564
+ }
565
+ return previousWorkflowResult;
566
+ }
567
+ function prf(tp, fp, fn) {
568
+ const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
569
+ const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
570
+ return { precision, recall, f1: precision + recall === 0 ? 0 : 2 * precision * recall / (precision + recall) };
571
+ }
572
+ function multiclass(samples, key, pack) {
573
+ const classes = key === 'issueType' ? pack.taxonomies.issueTypes : pack.taxonomies.areas;
574
+ const perClass = Object.fromEntries(classes.map(label => {
575
+ const tp = samples.filter(sample => sample.label[key] === label && sample.cascade[key] === label).length;
576
+ const fp = samples.filter(sample => sample.label[key] !== label && sample.cascade[key] === label).length;
577
+ const fn = samples.filter(sample => sample.label[key] === label && sample.cascade[key] !== label).length;
578
+ return [label, prf(tp, fp, fn)];
579
+ }));
580
+ const macro = {
581
+ precision: classes.reduce((sum, label) => sum + perClass[label].precision, 0) / classes.length,
582
+ recall: classes.reduce((sum, label) => sum + perClass[label].recall, 0) / classes.length,
583
+ f1: classes.reduce((sum, label) => sum + perClass[label].f1, 0) / classes.length,
584
+ };
585
+ return { sampleN: samples.length, macro, perClass };
586
+ }
587
+ function binary(samples, predicted, actual) {
588
+ const tp = samples.filter(sample => predicted(sample) && actual(sample)).length;
589
+ const fp = samples.filter(sample => predicted(sample) && !actual(sample)).length;
590
+ const fn = samples.filter(sample => !predicted(sample) && actual(sample)).length;
591
+ return { sampleN: samples.length, ...prf(tp, fp, fn) };
592
+ }
593
+ function rate(numerator, denominator) {
594
+ return denominator ? numerator / denominator : 0;
595
+ }
596
+ function correct(sample, which) {
597
+ const observed = sample[which];
598
+ return observed.issueType === sample.label.issueType
599
+ && observed.area === sample.label.area
600
+ && observed.urgency === sample.label.urgency
601
+ && observed.completeness === sample.label.completeness
602
+ && observed.clarificationNeed === sample.label.clarificationNeed
603
+ && observed.duplicate.issueId === sample.label.duplicateOf;
604
+ }
605
+ function gateDecision(upstream, findings) {
606
+ if (upstream === 'ROLLBACK')
607
+ return 'ROLLBACK';
608
+ if (upstream === 'HOLD' || findings.length > 0)
609
+ return 'HOLD';
610
+ return 'PROMOTE';
611
+ }
612
+ /** Supported interval level in bps, or null. The paired helpers accept integer levels strictly in (5000, 9999). */
613
+ function levelInBps(confidenceInterval) {
614
+ if (confidenceInterval.method !== 'wilson')
615
+ return null;
616
+ const scaled = confidenceInterval.level * 10000;
617
+ const levelBps = Math.round(scaled);
618
+ return Math.abs(scaled - levelBps) < 1e-6 && levelBps > 5000 && levelBps < 9999 ? levelBps : null;
619
+ }
620
+ /**
621
+ * The evaluated samples must be exactly the test split of splits frozen before scoring, and those splits must be
622
+ * the ones whose digests the manifest preregistered.
623
+ */
624
+ function verifiedSplits(manifest, splits, samples) {
625
+ verifyQualificationSplits(splits);
626
+ const byName = new Map(splits.map(split => [split.name, split]));
627
+ const pinned = manifest.dataset.splitDigests;
628
+ if (byName.get('tuning').digest !== pinned.tuning || byName.get('calibration').digest !== pinned.calibration
629
+ || byName.get('test').digest !== pinned.test) {
630
+ throw new IssueTriagePilotError('evaluation split digests do not match the preregistered manifest');
631
+ }
632
+ const test = byName.get('test');
633
+ const ids = samples.map(sample => sample.id);
634
+ if (ids.length !== test.ids.length || new Set(ids).size !== ids.length || ids.some(id => !test.ids.includes(id))) {
635
+ throw new IssueTriagePilotError('evaluation samples do not match the frozen test split membership');
636
+ }
637
+ return [byName.get('tuning'), byName.get('calibration'), test];
638
+ }
639
+ /** Scores a subset of the already-verified test split with the same frozen tuning/calibration splits. */
640
+ function subsetSplits(splits, ids) {
641
+ return [splits[0], splits[1], freezeQualificationSplit('test', ids)];
642
+ }
643
+ /** Re-derives the deterministic ordering and checks the cascade's duplicate answer against it. */
644
+ function verifySampleLineage(sample, pack) {
645
+ const lineage = sample.lineage;
646
+ const where = `sample ${sample.id}`;
647
+ if (!lineage || lineage.generator !== pack.candidatePolicy.generator || lineage.noneOutcome !== 'none' || !Array.isArray(lineage.candidates)) {
648
+ throw new IssueTriagePilotError(`${where} lineage is missing or was not produced by the pinned generator`);
649
+ }
650
+ const candidates = lineage.candidates;
651
+ if (candidates.length > pack.candidatePolicy.maxCandidates || new Set(candidates.map(item => item.issueId)).size !== candidates.length
652
+ || candidates.some(item => item.issueId === 'none')) {
653
+ throw new IssueTriagePilotError(`${where} lineage exceeds the candidate policy or repeats an ID`);
654
+ }
655
+ for (let index = 0; index < candidates.length; index++) {
656
+ const current = candidates[index];
657
+ const previous = candidates[index - 1];
658
+ if (current.rank !== index + 1 || !Number.isFinite(current.score) || current.score <= 0 || current.score > 1
659
+ || (previous && (previous.score < current.score || (previous.score === current.score && previous.issueId.localeCompare(current.issueId) > 0)))) {
660
+ throw new IssueTriagePilotError(`${where} lineage is not deterministically ordered`);
661
+ }
662
+ }
663
+ const answer = sample.cascade.duplicate;
664
+ if (answer.issueId === 'none') {
665
+ if (answer.rank !== null)
666
+ throw new IssueTriagePilotError(`${where} none duplicate outcome must not carry a rank`);
667
+ return;
668
+ }
669
+ const listed = candidates.find(item => item.issueId === answer.issueId);
670
+ if (!listed)
671
+ throw new IssueTriagePilotError(`${where} duplicate ${answer.issueId} is not in the deterministic lineage`);
672
+ if (listed.rank !== answer.rank) {
673
+ throw new IssueTriagePilotError(`${where} duplicate rank for ${answer.issueId} does not match deterministic rank ${listed.rank}`);
674
+ }
675
+ }
676
+ function armUsage(samples, which, key) {
677
+ const totals = samples.map(sample => receiptTotal(sample[which].usageReceipts, key));
678
+ if (totals.some(total => total === null))
679
+ return null;
680
+ return totals.reduce((sum, value) => sum + value, 0);
681
+ }
682
+ function sliceMetrics(samples) {
683
+ const accepted = samples.filter(sample => sample.cascade.acceptance.acceptedScoring);
684
+ const noneCases = samples.filter(sample => sample.label.duplicateOf === 'none');
685
+ const duplicateCases = samples.filter(sample => sample.label.duplicateOf !== 'none');
686
+ return {
687
+ cascadeQuality: rate(samples.filter(sample => correct(sample, 'cascade')).length, samples.length),
688
+ baselineQuality: rate(samples.filter(sample => correct(sample, 'baseline')).length, samples.length),
689
+ acceptedCoverage: rate(accepted.length, samples.length),
690
+ falseAutoRate: accepted.length ? accepted.filter(sample => !correct(sample, 'cascade')).length / accepted.length : null,
691
+ falseDuplicateRate: noneCases.length ? noneCases.filter(sample => sample.cascade.duplicate.issueId !== 'none').length / noneCases.length : null,
692
+ duplicateRecall: duplicateCases.length
693
+ ? duplicateCases.filter(sample => sample.cascade.duplicate.issueId === sample.label.duplicateOf).length / duplicateCases.length : null,
694
+ };
695
+ }
696
+ /**
697
+ * Validates each sample and recomputes its cascade acceptance with the same rule the shadow runtime uses.
698
+ * Caller-supplied acceptance and compatibility pins are discarded.
699
+ */
700
+ function scoredSamples(input, pack, manifest) {
701
+ const registeredSlices = new Set(manifest.slices.map(slice => slice.id));
702
+ return input.samples.map(sample => {
703
+ const where = `sample ${sample.id}`;
704
+ if (sample.label.id !== sample.id)
705
+ throw new IssueTriagePilotError(`${where} label id does not match`);
706
+ for (const arm of ['baseline', 'cascade']) {
707
+ if (sample[arm].issueId !== sample.id) {
708
+ throw new IssueTriagePilotError(`${where} ${arm} issue id ${sample[arm].issueId} does not match`);
709
+ }
710
+ }
711
+ for (const key of ['issueType', 'area']) {
712
+ const allowed = key === 'issueType' ? pack.taxonomies.issueTypes : pack.taxonomies.areas;
713
+ if (!allowed.includes(sample.label[key])) {
714
+ throw new IssueTriagePilotError(`${where} label ${key} ${sample.label[key]} is not in the governed taxonomy`);
715
+ }
716
+ }
717
+ if (!pack.taxonomies.stateCompleteness.includes(sample.label.completeness)
718
+ || !pack.taxonomies.clarificationNeed.includes(sample.label.clarificationNeed)) {
719
+ throw new IssueTriagePilotError(`${where} label completeness or clarification is not in the governed taxonomy`);
720
+ }
721
+ for (const slice of sample.label.slices) {
722
+ if (!registeredSlices.has(slice))
723
+ throw new IssueTriagePilotError(`${where} slice ${slice} is not preregistered`);
724
+ }
725
+ verifySampleLineage(sample, pack);
726
+ semantic(responseProblems(pack, sample.id, sample.lineage, sample.baseline), `${where} baseline response rejected`);
727
+ const { acceptance: _ignored, ...response } = sample.cascade;
728
+ semantic(responseProblems(pack, sample.id, sample.lineage, response), `${where} cascade response rejected`);
729
+ const registry = input.calibration.registry;
730
+ const request = registry?.requests[sample.id];
731
+ const acceptance = acceptanceFor(pack, response, {
732
+ requestedModel: input.calibration.requestedModel,
733
+ actualModel: response.actualModel,
734
+ compatibleActualModels: input.calibration.compatibleActualModels,
735
+ uncertaintyProfile: input.calibration.uncertaintyProfile,
736
+ ...(registry && request ? { registry: { registry: registry.registry, policy: registry.policy, request } } : {}),
737
+ });
738
+ return { ...sample, cascade: { ...response, acceptance } };
739
+ });
740
+ }
741
+ /**
742
+ * Freezes caller-supplied calibration evidence into a manifest pin: the canonical digest of the registry
743
+ * content (artifacts, relations and the alias history for the rule's alias) and of the policy, plus the
744
+ * rule every per-sample request must satisfy. Two registries built from the same content pin identically.
745
+ */
746
+ export function pinIssueTriageCalibration(registry, policy, requestRule) {
747
+ if (!requestRule.requestedAlias.trim() || !requestRule.runIdPrefix.trim()
748
+ || !Number.isFinite(Date.parse(requestRule.notBefore))) {
749
+ throw new IssueTriagePilotError('issue triage calibration request rule must pin an alias, run id prefix and notBefore instant');
750
+ }
751
+ return {
752
+ registryDigest: digest({
753
+ artifacts: registry.artifactHistory(),
754
+ relations: registry.compatibilityHistory(),
755
+ aliases: registry.aliasHistory(requestRule.requestedAlias),
756
+ }),
757
+ policyDigest: digest(policy),
758
+ requestRule: { ...requestRule },
759
+ };
760
+ }
761
+ /** The evaluated pack must be exactly the pack the manifest preregistered: same id, version and canonical digest. */
762
+ function verifyPilotPackPin(pack, manifest) {
763
+ const pin = manifest.pilotPack;
764
+ if (pin.id !== pack.id || pin.version !== pack.version || pin.digest !== artifactDigest(pack)) {
765
+ throw new IssueTriagePilotError('issue triage pilot pack does not match the manifest pin');
766
+ }
767
+ }
768
+ /**
769
+ * Caller-supplied calibration evidence must be exactly what the manifest preregistered: the registry
770
+ * content and policy digests must match, and every evaluated sample needs exactly one request satisfying
771
+ * the preregistered rule. Anything else is rejected before scoring, never absorbed into a finding.
772
+ */
773
+ function verifyCalibrationPin(manifest, calibration, sampleIds) {
774
+ const pin = manifest.calibration;
775
+ const supplied = calibration.registry;
776
+ if (pin.registryDigest === null || pin.policyDigest === null || pin.requestRule === null) {
777
+ if (supplied)
778
+ throw new IssueTriagePilotError('issue triage calibration registry was not preregistered in the manifest');
779
+ return;
780
+ }
781
+ if (!supplied)
782
+ throw new IssueTriagePilotError('issue triage evaluation requires the preregistered calibration registry');
783
+ const rule = pin.requestRule;
784
+ if (digest({
785
+ artifacts: supplied.registry.artifactHistory(),
786
+ relations: supplied.registry.compatibilityHistory(),
787
+ aliases: supplied.registry.aliasHistory(rule.requestedAlias),
788
+ }) !== pin.registryDigest) {
789
+ throw new IssueTriagePilotError('issue triage calibration registry does not match the manifest pin');
790
+ }
791
+ if (digest(supplied.policy) !== pin.policyDigest) {
792
+ throw new IssueTriagePilotError('issue triage calibration policy does not match the manifest pin');
793
+ }
794
+ if (canonicalJson(Object.keys(supplied.requests).sort()) !== canonicalJson([...sampleIds].sort())) {
795
+ throw new IssueTriagePilotError('issue triage calibration requests do not cover the evaluated samples exactly once');
796
+ }
797
+ for (const id of sampleIds) {
798
+ const request = supplied.requests[id];
799
+ const where = `issue triage calibration request for sample ${id}`;
800
+ if (request.runId !== `${rule.runIdPrefix}${id}`) {
801
+ throw new IssueTriagePilotError(`${where} request runId ${request.runId} does not match the preregistered rule`);
802
+ }
803
+ if (request.requestedAlias !== rule.requestedAlias) {
804
+ throw new IssueTriagePilotError(`${where} requestedAlias ${request.requestedAlias} does not match the preregistered rule`);
805
+ }
806
+ if ((request.calibrationArtifactId ?? null) !== rule.calibrationArtifactId) {
807
+ throw new IssueTriagePilotError(`${where} calibrationArtifactId ${request.calibrationArtifactId ?? 'null'} does not match the preregistered rule`);
808
+ }
809
+ const at = Date.parse(request.at);
810
+ if (!Number.isFinite(at))
811
+ throw new IssueTriagePilotError(`${where} has an invalid at instant`);
812
+ if (at < Date.parse(rule.notBefore)) {
813
+ throw new IssueTriagePilotError(`${where} predates the preregistered rule`);
814
+ }
815
+ }
816
+ }
817
+ export function buildIssueTriageEvaluationReport(input) {
818
+ const pack = validateIssueTriagePilotPack(input.pack);
819
+ const manifest = validateIssueTriageEvaluationManifest(input.manifest);
820
+ verifyPilotPackPin(pack, manifest);
821
+ if (!input.samples.length)
822
+ throw new IssueTriagePilotError('issue triage evaluation requires at least one held-out sample');
823
+ verifyCalibrationPin(manifest, input.calibration, input.samples.map(sample => sample.id));
824
+ const splits = verifiedSplits(manifest, input.splits, input.samples);
825
+ const samples = scoredSamples(input, pack, manifest);
826
+ const findings = new Set();
827
+ const levelBps = levelInBps(manifest.thresholds.confidenceInterval);
828
+ if (levelBps === null)
829
+ findings.add('confidence-interval-unsupported');
830
+ if (samples.length < manifest.thresholds.minimumTotalSamples)
831
+ findings.add('insufficient-total-samples');
832
+ const sliceReports = manifest.slices.map(slice => {
833
+ const members = samples.filter(sample => sample.label.slices.includes(slice.id));
834
+ const minimumSupport = Math.max(slice.minimumSupport, manifest.thresholds.minimumPerSliceSamples);
835
+ const status = members.length >= minimumSupport ? 'supported' : 'insufficient';
836
+ if (status === 'insufficient')
837
+ findings.add(`slice-insufficient:${slice.id}`);
838
+ const suppressed = status === 'insufficient' && slice.aggregation === 'suppress-small-n';
839
+ return {
840
+ id: slice.id, dimension: slice.dimension, aggregation: slice.aggregation,
841
+ sampleN: suppressed ? null : members.length, minimumSupport, status, suppressed,
842
+ metrics: suppressed || !members.length ? null : sliceMetrics(members),
843
+ };
844
+ });
845
+ const urgencyOrdinal = new Map(pack.urgencyRubric.map(level => [level.id, level.ordinal]));
846
+ const ordinalOf = (id, where) => {
847
+ const ordinal = urgencyOrdinal.get(id);
848
+ if (ordinal === undefined)
849
+ throw new IssueTriagePilotError(`${where} urgency ${id} is not in the governed rubric`);
850
+ return ordinal;
851
+ };
852
+ const ordinal = evaluateOrdinalHeldout(splits, samples.map(sample => ({
853
+ id: sample.id,
854
+ trueLevel: ordinalOf(sample.label.urgency, `sample ${sample.id} label`),
855
+ predictedLevel: ordinalOf(sample.cascade.urgency, `sample ${sample.id} cascade`),
856
+ levels: pack.urgencyRubric.length,
857
+ })));
858
+ const duplicateCases = samples.filter(sample => sample.label.duplicateOf !== 'none');
859
+ const noneCases = samples.filter(sample => sample.label.duplicateOf === 'none');
860
+ const falseDuplicates = noneCases.filter(sample => sample.cascade.duplicate.issueId !== 'none').length;
861
+ const duplicateHits = duplicateCases.filter(sample => sample.cascade.duplicate.issueId === sample.label.duplicateOf).length;
862
+ const ranking = duplicateCases.length
863
+ ? evaluateRankingHeldout(subsetSplits(splits, duplicateCases.map(sample => sample.id)), duplicateCases.map(sample => {
864
+ const options = new Set([...sample.lineage.candidates.map(item => item.issueId), 'none', sample.label.duplicateOf]);
865
+ return {
866
+ id: sample.id,
867
+ gold: Object.fromEntries([...options].map(option => [option, option === sample.label.duplicateOf ? 1 : 0])),
868
+ predicted: Object.fromEntries([...options].map(option => [option, option === sample.cascade.duplicate.issueId ? 1 : 0])),
869
+ };
870
+ }))
871
+ : { sampleN: 0, comparablePairs: 0, concordance: null };
872
+ const duplicateSupported = duplicateCases.length >= manifest.thresholds.minimumDuplicateSamples;
873
+ if (!duplicateSupported)
874
+ findings.add('duplicate-insufficient');
875
+ // Ranks below were re-validated against each sample's deterministic lineage.
876
+ const topK = {};
877
+ for (const k of [1, 3, 5]) {
878
+ topK[`top${k}`] = rate(duplicateCases.filter(sample => sample.cascade.duplicate.issueId === sample.label.duplicateOf
879
+ && sample.cascade.duplicate.rank !== null && sample.cascade.duplicate.rank <= k).length, duplicateCases.length);
880
+ }
881
+ const ndcg = rate(duplicateCases.reduce((sum, sample) => sample.cascade.duplicate.issueId === sample.label.duplicateOf
882
+ && sample.cascade.duplicate.rank !== null ? sum + 1 / Math.log2(sample.cascade.duplicate.rank + 1) : sum, 0), duplicateCases.length);
883
+ // Selective risk and coverage come from the held-out helper: label 1 means "cascade answer correct".
884
+ const heldout = evaluateBinaryHeldout(splits, samples.map(sample => ({
885
+ id: sample.id,
886
+ slice: 'all',
887
+ label: 1,
888
+ probability: correct(sample, 'cascade') ? 1 : 0,
889
+ accepted: sample.cascade.acceptance.acceptedScoring,
890
+ latencyMs: sample.cascade.latencyMs,
891
+ inputTokens: receiptTotal(sample.cascade.usageReceipts, 'inputTokens'),
892
+ outputTokens: receiptTotal(sample.cascade.usageReceipts, 'outputTokens'),
893
+ costUsd: receiptTotal(sample.cascade.usageReceipts, 'costUsd'),
894
+ calls: sample.cascade.calls.jev + sample.cascade.calls.fallbackModel,
895
+ retries: sample.cascade.retries,
896
+ fallbacks: sample.cascade.fallbacks,
897
+ }))).overall;
898
+ const acceptedN = samples.filter(sample => sample.cascade.acceptance.acceptedScoring).length;
899
+ const acceptedErrors = heldout.selectiveRisk === null ? 0 : Math.round(heldout.selectiveRisk * acceptedN);
900
+ const selectiveRiskUpper = levelBps === null || acceptedN === 0 ? null
901
+ : wilsonScoreInterval({ events: acceptedErrors, n: acceptedN, levelBps })[1];
902
+ const coverageLower = levelBps === null ? null : wilsonScoreInterval({ events: acceptedN, n: samples.length, levelBps })[0];
903
+ const usageReconciled = samples.every(sample => usageReconciliationProblems(sample.baseline).length === 0
904
+ && usageReconciliationProblems(sample.cascade).length === 0);
905
+ if (!usageReconciled)
906
+ findings.add('usage-unreconciled');
907
+ const baselineComparison = comparison(samples, usageReconciled);
908
+ let qualityNonInferiority = null;
909
+ if (levelBps !== null) {
910
+ const counts = { both: 0, candidateOnly: 0, baselineOnly: 0, neither: 0 };
911
+ for (const sample of samples) {
912
+ const cascadeCorrect = correct(sample, 'cascade');
913
+ const baselineCorrect = correct(sample, 'baseline');
914
+ if (cascadeCorrect && baselineCorrect)
915
+ counts.both++;
916
+ else if (cascadeCorrect)
917
+ counts.candidateOnly++;
918
+ else if (baselineCorrect)
919
+ counts.baselineOnly++;
920
+ else
921
+ counts.neither++;
922
+ }
923
+ const marginBps = Math.round(manifest.thresholds.qualityNonInferiorityMargin * 10000);
924
+ const interval = pairedBinaryDifferenceInterval({ counts, levelBps, method: 'newcombe-10' });
925
+ const verdict = pairedNonInferiority({ interval, marginBps });
926
+ qualityNonInferiority = {
927
+ method: 'newcombe-hybrid-score', levelBps, marginBps, lowerBps: interval.lowerBps, upperBps: interval.upperBps,
928
+ estimateBps: interval.estimateBps, n: interval.n, decision: verdict.decision,
929
+ };
930
+ if (verdict.decision !== 'non-inferior')
931
+ findings.add('quality-non-inferiority');
932
+ if (selectiveRiskUpper === null)
933
+ findings.add('false-auto-insufficient');
934
+ else if (selectiveRiskUpper > manifest.thresholds.maximumFalseAutoRate)
935
+ findings.add('false-auto-rate');
936
+ if (coverageLower < manifest.thresholds.minimumAcceptedCoverage)
937
+ findings.add('accepted-coverage');
938
+ if (duplicateSupported && wilsonScoreInterval({ events: duplicateHits, n: duplicateCases.length, levelBps })[0]
939
+ < manifest.thresholds.minimumDuplicateRecall) {
940
+ findings.add('duplicate-recall');
941
+ }
942
+ if (!noneCases.length)
943
+ findings.add('false-duplicate-insufficient');
944
+ else if (wilsonScoreInterval({ events: falseDuplicates, n: noneCases.length, levelBps })[1] > manifest.thresholds.maximumFalseDuplicateRate) {
945
+ findings.add('false-duplicate-rate');
946
+ }
947
+ }
948
+ const compatibilityRequired = pack.acceptance.calibration === 'required';
949
+ if (compatibilityRequired && (!input.calibration.registry
950
+ || samples.some(sample => sample.cascade.acceptance.compatibility === null))) {
951
+ findings.add('calibration-required-unverified');
952
+ }
953
+ const benefit = economicsBenefit(baselineComparison, manifest.thresholds.benefit.metric);
954
+ if (benefit === null)
955
+ findings.add('benefit-insufficient');
956
+ else if (benefit <= 0)
957
+ findings.add('benefit-not-positive');
958
+ const sortedFindings = [...findings].sort();
959
+ const report = {
960
+ schemaVersion: 'decision-issue-triage-evaluation-report/v1',
961
+ id: input.id,
962
+ manifest: { id: manifest.id, version: 'v1', digest: digest(manifest) },
963
+ pilotPack: { id: pack.id, version: pack.version, digest: artifactDigest(pack) },
964
+ classCounts: {
965
+ issueType: countBy(samples.map(sample => sample.label.issueType)),
966
+ area: countBy(samples.map(sample => sample.label.area)),
967
+ completeness: countBy(samples.map(sample => sample.label.completeness)),
968
+ clarification: countBy(samples.map(sample => sample.label.clarificationNeed)),
969
+ },
970
+ classification: { issueType: multiclass(samples, 'issueType', pack), area: multiclass(samples, 'area', pack) },
971
+ urgency: {
972
+ sampleN: ordinal.sampleN,
973
+ meanAbsoluteError: ordinal.meanAbsoluteError,
974
+ normalizedAbsoluteError: ordinal.normalizedAbsoluteError,
975
+ exactRate: ordinal.exactRate,
976
+ },
977
+ completeness: binary(samples, sample => sample.cascade.completeness === 'complete', sample => sample.label.completeness === 'complete'),
978
+ clarification: binary(samples, sample => sample.cascade.clarificationNeed === 'needed', sample => sample.label.clarificationNeed === 'needed'),
979
+ duplicates: {
980
+ sampleN: samples.length,
981
+ duplicateN: duplicateCases.length,
982
+ noneN: noneCases.length,
983
+ recall: rate(duplicateHits, duplicateCases.length),
984
+ concordance: ranking.concordance,
985
+ comparablePairs: ranking.comparablePairs,
986
+ ndcg,
987
+ topK,
988
+ falseDuplicateRate: rate(falseDuplicates, noneCases.length),
989
+ noneRecall: rate(noneCases.length - falseDuplicates, noneCases.length),
990
+ },
991
+ slices: sliceReports,
992
+ calibration: {
993
+ riskCoverage: {
994
+ sampleN: heldout.sampleN, acceptedN, coverage: heldout.coverage, coverageLower,
995
+ selectiveRisk: heldout.selectiveRisk, selectiveRiskUpper,
996
+ },
997
+ driftEvents: [...new Set(samples.map(sample => sample.cascade.acceptance.driftEvent).filter((value) => value !== null))].sort(),
998
+ compatibilityRequired,
999
+ },
1000
+ operations: {
1001
+ reviewLoad: rate(samples.filter(sample => !sample.cascade.acceptance.acceptedScoring || sample.cascade.clarificationNeed === 'needed').length, samples.length),
1002
+ overrideRate: rate(samples.filter(sample => sample.label.reviewerWouldOverride).length, samples.length),
1003
+ latencyMs: heldout.latencyMs,
1004
+ tokens: { input: armUsage(samples, 'cascade', 'inputTokens'), output: armUsage(samples, 'cascade', 'outputTokens') },
1005
+ costUsd: armUsage(samples, 'cascade', 'costUsd'),
1006
+ calls: {
1007
+ jev: samples.reduce((sum, sample) => sum + sample.cascade.usageReceipts.filter(receipt => receipt.kind === 'jev').length, 0),
1008
+ fallbackModel: samples.reduce((sum, sample) => sum + sample.cascade.usageReceipts.filter(receipt => receipt.kind === 'fallback-model').length, 0),
1009
+ },
1010
+ retryRate: rate(samples.filter(sample => sample.cascade.retries > 0).length, samples.length),
1011
+ fallbackRate: rate(samples.filter(sample => sample.cascade.fallbacks > 0).length, samples.length),
1012
+ cache: {
1013
+ hits: samples.filter(sample => sample.cascade.usageReceipts.some(receipt => receipt.kind === 'cache')).length,
1014
+ misses: samples.filter(sample => !sample.cascade.usageReceipts.some(receipt => receipt.kind === 'cache')).length,
1015
+ },
1016
+ usageReconciled,
1017
+ },
1018
+ baselineComparison: { ...baselineComparison, qualityNonInferiority },
1019
+ integrity: { upstreamDecision: input.upstreamDecision, findings: sortedFindings },
1020
+ decision: gateDecision(input.upstreamDecision, sortedFindings),
1021
+ };
1022
+ return checkIssueTriageEvaluationReportShape(report);
1023
+ }
1024
+ /**
1025
+ * Net benefit of the cascade over the baseline on the preregistered metric only. A null value means the
1026
+ * evidence is insufficient; the token count is never substituted for an unknown cost.
1027
+ */
1028
+ function economicsBenefit(comparisonArms, metric) {
1029
+ const delta = metric === 'reviewer-time' ? comparisonArms.delta.reviewerTimeMinutes : comparisonArms.delta.costUsd;
1030
+ return delta === null ? null : -delta;
1031
+ }
1032
+ function countBy(values) {
1033
+ const counts = {};
1034
+ for (const value of values)
1035
+ counts[value] = (counts[value] ?? 0) + 1;
1036
+ return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
1037
+ }
1038
+ /** Arm totals are summed from provider receipts; unreconciled usage leaves token and cost comparisons unknown. */
1039
+ function comparison(samples, usageReconciled) {
1040
+ const arm = (which) => {
1041
+ const input = armUsage(samples, which, 'inputTokens');
1042
+ const output = armUsage(samples, which, 'outputTokens');
1043
+ return {
1044
+ quality: rate(samples.filter(sample => correct(sample, which)).length, samples.length),
1045
+ tokens: !usageReconciled || input === null || output === null ? null : input + output,
1046
+ costUsd: usageReconciled ? armUsage(samples, which, 'costUsd') : null,
1047
+ reviewerTimeMinutes: samples.some(sample => !Number.isFinite(sample.label.reviewerTimeBaselineMinutes)
1048
+ || !Number.isFinite(sample.label.reviewerTimeCascadeMinutes)) ? null
1049
+ : samples.reduce((sum, sample) => sum + (which === 'baseline' ? sample.label.reviewerTimeBaselineMinutes : sample.label.reviewerTimeCascadeMinutes), 0),
1050
+ };
1051
+ };
1052
+ const baseline = arm('baseline');
1053
+ const cascade = arm('cascade');
1054
+ return {
1055
+ baseline,
1056
+ cascade,
1057
+ delta: {
1058
+ quality: cascade.quality - baseline.quality,
1059
+ tokens: baseline.tokens === null || cascade.tokens === null ? null : cascade.tokens - baseline.tokens,
1060
+ costUsd: baseline.costUsd === null || cascade.costUsd === null ? null : cascade.costUsd - baseline.costUsd,
1061
+ reviewerTimeMinutes: baseline.reviewerTimeMinutes === null || cascade.reviewerTimeMinutes === null
1062
+ ? null : cascade.reviewerTimeMinutes - baseline.reviewerTimeMinutes,
1063
+ },
1064
+ };
1065
+ }
1066
+ export function issueTriageArtifactDigest(value) {
1067
+ return artifactDigest(value);
1068
+ }
1069
+ //# sourceMappingURL=runtime.js.map