@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.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. 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
+ }