@bitkyc08/opencodex 2.12.0 → 2.13.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/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
2
|
+
import { PROVIDER_REGISTRY, type ProviderAuthKind } from "../../providers/registry";
|
|
3
|
+
import { localFingerprint } from "../../lab/digest";
|
|
4
|
+
import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
|
|
5
|
+
|
|
6
|
+
export function upstreamProtocolForAdapter(adapter: string): string {
|
|
7
|
+
switch (adapter) {
|
|
8
|
+
case "openai-responses":
|
|
9
|
+
return "openai-responses";
|
|
10
|
+
case "openai-chat":
|
|
11
|
+
case "command-code":
|
|
12
|
+
case "cursor":
|
|
13
|
+
case "azure":
|
|
14
|
+
case "azure-openai":
|
|
15
|
+
case "kiro":
|
|
16
|
+
case "mimo-free":
|
|
17
|
+
return "openai-chat";
|
|
18
|
+
case "anthropic":
|
|
19
|
+
return "anthropic-messages";
|
|
20
|
+
case "google":
|
|
21
|
+
return "google-generate";
|
|
22
|
+
default:
|
|
23
|
+
return "openai-responses";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function surfaceForProtocols(inboundProtocol: string, upstreamProtocol: string): string {
|
|
28
|
+
if (inboundProtocol === "anthropic-messages") return "anthropic-messages-http";
|
|
29
|
+
if (upstreamProtocol === "anthropic-messages") return "responses-http";
|
|
30
|
+
if (upstreamProtocol === "openai-chat") return "responses-sse";
|
|
31
|
+
return "responses-http";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function behaviorRow(source: LabBehaviorSource, value: unknown) {
|
|
35
|
+
return { source, value };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function includesModel(list: string[] | undefined, modelId: string): boolean {
|
|
39
|
+
return Array.isArray(list) && list.includes(modelId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function modelValue<T>(map: Record<string, T> | undefined, modelId: string): T | undefined {
|
|
43
|
+
return map?.[modelId];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CREDENTIAL_HEADER = /(authorization|api[-_]?key|token|secret|credential|cookie)/i;
|
|
47
|
+
|
|
48
|
+
function nonCredentialHeaderDigest(
|
|
49
|
+
headers: Record<string, string> | undefined,
|
|
50
|
+
installationSalt: Uint8Array | string,
|
|
51
|
+
): string {
|
|
52
|
+
const rows = Object.entries(headers ?? {})
|
|
53
|
+
.filter(([name]) => !CREDENTIAL_HEADER.test(name))
|
|
54
|
+
.map(([name, value]) => [name.toLowerCase().trim(), value] as const)
|
|
55
|
+
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
56
|
+
return localFingerprint("nonCredentialHeaders", rows, installationSalt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function authTransportFor(
|
|
60
|
+
effective: OcxProviderConfig,
|
|
61
|
+
adapter: string,
|
|
62
|
+
mode: ProviderAuthKind,
|
|
63
|
+
): string {
|
|
64
|
+
if (mode === "oauth") return "oauth_bearer";
|
|
65
|
+
if (mode === "forward") return "forwarded_authorization";
|
|
66
|
+
if (mode === "local") return "none";
|
|
67
|
+
if (adapter === "anthropic" && effective.apiKeyTransport !== "bearer") return "x_api_key";
|
|
68
|
+
return "authorization_bearer";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function effectiveOpenRouterRouting(effective: OcxProviderConfig, modelId: string) {
|
|
72
|
+
return effective.modelOpenRouterRouting?.[modelId] ?? effective.openRouterRouting;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Production behavior resolver for route-subject fingerprinting.
|
|
77
|
+
*
|
|
78
|
+
* The caller supplies the already-resolved effective provider config. This is
|
|
79
|
+
* deliberately the same effective config used for routeContext identity so a
|
|
80
|
+
* model wire override cannot disagree with the behavior fingerprint.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveProductionBehaviorValues(
|
|
83
|
+
config: OcxConfig,
|
|
84
|
+
providerName: string,
|
|
85
|
+
modelId: string,
|
|
86
|
+
effective: OcxProviderConfig,
|
|
87
|
+
installationSalt: Uint8Array | string,
|
|
88
|
+
): LabBehaviorValues | null {
|
|
89
|
+
const provider = config.providers[providerName];
|
|
90
|
+
if (!provider || provider.disabled === true) return null;
|
|
91
|
+
const adapter = effective.adapter ?? "openai-responses";
|
|
92
|
+
const upstreamProtocol = upstreamProtocolForAdapter(adapter);
|
|
93
|
+
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
94
|
+
const authMode = effective.authMode ?? registryEntry?.authKind ?? "key";
|
|
95
|
+
const reasoningEfforts = modelValue(effective.modelReasoningEfforts, modelId)
|
|
96
|
+
?? effective.reasoningEfforts
|
|
97
|
+
?? [];
|
|
98
|
+
const defaultReasoningEffort = modelValue(effective.modelDefaultReasoningEfforts, modelId) ?? null;
|
|
99
|
+
const reasoningEffortMap = modelValue(effective.modelReasoningEffortMap, modelId)
|
|
100
|
+
?? effective.reasoningEffortMap
|
|
101
|
+
?? {};
|
|
102
|
+
const openRouterRouting = effectiveOpenRouterRouting(effective, modelId);
|
|
103
|
+
const project = typeof effective.project === "string" && effective.project ? effective.project : null;
|
|
104
|
+
const location = typeof effective.location === "string" && effective.location ? effective.location : null;
|
|
105
|
+
const nativeLocalExec = effective.nativeLocalExec === "on" || effective.unsafeAllowNativeLocalExec === true;
|
|
106
|
+
|
|
107
|
+
const values: LabBehaviorValues = {
|
|
108
|
+
"wire.adapter": behaviorRow("provider_config", adapter),
|
|
109
|
+
"wire.upstreamProtocol": behaviorRow("provider_config", upstreamProtocol),
|
|
110
|
+
"wire.responsesPath": behaviorRow("provider_config", effective.responsesPath ?? null),
|
|
111
|
+
"wire.commandCodeVersion": behaviorRow("provider_config", effective.commandCodeVersion ?? null),
|
|
112
|
+
"wire.modelSuffixMode": behaviorRow(
|
|
113
|
+
"provider_config",
|
|
114
|
+
effective.modelSuffixBracketStrip === true ? "bracket_strip" : "none",
|
|
115
|
+
),
|
|
116
|
+
"auth.mode": behaviorRow("provider_config", authMode),
|
|
117
|
+
"auth.transport": behaviorRow("provider_config", authTransportFor(effective, adapter, authMode)),
|
|
118
|
+
"responses.stateful": behaviorRow("provider_config", effective.statelessResponses !== true),
|
|
119
|
+
"responses.serviceTier": behaviorRow("provider_config", effective.supportsServiceTier ?? null),
|
|
120
|
+
"responses.snapshotRepair": behaviorRow("provider_config", effective.responsesSnapshotRepair === true),
|
|
121
|
+
"responses.itemIdRepair": behaviorRow("provider_config", effective.responsesItemIdRepair ?? null),
|
|
122
|
+
"limits.contextWindow": behaviorRow(
|
|
123
|
+
"provider_config",
|
|
124
|
+
modelValue(effective.modelContextWindows, modelId) ?? effective.contextWindow ?? null,
|
|
125
|
+
),
|
|
126
|
+
"limits.maxInputTokens": behaviorRow(
|
|
127
|
+
"provider_config",
|
|
128
|
+
modelValue(effective.modelMaxInputTokens, modelId) ?? null,
|
|
129
|
+
),
|
|
130
|
+
"limits.maxOutputTokens": behaviorRow(
|
|
131
|
+
"provider_config",
|
|
132
|
+
modelValue(effective.modelMaxOutputTokens, modelId) ?? effective.defaultMaxOutputTokens ?? null,
|
|
133
|
+
),
|
|
134
|
+
"modalities.input": behaviorRow(
|
|
135
|
+
"provider_config",
|
|
136
|
+
modelValue(effective.modelInputModalities, modelId) ?? ["text"],
|
|
137
|
+
),
|
|
138
|
+
"sampling.omitTemperature": behaviorRow("provider_config", includesModel(effective.noTemperatureModels, modelId)),
|
|
139
|
+
"sampling.omitTopP": behaviorRow("provider_config", includesModel(effective.noTopPModels, modelId)),
|
|
140
|
+
"sampling.omitPenalties": behaviorRow("provider_config", includesModel(effective.noPenaltyModels, modelId)),
|
|
141
|
+
"reasoning.supported": behaviorRow(
|
|
142
|
+
"provider_config",
|
|
143
|
+
includesModel(effective.noReasoningModels, modelId)
|
|
144
|
+
? false
|
|
145
|
+
: reasoningEfforts.length > 0 || Boolean(registryEntry?.reasoningEfforts),
|
|
146
|
+
),
|
|
147
|
+
"reasoning.efforts": behaviorRow("provider_config", reasoningEfforts),
|
|
148
|
+
"reasoning.defaultEffort": behaviorRow("provider_config", defaultReasoningEffort),
|
|
149
|
+
"reasoning.effortMap": behaviorRow("provider_config", reasoningEffortMap),
|
|
150
|
+
"reasoning.wireFormat": behaviorRow("provider_config", effective.reasoningWireFormat ?? null),
|
|
151
|
+
"reasoning.summaryMode": behaviorRow("provider_config", {
|
|
152
|
+
supported: modelValue(effective.modelSupportsReasoningSummaries, modelId) ?? null,
|
|
153
|
+
delivery: modelValue(effective.modelReasoningSummaryDelivery, modelId) ?? null,
|
|
154
|
+
}),
|
|
155
|
+
"reasoning.replayMode": behaviorRow("provider_config", {
|
|
156
|
+
preserveResponses: effective.preserveResponsesReasoningContent === true,
|
|
157
|
+
preserveContent: includesModel(effective.preserveReasoningContentModels, modelId),
|
|
158
|
+
placeholder: includesModel(effective.requiresReasoningPlaceholderModels, modelId),
|
|
159
|
+
}),
|
|
160
|
+
"reasoning.splitMode": behaviorRow("provider_config", includesModel(effective.reasoningSplitModels, modelId)),
|
|
161
|
+
"reasoning.toggleMode": behaviorRow("provider_config", includesModel(effective.thinkingToggleModels, modelId)),
|
|
162
|
+
"reasoning.budgetMode": behaviorRow("provider_config", includesModel(effective.thinkingBudgetModels, modelId)),
|
|
163
|
+
"tools.choiceRestrictions": behaviorRow(
|
|
164
|
+
"provider_config",
|
|
165
|
+
includesModel(effective.autoToolChoiceOnlyModels, modelId) ? ["auto"] : [],
|
|
166
|
+
),
|
|
167
|
+
"tools.parallel": behaviorRow(
|
|
168
|
+
"provider_config",
|
|
169
|
+
effective.parallelToolCalls ?? (upstreamProtocol === "openai-chat"),
|
|
170
|
+
),
|
|
171
|
+
"tools.hostedPreference": behaviorRow("provider_config", {
|
|
172
|
+
tools: modelValue(effective.modelPreferHostedTools, modelId) ?? [],
|
|
173
|
+
}),
|
|
174
|
+
"tools.builtinNameEscaping": behaviorRow("provider_config", effective.escapeBuiltinToolNames === true),
|
|
175
|
+
"cache.forwarding": behaviorRow("provider_config", effective.promptCacheKey === true),
|
|
176
|
+
"cache.retention": behaviorRow("global_config", config.cacheRetention ?? "short"),
|
|
177
|
+
"anthropic.eofPolicy": behaviorRow("provider_config", effective.anthropicEofTolerance === true ? "tolerant" : "strict"),
|
|
178
|
+
"google.mode": behaviorRow("provider_config", effective.googleMode ?? null),
|
|
179
|
+
"google.projectFingerprint": behaviorRow(
|
|
180
|
+
"provider_config",
|
|
181
|
+
project ? localFingerprint("googleProject", project, installationSalt) : null,
|
|
182
|
+
),
|
|
183
|
+
"google.locationFingerprint": behaviorRow(
|
|
184
|
+
"provider_config",
|
|
185
|
+
location ? localFingerprint("googleLocation", location, installationSalt) : null,
|
|
186
|
+
),
|
|
187
|
+
"openrouter.order": behaviorRow("provider_config", openRouterRouting?.order ?? []),
|
|
188
|
+
"openrouter.only": behaviorRow("provider_config", openRouterRouting?.only ?? []),
|
|
189
|
+
"openrouter.allowFallbacks": behaviorRow("provider_config", openRouterRouting?.allowFallbacks ?? null),
|
|
190
|
+
"sidecars.vision": behaviorRow(
|
|
191
|
+
"global_config",
|
|
192
|
+
localFingerprint("visionSidecar", config.visionSidecar ?? null, installationSalt),
|
|
193
|
+
),
|
|
194
|
+
"sidecars.webSearch": behaviorRow(
|
|
195
|
+
"global_config",
|
|
196
|
+
localFingerprint("webSearchSidecar", config.webSearchSidecar ?? null, installationSalt),
|
|
197
|
+
),
|
|
198
|
+
"mcp.maxTools": behaviorRow("provider_config", effective.mcpMaxTools ?? null),
|
|
199
|
+
"mcp.maxSchemaBytes": behaviorRow("provider_config", effective.mcpMaxSchemaBytes ?? null),
|
|
200
|
+
"mcp.maxResultBytes": behaviorRow("provider_config", effective.mcpMaxResultBytes ?? null),
|
|
201
|
+
"mcp.nativeLocalExec": behaviorRow("provider_config", nativeLocalExec),
|
|
202
|
+
"runtime.bunVersion": behaviorRow("registry_runtime_default", Bun.version),
|
|
203
|
+
"runtime.platform": behaviorRow("registry_runtime_default", process.platform),
|
|
204
|
+
"runtime.arch": behaviorRow("registry_runtime_default", process.arch),
|
|
205
|
+
"runtime.streamMode": behaviorRow("global_config", config.streamMode ?? "auto"),
|
|
206
|
+
"runtime.fastMode": behaviorRow("global_config", config.fastMode === true),
|
|
207
|
+
"runtime.effortCap": behaviorRow("global_config", config.effortCap ?? null),
|
|
208
|
+
"headers.nonCredentialBehaviorDigest": behaviorRow(
|
|
209
|
+
"provider_config",
|
|
210
|
+
nonCredentialHeaderDigest(effective.headers, installationSalt),
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return values;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function providerInstanceKey(
|
|
218
|
+
providerName: string,
|
|
219
|
+
effective: OcxProviderConfig,
|
|
220
|
+
): string {
|
|
221
|
+
const baseUrl = typeof effective.baseUrl === "string" ? effective.baseUrl.trim() : "";
|
|
222
|
+
const adapter = typeof effective.adapter === "string" ? effective.adapter : "";
|
|
223
|
+
return `${providerName}\0${baseUrl}\0${adapter}`;
|
|
224
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { queryLabCatalog } from "../../lab/query/catalog";
|
|
2
|
+
import type { CatalogScenarioDto } from "../../lab/query/types";
|
|
3
|
+
import type {
|
|
4
|
+
CompatibilitySuiteRequirement,
|
|
5
|
+
RoutingCompatibilityEvidenceLayer,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export interface CompatibilityCatalogSuite {
|
|
9
|
+
suiteId: string;
|
|
10
|
+
evidenceLayer: RoutingCompatibilityEvidenceLayer;
|
|
11
|
+
suiteVersion: string;
|
|
12
|
+
suiteManifestDigest: string;
|
|
13
|
+
maxAgeMs: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CompatibilityCatalogSnapshot = Map<string, CompatibilityCatalogSuite>;
|
|
17
|
+
|
|
18
|
+
export function compatibilitySuiteKey(
|
|
19
|
+
evidenceLayer: RoutingCompatibilityEvidenceLayer,
|
|
20
|
+
suiteId: string,
|
|
21
|
+
): string {
|
|
22
|
+
return `${evidenceLayer}:${suiteId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function scenarioMaxAge(row: CatalogScenarioDto): number | null | undefined {
|
|
26
|
+
if (!row.freshness || typeof row.freshness !== "object") return undefined;
|
|
27
|
+
if (!Object.hasOwn(row.freshness, "maxAgeMs")) return undefined;
|
|
28
|
+
const value = row.freshness.maxAgeMs;
|
|
29
|
+
if (value === null) return null;
|
|
30
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve current suite identities/freshness once per evidence layer before a
|
|
36
|
+
* routing evaluation. The pure evaluator never performs manifest I/O.
|
|
37
|
+
*/
|
|
38
|
+
export function loadCompatibilityCatalogSnapshot(
|
|
39
|
+
requirements: readonly CompatibilitySuiteRequirement[],
|
|
40
|
+
): CompatibilityCatalogSnapshot {
|
|
41
|
+
const wanted = new Map<RoutingCompatibilityEvidenceLayer, Set<string>>();
|
|
42
|
+
for (const requirement of requirements) {
|
|
43
|
+
const suites = wanted.get(requirement.evidenceLayer) ?? new Set<string>();
|
|
44
|
+
suites.add(requirement.suiteId);
|
|
45
|
+
wanted.set(requirement.evidenceLayer, suites);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const out: CompatibilityCatalogSnapshot = new Map();
|
|
49
|
+
const invalid = new Set<string>();
|
|
50
|
+
|
|
51
|
+
for (const [layer, suiteIds] of wanted) {
|
|
52
|
+
let rows: CatalogScenarioDto[];
|
|
53
|
+
try {
|
|
54
|
+
rows = queryLabCatalog({ layer });
|
|
55
|
+
} catch {
|
|
56
|
+
// Catalog/manifest failure is fail-safe: affected requirements become
|
|
57
|
+
// missing evidence and follow the profile's unknown-evidence policy.
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
if (!suiteIds.has(row.suiteId)) continue;
|
|
63
|
+
const key = compatibilitySuiteKey(layer, row.suiteId);
|
|
64
|
+
if (invalid.has(key)) continue;
|
|
65
|
+
const maxAgeMs = scenarioMaxAge(row);
|
|
66
|
+
if (maxAgeMs === undefined) {
|
|
67
|
+
out.delete(key);
|
|
68
|
+
invalid.add(key);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const prior = out.get(key);
|
|
73
|
+
if (prior && (
|
|
74
|
+
prior.suiteVersion !== row.suiteVersion
|
|
75
|
+
|| prior.suiteManifestDigest !== row.suiteManifestDigest
|
|
76
|
+
)) {
|
|
77
|
+
// One current suite id must have one exact version/digest identity.
|
|
78
|
+
out.delete(key);
|
|
79
|
+
invalid.add(key);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const effectiveMaxAge = prior?.maxAgeMs == null
|
|
84
|
+
? maxAgeMs
|
|
85
|
+
: maxAgeMs == null
|
|
86
|
+
? prior.maxAgeMs
|
|
87
|
+
: Math.min(prior.maxAgeMs, maxAgeMs);
|
|
88
|
+
out.set(key, {
|
|
89
|
+
suiteId: row.suiteId,
|
|
90
|
+
evidenceLayer: layer,
|
|
91
|
+
suiteVersion: row.suiteVersion,
|
|
92
|
+
suiteManifestDigest: row.suiteManifestDigest,
|
|
93
|
+
maxAgeMs: effectiveMaxAge,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS-free endpoint fingerprint for routing-time route subject construction.
|
|
3
|
+
* Mirrors the snapshot fields hashed in `createLabDestination` without resolution.
|
|
4
|
+
*/
|
|
5
|
+
import { localFingerprint } from "../../lab/digest";
|
|
6
|
+
|
|
7
|
+
function normalizeBasePath(pathname: string): string {
|
|
8
|
+
if (!pathname || pathname === "/") return "";
|
|
9
|
+
const trimmed = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
10
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EndpointFingerprintParts {
|
|
14
|
+
scheme: "http" | "https";
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
basePath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Parse and normalize a provider base URL without network I/O. */
|
|
21
|
+
export function parseEndpointFingerprintParts(baseUrl: string): EndpointFingerprintParts | null {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(baseUrl);
|
|
24
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
25
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) return null;
|
|
26
|
+
const scheme = parsed.protocol === "https:" ? "https" as const : "http" as const;
|
|
27
|
+
const port = parsed.port ? Number(parsed.port) : scheme === "https" ? 443 : 80;
|
|
28
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
|
|
29
|
+
return {
|
|
30
|
+
scheme,
|
|
31
|
+
host: parsed.hostname.toLowerCase(),
|
|
32
|
+
port,
|
|
33
|
+
basePath: normalizeBasePath(parsed.pathname),
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function endpointFingerprintFromBaseUrl(
|
|
41
|
+
baseUrl: string,
|
|
42
|
+
installationSalt: Uint8Array | string,
|
|
43
|
+
): string | null {
|
|
44
|
+
const parts = parseEndpointFingerprintParts(baseUrl);
|
|
45
|
+
if (!parts) return null;
|
|
46
|
+
return localFingerprint("endpoint", {
|
|
47
|
+
scheme: parts.scheme,
|
|
48
|
+
host: parts.host,
|
|
49
|
+
port: parts.port,
|
|
50
|
+
basePath: parts.basePath,
|
|
51
|
+
}, installationSalt);
|
|
52
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { CompatibilityVerdict } from "../../lab/constants";
|
|
2
|
+
import type { NormalizedRoutingProfileCompatibility } from "../profile";
|
|
3
|
+
import type {
|
|
4
|
+
CandidateCompatibilityEvidence,
|
|
5
|
+
CompatibilityEvaluationOutcome,
|
|
6
|
+
} from "./types";
|
|
7
|
+
import { COMPATIBILITY_UNKNOWN_PENALTY_SCORE, MAX_TRACE_COMPATIBILITY_SUITES } from "./types";
|
|
8
|
+
import type { RouteCompatibilityEvidence } from "../trace";
|
|
9
|
+
|
|
10
|
+
function positiveThresholdMet(
|
|
11
|
+
verdict: CompatibilityVerdict,
|
|
12
|
+
minStatus: "PROBED" | "VERIFIED" | undefined,
|
|
13
|
+
): boolean {
|
|
14
|
+
if (!minStatus) return true;
|
|
15
|
+
if (minStatus === "PROBED") return verdict === "PROBED" || verdict === "VERIFIED";
|
|
16
|
+
return verdict === "VERIFIED";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function effectiveMaxAgeMs(
|
|
20
|
+
catalogMaxAgeMs: number | null,
|
|
21
|
+
profileMaxAgeMs: number | undefined,
|
|
22
|
+
): number | null {
|
|
23
|
+
if (catalogMaxAgeMs === null) return profileMaxAgeMs ?? null;
|
|
24
|
+
if (profileMaxAgeMs === undefined) return catalogMaxAgeMs;
|
|
25
|
+
return Math.min(catalogMaxAgeMs, profileMaxAgeMs);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mergePenalty(current: number | null, next: number | null): number | null {
|
|
29
|
+
if (current === null) return next;
|
|
30
|
+
if (next === null) return current;
|
|
31
|
+
return Math.min(current, next);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyUnknownPolicy(
|
|
35
|
+
policy: NormalizedRoutingProfileCompatibility["unknownEvidence"],
|
|
36
|
+
code: string,
|
|
37
|
+
detail: string,
|
|
38
|
+
): Pick<CompatibilityEvaluationOutcome, "exclusions" | "penaltyScore"> {
|
|
39
|
+
if (policy === "exclude") {
|
|
40
|
+
return { exclusions: [{ code, detail }], penaltyScore: null };
|
|
41
|
+
}
|
|
42
|
+
if (policy === "penalize") {
|
|
43
|
+
return { exclusions: [], penaltyScore: COMPATIBILITY_UNKNOWN_PENALTY_SCORE };
|
|
44
|
+
}
|
|
45
|
+
return { exclusions: [], penaltyScore: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function evaluateCompatibilityForCandidate(
|
|
49
|
+
policy: NormalizedRoutingProfileCompatibility | undefined,
|
|
50
|
+
evidence: CandidateCompatibilityEvidence | undefined,
|
|
51
|
+
now = Date.now(),
|
|
52
|
+
): CompatibilityEvaluationOutcome & { trace?: RouteCompatibilityEvidence } {
|
|
53
|
+
const outcome: CompatibilityEvaluationOutcome = {
|
|
54
|
+
exclusions: [],
|
|
55
|
+
penaltyScore: null,
|
|
56
|
+
suiteTraces: [],
|
|
57
|
+
traceTruncated: false,
|
|
58
|
+
};
|
|
59
|
+
if (!policy || policy.requiredSuites.length === 0) return outcome;
|
|
60
|
+
|
|
61
|
+
const pushTrace = (entry: CompatibilityEvaluationOutcome["suiteTraces"][number]): void => {
|
|
62
|
+
if (outcome.suiteTraces.length < MAX_TRACE_COMPATIBILITY_SUITES) {
|
|
63
|
+
outcome.suiteTraces.push(entry);
|
|
64
|
+
} else {
|
|
65
|
+
outcome.traceTruncated = true;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let penaltyScore: number | null = null;
|
|
70
|
+
for (const requirement of policy.requiredSuites) {
|
|
71
|
+
const subjectId = evidence?.subjectIds[requirement.evidenceLayer];
|
|
72
|
+
const row = evidence?.suites.find(suite =>
|
|
73
|
+
suite.subjectId === subjectId
|
|
74
|
+
&& suite.suiteId === requirement.suiteId
|
|
75
|
+
&& suite.evidenceLayer === requirement.evidenceLayer);
|
|
76
|
+
const maxAgeMs = row
|
|
77
|
+
? effectiveMaxAgeMs(row.maxAgeMs, policy.maxEvidenceAgeMs)
|
|
78
|
+
: policy.maxEvidenceAgeMs ?? null;
|
|
79
|
+
const ageMs = row ? now - row.asOf : Number.POSITIVE_INFINITY;
|
|
80
|
+
const fresh = row ? ageMs >= 0 && (maxAgeMs === null || ageMs <= maxAgeMs) : false;
|
|
81
|
+
const verdict = row?.verdict;
|
|
82
|
+
|
|
83
|
+
const trace = {
|
|
84
|
+
suiteId: requirement.suiteId,
|
|
85
|
+
evidenceLayer: requirement.evidenceLayer,
|
|
86
|
+
...(subjectId ? { subjectIdPrefix: subjectId.slice(0, 16) } : {}),
|
|
87
|
+
...(verdict ? { verdict } : {}),
|
|
88
|
+
...(policy.minStatus ? { minStatus: policy.minStatus } : {}),
|
|
89
|
+
fresh,
|
|
90
|
+
unknownPolicy: policy.unknownEvidence,
|
|
91
|
+
degradedPolicy: policy.degradedEvidence,
|
|
92
|
+
outcome: "unknown" as const,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!subjectId) {
|
|
96
|
+
const applied = applyUnknownPolicy(policy.unknownEvidence, "compatibility-unknown", requirement.suiteId);
|
|
97
|
+
outcome.exclusions.push(...applied.exclusions);
|
|
98
|
+
penaltyScore = mergePenalty(penaltyScore, applied.penaltyScore);
|
|
99
|
+
pushTrace({
|
|
100
|
+
...trace,
|
|
101
|
+
outcome: applied.exclusions.length ? "excluded" : applied.penaltyScore !== null ? "penalized" : "unknown",
|
|
102
|
+
reason: "subject-unresolved",
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!evidence?.projectionAvailable) {
|
|
108
|
+
const applied = applyUnknownPolicy(policy.unknownEvidence, "compatibility-unknown", requirement.suiteId);
|
|
109
|
+
outcome.exclusions.push(...applied.exclusions);
|
|
110
|
+
penaltyScore = mergePenalty(penaltyScore, applied.penaltyScore);
|
|
111
|
+
pushTrace({
|
|
112
|
+
...trace,
|
|
113
|
+
outcome: applied.exclusions.length ? "excluded" : applied.penaltyScore !== null ? "penalized" : "unknown",
|
|
114
|
+
reason: "projection-unavailable",
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!row || !fresh) {
|
|
120
|
+
const applied = applyUnknownPolicy(
|
|
121
|
+
policy.unknownEvidence,
|
|
122
|
+
row && !fresh ? "compatibility-stale" : "compatibility-unknown",
|
|
123
|
+
requirement.suiteId,
|
|
124
|
+
);
|
|
125
|
+
outcome.exclusions.push(...applied.exclusions);
|
|
126
|
+
penaltyScore = mergePenalty(penaltyScore, applied.penaltyScore);
|
|
127
|
+
pushTrace({
|
|
128
|
+
...trace,
|
|
129
|
+
outcome: applied.exclusions.length ? "excluded" : applied.penaltyScore !== null ? "penalized" : "unknown",
|
|
130
|
+
reason: !row ? "missing-evidence" : "stale-evidence",
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (verdict === "UNSUPPORTED") {
|
|
136
|
+
outcome.exclusions.push({ code: "compatibility-unsupported", detail: requirement.suiteId });
|
|
137
|
+
pushTrace({ ...trace, outcome: "excluded", reason: "unsupported" });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (verdict === "DEGRADED") {
|
|
142
|
+
if (policy.degradedEvidence === "exclude") {
|
|
143
|
+
outcome.exclusions.push({ code: "compatibility-degraded", detail: requirement.suiteId });
|
|
144
|
+
pushTrace({ ...trace, outcome: "excluded", reason: "degraded" });
|
|
145
|
+
} else if (policy.degradedEvidence === "penalize") {
|
|
146
|
+
penaltyScore = mergePenalty(penaltyScore, COMPATIBILITY_UNKNOWN_PENALTY_SCORE);
|
|
147
|
+
pushTrace({ ...trace, outcome: "penalized", reason: "degraded" });
|
|
148
|
+
} else {
|
|
149
|
+
pushTrace({ ...trace, outcome: "satisfied", reason: "degraded-allowed" });
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (verdict === "UNKNOWN" || verdict === "CLAIMED" || verdict === "BLOCKED") {
|
|
155
|
+
const applied = applyUnknownPolicy(policy.unknownEvidence, "compatibility-unknown", requirement.suiteId);
|
|
156
|
+
outcome.exclusions.push(...applied.exclusions);
|
|
157
|
+
penaltyScore = mergePenalty(penaltyScore, applied.penaltyScore);
|
|
158
|
+
pushTrace({
|
|
159
|
+
...trace,
|
|
160
|
+
outcome: applied.exclusions.length ? "excluded" : applied.penaltyScore !== null ? "penalized" : "unknown",
|
|
161
|
+
reason: verdict.toLowerCase(),
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!positiveThresholdMet(verdict!, policy.minStatus)) {
|
|
167
|
+
outcome.exclusions.push({ code: "compatibility-insufficient", detail: requirement.suiteId });
|
|
168
|
+
pushTrace({ ...trace, outcome: "excluded", reason: "below-min-status" });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pushTrace({ ...trace, outcome: "satisfied", reason: "threshold-met" });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
outcome.penaltyScore = penaltyScore;
|
|
176
|
+
const trace: RouteCompatibilityEvidence = {
|
|
177
|
+
suites: outcome.suiteTraces,
|
|
178
|
+
...(outcome.traceTruncated ? { truncated: true } : {}),
|
|
179
|
+
};
|
|
180
|
+
return { ...outcome, trace };
|
|
181
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CompatibilityVerdict, EvidenceLayer } from "../../lab/constants";
|
|
2
|
+
import { closeLabReadConnection, openLabReadConnection } from "../../lab/query/connection";
|
|
3
|
+
|
|
4
|
+
export interface VerdictRow {
|
|
5
|
+
subjectId: string;
|
|
6
|
+
evidenceLayer: EvidenceLayer;
|
|
7
|
+
suiteId: string;
|
|
8
|
+
suiteVersion: string;
|
|
9
|
+
suiteManifestDigest: string;
|
|
10
|
+
verdict: CompatibilityVerdict;
|
|
11
|
+
asOf: number;
|
|
12
|
+
notes: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CompatibilityEvidenceSnapshot {
|
|
16
|
+
projectionAvailable: boolean;
|
|
17
|
+
projectionIncompatible: boolean;
|
|
18
|
+
bySubject: Map<string, VerdictRow[]>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseNotes(raw: string): string[] {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
24
|
+
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : [];
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bounded read-only compatibility verdict snapshot for routing evaluation.
|
|
32
|
+
* One connection + one query for all subject IDs.
|
|
33
|
+
*/
|
|
34
|
+
export function loadCompatibilityEvidenceSnapshot(
|
|
35
|
+
subjectIds: readonly string[],
|
|
36
|
+
configDir?: string,
|
|
37
|
+
): CompatibilityEvidenceSnapshot {
|
|
38
|
+
const empty: CompatibilityEvidenceSnapshot = {
|
|
39
|
+
projectionAvailable: false,
|
|
40
|
+
projectionIncompatible: false,
|
|
41
|
+
bySubject: new Map(),
|
|
42
|
+
};
|
|
43
|
+
if (subjectIds.length === 0) return { ...empty, projectionAvailable: true };
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const conn = openLabReadConnection(configDir);
|
|
47
|
+
try {
|
|
48
|
+
const uniqueSubjectIds = [...new Set(subjectIds)];
|
|
49
|
+
const placeholders = uniqueSubjectIds.map(() => "?").join(", ");
|
|
50
|
+
const rows = conn.db.prepare(
|
|
51
|
+
`SELECT subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest, verdict, as_of, notes_json
|
|
52
|
+
FROM verdicts
|
|
53
|
+
WHERE subject_id IN (${placeholders})`,
|
|
54
|
+
).all(...uniqueSubjectIds) as Array<{
|
|
55
|
+
subject_id: string;
|
|
56
|
+
evidence_layer: string;
|
|
57
|
+
suite_id: string;
|
|
58
|
+
suite_version: string;
|
|
59
|
+
suite_manifest_digest: string;
|
|
60
|
+
verdict: string;
|
|
61
|
+
as_of: number;
|
|
62
|
+
notes_json: string;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
const bySubject = new Map<string, VerdictRow[]>();
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
const list = bySubject.get(row.subject_id) ?? [];
|
|
68
|
+
list.push({
|
|
69
|
+
subjectId: row.subject_id,
|
|
70
|
+
evidenceLayer: row.evidence_layer as EvidenceLayer,
|
|
71
|
+
suiteId: row.suite_id,
|
|
72
|
+
suiteVersion: row.suite_version,
|
|
73
|
+
suiteManifestDigest: row.suite_manifest_digest,
|
|
74
|
+
verdict: row.verdict as CompatibilityVerdict,
|
|
75
|
+
asOf: row.as_of,
|
|
76
|
+
notes: parseNotes(row.notes_json),
|
|
77
|
+
});
|
|
78
|
+
bySubject.set(row.subject_id, list);
|
|
79
|
+
}
|
|
80
|
+
return { projectionAvailable: true, projectionIncompatible: false, bySubject };
|
|
81
|
+
} finally {
|
|
82
|
+
closeLabReadConnection(conn);
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
86
|
+
const code = String((err as { code: unknown }).code);
|
|
87
|
+
if (code === "lab_projection_incompatible") {
|
|
88
|
+
return { projectionAvailable: false, projectionIncompatible: true, bySubject: new Map() };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return empty;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Match the current canonical projection identity, never a stale suite revision. */
|
|
96
|
+
export function findVerdictForSuite(
|
|
97
|
+
snapshot: CompatibilityEvidenceSnapshot,
|
|
98
|
+
subjectId: string,
|
|
99
|
+
evidenceLayer: EvidenceLayer,
|
|
100
|
+
suiteId: string,
|
|
101
|
+
suiteVersion: string,
|
|
102
|
+
suiteManifestDigest: string,
|
|
103
|
+
): VerdictRow | undefined {
|
|
104
|
+
const rows = snapshot.bySubject.get(subjectId) ?? [];
|
|
105
|
+
return rows.find(row =>
|
|
106
|
+
row.evidenceLayer === evidenceLayer
|
|
107
|
+
&& row.suiteId === suiteId
|
|
108
|
+
&& row.suiteVersion === suiteVersion
|
|
109
|
+
&& row.suiteManifestDigest === suiteManifestDigest);
|
|
110
|
+
}
|