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.
- package/LICENSE +116 -0
- package/LICENSING.md +62 -0
- package/README.md +315 -2
- package/baselines/lift-baselines.json +22 -0
- package/bin/publication-lock.mjs +748 -0
- package/bin/runtime-snapshot.mjs +266 -0
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +96 -0
- package/dist/alerts/detect.js +245 -0
- package/dist/alerts/notify.js +66 -0
- package/dist/alloc/apply.js +0 -0
- package/dist/alloc/epistemic.js +154 -0
- package/dist/alloc/exact.js +714 -0
- package/dist/alloc/rules.js +0 -0
- package/dist/billing/countermodels.js +123 -0
- package/dist/billing/epistemic.js +636 -0
- package/dist/billing/importer.js +75 -0
- package/dist/billing/mapping.js +239 -0
- package/dist/billing/openaiCosts.js +352 -0
- package/dist/billing/openaiCostsCoverage.js +108 -0
- package/dist/billing/readiness.js +68 -0
- package/dist/billing/reconcile.js +414 -0
- package/dist/billing/scope.js +68 -0
- package/dist/billing/types.js +180 -0
- package/dist/budget/allocate.js +100 -0
- package/dist/budget/capDecision.js +457 -0
- package/dist/budget/controlLock.js +71 -0
- package/dist/budget/enforceability.js +39 -0
- package/dist/budget/guard.js +313 -0
- package/dist/budget/onlineControl.js +404 -0
- package/dist/budget/recommend.js +153 -0
- package/dist/capital.js +220 -0
- package/dist/causal/assignment.js +179 -0
- package/dist/causal/design.js +161 -0
- package/dist/causal/epistemic.js +428 -0
- package/dist/causal/estimand.js +64 -0
- package/dist/causal/estimate.js +237 -0
- package/dist/causal/family.js +61 -0
- package/dist/causal/identity.js +18 -0
- package/dist/causal/inference-ledger.js +400 -0
- package/dist/causal/ledger.js +201 -0
- package/dist/causal/measurement.js +150 -0
- package/dist/causal/ope.js +297 -0
- package/dist/causal/precision.js +206 -0
- package/dist/causal/producer.js +774 -0
- package/dist/causal/protocol.js +962 -0
- package/dist/causal/qualification.js +260 -0
- package/dist/causal/records.js +416 -0
- package/dist/causal/registry.js +58 -0
- package/dist/causal/sequential.js +679 -0
- package/dist/causal/transport.js +138 -0
- package/dist/causal/types.js +10 -0
- package/dist/cli/allocCmd.js +340 -0
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +100 -0
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +1007 -0
- package/dist/cli/capitalCmd.js +65 -0
- package/dist/cli/causalCmd.js +340 -0
- package/dist/cli/connectCmd.js +466 -0
- package/dist/cli/controlCmd.js +341 -0
- package/dist/cli/diagnosticsCmd.js +41 -0
- package/dist/cli/economicCmd.js +384 -0
- package/dist/cli/egressCmd.js +248 -0
- package/dist/cli/evidenceCmd.js +157 -0
- package/dist/cli/featuresCmd.js +53 -0
- package/dist/cli/flags.js +128 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +657 -0
- package/dist/cli/launchCmd.js +90 -0
- package/dist/cli/marketCmd.js +178 -0
- package/dist/cli/opsCmd.js +335 -0
- package/dist/cli/outcomeCmd.js +120 -0
- package/dist/cli/packCmd.js +172 -0
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pluginCmd.js +145 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/retention.js +28 -0
- package/dist/cli/runCmd.js +591 -0
- package/dist/cli/showCmd.js +500 -0
- package/dist/cli/teamCmd.js +760 -0
- package/dist/cli/ui.js +128 -0
- package/dist/cli/valueCmd.js +1001 -0
- package/dist/cli.js +628 -0
- package/dist/config.js +677 -0
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +218 -0
- package/dist/connect/codex.js +320 -0
- package/dist/connect/connectors.js +285 -0
- package/dist/connect/importShared.js +283 -0
- package/dist/connect/opencode.js +214 -0
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/coverage.js +53 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +132 -0
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +997 -0
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +271 -0
- package/dist/dashboard/contracts.js +211 -0
- package/dist/dashboard/routes.js +2004 -0
- package/dist/dashboard/server.js +76 -0
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +109 -0
- package/dist/dashboard/shared-types.js +6 -0
- package/dist/dashboard/static.js +111 -0
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +138 -0
- package/dist/dashboard/web/app/components/drawer.js +284 -0
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +163 -0
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +1021 -0
- package/dist/dashboard/web/app/core/api.js +164 -0
- package/dist/dashboard/web/app/core/chain.js +40 -0
- package/dist/dashboard/web/app/core/claimLayers.js +392 -0
- package/dist/dashboard/web/app/core/claimTypes.js +222 -0
- package/dist/dashboard/web/app/core/dom.js +138 -0
- package/dist/dashboard/web/app/core/fmt.js +159 -0
- package/dist/dashboard/web/app/core/focus.js +10 -0
- package/dist/dashboard/web/app/core/generated-contract.js +211 -0
- package/dist/dashboard/web/app/core/generated-payload-contract.js +4579 -0
- package/dist/dashboard/web/app/core/generated-types.js +8 -0
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +337 -0
- package/dist/dashboard/web/app/core/signal.js +140 -0
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +362 -0
- package/dist/dashboard/web/app/views/allocation.js +278 -0
- package/dist/dashboard/web/app/views/control.js +249 -0
- package/dist/dashboard/web/app/views/data.js +136 -0
- package/dist/dashboard/web/app/views/evidence.js +553 -0
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +143 -0
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +80 -0
- package/dist/dashboard/web/app/views/value.js +534 -0
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +2151 -0
- package/dist/dashboard/web/index.html +51 -0
- package/dist/dashboard/web/styles/app.css +1336 -0
- package/dist/decision/assurance.js +425 -0
- package/dist/decision/control.js +336 -0
- package/dist/decision/countermodels.js +118 -0
- package/dist/decision/engine.js +382 -0
- package/dist/decision/epistemic.js +546 -0
- package/dist/demo/seed.js +741 -0
- package/dist/diagnostics.js +236 -0
- package/dist/economics/attribution.js +201 -0
- package/dist/economics/close.js +133 -0
- package/dist/economics/corrections.js +120 -0
- package/dist/economics/epistemic.js +164 -0
- package/dist/economics/events.js +294 -0
- package/dist/economics/fx.js +245 -0
- package/dist/economics/ledger.js +1516 -0
- package/dist/economics/money.js +177 -0
- package/dist/economics/rate.js +331 -0
- package/dist/economics/request.js +89 -0
- package/dist/economics/serialization.js +99 -0
- package/dist/egress/policy.js +192 -0
- package/dist/egress/proxyHealth.js +36 -0
- package/dist/egress/receipts.js +871 -0
- package/dist/egress/ruleValidation.js +127 -0
- package/dist/egress/transport.js +395 -0
- package/dist/epistemic/abstract.js +432 -0
- package/dist/epistemic/admissibility.js +213 -0
- package/dist/epistemic/assumption.js +134 -0
- package/dist/epistemic/claim-uses.js +202 -0
- package/dist/epistemic/claim.js +264 -0
- package/dist/epistemic/countermodel.js +278 -0
- package/dist/epistemic/dag.js +421 -0
- package/dist/epistemic/derivation.js +391 -0
- package/dist/epistemic/evidence.js +254 -0
- package/dist/epistemic/grain.js +82 -0
- package/dist/epistemic/issuance-map.js +260 -0
- package/dist/epistemic/ledger.js +1489 -0
- package/dist/epistemic/obligation.js +149 -0
- package/dist/epistemic/preservation.js +178 -0
- package/dist/epistemic/profile.js +61 -0
- package/dist/epistemic/revocation.js +65 -0
- package/dist/epistemic/scope.js +66 -0
- package/dist/epistemic/serialization.js +171 -0
- package/dist/epistemic/state.js +46 -0
- package/dist/epistemic/time.js +37 -0
- package/dist/epistemic/ux.js +104 -0
- package/dist/epistemic/witness.js +176 -0
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +36 -0
- package/dist/export/csv.js +52 -0
- package/dist/export/economic.js +105 -0
- package/dist/export/focusBilling.js +66 -0
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/completeness.js +78 -0
- package/dist/git/contribution.js +598 -0
- package/dist/git/correlate.js +286 -0
- package/dist/git/quality.js +467 -0
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/githubActionsEvidence.js +234 -0
- package/dist/guide.js +159 -0
- package/dist/judge/call.js +161 -0
- package/dist/judge/orchestrate.js +127 -0
- package/dist/judge/payload.js +47 -0
- package/dist/judge/tier.js +107 -0
- package/dist/judge/transcript.js +324 -0
- package/dist/market/consensus.js +181 -0
- package/dist/market/market.js +323 -0
- package/dist/market/refresh.js +119 -0
- package/dist/market/sources.js +236 -0
- package/dist/market/zip.js +98 -0
- package/dist/measurement/completeness.js +125 -0
- package/dist/measurement/model.js +113 -0
- package/dist/measurement/registry.js +166 -0
- package/dist/measurement/surrogate.js +325 -0
- package/dist/outcomes/contract.js +107 -0
- package/dist/outcomes/registry.js +259 -0
- package/dist/outcomes/work-unit.js +42 -0
- package/dist/pack/canonical.js +63 -0
- package/dist/pack/export.js +120 -0
- package/dist/pack/index.js +5 -0
- package/dist/pack/manifest.js +77 -0
- package/dist/pack/signature.js +69 -0
- package/dist/pack/types.js +455 -0
- package/dist/pack/verifier.js +275 -0
- package/dist/plans/detect.js +147 -0
- package/dist/plugins/contract.js +497 -0
- package/dist/plugins/host.js +333 -0
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/intake.js +137 -0
- package/dist/plugins/invocation.js +158 -0
- package/dist/plugins/isolation.js +203 -0
- package/dist/proxy/server.js +891 -0
- package/dist/proxy/stream-proposals.js +249 -0
- package/dist/proxy/usage.js +213 -0
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/research/complexity/calibration.js +110 -0
- package/dist/research/complexity/lab.js +134 -0
- package/dist/research/complexity/profile.js +78 -0
- package/dist/research/economics/budgetRisk.js +100 -0
- package/dist/research/economics/capital.js +101 -0
- package/dist/research/economics/decisionLedger.js +103 -0
- package/dist/research/economics/decomposition.js +106 -0
- package/dist/research/economics/execution.js +42 -0
- package/dist/research/economics/frontier.js +113 -0
- package/dist/research/economics/metricSafety.js +74 -0
- package/dist/research/economics/offPolicy.js +120 -0
- package/dist/research/economics/promotion.js +85 -0
- package/dist/scan/knownApps.js +113 -0
- package/dist/scan/scan.js +220 -0
- package/dist/store/allocation.js +543 -0
- package/dist/store/backup.js +368 -0
- package/dist/store/billing.js +919 -0
- package/dist/store/causal.js +2547 -0
- package/dist/store/causalLineage.js +912 -0
- package/dist/store/causalProducer.js +462 -0
- package/dist/store/db.js +2726 -0
- package/dist/store/economicReadModel.js +270 -0
- package/dist/store/ope.js +67 -0
- package/dist/store/realization.js +379 -0
- package/dist/store/rows.js +52 -0
- package/dist/store/schema.js +2218 -0
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/team/ledger-evidence.js +82 -0
- package/dist/team/rollup.js +496 -0
- package/dist/team/standardize.js +115 -0
- package/dist/util/json.js +24 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/quiet.js +18 -0
- package/dist/util/resource-limits.js +113 -0
- package/dist/util/statements.js +61 -0
- package/dist/util/time.js +7 -0
- package/dist/util/transaction.js +68 -0
- package/dist/value/anytime.js +106 -0
- package/dist/value/characterization.js +77 -0
- package/dist/value/cohort.js +212 -0
- package/dist/value/drift.js +211 -0
- package/dist/value/epistemic.js +448 -0
- package/dist/value/frontier.js +513 -0
- package/dist/value/gates.js +276 -0
- package/dist/value/instrumentationSensitivity.js +76 -0
- package/dist/value/lenses.js +398 -0
- package/dist/value/lift.js +206 -0
- package/dist/value/liftBaseline.js +355 -0
- package/dist/value/liftEfficiency.js +68 -0
- package/dist/value/marginal.js +154 -0
- package/dist/value/proposals.js +263 -0
- package/dist/value/realization.js +1159 -0
- package/dist/value/receipt.js +222 -0
- package/dist/value/receiptReconciliation.js +117 -0
- package/dist/value/reliability.js +101 -0
- package/dist/value/report.js +338 -0
- package/dist/value/selfReported.js +224 -0
- package/dist/value/sourceDepth.js +25 -0
- package/dist/value/taskType.js +46 -0
- package/dist/value/timeReclaimed.js +90 -0
- package/dist/value/usage.js +281 -0
- package/dist/version.js +20 -0
- package/docs/ALLOCATION.md +263 -0
- package/docs/ARCHITECTURE.md +452 -0
- package/docs/BILLING-EVIDENCE-IMPORT.md +274 -0
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +220 -0
- package/docs/CAUSAL-EVIDENCE-PROTOCOL.md +295 -0
- package/docs/CAUSAL-PRODUCER-CONTRACT.md +128 -0
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/COMPATIBILITY.md +64 -0
- package/docs/DATA-BOUNDARIES.md +300 -0
- package/docs/EVIDENCE-PROVENANCE.md +40 -0
- package/docs/FAQ.md +275 -0
- package/docs/FOCUS-COMPATIBILITY.md +15 -0
- package/docs/GETTING-STARTED.md +236 -0
- package/docs/GUIDE.md +830 -0
- package/docs/INTEGRATIONS.md +233 -0
- package/docs/MARKET-SOURCES.md +142 -0
- package/docs/METHODOLOGY.md +166 -0
- package/docs/NEUTRALITY.md +95 -0
- package/docs/PRIVACY.md +51 -0
- package/docs/PROVIDER-RECONCILIATION.md +260 -0
- package/docs/README.md +52 -0
- package/docs/REAL-PROVIDER-RECONCILIATION-RUNBOOK.md +44 -0
- package/docs/RELEASE-GATE.md +682 -0
- package/docs/RELEASE-PROCESS.md +68 -0
- package/docs/RELIABILITY-PERFORMANCE.md +149 -0
- package/docs/RETURN-ON-INTELLIGENCE.md +808 -0
- package/docs/SUPPORT.md +37 -0
- package/docs/THE-STANDARD.md +235 -0
- package/docs/THREAT-MODEL.md +114 -0
- package/market/consensus-fits.json +1 -0
- package/market/snapshot.json +1 -0
- package/package.json +79 -4
- package/pricing/models.json +1400 -0
- package/standalone/segreantpack-verifier.d.mts +61 -0
- package/standalone/segreantpack-verifier.mjs +715 -0
- 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
|
+
}
|