@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,596 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { heldoutApprovalTemplate, heldoutDigest, heldoutRequestSize, heldoutReservationMicros, heldoutReservationTokens, validateHeldoutAttempt, validateHeldoutInputs } from '../heldout/contract.js';
3
+ import { heldoutCollectionAllowance } from '../heldout/journal.js';
4
+ import { wilsonScoreInterval } from '../qualification/quality.js';
5
+ import { D17_MULTIFACT_CALIBRATION_NOT_AFTER, D17_MULTIFACT_GENERATOR_ID, D17_MULTIFACT_PUBLIC_SEEDS, d17MultifactPublicSeed, studyHeldoutGeneratorDigest } from '../heldout/study-generators.js';
6
+ import { applyD17Mapping, d17AttemptsByRow, d17NativeYes, qualifyD17Calibration } from './calibration.js';
7
+ import { D17_CALIBRATION } from './protocol.js';
8
+ import { D17MF_CELLS, D17MF_DEFINITION, D17MF_HOPS, D17MF_OUTCOMES, D17MF_POOL_IDS, D17MF_POOLS, D17MF_ROWS, D17MF_TEST_COUNTS, cellKey, d17MfGenerateCase, d17MfTextOracle } from './multifact.js';
9
+ import { validateD17MultifactArtifact } from './multifact-artifacts.js';
10
+ /**
11
+ * D17-MF: preregistered, calibrated probe of Jev's multi-fact degradation (#2850).
12
+ *
13
+ * Primary reasoning metrics are threshold-free of any calibrator: the raw native answer (P(yes) > 0.5) and the AUROC of
14
+ * the raw P(yes) between yes rows and missing-link rows. The registered D17 member calibrator was fitted on the pool-b
15
+ * grammar, so calibrated answers would confound pool contrasts with where that calibrator sets its threshold; calibrated
16
+ * metrics serve only the calibration-under-shift rule.
17
+ *
18
+ * Calibration choice: collector artifact mode binds the registered D17 calibration set, and the member calibrator is
19
+ * applied to the single fresh call per row. A calibrator fitted on the shifted pools would absorb exactly the
20
+ * miscalibration under shift this probe measures. The calibrator is re-derived from the D17 seal and qualified at bundle
21
+ * time, at collection end and at the scoring clock; it is never trusted unverified and never skipped.
22
+ */
23
+ /** The registered D17 member calibrator's expiry: effectiveAt 2026-10-02T10:02:34.263Z plus the profile's 30 days. */
24
+ export const D17MF_ARTIFACT_EXPIRY = D17_MULTIFACT_CALIBRATION_NOT_AFTER;
25
+ export const D17MF_PROTOCOL = Object.freeze({
26
+ schemaVersion: 'decision-d17mf-protocol/v2', issue: 'roctinam/aiwg#2850', study: 'D17', generator: D17_MULTIFACT_GENERATOR_ID,
27
+ design: Object.freeze({ pools: D17MF_POOL_IDS, hops: D17MF_HOPS, outcomes: D17MF_OUTCOMES, layouts: ['equalized', 'd17-faithful'],
28
+ counts: D17MF_TEST_COUNTS, developmentPerCell: 2, rows: D17MF_ROWS,
29
+ keyCells: 'equalized missing-link cells: 3 pools x 2 hops x 3 written encodings, plus pure absence at 3 hops; 220 rows each (Wilson 95% half-width 0.066 at p = 0.5)',
30
+ supportingCells: 'equalized yes (110) and relay-off (40) cells and D17-faithful control cells (30)',
31
+ rule: 'equalized cells state an exhaustive link-only rule; the D17-faithful control keeps the original D17 rule',
32
+ order: 'equalized fact lines are shuffled uniformly; the rule line is last',
33
+ sizeBound: 'one corpus stays inside the shared credential-scan traversal limit (100,000 nodes, 17 per row)',
34
+ limitation: 'within a pool the link verb and the framing sentence co-vary (no verb x framing cross-cell fits the row bound), so H1 attributes a pool difference to the pool wording as a whole',
35
+ dispatchOrder: 'development rows first, then test rows in a seeded Fisher-Yates permutation keyed by the corpus seed digest',
36
+ requestsPerRow: 1, request: 'champion: one fresh call of jev-1.13.0 with the D17 DecisionDefinition' }),
37
+ metrics: Object.freeze({
38
+ primary: ['raw native answer accuracy (P(yes) > 0.5) per cell, Wilson 95%',
39
+ 'AUROC of the raw P(yes), yes rows vs missing-link rows per pool x hops (pooled and per encoding), Hanley-McNeil 95%'],
40
+ secondary: 'calibrated accuracy, calibrated confidence and decile ECE through the registered D17 member calibrator; used only by the calibration-under-shift rule',
41
+ positions: 'failure position as first, interior or last link (or relay); accuracy is reported by position'
42
+ }),
43
+ calibration: Object.freeze({ mode: 'artifact', calibrator: D17_CALIBRATION.calibrators.member.id,
44
+ binding: 'approval.calibration.calibrationArtifactDigest = registered D17 calibration set digest',
45
+ verification: 'the D17 calibration set is re-derived from its sealed calibration phase; the member artifact is qualified at bundle time, at collection end and at the scoring clock',
46
+ expiresAt: D17MF_ARTIFACT_EXPIRY, profile: D17_CALIBRATION.profile }),
47
+ interval: Object.freeze({ cell: 'wilson', difference: 'newcombe-hybrid-score-independent', auroc: 'hanley-mcneil', levelBps: 9500 }),
48
+ multiplicity: Object.freeze({ method: 'holm', alpha: 0.05, test: 'two-sided two-proportion z (pooled)', families: ['H1 pool-pair contrasts', 'H4 per-pool layout contrasts'] }),
49
+ coverage: Object.freeze({ minimumObservedFraction: 0.95, rule: 'every preregistered test cell must reach it; otherwise the report is incomplete and every verdict is "insufficient"' }),
50
+ scoringClock: 'collection end is bounded by the latest session start bound into frozen.json (digest-covered evidence) plus the preregistered session limit, across the run lineage; evaluatedAt is at least that bound and at most now; the collector refuses any session whose window passes the calibrator expiry; scoring happens at the exact approved source commit',
51
+ hypotheses: Object.freeze([
52
+ Object.freeze({ id: 'H1', name: 'wording', claim: 'Missing-link raw accuracy depends on the wording pool at fixed hops and encoding.' }),
53
+ Object.freeze({ id: 'H2', name: 'depth', claim: 'Raw accuracy falls from one relay hop to three at fixed pool, outcome and failure position (first or last).' }),
54
+ Object.freeze({ id: 'H3', name: 'missing-link-polarity', claim: 'A missing link is read as established (permissive linking), while a disabled relay is handled.' }),
55
+ Object.freeze({ id: 'H3a', name: 'encoding', claim: 'The failure is specific to the contrastive verb rather than to missing links in general (pure absence is the baseline).' }),
56
+ Object.freeze({ id: 'H4', name: 'layout', claim: 'At one hop and the last link, the D17-faithful rendering differs from the equalized rendering (rule wording, line order, decoys, names).' }),
57
+ ]),
58
+ decisionRules: Object.freeze({
59
+ H1: 'supported when, for some missing-link encoding, a pool-pair raw-accuracy contrast is Holm-significant (family-wise 0.05) with |difference| >= 0.10 at every hop level the encoding has',
60
+ H2: 'supported when, for some outcome, raw accuracy(h3) - accuracy(h1), restricted to first- and last-position failures, has a Newcombe 95% upper bound below -0.05 in at least 2 of 3 pools',
61
+ H3: 'supported when, in every pool (pooled over hops), relay-off minus missing-link raw accuracy has a 95% lower bound above 0.10 for some encoding and that encoding is answered "yes" on more than half its rows',
62
+ H3a: '"contrastive-specific" when, in at least 2 pools, contrastive raw accuracy is below both negation and the non-connective verb (95% upper bound below 0 and |difference| >= 0.15); "general" when the three written encodings are within 0.10 of each other and all below relay-off; otherwise "mixed"; the pure-absence baseline is reported against interior-position contrastive rows',
63
+ H4: 'supported when some pool shows a Holm-significant raw-accuracy difference (d17-faithful minus equalized, one hop, last-link failures, missing-contrastive)',
64
+ replication: 'the D17 failure replicates when pool-c d17-faithful missing-contrastive raw accuracy has a Wilson 95% upper bound at most 0.50',
65
+ calibrationUnderShift: 'a cell is calibration-unreliable when mean calibrated confidence minus calibrated accuracy exceeds 0.10 and the calibrated accuracy upper bound is below the mean confidence',
66
+ }),
67
+ shortcutAudit: Object.freeze({ population: 'equalized test rows, per pool x hops, target gold yes/no', folds: 5, limit: 0.6,
68
+ learners: ['cross-validated best single scalar threshold', 'cross-validated best token-count threshold', 'cross-validated depth-2 tree over scalar features',
69
+ 'cross-validated L2 logistic regression over scalar features'],
70
+ features: ['line count', 'character length', 'digit count', 'word count', 'cue counts (link, contrastive, negation, non-connective, enabled, disabled)',
71
+ 'per cue class: mean, minimum and maximum normalized line position', 'positions of the first lines mentioning the query subject and destination and their order',
72
+ 'classes of the first and last fact lines', 'distinct entities', 'query subject and destination mentions', 'token counts with entity names abstracted'],
73
+ positiveControl: 'the chain-first line order of the earlier layout must fail the audit (regression-tested)',
74
+ control: 'd17-faithful rows are audited separately and reported; they reproduce D17 and are expected to be cue-solvable' }),
75
+ budget: 'shares the D17 study cap (USD 8) with the D17 runs: the approval attests all prior D17 and program spend as floors; this study budget is USD 5',
76
+ promotion: 'none: a diagnostic probe of the provider; the study decision is always HOLD',
77
+ });
78
+ /** Public and review seeds can regenerate gold from source; they never back a paid collection. */
79
+ export const D17MF_PUBLIC_SEEDS = D17_MULTIFACT_PUBLIC_SEEDS;
80
+ /** The same rule `validateHeldoutBundle` enforces at the collector boundary (shared in study-generators.ts). */
81
+ export function assertD17MultifactPrivateSeed(seed) {
82
+ if (d17MultifactPublicSeed(seed))
83
+ refuse('public-seed');
84
+ }
85
+ const refuse = (reason) => { const error = new Error(`D17-MF refused (${reason})`); error.reason = reason; throw error; };
86
+ // ---------- shortcut audit ----------
87
+ const NAME = /\b[A-Z][a-z0-9]{4}\b/g;
88
+ const CLASSES = ['relation', 'contrastive', 'negation', 'unrelated', 'enabled', 'disabled'];
89
+ function lineClass(line, pool) {
90
+ const p = D17MF_POOLS[pool];
91
+ if (line.includes(' does not '))
92
+ return 2;
93
+ if (line.includes(` ${p.contrastive} `))
94
+ return 1;
95
+ if (line.includes(` ${p.unrelated} `))
96
+ return 3;
97
+ if (line.includes(` ${p.relation} `))
98
+ return 0;
99
+ if (line.endsWith(` is ${p.enabled}.`))
100
+ return 4;
101
+ if (line.endsWith(` is ${p.disabled}.`))
102
+ return 5;
103
+ return 6;
104
+ }
105
+ export function d17MultifactFeatures(payload, pool) {
106
+ const facts = payload.split('\nFacts:\n')[1].split('\nQuestion:')[0].split('\n').slice(0, -1);
107
+ const q = /can reach (\S+)\?$/.exec(payload)[1], s = /established that (\S+) can reach/.exec(payload)[1];
108
+ const classes = facts.map(line => lineClass(line, pool)), span = Math.max(1, facts.length - 1);
109
+ const words = payload.replace(NAME, 'ENTITY').split(/\s+/).filter(Boolean);
110
+ const mentions = (name) => facts.map((line, i) => line.split(/[ .]/).includes(name) ? i : -1).filter(i => i >= 0);
111
+ const subj = mentions(s), dest = mentions(q);
112
+ const scalar = { lines: facts.length, chars: payload.length, digits: (payload.match(/\d/g) ?? []).length, words: words.length,
113
+ entities: new Set(facts.join(' ').match(NAME) ?? []).size, subjectMentions: subj.length, destinationMentions: dest.length,
114
+ subjectFirstPos: subj.length ? subj[0] / span : -1, destinationFirstPos: dest.length ? dest[0] / span : -1,
115
+ subjectBeforeDestination: subj.length && dest.length ? Number(subj[0] < dest[0]) : -1,
116
+ firstLineClass: classes[0] ?? -1, lastLineClass: classes.at(-1) ?? -1 };
117
+ CLASSES.forEach((name, c) => {
118
+ const positions = classes.map((k, i) => k === c ? i / span : -1).filter(v => v >= 0);
119
+ scalar[name] = positions.length;
120
+ scalar[`meanPos_${name}`] = positions.length ? positions.reduce((a, b) => a + b, 0) / positions.length : -1;
121
+ scalar[`minPos_${name}`] = positions.length ? Math.min(...positions) : -1;
122
+ scalar[`maxPos_${name}`] = positions.length ? Math.max(...positions) : -1;
123
+ });
124
+ const tokens = {};
125
+ for (const w of words)
126
+ tokens[w] = (tokens[w] ?? 0) + 1;
127
+ return { scalar, tokens };
128
+ }
129
+ function balanced(rows) {
130
+ const pos = rows.filter(r => r.y), neg = rows.filter(r => !r.y);
131
+ if (!pos.length || !neg.length)
132
+ return 0.5;
133
+ return (pos.filter(r => r.predict).length / pos.length + neg.filter(r => !r.predict).length / neg.length) / 2;
134
+ }
135
+ function bestThreshold(train, keys) {
136
+ let best = { score: -1, rule: () => false };
137
+ for (const key of keys) {
138
+ const values = [...new Set(train.map(r => r.x[key] ?? 0))].sort((a, b) => a - b);
139
+ for (const t of values)
140
+ for (const above of [true, false]) {
141
+ const rule = x => above ? (x[key] ?? 0) >= t : (x[key] ?? 0) < t;
142
+ const score = balanced(train.map(r => ({ y: r.y, predict: rule(r.x) })));
143
+ if (score > best.score)
144
+ best = { score, rule };
145
+ }
146
+ }
147
+ return best.rule;
148
+ }
149
+ function stumpTree(train, keys) {
150
+ const root = bestThreshold(train, keys), left = train.filter(r => root(r.x)), right = train.filter(r => !root(r.x));
151
+ const l = left.length ? bestThreshold(left, keys) : () => false, r = right.length ? bestThreshold(right, keys) : () => false;
152
+ return x => root(x) ? l(x) : r(x);
153
+ }
154
+ /** Class-balanced L2 logistic regression on standardized features (fixed iterations and step: deterministic). */
155
+ function logistic(train, keys) {
156
+ const mean = keys.map(k => train.reduce((s, r) => s + (r.x[k] ?? 0), 0) / train.length);
157
+ const sd = keys.map((k, j) => Math.sqrt(train.reduce((s, r) => s + ((r.x[k] ?? 0) - mean[j]) ** 2, 0) / train.length) || 1);
158
+ const z = (x) => keys.map((k, j) => ((x[k] ?? 0) - mean[j]) / sd[j]);
159
+ const pos = train.filter(r => r.y).length, neg = train.length - pos, w = new Array(keys.length + 1).fill(0);
160
+ const data = train.map(r => ({ v: z(r.x), y: Number(r.y), weight: r.y ? 0.5 / Math.max(1, pos) : 0.5 / Math.max(1, neg) }));
161
+ for (let it = 0; it < 200; it++) {
162
+ const g = new Array(keys.length + 1).fill(0);
163
+ for (const d of data) {
164
+ const p = 1 / (1 + Math.exp(-(w[0] + d.v.reduce((s, v, j) => s + v * w[j + 1], 0))));
165
+ const e = (p - d.y) * d.weight;
166
+ g[0] += e;
167
+ d.v.forEach((v, j) => { g[j + 1] += e * v; });
168
+ }
169
+ for (let j = 0; j < w.length; j++)
170
+ w[j] -= 0.5 * (g[j] + (j ? 0.01 * w[j] : 0));
171
+ }
172
+ return x => w[0] + z(x).reduce((s, v, j) => s + v * w[j + 1], 0) > 0;
173
+ }
174
+ function crossValidated(rows, learn, folds) {
175
+ const ordered = [...rows].sort((a, b) => a.id < b.id ? -1 : 1), predictions = [];
176
+ for (let k = 0; k < folds; k++) {
177
+ const rule = learn(ordered.filter((_, i) => i % folds !== k));
178
+ for (const r of ordered.filter((_, i) => i % folds === k))
179
+ predictions.push({ y: r.y, predict: rule(r.x) });
180
+ }
181
+ return +balanced(predictions).toFixed(4);
182
+ }
183
+ /** D29-style shortcut audit, counts and line order included: no surface learner may predict gold above the limit. */
184
+ export function d17MultifactShortcutAudit(corpus, gold) {
185
+ const { folds, limit } = D17MF_PROTOCOL.shortcutAudit;
186
+ const groups = new Map();
187
+ for (const row of corpus.rows.filter(r => r.split === 'test')) {
188
+ const world = gold.worlds[row.id], key = `${world.layout}:${world.pool}:h${world.hops}`;
189
+ const f = d17MultifactFeatures(String(row.input.payload), world.pool);
190
+ groups.set(key, [...(groups.get(key) ?? []), { x: f.scalar, t: f.tokens, y: gold.labels[row.id] === 'yes', id: row.id }]);
191
+ }
192
+ const results = [...groups].sort(([a], [b]) => a < b ? -1 : 1).map(([group, rows]) => {
193
+ const scalarKeys = Object.keys(rows[0].x).sort(), tokenKeys = [...new Set(rows.flatMap(r => Object.keys(r.t)))].sort();
194
+ const tokenRows = rows.map(r => ({ x: r.t, y: r.y, id: r.id }));
195
+ const scores = { scalarThreshold: crossValidated(rows, train => bestThreshold(train, scalarKeys), folds),
196
+ tokenThreshold: crossValidated(tokenRows, train => bestThreshold(train, tokenKeys), folds),
197
+ depth2Tree: crossValidated(rows, train => stumpTree(train, scalarKeys), folds),
198
+ logistic: crossValidated(rows, train => logistic(train, scalarKeys), folds) };
199
+ const max = Math.max(...Object.values(scores));
200
+ return { group, n: rows.length, yes: rows.filter(r => r.y).length, scores, max, passes: group.startsWith('d17-faithful') ? null : max <= limit };
201
+ });
202
+ const equalized = results.filter(r => r.passes !== null);
203
+ const audit = { schemaVersion: 'decision-d17mf-shortcut-audit/v2', limit, folds, groups: results,
204
+ equalizedPasses: equalized.every(r => r.passes), equalizedMax: Math.max(...equalized.map(r => r.max)),
205
+ controlMax: Math.max(...results.filter(r => r.passes === null).map(r => r.max)) };
206
+ return audit;
207
+ }
208
+ // ---------- preparation ----------
209
+ function reviewTemplate(rows, gold) {
210
+ // 40 development items spread over outcomes, pools, hops and layouts in allocation order (absence included).
211
+ const picked = [], per = [8, 7, 7, 6, 6, 6];
212
+ D17MF_OUTCOMES.forEach((outcome, o) => {
213
+ const candidates = rows.filter(r => r.split === 'tuning' && gold.worlds[r.id].outcome === outcome);
214
+ const byKey = new Map();
215
+ for (const r of candidates)
216
+ byKey.set(r.slice, [...(byKey.get(r.slice) ?? []), r]);
217
+ const keys = [...byKey.keys()].sort();
218
+ for (let i = 0, taken = 0; taken < per[o]; i++) {
219
+ const item = byKey.get(keys[i % keys.length])[Math.floor(i / keys.length)];
220
+ if (item) {
221
+ picked.push(item);
222
+ taken++;
223
+ }
224
+ }
225
+ });
226
+ return { schemaVersion: 'decision-d17mf-review/v1', reviewer: null, preregistrationReview: null,
227
+ assessments: picked.map((row, i) => ({ assessmentId: `mf-assessment-${String(i + 1).padStart(2, '0')}`, rowId: row.id, cell: row.slice,
228
+ inputDigest: heldoutDigest(row.input), payload: String(row.input.payload),
229
+ reviewedAt: null, goldAuditLabel: null, goldAmbiguousOrIncorrect: null, rationale: null })),
230
+ instructions: 'Label each development payload yes/no from the facts and rule alone, flag ambiguity, give a rationale and the review time, then compare with gold and the text oracle. Any ambiguous or incorrect gold stops the probe.' };
231
+ }
232
+ /** Seeded Fisher-Yates over the test rows: dispatch interleaves cells instead of collecting them in blocks. */
233
+ function dispatchOrder(rows, seed) {
234
+ const development = rows.filter(r => r.split !== 'test'), test = rows.filter(r => r.split === 'test');
235
+ const key = `aiwg-holdout-2850-v1:D17MF:dispatch:${heldoutDigest(seed)}`;
236
+ let counter = 0;
237
+ const next = (bound) => {
238
+ const limit = Math.floor(0x100000000 / bound) * bound;
239
+ for (;;) {
240
+ const v = createHash('sha256').update(`${key}:${counter++}`).digest().readUInt32BE(0);
241
+ if (v < limit)
242
+ return v % bound;
243
+ }
244
+ };
245
+ for (let i = test.length - 1; i > 0; i--) {
246
+ const j = next(i + 1);
247
+ [test[i], test[j]] = [test[j], test[i]];
248
+ }
249
+ return [...development, ...test];
250
+ }
251
+ export function prepareD17MultifactStudy(seed, moduleDigest, sourceDigests) {
252
+ if (!/^[a-z0-9][a-z0-9-]{0,31}$/.test(seed) || !/^sha256:[a-f0-9]{64}$/.test(moduleDigest)
253
+ || !Object.keys(sourceDigests).length || Object.values(sourceDigests).some(v => !/^sha256:[a-f0-9]{64}$/.test(v)))
254
+ throw new Error('D17-MF source or seed pins');
255
+ const generated = [];
256
+ const gold = { schemaVersion: 'decision-d17mf-gold/v1', labels: {}, worlds: {} };
257
+ for (let index = 0; index < D17MF_ROWS; index++) {
258
+ const rowSeed = `${seed}:${index}:single`, { row, world, label } = d17MfGenerateCase(rowSeed);
259
+ if (d17MfTextOracle(String(row.input.payload)) !== label)
260
+ throw new Error('D17-MF oracle disagreement');
261
+ gold.labels[row.id] = label;
262
+ gold.worlds[row.id] = world;
263
+ generated.push({ ...row, provenance: { generatorId: D17_MULTIFACT_GENERATOR_ID, seed: rowSeed, outputDigest: heldoutDigest(row) } });
264
+ }
265
+ const rows = dispatchOrder(generated, seed);
266
+ const corpus = { schemaVersion: 'decision-heldout-corpus/v1', study: 'D17', syntheticOnly: true,
267
+ provenance: { kind: 'authored-synthetic', generatorDigest: studyHeldoutGeneratorDigest(D17_MULTIFACT_GENERATOR_ID), seed, goldDigest: heldoutDigest(gold) },
268
+ definitions: [structuredClone(D17MF_DEFINITION)], rows };
269
+ const audit = d17MultifactShortcutAudit(corpus, gold);
270
+ validateD17MultifactArtifact('audit', audit);
271
+ if (!audit.equalizedPasses)
272
+ throw new Error('D17-MF shortcut audit');
273
+ const analysis = { schemaVersion: 'decision-d17mf-analysis/v2', protocol: D17MF_PROTOCOL, sourceDigests, auditDigest: heldoutDigest(audit),
274
+ dispatchOrderDigest: heldoutDigest(rows.map(r => r.id)),
275
+ cells: D17MF_CELLS.map(c => ({ split: c.split, key: cellKey(c), rule: c.rule, count: c.count })) };
276
+ validateD17MultifactArtifact('analysis', analysis);
277
+ const preregistration = { schemaVersion: 'decision-heldout-preregistration/v1', study: 'D17',
278
+ frozenAt: '2026-10-02T00:00:00Z', corpusDigest: heldoutDigest(corpus), studyAnalysisDigest: heldoutDigest(analysis), scorerDigest: moduleDigest,
279
+ calibration: { scope: 'calibrated', allowedModes: ['artifact'] },
280
+ providerFailurePolicy: { maxRetries: 1, maximumSliceFailureBps: 500, retryOnlyTerminal: true }, perRequestTokenBound: 4000,
281
+ outputAndHiddenTokenAllowance: 256, providerOverheadTokens: 512, requestTimeoutMs: 60000, minDispatchIntervalMs: 1000, sessionLimitMs: 1800000 };
282
+ validateHeldoutInputs(corpus, preregistration);
283
+ const base = heldoutApprovalTemplate(corpus, preregistration);
284
+ const approvalTemplate = { ...base, budget: { ...D17MF_BUDGET }, calibration: { mode: 'artifact', calibrationArtifactDigest: null },
285
+ priceBound: { inputUsdPerMTok: 0.042, outputUsdPerMTok: 0, perRequestUsd: 0,
286
+ evidenceReferences: ['https://www.eesel.ai/blog/typesafe-jev-pricing', 'https://www.mindstudio.ai/blog/jev-pricing-cost-per-token', 'roctinam/aiwg#2613 comment 153093'],
287
+ approvalReference: null } };
288
+ const review = reviewTemplate(generated, gold);
289
+ validateD17MultifactArtifact('reviewTemplate', review);
290
+ return { corpus, preregistration, gold, analysis, audit, approvalTemplate, reviewTemplate: review };
291
+ }
292
+ /** Study budget: USD 5 cap (program-authorized for #2850, inside the shared USD 8 D17 cap); calls and tokens leave headroom at the 80% stop. */
293
+ export const D17MF_BUDGET = Object.freeze({ calls: 16000, tokens: 30000000, usd: 5 });
294
+ /** Worst-case plan from the actual projected request sizes and the approval price bound; fits against an allowance. */
295
+ export async function d17MultifactPlan(prepared, approval) {
296
+ const sizing = { ...approval, calibration: { mode: 'uncalibrated-diagnostic' } };
297
+ const attempts = 1 + prepared.preregistration.providerFailurePolicy.maxRetries;
298
+ let rows = 0, first = 0, tokens = 0, usd = 0, max = 0;
299
+ for (const row of prepared.corpus.rows) {
300
+ rows++;
301
+ for (const request of row.requests) {
302
+ const { estimatedTokens } = await heldoutRequestSize(prepared.corpus, prepared.preregistration, sizing, row, request);
303
+ first++;
304
+ max = Math.max(max, estimatedTokens);
305
+ tokens += attempts * heldoutReservationTokens(prepared.preregistration, estimatedTokens);
306
+ usd += attempts * heldoutReservationMicros(sizing, estimatedTokens);
307
+ }
308
+ }
309
+ return { rows, firstAttempts: first, maximumAttempts: first * attempts, maximumRequestEstimateTokens: max, reservedTokens: tokens, reservedUsdMicros: usd };
310
+ }
311
+ export function d17MultifactFit(plan, allowance) {
312
+ const headroom = { calls: allowance.calls - plan.maximumAttempts, tokens: allowance.tokens - plan.reservedTokens, usdMicros: allowance.usdMicros - plan.reservedUsdMicros };
313
+ return { allowance: { ...allowance }, headroom, fits: headroom.calls >= 0 && headroom.tokens >= 0 && headroom.usdMicros >= 0 };
314
+ }
315
+ export function d17MultifactFreshAllowance(approval) {
316
+ return heldoutCollectionAllowance(approval, { studyUsdMicros: Math.ceil(approval.priorStudySpendUsd * 1_000_000),
317
+ portfolioUsdMicros: Math.ceil(approval.priorPortfolioSpendUsd * 1_000_000), studyCalls: 0, studyReservedTokens: 0,
318
+ counterBlocked: false, attempts: [], journalDigests: [], runs: [] });
319
+ }
320
+ // ---------- development review ----------
321
+ /** 40 complete development assessments agreeing with gold and the text oracle; the approval must cite the digest. */
322
+ export function validateD17MultifactReview(prepared, review, trustedDigest, options = {}) {
323
+ try {
324
+ validateD17MultifactArtifact('review', review);
325
+ }
326
+ catch {
327
+ refuse('development-review');
328
+ }
329
+ const value = review;
330
+ const template = prepared.reviewTemplate;
331
+ if (typeof trustedDigest !== 'string' || heldoutDigest(value) !== trustedDigest || !value.reviewer?.trim() || !value.preregistrationReview?.trim()
332
+ || value.assessments.length !== template.assessments.length)
333
+ refuse('development-review');
334
+ value.assessments.forEach((item, i) => {
335
+ const expected = template.assessments[i], gold = prepared.gold.labels[item.rowId];
336
+ if (item.assessmentId !== expected.assessmentId || item.rowId !== expected.rowId || item.payload !== expected.payload
337
+ || item.inputDigest !== expected.inputDigest || item.cell !== expected.cell)
338
+ refuse('development-review');
339
+ if (item.goldAmbiguousOrIncorrect !== false)
340
+ refuse('development-gold-invalid');
341
+ if (!Number.isFinite(Date.parse(item.reviewedAt)) || !item.rationale?.trim() || item.goldAuditLabel !== gold
342
+ || d17MfTextOracle(item.payload) !== gold)
343
+ refuse('development-review-incomplete');
344
+ if (options.before !== undefined && !(Date.parse(item.reviewedAt) < Date.parse(options.before)))
345
+ refuse('development-review-time');
346
+ });
347
+ return value;
348
+ }
349
+ /**
350
+ * Blind-then-unblind material for the 40 development assessments, in the D17 development review material format: each item
351
+ * carries `blind.payload` (shown first) and `unblind` (generator gold, independent text-oracle label, cell, latent world).
352
+ * Development (`tuning`) rows only; it contains gold and must stay private (the CLI writes it mode 600).
353
+ */
354
+ export function d17MultifactDevReviewMaterial(prepared, options) {
355
+ const rows = new Map(prepared.corpus.rows.map(row => [row.id, row]));
356
+ const items = prepared.reviewTemplate.assessments.map(item => {
357
+ const row = rows.get(item.rowId), world = prepared.gold.worlds[item.rowId], label = prepared.gold.labels[item.rowId];
358
+ if (!row || row.split !== 'tuning' || !world || !label || heldoutDigest(row.input) !== item.inputDigest)
359
+ refuse('development-review-material');
360
+ return { assessmentId: item.assessmentId, rowId: item.rowId, cell: item.cell, inputDigest: item.inputDigest,
361
+ blind: { payload: item.payload },
362
+ unblind: { goldLabel: label, textOracleLabel: d17MfTextOracle(item.payload), variant: item.cell, cell: item.cell,
363
+ factors: { pool: world.pool, hops: world.hops, outcome: world.outcome, layout: world.layout, rule: world.rule },
364
+ latentWorld: { chain: [...world.chain], decoys: [...world.decoys], failureKind: world.failureKind, failurePosition: world.failurePosition,
365
+ targets: { ...world.targets }, day: world.day, quantity: world.quantity } } };
366
+ });
367
+ const material = { schemaVersion: 'd17mf-development-review-material/v1', study: 'D17-MF', scope: 'multi-fact probe development review (#2850)',
368
+ preparedDirectory: options.preparedDirectory, seedDigest: heldoutDigest(prepared.corpus.provenance.seed),
369
+ reviewTemplateDigest: heldoutDigest(prepared.reviewTemplate), corpusDigest: heldoutDigest(prepared.corpus), goldDigest: heldoutDigest(prepared.gold),
370
+ preregistrationDigest: heldoutDigest(prepared.preregistration),
371
+ splitOfItems: 'tuning (development) only; no test rows', itemCount: items.length,
372
+ protocol: ['Show only `blind.payload`; the reviewer records a label (yes/no), ambiguity flag, rationale and review time.',
373
+ 'Then unblind `unblind` (generator gold, independent text-oracle label, cell, factors, latent world) and compare.',
374
+ 'Any ambiguous or incorrect gold, or gold/oracle disagreement, stops the probe: revise the generator and use a new seed.',
375
+ 'Record results in reviewTemplate.json (goldAuditLabel, goldAmbiguousOrIncorrect, rationale, reviewedAt), add reviewer and preregistrationReview, and anchor its digest.'],
376
+ guide: { population: 'Closed fictional worlds: each payload states facts and, last, the reachability rule; nothing outside the payload counts.',
377
+ yes: 'The rule, applied to the stated facts alone, makes the queried destination reachable from the queried entity.',
378
+ no: 'The stated facts do not satisfy the rule: a required link is not stated in the link form (contrastive, negated, non-connective or absent), or an entity in between is not in the enabled state.',
379
+ authority: 'The latent world defines gold; the text oracle re-derives the label from the payload independently.',
380
+ invalidGold: 'Mark goldAmbiguousOrIncorrect=true when a careful reader could defend the other label from the payload, or gold contradicts the stated rule.',
381
+ timing: 'Review blind before unblinding; reviewedAt must precede the approval anchoring.',
382
+ independence: 'The reviewer did not author the generator change under review.' },
383
+ items };
384
+ validateD17MultifactArtifact('reviewMaterial', material);
385
+ return material;
386
+ }
387
+ // ---------- statistics ----------
388
+ export function newcombeDifference(k1, n1, k2, n2, levelBps = 9500) {
389
+ if (!n1 || !n2)
390
+ return null;
391
+ const p1 = k1 / n1, p2 = k2 / n2, [l1, u1] = wilsonScoreInterval({ events: k1, n: n1, levelBps }), [l2, u2] = wilsonScoreInterval({ events: k2, n: n2, levelBps });
392
+ const d = p1 - p2;
393
+ return { difference: +d.toFixed(4), lower: +(d - Math.sqrt((p1 - l1) ** 2 + (u2 - p2) ** 2)).toFixed(4), upper: +(d + Math.sqrt((u1 - p1) ** 2 + (p2 - l2) ** 2)).toFixed(4) };
394
+ }
395
+ /** Standard normal CDF (Abramowitz-Stegun 7.1.26 erf, |error| < 1.5e-7). */
396
+ function phi(x) {
397
+ const t = 1 / (1 + 0.3275911 * Math.abs(x) / Math.SQRT2);
398
+ const erf = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-(x * x) / 2);
399
+ return x >= 0 ? (1 + erf) / 2 : (1 - erf) / 2;
400
+ }
401
+ export function twoProportionP(k1, n1, k2, n2) {
402
+ if (!n1 || !n2)
403
+ return null;
404
+ const p = (k1 + k2) / (n1 + n2), se = Math.sqrt(p * (1 - p) * (1 / n1 + 1 / n2));
405
+ if (se === 0)
406
+ return 1;
407
+ return 2 * (1 - phi(Math.abs(k1 / n1 - k2 / n2) / se));
408
+ }
409
+ /** Holm step-down: adjusted p-values in the original order (monotone, capped at 1). */
410
+ export function holmAdjust(pValues) {
411
+ const present = pValues.map((p, i) => ({ p, i })).filter((x) => x.p !== null).sort((a, b) => a.p - b.p);
412
+ const out = pValues.map(() => null), m = present.length;
413
+ let running = 0;
414
+ present.forEach(({ p, i }, rank) => { running = Math.max(running, Math.min(1, (m - rank) * p)); out[i] = +running.toFixed(6); });
415
+ return out;
416
+ }
417
+ /** AUROC of scores between positives and negatives (ties count half) with the Hanley-McNeil 95% interval. */
418
+ export function auroc(positives, negatives) {
419
+ if (!positives.length || !negatives.length)
420
+ return null;
421
+ let wins = 0;
422
+ for (const a of positives)
423
+ for (const b of negatives)
424
+ wins += a > b ? 1 : a === b ? 0.5 : 0;
425
+ const A = wins / (positives.length * negatives.length), q1 = A / (2 - A), q2 = 2 * A * A / (1 + A);
426
+ const se = Math.sqrt(Math.max(0, (A * (1 - A) + (positives.length - 1) * (q1 - A * A) + (negatives.length - 1) * (q2 - A * A)) / (positives.length * negatives.length)));
427
+ return { auroc: +A.toFixed(4), lower: +Math.max(0, A - 1.96 * se).toFixed(4), upper: +Math.min(1, A + 1.96 * se).toFixed(4), positives: positives.length, negatives: negatives.length };
428
+ }
429
+ /**
430
+ * Upper bound on the end of collection across a run lineage, from digest-covered evidence only: each session's
431
+ * `collectionStartedAt` (bound into frozen.json, hence into the trusted evidence digest) plus the preregistered session
432
+ * limit, which the collector never dispatches past. `qualification.json` times are not used: they are not digest-covered.
433
+ */
434
+ export function d17MultifactCollectionEnd(frozen) {
435
+ if (!frozen.length)
436
+ refuse('collection-clock');
437
+ const ends = frozen.map(record => {
438
+ const started = typeof record.collectionStartedAt === 'string' ? Date.parse(record.collectionStartedAt) : NaN;
439
+ const limit = record.bundle.preregistration.sessionLimitMs;
440
+ if (!Number.isSafeInteger(started) || !Number.isSafeInteger(limit) || limit <= 0)
441
+ refuse('collection-clock');
442
+ return started + limit;
443
+ });
444
+ return new Date(Math.max(...ends)).toISOString();
445
+ }
446
+ export async function scoreD17Multifact(input, prepared, context) {
447
+ if (heldoutDigest(input.corpus) !== heldoutDigest(prepared.corpus) || heldoutDigest(input.preregistration) !== heldoutDigest(prepared.preregistration)
448
+ || heldoutDigest(input.gold) !== prepared.corpus.provenance.goldDigest || heldoutDigest(prepared.gold) !== prepared.corpus.provenance.goldDigest)
449
+ refuse('frozen-pins');
450
+ const approved = input.approvedCalibration;
451
+ if (approved?.mode !== 'artifact' || input.calibrated !== null || approved.calibrationArtifactDigest !== context?.trustedCalibrationSetDigest)
452
+ refuse('approved-calibration');
453
+ const ended = Date.parse(context.collectionEndedAt);
454
+ if (!Number.isSafeInteger(context.nowEpochMs) || !Number.isFinite(ended) || context.nowEpochMs < ended)
455
+ refuse('scoring-clock');
456
+ // The applied calibrator: the registered member artifact and its mapping, for this exact definition.
457
+ const { artifact, mapping } = context.member;
458
+ if (artifact.digest !== context.set.member.artifactDigest || artifact.id !== context.set.member.artifactId
459
+ || heldoutDigest(mapping) !== context.set.member.mappingDigest || artifact.identity.calibrator.parametersDigest !== context.set.member.mappingDigest
460
+ || artifact.identity.calibrator.id !== D17_CALIBRATION.calibrators.member.id || artifact.identity.definitionDigest !== heldoutDigest(prepared.corpus.definitions)
461
+ || artifact.identity.actualModel !== 'jev-1.13.0' || artifact.approval.state !== 'approved')
462
+ refuse('calibration-artifact');
463
+ // Qualified at collection end and at the scoring clock: an expired calibrator refuses either way.
464
+ qualifyD17Calibration(artifact, new Date(ended).toISOString());
465
+ const at = new Date(context.nowEpochMs).toISOString(), compatibility = qualifyD17Calibration(artifact, at);
466
+ const rowsById = new Map(prepared.corpus.rows.map(r => [r.id, r])), seen = new Set();
467
+ for (const attempt of input.attempts) {
468
+ validateHeldoutAttempt(attempt);
469
+ const key = `${attempt.rowId}/${attempt.requestId}/${attempt.ordinal}`;
470
+ if (!rowsById.has(attempt.rowId) || attempt.requestId !== 'champion' || seen.has(key) || attempt.study !== 'D17'
471
+ || attempt.corpusDigest !== input.preregistration.corpusDigest || attempt.preregistrationDigest !== heldoutDigest(input.preregistration))
472
+ refuse('attempt-membership');
473
+ seen.add(key);
474
+ }
475
+ const grouped = d17AttemptsByRow(input.attempts), gold = prepared.gold;
476
+ const results = prepared.corpus.rows.map(row => {
477
+ const world = gold.worlds[row.id], label = gold.labels[row.id], raw = d17NativeYes(grouped.get(row.id) ?? [], row.id, 'champion');
478
+ const c = raw === null ? null : applyD17Mapping(mapping, raw), answer = (p) => p === null ? null : p > 0.5 ? 'yes' : p < 0.5 ? 'no' : 'defer';
479
+ const last = world.failureKind === 'link' ? world.hops : world.hops - 1;
480
+ const position = world.failurePosition === null ? null : world.failurePosition === 0 ? 'first' : world.failurePosition === last ? 'last' : 'interior';
481
+ return { id: row.id, key: row.slice, split: row.split, pool: world.pool, hops: world.hops, outcome: world.outcome, layout: world.layout, gold: label, position,
482
+ observed: raw !== null, raw, rawCorrect: answer(raw) === label, rawSaidYes: answer(raw) === 'yes',
483
+ calCorrect: answer(c) === label, calConfidence: c === null ? 0.5 : Math.max(c, 1 - c), calYes: c };
484
+ });
485
+ const test = results.filter(r => r.split === 'test');
486
+ const wil = (k, n) => n ? wilsonScoreInterval({ events: k, n, levelBps: 9500 }).map(v => +v.toFixed(4)) : null;
487
+ const stats = (list) => {
488
+ const obs = list.filter(r => r.observed), n = obs.length, rawK = obs.filter(r => r.rawCorrect).length, calK = obs.filter(r => r.calCorrect).length;
489
+ const meanConfidence = n ? obs.reduce((s, r) => s + r.calConfidence, 0) / n : null;
490
+ const bins = new Map();
491
+ for (const r of obs) {
492
+ const b = Math.min(9, Math.floor((r.calYes ?? 0.5) * 10));
493
+ bins.set(b, [...(bins.get(b) ?? []), r]);
494
+ }
495
+ const ece = n ? [...bins.values()].reduce((s, b) => s + b.length / n * Math.abs(b.reduce((a, r) => a + (r.calYes ?? 0.5), 0) / b.length
496
+ - b.filter(r => r.gold === 'yes').length / b.length), 0) : null;
497
+ const calInterval = wil(calK, n);
498
+ return { n: list.length, observed: n, coverage: list.length ? +(n / list.length).toFixed(4) : null,
499
+ raw: { correct: rawK, accuracy: n ? +(rawK / n).toFixed(4) : null, wilson95: wil(rawK, n), saidYesRate: n ? +(obs.filter(r => r.rawSaidYes).length / n).toFixed(4) : null },
500
+ calibrated: { correct: calK, accuracy: n ? +(calK / n).toFixed(4) : null, wilson95: calInterval,
501
+ meanConfidence: meanConfidence === null ? null : +meanConfidence.toFixed(4), confidenceMinusAccuracy: n ? +(meanConfidence - calK / n).toFixed(4) : null,
502
+ ece: ece === null ? null : +ece.toFixed(4), calibrationUnreliable: n ? meanConfidence - calK / n > 0.1 && calInterval[1] < meanConfidence : null } };
503
+ };
504
+ const pick = (f) => test.filter(f);
505
+ const cells = Object.fromEntries([...new Set(test.map(r => r.key))].sort().map(key => [key, stats(pick(r => r.key === key))]));
506
+ const complete = Object.values(cells).every(c => (c.coverage ?? 0) >= D17MF_PROTOCOL.coverage.minimumObservedFraction);
507
+ const eq = (r) => r.layout === 'equalized';
508
+ const rawCounts = (rows) => { const obs = rows.filter(r => r.observed); return { k: obs.filter(r => r.rawCorrect).length, n: obs.length }; };
509
+ const diff = (a, b) => { const x = rawCounts(a), y = rawCounts(b); return newcombeDifference(x.k, x.n, y.k, y.n); };
510
+ const pValue = (a, b) => { const x = rawCounts(a), y = rawCounts(b); return twoProportionP(x.k, x.n, y.k, y.n); };
511
+ const written = ['missing-contrastive', 'missing-negation', 'missing-unrelated'];
512
+ const encodings = [...written, 'missing-absence'];
513
+ const hopsFor = (o) => o === 'missing-absence' ? [3] : [...D17MF_HOPS];
514
+ // H1: pool-pair contrasts, Holm across the whole family.
515
+ const h1Tests = encodings.flatMap(outcome => hopsFor(outcome).flatMap(hops => [['pool-a', 'pool-b'], ['pool-a', 'pool-c'], ['pool-b', 'pool-c']].map(([x, y]) => {
516
+ const a = pick(r => eq(r) && r.outcome === outcome && r.hops === hops && r.pool === x), b = pick(r => eq(r) && r.outcome === outcome && r.hops === hops && r.pool === y);
517
+ return { outcome, hops, pools: `${x} - ${y}`, interval: diff(a, b), p: pValue(a, b) };
518
+ })));
519
+ const h1Adjusted = holmAdjust(h1Tests.map(t => t.p));
520
+ const h1Rows = h1Tests.map((t, i) => ({ ...t, holmP: h1Adjusted[i] ?? null,
521
+ meets: Boolean(t.interval && (h1Adjusted[i] ?? 1) < 0.05 && Math.abs(t.interval.difference) >= 0.1) }));
522
+ const h1 = encodings.map(outcome => ({ outcome, supported: hopsFor(outcome).every(hops => h1Rows.some(t => t.outcome === outcome && t.hops === hops && t.meets)) }));
523
+ // H2: h3 - h1 restricted to first and last failure positions (yes rows: all).
524
+ const endpoint = (r) => r.position === null || r.position === 'first' || r.position === 'last';
525
+ const h2 = D17MF_OUTCOMES.filter(o => o !== 'missing-absence').map(outcome => {
526
+ const pools = D17MF_POOL_IDS.map(pool => ({ pool, interval: diff(pick(r => eq(r) && endpoint(r) && r.outcome === outcome && r.pool === pool && r.hops === 3), pick(r => eq(r) && endpoint(r) && r.outcome === outcome && r.pool === pool && r.hops === 1)) }));
527
+ return { outcome, matchedPositions: 'first and last', pools, supported: pools.filter(p => p.interval && p.interval.upper < -0.05).length >= 2 };
528
+ });
529
+ // H3: relay-off vs each missing encoding, pooled over hops.
530
+ const h3 = D17MF_POOL_IDS.map(pool => {
531
+ const relayOff = pick(r => eq(r) && r.pool === pool && r.outcome === 'relay-off');
532
+ const byEncoding = encodings.map(outcome => {
533
+ const rows = pick(r => eq(r) && r.pool === pool && r.outcome === outcome);
534
+ const interval = diff(relayOff, rows), s = stats(rows);
535
+ return { outcome, interval, rawSaidYesRate: s.raw.saidYesRate, meets: Boolean(interval && interval.lower > 0.1 && (s.raw.saidYesRate ?? 0) > 0.5) };
536
+ });
537
+ return { pool, byEncoding, meets: byEncoding.some(e => e.meets) };
538
+ });
539
+ // H3a: encoding specificity, and the pure-absence baseline against interior-position contrastive rows (three hops).
540
+ const h3a = D17MF_POOL_IDS.map(pool => {
541
+ const by = (o) => pick(r => eq(r) && r.pool === pool && r.outcome === o);
542
+ const vsNegation = diff(by('missing-contrastive'), by('missing-negation')), vsUnrelated = diff(by('missing-contrastive'), by('missing-unrelated'));
543
+ const negVsUnrelated = diff(by('missing-negation'), by('missing-unrelated'));
544
+ const absenceBaseline = diff(pick(r => eq(r) && r.pool === pool && r.hops === 3 && r.outcome === 'missing-contrastive' && r.position === 'interior'), pick(r => eq(r) && r.pool === pool && r.outcome === 'missing-absence'));
545
+ const relayOff = stats(by('relay-off')).raw.accuracy ?? 0, acc = written.map(o => stats(by(o)).raw.accuracy ?? 0);
546
+ const specific = [vsNegation, vsUnrelated].every(d => d && d.upper < 0 && Math.abs(d.difference) >= 0.15);
547
+ const general = [vsNegation, vsUnrelated, negVsUnrelated].every(d => d && Math.abs(d.difference) <= 0.1) && acc.every(a => a < relayOff);
548
+ return { pool, vsNegation, vsUnrelated, negVsUnrelated, contrastiveInteriorVsAbsence: absenceBaseline, specific, general };
549
+ });
550
+ const h3aVerdict = h3a.filter(p => p.specific).length >= 2 ? 'contrastive-specific' : h3a.every(p => p.general) ? 'general' : 'mixed';
551
+ // H4: layout at one hop on last-link failures (the faithful control always breaks the relay-to-destination link), Holm across pools.
552
+ const h4Tests = D17MF_POOL_IDS.map(pool => {
553
+ const a = pick(r => r.layout === 'd17-faithful' && r.pool === pool && r.outcome === 'missing-contrastive');
554
+ const b = pick(r => eq(r) && r.pool === pool && r.hops === 1 && r.outcome === 'missing-contrastive' && r.position === 'last');
555
+ const unmatched = diff(a, pick(r => eq(r) && r.pool === pool && r.hops === 1 && r.outcome === 'missing-contrastive'));
556
+ return { pool, matchedPosition: 'last link', interval: diff(a, b), unmatchedInterval: unmatched, p: pValue(a, b) };
557
+ });
558
+ const h4Adjusted = holmAdjust(h4Tests.map(t => t.p));
559
+ const h4 = h4Tests.map((t, i) => ({ ...t, holmP: h4Adjusted[i] ?? null, meets: Boolean(t.interval && (h4Adjusted[i] ?? 1) < 0.05) }));
560
+ const replication = stats(pick(r => r.layout === 'd17-faithful' && r.pool === 'pool-c' && r.outcome === 'missing-contrastive'));
561
+ // Threshold-free primary metric: AUROC of the raw P(yes), yes rows vs missing-link rows, per pool x hops.
562
+ const scores = (rows) => rows.filter(r => r.observed).map(r => r.raw);
563
+ const aurocs = D17MF_POOL_IDS.flatMap(pool => D17MF_HOPS.map(hops => {
564
+ const yes = scores(pick(r => eq(r) && r.pool === pool && r.hops === hops && r.outcome === 'yes'));
565
+ const missingRows = (o) => scores(pick(r => eq(r) && r.pool === pool && r.hops === hops && (o ? r.outcome === o : r.outcome.startsWith('missing'))));
566
+ return { pool, hops, pooled: auroc(yes, missingRows()), byEncoding: Object.fromEntries(encodings.filter(o => hopsFor(o).includes(hops)).map(o => [o, auroc(yes, missingRows(o))])) };
567
+ }));
568
+ const positions = Object.fromEntries([...new Set(test.filter(r => r.position !== null).map(r => `${r.key}:${r.position}`))].sort()
569
+ .map(k => { const [key, position] = k.split(':'); return [k, stats(pick(r => r.key === key && r.position === position))]; }));
570
+ const verdict = (value) => complete ? value : 'insufficient';
571
+ const report = { schemaVersion: 'decision-d17mf-report/v2', issue: 'roctinam/aiwg#2850', scope: 'synthetic-calibrated-probe',
572
+ calibrated: true, primaryMetric: 'raw native answer and raw-score AUROC (calibrated metrics: calibration-under-shift only)',
573
+ calibration: { mode: 'artifact', calibrationSetDigest: context.trustedCalibrationSetDigest, memberArtifactDigest: artifact.digest,
574
+ mappingDigest: context.set.member.mappingDigest, compatibility: { action: compatibility.action, state: compatibility.state },
575
+ qualifiedAt: at, collectionEndedAt: new Date(ended).toISOString(), expiresAt: D17MF_ARTIFACT_EXPIRY },
576
+ corpusDigest: heldoutDigest(prepared.corpus), preregistrationDigest: heldoutDigest(prepared.preregistration), analysisDigest: heldoutDigest(prepared.analysis),
577
+ integrityDigest: heldoutDigest(input.integrity), auditDigest: prepared.analysis.auditDigest,
578
+ coverage: { testRows: test.length, observed: test.filter(r => r.observed).length, developmentRows: results.length - test.length,
579
+ minimumObservedFraction: D17MF_PROTOCOL.coverage.minimumObservedFraction, complete },
580
+ overall: stats(test), cells, byFailurePosition: positions, auroc: aurocs,
581
+ hypotheses: {
582
+ H1: { verdict: verdict(h1.some(h => h.supported)), byEncoding: h1, tests: h1Rows },
583
+ H2: { verdict: verdict(h2.some(h => h.supported)), byOutcome: h2 },
584
+ H3: { verdict: verdict(h3.every(p => p.meets)), byPool: h3 },
585
+ H3a: { verdict: verdict(h3aVerdict), byPool: h3a },
586
+ H4: { verdict: verdict(h4.some(p => p.meets)), byPool: h4,
587
+ differences: ['rule wording (D17 sufficient one-relay rule vs exhaustive link-only rule)', 'line order (fixed vs uniformly shuffled)',
588
+ 'decoy lines (none vs label-independent decoys)', 'entity names (role-prefixed vs neutral)'] },
589
+ replication: { verdict: verdict(Boolean(replication.raw.wilson95 && replication.raw.wilson95[1] <= 0.5)), cell: replication },
590
+ calibrationUnderShift: { unreliableCells: Object.entries(cells).filter(([, c]) => c.calibrated.calibrationUnreliable).map(([k]) => k).sort() }
591
+ },
592
+ decision: 'HOLD', promotion: 'none: diagnostic probe' };
593
+ validateD17MultifactArtifact('report', report);
594
+ return report;
595
+ }
596
+ //# sourceMappingURL=multifact-study.js.map