@bitkyc08/opencodex 2.11.1 → 2.12.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/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type { ObservationEvent, ProtocolSubjectV1, RouteSubjectV1 } from "../events/types";
|
|
2
|
+
import { EVIDENCE_LAYERS, type ExecutionMode } from "../constants";
|
|
3
|
+
import type { SuiteManifestV1 } from "../conformance/suite-manifest";
|
|
4
|
+
import type { VerificationRole } from "../conformance/types";
|
|
5
|
+
import { isSha256Hex } from "../digest";
|
|
6
|
+
|
|
7
|
+
export interface VerificationEvaluation {
|
|
8
|
+
applicableRequiredScenarioIds: string[];
|
|
9
|
+
passingRequiredScenarioIds: string[];
|
|
10
|
+
missingRequiredScenarioIds: string[];
|
|
11
|
+
canVerify: boolean;
|
|
12
|
+
notes: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type LoadScenarioManifest = (digest: string) => Record<string, unknown> | null;
|
|
16
|
+
|
|
17
|
+
export interface ScenarioRequirements {
|
|
18
|
+
inboundProtocols?: string[];
|
|
19
|
+
upstreamProtocols?: string[];
|
|
20
|
+
surfaces?: string[];
|
|
21
|
+
requiredClaims?: string[];
|
|
22
|
+
freshness?: { maxAgeMs: number | null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type LoadScenarioRequirements = (digest: string) => ScenarioRequirements | null;
|
|
26
|
+
|
|
27
|
+
/** Live-reserved scenarios are inapplicable in fixture-mode protocol conformance. */
|
|
28
|
+
export function isScenarioApplicable(
|
|
29
|
+
scenarioId: string,
|
|
30
|
+
executionMode: ExecutionMode,
|
|
31
|
+
evidenceLayer: string,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (evidenceLayer === "protocol_conformance" && executionMode === "fixture") {
|
|
34
|
+
// The frozen CL-00 scenario schema has no explicit execution-mode field.
|
|
35
|
+
// Use an exact dot-delimited `live` segment rather than substring matching.
|
|
36
|
+
if (scenarioId.split(".").includes("live")) return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scenarioApplicableToRequirements(
|
|
42
|
+
requirements: ScenarioRequirements,
|
|
43
|
+
subject: ProtocolSubjectV1,
|
|
44
|
+
): boolean {
|
|
45
|
+
const inbound = requirements.inboundProtocols ?? [];
|
|
46
|
+
const upstream = requirements.upstreamProtocols ?? [];
|
|
47
|
+
const surfaces = requirements.surfaces ?? [];
|
|
48
|
+
return (
|
|
49
|
+
inbound.includes(subject.inboundProtocol) &&
|
|
50
|
+
upstream.includes(subject.upstreamProtocol) &&
|
|
51
|
+
surfaces.includes(subject.surface)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Applicability for live_route_compatibility using exact RouteSubjectV1 plus validated claim state. */
|
|
56
|
+
export function routeSubjectApplicableToRequirements(
|
|
57
|
+
requirements: ScenarioRequirements,
|
|
58
|
+
subject: RouteSubjectV1,
|
|
59
|
+
supportedClaims: readonly string[],
|
|
60
|
+
): boolean {
|
|
61
|
+
const inbound = requirements.inboundProtocols ?? [];
|
|
62
|
+
const upstream = requirements.upstreamProtocols ?? [];
|
|
63
|
+
const surfaces = requirements.surfaces ?? [];
|
|
64
|
+
const requiredClaims = requirements.requiredClaims ?? [];
|
|
65
|
+
const claimsOk = requiredClaims.every((claim) => supportedClaims.includes(claim));
|
|
66
|
+
return (
|
|
67
|
+
inbound.includes(subject.inboundProtocol) &&
|
|
68
|
+
upstream.includes(subject.upstreamProtocol) &&
|
|
69
|
+
surfaces.includes(subject.surface) &&
|
|
70
|
+
claimsOk
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
75
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseFreshness(value: unknown): { maxAgeMs: number | null } | null {
|
|
79
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
80
|
+
const maxAgeMs = (value as { maxAgeMs?: unknown }).maxAgeMs;
|
|
81
|
+
if (maxAgeMs === null) return { maxAgeMs: null };
|
|
82
|
+
if (!isNonNegativeInteger(maxAgeMs)) return null;
|
|
83
|
+
return { maxAgeMs };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseStringArray(value: unknown): string[] | null {
|
|
87
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) return null;
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scenarioContractFromManifest(
|
|
92
|
+
scenarioManifest: Record<string, unknown> | null,
|
|
93
|
+
): ScenarioRequirements | null {
|
|
94
|
+
if (!scenarioManifest) return null;
|
|
95
|
+
const req = scenarioManifest.requirements;
|
|
96
|
+
if (!req || typeof req !== "object" || Array.isArray(req)) return null;
|
|
97
|
+
const row = req as Record<string, unknown>;
|
|
98
|
+
const inboundProtocols = parseStringArray(row.inboundProtocols);
|
|
99
|
+
const upstreamProtocols = parseStringArray(row.upstreamProtocols);
|
|
100
|
+
const surfaces = parseStringArray(row.surfaces);
|
|
101
|
+
if (!inboundProtocols || !upstreamProtocols || !surfaces) return null;
|
|
102
|
+
const requiredClaims = row.requiredClaims === undefined ? [] : parseStringArray(row.requiredClaims);
|
|
103
|
+
if (!requiredClaims) return null;
|
|
104
|
+
const freshness = parseFreshness(scenarioManifest.freshness);
|
|
105
|
+
if (!freshness) return null;
|
|
106
|
+
return { inboundProtocols, upstreamProtocols, surfaces, requiredClaims, freshness };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function effectiveMaxAgeMs(
|
|
110
|
+
suiteMaxAgeMs: number | null,
|
|
111
|
+
scenarioMaxAgeMs: number | null,
|
|
112
|
+
): number | null {
|
|
113
|
+
if (suiteMaxAgeMs === null) return scenarioMaxAgeMs;
|
|
114
|
+
if (scenarioMaxAgeMs === null) return suiteMaxAgeMs;
|
|
115
|
+
return Math.min(suiteMaxAgeMs, scenarioMaxAgeMs);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function newestObservationByScenario(
|
|
119
|
+
observations: ObservationEvent[],
|
|
120
|
+
): Map<string, ObservationEvent> {
|
|
121
|
+
const byScenario = new Map<string, ObservationEvent>();
|
|
122
|
+
const ordered = [...observations].sort((a, b) => {
|
|
123
|
+
if (a.completedAt !== b.completedAt) return a.completedAt - b.completedAt;
|
|
124
|
+
return a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0;
|
|
125
|
+
});
|
|
126
|
+
for (const obs of ordered) {
|
|
127
|
+
byScenario.set(obs.scenarioId, obs);
|
|
128
|
+
}
|
|
129
|
+
return byScenario;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Evaluate `all-applicable-required-pass-v1` per frozen CL-00 semantics.
|
|
134
|
+
* Positive VERIFIED requires a non-empty applicable required/control set and a
|
|
135
|
+
* current, fresh pass for every applicable required scenario and negative control.
|
|
136
|
+
*/
|
|
137
|
+
export function evaluateAllApplicableRequiredPassV1(
|
|
138
|
+
suiteManifest: SuiteManifestV1,
|
|
139
|
+
observations: ObservationEvent[],
|
|
140
|
+
executionMode: ExecutionMode,
|
|
141
|
+
opts: {
|
|
142
|
+
subject?: ProtocolSubjectV1 | RouteSubjectV1;
|
|
143
|
+
/** For live projection this must come from validated current claim snapshots for subjectId. */
|
|
144
|
+
routeSupportedClaims?: readonly string[];
|
|
145
|
+
loadScenarioManifest?: LoadScenarioManifest;
|
|
146
|
+
loadScenarioRequirements?: LoadScenarioRequirements;
|
|
147
|
+
asOf?: number;
|
|
148
|
+
} = {},
|
|
149
|
+
): VerificationEvaluation {
|
|
150
|
+
const notes: string[] = [];
|
|
151
|
+
if (suiteManifest.verificationRule !== "all-applicable-required-pass-v1") {
|
|
152
|
+
return {
|
|
153
|
+
applicableRequiredScenarioIds: [],
|
|
154
|
+
passingRequiredScenarioIds: [],
|
|
155
|
+
missingRequiredScenarioIds: [],
|
|
156
|
+
canVerify: false,
|
|
157
|
+
notes: ["unsupported_verification_rule"],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (suiteManifest.evidenceLayer === "live_route_compatibility") {
|
|
161
|
+
if (opts.subject?.subjectKind !== "route") {
|
|
162
|
+
return { applicableRequiredScenarioIds: [], passingRequiredScenarioIds: [], missingRequiredScenarioIds: [], canVerify: false, notes: ["route_subject_required"] };
|
|
163
|
+
}
|
|
164
|
+
if (opts.routeSupportedClaims === undefined) {
|
|
165
|
+
return { applicableRequiredScenarioIds: [], passingRequiredScenarioIds: [], missingRequiredScenarioIds: [], canVerify: false, notes: ["route_claim_state_required"] };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const requiredScenarios = suiteManifest.scenarios.filter(
|
|
170
|
+
(s) => s.role === "required" || s.role === "negative_control",
|
|
171
|
+
);
|
|
172
|
+
const applicableRequired: string[] = [];
|
|
173
|
+
const unavailableManifests: string[] = [];
|
|
174
|
+
const scenarioMaxAgeById = new Map<string, number | null>();
|
|
175
|
+
|
|
176
|
+
for (const s of requiredScenarios) {
|
|
177
|
+
if (!isScenarioApplicable(s.id, executionMode, suiteManifest.evidenceLayer)) continue;
|
|
178
|
+
|
|
179
|
+
let requirements: ScenarioRequirements | null = null;
|
|
180
|
+
const scenarioManifest = opts.loadScenarioManifest?.(s.manifestDigest) ?? null;
|
|
181
|
+
if (scenarioManifest) {
|
|
182
|
+
requirements = scenarioContractFromManifest(scenarioManifest);
|
|
183
|
+
if (!requirements) {
|
|
184
|
+
unavailableManifests.push(s.id);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
requirements = opts.loadScenarioRequirements?.(s.manifestDigest) ?? null;
|
|
189
|
+
if (!requirements || !requirements.freshness) {
|
|
190
|
+
unavailableManifests.push(s.id);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (suiteManifest.evidenceLayer === "protocol_conformance" && opts.subject?.subjectKind === "protocol") {
|
|
196
|
+
if (!scenarioApplicableToRequirements(requirements, opts.subject)) continue;
|
|
197
|
+
}
|
|
198
|
+
if (suiteManifest.evidenceLayer === "live_route_compatibility" && opts.subject?.subjectKind === "route") {
|
|
199
|
+
if (!routeSubjectApplicableToRequirements(requirements, opts.subject, opts.routeSupportedClaims!)) continue;
|
|
200
|
+
}
|
|
201
|
+
applicableRequired.push(s.id);
|
|
202
|
+
scenarioMaxAgeById.set(s.id, requirements.freshness?.maxAgeMs ?? null);
|
|
203
|
+
}
|
|
204
|
+
applicableRequired.sort();
|
|
205
|
+
|
|
206
|
+
if (unavailableManifests.length > 0) {
|
|
207
|
+
return {
|
|
208
|
+
applicableRequiredScenarioIds: applicableRequired,
|
|
209
|
+
passingRequiredScenarioIds: [],
|
|
210
|
+
missingRequiredScenarioIds: [...new Set([...applicableRequired, ...unavailableManifests])].sort(),
|
|
211
|
+
canVerify: false,
|
|
212
|
+
notes: unavailableManifests.map((id) => `scenario_manifest_unavailable:${id}`),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (applicableRequired.length === 0) {
|
|
217
|
+
return {
|
|
218
|
+
applicableRequiredScenarioIds: [],
|
|
219
|
+
passingRequiredScenarioIds: [],
|
|
220
|
+
missingRequiredScenarioIds: [],
|
|
221
|
+
canVerify: false,
|
|
222
|
+
notes: ["empty_applicable_required_set"],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const newest = newestObservationByScenario(observations);
|
|
227
|
+
const passing: string[] = [];
|
|
228
|
+
const missing: string[] = [];
|
|
229
|
+
const asOf = opts.asOf ?? observations.reduce((max, obs) => Math.max(max, obs.completedAt), 0);
|
|
230
|
+
|
|
231
|
+
for (const scenarioId of applicableRequired) {
|
|
232
|
+
const scenarioRef = requiredScenarios.find((s) => s.id === scenarioId)!;
|
|
233
|
+
const obs = newest.get(scenarioId);
|
|
234
|
+
if (!obs) {
|
|
235
|
+
missing.push(scenarioId);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (obs.scenarioManifestDigest !== scenarioRef.manifestDigest) {
|
|
239
|
+
missing.push(scenarioId);
|
|
240
|
+
notes.push(`digest_mismatch:${scenarioId}`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const maxAgeMs = effectiveMaxAgeMs(
|
|
244
|
+
suiteManifest.freshness.maxAgeMs,
|
|
245
|
+
scenarioMaxAgeById.get(scenarioId) ?? null,
|
|
246
|
+
);
|
|
247
|
+
if (maxAgeMs !== null && asOf - obs.completedAt > maxAgeMs) {
|
|
248
|
+
missing.push(scenarioId);
|
|
249
|
+
notes.push(`stale_observation:${scenarioId}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (obs.outcome !== "pass") {
|
|
253
|
+
missing.push(scenarioId);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
passing.push(scenarioId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
applicableRequiredScenarioIds: applicableRequired,
|
|
261
|
+
passingRequiredScenarioIds: passing,
|
|
262
|
+
missingRequiredScenarioIds: missing,
|
|
263
|
+
canVerify: missing.length === 0 && passing.length === applicableRequired.length,
|
|
264
|
+
notes,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function requireNonEmptyString(value: unknown): string | null {
|
|
269
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function parseSuiteManifestFromArtifact(parsed: unknown): SuiteManifestV1 | null {
|
|
273
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
274
|
+
const raw = parsed as Record<string, unknown>;
|
|
275
|
+
if (raw.schemaVersion !== 1) return null;
|
|
276
|
+
|
|
277
|
+
const id = requireNonEmptyString(raw.id);
|
|
278
|
+
const version = requireNonEmptyString(raw.version);
|
|
279
|
+
const capability = requireNonEmptyString(raw.capability);
|
|
280
|
+
const assertionDslVersion = requireNonEmptyString(raw.assertionDslVersion);
|
|
281
|
+
const evidenceSchemaVersion = requireNonEmptyString(raw.evidenceSchemaVersion);
|
|
282
|
+
const contradictionRule = requireNonEmptyString(raw.contradictionRule);
|
|
283
|
+
const verificationRule = requireNonEmptyString(raw.verificationRule);
|
|
284
|
+
if (
|
|
285
|
+
!id || !version || !capability || !assertionDslVersion ||
|
|
286
|
+
!evidenceSchemaVersion || !contradictionRule ||
|
|
287
|
+
verificationRule !== "all-applicable-required-pass-v1"
|
|
288
|
+
) return null;
|
|
289
|
+
if (
|
|
290
|
+
typeof raw.evidenceLayer !== "string" ||
|
|
291
|
+
!(EVIDENCE_LAYERS as readonly string[]).includes(raw.evidenceLayer)
|
|
292
|
+
) return null;
|
|
293
|
+
const freshness = parseFreshness(raw.freshness);
|
|
294
|
+
if (!freshness || !Array.isArray(raw.scenarios)) return null;
|
|
295
|
+
|
|
296
|
+
const roles = new Set<VerificationRole>(["required", "supplemental", "negative_control"]);
|
|
297
|
+
const seenScenarioIds = new Set<string>();
|
|
298
|
+
const scenarios: SuiteManifestV1["scenarios"] = [];
|
|
299
|
+
for (const s of raw.scenarios) {
|
|
300
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) return null;
|
|
301
|
+
const row = s as Record<string, unknown>;
|
|
302
|
+
const scenarioId = requireNonEmptyString(row.id);
|
|
303
|
+
const scenarioVersion = requireNonEmptyString(row.version);
|
|
304
|
+
const manifestDigest = requireNonEmptyString(row.manifestDigest);
|
|
305
|
+
if (
|
|
306
|
+
!scenarioId || !scenarioVersion || !manifestDigest || !isSha256Hex(manifestDigest) ||
|
|
307
|
+
typeof row.role !== "string" || !roles.has(row.role as VerificationRole) ||
|
|
308
|
+
seenScenarioIds.has(scenarioId)
|
|
309
|
+
) return null;
|
|
310
|
+
seenScenarioIds.add(scenarioId);
|
|
311
|
+
scenarios.push({
|
|
312
|
+
id: scenarioId,
|
|
313
|
+
version: scenarioVersion,
|
|
314
|
+
role: row.role as VerificationRole,
|
|
315
|
+
manifestDigest,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
schemaVersion: 1,
|
|
321
|
+
id,
|
|
322
|
+
version,
|
|
323
|
+
evidenceLayer: raw.evidenceLayer,
|
|
324
|
+
capability,
|
|
325
|
+
assertionDslVersion,
|
|
326
|
+
evidenceSchemaVersion,
|
|
327
|
+
freshness,
|
|
328
|
+
contradictionRule,
|
|
329
|
+
scenarios,
|
|
330
|
+
verificationRule,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
discoverLiveScenarios,
|
|
3
|
+
expandLiveScenario,
|
|
4
|
+
loadLiveCaseAuthority,
|
|
5
|
+
} from "../live/manifest";
|
|
6
|
+
import {
|
|
7
|
+
liveSuiteManifestDigestForCase,
|
|
8
|
+
} from "../live/suite-manifest";
|
|
9
|
+
import {
|
|
10
|
+
discoverScenarios,
|
|
11
|
+
expandScenario,
|
|
12
|
+
loadCaseAuthority,
|
|
13
|
+
} from "../conformance/manifest";
|
|
14
|
+
import {
|
|
15
|
+
suiteManifestDigestForCase,
|
|
16
|
+
} from "../conformance/suite-manifest";
|
|
17
|
+
import { scenarioManifestDigest } from "../digest";
|
|
18
|
+
import type { CaseRecord } from "../conformance/types";
|
|
19
|
+
import type { CatalogFilters, CatalogScenarioDto } from "./types";
|
|
20
|
+
|
|
21
|
+
function mapCaseToCatalogScenario(
|
|
22
|
+
caseRecord: CaseRecord,
|
|
23
|
+
authority: ReturnType<typeof loadCaseAuthority>,
|
|
24
|
+
suiteDigestFn: (caseRecord: CaseRecord, authority: ReturnType<typeof loadCaseAuthority>) => string,
|
|
25
|
+
expandFn: (caseRecord: CaseRecord, authority: ReturnType<typeof loadCaseAuthority>) => Record<string, unknown>,
|
|
26
|
+
): CatalogScenarioDto {
|
|
27
|
+
const expanded = expandFn(caseRecord, authority);
|
|
28
|
+
return {
|
|
29
|
+
scenarioId: caseRecord.id,
|
|
30
|
+
scenarioVersion: authority.manifestDefaults.version,
|
|
31
|
+
evidenceLayer: authority.manifestDefaults.evidenceLayer,
|
|
32
|
+
suiteId: caseRecord.suite,
|
|
33
|
+
suiteVersion: authority.manifestDefaults.suiteVersion,
|
|
34
|
+
capability: caseRecord.capability,
|
|
35
|
+
verificationRole: caseRecord.verificationRole ?? authority.manifestDefaults.verificationRole,
|
|
36
|
+
requirements: {
|
|
37
|
+
inboundProtocols: [...caseRecord.requirements.inboundProtocols],
|
|
38
|
+
upstreamProtocols: [...caseRecord.requirements.upstreamProtocols],
|
|
39
|
+
surfaces: [...caseRecord.requirements.surfaces],
|
|
40
|
+
},
|
|
41
|
+
freshness: { ...authority.manifestDefaults.freshness },
|
|
42
|
+
scenarioManifestDigest: scenarioManifestDigest(expanded),
|
|
43
|
+
suiteManifestDigest: suiteDigestFn(caseRecord, authority),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function queryLabCatalog(filters: CatalogFilters = {}): CatalogScenarioDto[] {
|
|
48
|
+
const items: CatalogScenarioDto[] = [];
|
|
49
|
+
if (!filters.layer || filters.layer === "protocol_conformance") {
|
|
50
|
+
const authority = loadCaseAuthority();
|
|
51
|
+
const suites = filters.suiteId ? [filters.suiteId] : undefined;
|
|
52
|
+
const scenarios = discoverScenarios(authority, suites ?? undefined);
|
|
53
|
+
for (const caseRecord of scenarios) {
|
|
54
|
+
items.push(mapCaseToCatalogScenario(caseRecord, authority, suiteManifestDigestForCase, expandScenario));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!filters.layer || filters.layer === "live_route_compatibility") {
|
|
58
|
+
const authority = loadLiveCaseAuthority();
|
|
59
|
+
const suites = filters.suiteId ? [filters.suiteId] : undefined;
|
|
60
|
+
const scenarios = discoverLiveScenarios(authority, suites ?? undefined);
|
|
61
|
+
for (const caseRecord of scenarios) {
|
|
62
|
+
items.push(mapCaseToCatalogScenario(caseRecord, authority, liveSuiteManifestDigestForCase, expandLiveScenario));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return items.sort((a, b) => {
|
|
66
|
+
const layerCmp = a.evidenceLayer.localeCompare(b.evidenceLayer);
|
|
67
|
+
if (layerCmp !== 0) return layerCmp;
|
|
68
|
+
const suiteCmp = a.suiteId.localeCompare(b.suiteId);
|
|
69
|
+
if (suiteCmp !== 0) return suiteCmp;
|
|
70
|
+
return a.scenarioId.localeCompare(b.scenarioId);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { LAB_PROJECTION_SPEC_VERSION } from "../constants";
|
|
4
|
+
import { labSqlitePath } from "../paths";
|
|
5
|
+
import { LAB_SQLITE_SCHEMA_VERSION } from "../projection/schema";
|
|
6
|
+
import { LabProjectionIncompatibleError, LabProjectionUnavailableError } from "./errors";
|
|
7
|
+
|
|
8
|
+
export interface LabReadConnection {
|
|
9
|
+
db: Database;
|
|
10
|
+
sqlitePath: string;
|
|
11
|
+
schemaVersion: number;
|
|
12
|
+
projectionSpecVersion: string;
|
|
13
|
+
builtAtMs: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const COUNTABLE_TABLES = [
|
|
17
|
+
"events",
|
|
18
|
+
"subjects",
|
|
19
|
+
"observations",
|
|
20
|
+
"claims",
|
|
21
|
+
"verdicts",
|
|
22
|
+
"artifacts",
|
|
23
|
+
"corruption",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type CountableTable = (typeof COUNTABLE_TABLES)[number];
|
|
27
|
+
|
|
28
|
+
const COUNT_TABLE_SQL: Record<CountableTable, string> = {
|
|
29
|
+
events: "SELECT COUNT(*) AS c FROM events",
|
|
30
|
+
subjects: "SELECT COUNT(*) AS c FROM subjects",
|
|
31
|
+
observations: "SELECT COUNT(*) AS c FROM observations",
|
|
32
|
+
claims: "SELECT COUNT(*) AS c FROM claims",
|
|
33
|
+
verdicts: "SELECT COUNT(*) AS c FROM verdicts",
|
|
34
|
+
artifacts: "SELECT COUNT(*) AS c FROM artifacts",
|
|
35
|
+
corruption: "SELECT COUNT(*) AS c FROM corruption",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function resolveLabSqlitePath(configDir?: string): string {
|
|
39
|
+
return labSqlitePath(configDir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function openLabReadConnection(configDir?: string): LabReadConnection {
|
|
43
|
+
const sqlitePath = resolveLabSqlitePath(configDir);
|
|
44
|
+
if (!existsSync(sqlitePath)) {
|
|
45
|
+
throw new LabProjectionUnavailableError();
|
|
46
|
+
}
|
|
47
|
+
let db: Database;
|
|
48
|
+
try {
|
|
49
|
+
db = new Database(sqlitePath, { readonly: true });
|
|
50
|
+
} catch {
|
|
51
|
+
throw new LabProjectionUnavailableError();
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const metaRows = db
|
|
55
|
+
.query("SELECT key, value FROM schema_meta")
|
|
56
|
+
.all() as Array<{ key: string; value: string }>;
|
|
57
|
+
const meta = new Map(metaRows.map((r) => [r.key, r.value]));
|
|
58
|
+
const schemaRaw = meta.get("schema_version");
|
|
59
|
+
const specRaw = meta.get("projection_spec_version");
|
|
60
|
+
const builtRaw = meta.get("built_at_ms");
|
|
61
|
+
if (!schemaRaw || !specRaw || !builtRaw) {
|
|
62
|
+
db.close();
|
|
63
|
+
throw new LabProjectionUnavailableError();
|
|
64
|
+
}
|
|
65
|
+
const schemaVersion = Number(schemaRaw);
|
|
66
|
+
const projectionSpecVersion = specRaw;
|
|
67
|
+
const builtAtMs = Number(builtRaw);
|
|
68
|
+
if (
|
|
69
|
+
!Number.isInteger(schemaVersion) ||
|
|
70
|
+
schemaVersion !== LAB_SQLITE_SCHEMA_VERSION ||
|
|
71
|
+
projectionSpecVersion !== LAB_PROJECTION_SPEC_VERSION ||
|
|
72
|
+
!Number.isFinite(builtAtMs)
|
|
73
|
+
) {
|
|
74
|
+
db.close();
|
|
75
|
+
throw new LabProjectionIncompatibleError();
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
db,
|
|
79
|
+
sqlitePath,
|
|
80
|
+
schemaVersion,
|
|
81
|
+
projectionSpecVersion,
|
|
82
|
+
builtAtMs,
|
|
83
|
+
};
|
|
84
|
+
} catch (err) {
|
|
85
|
+
try {
|
|
86
|
+
db.close();
|
|
87
|
+
} catch {
|
|
88
|
+
/* ignore */
|
|
89
|
+
}
|
|
90
|
+
if (err instanceof LabProjectionUnavailableError || err instanceof LabProjectionIncompatibleError) {
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
throw new LabProjectionUnavailableError();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function closeLabReadConnection(conn: LabReadConnection): void {
|
|
98
|
+
conn.db.close();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function countTable(conn: LabReadConnection, table: CountableTable): number {
|
|
102
|
+
if (!(COUNTABLE_TABLES as readonly string[]).includes(table)) {
|
|
103
|
+
throw new LabProjectionUnavailableError();
|
|
104
|
+
}
|
|
105
|
+
const row = conn.db.query(COUNT_TABLE_SQL[table]).get() as { c: number };
|
|
106
|
+
return Number(row.c);
|
|
107
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { LAB_QUERY_CURSOR_VERSION, LAB_QUERY_MAX_CURSOR_BYTES } from "./constants";
|
|
2
|
+
import { InvalidCursorError } from "./errors";
|
|
3
|
+
|
|
4
|
+
export type VerdictCursorPayload = {
|
|
5
|
+
v: typeof LAB_QUERY_CURSOR_VERSION;
|
|
6
|
+
k: "verdicts";
|
|
7
|
+
a: number;
|
|
8
|
+
p: string;
|
|
9
|
+
f: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ObservationCursorPayload = {
|
|
13
|
+
v: typeof LAB_QUERY_CURSOR_VERSION;
|
|
14
|
+
k: "observations";
|
|
15
|
+
c: number;
|
|
16
|
+
e: string;
|
|
17
|
+
f: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type EventCursorPayload = {
|
|
21
|
+
v: typeof LAB_QUERY_CURSOR_VERSION;
|
|
22
|
+
k: "events";
|
|
23
|
+
r: number;
|
|
24
|
+
e: string;
|
|
25
|
+
f: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SubjectCursorPayload = {
|
|
29
|
+
v: typeof LAB_QUERY_CURSOR_VERSION;
|
|
30
|
+
k: "subjects";
|
|
31
|
+
s: string;
|
|
32
|
+
f: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ArtifactCursorPayload = {
|
|
36
|
+
v: typeof LAB_QUERY_CURSOR_VERSION;
|
|
37
|
+
k: "artifacts";
|
|
38
|
+
d: string;
|
|
39
|
+
f: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type LabCursorPayload =
|
|
43
|
+
| VerdictCursorPayload
|
|
44
|
+
| ObservationCursorPayload
|
|
45
|
+
| EventCursorPayload
|
|
46
|
+
| SubjectCursorPayload
|
|
47
|
+
| ArtifactCursorPayload;
|
|
48
|
+
|
|
49
|
+
function stableFilterKey(filters: Record<string, unknown>): string {
|
|
50
|
+
const entries = Object.entries(filters)
|
|
51
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
52
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
53
|
+
return JSON.stringify(entries);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function encodeLabCursor(payload: LabCursorPayload): string {
|
|
57
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function decodeLabCursor(raw: string | null | undefined): LabCursorPayload | null {
|
|
61
|
+
if (typeof raw !== "string" || raw.length === 0 || raw.length > LAB_QUERY_MAX_CURSOR_BYTES) return null;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8")) as Record<string, unknown>;
|
|
64
|
+
if (parsed.v !== LAB_QUERY_CURSOR_VERSION || typeof parsed.k !== "string") return null;
|
|
65
|
+
if (parsed.k === "verdicts") {
|
|
66
|
+
if (typeof parsed.a !== "number" || !Number.isFinite(parsed.a)) return null;
|
|
67
|
+
if (typeof parsed.p !== "string" || parsed.p.length === 0 || parsed.p.length > 256) return null;
|
|
68
|
+
if (typeof parsed.f !== "string") return null;
|
|
69
|
+
return parsed as VerdictCursorPayload;
|
|
70
|
+
}
|
|
71
|
+
if (parsed.k === "observations") {
|
|
72
|
+
if (typeof parsed.c !== "number" || !Number.isFinite(parsed.c)) return null;
|
|
73
|
+
if (typeof parsed.e !== "string" || parsed.e.length === 0 || parsed.e.length > 256) return null;
|
|
74
|
+
if (typeof parsed.f !== "string") return null;
|
|
75
|
+
return parsed as ObservationCursorPayload;
|
|
76
|
+
}
|
|
77
|
+
if (parsed.k === "events") {
|
|
78
|
+
if (typeof parsed.r !== "number" || !Number.isFinite(parsed.r)) return null;
|
|
79
|
+
if (typeof parsed.e !== "string" || parsed.e.length === 0 || parsed.e.length > 256) return null;
|
|
80
|
+
if (typeof parsed.f !== "string") return null;
|
|
81
|
+
return parsed as EventCursorPayload;
|
|
82
|
+
}
|
|
83
|
+
if (parsed.k === "subjects") {
|
|
84
|
+
if (typeof parsed.s !== "string" || parsed.s.length === 0 || parsed.s.length > 256) return null;
|
|
85
|
+
if (typeof parsed.f !== "string") return null;
|
|
86
|
+
return parsed as SubjectCursorPayload;
|
|
87
|
+
}
|
|
88
|
+
if (parsed.k === "artifacts") {
|
|
89
|
+
if (typeof parsed.d !== "string" || parsed.d.length === 0 || parsed.d.length > 128) return null;
|
|
90
|
+
if (typeof parsed.f !== "string") return null;
|
|
91
|
+
return parsed as ArtifactCursorPayload;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function assertCursorFilters(
|
|
100
|
+
cursor: LabCursorPayload,
|
|
101
|
+
filters: Record<string, unknown>,
|
|
102
|
+
kind: LabCursorPayload["k"],
|
|
103
|
+
): void {
|
|
104
|
+
if (cursor.k !== kind) throw new InvalidCursorError();
|
|
105
|
+
const expected = stableFilterKey(filters);
|
|
106
|
+
const actual = (cursor as { f: string }).f;
|
|
107
|
+
if (actual !== expected) throw new InvalidCursorError();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function filterKeyFor(filters: Record<string, unknown>): string {
|
|
111
|
+
return stableFilterKey(filters);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function verdictCursor(asOf: number, projectionKey: string, filters: Record<string, unknown>): VerdictCursorPayload {
|
|
115
|
+
return { v: LAB_QUERY_CURSOR_VERSION, k: "verdicts", a: asOf, p: projectionKey, f: stableFilterKey(filters) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function observationCursor(completedAt: number, eventId: string, filters: Record<string, unknown>): ObservationCursorPayload {
|
|
119
|
+
return { v: LAB_QUERY_CURSOR_VERSION, k: "observations", c: completedAt, e: eventId, f: stableFilterKey(filters) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function eventCursor(recordedAt: number, eventId: string, filters: Record<string, unknown>): EventCursorPayload {
|
|
123
|
+
return { v: LAB_QUERY_CURSOR_VERSION, k: "events", r: recordedAt, e: eventId, f: stableFilterKey(filters) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function subjectCursor(subjectId: string, filters: Record<string, unknown>): SubjectCursorPayload {
|
|
127
|
+
return { v: LAB_QUERY_CURSOR_VERSION, k: "subjects", s: subjectId, f: stableFilterKey(filters) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function artifactCursor(digest: string, filters: Record<string, unknown>): ArtifactCursorPayload {
|
|
131
|
+
return { v: LAB_QUERY_CURSOR_VERSION, k: "artifacts", d: digest, f: stableFilterKey(filters) };
|
|
132
|
+
}
|