segreant 0.0.0-stage → 0.2.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 (349) hide show
  1. package/LICENSE +116 -0
  2. package/LICENSING.md +62 -0
  3. package/README.md +315 -2
  4. package/baselines/lift-baselines.json +22 -0
  5. package/bin/publication-lock.mjs +748 -0
  6. package/bin/runtime-snapshot.mjs +266 -0
  7. package/bin/segreant-statusline.mjs +20 -0
  8. package/bin/segreant.mjs +96 -0
  9. package/dist/alerts/detect.js +245 -0
  10. package/dist/alerts/notify.js +66 -0
  11. package/dist/alloc/apply.js +0 -0
  12. package/dist/alloc/epistemic.js +154 -0
  13. package/dist/alloc/exact.js +714 -0
  14. package/dist/alloc/rules.js +0 -0
  15. package/dist/billing/countermodels.js +123 -0
  16. package/dist/billing/epistemic.js +636 -0
  17. package/dist/billing/importer.js +75 -0
  18. package/dist/billing/mapping.js +239 -0
  19. package/dist/billing/openaiCosts.js +352 -0
  20. package/dist/billing/openaiCostsCoverage.js +108 -0
  21. package/dist/billing/readiness.js +68 -0
  22. package/dist/billing/reconcile.js +414 -0
  23. package/dist/billing/scope.js +68 -0
  24. package/dist/billing/types.js +180 -0
  25. package/dist/budget/allocate.js +100 -0
  26. package/dist/budget/capDecision.js +457 -0
  27. package/dist/budget/controlLock.js +71 -0
  28. package/dist/budget/enforceability.js +39 -0
  29. package/dist/budget/guard.js +313 -0
  30. package/dist/budget/onlineControl.js +404 -0
  31. package/dist/budget/recommend.js +153 -0
  32. package/dist/capital.js +220 -0
  33. package/dist/causal/assignment.js +179 -0
  34. package/dist/causal/design.js +161 -0
  35. package/dist/causal/epistemic.js +428 -0
  36. package/dist/causal/estimand.js +64 -0
  37. package/dist/causal/estimate.js +237 -0
  38. package/dist/causal/family.js +61 -0
  39. package/dist/causal/identity.js +18 -0
  40. package/dist/causal/inference-ledger.js +400 -0
  41. package/dist/causal/ledger.js +201 -0
  42. package/dist/causal/measurement.js +150 -0
  43. package/dist/causal/ope.js +297 -0
  44. package/dist/causal/precision.js +206 -0
  45. package/dist/causal/producer.js +774 -0
  46. package/dist/causal/protocol.js +962 -0
  47. package/dist/causal/qualification.js +260 -0
  48. package/dist/causal/records.js +416 -0
  49. package/dist/causal/registry.js +58 -0
  50. package/dist/causal/sequential.js +679 -0
  51. package/dist/causal/transport.js +138 -0
  52. package/dist/causal/types.js +10 -0
  53. package/dist/cli/allocCmd.js +340 -0
  54. package/dist/cli/askModelPrices.js +59 -0
  55. package/dist/cli/backupCmd.js +100 -0
  56. package/dist/cli/basisLines.js +45 -0
  57. package/dist/cli/billingCmd.js +1007 -0
  58. package/dist/cli/capitalCmd.js +65 -0
  59. package/dist/cli/causalCmd.js +340 -0
  60. package/dist/cli/connectCmd.js +466 -0
  61. package/dist/cli/controlCmd.js +341 -0
  62. package/dist/cli/diagnosticsCmd.js +41 -0
  63. package/dist/cli/economicCmd.js +384 -0
  64. package/dist/cli/egressCmd.js +248 -0
  65. package/dist/cli/evidenceCmd.js +157 -0
  66. package/dist/cli/featuresCmd.js +53 -0
  67. package/dist/cli/flags.js +128 -0
  68. package/dist/cli/homeCmd.js +117 -0
  69. package/dist/cli/importCmd.js +657 -0
  70. package/dist/cli/launchCmd.js +90 -0
  71. package/dist/cli/marketCmd.js +178 -0
  72. package/dist/cli/opsCmd.js +335 -0
  73. package/dist/cli/outcomeCmd.js +120 -0
  74. package/dist/cli/packCmd.js +172 -0
  75. package/dist/cli/planCmd.js +181 -0
  76. package/dist/cli/pluginCmd.js +145 -0
  77. package/dist/cli/pricingDeclareCmd.js +178 -0
  78. package/dist/cli/quotaCmd.js +190 -0
  79. package/dist/cli/retention.js +28 -0
  80. package/dist/cli/runCmd.js +591 -0
  81. package/dist/cli/showCmd.js +500 -0
  82. package/dist/cli/teamCmd.js +760 -0
  83. package/dist/cli/ui.js +128 -0
  84. package/dist/cli/valueCmd.js +1001 -0
  85. package/dist/cli.js +628 -0
  86. package/dist/config.js +677 -0
  87. package/dist/connect/antigravity.js +310 -0
  88. package/dist/connect/claudeCode.js +218 -0
  89. package/dist/connect/codex.js +320 -0
  90. package/dist/connect/connectors.js +285 -0
  91. package/dist/connect/importShared.js +283 -0
  92. package/dist/connect/opencode.js +214 -0
  93. package/dist/connect/protobuf.js +126 -0
  94. package/dist/connect/registry.js +46 -0
  95. package/dist/connect/repoRoot.js +112 -0
  96. package/dist/cost/basis.js +168 -0
  97. package/dist/cost/coverage.js +53 -0
  98. package/dist/cost/declarations.js +37 -0
  99. package/dist/cost/exactPricing.js +132 -0
  100. package/dist/cost/firstSight.js +98 -0
  101. package/dist/cost/overrides.js +230 -0
  102. package/dist/cost/pricing.js +997 -0
  103. package/dist/cost/reprice.js +130 -0
  104. package/dist/dashboard/claim-support.js +271 -0
  105. package/dist/dashboard/contracts.js +211 -0
  106. package/dist/dashboard/routes.js +2004 -0
  107. package/dist/dashboard/server.js +76 -0
  108. package/dist/dashboard/sessions.js +345 -0
  109. package/dist/dashboard/settings.js +109 -0
  110. package/dist/dashboard/shared-types.js +6 -0
  111. package/dist/dashboard/static.js +111 -0
  112. package/dist/dashboard/value-cache.js +105 -0
  113. package/dist/dashboard/web/app/components/claimInspector.js +138 -0
  114. package/dist/dashboard/web/app/components/drawer.js +284 -0
  115. package/dist/dashboard/web/app/components/ruleFields.js +144 -0
  116. package/dist/dashboard/web/app/components/spine.js +163 -0
  117. package/dist/dashboard/web/app/components/workflow.js +82 -0
  118. package/dist/dashboard/web/app/core/actions.js +1021 -0
  119. package/dist/dashboard/web/app/core/api.js +164 -0
  120. package/dist/dashboard/web/app/core/chain.js +40 -0
  121. package/dist/dashboard/web/app/core/claimLayers.js +392 -0
  122. package/dist/dashboard/web/app/core/claimTypes.js +222 -0
  123. package/dist/dashboard/web/app/core/dom.js +138 -0
  124. package/dist/dashboard/web/app/core/fmt.js +159 -0
  125. package/dist/dashboard/web/app/core/focus.js +10 -0
  126. package/dist/dashboard/web/app/core/generated-contract.js +211 -0
  127. package/dist/dashboard/web/app/core/generated-payload-contract.js +4579 -0
  128. package/dist/dashboard/web/app/core/generated-types.js +8 -0
  129. package/dist/dashboard/web/app/core/griffin.js +10 -0
  130. package/dist/dashboard/web/app/core/jobs.js +12 -0
  131. package/dist/dashboard/web/app/core/period.js +35 -0
  132. package/dist/dashboard/web/app/core/periodWords.js +10 -0
  133. package/dist/dashboard/web/app/core/periods.js +32 -0
  134. package/dist/dashboard/web/app/core/pricingActions.js +264 -0
  135. package/dist/dashboard/web/app/core/privacyText.js +78 -0
  136. package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
  137. package/dist/dashboard/web/app/core/registry.js +337 -0
  138. package/dist/dashboard/web/app/core/signal.js +140 -0
  139. package/dist/dashboard/web/app/core/slug.js +9 -0
  140. package/dist/dashboard/web/app/core/theme.js +48 -0
  141. package/dist/dashboard/web/app/core/view.js +69 -0
  142. package/dist/dashboard/web/app/main.js +362 -0
  143. package/dist/dashboard/web/app/views/allocation.js +278 -0
  144. package/dist/dashboard/web/app/views/control.js +249 -0
  145. package/dist/dashboard/web/app/views/data.js +136 -0
  146. package/dist/dashboard/web/app/views/evidence.js +553 -0
  147. package/dist/dashboard/web/app/views/privacy.js +13 -0
  148. package/dist/dashboard/web/app/views/spend.js +143 -0
  149. package/dist/dashboard/web/app/views/statement.js +189 -0
  150. package/dist/dashboard/web/app/views/system.js +80 -0
  151. package/dist/dashboard/web/app/views/value.js +534 -0
  152. package/dist/dashboard/web/app/views/workbench.js +312 -0
  153. package/dist/dashboard/web/classic.html +2151 -0
  154. package/dist/dashboard/web/index.html +51 -0
  155. package/dist/dashboard/web/styles/app.css +1336 -0
  156. package/dist/decision/assurance.js +425 -0
  157. package/dist/decision/control.js +336 -0
  158. package/dist/decision/countermodels.js +118 -0
  159. package/dist/decision/engine.js +382 -0
  160. package/dist/decision/epistemic.js +546 -0
  161. package/dist/demo/seed.js +741 -0
  162. package/dist/diagnostics.js +236 -0
  163. package/dist/economics/attribution.js +201 -0
  164. package/dist/economics/close.js +133 -0
  165. package/dist/economics/corrections.js +120 -0
  166. package/dist/economics/epistemic.js +164 -0
  167. package/dist/economics/events.js +294 -0
  168. package/dist/economics/fx.js +245 -0
  169. package/dist/economics/ledger.js +1516 -0
  170. package/dist/economics/money.js +177 -0
  171. package/dist/economics/rate.js +331 -0
  172. package/dist/economics/request.js +89 -0
  173. package/dist/economics/serialization.js +99 -0
  174. package/dist/egress/policy.js +192 -0
  175. package/dist/egress/proxyHealth.js +36 -0
  176. package/dist/egress/receipts.js +871 -0
  177. package/dist/egress/ruleValidation.js +127 -0
  178. package/dist/egress/transport.js +395 -0
  179. package/dist/epistemic/abstract.js +432 -0
  180. package/dist/epistemic/admissibility.js +213 -0
  181. package/dist/epistemic/assumption.js +134 -0
  182. package/dist/epistemic/claim-uses.js +202 -0
  183. package/dist/epistemic/claim.js +264 -0
  184. package/dist/epistemic/countermodel.js +278 -0
  185. package/dist/epistemic/dag.js +421 -0
  186. package/dist/epistemic/derivation.js +391 -0
  187. package/dist/epistemic/evidence.js +254 -0
  188. package/dist/epistemic/grain.js +82 -0
  189. package/dist/epistemic/issuance-map.js +260 -0
  190. package/dist/epistemic/ledger.js +1489 -0
  191. package/dist/epistemic/obligation.js +149 -0
  192. package/dist/epistemic/preservation.js +178 -0
  193. package/dist/epistemic/profile.js +61 -0
  194. package/dist/epistemic/revocation.js +65 -0
  195. package/dist/epistemic/scope.js +66 -0
  196. package/dist/epistemic/serialization.js +171 -0
  197. package/dist/epistemic/state.js +46 -0
  198. package/dist/epistemic/time.js +37 -0
  199. package/dist/epistemic/ux.js +104 -0
  200. package/dist/epistemic/witness.js +176 -0
  201. package/dist/export/allocationCsv.js +70 -0
  202. package/dist/export/billingCsv.js +36 -0
  203. package/dist/export/csv.js +52 -0
  204. package/dist/export/economic.js +105 -0
  205. package/dist/export/focusBilling.js +66 -0
  206. package/dist/export/statementCsv.js +104 -0
  207. package/dist/git/completeness.js +78 -0
  208. package/dist/git/contribution.js +598 -0
  209. package/dist/git/correlate.js +286 -0
  210. package/dist/git/quality.js +467 -0
  211. package/dist/git/repoScope.js +207 -0
  212. package/dist/git/survivalStore.js +186 -0
  213. package/dist/githubActionsEvidence.js +234 -0
  214. package/dist/guide.js +159 -0
  215. package/dist/judge/call.js +161 -0
  216. package/dist/judge/orchestrate.js +127 -0
  217. package/dist/judge/payload.js +47 -0
  218. package/dist/judge/tier.js +107 -0
  219. package/dist/judge/transcript.js +324 -0
  220. package/dist/market/consensus.js +181 -0
  221. package/dist/market/market.js +323 -0
  222. package/dist/market/refresh.js +119 -0
  223. package/dist/market/sources.js +236 -0
  224. package/dist/market/zip.js +98 -0
  225. package/dist/measurement/completeness.js +125 -0
  226. package/dist/measurement/model.js +113 -0
  227. package/dist/measurement/registry.js +166 -0
  228. package/dist/measurement/surrogate.js +325 -0
  229. package/dist/outcomes/contract.js +107 -0
  230. package/dist/outcomes/registry.js +259 -0
  231. package/dist/outcomes/work-unit.js +42 -0
  232. package/dist/pack/canonical.js +63 -0
  233. package/dist/pack/export.js +120 -0
  234. package/dist/pack/index.js +5 -0
  235. package/dist/pack/manifest.js +77 -0
  236. package/dist/pack/signature.js +69 -0
  237. package/dist/pack/types.js +455 -0
  238. package/dist/pack/verifier.js +275 -0
  239. package/dist/plans/detect.js +147 -0
  240. package/dist/plugins/contract.js +497 -0
  241. package/dist/plugins/host.js +333 -0
  242. package/dist/plugins/index.js +4 -0
  243. package/dist/plugins/intake.js +137 -0
  244. package/dist/plugins/invocation.js +158 -0
  245. package/dist/plugins/isolation.js +203 -0
  246. package/dist/proxy/server.js +891 -0
  247. package/dist/proxy/stream-proposals.js +249 -0
  248. package/dist/proxy/usage.js +213 -0
  249. package/dist/quota/limits.js +138 -0
  250. package/dist/quota/statusline.js +68 -0
  251. package/dist/quota/view.js +122 -0
  252. package/dist/research/complexity/calibration.js +110 -0
  253. package/dist/research/complexity/lab.js +134 -0
  254. package/dist/research/complexity/profile.js +78 -0
  255. package/dist/research/economics/budgetRisk.js +100 -0
  256. package/dist/research/economics/capital.js +101 -0
  257. package/dist/research/economics/decisionLedger.js +103 -0
  258. package/dist/research/economics/decomposition.js +106 -0
  259. package/dist/research/economics/execution.js +42 -0
  260. package/dist/research/economics/frontier.js +113 -0
  261. package/dist/research/economics/metricSafety.js +74 -0
  262. package/dist/research/economics/offPolicy.js +120 -0
  263. package/dist/research/economics/promotion.js +85 -0
  264. package/dist/scan/knownApps.js +113 -0
  265. package/dist/scan/scan.js +220 -0
  266. package/dist/store/allocation.js +543 -0
  267. package/dist/store/backup.js +368 -0
  268. package/dist/store/billing.js +919 -0
  269. package/dist/store/causal.js +2547 -0
  270. package/dist/store/causalLineage.js +912 -0
  271. package/dist/store/causalProducer.js +462 -0
  272. package/dist/store/db.js +2726 -0
  273. package/dist/store/economicReadModel.js +270 -0
  274. package/dist/store/ope.js +67 -0
  275. package/dist/store/realization.js +379 -0
  276. package/dist/store/rows.js +52 -0
  277. package/dist/store/schema.js +2218 -0
  278. package/dist/store/scopeEvidence.js +67 -0
  279. package/dist/team/ledger-evidence.js +82 -0
  280. package/dist/team/rollup.js +496 -0
  281. package/dist/team/standardize.js +115 -0
  282. package/dist/util/json.js +24 -0
  283. package/dist/util/pool.js +19 -0
  284. package/dist/util/quiet.js +18 -0
  285. package/dist/util/resource-limits.js +113 -0
  286. package/dist/util/statements.js +61 -0
  287. package/dist/util/time.js +7 -0
  288. package/dist/util/transaction.js +68 -0
  289. package/dist/value/anytime.js +106 -0
  290. package/dist/value/characterization.js +77 -0
  291. package/dist/value/cohort.js +212 -0
  292. package/dist/value/drift.js +211 -0
  293. package/dist/value/epistemic.js +448 -0
  294. package/dist/value/frontier.js +513 -0
  295. package/dist/value/gates.js +276 -0
  296. package/dist/value/instrumentationSensitivity.js +76 -0
  297. package/dist/value/lenses.js +398 -0
  298. package/dist/value/lift.js +206 -0
  299. package/dist/value/liftBaseline.js +355 -0
  300. package/dist/value/liftEfficiency.js +68 -0
  301. package/dist/value/marginal.js +154 -0
  302. package/dist/value/proposals.js +263 -0
  303. package/dist/value/realization.js +1159 -0
  304. package/dist/value/receipt.js +222 -0
  305. package/dist/value/receiptReconciliation.js +117 -0
  306. package/dist/value/reliability.js +101 -0
  307. package/dist/value/report.js +338 -0
  308. package/dist/value/selfReported.js +224 -0
  309. package/dist/value/sourceDepth.js +25 -0
  310. package/dist/value/taskType.js +46 -0
  311. package/dist/value/timeReclaimed.js +90 -0
  312. package/dist/value/usage.js +281 -0
  313. package/dist/version.js +20 -0
  314. package/docs/ALLOCATION.md +263 -0
  315. package/docs/ARCHITECTURE.md +452 -0
  316. package/docs/BILLING-EVIDENCE-IMPORT.md +274 -0
  317. package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +220 -0
  318. package/docs/CAUSAL-EVIDENCE-PROTOCOL.md +295 -0
  319. package/docs/CAUSAL-PRODUCER-CONTRACT.md +128 -0
  320. package/docs/CLI-JSON-CONTRACT.md +38 -0
  321. package/docs/COMPATIBILITY.md +64 -0
  322. package/docs/DATA-BOUNDARIES.md +300 -0
  323. package/docs/EVIDENCE-PROVENANCE.md +40 -0
  324. package/docs/FAQ.md +275 -0
  325. package/docs/FOCUS-COMPATIBILITY.md +15 -0
  326. package/docs/GETTING-STARTED.md +236 -0
  327. package/docs/GUIDE.md +830 -0
  328. package/docs/INTEGRATIONS.md +233 -0
  329. package/docs/MARKET-SOURCES.md +142 -0
  330. package/docs/METHODOLOGY.md +166 -0
  331. package/docs/NEUTRALITY.md +95 -0
  332. package/docs/PRIVACY.md +51 -0
  333. package/docs/PROVIDER-RECONCILIATION.md +260 -0
  334. package/docs/README.md +52 -0
  335. package/docs/REAL-PROVIDER-RECONCILIATION-RUNBOOK.md +44 -0
  336. package/docs/RELEASE-GATE.md +682 -0
  337. package/docs/RELEASE-PROCESS.md +68 -0
  338. package/docs/RELIABILITY-PERFORMANCE.md +149 -0
  339. package/docs/RETURN-ON-INTELLIGENCE.md +808 -0
  340. package/docs/SUPPORT.md +37 -0
  341. package/docs/THE-STANDARD.md +235 -0
  342. package/docs/THREAT-MODEL.md +114 -0
  343. package/market/consensus-fits.json +1 -0
  344. package/market/snapshot.json +1 -0
  345. package/package.json +79 -4
  346. package/pricing/models.json +1400 -0
  347. package/standalone/segreantpack-verifier.d.mts +61 -0
  348. package/standalone/segreantpack-verifier.mjs +715 -0
  349. package/web/assets/brand/mark-256.png +0 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The actual outbound call to an OpenAI-compatible judge endpoint (local or
3
+ * hosted — same wire shape either way, only baseUrl/apiKey differ). This module
4
+ * is deliberately STRICT: it throws JudgeCallError on anything short of a
5
+ * well-formed judgment, and never guesses. All graceful-degradation behavior
6
+ * (falling back to the algorithmic signal on failure) lives one layer up in
7
+ * judge/orchestrate.ts — this file either returns a trustworthy SessionJudgment
8
+ * or throws, nothing in between.
9
+ *
10
+ * See docs/DATA-BOUNDARIES.md for the judge egress and credential boundary:
11
+ * the judge credential is dedicated, never the metered proxy key.
12
+ */
13
+ import { egressFetch, EgressError } from '../egress/transport.js';
14
+ import { readBoundedResponseText, ResourceLimitError, RESOURCE_LIMITS } from '../util/resource-limits.js';
15
+ export class JudgeCallError extends Error {
16
+ reason;
17
+ egressCode;
18
+ constructor(message, reason, egressCode) {
19
+ super(message);
20
+ this.name = 'JudgeCallError';
21
+ this.reason = reason;
22
+ this.egressCode = egressCode;
23
+ }
24
+ }
25
+ // Matches liftEfficiency.ts's algorithmic bound in spirit but wider, per the
26
+ // design doc's §3 sketch ("bounded to [0.5, 1.5]") — an LLM judge has more
27
+ // signal than the algorithmic pool, so its ceiling on how much it can move Lift
28
+ // is deliberately looser, while still being a REAL bound, never open-ended.
29
+ export const JUDGE_MULTIPLIER_FLOOR = 0.5;
30
+ export const JUDGE_MULTIPLIER_CAP = 1.5;
31
+ const CALL_TIMEOUT_MS = 30_000;
32
+ function judgePrompt(summary, transcript) {
33
+ const metrics = `Session metrics:\n${JSON.stringify({
34
+ requestCount: summary.requestCount,
35
+ proposalCount: summary.proposalCount,
36
+ proposalCaptureCoverage: summary.proposalCaptureCoverage ?? 'unknown',
37
+ interTurnGapsSec: summary.interTurnGapsSec,
38
+ requestSizeTrend: summary.requestSizeTrend,
39
+ spanMinutes: Math.round(summary.spanMinutes * 10) / 10,
40
+ }, null, 2)}`;
41
+ if (!transcript) {
42
+ return ('You are judging how EFFICIENTLY an AI coding session used its time — not whether the code was good. ' +
43
+ 'You will NOT see any prompt or code content, only structural session metrics. Reply with ONLY a JSON ' +
44
+ 'object of the exact shape {"efficiencyMultiplier": number, "rationale": string}. ' +
45
+ `efficiencyMultiplier must be between ${JUDGE_MULTIPLIER_FLOOR} and ${JUDGE_MULTIPLIER_CAP}, where 1.0 means ` +
46
+ 'no adjustment, above 1.0 means the session used AI-assisted time unusually well (few turns, tight timing, ' +
47
+ 'proposals converging rather than sprawling), and below 1.0 means it looks like it flailed (many turns, ' +
48
+ 'erratic or growing request sizes, few proposals relative to turns). rationale must be one short sentence.\n\n' +
49
+ metrics);
50
+ }
51
+ // Full-content variant: same task, same JSON contract, same bounds — the
52
+ // ONLY difference is a bounded transcript excerpt after the metrics, so a
53
+ // model prompted either way is judging the same question on more evidence.
54
+ const turnsBlock = transcript.turns.map((t) => `${t.role.toUpperCase()}: ${t.text}`).join('\n');
55
+ const clipNote = transcript.clippedTurns > 0 || transcript.droppedTurns > 0 || (transcript.truncatedLines ?? 0) > 0
56
+ ? `\n(Excerpt bounded: ${transcript.clippedTurns} turns clipped, ${transcript.droppedTurns} later turns dropped, ${transcript.truncatedLines ?? 0} oversized source lines skipped.)`
57
+ : '';
58
+ return ('You are judging how EFFICIENTLY an AI coding session used its time — not whether the code was good. ' +
59
+ 'You will see structural session metrics AND a bounded excerpt of the actual session transcript. Reply with ' +
60
+ 'ONLY a JSON object of the exact shape {"efficiencyMultiplier": number, "rationale": string}. ' +
61
+ `efficiencyMultiplier must be between ${JUDGE_MULTIPLIER_FLOOR} and ${JUDGE_MULTIPLIER_CAP}, where 1.0 means ` +
62
+ 'no adjustment, above 1.0 means the session used AI-assisted time unusually well (clear asks, converging work, ' +
63
+ 'little repetition), and below 1.0 means it flailed (re-asking the same thing, going in circles, long confused ' +
64
+ 'stretches). Judge efficiency of the collaboration, not code quality. rationale must be one short sentence.\n\n' +
65
+ `${metrics}\n\nTranscript excerpt (${transcript.turns.length} turns):\n${turnsBlock}${clipNote}`);
66
+ }
67
+ function clamp(v, lo, hi) {
68
+ return Math.min(hi, Math.max(lo, v));
69
+ }
70
+ /**
71
+ * Calls one OpenAI-compatible Chat Completions endpoint and returns a validated
72
+ * SessionJudgment, or throws JudgeCallError. `apiKey` is null for local calls —
73
+ * no Authorization header is sent in that case, matching how the reverse proxy's
74
+ * own local-server support (x-segreant-openai-base) never assumes a key either.
75
+ */
76
+ export async function callJudgeApi(baseUrl, model, apiKey, summary, confidence, timeoutMs = CALL_TIMEOUT_MS, transcript = null, purpose = 'local_judge') {
77
+ const controller = new AbortController();
78
+ const timeoutTimer = setTimeout(() => controller.abort(), timeoutMs);
79
+ let res;
80
+ try {
81
+ res = await egressFetch(baseUrl.replace(/\/$/, '') + '/chat/completions', {
82
+ purpose,
83
+ dataClass: transcript ? 'judge_transcript_excerpt' : 'judge_structural_summary',
84
+ method: 'POST',
85
+ headers: {
86
+ 'content-type': 'application/json',
87
+ ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
88
+ },
89
+ body: JSON.stringify({
90
+ model,
91
+ messages: [{ role: 'user', content: judgePrompt(summary, transcript) }],
92
+ response_format: { type: 'json_object' },
93
+ temperature: 0,
94
+ }),
95
+ signal: controller.signal,
96
+ });
97
+ }
98
+ catch (err) {
99
+ const timedOut = controller.signal.aborted;
100
+ if (err instanceof EgressError && err.code !== 'transport_failed') {
101
+ const repair = err.code === 'receipt_integrity_failed' || err.code === 'receipt_persistence_failed'
102
+ ? '; repair/restore the local receipt history before retrying'
103
+ : '';
104
+ throw new JudgeCallError(`Segreant egress boundary refused the judge request (${err.code}): ${err.message}${repair}`, 'egress-boundary', err.code);
105
+ }
106
+ throw new JudgeCallError(timedOut ? `judge call timed out after ${timeoutMs}ms` : `judge endpoint unreachable: ${String(err)}`, timedOut ? 'timeout' : 'network');
107
+ }
108
+ finally {
109
+ clearTimeout(timeoutTimer);
110
+ }
111
+ if (!res.ok) {
112
+ try {
113
+ await readBoundedResponseText(res, RESOURCE_LIMITS.judgeResponseBytes, 'judge_response_bytes');
114
+ }
115
+ catch {
116
+ // The status is still the primary failure classification; the body is
117
+ // drained only up to the same bounded capture policy.
118
+ }
119
+ throw new JudgeCallError(`judge endpoint returned HTTP ${res.status}`, 'http-status');
120
+ }
121
+ let responseText;
122
+ try {
123
+ responseText = await readBoundedResponseText(res, RESOURCE_LIMITS.judgeResponseBytes, 'judge_response_bytes');
124
+ }
125
+ catch (error) {
126
+ if (error instanceof ResourceLimitError) {
127
+ throw new JudgeCallError('judge endpoint response exceeded the bounded response capture limit', 'malformed-response');
128
+ }
129
+ throw new JudgeCallError('judge endpoint response was not valid JSON', 'malformed-response');
130
+ }
131
+ let envelope;
132
+ try {
133
+ envelope = JSON.parse(responseText);
134
+ }
135
+ catch {
136
+ throw new JudgeCallError('judge endpoint response was not valid JSON', 'malformed-response');
137
+ }
138
+ const content = envelope?.choices?.[0]?.message
139
+ ?.content;
140
+ if (typeof content !== 'string') {
141
+ throw new JudgeCallError('judge endpoint response had no choices[0].message.content string', 'malformed-response');
142
+ }
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(content);
146
+ }
147
+ catch {
148
+ throw new JudgeCallError('judge model reply was not valid JSON', 'malformed-response');
149
+ }
150
+ const rawMultiplier = parsed?.efficiencyMultiplier;
151
+ const rawRationale = parsed?.rationale;
152
+ if (typeof rawMultiplier !== 'number' || !Number.isFinite(rawMultiplier)) {
153
+ throw new JudgeCallError('judge model reply had a non-numeric or non-finite efficiencyMultiplier', 'malformed-response');
154
+ }
155
+ return {
156
+ sessionId: summary.sessionId,
157
+ efficiencyMultiplier: clamp(rawMultiplier, JUDGE_MULTIPLIER_FLOOR, JUDGE_MULTIPLIER_CAP),
158
+ confidence,
159
+ rationale: typeof rawRationale === 'string' && rawRationale.trim() ? rawRationale.trim().slice(0, 500) : '(judge gave no rationale)',
160
+ };
161
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * ISSUANCE CLASS: display_only — see `src/epistemic/issuance-map.ts`. A judge
3
+ * verdict is one model's opinion under a declared trust tier. It is never
4
+ * converted into a supported quality Claim, and no canonical issuance boundary
5
+ * consumes it.
6
+ *
7
+ * Ties the trust-ladder gate (tier.ts), the structural payload (payload.ts), and
8
+ * the outbound call (call.ts) into one entry point: `judgeSession`. This is the
9
+ * ONLY function anything else in the codebase should call to get a judgment —
10
+ * it is also the ONLY place that swallows a judge failure into a neutral
11
+ * result, and it does so visibly (via `rationale`), never silently.
12
+ *
13
+ * Safety property this file exists to guarantee: resolveJudgeTier is consulted
14
+ * BEFORE any payload is built or any network call is attempted. When it returns
15
+ * 'algorithmic', `judgeSession` returns immediately — no fetch, no summary
16
+ * construction, nothing. test/judge-orchestrate.test.ts asserts this directly by
17
+ * pointing an 'algorithmic'-tier config at an endpoint that would throw loudly
18
+ * if it were ever actually called.
19
+ */
20
+ import { resolveJudgeTier, hasHostedJudgeApiKey } from './tier.js';
21
+ import { buildStructuralSummary } from './payload.js';
22
+ import { callJudgeApi, JudgeCallError } from './call.js';
23
+ import { loadTranscriptExcerpt, transcriptSupport } from './transcript.js';
24
+ function isSet(s) {
25
+ return typeof s === 'string' && s.trim().length > 0;
26
+ }
27
+ function neutralJudgment(sessionId, rationale) {
28
+ return { sessionId, efficiencyMultiplier: 1, confidence: 'algorithmic', rationale };
29
+ }
30
+ /**
31
+ * Pure except for the outbound HTTP call itself (and reading
32
+ * SEGREANT_JUDGE_API_KEY, via hasHostedJudgeApiKey). Takes already-fetched rows so
33
+ * it stays fully testable with synthetic data — see judgeSessionFromStore below
34
+ * for the store-integrated convenience wrapper.
35
+ */
36
+ export async function judgeSession(sessionId, requests, proposals, cfg, transcript = null) {
37
+ const decision = resolveJudgeTier(cfg, hasHostedJudgeApiKey());
38
+ const notes = [...decision.notes];
39
+ if (decision.tier === 'algorithmic') {
40
+ return neutralJudgment(sessionId, notes.join(' ') || 'Judge tier: algorithmic (default).');
41
+ }
42
+ const isLocal = decision.tier === 'local-structural' || decision.tier === 'local-full';
43
+ const localEndpointIsOnDevice = isLocal && !decision.sendsContentOffDevice;
44
+ const baseUrl = isLocal ? cfg.localBaseUrl : cfg.hostedBaseUrl;
45
+ const model = isLocal ? cfg.localModel : cfg.hostedModel;
46
+ const apiKey = isLocal ? null : (process.env.SEGREANT_JUDGE_API_KEY ?? null);
47
+ if (!isSet(baseUrl) || !isSet(model)) {
48
+ const field = isLocal ? 'judge.localModel' : 'judge.hostedModel';
49
+ notes.push(`Judge tier ${decision.tier} is configured but missing a model name (${field}) — falling back to the algorithmic signal.`);
50
+ return neutralJudgment(sessionId, notes.join(' '));
51
+ }
52
+ // Full-content tiers send a real, bounded transcript excerpt when the caller
53
+ // supplied one (read ephemerally from the tool's own on-disk log — the store
54
+ // still never persists content). When no transcript exists for this session,
55
+ // downgrade the ACTUAL payload and the REPORTED confidence together — never
56
+ // claim a higher-fidelity source than what was truly sent. A transcript
57
+ // passed to a STRUCTURAL tier is deliberately ignored: the tier the user
58
+ // consented to caps what may be sent, never the other way around.
59
+ const wantsFullContent = decision.tier === 'local-full' || decision.tier === 'hosted-full';
60
+ const sendTranscript = wantsFullContent && transcript !== null && transcript.turns.length > 0 ? transcript : null;
61
+ const confidence = wantsFullContent
62
+ ? sendTranscript
63
+ ? decision.confidence // 'local-llm' or 'hosted-llm-full' — genuinely earned now
64
+ : isLocal
65
+ ? 'local-llm'
66
+ : 'hosted-llm-structural'
67
+ : decision.confidence;
68
+ if (wantsFullContent && sendTranscript) {
69
+ const bounds = sendTranscript.clippedTurns > 0 || sendTranscript.droppedTurns > 0
70
+ ? ` — bounded: ${sendTranscript.clippedTurns} turns clipped, ${sendTranscript.droppedTurns} dropped`
71
+ : '';
72
+ notes.push(`Transcript excerpt (${sendTranscript.turns.length} turns) read ephemerally from the tool's own log${bounds}; nothing was persisted.`);
73
+ }
74
+ else if (wantsFullContent) {
75
+ // decision.notes was seeded by tier.ts based on CONFIGURED intent (before
76
+ // this function knew the payload would be downgraded) and claims a
77
+ // fidelity — "full session content" — that never actually gets sent.
78
+ // Strip that claim rather than merely appending a correction after it, so
79
+ // the rationale never asserts something that didn't happen even
80
+ // momentarily within the same string.
81
+ for (let i = notes.length - 1; i >= 0; i--) {
82
+ if (/full session content/i.test(notes[i]))
83
+ notes.splice(i, 1);
84
+ }
85
+ notes.push(isLocal
86
+ ? localEndpointIsOnDevice
87
+ ? 'Judge tier: local LLM (full-content configured but no on-disk transcript found for this session — the structural-summary request targets the validated loopback endpoint; it stays within the configured loopback boundary).'
88
+ : 'Judge tier: local LLM (full-content configured but no on-disk transcript found for this session — the structural-summary request targets the configured endpoint; this destination is reported as remote/off-device).'
89
+ : 'Judge tier: hosted API (full-content configured but no on-disk transcript found for this session — only a structural summary left this machine, never raw content).');
90
+ }
91
+ const summary = buildStructuralSummary(requests, proposals, sessionId);
92
+ try {
93
+ const judgment = await callJudgeApi(baseUrl, model, apiKey, summary, confidence, undefined, sendTranscript, isLocal ? 'local_judge' : 'hosted_judge');
94
+ return notes.length ? { ...judgment, rationale: `${judgment.rationale} (${notes.join(' ')})` } : judgment;
95
+ }
96
+ catch (err) {
97
+ const reason = err instanceof JudgeCallError ? err.message : String(err);
98
+ notes.push(`Judge call failed (${reason}) — falling back to the algorithmic signal (multiplier 1, no adjustment).`);
99
+ return neutralJudgment(sessionId, notes.join(' '));
100
+ }
101
+ }
102
+ /**
103
+ * Store-integrated convenience wrapper, mirroring liftOptionsFromStore's
104
+ * relationship to liftFromData in src/value/. Project-scoped because the
105
+ * store's only range query over proposals (Store.proposalsInWindow) is.
106
+ *
107
+ * When (and only when) the resolved tier is a full-content one, this attempts
108
+ * the ephemeral transcript read: the session's recorded tool says whether an
109
+ * on-disk transcript can exist (claude-code names its files by session id),
110
+ * and the excerpt — if found — is passed to judgeSession, never persisted.
111
+ * The tier check happens BEFORE any file is touched, so a structural-tier or
112
+ * algorithmic-tier run never reads a transcript it would not be allowed to send.
113
+ */
114
+ export async function judgeSessionFromStore(store, project, sessionId, windowStartMs, windowEndMs, cfg, transcriptRoots) {
115
+ const requests = store.requestsInRange(windowStartMs, windowEndMs);
116
+ const proposals = store.proposalsInWindow(project, windowStartMs, windowEndMs);
117
+ let transcript = null;
118
+ const decision = resolveJudgeTier(cfg, hasHostedJudgeApiKey());
119
+ if (decision.tier === 'local-full' || decision.tier === 'hosted-full') {
120
+ const meta = store.getSessionMeta(sessionId);
121
+ const tool = meta?.tool ?? null;
122
+ if (transcriptSupport(tool) === 'supported') {
123
+ transcript = await loadTranscriptExcerpt(sessionId, tool, transcriptRoots ?? {});
124
+ }
125
+ }
126
+ return judgeSession(sessionId, requests, proposals, cfg, transcript);
127
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The structural payload sent to an LLM judge tier — content-free by construction.
3
+ * See docs/RETURN-ON-INTELLIGENCE.md for the three signal
4
+ * types this draws from (proposal counts, turn/timing, request-size trend).
5
+ *
6
+ * Pure by design (plain rows in, a plain summary out — no store, no fetch) so it
7
+ * composes with judge/tier.ts's gate and stays exhaustively unit-testable. The
8
+ * store-integrated convenience wrapper lives in judge/orchestrate.ts, mirroring
9
+ * how value/lift.ts stays pure and value/realization.ts bridges it to the store.
10
+ *
11
+ * IMPORTANT — what "structural" means here in practice: Segreant's store never
12
+ * persists prompt text or the AI's response text (src/store/db.ts's RequestRow
13
+ * has no content field; ProposalRow stores only the proposed file diffs for the
14
+ * Acceptance lens). So this summary is genuinely ALL the session-level signal
15
+ * the STORE holds. The full-content judge tiers get more only via
16
+ * judge/transcript.ts, which reads the tool's own on-disk session log
17
+ * ephemerally at judge time — still nothing persisted by Segreant.
18
+ */
19
+ /**
20
+ * Builds the summary for one session from already-fetched rows. Filters both
21
+ * arrays to `sessionId` internally, so callers can pass a whole Lift-window's
22
+ * worth of rows without pre-filtering.
23
+ */
24
+ export function buildStructuralSummary(requests, proposals, sessionId) {
25
+ const sessionRequests = requests.filter((r) => r.sessionId === sessionId).sort((a, b) => a.tsEpochMs - b.tsEpochMs);
26
+ const sessionProposals = proposals.filter((p) => p.sessionId === sessionId);
27
+ const proposalCount = sessionProposals.filter((proposal) => proposal.captureCoverage === undefined || proposal.captureCoverage === 'complete').length;
28
+ const proposalCaptureCoverage = sessionProposals.length === 0
29
+ ? 'unknown'
30
+ : sessionProposals.some((proposal) => proposal.captureCoverage === 'truncated')
31
+ ? 'truncated'
32
+ : sessionProposals.some((proposal) => proposal.captureCoverage === 'legacy_unknown')
33
+ ? 'legacy_unknown'
34
+ : sessionProposals.some((proposal) => proposal.captureCoverage === 'unknown')
35
+ ? 'unknown'
36
+ : 'complete';
37
+ const interTurnGapsSec = [];
38
+ for (let i = 1; i < sessionRequests.length; i++) {
39
+ interTurnGapsSec.push((sessionRequests[i].tsEpochMs - sessionRequests[i - 1].tsEpochMs) / 1000);
40
+ }
41
+ const requestSizeTrend = sessionRequests.map((r) => r.inputTokens + r.outputTokens);
42
+ const totalCostUsd = sessionRequests.reduce((s, r) => s + r.costUsd, 0);
43
+ const spanMinutes = sessionRequests.length > 1
44
+ ? (sessionRequests[sessionRequests.length - 1].tsEpochMs - sessionRequests[0].tsEpochMs) / 60_000
45
+ : 0;
46
+ return { sessionId, requestCount: sessionRequests.length, proposalCount, proposalCaptureCoverage, interTurnGapsSec, requestSizeTrend, totalCostUsd, spanMinutes };
47
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The Lift judge trust ladder — a single, pure gate deciding which judge tier is
3
+ * active. See docs/DATA-BOUNDARIES.md for the public egress contract.
4
+ *
5
+ * This is the one place that decision gets made. Everything above the always-on
6
+ * algorithmic default (src/value/liftEfficiency.ts) is off unless the user has
7
+ * taken an explicit action, and no two tiers can be conflated: local and hosted
8
+ * are gated independently, and each has its own separate "send full content"
9
+ * flag so opting one tier into full-content sending can never silently affect
10
+ * the other. `resolveJudgeTier` takes plain data in and returns plain data out —
11
+ * no network calls, no file reads, no env access — so the gating logic itself is
12
+ * exhaustively testable without mocking anything. The one impure fact it needs
13
+ * (whether SEGREANT_JUDGE_API_KEY is set) is read by the separate
14
+ * `hasHostedJudgeApiKey` below and passed in by the caller.
15
+ */
16
+ function isSet(s) {
17
+ return typeof s === 'string' && s.trim().length > 0;
18
+ }
19
+ /**
20
+ * A configured URL is treated as on-device only when its parsed host is literal
21
+ * loopback. A non-loopback or malformed URL remains an explicit local-tier
22
+ * selection, but its egress bit must warn that the endpoint is off-device.
23
+ */
24
+ function isValidatedLoopbackEndpoint(value) {
25
+ if (!isSet(value))
26
+ return false;
27
+ try {
28
+ const parsed = new URL(value.trim());
29
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
30
+ return false;
31
+ const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '');
32
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /**
39
+ * Reads SEGREANT_JUDGE_API_KEY — deliberately a DIFFERENT env var than any credential
40
+ * the reverse proxy forwards. Reusing the metered key would be circular (using the
41
+ * thing being measured to also measure itself) and would show judge calls up as
42
+ * confusing extra spend on the same ledger they're supposed to be judging
43
+ * (see docs/DATA-BOUNDARIES.md). Never logged, never persisted to
44
+ * config.json, never returned by this function — only whether it's set.
45
+ */
46
+ export function hasHostedJudgeApiKey(environment = process.env) {
47
+ return isSet(environment.SEGREANT_JUDGE_API_KEY);
48
+ }
49
+ /**
50
+ * The trust-ladder gate. Precedence when BOTH local and hosted are fully
51
+ * configured: local wins. It selects the configured local endpoint and does not
52
+ * select a hosted judge, so it is the strictly more conservative choice within
53
+ * the declared Segreant-process egress boundary; the loser is easy to switch (unset
54
+ * judge.localBaseUrl) — the alternative (silently preferring hosted) would mean
55
+ * a config that types out to "local is available" can still send content off
56
+ * the user's machine, which is exactly the silent-escalation shape
57
+ * the egress contract rules out.
58
+ */
59
+ export function resolveJudgeTier(cfg, hostedApiKeyPresent) {
60
+ const notes = [];
61
+ const localOn = isSet(cfg.localBaseUrl);
62
+ const localLoopback = isValidatedLoopbackEndpoint(cfg.localBaseUrl);
63
+ const hostedConsent = cfg.hostedEnabled && hostedApiKeyPresent;
64
+ const hostedOperational = hostedConsent && isSet(cfg.hostedBaseUrl);
65
+ if (cfg.hostedEnabled !== hostedApiKeyPresent) {
66
+ notes.push(cfg.hostedEnabled
67
+ ? 'Hosted judge tier: judge.hostedEnabled is true but SEGREANT_JUDGE_API_KEY is not set — hosted judging stays off until both are true.'
68
+ : 'Hosted judge tier: SEGREANT_JUDGE_API_KEY is set but judge.hostedEnabled is false — hosted judging stays off until both are true.');
69
+ }
70
+ if (localOn && hostedOperational) {
71
+ notes.push('Both local and hosted judge tiers are configured; using local endpoint (configured) and no hosted judge call. ' +
72
+ 'Unset judge.localBaseUrl to use the hosted tier instead.');
73
+ }
74
+ if (localOn) {
75
+ const full = cfg.localSendFullContent;
76
+ if (!localLoopback) {
77
+ notes.push('Judge tier: configured local endpoint is not a validated loopback URL; report this destination as remote/off-device.');
78
+ }
79
+ notes.push(full
80
+ ? 'Judge tier: local LLM endpoint, full session content; hosted tier is not selected.'
81
+ : 'Judge tier: local LLM endpoint, structural summary only; hosted tier is not selected.');
82
+ return {
83
+ tier: full ? 'local-full' : 'local-structural',
84
+ confidence: 'local-llm',
85
+ sendsContentOffDevice: !localLoopback,
86
+ notes,
87
+ };
88
+ }
89
+ if (hostedConsent && !isSet(cfg.hostedBaseUrl)) {
90
+ notes.push('Hosted judge tier: consented (hostedEnabled + SEGREANT_JUDGE_API_KEY) but judge.hostedBaseUrl is not ' +
91
+ 'set — falling back to the algorithmic signal.');
92
+ }
93
+ if (hostedOperational) {
94
+ const full = cfg.hostedSendFullContent;
95
+ notes.push(full
96
+ ? 'Judge tier: hosted API, FULL session content leaves this machine.'
97
+ : 'Judge tier: hosted API, a structural proposal-count/timing summary leaves this machine (never raw content).');
98
+ return {
99
+ tier: full ? 'hosted-full' : 'hosted-structural',
100
+ confidence: full ? 'hosted-llm-full' : 'hosted-llm-structural',
101
+ sendsContentOffDevice: true,
102
+ notes,
103
+ };
104
+ }
105
+ notes.push('Judge tier: algorithmic (default) — no opt-in judge tier is fully configured.');
106
+ return { tier: 'algorithmic', confidence: 'algorithmic', sendsContentOffDevice: false, notes };
107
+ }