Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ independent review, blockers, and whether a later phase is authorized.
| CL-01 | `feat/cl-01-conformance-harness` | `c2113ca47b8a05c5a5f90679e4eaa640ca2c6a66` | `22d608c82d82e2746c0cef9cd761db19a8e465ee` | [#1320](https://github.com/lidge-jun/opencodex/pull/1320) | MERGED TO `dev` at `4bb249b756abd468c675d2d92fffe4da95ad3e2a` |
| CL-02 | `feat/cl-02-evidence-ledger` | `4bb249b756abd468c675d2d92fffe4da95ad3e2a` | NOT RECORDED | [#1333](https://github.com/lidge-jun/opencodex/pull/1333) | MERGED TO `dev` at `025c37916225dd685d9217e5b40190600f06d278`; POST-MERGE HARDENING [#1343](https://github.com/lidge-jun/opencodex/pull/1343) MERGED at `eee2dab4d1bbacefce56057adad51d734f346702`; FINAL CLOSURE GATE [#1348](https://github.com/lidge-jun/opencodex/pull/1348) |
| CL-03 | `feat/cl-03-live-route-probes` | `4f746d13799888ea0a8c7a111aa2ad61c2126ea0` | `003f7402f49bfe8dd710a7beba52f717051bfadf` | [#1352](https://github.com/lidge-jun/opencodex/pull/1352) | MERGED TO `dev` at `68c71a4e9cdf882d812f09fd94783a28749db629`; ACCEPTED/CLOSED |
| CL-04 | `feat/cl-04-lab-read-surfaces` | `68c71a4e9cdf882d812f09fd94783a28749db629` | NOT RECORDED | DRAFT PENDING | IMPLEMENTATION IN PROGRESS (not accepted) |
| CL-04 | `feat/cl-04-lab-read-surfaces` | `68c71a4e9cdf882d812f09fd94783a28749db629` | NOT RECORDED | [#1378](https://github.com/lidge-jun/opencodex/pull/1378) | MERGED TO `dev` at `d517161aeaa3a974ad3c0360ff0c97b03b4c4520` |
| CL-05 | `feat/cl-05-compatibility-matrix-ui` | `d517161aeaa3a974ad3c0360ff0c97b03b4c4520` | `2a159b8b7` (Models tab placement) | [#1384](https://github.com/lidge-jun/opencodex/pull/1384) | IMPLEMENTATION IN PROGRESS (not accepted) |

The CL-01 starting SHA is the exact CL-00 tip recorded when CL-01 began. Its
moving base-ref name is not a substitute for that historical SHA.
Expand Down Expand Up @@ -158,7 +159,8 @@ Claims cannot produce `PROBED`/`VERIFIED`.
- CL-01: **MERGED** via #1320 at `4bb249b756abd468c675d2d92fffe4da95ad3e2a`.
- CL-02: **MERGED** via #1333 at `025c37916225dd685d9217e5b40190600f06d278`; post-merge hardening #1343 is also **MERGED** at `eee2dab4d1bbacefce56057adad51d734f346702`; final closure is tracked in #1348.
- CL-03: **ACCEPTED/CLOSED** via [#1352](https://github.com/lidge-jun/opencodex/pull/1352), merged to `dev` at `68c71a4e9cdf882d812f09fd94783a28749db629`.
- CL-04: **IMPLEMENTATION IN PROGRESS**, authorized from accepted CL-03 merge `68c71a4e9cdf882d812f09fd94783a28749db629`.
- CL-04: **MERGED** via #1378 at `d517161aeaa3a974ad3c0360ff0c97b03b4c4520`.
- CL-05: **IMPLEMENTATION IN PROGRESS**, authorized from CL-04 merge `d517161aeaa3a974ad3c0360ff0c97b03b4c4520`.

## CL-03 implementation log (2026-08-09)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# CL-05 implementation record - Compatibility Matrix UI

## Programme position

- **Phase:** CL-05 (read-only GUI)
- **Starting `upstream/dev` SHA:** `d517161aeaa3a974ad3c0360ff0c97b03b4c4520` (merge #1378 / CL-04)
- **Branch:** `feat/cl-05-compatibility-matrix-ui`
- **PR:** DRAFT → `lidge-jun/opencodex:dev` ([#1384](https://github.com/lidge-jun/opencodex/pull/1384))
- **CL-06:** not started

## Scope delivered

### GUI (`gui/src/pages/`)

- **Models → Compatibility tab** at `#models/compatibility` (not a standalone sidebar page)
- Legacy `#lab` hash redirects to `#models/compatibility`
- `CompatibilityMatrix.tsx` - read-only verdict matrix over CL-04 management APIs
- `compatibility-matrix-api.ts` - bounded paginated fetch helpers for `/api/lab/status`, `/api/lab/verdicts`, `/api/lab/subjects`, and detail reads
- `compatibility-matrix-shared.ts` - fail-closed DTO parsing, matrix grouping, and filters
- `styles-compatibility-matrix.css` - scrollable matrix + detail pane
- Compatibility Lab copy localized for all seven locales (`en`, `de`, `ko`, `zh`, `ru`, `ja`, `tr`)
- Component/layout tests in `gui/tests/compatibility-lab.test.tsx` and `gui/tests/compatibility-matrix-layout.test.ts`

### Behaviour

- Projection status cards (subjects, verdicts, observations, events, built-at)
- Subject × evidence-layer matrix with per-suite verdict badges
- Server-side verdict filters for evidence layer, verdict, and an exact Subject-ID picker, with paginated "Load more"
- Verdict detail pane (subject, all paginated observations, bounded evidence events, artifact metadata only)
- Verdict selection is presentation state only; it never changes matrix filters or pagination identity
- Empty/unavailable/incompatible projection states via existing data-surface contract; malformed successful API payloads fail closed as load errors
- Lazy mount with `active` / hidden-panel gating inside Models workspace
- No probe execution, projection rebuild, or evidence mutation

## Review remediation

The follow-up review pass addresses all validated CodeRabbit findings plus the independent lifecycle findings found while reviewing #1384:

- legacy `#lab` cold-load page alignment and delimiter-aware regression coverage
- repeated-cursor contract guards plus explicit truncation when advancing pagination reaches the browser-side safety cap
- strict verdict/detail DTO validation and malformed-200 handling
- bounded, partial-failure-tolerant event/artifact enrichment
- detail-selection and load-more stale-response protection
- first-page refresh/poll invalidation of appended cursor pages
- exact Subject-ID picker semantics instead of misleading free-text search
- localized closed-value labels and neutral evidence-event wording
- real button semantics for verdict-detail selection
- inactive-panel, selection/filter, pagination, localization, and routing regression coverage
- focused `compatibility-pagination-cap.test.ts` coverage proving a large advancing dataset is truncated rather than mislabeled as a broken contract
- Models-tab layout test updated from the removed standalone App/sidebar architecture

## Validation recorded before review remediation

- `bun x tsc --noEmit` - passed
- `bun test tests/lab-read-surfaces.test.ts tests/models-workspace-tabs.test.ts` - 33/33 passed
- `cd gui && bun test tests/compatibility-lab.test.tsx tests/models-workspace-panels.test.tsx` - 29/29 passed
- `cd gui && bun test tests/compatibility-matrix-layout.test.ts` - 2/2 passed in the full GUI gate
- `bun run lint:gui && bun run doctor:gui && bun run build:gui && bun run privacy:scan` - passed
Comment thread
coderabbitai[bot] marked this conversation as resolved.

The latest pagination-cap remediation adds `gui/tests/compatibility-pagination-cap.test.ts` and requires a fresh CI pass before acceptance; the earlier results above are retained only as baseline evidence.

## Acceptance blockers

- Fresh cross-platform CI on the remediation head must be green
- Independent acceptance review not performed

## Out of scope (confirmed)

- CL-06 routing profile compatibility fields, CL-07 fabric, CL-08 shadow/automatic/public publish
- New management APIs or CLI changes (CL-04 read surfaces are sufficient)
- Probe execution, projection rebuild triggers, raw artifact download in GUI
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 19 additions & 39 deletions gui/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ export function readPageFromHash(hash?: string): Page {
const pageId = raw.split("/")[0] as Page;
// Legacy: Debug used to be a standalone page; it now lives as a tab on Logs.
if (pageId === ("debug" as Page)) return "logs";
// Legacy: Combos and Routing used to be standalone pages; both are Models tabs now.
if (pageId === ("combos" as Page) || pageId === ("routing" as Page)) return "models";
// Legacy: Combos, Routing, and Lab are Models tabs now. Resolve the destination page
// immediately so a cold legacy hash never flashes Dashboard before replacement.
if (pageId === ("combos" as Page)
|| pageId === ("routing" as Page)
|| pageId === ("lab" as Page)) return "models";
// Legacy integration pages now live below one Integrations route. Returning
// the destination page here keeps the initial hook state aligned until the
// resolver replaces the hash with the exact nested destination.
Expand All @@ -54,11 +57,11 @@ export function readPageFromHash(hash?: string): Page {
export const DASHBOARD_TAB_HASHES = ["dashboard/providers", "dashboard/models"] as const;

/**
* Models owns three tabs: the catalog, Combos, and Routing. The catalog is the bare
* `#models`, so it has no suffix entry here same convention Dashboard uses for
* Overview and Logs uses for the log list.
* Models owns four tabs: the catalog, Combos, Routing, and Compatibility. The catalog
* is the bare `#models`, so it has no suffix entry here - same convention Dashboard
* uses for Overview and Logs uses for the log list.
*/
export const MODELS_TAB_HASHES = ["models/combos", "models/routing"] as const;
export const MODELS_TAB_HASHES = ["models/combos", "models/routing", "models/compatibility"] as const;

/**
* `#dashboard/update` is an action deep link, not a tab: the sidebar update button uses
Expand Down Expand Up @@ -98,7 +101,6 @@ export function hashBelongsToPage(rawHash: string, page: Page): boolean {
&& (INTEGRATION_TAB_HASHES as readonly string[]).includes(rawHash));
}


/** Result of resolving an incoming hash. */
export type AppHashChangeAction = {
page: Page;
Expand All @@ -119,48 +121,26 @@ export function resolveAppHashChange(rawHash: string): AppHashChangeAction {
return { page: "logs", replaceTo: "logs/debug" };
}

/*
* Legacy: Combos and Routing used to be standalone pages, now Models tabs.
*
* The `startsWith` arm is not decoration. Without it the generic normalization below
* would rewrite `#routing/anything` to the bare page and drop the destination — the
* same bug the `#api` comment below documents.
*/
/* Legacy Models pages. Delimiter-aware prefix arms preserve nested legacy bookmarks. */
if (rawHash === "combos" || rawHash.startsWith("combos/")) {
return { page: "models", replaceTo: "models/combos" };
}
if (rawHash === "routing" || rawHash.startsWith("routing/")) {
return { page: "models", replaceTo: "models/routing" };
}

/*
* Legacy top-level integration pages.
*
* `readPageFromHash("api")` already answers `integrations`, so without these
* branches the generic normalization below would rewrite the hash to the
* bare page and silently drop the nested destination — an old `#api`
* bookmark would land on Overview instead of API Keys. `replaceTo` is
* applied with replaceState, so the correction adds no history entry.
*/
if (rawHash === "api") {
return { page: "integrations", replaceTo: "integrations/keys" };
}
if (rawHash === "claude") {
return { page: "integrations", replaceTo: "integrations/claude" };
}
if (rawHash === "grok") {
return { page: "integrations", replaceTo: "integrations/grok" };
if (rawHash === "lab" || rawHash.startsWith("lab/")) {
return { page: "models", replaceTo: "models/compatibility" };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/* Legacy top-level integration pages. */
if (rawHash === "api") return { page: "integrations", replaceTo: "integrations/keys" };
if (rawHash === "claude") return { page: "integrations", replaceTo: "integrations/claude" };
if (rawHash === "grok") return { page: "integrations", replaceTo: "integrations/grok" };

// Legacy deep link from the removed dual-layout era.
if (rawHash === "providers/workspace") {
return { page: "providers", replaceTo: "providers" };
}
if (rawHash === "providers/workspace") return { page: "providers", replaceTo: "providers" };

// An unrecognised sub-hash is normalised away rather than left in the URL.
if (!hashBelongsToPage(rawHash, nextPage)) {
return { page: nextPage, replaceTo: nextPage };
}

if (!hashBelongsToPage(rawHash, nextPage)) return { page: nextPage, replaceTo: nextPage };
return { page: nextPage, replaceTo: null };
}
22 changes: 20 additions & 2 deletions gui/src/i18n/catalogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@ import { zh } from "./zh";
import { ru } from "./ru";
import { ja } from "./ja";
import { tr } from "./tr";
import { LAB_CATALOG_OVERRIDES, type LabLocale } from "./lab-translations";

/** React-free locale catalog registry for formatters and other shared helpers. */
export type Locale = "en" | "de" | "ko" | "zh" | "ru" | "ja" | "tr";
export type Locale = LabLocale;

export const DICTS: Record<Locale, Record<TKey, string>> = { en, de, ko, zh, ru, ja, tr };
function withLabTranslations(locale: Locale, catalog: Record<TKey, string>): Record<TKey, string> {
return { ...catalog, ...LAB_CATALOG_OVERRIDES[locale] };
}

/**
* CL-05 translations are overlaid centrally so the compatibility surface cannot regress to
* copied English values in a locale catalog. The locale parity test still validates the base
* catalogs; this overlay is deliberately limited to the closed `lab.*` namespace.
*/
export const DICTS: Record<Locale, Record<TKey, string>> = {
en: withLabTranslations("en", en),
de: withLabTranslations("de", de),
ko: withLabTranslations("ko", ko),
zh: withLabTranslations("zh", zh),
ru: withLabTranslations("ru", ru),
ja: withLabTranslations("ja", ja),
tr: withLabTranslations("tr", tr),
};

/** Native language names shown by the language picker, kept inside i18n rather than UI metadata. */
export function localeDisplayName(locale: Locale): string {
Expand Down
49 changes: 49 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,11 @@ export const de: Record<TKey, string> = {
"prov.openaiApiSetup": "API-Schlüssel einrichten",
"models.tab.catalog": "Modelle",
"models.tab.combos": "Combos",
"models.tab.compatibility": "Kompatibilität",
"models.tab.routing": "Routing (beta)",
"models.tabsLabel": "Modell-Oberflächen",
"models.subtitle.combos": "Geordnete Modellgruppen, die unter einer id antworten. Failover probiert Ziele der Reihe nach, Round-Robin verteilt die Last.",
"models.subtitle.compatibility": "Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.",
"models.subtitle.routing": "Policy-Profile, Dry-Run-Auswertung und quellenbasierte Routing-Analysen.",
"models.subtitle": "Steuere, welche Modelle Codex sieht — natives GPT-Passthrough und geroutete Anbieter, nach Anbieter gruppiert (Kopfzeile zum Einklappen anklicken). Ausgeblendete Modelle fehlen in Katalog und Auswahl, bleiben aber per genauer ID aufrufbar. Änderungen gelten bei der nächsten Codex-Runde — opencodex invalidiert Codex 5-Minuten-Modell-Cache, kein Neustart nötig.",
"models.nativeGroupLabel": "OpenAI nativ",
Expand Down Expand Up @@ -1908,4 +1910,51 @@ export const de: Record<TKey, string> = {
"sub.delegation.modelHint": "Das Modell, zu dem Codex zuerst greift, wenn es Arbeit übergibt. Oben steht, wen es überhaupt aufrufen darf; hier wählst du den Ersten davon.",
"dash.syncModelsHint": "Schreibt Codex' Modellkatalog anhand deiner verbundenen Provider neu.",
"dash.syncRun": "Jetzt synchronisieren",
"lab.title": "Kompatibilitäts-Labor",
"lab.subtitle": "Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.",
"lab.loadFailed": "Kompatibilitäts-Lab-Daten konnten nicht geladen werden",
"lab.projectionUnavailable": "Lab-Projektion ist nicht verfügbar. Führen Sie zuerst Konformitäts- oder Live-Probes aus.",
"lab.projectionIncompatible": "Lab-Projektionsschema ist inkompatibel. Projektion neu aufbauen.",
"lab.statusTitle": "Projektionsstatus",
"lab.matrixTitle": "Kompatibilitätsmatrix",
"lab.verdictsTitle": "Urteilsdatensätze",
"lab.filter.layer": "Evidenzschicht",
"lab.filter.verdict": "Urteil",
"lab.filter.subject": "Subject-ID",
"lab.filter.all": "Alle",
"lab.col.subject": "Subject",
"lab.col.layer": "Schicht",
"lab.col.suite": "Suite",
"lab.col.verdict": "Urteil",
"lab.col.asOf": "Stand",
"lab.col.protocol": "Protokollkonformität",
"lab.col.live": "Live-Route-Kompatibilität",
"lab.col.task": "Aufgabenwirksamkeit",
"lab.empty": "Noch keine Kompatibilitätsurteile in der Projektion.",
"lab.subjectKind": "Art",
"lab.observationCount": "Beobachtungen",
"lab.eventCount": "Ereignisse",
"lab.verdictCount": "Urteile",
"lab.subjectCount": "Subjects",
"lab.builtAt": "Erstellt",
"lab.loading": "Kompatibilitätsevidenz wird geladen…",
"lab.loadMore": "Load more",
"lab.detailTitle": "Verdict detail",
"lab.detailClose": "Close",
"lab.detailSubject": "Subject",
"lab.detailObservations": "Observations",
"lab.detailEvents": "Contributing events",
"lab.detailArtifacts": "Artifact metadata",
"lab.detailLoadFailed": "Could not load verdict detail",
"lab.refresh": "Aktualisieren",
"lab.verdict.UNKNOWN": "Unbekannt",
"lab.verdict.CLAIMED": "Behauptet",
"lab.verdict.PROBED": "Geprüft",
"lab.verdict.VERIFIED": "Verifiziert",
"lab.verdict.DEGRADED": "Eingeschränkt",
"lab.verdict.BLOCKED": "Blockiert",
"lab.verdict.UNSUPPORTED": "Nicht unterstützt",
"lab.layer.protocol_conformance": "Protokollkonformität",
"lab.layer.live_route_compatibility": "Live-Route-Kompatibilität",
"lab.layer.task_effectiveness": "Aufgabenwirksamkeit",
};
51 changes: 51 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,11 @@ export const en = {
// models
"models.tab.catalog": "Models",
"models.tab.combos": "Combos",
"models.tab.compatibility": "Compatibility",
"models.tab.routing": "Routing (beta)",
"models.tabsLabel": "Model surfaces",
"models.subtitle.combos": "Ordered groups of models that answer as one id. Failover tries targets in order; round-robin spreads the load.",
"models.subtitle.compatibility": "Read-only compatibility verdict matrix from lab projection evidence.",
"models.subtitle.routing": "Policy profiles, dry-run evaluation, and source-backed routing analytics.",
"models.subtitle": "Toggle which models Codex sees — native GPT passthrough and routed providers, grouped by provider (click a header to collapse). Hidden models stay off the catalog + model picker but remain directly callable by exact id. Changes apply on the next Codex turn — opencodex invalidates Codex's 5-min model cache so no restart is needed.",
"models.nativeGroupLabel": "OpenAI native",
Expand Down Expand Up @@ -1936,6 +1938,55 @@ export const en = {
"claudeDesktop.effort.supported": "effort",
"claudeDesktop.effort.displayOnly": "effort (display only)",

// compatibility lab (CL-05)
"lab.title": "Compatibility Lab",
"lab.subtitle": "Read-only compatibility verdict matrix from lab projection evidence.",
"lab.loadFailed": "Could not load compatibility lab data",
"lab.projectionUnavailable": "Lab projection is not available. Run conformance or live probes first.",
"lab.projectionIncompatible": "Lab projection schema is incompatible. Rebuild the projection.",
"lab.statusTitle": "Projection status",
"lab.matrixTitle": "Compatibility matrix",
"lab.verdictsTitle": "Verdict records",
"lab.filter.layer": "Evidence layer",
"lab.filter.verdict": "Verdict",
"lab.filter.subject": "Subject ID",
"lab.filter.all": "All",
"lab.col.subject": "Subject",
"lab.col.layer": "Layer",
"lab.col.suite": "Suite",
"lab.col.verdict": "Verdict",
"lab.col.asOf": "As of",
"lab.col.protocol": "Protocol conformance",
"lab.col.live": "Live route compatibility",
"lab.col.task": "Task effectiveness",
"lab.empty": "No compatibility verdicts in the projection yet.",
"lab.subjectKind": "Kind",
"lab.observationCount": "Observations",
"lab.eventCount": "Events",
"lab.verdictCount": "Verdicts",
"lab.subjectCount": "Subjects",
"lab.builtAt": "Built",
"lab.loading": "Loading compatibility evidence…",
"lab.loadMore": "Load more",
"lab.detailTitle": "Verdict detail",
"lab.detailClose": "Close",
"lab.detailSubject": "Subject",
"lab.detailObservations": "Observations",
"lab.detailEvents": "Contributing events",
"lab.detailArtifacts": "Artifact metadata",
"lab.detailLoadFailed": "Could not load verdict detail",
"lab.refresh": "Refresh",
"lab.verdict.UNKNOWN": "Unknown",
"lab.verdict.CLAIMED": "Claimed",
"lab.verdict.PROBED": "Probed",
"lab.verdict.VERIFIED": "Verified",
"lab.verdict.DEGRADED": "Degraded",
"lab.verdict.BLOCKED": "Blocked",
"lab.verdict.UNSUPPORTED": "Unsupported",
"lab.layer.protocol_conformance": "Protocol conformance",
"lab.layer.live_route_compatibility": "Live route compatibility",
"lab.layer.task_effectiveness": "Task effectiveness",

} as const;

export type TKey = keyof typeof en;
Loading
Loading