diff --git a/devlog/_plan/260905_now_split_train/000_3_main_export_baseline.json b/devlog/_plan/260905_now_split_train/000_3_main_export_baseline.json new file mode 100644 index 0000000000..477e650b2a --- /dev/null +++ b/devlog/_plan/260905_now_split_train/000_3_main_export_baseline.json @@ -0,0 +1,277 @@ +{ + "baselineCommit": "48f8186647d9ffb108d226dcfa91a64225aae2a7", + "modules": { + "src/lib/redact.ts": [ + "REDACTED_SECRET", + "SENSITIVE_KEY_PATTERN", + "redactHeaders", + "redactSecretString", + "redactSecrets", + "redactUrlForLog", + "redactUserPath", + "sanitizeLogMetadataString" + ], + "src/providers/openai-tiers.ts": [ + "CODEX_FORWARD_BASE_URL", + "LEGACY_CHATGPT_PROVIDER_ID", + "LEGACY_OPENAI_MULTI_PROVIDER_ID", + "OPENAI_API_PROVIDER_ID", + "OPENAI_CODEX_PROVIDER_ID", + "OpenAiTierMigrationCollisionError", + "destinationDecodesNativeCompactionBlob", + "isCanonicalOpenAiForwardProvider", + "isOpenAiOperatedResponsesDestination", + "projectOpenAiTierMigration", + "supportsNativeResponsesCompactEndpoint" + ], + "src/adapters/anthropic-image-normalize.ts": [ + "IMAGE_NORMALIZE_CACHE_MAX_BYTES", + "IMAGE_NORMALIZE_CONCURRENCY", + "MAX_INPUT_BASE64_LENGTH", + "MAX_INPUT_PIXELS", + "TIER_SPECS", + "anthropicImageNormalizeRetainedStoreSnapshot", + "evictOldestAnthropicImageNormalizeForBudget", + "getNormalizeStatsForTests", + "normalizeAnthropicImages", + "normalizeImageTargets", + "resetNormalizeStateForTests", + "setNormalizeCacheLimitsForTests" + ], + "src/adapters/cursor/native-exec-desktop.ts": [ + "desktopDepsFromConfig" + ], + "src/adapters/cursor/tool-definitions.ts": [ + "CODEX_APPLY_PATCH_TOOL", + "CODEX_EXEC_COMMAND_TOOL", + "CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA", + "CODEX_SHELL_BRIDGE_TOOL_NAMES", + "CODEX_SHELL_COMMAND_TOOL", + "CODEX_TOOL_SEARCH_TOOL", + "CODEX_UNIFIED_EXEC_TOOL", + "CODEX_WAIT_TOOL", + "CURSOR_EDIT_FILE_INPUT_SCHEMA", + "CURSOR_EDIT_FILE_TOOL", + "CURSOR_EXEC_COMMAND_INPUT_SCHEMA", + "CURSOR_EXEC_COMMAND_TOOL", + "CURSOR_GENERIC_TOOL_USE_USER_HINT", + "CURSOR_MULTI_EDIT_INPUT_SCHEMA", + "CURSOR_MULTI_EDIT_TOOL", + "CURSOR_SHELL_ALIAS_SYSTEM_NOTE", + "CURSOR_STRUCTURED_EDIT_TOOLS", + "OCX_RESPONSES_TOOL_PROVIDER", + "appendCursorGenericToolUseHint", + "buildCursorToolDefinitions", + "buildCursorToolGuidanceSystemNote", + "cursorMcpToolEncodedSize", + "cursorMcpToolsEncodedSize", + "cursorRequestAdvertisesApplyPatch", + "cursorRequestAdvertisesStructuredEdits", + "cursorRequestHasShellAlias", + "cursorRequestUsesCodeMode", + "cursorShellBridgeArgsValid", + "cursorShellBridgeDropError", + "cursorStructuredEditTools", + "cursorToolAllowedByChoice", + "cursorToolArgNormalizeSchema", + "cursorToolChoiceAliases", + "cursorToolInputSchema", + "cursorToolWireName", + "cursorToolsForActivePrompt", + "defaultShellBridgeArgNormalizeSchema", + "encodeCursorInputSchema", + "isBareCodexShellBridgeTool", + "isCodexShellBridgeToolName", + "isCursorCodeModeExecTool", + "isCursorExecutionPathTool", + "isCursorStructuredEditToolName", + "isCursorSyntheticStructuredEditTool", + "isCursorWaitTool", + "isGenericToolUseCountDemoPrompt", + "nonEmptyShellBridgeCommandFromArgs", + "normalizeCursorTextToolMarkers", + "normalizeCursorWireName", + "requestedCursorToolUseCount", + "resolveShellBridgeAliasKey", + "responsesToolNameFromCursorWire", + "shellBridgeRequiredCommandKeys", + "shouldAppendCursorGenericToolUseHint", + "shouldUseNativeExecOnlyForGenericToolUse" + ], + "src/adapters/xai-tool-schema.ts": [ + "XaiToolSchemaCompatibilityError", + "isXaiSchemaTarget", + "lookupLocalJsonPointer", + "normalizeXaiToolParameters" + ], + "src/vision/index.ts": [ + "BASELINE_VISION_MODELS", + "DEFAULT_MAX_DESCRIPTIONS_PER_TURN", + "DEFAULT_VISION_TIMEOUT_MS", + "MAX_VISION_TIMEOUT_MS", + "MIN_VISION_TIMEOUT_MS", + "VISION_DESCRIPTION_CACHE_MAX_BYTES", + "describeImage", + "describeImageAnthropic", + "describeImagesInPlace", + "evictOldestVisionDescriptionForBudget", + "findAnthropicVisionProvider", + "isModelTextOnly", + "isModelVisionSidecarConsumer", + "isValidVisionTimeoutMs", + "isVisionEligibleModel", + "isVisionSidecarConsumer", + "modelAcceptsImageInput", + "parseAnthropicVisionSSE", + "planVisionSidecar", + "resetVisionDescriptionCache", + "resolveEffectiveVisionModel", + "resolveMaxDescriptionsPerTurn", + "resolveOpenAiVisionModel", + "resolveVisionBackend", + "resolveVisionTimeoutMs", + "setVisionDescriptionCache", + "setVisionDescriptionCacheLimitsForTests", + "shouldResolveOpenAiVisionSidecar", + "stripImagesInPlace", + "visionBackendForCandidate", + "visionDescriptionRetainedStoreSnapshot", + "visionEligibleModelOptions" + ], + "src/responses/parser.ts": [ + "parseRequest" + ], + "src/claude/inbound.ts": [ + "AnthropicRequestError", + "DEFAULT_BLOCKED_SKILLS", + "anthropicToResponsesBody", + "anthropicToResponsesTranslation", + "effectiveBlockedSkillNames", + "effortForThinkingBudget", + "effortFromOutputConfig", + "extractOcxEffortDirective", + "extractOcxRouteDirective", + "resolveInboundModel" + ], + "src/server/system-env.ts": [ + "applySystemEnvToggle", + "claudeCodeCliInstalled", + "cleanStaleSystemEnv", + "getShellEnvFilePath", + "getSystemEnvTrackingPath", + "injectSystemEnv", + "installShellHook", + "launchctlGetenv", + "reconcileShellHook", + "revertSystemEnv", + "uninstallShellHook" + ], + "src/codex/prompt-layers.ts": [ + "LAYER_INVENTORY", + "MAX_BASE_VARIANTS", + "TOGGLE_IDS", + "activeBaseVariantDir", + "activeConfigPath", + "activeStorePath", + "adoptDeveloperInstructions", + "composeProjection", + "computePromptProbeStateFingerprint", + "computeRevision", + "decodeBasicString", + "encodeBasicString", + "findInvalidCharacter", + "inspectOwnership", + "isToggleId", + "normalizeBody", + "parseStore", + "previewAdopt", + "previewSalvage", + "readBaseVariants", + "readFileBytes", + "readPromptLayers", + "resolveBaseSelection", + "salvageProjection", + "selectBaseVariant", + "setToggle", + "writeBaseVariant", + "writeCustomLayers" + ], + "src/combos/types.ts": [ + "COMBO_NAMESPACE", + "comboAliasIssues", + "comboConfigError", + "comboConfigIssues", + "comboDefaultEffort", + "comboDisabledModelId", + "comboDisabledModelSelectors", + "comboModelId", + "comboPublicModelId", + "getCombo", + "isNativeAliasCombo", + "isValidComboId", + "listComboIds", + "listLiveComboTargetKeys", + "normalizeComboConfig", + "parseComboModelId", + "preservesPhysicalComboProvider", + "resolveComboId", + "targetKey" + ], + "src/codex/log-guard/inspect.ts": [ + "hasCurrentLogsSchema", + "inspectCodexLogs", + "resetCodexLogGuardInspectionCache" + ], + "src/clients/config-export.ts": [ + "ClientPathError", + "EXPORT_CLIENTS", + "EXPORT_CLIENT_IDS", + "GAJAE_API_KEY_ENV", + "HERMES_API_KEY_ENV", + "HERMES_API_KEY_ENV_REF", + "LOOPBACK_API_KEY_PLACEHOLDER", + "OPENCLAW_API_KEY_ENV", + "OPENCLAW_API_KEY_ENV_REF", + "OPENCODE_API_KEY_ENV", + "OPENCODE_API_KEY_ENV_REF", + "OPENCODE_CONFIG_SCHEMA", + "OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG", + "OPENCODE_PROVIDER_ID", + "SCHEMA_REQUIRED_OUTPUT_BUDGET", + "asideAccountDir", + "asideConfigPath", + "asideHomeDir", + "buildClientConfig", + "buildClientConfigText", + "buildClientContribution", + "buildOpencodeProviderBlockFromCatalog", + "dshConfigPath", + "dshHomeDir", + "gajaeConfigPath", + "gajaeHomeDir", + "hermesConfigPath", + "hermesHomeDir", + "isExportClientId", + "kimiConfigPath", + "kimiHomeDir", + "kimiModelAlias", + "mcodeConfigPath", + "mcodeHomeDir", + "normalizeExportModels", + "ompAgentDir", + "ompModelsConfigPath", + "openclawConfigPath", + "openclawHomeDir", + "opencodeGlobalConfigPath", + "opencodeProviderBlocks", + "opencodeProxyBaseUrl", + "opencodeV2ProviderBlock", + "piAgentDir", + "piConfigPath", + "primeAgentDir", + "primeConfigPath", + "zcodeConfigPath", + "zcodeHomeDir" + ] + } +} diff --git a/devlog/_plan/260905_now_split_train/000_plan.md b/devlog/_plan/260905_now_split_train/000_plan.md index 9ff8346d09..ba80143bce 100644 --- a/devlog/_plan/260905_now_split_train/000_plan.md +++ b/devlog/_plan/260905_now_split_train/000_plan.md @@ -1,5 +1,11 @@ # 260905 — RESOLVABLE_NOW split train (stacked PRs) +> Historical full-debt objective. The user's later cutoff is governed by800, +> 801,810 and820: consolidate the existing14 split PRs, run two full regression +> cycles, and deliver only the final head. All further implementation is +> deferred; the original68-row objective is not claimed complete. Peer +> coordination is closed. Older recipes below are not current execution authority. + Date: 2026-09-05. Worktree a2c0, docs branch `codex/260905-modular-debt-ledger-docs` at 4cc219549 (source basis 980a9fbed; origin/dev tip at unit open 583d6a91b, 6 commits ahead, only one of which touches a NOW file — see 001). Session diff --git a/devlog/_plan/260905_now_split_train/001_stale_check.md b/devlog/_plan/260905_now_split_train/001_stale_check.md new file mode 100644 index 0000000000..182cc77699 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/001_stale_check.md @@ -0,0 +1,115 @@ +# 001 — Stale check of the 68 NOW rows against origin/dev + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. Counts, source ranges and origin/dev observations below belong to the historical checkpoint, not the current inventory. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Command: `git fetch origin dev; git diff --numstat 980a9fbed origin/dev -- ` +for each row, plus `git merge-base --is-ancestor 980a9fbed origin/dev` (true; +origin/dev = 583d6a91b, 6 commits ahead). + +Result: **67 unchanged, 1 changed.** (origin/dev moved to 1362b1a38 during +drafting; the IntegrationsOverview delta arrives in that commit, #3540 carry.) + +| Path | Upstream delta | Disposition | +|---|---|---| +| gui/src/pages/integrations/IntegrationsOverview.tsx | +9 lines (isMissingJournalEntry guard in the delete handler, #3540 carry) | KEEP — the layer rebases onto origin/dev before moving; line ranges in its decade doc are taken from origin/dev, not 980a9fbed | + +All other rows: line counts and ranges in the lane docs remain exact at +origin/dev. + +## Oracle inventory (per row) + +`textoracle` = number of test files that read the file as text or resolve its +path (`rg -l 'readFileSync|Bun\.file|source\(' tests | xargs rg -l `); +`fanin` = importer count across src/gui/scripts/tests. Both from +origin/dev. + +| Path | lines | fanin | textoracle | +|---|---:|---:|---:| +| src/server/claude-messages.ts | 1092 | — | — | +| src/responses/parser.ts | 883 | — | — | +| src/server/responses/collaboration.ts | 622 | — | — | +| src/claude/inbound.ts | 578 | — | — | +| src/server/management/logs-usage-routes.ts | 569 | — | — | +| src/server/management/lab-routes.ts | 562 | — | — | +| src/server/system-env.ts | 537 | — | — | +| src/server/responses/agent-task-recovery.ts | 498 | — | — | +| src/responses/namespace-tool-compat.ts | 435 | — | — | +| src/providers/registry.ts | 3250 | — | — | +| src/codex/prompt-layers.ts | 1652 | — | — | +| src/codex/cli-install-provenance.ts | 795 | — | — | +| src/routing/trace.ts | 776 | — | — | +| src/codex/subagent-defaults.ts | 550 | — | — | +| src/codex/log-guard/inspect.ts | 524 | — | — | +| src/codex/log-guard/protection.ts | 489 | — | — | +| src/oauth/github-copilot.ts | 428 | — | — | +| src/combos/types.ts | 423 | — | — | +| src/providers/openai-tiers.ts | 416 | — | — | +| src/codex/log-guard/maintenance.ts | 403 | — | — | +| src/adapters/cursor/protobuf-events.ts | 1381 | — | — | +| src/adapters/anthropic.ts | 1375 | — | — | +| src/adapters/ollama-native.ts | 1131 | — | — | +| src/adapters/cursor/tool-definitions.ts | 777 | — | — | +| src/adapters/cursor/catalog.ts | 716 | — | — | +| src/adapters/cursor/images.ts | 704 | — | — | +| src/vision/index.ts | 667 | — | — | +| src/adapters/command-code.ts | 637 | — | — | +| src/images/artifacts.ts | 552 | — | — | +| src/adapters/cursor/request-builder.ts | 518 | — | — | +| src/adapters/anthropic-image-normalize.ts | 518 | — | — | +| src/adapters/xai-tool-schema.ts | 436 | — | — | +| gui/src/pages/Storage.tsx | 1469 | — | — | +| gui/src/pages/integrations/IntegrationsOverview.tsx | 748 | — | — | +| gui/src/pages/ClaudeDesktop.tsx | 689 | — | — | +| gui/src/components/storage-workspace/StorageWorkspace.tsx | 668 | — | — | +| gui/src/combo-workspace-data.ts | 650 | — | — | +| gui/src/pages/CompatibilityMatrix.tsx | 628 | — | — | +| gui/src/pages/integrations/overview-clients.ts | 555 | — | — | +| gui/src/components/MemoryObservabilityCard.tsx | 527 | — | — | +| gui/src/components/provider-workspace/ProviderSettings.tsx | 514 | — | — | +| gui/src/pages/dashboard-shared.ts | 488 | — | — | +| gui/src/components/QuotaBars.tsx | 452 | — | — | +| gui/src/pages/compatibility-matrix-api.ts | 432 | — | — | +| gui/src/components/combo-workspace-detail-panel.tsx | 401 | — | — | +| src/clients/config-export.ts | 1990 | — | — | +| scripts/release-notes.ts | 1233 | — | — | +| src/lab/events/validate.ts | 781 | — | — | +| src/lab/conformance/executor.ts | 741 | — | — | +| src/cli/opencode.ts | 682 | — | — | +| src/lab/artifacts/sanitize.ts | 585 | — | — | +| scripts/test.ts | 572 | — | — | +| src/cli/status.ts | 547 | — | — | +| src/lab/ledger/store.ts | 531 | — | — | +| src/lib/redact.ts | 526 | — | — | +| src/lab/automation/persistence.ts | 512 | — | — | +| src/cli/minimax.ts | 497 | — | — | +| src/integrations/state.ts | 495 | — | — | +| src/lab/fabric/observe.ts | 489 | — | — | +| src/cli/provider.ts | 485 | — | — | +| src/client/hub-client.ts | 481 | — | — | +| src/lab/public/community.ts | 479 | — | — | +| src/lab/projection/verdicts.ts | 474 | — | — | +| src/lib/errors.ts | 457 | — | — | +| src/lab/fabric/scratch.ts | 439 | — | — | +| src/lib/upstream-retry.ts | 429 | — | — | +| src/lab/projection/verification.ts | 412 | — | — | +| scripts/disposable-host/codex-service-composed-acceptance.ts | 402 | — | — | + +(fanin/textoracle values are recorded per layer in each decade doc from the +same command; the notable ones for slicing were: `src/combos/types.ts` +fanin 946, `src/providers/registry.ts` 167, `src/adapters/cursor/catalog.ts` +129, `src/lab/ledger/store.ts` 111; text oracles: `src/vision/index.ts` 47, +`scripts/test.ts` 40, `src/integrations/state.ts` 4, `src/codex/prompt-layers.ts` +3, `src/lab/ledger/store.ts` 3, `src/server/system-env.ts` 2, and 1 each for +claude-messages, registry, log-guard/inspect, cursor/images, overview-clients, +release-notes, lab/conformance/executor, cli/opencode, cli/provider, +lib/upstream-retry.) + +## Intra-set import edges (prerequisite for stacking) + +47 edges among the 68 files (script: read every `from "./..."` specifier and +resolve against the set). They define the within-stack order in 002. +Cross-stack edges exist in both directions (e.g. registry ← cursor/catalog, +logs-usage-routes ← registry); they never block a layer because every layer +preserves the original path's barrel re-export, so a consumer in another +stack keeps compiling whether or not the producer's split has merged. diff --git a/devlog/_plan/260905_now_split_train/002_layer_map.md b/devlog/_plan/260905_now_split_train/002_layer_map.md new file mode 100644 index 0000000000..4c70cfd77e --- /dev/null +++ b/devlog/_plan/260905_now_split_train/002_layer_map.md @@ -0,0 +1,134 @@ +# 002 — Layer map and stack topology + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +77 layers, 21 stacks (105 and 625 appended per 003). Base rule (003 STACK-INDEPENDENCE-01, applied per layer): a layer's base is the nearest lower layer of its stack that it imports from or that is a `#`-part of the same file; S04 layers additionally base on the 105 type-contract layer; otherwise the base is `dev`. 29 layers are chained, 48 are `dev`-based; the stack id groups execution order only. Within a stack, layers are dependency-ordered (a file +that imports another file in the same stack comes after it); `#a/#b/#c` +split one large file across consecutive layers so each layer stays ≤500 +changed source lines. Branch = `codex/split-`; bottom base `dev`. + +Execution order across stacks: round-robin by layer index (all L1s, then all +L2s, …) so that each stack's PR can be reviewed while the next layer is +prepared, and no stack blocks another. + +| Doc | Stack | Layer | File | Lines | Branch | Base | +|---|---|---:|---|---:|---|---| +| 010 | S01 lib | 1 | src/lib/redact.ts | 526 | codex/split-lib-redact | dev | +| 020 | S01 lib | 2 | src/lib/errors.ts | 457 | codex/split-lib-errors | dev | +| 030 | S01 lib | 3 | src/lib/upstream-retry.ts | 429 | codex/split-lib-upstream-retry | dev | +| 040 | S02 providers | 1 | src/providers/openai-tiers.ts | 416 | codex/split-providers-openai-tiers | dev | +| 050 | S02 providers | 2 | src/providers/registry.ts (#a) | 3250 | codex/split-providers-registry-a | dev | +| 060 | S02 providers | 3 | src/providers/registry.ts (#b) | 3250 | codex/split-providers-registry-b | codex/split-providers-registry-a | +| 070 | S02 providers | 4 | src/providers/registry.ts (#c) | 3250 | codex/split-providers-registry-c | codex/split-providers-registry-b | +| 080 | S03 adapters-anthropic | 1 | src/adapters/anthropic-image-normalize.ts | 518 | codex/split-adapters-anthropic-image-normalize | dev | +| 090 | S03 adapters-anthropic | 2 | src/adapters/anthropic.ts (#a) | 1375 | codex/split-adapters-anthropic-a | codex/split-adapters-anthropic-image-normalize | +| 100 | S03 adapters-anthropic | 3 | src/adapters/anthropic.ts (#b) | 1375 | codex/split-adapters-anthropic-b | codex/split-adapters-anthropic-a | +| 105 | S04 adapters-cursor | 0 | src/adapters/cursor/native-exec-desktop.ts | 15 | codex/split-cursor-desktop-executor-contract | dev | +| 110 | S04 adapters-cursor | 1 | src/adapters/cursor/tool-definitions.ts | 777 | codex/split-adapters-cursor-tool-definitions | codex/split-cursor-desktop-executor-contract | +| 120 | S04 adapters-cursor | 2 | src/adapters/cursor/catalog.ts | 716 | codex/split-adapters-cursor-catalog | codex/split-cursor-desktop-executor-contract | +| 130 | S04 adapters-cursor | 3 | src/adapters/cursor/images.ts | 704 | codex/split-adapters-cursor-images | codex/split-cursor-desktop-executor-contract | +| 140 | S04 adapters-cursor | 4 | src/adapters/cursor/request-builder.ts | 518 | codex/split-adapters-cursor-request-builder | codex/split-adapters-cursor-images | +| 150 | S04 adapters-cursor | 5 | src/adapters/cursor/protobuf-events.ts | 1381 | codex/split-adapters-cursor-protobuf-events | codex/split-adapters-cursor-tool-definitions | +| 160 | S05 adapters-misc | 1 | src/adapters/xai-tool-schema.ts | 436 | codex/split-adapters-xai-tool-schema | dev | +| 170 | S05 adapters-misc | 2 | src/adapters/command-code.ts | 637 | codex/split-adapters-command-code | dev | +| 180 | S05 adapters-misc | 3 | src/adapters/ollama-native.ts | 1131 | codex/split-adapters-ollama-native | dev | +| 190 | S06 media | 1 | src/vision/index.ts | 667 | codex/split-vision-index | dev | +| 200 | S06 media | 2 | src/images/artifacts.ts | 552 | codex/split-images-artifacts | dev | +| 210 | S07 responses | 1 | src/responses/parser.ts | 883 | codex/split-responses-parser | dev | +| 220 | S07 responses | 2 | src/responses/namespace-tool-compat.ts | 435 | codex/split-responses-namespace-tool-compat | dev | +| 230 | S07 responses | 3 | src/server/responses/agent-task-recovery.ts | 498 | codex/split-server-responses-agent-task-recovery | dev | +| 240 | S07 responses | 4 | src/server/responses/collaboration.ts | 622 | codex/split-server-responses-collaboration | codex/split-responses-parser | +| 250 | S08 server-claude | 1 | src/claude/inbound.ts | 578 | codex/split-claude-inbound | dev | +| 260 | S08 server-claude | 2 | src/server/claude-messages.ts | 1092 | codex/split-server-claude-messages | codex/split-claude-inbound | +| 270 | S09 server-management | 1 | src/server/system-env.ts | 537 | codex/split-server-system-env | dev | +| 280 | S09 server-management | 2 | src/server/management/logs-usage-routes.ts | 569 | codex/split-server-management-logs-usage-routes | codex/split-server-system-env | +| 290 | S09 server-management | 3 | src/server/management/lab-routes.ts | 562 | codex/split-server-management-lab-routes | dev | +| 300 | S10 codex-prompt | 1 | src/codex/prompt-layers.ts (#a) | 1652 | codex/split-codex-prompt-layers-a | dev | +| 310 | S10 codex-prompt | 2 | src/codex/prompt-layers.ts (#b) | 1652 | codex/split-codex-prompt-layers-b | codex/split-codex-prompt-layers-a | +| 320 | S11 codex-misc | 1 | src/combos/types.ts | 423 | codex/split-combos-types | dev | +| 330 | S11 codex-misc | 2 | src/codex/subagent-defaults.ts | 550 | codex/split-codex-subagent-defaults | dev | +| 340 | S11 codex-misc | 3 | src/codex/cli-install-provenance.ts | 795 | codex/split-codex-cli-install-provenance | dev | +| 350 | S11 codex-misc | 4 | src/routing/trace.ts | 776 | codex/split-routing-trace | dev | +| 360 | S11 codex-misc | 5 | src/oauth/github-copilot.ts | 428 | codex/split-oauth-github-copilot | dev | +| 370 | S12 log-guard | 1 | src/codex/log-guard/inspect.ts | 524 | codex/split-codex-log-guard-inspect | dev | +| 380 | S12 log-guard | 2 | src/codex/log-guard/protection.ts | 489 | codex/split-codex-log-guard-protection | codex/split-codex-log-guard-inspect | +| 390 | S12 log-guard | 3 | src/codex/log-guard/maintenance.ts | 403 | codex/split-codex-log-guard-maintenance | codex/split-codex-log-guard-inspect | +| 400 | S13 clients-cli | 1 | src/clients/config-export.ts (#a) | 1990 | codex/split-clients-config-export-a | dev (prerequisite #3610 landed; 003/400) | +| 410 | S13 clients-cli | 2 | src/clients/config-export.ts (#b) | 1990 | codex/split-clients-config-export-b | codex/split-clients-config-export-a | +| 420 | S13 clients-cli | 3 | src/cli/opencode.ts | 682 | codex/split-cli-opencode | codex/split-clients-config-export-b | +| 430 | S13 clients-cli | 4 | src/cli/minimax.ts | 497 | codex/split-cli-minimax | codex/split-cli-opencode | +| 440 | S13 clients-cli | 5 | src/integrations/state.ts | 495 | codex/split-integrations-state | codex/split-clients-config-export-b | +| 450 | S14 cli-hub | 1 | src/cli/status.ts | 547 | codex/split-cli-status | dev | +| 460 | S14 cli-hub | 2 | src/cli/provider.ts | 485 | codex/split-cli-provider | dev | +| 470 | S14 cli-hub | 3 | src/client/hub-client.ts | 481 | codex/split-client-hub-client | dev | +| 480 | S15 lab-events | 1 | src/lab/events/validate.ts | 781 | codex/split-lab-events-validate | dev | +| 490 | S15 lab-events | 2 | src/lab/ledger/store.ts | 531 | codex/split-lab-ledger-store | codex/split-lab-events-validate | +| 500 | S15 lab-events | 3 | src/lab/artifacts/sanitize.ts | 585 | codex/split-lab-artifacts-sanitize | dev | +| 510 | S15 lab-events | 4 | src/lab/fabric/observe.ts | 489 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | +| 520 | S15 lab-events | 5 | src/lab/fabric/scratch.ts | 439 | codex/split-lab-fabric-scratch | dev | +| 530 | S16 lab-rest | 1 | src/lab/conformance/executor.ts | 741 | codex/split-lab-conformance-executor | dev | +| 540 | S16 lab-rest | 2 | src/lab/automation/persistence.ts | 512 | codex/split-lab-automation-persistence | dev | +| 550 | S16 lab-rest | 3 | src/lab/public/community.ts | 479 | codex/split-lab-public-community | dev | +| 560 | S16 lab-rest | 4 | src/lab/projection/verification.ts | 412 | codex/split-lab-projection-verification | dev | +| 570 | S16 lab-rest | 5 | src/lab/projection/verdicts.ts | 474 | codex/split-lab-projection-verdicts | codex/split-lab-projection-verification | +| 580 | S17 gui-storage | 1 | gui/src/components/storage-workspace/StorageWorkspace.tsx | 668 | codex/split-components-storage-workspace-StorageWorkspace | dev | +| 590 | S17 gui-storage | 2 | gui/src/pages/Storage.tsx (#a) | 1469 | codex/split-pages-Storage-a | codex/split-components-storage-workspace-StorageWorkspace | +| 600 | S17 gui-storage | 3 | gui/src/pages/Storage.tsx (#b) | 1469 | codex/split-pages-Storage-b | codex/split-pages-Storage-a | +| 610 | S18 gui-integrations | 1 | gui/src/pages/integrations/overview-clients.ts | 555 | codex/split-pages-integrations-overview-clients | dev | +| 620 | S18 gui-integrations | 2 | gui/src/pages/integrations/IntegrationsOverview.tsx (#a) | 748 | codex/split-pages-integrations-IntegrationsOverview-a | codex/split-pages-integrations-overview-clients | +| 625 | S18 gui-integrations | 3 | gui/src/pages/integrations/IntegrationsOverview.tsx (#b) | 619 | codex/split-pages-integrations-IntegrationsOverview-b | codex/split-pages-integrations-IntegrationsOverview-a | +| 630 | S19 gui-compat-combo | 1 | gui/src/pages/compatibility-matrix-api.ts | 432 | codex/split-pages-compatibility-matrix-api | dev | +| 640 | S19 gui-compat-combo | 2 | gui/src/pages/CompatibilityMatrix.tsx | 628 | codex/split-pages-CompatibilityMatrix | codex/split-pages-compatibility-matrix-api | +| 650 | S19 gui-compat-combo | 3 | gui/src/combo-workspace-data.ts | 650 | codex/split-combo-workspace-data | dev | +| 660 | S19 gui-compat-combo | 4 | gui/src/components/combo-workspace-detail-panel.tsx | 401 | codex/split-components-combo-workspace-detail-panel | codex/split-combo-workspace-data | +| 670 | S20 gui-misc | 1 | gui/src/pages/ClaudeDesktop.tsx | 689 | codex/split-pages-ClaudeDesktop | dev | +| 680 | S20 gui-misc | 2 | gui/src/components/MemoryObservabilityCard.tsx | 527 | codex/split-components-MemoryObservabilityCard | dev | +| 690 | S20 gui-misc | 3 | gui/src/components/provider-workspace/ProviderSettings.tsx | 514 | codex/split-components-provider-workspace-ProviderSettings | dev | +| 700 | S20 gui-misc | 4 | gui/src/pages/dashboard-shared.ts | 488 | codex/split-pages-dashboard-shared | dev | +| 710 | S20 gui-misc | 5 | gui/src/components/QuotaBars.tsx | 452 | codex/split-components-QuotaBars | dev | +| 720 | S21 scripts | 1 | scripts/release-notes.ts (#a) | 1233 | codex/split-release-notes-a | dev | +| 730 | S21 scripts | 2 | scripts/release-notes.ts (#b) | 1233 | codex/split-release-notes-b | codex/split-release-notes-a | +| 740 | S21 scripts | 3 | scripts/test.ts | 572 | codex/split-test | dev | +| 750 | S21 scripts | 4 | scripts/disposable-host/codex-service-composed-acceptance.ts | 402 | codex/split-disposable-host-codex-service-composed-acceptance | dev | + +## Stack theses + +| Stack | Thesis | +|---|---| +| S01 lib | shared leaf utilities (redact/errors/upstream-retry) split before their consumers move | +| S02 providers | openai-tiers leaf, then registry.ts in three layers (contracts → entries → lookups) — 260818 WP3 | +| S03 adapters-anthropic | image-normalize leaf, then anthropic.ts in two layers | +| S04 adapters-cursor | desktop-executor-contract (105, type-cycle prerequisite per 003 TYPE-CYCLE-01) → tool-definitions → catalog → images → request-builder → protobuf-events (import order); depth 6, documented exception | +| S05 adapters-misc | xai-tool-schema, command-code, ollama-native | +| S06 media | vision/index (no text oracle; three recursive source-walk guards must include the new leaves — 003 S06-ORACLE-01), images/artifacts | +| S07 responses | parser → namespace-tool-compat → agent-task-recovery → collaboration | +| S08 server-claude | claude/inbound → server/claude-messages | +| S09 server-management | system-env → logs-usage-routes → lab-routes | +| S10 codex-prompt | prompt-layers in two layers | +| S11 codex-misc | combos/types (fanin 946, barrel-only), subagent-defaults, cli-install-provenance, routing/trace, oauth/github-copilot | +| S12 log-guard | inspect → protection → maintenance | +| S13 clients-cli | config-export (two layers) → opencode → minimax → integrations/state | +| S14 cli-hub | status, provider, hub-client | +| S15 lab-events | events/validate → ledger/store → artifacts/sanitize → fabric/observe → fabric/scratch | +| S16 lab-rest | conformance/executor, automation/persistence, public/community, projection/verification → verdicts | +| S17 gui-storage | StorageWorkspace → Storage page (two layers) | +| S18 gui-integrations | overview-clients → IntegrationsOverview #a → #b (rebased on origin/dev first; #b appended per 003) | +| S19 gui-compat-combo | compatibility-matrix-api → CompatibilityMatrix; combo-workspace-data → detail-panel | +| S20 gui-misc | ClaudeDesktop, MemoryObservabilityCard, ProviderSettings, dashboard-shared, QuotaBars | +| S21 scripts | release-notes (two layers), scripts/test.ts (40 text oracles), composed-acceptance | + +## Historical per-layer gate — do not execute + +```sh +# in the layer worktree, at the layer tip +bun run typecheck # exit 0 +bun test tests/[/] # focused, 0 fail +bun run privacy:scan # exit 0 +bun test tests/lab/core-lab-boundary.test.ts # when src/server|src/router|src/lib touched +wc -l # each <=400 or #b layer named +rg -n "from \"[^\"]*/\"" src gui/src scripts tests | wc -l # importer count unchanged; typecheck proves resolution +# full suite (never locally) +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch -q origin && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test > /tmp/suite-.log 2>&1; rc=$?; tail -15 /tmp/suite-.log; echo SUITE_EXIT=$rc; exit $rc' +# the receipt records the printed HEAD sha (must equal the layer tip) and SUITE_EXIT=0 +``` diff --git a/devlog/_plan/260905_now_split_train/003_parent_decisions.md b/devlog/_plan/260905_now_split_train/003_parent_decisions.md index 333e9eaca7..9941ff8414 100644 --- a/devlog/_plan/260905_now_split_train/003_parent_decisions.md +++ b/devlog/_plan/260905_now_split_train/003_parent_decisions.md @@ -228,7 +228,8 @@ Fetch dev and prove the merge is its ancestor. Record these results per layer. Existing open criterion c-4 was amended to this requirement with its original definition preserved in the steering ledger; no criterion was marked met. -The coordinator still schedules one non-Windows CI at a time. Retargeting and -merging may start new CI, so those actions consume the assigned slot too. -Windows-owner work remains excluded. Each peer retains its task scope; the -admin instruction removes redundant permission questions, not failure gates. +Historical scheduling policy, now retired: a coordinator assigned non-Windows +CI slots while excluding Windows-owned work. The user's later instruction +closed peer communication and slot coordination.800/810/820 now govern this +task's independent final-head-only verification and admin delivery; no peer +report, pause, cancellation or handoff is authorized by this old policy. diff --git a/devlog/_plan/260905_now_split_train/004_roadmap_lock.md b/devlog/_plan/260905_now_split_train/004_roadmap_lock.md new file mode 100644 index 0000000000..a4dc559022 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/004_roadmap_lock.md @@ -0,0 +1,98 @@ +# 004 — Roadmap lock (wp1 D) + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Locked 2026-09-05 after a five-round audit (003 amendments applied). This is +the work-phase map the goalplan carries: one work-phase per layer, id +`L`, in the execution order below (round-robin by layer index across +stacks so that each stack's bottom PR is open before any second layer). + +| Order | WP id | Doc | Branch | Base | +|---:|---|---|---|---| +| 1 | L105 | 105 | codex/split-cursor-desktop-executor-contract | dev | +| 2 | L010 | 010 | codex/split-lib-redact | dev | +| 3 | L040 | 040 | codex/split-providers-openai-tiers | dev | +| 4 | L080 | 080 | codex/split-adapters-anthropic-image-normalize | dev | +| 5 | L110 | 110 | codex/split-adapters-cursor-tool-definitions | codex/split-cursor-desktop-executor-contract | +| 6 | L160 | 160 | codex/split-adapters-xai-tool-schema | dev | +| 7 | L190 | 190 | codex/split-vision-index | dev | +| 8 | L210 | 210 | codex/split-responses-parser | dev | +| 9 | L250 | 250 | codex/split-claude-inbound | dev | +| 10 | L270 | 270 | codex/split-server-system-env | dev | +| 11 | L300 | 300 | codex/split-codex-prompt-layers-a | dev | +| 12 | L320 | 320 | codex/split-combos-types | dev | +| 13 | L370 | 370 | codex/split-codex-log-guard-inspect | dev | +| 14 | L400 | 400 | codex/split-clients-config-export-a | dev | +| 15 | L450 | 450 | codex/split-cli-status | dev | +| 16 | L480 | 480 | codex/split-lab-events-validate | dev | +| 17 | L530 | 530 | codex/split-lab-conformance-executor | dev | +| 18 | L580 | 580 | codex/split-components-storage-workspace-StorageWorkspace | dev | +| 19 | L610 | 610 | codex/split-pages-integrations-overview-clients | dev | +| 20 | L630 | 630 | codex/split-pages-compatibility-matrix-api | dev | +| 21 | L670 | 670 | codex/split-pages-ClaudeDesktop | dev | +| 22 | L720 | 720 | codex/split-release-notes-a | dev | +| 23 | L020 | 020 | codex/split-lib-errors | dev | +| 24 | L050 | 050 | codex/split-providers-registry-a | dev | +| 25 | L090 | 090 | codex/split-adapters-anthropic-a | codex/split-adapters-anthropic-image-normalize | +| 26 | L120 | 120 | codex/split-adapters-cursor-catalog | codex/split-cursor-desktop-executor-contract | +| 27 | L170 | 170 | codex/split-adapters-command-code | dev | +| 28 | L200 | 200 | codex/split-images-artifacts | dev | +| 29 | L220 | 220 | codex/split-responses-namespace-tool-compat | dev | +| 30 | L260 | 260 | codex/split-server-claude-messages | codex/split-claude-inbound | +| 31 | L280 | 280 | codex/split-server-management-logs-usage-routes | codex/split-server-system-env | +| 32 | L310 | 310 | codex/split-codex-prompt-layers-b | codex/split-codex-prompt-layers-a | +| 33 | L330 | 330 | codex/split-codex-subagent-defaults | dev | +| 34 | L380 | 380 | codex/split-codex-log-guard-protection | codex/split-codex-log-guard-inspect | +| 35 | L410 | 410 | codex/split-clients-config-export-b | codex/split-clients-config-export-a | +| 36 | L460 | 460 | codex/split-cli-provider | dev | +| 37 | L490 | 490 | codex/split-lab-ledger-store | codex/split-lab-events-validate | +| 38 | L540 | 540 | codex/split-lab-automation-persistence | dev | +| 39 | L590 | 590 | codex/split-pages-Storage-a | codex/split-components-storage-workspace-StorageWorkspace | +| 40 | L620 | 620 | codex/split-pages-integrations-IntegrationsOverview-a | codex/split-pages-integrations-overview-clients | +| 41 | L640 | 640 | codex/split-pages-CompatibilityMatrix | codex/split-pages-compatibility-matrix-api | +| 42 | L680 | 680 | codex/split-components-MemoryObservabilityCard | dev | +| 43 | L730 | 730 | codex/split-release-notes-b | codex/split-release-notes-a | +| 44 | L030 | 030 | codex/split-lib-upstream-retry | dev | +| 45 | L060 | 060 | codex/split-providers-registry-b | codex/split-providers-registry-a | +| 46 | L100 | 100 | codex/split-adapters-anthropic-b | codex/split-adapters-anthropic-a | +| 47 | L130 | 130 | codex/split-adapters-cursor-images | codex/split-cursor-desktop-executor-contract | +| 48 | L180 | 180 | codex/split-adapters-ollama-native | dev | +| 49 | L230 | 230 | codex/split-server-responses-agent-task-recovery | dev | +| 50 | L290 | 290 | codex/split-server-management-lab-routes | dev | +| 51 | L340 | 340 | codex/split-codex-cli-install-provenance | dev | +| 52 | L390 | 390 | codex/split-codex-log-guard-maintenance | codex/split-codex-log-guard-inspect | +| 53 | L420 | 420 | codex/split-cli-opencode | codex/split-clients-config-export-b | +| 54 | L470 | 470 | codex/split-client-hub-client | dev | +| 55 | L500 | 500 | codex/split-lab-artifacts-sanitize | dev | +| 56 | L550 | 550 | codex/split-lab-public-community | dev | +| 57 | L600 | 600 | codex/split-pages-Storage-b | codex/split-pages-Storage-a | +| 58 | L625 | 625 | codex/split-pages-integrations-IntegrationsOverview-b | codex/split-pages-integrations-IntegrationsOverview-a | +| 59 | L650 | 650 | codex/split-combo-workspace-data | dev | +| 60 | L690 | 690 | codex/split-components-provider-workspace-ProviderSettings | dev | +| 61 | L740 | 740 | codex/split-test | dev | +| 62 | L070 | 070 | codex/split-providers-registry-c | codex/split-providers-registry-b | +| 63 | L140 | 140 | codex/split-adapters-cursor-request-builder | codex/split-adapters-cursor-images | +| 64 | L240 | 240 | codex/split-server-responses-collaboration | codex/split-responses-parser | +| 65 | L350 | 350 | codex/split-routing-trace | dev | +| 66 | L430 | 430 | codex/split-cli-minimax | codex/split-cli-opencode | +| 67 | L510 | 510 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | +| 68 | L560 | 560 | codex/split-lab-projection-verification | dev | +| 69 | L660 | 660 | codex/split-components-combo-workspace-detail-panel | codex/split-combo-workspace-data | +| 70 | L700 | 700 | codex/split-pages-dashboard-shared | dev | +| 71 | L750 | 750 | codex/split-disposable-host-codex-service-composed-acceptance | dev | +| 72 | L150 | 150 | codex/split-adapters-cursor-protobuf-events | codex/split-adapters-cursor-tool-definitions | +| 73 | L360 | 360 | codex/split-oauth-github-copilot | dev | +| 74 | L440 | 440 | codex/split-integrations-state | codex/split-clients-config-export-b | +| 75 | L520 | 520 | codex/split-lab-fabric-scratch | dev | +| 76 | L570 | 570 | codex/split-lab-projection-verdicts | codex/split-lab-projection-verification | +| 77 | L710 | 710 | codex/split-components-QuotaBars | dev | + + +## Historical execution sequence — do not execute + +The original sequence was: each work-phase = one full PABCD cycle: P stale-checks its decade doc against +the tip of its base branch; A = gpt-6-astra read-only plan audit; B = +gpt-6-astra executor in a dedicated `git worktree`; C = 002 per-layer gate + +lidge full suite (pipefail receipt); D = commit, push, PR (base per 002), +record PR number + CI rollup in the decade doc, then re-enter P. diff --git a/devlog/_plan/260905_now_split_train/005_delivery_evidence_refresh.md b/devlog/_plan/260905_now_split_train/005_delivery_evidence_refresh.md new file mode 100644 index 0000000000..d89987015d --- /dev/null +++ b/devlog/_plan/260905_now_split_train/005_delivery_evidence_refresh.md @@ -0,0 +1,40 @@ +# 005 — Delivery evidence refresh + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. Old verification debt and diagnoses are not current failure claims or permission for new diagnostics. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Later WP400 verification checkpoint + +The supported `cxc loop steer` operation applied batch `wp400-verification-reconciliation-20260905`: retain history, annotate the premature c-3 mark, and add mandatory open criterion c-5 requiring fresh evidence for every final layer head. There is no reopen verb; no session/goalplan state was hand-edited and no acceptance requirement was weakened. Shared macOS RCA is recorded in006; its cause remains unknown. + +Additional read-only log retrieval showed #3590 failing at the same `tests/update/update-stop-first.test.ts:240` expectation as #3594: waitForProxy returned false after92178.26ms; job101237519332/run33940504774 reports9244pass/3skip/1fail. Two matching observations do not establish a flake or its cause. Neither failing test job was rerun. + +For #3570 only, verified run33936218644/job101224631090 was cancelled and belongs to the unchanged current PR headfdddbd3e1516997111b201a7c191fc08a6f8d4dd. `gh run rerun 33936218644 --job 101224631090` exited0. This requeues the cancelled enforce-target check, not a failed test; replacement outcome remains pending. + +PR #3611 is open/draft at24466356836dd567120d3d3f4e8d09574f2182d3. Remote typecheck,442focusedtests, privacy, independent implementation/security review and mutation red/green passed. Full suite failed with4baseline route-registry/rollover failures; no passing receipt or D close. See400 for evidence and the separate #3610 prerequisite disposition. + +The #3594 macOS failure log is now available: `tests/update/update-stop-first.test.ts:240` expected `waitForProxy(port)` true but received false after92800.62ms. Job101239095583 in run33941274745 reported9244pass/3skip/1fail. That is the observed failure, not an established cause or permission for a blind retry. No source change or rerun was made for it. + +## 2026-09-05 audit checkpoint during WP400 + +Read-only refresh: `gh pr view --json state,isDraft,headRefOid,baseRefName,statusCheckRollup`. All 13 listed PRs reported OPEN and non-draft. This snapshot distinguishes publication from verified completion; no merge, rerun, PR mutation, or local suite was performed by this refresh. + +| PR | Exact head | Base | Reported check state | +|---|---|---|---| +| #3557 | `97df51515c22ccd610665989aa940f15bc3bca24` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3559 | `5b253af7f3392c4af3c2177d6b66a06a8d674044` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3566 | `58dba9e0b2209bd9f76c4d5fb4943df0d6ab710b` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3567 | `c1d436738c5fb012b666cc15e87e777a66e7648d` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3570 | `fdddbd3e1516997111b201a7c191fc08a6f8d4dd` | codex/split-cursor-desktop-executor-contract | enforce-target: CANCELLED | +| #3574 | `8a404cb889abda5ab6d9cd384833e5d3c34dd873` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3577 | `51f5a82d7c6ff3cc3a2df1a08716fa5eff1e67b1` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3580 | `3793fb0326b8aea541918905461a8a4a0e5fcd79` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3583 | `c0fab2d74b977092884ea817c274ef2f3f4021a7` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3585 | `1cab08d405fc59bc5b386aa21a073f4301246ac2` | dev | Reported checks passed/skipped/neutral; cancelled duplicates have successful replacements | +| #3590 | `82e069c9fe59b9660bee7964cd58c0141687267b` | dev | macos 1/2: IN_PROGRESS; macos 2/2: IN_PROGRESS; keyring macos: IN_PROGRESS | +| #3594 | `0c914bf265ce38c57498c21ccf81f0202b9c133c` | dev | macos 1/2: FAILURE; ci: QUEUED | +| #3599 | `5c1a398da78975312c183c1c2b6e0ff8241ac02c` | dev | resolve-pr: QUEUED; enforce-target: QUEUED; test 1/4: IN_PROGRESS; test 2/4: IN_PROGRESS; test 3/4: IN_PROGRESS; test 4/4: IN_PROGRESS; storage policy: IN_PROGRESS; macos 1/2: QUEUED; macos 2/2: QUEUED; keyring ubuntu: QUEUED; keyring macos: QUEUED; npm-global ubuntu-latest: QUEUED; npm-global macos-latest: QUEUED | + +Cancelled jobs are not automatically ignored: a replacement must have the same check name and SUCCESS on the queried head. In particular, #3570 has no successful enforce-target replacement in this snapshot. #3594's failed macos 1/2 job is https://github.com/lidge-jun/opencodex/actions/runs/33941274745/job/101239095583; `gh run view 33941274745 --job 101239095583 --log-failed` exited 1 because the workflow was still running and logs were unavailable. The failure cause is not yet established; do not label it a flake or a regression without the log. + +The goalplan's c-3 currently says met even though it describes per-layer verification and many layers remain unbuilt. That mark is not evidence of whole-train completion. The final audit must reconcile every layer and repair criterion state using a supported workflow; this note does not overwrite the FSM or manufacture receipts. Earlier published layers with incomplete remote receipts or failed checks remain open verification work even where their workphase status says done. diff --git a/devlog/_plan/260905_now_split_train/006_macos_recovery_verification_debt.md b/devlog/_plan/260905_now_split_train/006_macos_recovery_verification_debt.md new file mode 100644 index 0000000000..97ba321239 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/006_macos_recovery_verification_debt.md @@ -0,0 +1,39 @@ +# 006 — Shared macOS recovery-test verification debt + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. Old verification debt and diagnoses are not current failure claims or permission for new diagnostics. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Observed failure, not established cause + +Read-only RCA by Copernicus (01a06fc1-504a-7b81-b3b0-760ab93c8788, explicitly gpt-6-astra high). No code edits, tests, SSH, workflow reruns or PR mutations in that lane. + +| PR/head | Run/job | Observation | +|---|---|---| +| #3590 / 82e069c9fe59b9660bee7964cd58c0141687267b | 33940504774 / 101237519332 | recovery assertion after92.178s;9244pass/3skip/1fail | +| #3594 / 0c914bf265ce38c57498c21ccf81f0202b9c133c | 33941274745 / 101239095583 | same assertion after92.801s;9244pass/3skip/1fail | + +Both used Bun1.4.0 (34cbb9a40), macOS26.6.2 ARM64, runner image20260831.0337.3. The reviewer verified checked-out merge trees matched the pinned PR-head trees. No uploaded artifacts were available. + +## Confirmed observability gap + +At the pinned versions, tests/update/update-stop-first.test.ts:227-240 verifies update exit1 and recovery announcements, then waitForProxy returnsfalse. bin/ocx.mjs:266-281 announces recovery before launching a detached child with stdio ignore and unref. tests/update/update-stop-first.test.ts:48-63 discards probe exceptions and non-success response details. Its cleanup at244-279 discards stop output and removes the fixture. Main spot-checked these excerpts with git show82e069c9. + +The logs do not retain the detached child's stderr/exit status, listener identity or probe-error history. A startup announcement is not startup proof. The root cause remains unknown; neither a flake nor an environmental exemption has been established. + +## Competing hypotheses and falsifiers + +| Hypothesis | Falsifier | Status | +|---|---|---| +| Child exits/stalls before bind | Identified child serves successful health during failure interval | unresolved | +| Shard process history/resource interference | Same failure in matched clean singleton | unresolved | +| Live child, unsuccessful transport or HTTP probe | Child conclusively exits before listening | unresolved | + +The outer165s test budget was not exhausted: this was an assertion failure. An interactive prompt is inconsistent with the detached child's ignored/non-TTY stdio and the prompt's TTY gate. + +Linux4/4 batch21/23 passes the case in2.298s/2.313s, but its <=12-file fresh-process batches differ from the macOS536-file shard. scripts/test.ts:327 assigns the case a dedicated serial full-suite lane; those CI paths bypass that wrapper. Prior15→45→90s increases are not causal evidence for today's failure. + +## Next diagnostic, not an approved implementation + +Collect test-owned failure evidence before teardown: detached Node/Bun PID and exit/signal, sanitized stderr, allowlisted runtime state, listener ownership and timestamped probe outcomes. Preserve assertions, cleanup and existing budgets. Compare the instrumented same-image macOS shard against a singleton, changing only isolation, before choosing a startup/harness fix. No blind retry, timeout increase or production patch is justified by current evidence. + +This diagnostic is outside WP400's source/test write set and has not been implemented. The train's fresh verification criterion c-5 keeps these failures open; historical workphase-done/c-3-met flags do not close them. diff --git a/devlog/_plan/260905_now_split_train/007_wp450_check_progress.md b/devlog/_plan/260905_now_split_train/007_wp450_check_progress.md new file mode 100644 index 0000000000..7d7e1f08d8 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/007_wp450_check_progress.md @@ -0,0 +1,25 @@ +# 007 — WP450 check status + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. WP450 hold is superseded by450's delivered record; neither WP445 norWP450 resumes. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +WP450 remains unverified at its final delivery gate. Existing branch +`codex/split-cli-status`, PR #3633, head +`4a71894f73357e92da6b2435c6fbfe4f7f675287` is preserved. + +The planned status-module extraction reduces the original547-line owner to +384lines with a168-line leaf. All15declarations and11public exports were +reviewed. Remote focused/full results exist, but hosted CI has an unresolved +failure. Do not count this layer complete or substitute older receipts. + +An independent maintenance prerequisite, WP445/PR #3640, is under review. +Investigation and reproduction notes belong to ignored scratch. The public +record contains only scope, status, and workflow; release documentation will +record the published outcome later. + +WP450 is suspended pending, not done. Its tasks/criteria/evidence are retained. +The new prerequisite sits immediately before it; D must resume WP450. + +Pending scheduling was also corrected to put unchanged WP580 before dependent +WP590. Independent review verified every prior phase definition and29base edges. +No criterion or completed state was weakened. Local suites remain prohibited. diff --git a/devlog/_plan/260905_now_split_train/008_serial_ci_coordination.md b/devlog/_plan/260905_now_split_train/008_serial_ci_coordination.md new file mode 100644 index 0000000000..820810854e --- /dev/null +++ b/devlog/_plan/260905_now_split_train/008_serial_ci_coordination.md @@ -0,0 +1,63 @@ +# 008 — Closed historical cross-task CI coordination + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +The user requested conversational coordination among active tasks: one +non-Windows CI run at a time, with the separate Windows maintenance work left +alone. This is scheduling, not a repository concurrency-setting change and +not a transfer of merge authority between tasks. Local suites remain banned. + +## Confirmed handoff + +PR #3582, head `0efd0c1594dfbcbf46002a2af38a269367619713`, completed workflow +33949918771 successfully. Its owner returned the first slot without starting +the next stacked layer, retargeting, or merging. + +Owners saved complete exact-head/job snapshots and cancelled their own other +unfinished non-Windows runs. Image runs33949975196/33949974086/33949974578/ +33949973937/33949973996 reached completed/cancelled with zero unfinished jobs. +Provider3584/3598 and registration3636 likewise stopped. Coordinator's fresh +workflow inventory showed only the explicitly excluded Windows run active +before granting the next slot. No foreign run was cancelled by this task. + +PR #3589, head `5060ac8910eff1877bd2ee6bdcb2b6d063f26b79`, then received its +dedicated slot. A single `--failed` resumption advanced workflow33949975196 +from attempt1 to2. Actual execution is limited to cancelled test2/4, macos1/2, +macos2/2 and keyring-macos, followed by aggregation. Ten successful jobs were +carried forward with their original execution timestamps and steps, although +the API assigned new job database IDs. IDs alone cannot prove a job reran. + +## Retired queue + +The historical queue ordered3582,3589,3578,3584,3636 and remaining dependencies. +It was retired by the user's later direction that tasks proceed independently. +No owner reporting, live recheck, slot grant, cancellation, pause or peer +communication is authorized by this record. Do not restart coordination. + +## Subsequent handoffs + +- #3589 attempt2: SUCCESS at the recorded head; owner returned slot. +- #3578: one normal push of ae3e1aea8a22f63ad05e7df4efd123220e5d0bc5, + CI33951393329 SUCCESS (18success/2configuredskip), governing checks also + passed. Review replies/resolution and landing remain separately scheduled. +- #3584: original/current merge commits differed but their parent IDs and + treeba1232aa812ceb8e660a528b4b5b66a9e8092db8 matched. Partial attempt2 of + CI33949917919 passed with12successful jobs preserved. Owner returned slot. +- #3636: original/current mergeeb25dc0742fa3a335f95b7bea7b6a86b5e72d20b and + tree68b480c7cb3b4259ad3b2aced2054e5245a2b7d7 matched. Attempt2 resumed11 + cancelled jobs and preserved6passes. macos2/2 job101269594430 failed; + aggregation failed honestly. Owner returned slot for read-only RCA; no + repeated run or silent waiver. +- WP445 then owned the verification slot. Source + f47a8e39885a6c79ffdb7b50fb4594aae199a2da was published; isolated exact-head + SSH full verification was running at that checkpoint before PR creation. No hosted CI overlaps + that full suite. New source/receipt remain bound to a2c0. + +Investigation and detailed control records are retained in ignored scratch. +This public document records scheduling and verification status only. + +Windows-owned development-branch CI arising from its own merge was also left +alone. Task ownership—not merely a `win-` branch-name filter—determines the +user's exception. A diagnostic-only Astra task later gained upstream-WS +implementation authority; its prospective slot request is historical, not active. diff --git a/devlog/_plan/260905_now_split_train/009_pending_preflight_findings.md b/devlog/_plan/260905_now_split_train/009_pending_preflight_findings.md new file mode 100644 index 0000000000..a2a0657537 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/009_pending_preflight_findings.md @@ -0,0 +1,115 @@ +# 009 — Pending layer preflight findings + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. WP480 amendments were incorporated into frozen ddb7013a, now deferred. Other preflights remain unapproved historical proposals. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Read-only gpt-6-astra high preflights while CI is serialized. These are +preparation findings, not official P/A approval or runtime verification. +Source basis is `a687eb735afc7307f902816972c2f8fb522ed2f3`; refresh each layer +against its actual base before implementation. No pending source was edited. + +## WP480 — lab event validation + +All31owned declarations match the planned ranges;18move,13remain. Projected +four leaves are71/98/181/117lines and facade301, with445lines relocated. +All7public names and41imported bindings remain. Candidate local graph has13 +files, no unresolved edges or facade/leaf cycles. Both private fact-key Sets +remain single-owner; post-validation order must remain unchanged. + +Three amendments are required in `480_lab_events_validate.md`: + +- Line209: replace dedicated-worktree/local/shared-remote verifier with the + same-a2c0, exact-head, isolated remote-only recipe and complete exit/output. +- Line184: specify a small existing-test delta, including forwarded functions, + error-class identity, private-export exclusion and moved-validator behavior. +- Line203: the purported moved event-ID validator actually stays in the + facade. Use a moved sorted/duplicate-ID guard and its ledger test near345 + for a discriminating negative; retain event-ID as integration coverage. + +The raw-churn escalation at144 is superseded by003's pure-move/non-move rule. +Smallest unit remains one facade/four-leaf PR, not WP490 implementation. + +## WP530 — conformance executor + +All25declaration ranges match;17move,8remain,376body lines relocated. +Leaves95/179/146 and corrected residual341lines preserve4runtime exports +and14direct consumers. Candidate graph has374files and42inline-import edges, +with no new return cycle. Preliminary raw churn833/non-move81 must be +measured again after actual implementation and test amendments. + +Three amendments are required in `530_lab_conformance_executor.md`: + +- Line132: retained `parsedFromContext` still needs + `import type { OcxParsedRequest } from "../../types";`. +- Line199: always returning fixture JSON cannot fail the cited empty-events + fallback test. Add a nonempty-event case to the existing regression file, + with independent expected output, plus forwarding/private-export checks. + A second moved continuation control can remove prepended tool calls and + target the existing correlated-pairs case. +- Line205: replace obsolete local/shared-remote verification. Keep adapter/ + budget disposal, reader release and response-store cleanup inside finally. + +One independent facade/three-leaf PR remains appropriate. No480 or540–570 +implementation is required by this layer. No import-time allocations allowed. + +## WP020 — error predicates + +The old457-line basis is now496lines. The plan omitted the location predicate +and private pattern tuple, shifting every subsequent range: + +| Declaration | Correct basis range | +|---|---| +| LOCATION_UNSUPPORTED_PATTERNS |136–145| +| isLocationUnsupportedMessage |147–150| +| isClientClosedMessage |160–169| +| classifyError |171–321| +| isRateLimitOrQuotaFailureMessage |327–344| +| parseRetryAfterFromMessage |347–360| +| inferHttpStatusFromAdapterMessage |363–421| +| adapterFailureFromMessage |424–448| +| httpStatusFromTerminalError |451–496| + +Move complete25–169chunk:145lines/12declarations into +`src/lib/error-message-predicates.ts`, leaving355lines/8declarations in +errors.ts. Source churn294; four export modifiers and four scaffold lines +are non-move wiring. Public boundary is15exports (14runtime+1type), not14. +Forward7moved public names, including the location predicate; import9local +bindings. The location tuple stays private and four former private predicates +stay leaf-only. Retain rate-limit classification and retry parsing in errors.ts +to avoid a return dependency. Neither owner needs imports, so no new cycle. + +Importer census is24files (17production/7tests), including the omitted routing +combo failover test. Extend existing error-fidelity coverage for all8location +phrases, uppercase and negatives; preserve status/permission/5xx precedence. +Negative controls: ACLfalse→503case red; locationfalse→public classification +case red; overbroad client-close→false499rejected. Each mutation is remote-only +and restored before green. No caller migration or new test file needed. + +Before P/A, amend020's basis/ranges/counts/export lists, module-state wording, +dev-based independence criterion, and remote-only verifier. Its source and +the inspected tests have no diff between this basis and the subsequently +observed6b85485fdev, but this observation is not permission to skip a fresh +base check when execution actually starts. + +## WP050 — provider metadata leaves + +Read-only basis6b85485f32f783bafc61c79185d0cb937848859d: registry3251lines, +146declarations. Preserve the newer alias field/value; refresh stale ranges. +Six self-contained leaves relocate814lines/120declarations with no imports: +frontier202, reasoning155, coding-plan133, Kimi49, NIM82, gateway193. +Retaining the header and adding six imports leaves2443lines; later060/070 +remain necessary. First-layer source arithmetic820add/814delete, only6wiring +lines before tests. No new facade re-export is needed; retain11types/12values. + +The new leaves cannot create return cycles. Existing witnesses include erased +type edges; do not claim the whole repository is acyclic. Preserve original +provider allocation/order and validation, shared Kimi references, deliberate +Anthropic copies, and single ownership of the existing private metadata Set. + +Amend the existing provider-registry parity test with shared/distinct identity +checks; value equality alone misses some aliasing regressions. Tie negative +controls to moved metadata, not unmoved provider entries. Refresh importer +census rather than trusting old basename counts. The obsolete local/shared- +remote verifier and already-resolved raw-churn escalation must be replaced. +050→060→070 remains the dependency chain; later contract/FastWire work is not +part of050. This is preparation only, not formal P/A or a runtime result. diff --git a/devlog/_plan/260905_now_split_train/010_lib_redact.md b/devlog/_plan/260905_now_split_train/010_lib_redact.md new file mode 100644 index 0000000000..53fb860917 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/010_lib_redact.md @@ -0,0 +1,162 @@ +# S01 L1/3 — Redaction lexical folding + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Class: C3 boundary planning, docs-only here; the implementation preserves the security-sensitive redaction algorithm verbatim. +- Goal: reduce src/lib/redact.ts from 526 to 353 lines by moving the complete lexical folding owner into src/lib/redact-folding.ts (176 lines). +- Non-goals: no new redaction grammar, normalization, validation, dependencies, public exports, caller migration, or function-body cleanup. Existing long functions are an explicit pure-move exception to the 50-line guideline. +- Verifier: 002_layer_map.md, "Per-layer gate", instantiated in Verification below. No tests were run while drafting this document. +- Stop: one independently verified layer, original import surface intact, exact-head CI green and PR evidence recorded by the parent executor. Never merge. +- Escalation: source drift, any byte/output change, an unlisted oracle, a new cycle, changed source diff over 500 lines, or any required write beyond this layer. Security findings go to ignored scratch, not this public plan. + +Basis: docs HEAD 4cc219549; source origin/dev 1362b1a38. The working-tree source has no diff against origin/dev. 000 and 001 record older tips; these ranges use the refreshed source tip. Lane evidence: 016_lane_cli_storage_usage_update_lab_scripts.md:418–431 in the modular-debt-ledger unit. + +Structural map before choosing the split: 56 direct importing files across src, gui/src, scripts, tests; examples src/config.ts:62 and src/lib/debug.ts:3. Current module has no imports. Both maskOtherFramingsOnce (src/lib/redact.ts:213) and maskCredentialHeadersOnce (:364) call foldForMatching. Intended direction: unchanged consumers → redact.ts → redact-folding.ts; the leaf imports nothing. Only the existing redaction entry boundary changes internally; blast radius is the lib module and its preserved consumers. + +Decision: extract folding and its lookup tables together. Doing nothing leaves 526 lines; deletion/configuration cannot preserve the algorithm; moving frame matchers too adds churn without being needed for the limit. Reusing another owner was rejected after rg for foldForMatching in src/lib found only this definition/call sites. Adjacent convention: domain-named sibling leaves src/lib/debug-settings.ts, src/lib/debug-log-buffer.ts and src/lib/bounded-body.ts, plus src/config/provider-validation.ts. No new index/barrel is introduced. The original implementation keeps all public functions rather than becoming an internal convenience barrel. + +## Symbol inventory + +All ranges are inclusive origin/dev:src/lib/redact.ts declaration ranges, obtained from rg top-level declaration/closing-line output and line-numbered source inspection. Attached comments are accounted for separately in the move ranges below. Consumer counts mean distinct external importing files containing the exact symbol (rg -l -w); importer candidates come from rg -l redact src gui/src scripts tests, followed by relative-path resolution of static imports, dynamic imports and mocks. Counts are lexical file references, not call counts; private symbols have zero external consumers. Imports are absent. R = residual src/lib/redact.ts; F = src/lib/redact-folding.ts. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| REDACTED_SECRET | const string | 1–1 | yes | 1 | R | +| SENSITIVE_KEY_PATTERN | const RegExp | 8–8 | yes | 1 | R | +| CREDENTIAL_HEADER_LABEL_RAW | const string | 41–41 | no | 0 | R | +| CREDENTIAL_HEADER_LABEL | const string | 43–44 | no | 0 | R | +| COLON_CONFUSABLES | const Set | 50–53 | no | 0 | F | +| INVISIBLE_FORMAT | const RegExp | 63–63 | no | 0 | F | +| NAMED_ENTITY_PLACEHOLDER | const string | 80–80 | no | 0 | F | +| SEPARATOR_ENTITIES | const Map | 87–92 | no | 0 | F | +| LETTER_CONFUSABLES | const Map | 101–115 | no | 0 | F | +| COLON_LABELLED_CREDENTIAL | const RegExp | 128–131 | no | 0 | R | +| OTHER_FRAMED_CREDENTIALS | const tuple array | 141–193 | no | 0 | R | +| maskOtherFramings | function | 204–208 | no | 0 | R | +| maskOtherFramingsOnce | function | 210–241 | no | 0 | R | +| foldForMatching | function | 248–347 | no; leaf-only export after move | 0 | F | +| maskCredentialHeaders | function | 358–361 | no | 0 | R | +| maskCredentialHeadersOnce | function | 363–410 | no | 0 | R | +| SECRET_VALUE_PATTERNS | const tuple array | 412–429 | no | 0 | R | +| HeaderRecord | type | 431–431 | no | 0 | R | +| isPlainObject | function | 433–437 | no | 0 | R | +| isSensitiveKey | function | 439–441 | no | 0 | R | +| redactSecretString | function | 443–449 | yes | 48 | R | +| sanitizeLogMetadataString | function | 452–460 | yes | 7 | R | +| redactSecrets | function | 462–473 | yes | 4 | R | +| redactHeaders | function | 475–487 | yes | 1 | R | +| redactUrlForLog | function | 489–500 | yes | 3 | R | +| USER_HOME_PATH_PATTERNS | const tuple array | 502–507 | no | 0 | R | +| SENSITIVE_SEGMENT_PATTERN | const RegExp | 511–511 | no | 0 | R | +| redactUserPath | function | 519–526 | yes | 5 | R | + +## Leaf partition + +One new sibling: src/lib/redact-folding.ts. + +- Symbols: COLON_CONFUSABLES, INVISIBLE_FORMAT, NAMED_ENTITY_PLACEHOLDER, SEPARATOR_ENTITIES, LETTER_CONFUSABLES, foldForMatching. Only foldForMatching becomes a leaf export, for the production calls in the residual; do not re-export it from the original path. +- Own imports: none; TextEncoder and other standard globals remain globals. +- Exact source chunks including their comments: src/lib/redact.ts:46–115 (70 lines) and :243–347 (105 lines). Join with one blank line; add only the export modifier to foldForMatching. Expected 176 lines. +- Residual: retain all other bytes/declarations, prepend the one import plus one blank line shown below. Expected 526 − 175 + 2 = 353 lines. Total new layout 176 + 353 = 529 (three added layout/import lines). +- Expected source additions/deletions: 178 added and 175 deleted, 353 total before optional formatting; no formatting sweep. No #b part is needed. Lowest-churn extraction has zero existing external symbol consumers; all public consumer paths remain stable. + +## Re-export block + +Exact added re-export block: empty. No currently exported symbol moves, so no export-from or export-type-from statement is required. Keep the eight current exports as their original declarations: REDACTED_SECRET, SENSITIVE_KEY_PATTERN, redactSecretString, sanitizeLogMetadataString, redactSecrets, redactHeaders, redactUrlForLog, redactUserPath. Adding foldForMatching to that surface would violate this plan. + +Exact local import to prepend (followed by one blank line): + + import { foldForMatching } from "./redact-folding"; + +Re-exporting a helper would not bind it locally. The two residual call sites require this import even if a future layer chooses to re-export it. + +## Module-level state and cycles + +- One owner each, all private to F: COLON_CONFUSABLES Set (:50), SEPARATOR_ENTITIES Map (:87), LETTER_CONFUSABLES Map (:101). They are initialized once and only read; do not duplicate/export the tables or turn them into per-call factories. INVISIBLE_FORMAT (:63) and NAMED_ENTITY_PLACEHOLDER (:80) move with them. +- Residual state stays together: CREDENTIAL_HEADER_LABEL_RAW (:41), derived CREDENTIAL_HEADER_LABEL (:43), SENSITIVE_KEY_PATTERN (:8), COLON_LABELLED_CREDENTIAL (:128), OTHER_FRAMED_CREDENTIALS (:141), SECRET_VALUE_PATTERNS (:412), USER_HOME_PATH_PATTERNS (:502), SENSITIVE_SEGMENT_PATTERN (:511), REDACTED_SECRET (:1). Global regex lastIndex writes in the masking loops remain with those regex owners; preserve resets at :215 and :365 and the sequential matching order. +- No top-level let, WeakMap, timer, lock or asynchronous initialization. foldForMatching's map, decoder and offsets are invocation-local, not module state. +- Intended graph is acyclic by construction: leaf has zero imports. In particular never import REDACTED_SECRET from the facade into F: the fold does not need it. The two callers retain matching/masking and byte-offset consumption; coupling is functional and sequential, not a shared mutable table API. +- The lab-boundary walker follows the added static edge automatically; no edits to its PROTECTED roots or scan list. + +## Tests + +Complete direct importing-test rg -l list (including dynamic import), each unchanged: + +| test file | import line | disposition | +|---|---:|---| +| tests/lib/redact.test.ts | 8 | unchanged, original path | +| tests/routing/fastwire-observability.test.ts | 6 | unchanged, original path | +| tests/web-search/web-search-backend-union.test.ts | 6 | unchanged, original path | +| tests/providers/github-copilot/github-copilot-oauth.test.ts | 339 | unchanged, dynamic original path | + +Discovery: rg -l 'src/lib/redact|lib/redact\.ts|redact\.ts' tests; inspect results for from/import/readFileSync/Bun.file/source. No direct filename-pinned source oracle exists. Transitive source oracle tests/lab/core-lab-boundary.test.ts reads every reachable module with readFileSync at :69, including this file; unchanged, automatically includes src/lib/redact-folding.ts through the new edge. Its direct-root reads at :278/:336 do not pin redact.ts. No retarget-to-leaf or add-leaf-to-scan-list edit is required. + +Guards to drive red once during implementation C, then restore and prove green: temporarily replace the leaf's COLON_CONFUSABLES with an empty Set and run tests/lib/redact.test.ts (the colon-confusable guard at :128 must fail). Temporarily introduce a static side-effect import of ../lab/paths into the new sibling leaf, run tests/lab/core-lab-boundary.test.ts (transitive guard :284 must fail), then remove it; never change the PROTECTED list. These are planned controlled mutations, not actions performed in this documentation task. Preserve existing byte/escape checks at redact.test.ts:143, :178, :360, :378 and :401. + +## Verification + +Future implementation only, at this layer tip in its dedicated worktree; local full suite is prohibited. Instantiate 002 as follows: + + bun run typecheck + bun test tests/lib/redact.test.ts tests/routing/fastwire-observability.test.ts tests/web-search/web-search-backend-union.test.ts tests/providers/github-copilot/github-copilot-oauth.test.ts + bun test tests/lib/debug.test.ts + bun run privacy:scan + bun test tests/lab/core-lab-boundary.test.ts + wc -l src/lib/redact.ts src/lib/redact-folding.ts + rg -n 'from "[^"]*/redact"' src gui/src scripts tests | wc -l + git diff --check + git diff --numstat dev...HEAD -- src/lib/redact.ts src/lib/redact-folding.ts + +Focused domains: lib, routing, web-search, providers/github-copilot; lab boundary is mandatory. Recorded static-from baseline for the command above: 55 matching lines. Recheck before/after and separately re-run the resolved importer census (56 files including dynamic imports); both must be unchanged. Compare the eight exports before/after and compare the moved function/table bodies with origin/dev, allowing only its export modifier. Verify the leaf has no imports with rg -n '^(import|export).*from|^import ' src/lib/redact-folding.ts (no matches expected); this proves the only new graph edge cannot return to the facade. Typecheck is not itself cycle proof. + +Full suite, only on lidge, using the 002 remote checkout procedure and preserving the real test exit status (do not pipe it into tail): + + ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lib-redact && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' + +Record remote SHA matching the PR head, complete test result/exit code, and exact-head GitHub CI rollup. The parent owns remote checkout coordination and all execution; this document author did not run these commands. + +## Accept criteria + +1. Exactly src/lib/redact-folding.ts is added; only the specified chunks move out of src/lib/redact.ts, plus the local import. No consumer/test path changes. +2. Inventory covers 28 declarations; six reside in F, 22 remain in R. All eight original exports remain at the original path; foldForMatching is not added to it. +3. Actual wc counts are at most 400 each (planned F=176, R=353); layer source churn is at most 500 changed lines. Any mismatch is reconciled before PR readiness. +4. Three lookup containers each have one owner; no import back to the facade, Lab, config, adapters or server from the leaf. +5. Focused checks, privacy scan, typecheck and lab boundary exit 0; both deliberate red drives fail for the expected assertion and return green after restoration. +6. Importer census remains 56 and moved bodies/comments are unchanged except export/import plumbing. Remote full suite and full exact-head CI rollup are green, with SHA recorded; no local full-suite run. +7. PR has the exact base and full repository template; no merge or release occurs. + +## PR + +Title: refactor(lib): isolate redaction lexical folding (split S01 L1/3) + +Branch: codex/split-lib-redact. Base: dev. Closes: none. + +Fill Summary, Verification, Checklist from .github/PULL_REQUEST_TEMPLATE.md; include this DEV-STACK-03 map, exact-tip results and pure-move thesis. Review this layer's diff only. PR numbers below are intentional pre-publication placeholders. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 3 | #TBD-S01-L3 | upstream retry | codex/split-lib-upstream-retry | dev | wait/body ownership | +| 2 | #TBD-S01-L2 | errors | codex/split-lib-errors | dev | message predicates | +| 1 | #TBD-S01-L1 | redact — this layer | codex/split-lib-redact | dev | folding and offset identity | + +Base: dev — no dependency on the layers below; no cascade obligation. + +## P stale-check (2026-09-05, wp010) + +origin/dev advanced past 445742966; `git diff --stat 445742966 origin/dev -- src/lib/redact.ts tests/lib/redact.test.ts` is empty, so every line range above is still exact. Plan audit (Ptolemy, gpt-6-astra high, 01a06edb-d0d7-7603-bb0d-96ca162c70ad) returned VERDICT: PASS with one citation nit: the first `lastIndex` reset is at redact.ts:214, not :215. Base for this layer is `dev` (S01 is an independent stack, 003 STACK-INDEPENDENCE-01). Executor rule learned at L105: never run `bun run test` or `bun scripts/test.ts` in the layer worktree; use `OCX_TEST_NO_QUEUE=1` for focused runs when another session holds the user test lock. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-010.7Jtzb1/wt` (branch `codex/split-lib-redact`, base origin/dev 4dde2db97). Executor: gpt-6-astra high (Dirac, 01a06f00-86a3-79b2-a538-dd26fe041cc5). +- Commits: 15907c6ff (move: redact-folding.ts 176 lines, redact.ts 353) and 5b253af7f (test: tests/lib/redact.test.ts +12 — folds a colon confusable with aligned offsets; leaf has no import line). Diff: 3 files, +190/−175; non-move diff = 1 import + 1 export modifier + 12 test lines. +- Local gate: typecheck 0; focused (redact, fastwire-observability, web-search-backend-union, github-copilot-oauth, debug) 118 pass / 0 fail; core-lab-boundary 17 pass / 0 fail; privacy scan passed; leaf zero imports. +- Red-drives: (a) empty COLON_CONFUSABLES → redact.test.ts 41 pass / 2 fail (colon look-alike guard + new leaf test), restored 43/0; (b) leaf importing ../lab/paths → core-lab-boundary 13 pass / 4 fail with chain `src/router.ts -> src/lib/redact.ts -> src/lib/redact-folding.ts -> src/lab/paths.ts`, restored 60/0 combined. +- Pushed: origin/codex/split-lib-redact = 5b253af7f. + +- Adversarial diff review (Hubble, gpt-6-astra high, 01a06f03-583c-7490-b9e7-7a85ca8b3935): VERDICT: PASS first round (slice diffs empty, exact residual reconstruction, 8 exports preserved, foldForMatching private, test non-tautological, 3 files). +- C receipt at 5b253af7f: typecheck 0, redact+lab-boundary 60 pass / 0 fail, privacy 0, DIRTY 0. +- lidge full suite at 5b253af7f: SUITE_EXIT=0, 18014 pass / 0 fail / 16 skip (/tmp/suite-split-lib-redact.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3559 (base dev, head 5b253af7f). CI rollup at record time: OPEN draft=false 5b253af7f select windows runner:SUCCESS resolve-pr:SUCCESS resolve-pr:SUCCESS label:SUCCESS label:SUCCESS hygiene:SUCCESS hygiene:SUCCESS react-doctor:SUCCESS enforce-target: changes:SUCCESS enforce-target:SUCCESS windows ${{ matrix.shard }}/4:SKIPPED test 1/4:SUCCESS test 2/4: test 3/4: test 4/4: storage policy:SUCCESS api usage:SUCCESS gates:SUCCESS macos 1/2: macos 2/2: macos control:SKIPPED keyring ubuntu: keyring windows:SUCCESS keyring macos: npm-global ubuntu-latest:SUCCESS npm-global windows-latest: npm-global macos-latest: CodeRabbit: diff --git a/devlog/_plan/260905_now_split_train/020_lib_errors.md b/devlog/_plan/260905_now_split_train/020_lib_errors.md new file mode 100644 index 0000000000..30e84a55c4 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/020_lib_errors.md @@ -0,0 +1,140 @@ +# S01 L2/3 — Error message predicates + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Class: C3 boundary planning, docs-only here. +- Goal: extract the message-predicate owner into src/lib/error-message-predicates.ts (123 lines) and leave src/lib/errors.ts at 338 lines without changing classification precedence or payloads. +- Non-goals: no new error code, status, message matching, signature, dependency, renamed export, caller migration or unrelated classifier cleanup. Existing >50-line classification functions remain a stated pure-move exception. +- Verifier: 002_layer_map.md "Per-layer gate", instantiated below. No tests run in this drafting task. +- Stop: standalone layer verified at its tip, open PR with exact-head green CI/evidence recorded by the parent; never merge. +- Escalation: source drift, altered precedence/output, new cycle, unknown oracle, >500 changed source lines, or a required scope expansion. Any unreleased security finding belongs in ignored scratch, not this plan. + +Basis: docs HEAD 4cc219549; origin/dev 1362b1a38, identical working-tree source for this file. Older tips in 000/001 are historical. Lane: 016_lane_cli_storage_usage_update_lab_scripts.md:682–695 in the modular-debt-ledger unit. + +Structural map: 22 direct importing files. Examples src/lib/retry-after.ts:2 and src/bridge.ts:17; current errors.ts has no imports. classifyError (:149), inferHttpStatusFromAdapterMessage (:332), and httpStatusFromTerminalError (:412) share the predicates at :29–147. Intended graph: existing consumers → errors.ts → error-message-predicates.ts, with no imports in the leaf. Public boundary is errors.ts; blast radius is the lib module with unchanged downstream error handling. + +Decision: extract all shared message predicates and their policy constants together, leaving status/payload composition in the original owner. Doing nothing leaves 457 lines; deleting/configuring changes policy; extracting classifyError alone would require separate shared types and predicates to avoid return imports. rg for isSubscriptionGateMessage and related predicate names in src/lib identifies this owner, not a reusable alternative. Do not move parseRetryAfterFromMessage into src/lib/retry-after.ts: that module already imports errors.ts:2, creating a return edge. Domain-named sibling convention matches src/lib/provider-url.ts, src/lib/retry-after.ts and src/lib/debug-settings.ts. Retaining named compatibility re-exports is explicitly required by this train; no new internal index barrel is added. + +## Symbol inventory + +Inclusive ranges at origin/dev:src/lib/errors.ts; rg top-level declarations and source closing lines establish exact ranges. Consumers are distinct external importing files with an exact rg -l -w symbol match, not call counts. Candidate rg -l errors src gui/src scripts tests is filtered by resolved relative static/dynamic import and mock paths. Private names have zero external consumers. No original imports. R = src/lib/errors.ts; P = src/lib/error-message-predicates.ts. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| OcxErrorPayload | interface | 1–5 | yes | 1 | R | +| upstreamErrorMessageFromPayload | function | 8–23 | yes | 2 | R | +| CYBER_POLICY_ERROR_CODE | const string | 26–26 | yes | 9 | P | +| CYBER_POLICY_FALLBACK_MESSAGE | const string | 27–27 | yes | 2 | P | +| isCyberPolicyCode | function | 29–31 | yes | 12 | P | +| cyberPolicyErrorType | function | 34–37 | yes | 6 | P | +| isCyberPolicyMessage | function | 45–56 | yes | 7 | P | +| isSubscriptionGateMessage | function | 58–69 | no; leaf-only export after move | 0 | P | +| isLocalAclHardeningMessage | function | 71–88 | no; leaf-only export after move | 0 | P | +| isAuthenticationMessage | function | 90–116 | no; leaf-only export after move | 0 | P | +| isPermissionMessage | function | 118–128 | no; leaf-only export after move | 0 | P | +| isClientClosedMessage | function | 138–147 | yes | 1 | P | +| classifyError | function | 149–290 | yes | 12 | R | +| isRateLimitOrQuotaFailureMessage | function | 296–313 | yes | 3 | R | +| parseRetryAfterFromMessage | function | 316–329 | yes | 2 | R | +| inferHttpStatusFromAdapterMessage | function | 332–382 | yes | 2 | R | +| adapterFailureFromMessage | function | 385–409 | yes | 5 | R | +| httpStatusFromTerminalError | function | 412–457 | yes | 4 | R | + +## Leaf partition + +One new sibling: src/lib/error-message-predicates.ts. + +- Symbols: CYBER_POLICY_ERROR_CODE, CYBER_POLICY_FALLBACK_MESSAGE, isCyberPolicyCode, cyberPolicyErrorType, isCyberPolicyMessage, isSubscriptionGateMessage, isLocalAclHardeningMessage, isAuthenticationMessage, isPermissionMessage, isClientClosedMessage. +- Own imports: none. The policy constant is colocated with its users. Add export modifiers to the four formerly private predicates strictly for the residual's production calls; do not expose those four through errors.ts. +- Move exact source chunk src/lib/errors.ts:25–147, including comments and blank lines: 123 lines. Leaf remains 123 lines; modifiers do not add physical lines. +- Residual keeps OcxErrorPayload, payload parsing, classification, retry-after parsing, status inference and adapter/terminal composition. Prepend the import, blank line, one re-export line, blank line below: 457 − 123 + 4 = 338 lines. +- Total layout: 123 + 338 = 461, four import/export/layout lines above the baseline. Source churn expected 127 additions + 123 deletions = 250, below 500. No #b needed. Shared zero-external-consumer predicates move with the public predicates they support; callers do not migrate. + +## Re-export block + +Exact new named compatibility export (six original exports): + + export { CYBER_POLICY_ERROR_CODE, CYBER_POLICY_FALLBACK_MESSAGE, isCyberPolicyCode, cyberPolicyErrorType, isCyberPolicyMessage, isClientClosedMessage } from "./error-message-predicates"; + +No type re-export is needed: OcxErrorPayload remains defined/exported in errors.ts. Retain its seven other exported functions unchanged: upstreamErrorMessageFromPayload, classifyError, isRateLimitOrQuotaFailureMessage, parseRetryAfterFromMessage, inferHttpStatusFromAdapterMessage, adapterFailureFromMessage, httpStatusFromTerminalError. + +Exact explicit local import, before the export with a blank line between and after: + + import { CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, isCyberPolicyMessage, isClientClosedMessage, isSubscriptionGateMessage, isLocalAclHardeningMessage, isAuthenticationMessage, isPermissionMessage } from "./error-message-predicates"; + +The re-export does not bind identifiers. classifyError needs CYBER_POLICY_ERROR_CODE and six message predicates; status inference/terminal mapping also need these imports. CYBER_POLICY_FALLBACK_MESSAGE and cyberPolicyErrorType have no residual local calls and are re-exported without unused imports. + +## Module-level state and cycles + +- No top-level let, Map, Set, WeakMap, lock, timer or other mutable singleton exists in the original or proposed leaf. +- CYBER_POLICY_ERROR_CODE (:26) and CYBER_POLICY_FALLBACK_MESSAGE (:27) have exactly one owner, P. They remain identical immutable strings, re-exported without redeclaration. +- Predicate regular expressions are function-local, as before. parseRetryAfterFromMessage's patterns (:317) are also invocation-local and remain in R; do not hoist them as incidental cleanup. +- No leaf-to-facade import, including type-only imports. P needs neither OcxErrorPayload nor classifyError; R keeps their ownership. This avoids errors.ts → P → errors.ts and errors.ts → retry-after.ts → errors.ts cycles. +- New edges are functional predicate calls. Existing classification precedence is not moved or reordered. No initialization ordering dependency is introduced. P has no imports, so the new edge cannot create a cycle or reach Lab. + +## Tests + +Complete direct importing-test list from rg -l 'src/lib/errors|lib/errors\.ts' tests: + +| test file | import line | disposition | +|---|---:|---| +| tests/lib/acl-error-classification.test.ts | 5 | unchanged, original path | +| tests/providers/cursor/cursor-errors.test.ts | 8 | unchanged, original path | +| tests/providers/cyber-policy-error-fidelity.test.ts | 15 | unchanged, original path | +| tests/server/errors-adapter-failure.test.ts | 6 | unchanged, original path | +| tests/server/error-fidelity.test.ts | 3 | unchanged, original path | +| tests/server/server-403-permission-e2e.test.ts | 8 | unchanged, original path | + +Qualified-path search plus rg -n 'errors\.ts' tests finds no direct source reader for this file (the kiro-errors.ts comment is unrelated). Transitive source reader tests/lab/core-lab-boundary.test.ts:69 follows the static graph; unchanged and automatically includes P. No retarget-to-leaf or add-leaf-to-scan-list is required. Public-path behavioral tests must not import the new private predicate leaf merely to satisfy coverage. + +Guards to drive red once in implementation C: temporarily make isLocalAclHardeningMessage return false and run tests/lib/acl-error-classification.test.ts; its local-hardening case at :8 must fail. Restore. Temporarily add a static ../lab/paths import to P, run tests/lab/core-lab-boundary.test.ts and observe the transitive guard :284 fail; remove and return green without editing PROTECTED. Existing mixed authentication/subscription precedence tests/server/errors-adapter-failure.test.ts:55 and narrow client-close case :93 must remain unchanged and green. No red mutation is performed by this drafting task. + +## Verification + +Future executor commands, instantiating 002 at this layer tip, in its dedicated worktree: + + bun run typecheck + bun test tests/lib/acl-error-classification.test.ts tests/providers/cursor/cursor-errors.test.ts tests/providers/cyber-policy-error-fidelity.test.ts tests/server/errors-adapter-failure.test.ts tests/server/error-fidelity.test.ts tests/server/server-403-permission-e2e.test.ts + bun run privacy:scan + bun test tests/lab/core-lab-boundary.test.ts + wc -l src/lib/errors.ts src/lib/error-message-predicates.ts + rg -n 'from "[^"]*/errors"' src gui/src scripts tests | wc -l + git diff --check + git diff --numstat origin/dev...HEAD -- src/lib/errors.ts src/lib/error-message-predicates.ts + +Domains: lib, providers/cursor, providers, server, plus mandatory lab boundary. Recorded static-from baseline: 33 matching lines (this basename-only gate includes other errors modules); compare before/after and also compare the path-resolved importer census to 22. No existing consumer is rewritten. Compare all 14 public exports (13 runtime, one type) across the move. Confirm P imports nothing with rg -n '^(import|export).*from|^import ' src/lib/error-message-predicates.ts (no matches), and compare every moved body/comment allowing only four export modifiers. Combined with the one outward facade edge, zero leaf imports prove no new cycle. + +Remote full suite only, preserving the actual exit status rather than hiding it behind tail: + + ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lib-errors && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' + +Parent coordinates the remote checkout, verifies the printed SHA equals this PR's head and records full-suite exit/results plus exact-head CI rollup. Do not run a local full suite. These are planned gates, not test claims for this docs-only task. + +## Accept criteria + +1. Exactly one new source leaf, P, owns the 10 listed declarations; eight declarations remain in R. No function-body, condition-order, code/message or signature changes. +2. Six moved public exports resolve from errors.ts; its eight retained exports remain intact. Four new leaf-only predicate exports are not re-exported by the original boundary. +3. wc shows at most 400 lines per file, planned P=123 and R=338; source churn stays at most 500 changed lines. Count drift is explained before readiness. +4. P has zero imports and no mutable state; policy constants are defined once. OcxErrorPayload remains in R and creates no reverse type edge. +5. All focused checks, typecheck, privacy scan and lab boundary exit 0; both deliberate red drives fail at the intended assertion and pass after restoration. +6. Original-path importer census stays 22; remote full suite and full exact-head CI are green and bound to the recorded PR head. No local full-suite execution. +7. Base is the current L1 branch, ancestry includes its current tip, repository PR template is complete, and nothing is merged or released. + +## PR + +Title: refactor(lib): isolate error message predicates (split S01 L2/3) + +Branch: codex/split-lib-errors. Base: dev. Closes: none. + +Use .github/PULL_REQUEST_TEMPLATE.md Summary, Verification, Checklist sections. Include this DEV-STACK-03 table and exact-head gate evidence. Review only this layer's diff. PR numbers are intentional pre-publication placeholders. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 3 | #TBD-S01-L3 | upstream retry | codex/split-lib-upstream-retry | dev | wait/body ownership | +| 2 | #TBD-S01-L2 | errors — this layer | codex/split-lib-errors | dev | message predicates | +| 1 | #TBD-S01-L1 | redact | codex/split-lib-redact | dev | folding and offset identity | + +Base: dev — no dependency on the layers below; no cascade obligation. diff --git a/devlog/_plan/260905_now_split_train/030_lib_upstream_retry.md b/devlog/_plan/260905_now_split_train/030_lib_upstream_retry.md new file mode 100644 index 0000000000..4b12305a32 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/030_lib_upstream_retry.md @@ -0,0 +1,164 @@ +# S01 L3/3 — Abort-aware retry waits + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Class: C3 boundary planning, docs-only here. +- Goal: move abort-aware waiting and bounded pre-wait body release into src/lib/upstream-retry-wait.ts (125 lines), leaving src/lib/upstream-retry.ts at 309 lines with its send-budget owner/oracle intact. +- Non-goals: no retry policy, delay, cancellation ordering, error attribution, heartbeat, deadline, status or signature changes. No new dependencies or caller migration. No resetting/multiplying budgets, and no cleanup of existing long functions. +- Verifier: 002_layer_map.md "Per-layer gate", instantiated below; this author runs no tests. +- Stop: independent layer verified at its own tip, open PR and exact-head green CI recorded by the parent; no merge. +- Escalation: source drift, an unlisted oracle/mock dependency, required budget move, altered timer/abort behavior, cycle, >500 source churn or write-scope expansion. Record unreleased security findings only in ignored scratch. + +Basis: docs HEAD 4cc219549 and source origin/dev 1362b1a38, working-tree source identical for this file. Lane evidence: 016_lane_cli_storage_usage_update_lab_scripts.md:768–781 in the modular-debt-ledger unit. 000/001's older tip annotations do not override this refreshed source basis. + +Structural map: 25 direct importing files. Examples src/lib/upstream-reachability.ts:27 and src/web-search/anthropic-executor.ts:7. Current dependency is clearableDeadline from ./abort (src/lib/upstream-retry.ts:17). The wait group (:49–173) is self-contained. Intended direction: preserved callers → upstream-retry.ts → upstream-retry-wait.ts; upstream-retry.ts → abort.ts remains. The new leaf imports nothing. Public boundary is the existing retry path; blast radius is lib with preserved adapter/server/web-search consumers. + +Decision: move the cohesive body-release/sleep/heartbeat group, retaining all retry orchestration and evidence types/classes. Doing nothing leaves 429 lines; deleting/configuring changes behavior. Existing src/lib/bounded-body.ts reads bounded data rather than performing pre-replay cancellation, and src/lib/abort.ts owns deadline/signal composition rather than heartbeat generators; reuse would conflate contracts. rg for releaseResponseBodyBestEffort and sleepWithHeartbeats in src/lib confirms this owner. Sibling names match src/lib/upstream-reachability.ts, src/lib/upstream-http-version.ts and src/lib/bounded-body.ts. No new index barrel. This minimal move avoids retargeting the source-checked retry budget. + +## Symbol inventory + +Inclusive origin/dev:src/lib/upstream-retry.ts ranges from rg declaration/closing-line inspection. Consumer counts: distinct importing files found by resolving relative static/dynamic/mock specifiers from rg -l upstream-retry src gui/src scripts tests, then rg -l -w per symbol; lexical file references, not call counts. Private names have zero external consumers. R = original src/lib/upstream-retry.ts; W = src/lib/upstream-retry-wait.ts. The import binding is included separately from the 28 top-level declarations. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| clearableDeadline | import binding from ./abort | 17–17 | no | 0 | R (existing dependency) | +| RESET_RETRY_MAX_ATTEMPTS | const number | 20–20 | no | 0 | R | +| RESET_RETRY_BASE_DELAY_MS | const number | 21–21 | no | 0 | R | +| RESET_RETRY_MAX_DELAY_MS | const number | 22–22 | no | 0 | R | +| TRANSIENT_RETRY_MAX_ATTEMPTS | const number | 25–25 | no | 0 | R | +| TRANSIENT_RETRY_BASE_DELAY_MS | const number | 26–26 | no | 0 | R | +| TRANSIENT_RETRY_MAX_DELAY_MS | const number | 27–27 | no | 0 | R | +| TRANSIENT_RETRY_SLOW_ATTEMPT_MS | const number | 30–30 | no | 0 | R | +| isTransientUpstreamStatus | function | 38–41 | yes | 3 | R | +| RetryBackoffOptions | interface | 43–47 | yes | 0 | R | +| abortError | function | 49–51 | yes | 3 | W | +| sleepWithAbort | async function | 53–72 | yes | 3 | W | +| releaseResponseBodyBestEffort | async function | 84–120 | yes | 1 | W | +| sleepWithHeartbeats | async generator | 129–146 | yes | 1 | W | +| SameTarget429WaitOptions | interface | 148–157 | yes | 0 | W | +| prepareSameTarget429Wait | async generator | 164–173 | yes | 5 | W | +| isConnectionResetError | function | 175–184 | yes | 2 | R | +| retryAfterDelayMs | function | 186–194 | no | 0 | R | +| retryBackoffDelayMs | function | 196–201 | yes | 4 | R | +| cancelResponseBodyBestEffort | function | 203–210 | yes | 2 | R | +| fetchWithAttemptDeadline | async function | 212–236 | yes | 2 | R | +| ResetRetryOptions | interface | 238–244 | yes | 0 | R | +| TransientRetryOptions | interface | 246–255 | yes | 0 | R | +| UpstreamSendRecovery | type | 257–257 | yes | 2 | R | +| ReplayableFetch | type | 258–258 | no | 0 | R | +| UpstreamRetryEvidenceError | class | 272–291 | yes | 2 | R | +| applyUpstreamRecoveryInit | function | 302–312 | yes | 14 | R | +| fetchWithResetRetry | async function | 319–353 | yes | 16 | R | +| fetchWithTransientRetry | async function | 366–429 | yes | 8 | R | + +## Leaf partition + +One new sibling: src/lib/upstream-retry-wait.ts. + +- Symbols: abortError, sleepWithAbort, releaseResponseBodyBestEffort, sleepWithHeartbeats, SameTarget429WaitOptions, prepareSameTarget429Wait; retain their existing export modifiers. +- Own imports: none. DOMException, timers, AbortSignal, ReadableStream and AsyncGenerator remain standard globals/types. The options interface moves beside its only declaring function; no return type edge to the facade. +- Move exact source chunk src/lib/upstream-retry.ts:49–173 (125 lines), with all internal comments/spacing unchanged. Expected leaf 125 lines. +- Residual retains all other source and the existing clearableDeadline import. Add the three one-line import/re-export statements plus two blank lines below: 429 − 125 + 5 = 309 lines. Total layout 125 + 309 = 434 (five added plumbing/layout lines). +- Expected source churn: 130 additions + 125 deletions = 255; no #b required. Highest-consumer fetch/recovery symbols remain untouched. cancelResponseBodyBestEffort (:203) intentionally stays with retry orchestration; it is the non-waiting cancellation variant, not the bounded pre-429 release helper. + +## Re-export block + +Exact named value and type exports: + + export { abortError, sleepWithAbort, releaseResponseBodyBestEffort, sleepWithHeartbeats, prepareSameTarget429Wait } from "./upstream-retry-wait"; + export type { SameTarget429WaitOptions } from "./upstream-retry-wait"; + +Exact explicit local import: + + import { abortError, sleepWithAbort } from "./upstream-retry-wait"; + +Insert the local import after the existing clearableDeadline import, then a blank line, the two export lines, then a blank line; retain original spacing otherwise. Re-export statements bind nothing: fetchWithResetRetry still calls abortError (:328) and sleepWithAbort (:346), and fetchWithTransientRetry calls sleepWithAbort (:413). + +All other original exports remain declarations in R: isTransientUpstreamStatus, RetryBackoffOptions, isConnectionResetError, retryBackoffDelayMs, cancelResponseBodyBestEffort, fetchWithAttemptDeadline, ResetRetryOptions, TransientRetryOptions, UpstreamSendRecovery, UpstreamRetryEvidenceError, applyUpstreamRecoveryInit, fetchWithResetRetry, fetchWithTransientRetry. ReplayableFetch remains private. + +## Module-level state and cycles + +- No top-level let, Map, Set, WeakMap, lock or timer exists. Seven numeric policy constants remain owned by R: RESET_RETRY_MAX_ATTEMPTS (:20), RESET_RETRY_BASE_DELAY_MS (:21), RESET_RETRY_MAX_DELAY_MS (:22), TRANSIENT_RETRY_MAX_ATTEMPTS (:25), TRANSIENT_RETRY_BASE_DELAY_MS (:26), TRANSIENT_RETRY_MAX_DELAY_MS (:27), TRANSIENT_RETRY_SLOW_ATTEMPT_MS (:30). +- Timers in sleepWithAbort (:57) and releaseResponseBodyBestEffort (:100), listeners and heartbeat remaining count (:139) are invocation-local; move with W without hoisting, duplicating or changing cleanup. The un-signalled release branch (:96) retains its exact existing timer behavior; this is not a cleanup patch. +- fetchWithTransientRetry's transientStatuses (:372), sent (:380), countedFetch (:381), remaining (:389), attemptStart (:394) and final onSendsConsumed (:427) stay call-local in R. UpstreamRetryEvidenceError stays one class constructor in R, preserving instanceof identity and mock behavior. +- W must not import R, abort.ts, adapters or server modules. R → W is functional; prepareSameTarget429Wait's body release → sleep/heartbeat sequence stays wholly inside W. No new shared mutable state or cycle; the only new target has no outgoing imports. The existing R → abort.ts edge is unchanged. + +## Tests + +Complete direct importing-test list from rg -l 'src/lib/upstream-retry|lib/upstream-retry\.ts' tests, separated from the one source-only result: + +| test file | import/pin line | disposition | +|---|---:|---| +| tests/lib/upstream-retry.test.ts | 9 | unchanged, original public path | +| tests/providers/upstream-transient-retry.test.ts | 2 | unchanged, original public path | +| tests/codex-integration/issue-914-transport-attribution.test.ts | 18 | unchanged, original public path | +| tests/codex-integration/upstream-reachability.test.ts | 8 | unchanged, original public path/class identity | +| tests/server/server-combo-failover-e2e.test.ts | 47 and 105 | unchanged dynamic import and mock.module path | + +Source oracles and disposition: + +| test file | exact read site | disposition | +|---|---|---| +| tests/lib/transient-budget-scope-source.test.ts | source("lib/upstream-retry.ts") at :48, readFileSync implementation at :7 | unchanged; TransientRetryOptions and fetchWithTransientRetry remain in R; assertions :49 and :51 retain full strength | +| tests/lab/core-lab-boundary.test.ts | readFileSync(current, "utf8") at :69 | unchanged; transitive walker automatically follows R → W; no scan-list addition | + +The server-combo-failover-e2e mock at :105 is not a source-text reader: it spreads actualRetry captured by import at :47 and overrides fetchWithTransientRetry. Keep both import and mocked function in the original path; no retargeting to W. No oracle needs retarget-to-leaf or add-leaf-to-scan-list. Do not weaken or combine the source assertions merely because another part of the file moved. + +Guards to drive red once in implementation C, restore then prove green: + +- Remove the onSendsConsumed call from R's finally block temporarily; tests/lib/transient-budget-scope-source.test.ts:51 must fail. Restore; the guarded function is not moved. +- In W temporarily remove releaseResponseBodyBestEffort's signal.addEventListener("abort", onAbort, { once: true }) statement (original :113); tests/lib/upstream-retry.test.ts:113 must fail because the aborted wait no longer settles promptly (the 60-second release deadline exceeds the test timeout). Restore without changing source logic in the final diff. +- Temporarily add a static ../lab/paths import to W; tests/lab/core-lab-boundary.test.ts:284 must fail through the existing protected-root graph. Remove; never edit PROTECTED. + +Keep behavioral wait tests at tests/lib/upstream-retry.test.ts:67, :76, :98, :113, :127, :268 and :288 unchanged. No guard was run or mutated during drafting. + +## Verification + +Implementation-only instantiation of 002 in this layer's dedicated worktree: + + bun run typecheck + bun test tests/lib/upstream-retry.test.ts tests/lib/transient-budget-scope-source.test.ts tests/providers/upstream-transient-retry.test.ts tests/codex-integration/issue-914-transport-attribution.test.ts tests/codex-integration/upstream-reachability.test.ts + bun test tests/server/server-combo-failover-e2e.test.ts + bun run privacy:scan + bun test tests/lab/core-lab-boundary.test.ts + wc -l src/lib/upstream-retry.ts src/lib/upstream-retry-wait.ts + rg -n 'from "[^"]*/upstream-retry"' src gui/src scripts tests | wc -l + git diff --check + git diff --numstat origin/dev...HEAD -- src/lib/upstream-retry.ts src/lib/upstream-retry-wait.ts + +Domains: lib, providers, codex-integration and server, plus mandatory lab boundary. Run the mock-heavy server-combo-failover-e2e file in its separate Bun process as shown. Recorded static-from baseline: 25 matching lines. Compare before/after and separately confirm the resolved importer census remains 25 distinct files, including dynamic/mock paths; these are different measures despite the equal totals. Compare all 19 original exports (14 runtime and five types) and verbatim moved bodies/comments. Require no imports in W via rg -n '^(import|export).*from|^import ' src/lib/upstream-retry-wait.ts (no matches); with R's single new outward edge this rules out any new cycle. Preserve R's original abort dependency. + +Full suite only on lidge, with true exit status retained instead of a tail pipeline: + + ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lib-upstream-retry && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' + +Parent coordinates the remote checkout and captures printed SHA, full-suite result/exit and exact-head CI rollup. Bind all results to the current PR head. No local full-suite run; no test execution by this document author. + +## Accept criteria + +1. Only W is added; the six wait-group declarations move verbatim from :49–173. Remaining 22 declarations and the abort import stay in R. +2. All 19 original exports remain importable at the original path. Five value and one type re-exports are explicit; the two residual helper calls have real local bindings. UpstreamRetryEvidenceError identity is unchanged. +3. Actual files are at most 400 lines, planned W=125 and R=309; source churn at most 500. Any formatting/count deviation is reconciled before readiness; no #b debt remains. +4. All seven numeric constants and the request-wide send budget retain their single owner. No moved timer/listener becomes module state, no return import or new cycle exists. +5. The budget source reader and server mock retain their original paths/assertions. All three planned red drives fail as expected and return green after restoration. +6. Focused suites, typecheck, privacy and lab boundary exit 0; original importer census remains 25. Full remote suite and full exact-head CI rollup are green at the recorded PR head; no local full-suite run. +7. PR base and ancestry match current L2, full template and stack map are present, and no merge/release is performed. + +## PR + +Title: refactor(lib): isolate abort-aware retry waits (split S01 L3/3) + +Branch: codex/split-lib-upstream-retry. Base: dev. Closes: none. + +Fill .github/PULL_REQUEST_TEMPLATE.md Summary, Verification and Checklist; include exact-head results and this DEV-STACK-03 map. Review this layer's diff only. PR numbers are intentional pre-publication placeholders. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 3 | #TBD-S01-L3 | upstream retry — this layer | codex/split-lib-upstream-retry | dev | wait/body ownership | +| 2 | #TBD-S01-L2 | errors | codex/split-lib-errors | dev | message predicates | +| 1 | #TBD-S01-L1 | redact | codex/split-lib-redact | dev | folding and offset identity | + +Base: dev — no dependency on the layers below; no cascade obligation. diff --git a/devlog/_plan/260905_now_split_train/040_providers_openai_tiers.md b/devlog/_plan/260905_now_split_train/040_providers_openai_tiers.md new file mode 100644 index 0000000000..44312f0514 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/040_providers_openai_tiers.md @@ -0,0 +1,179 @@ +# 040 — S02 providers L1/4 + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Classification: C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture determine size, compatibility and state ownership; parent owns loop/goal/orchestration. +- Goal: separate OpenAI destination classification, preserving every historical export and observable behavior of `src/providers/openai-tiers.ts`. +- Non-goals: no model refresh, endpoint/auth-policy changes, validation redesign, caching, new runtime dependency, bug fix, generated metadata rewrite, repository-wide local test, merge, release or deployment. Existing behavior stays literal, including comments explaining it. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current task verifies documentation only, not runtime correctness. +- Stop: this plan is complete when the inventory, ownership, exact wiring, test disposition and count ledger are consistent; execution stops only after its own tip passes the instantiated gate and records exact-head CI. Do not defer a failing layer upward. +- Escalation: any required source write outside the target/new leaf, semantic delta, new cycle, or missing gate evidence returns to the parent. + +Structural decision: the 416-line module combines destination predicates with a 301-line migration area. Move destination constants/predicates into one co-located leaf; leave migration helpers, projection type and collision class together. Rejected alternatives: doing nothing/configuring cannot meet the line limit; deleting declarations would change behavior; changing all consumer imports would widen churn; a new provider framework or generic utils barrel is unnecessary. Existing `src/types.ts → src/types/*`, `src/config/*.ts`, and `src/codex/catalog.ts → src/codex/catalog/*` establish kebab-case co-located leaf convention. Keep legacy facades as explicit compatibility boundaries; no new index.ts or export-star barrel. + +## Symbol inventory + +Basis: `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549`. Every range in this document is an original-source line range, not the intermediate branch's shifted coordinates. `git diff origin/dev -- src/providers/openai-tiers.ts` was empty. + +Ranges were measured with `sg run --lang typescript --kind --json=compact src/providers/openai-tiers.ts`, taking column-zero export/lexical/function/interface/type-alias/class declarations. Imports are listed separately below; the inventory does not confuse nested declarations with ESM state. + +Consumer count = distinct `rg -l -w ''` files among resolved static/dynamic importers of this exact module under `src gui/src scripts tests` (`*.ts`/`*.tsx`), excluding the defining file. This is textual fan-in within the importer set, not call frequency. Private symbols have zero external import consumers; coincident names/comments elsewhere are excluded. Importer discovery starts with `rg -l 'openai-tiers' src gui/src scripts tests` and resolves each relative specifier, so other registries do not count. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `OPENAI_CODEX_PROVIDER_ID` | const | 6–6 | yes | 30 | openai-tiers/destination.ts (L1) | +| `LEGACY_OPENAI_MULTI_PROVIDER_ID` | const | 7–7 | yes | 3 | openai-tiers/destination.ts (L1) | +| `OPENAI_API_PROVIDER_ID` | const | 8–8 | yes | 10 | openai-tiers/destination.ts (L1) | +| `LEGACY_CHATGPT_PROVIDER_ID` | const | 9–9 | yes | 2 | openai-tiers/destination.ts (L1) | +| `CODEX_FORWARD_BASE_URL` | const | 11–11 | yes | 8 | openai-tiers/destination.ts (L1) | +| `LEGACY_OPENAI_MULTI_PREFIX` | const | 12–12 | no | 0 | residual original file | +| `canonicalCodexForwardProvider` | function | 14–21 | no | 0 | residual original file | +| `normalizedBaseUrl` | function | 23–32 | no | 0 | openai-tiers/destination.ts (L1) | +| `isCanonicalOpenAiForwardProvider` | function | 34–38 | yes | 35 | openai-tiers/destination.ts (L1) | +| `OPENAI_API_ORIGIN` | const | 40–40 | no | 0 | openai-tiers/destination.ts (L1) | +| `OPENAI_API_BASE_URL` | const | 41–41 | no | 0 | openai-tiers/destination.ts (L1) | +| `OPENAI_API_RESPONSES_URL` | const | 42–42 | no | 0 | openai-tiers/destination.ts (L1) | +| `resolvedResponsesEndpoint` | function | 53–62 | no | 0 | openai-tiers/destination.ts (L1) | +| `isOfficialOpenAiResponsesDestination` | function | 64–68 | no | 0 | openai-tiers/destination.ts (L1) | +| `supportsNativeResponsesCompactEndpoint` | function | 76–84 | yes | 2 | openai-tiers/destination.ts (L1) | +| `isOpenAiOperatedResponsesDestination` | function | 94–98 | yes | 2 | openai-tiers/destination.ts (L1) | +| `destinationDecodesNativeCompactionBlob` | function | 111–114 | yes | 1 | openai-tiers/destination.ts (L1) | +| `OpenAiTierMigrationProjection` | interface | 116–121 | yes | 0 | residual original file | +| `OpenAiTierMigrationCollisionError` | class | 123–130 | yes | 2 | residual original file | +| `managedLegacyMultiOverlay` | function | 132–152 | no | 0 | residual original file | +| `validLegacyOverlayCosts` | function | 155–168 | no | 0 | residual original file | +| `rewriteLegacyOpenAiSelectedId` | function | 170–174 | no | 0 | residual original file | +| `rewriteLegacyOpenAiModelList` | function | 176–179 | no | 0 | residual original file | +| `rewriteLegacyOpenAiCostKeys` | function | 186–201 | no | 0 | residual original file | +| `mergeLegacyOpenAiProviderRows` | function | 203–229 | no | 0 | residual original file | +| `hasKnownLegacyOpenAiReference` | function | 231–251 | no | 0 | residual original file | +| `rewriteLegacyOpenAiReferences` | function | 253–292 | no | 0 | residual original file | +| `isKnownLegacyValuePath` | function | 294–313 | no | 0 | residual original file | +| `unknownLegacyOpenAiWarnings` | function | 315–335 | no | 0 | residual original file | +| `resolvedOpenAiMode` | function | 337–354 | no | 0 | residual original file | +| `projectOpenAiTierMigration` | function | 356–416 | yes | 4 | residual original file | + +## Leaf partition + +NEW `src/providers/openai-tiers/destination.ts`: destination identity/classification only. Move original ranges **6–11 and 23–114** (98 lines); 101 expected lines including the following two imports and one separator. All 15 symbols listed below are owned here: + +`OPENAI_CODEX_PROVIDER_ID`, `LEGACY_OPENAI_MULTI_PROVIDER_ID`, `OPENAI_API_PROVIDER_ID`, `LEGACY_CHATGPT_PROVIDER_ID`, `CODEX_FORWARD_BASE_URL`, `normalizedBaseUrl`, `isCanonicalOpenAiForwardProvider`, `OPENAI_API_ORIGIN`, `OPENAI_API_BASE_URL`, `OPENAI_API_RESPONSES_URL`, `resolvedResponsesEndpoint`, `isOfficialOpenAiResponsesDestination`, `supportsNativeResponsesCompactEndpoint`, `isOpenAiOperatedResponsesDestination`, `destinationDecodesNativeCompactionBlob`. + +Own imports: + +```ts +import type { OcxProviderConfig } from "../../types"; +import { openaiResponsesUrl } from "../../adapters/openai-responses-url"; +``` + +MODIFY `src/providers/openai-tiers.ts`: retain prefix at 12, canonical provider factory at 14–21, and the full migration area at 116–416. Expected residual **321 lines** using four import statements + separator + one re-export statement + separator + the 314 original retained body/blank lines (5–416 minus the 98 moved lines). No #b is needed. The migration class/type stay exported in place; the factory is private and imports destination constants directly. Physical accounting: 416 − 98 moved − 4 original header lines + 7 replacement header/separator lines = 321; preserve the original body and verify actual wc at implementation. + +## Re-export block + +The nine moved runtime exports remain importable from `src/providers/openai-tiers.ts`. Its three other exports (projection type, collision class, migration function) stay as their original declarations. Re-exporting does not create local bindings, so use the separate import below. + +```ts +import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types"; +import { OPENAI_PROVIDER_TIER_VERSION } from "../types"; +import { MAX_COST4_RATE } from "../usage/expected-prices"; +import { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "./openai-tiers/destination"; +export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers/destination"; +``` + +No `export type { ... }` is needed for this layer: no exported type moves. + +## Module-level state and cycles + +No top-level let/Map/Set/WeakMap/lock/timer exists. Top-level constants at 6–12 and 40–42 are identity strings; destination.ts owns those at 6–11 and 40–42, while residual owns LEGACY_OPENAI_MULTI_PREFIX at 12. Each has one owner. The Sets at 135, 178, 299, 316 and 415 are function-local (overlay allowlist, deduplication, known paths, warnings); they remain in their original functions and are not promoted to ESM state. + +Current dependents include `src/config.ts:80`, `src/routing/health.ts:21`, `src/routing/capability.ts:14`, `src/router.ts:33` and `src/codex/catalog/parsing.ts:20` (53 resolved callers total). Current dependencies are `src/providers/openai-tiers.ts:1–4`: types, tier version, URL builder, cost ceiling. Intended direction: callers → old facade → destination leaf → URL builder/types; migration residual → destination constants/predicate. destination.ts never imports `../openai-tiers`, `config.ts` or `registry.ts`. Keeping the helper's cost shape validation with migration preserves its existing cycle-avoidance rationale at 138–140. No cycle was found for the original target in lane 013; verify no new return edge after extraction. Blast radius is one local provider feature; coupling is functional, with no shared mutable state. + +## Tests + +Complete resolved `rg -l` importer list under tests: seven test files plus the child fixture. All eight are **unchanged** and keep the historical module path: + +- `tests/adapters/openai/openai-provider-option-migration.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option-startup.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option.test.ts` — unchanged. +- `tests/codex-integration/codex-convergence-account-selectors.test.ts` — unchanged. +- `tests/fixtures/openai-provider-option-migration-child.ts` — unchanged. +- `tests/responses/responses-compaction-routing.test.ts` — unchanged. +- `tests/responses/responses-compaction.test.ts` — unchanged. +- `tests/responses/responses-inbound-store-default.test.ts` — unchanged. + +The child fixture dynamically imports the facade at `tests/fixtures/openai-provider-option-migration-child.ts:97`; this is a behavioral module address, not a source-text oracle. The migration test's original assertion begins at `tests/adapters/openai/openai-provider-option-migration.test.ts:36` and stays untouched. + +Direct source-text oracles for `openai-tiers.ts`: **none**, confirmed by basename/full/segmented-path searches and source-read filtering (lane 013 agrees). No retarget-to-leaf or add-leaf-to-scan-list is required. `tests/lab/core-lab-boundary.test.ts:69` is the existing recursive graph source reader: unchanged, it automatically reaches destination.ts through the facade; keep PROTECTED at line 20 untouched. + +Planned red-once checks: temporarily make the moved canonical predicate accept key auth or a query-bearing URL and confirm `tests/adapters/openai/openai-provider-option.test.ts:37–38` fails; restore. Perturb the moved compact endpoint predicate and drive `tests/responses/responses-compaction-routing.test.ts:154–169` red; restore and rerun. Do not weaken assertions or mutate the migration projection snapshots. No tests are run in this drafting task. + +## Verification + +Instantiate `002_layer_map.md` → **Per-layer gate** at this layer's exact tip. This delegated turn is docs-only: do not run these now. Remote full-suite execution, branch creation and PR publication belong to the parent/executor, not this drafting task. + +```sh +bun run typecheck +bun test tests/adapters/openai/openai-provider-option.test.ts tests/adapters/openai/openai-provider-option-migration.test.ts tests/adapters/openai/openai-provider-option-startup.test.ts +bun test tests/responses/responses-compaction-routing.test.ts tests/responses/responses-compaction.test.ts tests/responses/responses-inbound-store-default.test.ts tests/codex-integration/codex-convergence-account-selectors.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/providers/openai-tiers/destination.ts src/providers/openai-tiers.ts +rg -n 'from "[^"]*/openai-tiers"' src gui/src scripts tests | wc -l +git diff --check +# Remote only, after parent confirms this checkout is dedicated to the layer: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-providers-openai-tiers && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The 002 grep is a trend signal, not an exact module-resolution count: it omits `./registry`, dynamic imports and type-only ownership corrections. Compare the resolved importer list as well: 53 existing callers, no call-site change. Run no repository-wide local suite. Every local focused group above must show zero failures; typecheck/privacy/diff checks must exit zero. The remote pipeline's final `tail` exit status alone is not proof of Bun success: retain the complete log and Bun exit status (pipefail or PIPESTATUS in the executor shell), exact tested commit, and pass/fail totals. Record exact-head CI rollup before claiming PR-ready. No passes are claimed here. + +Static architecture verification is separate from typecheck: use the installed ast-grep import/export scan, resolve relative .ts/.tsx/index paths, include type-only edges and compare return paths to the baseline witnesses in Module-level state and cycles. Reject any new leaf-to-facade edge or new SCC; unresolved existing strict cycle constraints go back to the parent. Compare moved AST bodies/literal arrays with original spans (permit only import/export wiring, indentation, and array wrapper/spread scaffolding). Keep exported function signatures and original-path runtime export names identical. + +## Accept criteria + +1. Exactly one new runtime leaf is planned at the stated path; wc reports leaf ≤400 and original ≤400 (expected 101 and 321). +2. All 31 original top-level declarations appear exactly once in the inventory; every moved body matches its cited origin/dev span. +3. The nine moved runtime exports are explicitly re-exported; the three migration exports stay declared in place. All 53 legacy-path callers still resolve without edits. +4. Residual migration imports its five needed destination names explicitly; no leaf imports its facade and no new shared state/cycle is introduced. +5. All eight test/support importers retain their paths; source-reader dispositions are honored and required red-once checks are restored to green. +6. Each instantiated per-layer gate succeeds at the exact layer tip, remote full-suite exit is captured honestly, and exact-head CI evidence is attached before PR-ready. +7. The PR base and four-row stack map match this document; no merge occurs. + +## PR + +Title: `refactor(providers): separate OpenAI destination classification (split S02 L1/4)` + +Branch: `codex/split-providers-openai-tiers`. Base: `dev`. Closes: **none**. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), recording only this layer's exact-tip evidence. Review only this layer's diff. Placeholder PR numbers below are intentional planning references, not opened PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S02-L1 | **Current: separate OpenAI destination classification** | `codex/split-providers-openai-tiers` | `dev` | destination predicates and migration parity | +| 2 | #TBD-S02-L2 | extract private model metadata | `codex/split-providers-registry-a` | `dev` | model values and single ownership | +| 3 | #TBD-S02-L3 | extract registry contracts and primary entries | `codex/split-providers-registry-b` | `codex/split-providers-registry-a` | types, initial entries, FastWire import | +| 4 | #TBD-S02-L4 | finish ordered registry entry extraction | `codex/split-providers-registry-c` | `codex/split-providers-registry-b` | tail ordering and final size | + +Base: dev — no dependency on the layers below; no cascade obligation. Publication is parent-owned; merges remain prohibited for this split train. + +## P stale-check (2026-09-05, wp040) + +origin/dev 4dde2db97; `git diff --stat 445742966 origin/dev -- src/providers/openai-tiers.ts` empty (416 lines). Symbol anchors 6/11/12/14/23/34/114/116/356 confirmed by sed. Base `dev` (S02 bottom). The plan's new subdirectory `src/providers/openai-tiers/` has no sibling precedent inside src/providers (all flat files); the audit decides between the subdirectory and a flat `src/providers/openai-tiers-destination.ts` sibling. + +## A amendment (Boyle audit, VERDICT: PASS) + +Naming adopted: flat sibling `src/providers/openai-tiers-destination.ts` (src/providers has no subdirectories; prefix grouping like alibaba-region-*.ts is the local convention). Path substitutions for execution: leaf imports become `"../types"` and `"../adapters/openai-responses-url"`; residual import + re-export become `"./openai-tiers-destination"`. Red-drive citation widened to responses-compaction-routing.test.ts:154–172. The "array wrapper/spread scaffolding" allowance at the old line 129 is void; only 003 PURE-MOVE-SIZE-01 transformations apply. Everything else in this doc stands. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-040.Q03dcg/wt` (branch `codex/split-providers-openai-tiers`, base origin/dev 4dde2db97). Executor: gpt-6-astra high (Turing, 01a06f0a-5f61-7893-8515-ca31ed11afc2). +- Commits: 73bb38781 (move: openai-tiers-destination.ts 102 lines, openai-tiers.ts 319) and 58dba9e0b (test: openai-provider-option.test.ts +13 — leaf bindings are identical to the facade re-exports; leaf does not import the facade). Diff: 3 files, +117/−99. +- Local gate: typecheck 0; focused (7 files) 179 pass / 0 fail; core-lab-boundary 17/0; privacy passed. +- Red-drives: (a) key-auth accepted → openai-provider-option.test.ts:37 fails (3/1), restored 4/0; (b) compact predicate inverted → responses-compaction-routing.test.ts:154 and :167 fail (0/2), restored 2/0. + +- Adversarial diff review (Gauss, gpt-6-astra high, 01a06f0e-518b-7402-8031-bf81e930bb3a): VERDICT: PASS first round (byte-identical slices, exact reconstruction, 12 exports preserved, no leaf→facade edge via Bun.Transpiler.scanImports, 3 files). +- C receipt at 58dba9e0b: typecheck 0, focused (option + compaction-routing + lab-boundary) 0 fail, privacy 0, DIRTY 0. +- lidge full suite at 58dba9e0b: SUITE_EXIT=0, 18014 pass / 0 fail / 16 skip (/tmp/suite-split-providers-openai-tiers.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3566 (base dev, head 58dba9e0b). CI rollup at record time: OPEN draft=false 58dba9e0b =1 =18 CANCELLED=1 SKIPPED=2 SUCCESS=5 diff --git a/devlog/_plan/260905_now_split_train/050_providers_registry_a.md b/devlog/_plan/260905_now_split_train/050_providers_registry_a.md new file mode 100644 index 0000000000..31d4a894e6 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/050_providers_registry_a.md @@ -0,0 +1,419 @@ +# 050 — S02 providers L2/4 + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Classification: C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture determine size, compatibility and state ownership; parent owns loop/goal/orchestration. +- Goal: extract private model metadata, preserving every historical export and observable behavior of `src/providers/registry.ts`. +- Non-goals: no model refresh, endpoint/auth-policy changes, validation redesign, caching, new runtime dependency, bug fix, generated metadata rewrite, repository-wide local test, merge, release or deployment. Existing behavior stays literal, including comments explaining it. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current task verifies documentation only, not runtime correctness. +- Stop: this plan is complete when the inventory, ownership, exact wiring, test disposition and count ledger are consistent; execution stops only after its own tip passes the instantiated gate and records exact-head CI. Do not defer a failing layer upward. +- Escalation: execution is conditional: 3,250 → ≤400 requires removing at least 2,850 original lines; three registry layers capped at 500 cannot remove that much even if additions are free. Ask the parent to explicitly waive the per-layer move-volume cap or expand 002; do not assert these three layers meet it. Also obtain authorization for the one FastWire type-import edit in L3 and disposition the pre-existing Antigravity type cycle under the strict cycle rule. + +Structural decision: the 3,250-line module combines contracts, private model metadata, ordered provider rows and lookup policy. Move the lowest-fan-in private model groups first, then contracts plus entry chunks, retaining the public facade and its lookup/validation code. Rejected alternatives: doing nothing/configuring cannot meet the line limit; deleting declarations would change behavior; changing all consumer imports would widen churn; a new provider framework or generic utils barrel is unnecessary. Existing `src/types.ts → src/types/*`, `src/config/*.ts`, and `src/codex/catalog.ts → src/codex/catalog/*` establish kebab-case co-located leaf convention. Keep legacy facades as explicit compatibility boundaries; no new index.ts or export-star barrel. + +## Symbol inventory + +Basis: `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549`. Every range in this document is an original-source line range, not the intermediate branch's shifted coordinates. `git diff origin/dev -- src/providers/registry.ts` was empty. + +Ranges were measured with `sg run --lang typescript --kind --json=compact src/providers/registry.ts`, taking column-zero export/lexical/function/interface/type-alias/class declarations. Imports are listed separately below; the inventory does not confuse nested declarations with ESM state. + +Consumer count = distinct `rg -l -w ''` files among resolved static/dynamic importers of this exact module under `src gui/src scripts tests` (`*.ts`/`*.tsx`), excluding the defining file. This is textual fan-in within the importer set, not call frequency. Private symbols have zero external import consumers; coincident names/comments elsewhere are excluded. Importer discovery starts with `rg -l 'registry' src gui/src scripts tests` and resolves each relative specifier, so other registries do not count. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ProviderAuthKind` | type | 25–25 | yes | 1 | registry/contracts.ts (L3) | +| `MetadataModelIdNormalize` | type | 26–26 | yes | 0 | registry/contracts.ts (L3) | +| `InboundWire` | type | 33–33 | yes | 7 | registry/contracts.ts (L3) | +| `ModelWireDefault` | type | 39–45 | yes | 2 | registry/contracts.ts (L3) | +| `ResponsesTerminalRepairPolicy` | interface | 47–50 | yes | 2 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryScalar` | type | 52–52 | yes | 1 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryPredicate` | type | 54–74 | yes | 1 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryFilter` | interface | 76–83 | yes | 2 | registry/contracts.ts (L3) | +| `ProviderModelDiscoverySharedSpec` | interface | 85–99 | no | 0 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryLocation` | type | 101–116 | no | 0 | registry/contracts.ts (L3) | +| `ProviderModelDiscoverySpec` | type | 122–122 | yes | 4 | registry/contracts.ts (L3) | +| `ProviderRegistryEntry` | interface | 124–330 | yes | 6 | registry/contracts.ts (L3) | +| `ProviderConfigSeed` | type | 332–342 | yes | 0 | registry/contracts.ts (L3) | +| `ANTHROPIC_MODELS` | const | 350–350 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_MODEL_CONTEXT_WINDOWS` | const | 351–351 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS` | const | 355–355 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_REASONING_EFFORTS` | const | 380–380 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_MODEL_REASONING_EFFORTS` | const | 381–383 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_53_MODELS` | const | 399–399 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_52_MODELS` | const | 400–400 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_MODELS` | const | 401–401 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_SIDECAR_VISION_MODELS` | const | 416–416 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_52_REASONING_EFFORTS` | const | 417–417 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_53_REASONING_EFFORTS` | const | 425–425 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_REASONING_EFFORTS` | const | 427–430 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_MODELS` | const | 433–439 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_MODEL_CONTEXT_WINDOWS` | const | 440–442 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_M3_REASONING_EFFORTS` | const | 443–443 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_M3_REASONING_EFFORT_MAP` | const | 444–452 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_GPT56_MODELS` | const | 453–453 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_GPT56_PRO_MODELS` | const | 454–454 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_CONTEXT_WINDOW` | const | 455–455 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_CONTEXT_WINDOWS` | const | 456–459 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_MAX_INPUT_TOKENS` | const | 460–463 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_VIRTUAL_MODELS` | const | 464–468 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_REASONING_EFFORTS` | const | 469–469 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_REASONING_EFFORTS` | const | 482–482 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_REASONING_EFFORT_MAP` | const | 490–492 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_CONTEXT_WINDOW` | const | 494–494 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_MODELS` | const | 495–495 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_MODELS` | const | 507–507 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_CONTEXT_WINDOWS` | const | 508–511 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_MAX_INPUT_TOKENS` | const | 512–515 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_REASONING_EFFORTS` | const | 524–526 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_MODELS` | const | 527–527 | no | 0 | registry/frontier-models.ts (L2) | +| `XAI_MODELS` | const | 528–537 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_CONTEXT_WINDOW` | const | 540–540 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_CONTEXT_WINDOWS` | const | 541–545 | no | 0 | registry/frontier-models.ts (L2) | +| `THINKING_TOGGLE_EFFORTS` | const | 553–553 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_TOGGLE_MAP` | const | 554–562 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_GO_THINKING_TOGGLE_MODELS` | const | 563–565 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_TEXT_MODELS` | const | 574–574 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_MODELS` | const | 575–575 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_INPUT_MODALITIES` | const | 576–579 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS` | const | 580–580 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_BUDGET_EFFORTS` | const | 581–581 | no | 0 | registry/reasoning-models.ts (L2) | +| `QWEN38_REASONING_EFFORTS` | const | 584–584 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_BUDGET_MODELS` | const | 585–588 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_GO_THINKING_BUDGET_MODELS` | const | 589–589 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_THINKING_MODELS` | const | 590–590 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_VISION_PREVIEW_MODEL` | const | 597–597 | no | 0 | registry/reasoning-models.ts (L2) | +| `COMMAND_CODE_IMAGE_MODELS` | const | 607–617 | no | 0 | registry/reasoning-models.ts (L2) | +| `COMMAND_CODE_MODEL_INPUT_MODALITIES` | const | 618–619 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_FREE_DEEPSEEK_MODELS` | const | 620–620 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_ZEN_TEXT_ONLY_MODELS` | const | 641–648 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_FLASH_THINKING_EFFORTS` | const | 672–672 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_PRO_THINKING_EFFORTS` | const | 673–673 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_PRO_REASONING_MAP` | const | 674–680 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_FLASH_REASONING_MAP` | const | 681–687 | no | 0 | registry/reasoning-models.ts (L2) | +| `isDeepseekFlashModel` | const | 695–696 | no | 0 | registry/reasoning-models.ts (L2) | +| `deepseekThinkingEffortsFor` | const | 697–698 | no | 0 | registry/reasoning-models.ts (L2) | +| `deepseekReasoningMapFor` | const | 699–700 | no | 0 | registry/reasoning-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_MODELS` | const | 705–708 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_QWEN_MODELS` | const | 709–711 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_INPUT_MODALITIES` | const | 712–721 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_MODELS` | const | 727–733 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS` | const | 734–736 | no | 0 | registry/coding-plan-models.ts (L2) | +| `TENCENT_CODING_PLAN_MODELS` | const | 743–743 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_ARK_MODELS` | const | 758–769 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_DOUBAO_THINKING_MODELS` | const | 770–774 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_CODING_PLAN_MODELS` | const | 775–785 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_AGENT_PLAN_MODELS` | const | 786–795 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_PLAN_INPUT_MODALITIES` | const | 796–802 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_PLAN_TEXT_ONLY_MODELS` | const | 806–814 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES` | const | 815–833 | no | 0 | registry/coding-plan-models.ts (L2) | +| `KIMI_K3_STANDARD_CONTEXT_WINDOW` | const | 841–841 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_K3_1M_CONTEXT_WINDOW` | const | 842–842 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_MODELS` | const | 843–843 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_LEGACY_API_MODELS` | const | 844–844 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODELS` | const | 845–845 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_MODELS` | const | 846–846 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_THINKING_MODELS` | const | 847–847 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_NO_REASONING_MODELS` | const | 848–848 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_NO_REASONING_MODELS` | const | 849–849 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_REASONING_EFFORTS` | const | 850–850 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_REASONING_EFFORT_MAP` | const | 851–858 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_REASONING_EFFORTS` | const | 859–861 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_DEFAULT_REASONING_EFFORTS` | const | 862–864 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_REASONING_EFFORT_MAPS` | const | 865–867 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_REASONING_EFFORTS` | const | 868–870 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_LOCKED_PARAMETER_MODELS` | const | 871–871 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS` | const | 872–872 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODEL_CONTEXT_WINDOWS` | const | 873–875 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODEL_INPUT_MODALITIES` | const | 876–876 | no | 0 | registry/kimi-models.ts (L2) | +| `NVIDIA_NIM_KIMI_THINKING_MODELS` | const | 881–883 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_KIMI_MODELS` | const | 884–887 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_VISION_MODELS` | const | 910–920 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_VISION_INPUT_MODALITIES` | const | 926–928 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_NO_VISION_MODELS` | const | 939–958 | no | 0 | registry/nim-models.ts (L2) | +| `KIMI_CODING_MODEL_CONTEXT_WINDOWS` | const | 959–961 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_MODEL_INPUT_MODALITIES` | const | 962–964 | no | 0 | registry/kimi-models.ts (L2) | +| `NEURALWATT_REASONING_HISTORY_MODELS` | const | 965–970 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_FULL_REASONING_EFFORTS` | const | 979–979 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_REASONING_EFFORTS` | const | 980–990 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_REASONING_EFFORT_MAP` | const | 991–1000 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_DEFAULT_REASONING_EFFORTS` | const | 1001–1006 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_INPUT_MODALITIES` | const | 1007–1012 | no | 0 | registry/gateway-models.ts (L2) | +| `DIGITALOCEAN_CHAT_COMPLETION_MODELS` | const | 1023–1053 | no | 0 | registry/gateway-models.ts (L2) | +| `SCALEWAY_SERVERLESS_CHAT_MODELS` | const | 1054–1069 | no | 0 | registry/gateway-models.ts (L2) | +| `SCALEWAY_MODEL_INPUT_MODALITIES` | const | 1070–1072 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODELS` | const | 1073–1082 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_REASONING_EFFORTS` | const | 1083–1083 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_GLM_REASONING_EFFORTS` | const | 1084–1084 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_GLM_53_REASONING_EFFORTS` | const | 1087–1087 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_TEXT_ONLY_MODELS` | const | 1092–1092 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODEL_CONTEXT_WINDOWS` | const | 1093–1104 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODEL_INPUT_MODALITIES` | const | 1105–1107 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODELS` | const | 1108–1123 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODEL_CONTEXT_WINDOWS` | const | 1124–1138 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_IMAGE_MODELS` | const | 1139–1151 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODALITY_KNOWN_MODELS` | const | 1152–1152 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_TEXT_ONLY_MODELS` | const | 1153–1153 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODEL_INPUT_MODALITIES` | const | 1154–1156 | no | 0 | registry/gateway-models.ts (L2) | +| `PROVIDER_REGISTRY` | const | 1158–3056 | yes | 62 | residual; element leaves in L3/L4 | +| `providerRegistryFastWireError` | function | 3058–3062 | yes | 1 | residual original file | +| `getProviderRegistryEntry` | function | 3069–3071 | yes | 58 | residual original file | +| `mergeRegistryStaticHeaders` | function | 3089–3101 | yes | 2 | residual original file | +| `registryModelServiceTierCapabilityApplies` | function | 3104–3110 | yes | 4 | residual original file | +| `normalizedProviderEndpoint` | function | 3112–3121 | no | 0 | residual original file | +| `providerMatchesRegistryTransport` | function | 3131–3145 | yes | 9 | residual original file | +| `registryEntryForProviderDestination` | function | 3159–3171 | yes | 8 | residual original file | +| `providerModelWireDefault` | function | 3179–3199 | yes | 3 | residual original file | +| `providerModelResponsesUpstreamStreaming` | function | 3202–3210 | yes | 1 | residual original file | +| `providerModelResponsesTerminalRepair` | function | 3213–3224 | yes | 2 | residual original file | +| `providerCodexAccountMode` | function | 3231–3237 | yes | 25 | residual original file | +| `effectiveGoogleMode` | function | 3244–3250 | yes | 4 | residual original file | + +Imports at `src/providers/registry.ts:1–23` are dependencies, not additional declared public symbols; see exact residual imports below. The top-level `for` at 3064–3067 is inventoried as an effect in Module-level state and cycles. The `PROVIDER_REGISTRY` declaration is not duplicated: its individual object literals are the entry units detailed below. + +## Leaf partition + +Source paths below are all NEW under `src/providers/registry/`. The ranges are cut boundaries including nearby comments/blanks; symbol ranges above exclude leading comments. Leaf counts include imports and typed array wrappers, using one import statement per physical line. Never shorten source comments to hit the limit. + +### `src/providers/registry/frontier-models.ts` + +- Original ranges: `344–545`. +- Symbols: `ANTHROPIC_MODELS`, `ANTHROPIC_MODEL_CONTEXT_WINDOWS`, `ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS`, `ANTHROPIC_REASONING_EFFORTS`, `ANTHROPIC_MODEL_REASONING_EFFORTS`, `ZAI_GLM_53_MODELS`, `ZAI_GLM_52_MODELS`, `ZAI_GLM_5X_MODELS`, `ZAI_GLM_5X_SIDECAR_VISION_MODELS`, `ZAI_GLM_52_REASONING_EFFORTS`, `ZAI_GLM_53_REASONING_EFFORTS`, `ZAI_GLM_5X_REASONING_EFFORTS`, `MINIMAX_MODELS`, `MINIMAX_MODEL_CONTEXT_WINDOWS`, `MINIMAX_M3_REASONING_EFFORTS`, `MINIMAX_M3_REASONING_EFFORT_MAP`, `OPENAI_GPT56_MODELS`, `OPENAI_GPT56_PRO_MODELS`, `OPENAI_API_GPT56_CONTEXT_WINDOW`, `OPENAI_API_GPT56_CONTEXT_WINDOWS`, `OPENAI_API_GPT56_MAX_INPUT_TOKENS`, `OPENAI_API_GPT56_VIRTUAL_MODELS`, `OPENAI_API_GPT56_REASONING_EFFORTS`, `META_MUSE_REASONING_EFFORTS`, `META_MUSE_REASONING_EFFORT_MAP`, `META_MUSE_CONTEXT_WINDOW`, `META_MUSE_MODELS`, `OPENAI_DAYBREAK_MODELS`, `OPENAI_DAYBREAK_CONTEXT_WINDOWS`, `OPENAI_DAYBREAK_MAX_INPUT_TOKENS`, `OPENAI_DAYBREAK_REASONING_EFFORTS`, `OPENROUTER_GPT56_MODELS`, `XAI_MODELS`, `OPENROUTER_GPT56_CONTEXT_WINDOW`, `OPENROUTER_GPT56_CONTEXT_WINDOWS`. +- Expected lines: **202** (≤400). +- Existing external import consumers: **0** for all moved declarations. Export only leaf-private wiring names used by the residual/entry leaves: `ANTHROPIC_MODELS`, `ANTHROPIC_MODEL_CONTEXT_WINDOWS`, `ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS`, `ANTHROPIC_MODEL_REASONING_EFFORTS`, `ZAI_GLM_53_MODELS`, `ZAI_GLM_5X_MODELS`, `ZAI_GLM_5X_SIDECAR_VISION_MODELS`, `ZAI_GLM_52_REASONING_EFFORTS`, `ZAI_GLM_53_REASONING_EFFORTS`, `ZAI_GLM_5X_REASONING_EFFORTS`, `MINIMAX_MODELS`, `MINIMAX_MODEL_CONTEXT_WINDOWS`, `MINIMAX_M3_REASONING_EFFORTS`, `MINIMAX_M3_REASONING_EFFORT_MAP`, `OPENAI_GPT56_MODELS`, `OPENAI_GPT56_PRO_MODELS`, `OPENAI_API_GPT56_CONTEXT_WINDOWS`, `OPENAI_API_GPT56_MAX_INPUT_TOKENS`, `OPENAI_API_GPT56_VIRTUAL_MODELS`, `OPENAI_API_GPT56_REASONING_EFFORTS`, `META_MUSE_REASONING_EFFORTS`, `META_MUSE_REASONING_EFFORT_MAP`, `META_MUSE_CONTEXT_WINDOW`, `META_MUSE_MODELS`, `OPENAI_DAYBREAK_MODELS`, `OPENAI_DAYBREAK_CONTEXT_WINDOWS`, `OPENAI_DAYBREAK_MAX_INPUT_TOKENS`, `OPENAI_DAYBREAK_REASONING_EFFORTS`, `OPENROUTER_GPT56_MODELS`, `XAI_MODELS`, `OPENROUTER_GPT56_CONTEXT_WINDOWS`. Other declarations remain private; none becomes a new export of `registry.ts`. +- Own imports: **none** (all expressions use local declarations and built-ins). + +### `src/providers/registry/reasoning-models.ts` + +- Original ranges: `546–700`. +- Symbols: `THINKING_TOGGLE_EFFORTS`, `THINKING_TOGGLE_MAP`, `OPENCODE_GO_THINKING_TOGGLE_MODELS`, `ZHIPU_BIGMODEL_TEXT_MODELS`, `ZHIPU_BIGMODEL_MODELS`, `ZHIPU_BIGMODEL_INPUT_MODALITIES`, `ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS`, `THINKING_BUDGET_EFFORTS`, `QWEN38_REASONING_EFFORTS`, `THINKING_BUDGET_MODELS`, `OPENCODE_GO_THINKING_BUDGET_MODELS`, `DEEPSEEK_THINKING_MODELS`, `DEEPSEEK_VISION_PREVIEW_MODEL`, `COMMAND_CODE_IMAGE_MODELS`, `COMMAND_CODE_MODEL_INPUT_MODALITIES`, `OPENCODE_FREE_DEEPSEEK_MODELS`, `OPENCODE_ZEN_TEXT_ONLY_MODELS`, `DEEPSEEK_FLASH_THINKING_EFFORTS`, `DEEPSEEK_PRO_THINKING_EFFORTS`, `DEEPSEEK_PRO_REASONING_MAP`, `DEEPSEEK_FLASH_REASONING_MAP`, `isDeepseekFlashModel`, `deepseekThinkingEffortsFor`, `deepseekReasoningMapFor`. +- Expected lines: **155** (≤400). +- Existing external import consumers: **0** for all moved declarations. Export only leaf-private wiring names used by the residual/entry leaves: `THINKING_TOGGLE_EFFORTS`, `THINKING_TOGGLE_MAP`, `OPENCODE_GO_THINKING_TOGGLE_MODELS`, `ZHIPU_BIGMODEL_MODELS`, `ZHIPU_BIGMODEL_INPUT_MODALITIES`, `ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS`, `THINKING_BUDGET_EFFORTS`, `QWEN38_REASONING_EFFORTS`, `THINKING_BUDGET_MODELS`, `OPENCODE_GO_THINKING_BUDGET_MODELS`, `DEEPSEEK_THINKING_MODELS`, `DEEPSEEK_VISION_PREVIEW_MODEL`, `COMMAND_CODE_MODEL_INPUT_MODALITIES`, `OPENCODE_FREE_DEEPSEEK_MODELS`, `OPENCODE_ZEN_TEXT_ONLY_MODELS`, `deepseekThinkingEffortsFor`, `deepseekReasoningMapFor`. Other declarations remain private; none becomes a new export of `registry.ts`. +- Own imports: **none** (all expressions use local declarations and built-ins). + +### `src/providers/registry/coding-plan-models.ts` + +- Original ranges: `701–833`. +- Symbols: `ALIBABA_TOKEN_PLAN_MODELS`, `ALIBABA_TOKEN_PLAN_QWEN_MODELS`, `ALIBABA_TOKEN_PLAN_INPUT_MODALITIES`, `ALIBABA_INTL_TOKEN_PLAN_MODELS`, `ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS`, `TENCENT_CODING_PLAN_MODELS`, `VOLCENGINE_ARK_MODELS`, `VOLCENGINE_DOUBAO_THINKING_MODELS`, `VOLCENGINE_CODING_PLAN_MODELS`, `VOLCENGINE_AGENT_PLAN_MODELS`, `VOLCENGINE_PLAN_INPUT_MODALITIES`, `VOLCENGINE_PLAN_TEXT_ONLY_MODELS`, `ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES`. +- Expected lines: **133** (≤400). +- Existing external import consumers: **0** for all moved declarations. Export only leaf-private wiring names used by the residual/entry leaves: `ALIBABA_TOKEN_PLAN_MODELS`, `ALIBABA_TOKEN_PLAN_QWEN_MODELS`, `ALIBABA_TOKEN_PLAN_INPUT_MODALITIES`, `ALIBABA_INTL_TOKEN_PLAN_MODELS`, `ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS`, `TENCENT_CODING_PLAN_MODELS`, `VOLCENGINE_ARK_MODELS`, `VOLCENGINE_DOUBAO_THINKING_MODELS`, `VOLCENGINE_CODING_PLAN_MODELS`, `VOLCENGINE_AGENT_PLAN_MODELS`, `VOLCENGINE_PLAN_INPUT_MODALITIES`, `VOLCENGINE_PLAN_TEXT_ONLY_MODELS`, `ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES`. Other declarations remain private; none becomes a new export of `registry.ts`. +- Own imports: **none** (all expressions use local declarations and built-ins). + +### `src/providers/registry/kimi-models.ts` + +- Original ranges: `834–876, 959–964`. +- Symbols: `KIMI_K3_STANDARD_CONTEXT_WINDOW`, `KIMI_K3_1M_CONTEXT_WINDOW`, `KIMI_CODING_K3_MODELS`, `KIMI_LEGACY_API_MODELS`, `KIMI_API_MODELS`, `KIMI_CODING_MODELS`, `KIMI_THINKING_MODELS`, `KIMI_CODING_NO_REASONING_MODELS`, `KIMI_API_NO_REASONING_MODELS`, `KIMI_CODING_K3_REASONING_EFFORTS`, `KIMI_CODING_K3_REASONING_EFFORT_MAP`, `KIMI_CODING_REASONING_EFFORTS`, `KIMI_CODING_DEFAULT_REASONING_EFFORTS`, `KIMI_CODING_REASONING_EFFORT_MAPS`, `KIMI_API_REASONING_EFFORTS`, `KIMI_LOCKED_PARAMETER_MODELS`, `KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS`, `KIMI_API_MODEL_CONTEXT_WINDOWS`, `KIMI_API_MODEL_INPUT_MODALITIES`, `KIMI_CODING_MODEL_CONTEXT_WINDOWS`, `KIMI_CODING_MODEL_INPUT_MODALITIES`. +- Expected lines: **49** (≤400). +- Existing external import consumers: **0** for all moved declarations. Export only leaf-private wiring names used by the residual/entry leaves: `KIMI_K3_STANDARD_CONTEXT_WINDOW`, `KIMI_API_MODELS`, `KIMI_CODING_MODELS`, `KIMI_THINKING_MODELS`, `KIMI_CODING_NO_REASONING_MODELS`, `KIMI_API_NO_REASONING_MODELS`, `KIMI_CODING_K3_REASONING_EFFORTS`, `KIMI_CODING_K3_REASONING_EFFORT_MAP`, `KIMI_CODING_REASONING_EFFORTS`, `KIMI_CODING_DEFAULT_REASONING_EFFORTS`, `KIMI_CODING_REASONING_EFFORT_MAPS`, `KIMI_API_REASONING_EFFORTS`, `KIMI_LOCKED_PARAMETER_MODELS`, `KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS`, `KIMI_API_MODEL_CONTEXT_WINDOWS`, `KIMI_API_MODEL_INPUT_MODALITIES`, `KIMI_CODING_MODEL_CONTEXT_WINDOWS`, `KIMI_CODING_MODEL_INPUT_MODALITIES`. Other declarations remain private; none becomes a new export of `registry.ts`. +- Own imports: **none** (all expressions use local declarations and built-ins). + +### `src/providers/registry/nim-models.ts` + +- Original ranges: `877–958`. +- Symbols: `NVIDIA_NIM_KIMI_THINKING_MODELS`, `NVIDIA_NIM_KIMI_MODELS`, `NVIDIA_NIM_VISION_MODELS`, `NVIDIA_NIM_VISION_INPUT_MODALITIES`, `NVIDIA_NIM_NO_VISION_MODELS`. +- Expected lines: **82** (≤400). +- Existing external import consumers: **0** for all moved declarations. Export only leaf-private wiring names used by the residual/entry leaves: `NVIDIA_NIM_KIMI_THINKING_MODELS`, `NVIDIA_NIM_KIMI_MODELS`, `NVIDIA_NIM_VISION_INPUT_MODALITIES`, `NVIDIA_NIM_NO_VISION_MODELS`. Other declarations remain private; none becomes a new export of `registry.ts`. +- Own imports: **none** (all expressions use local declarations and built-ins). + +### `src/providers/registry/gateway-models.ts` + +- Original ranges: `965–1157`. +- Symbols: `NEURALWATT_REASONING_HISTORY_MODELS`, `BASETEN_FULL_REASONING_EFFORTS`, `BASETEN_MODEL_REASONING_EFFORTS`, `BASETEN_MODEL_REASONING_EFFORT_MAP`, `BASETEN_MODEL_DEFAULT_REASONING_EFFORTS`, `BASETEN_MODEL_INPUT_MODALITIES`, `DIGITALOCEAN_CHAT_COMPLETION_MODELS`, `SCALEWAY_SERVERLESS_CHAT_MODELS`, `SCALEWAY_MODEL_INPUT_MODALITIES`, `UMANS_MODELS`, `UMANS_REASONING_EFFORTS`, `UMANS_GLM_REASONING_EFFORTS`, `UMANS_GLM_53_REASONING_EFFORTS`, `UMANS_TEXT_ONLY_MODELS`, `UMANS_MODEL_CONTEXT_WINDOWS`, `UMANS_MODEL_INPUT_MODALITIES`, `CLINE_PASS_MODELS`, `CLINE_PASS_MODEL_CONTEXT_WINDOWS`, `CLINE_PASS_IMAGE_MODELS`, `CLINE_PASS_MODALITY_KNOWN_MODELS`, `CLINE_PASS_TEXT_ONLY_MODELS`, `CLINE_PASS_MODEL_INPUT_MODALITIES`. +- Expected lines: **193** (≤400). +- Existing external import consumers: **0** for all moved declarations. Export only leaf-private wiring names used by the residual/entry leaves: `NEURALWATT_REASONING_HISTORY_MODELS`, `BASETEN_MODEL_REASONING_EFFORTS`, `BASETEN_MODEL_REASONING_EFFORT_MAP`, `BASETEN_MODEL_DEFAULT_REASONING_EFFORTS`, `BASETEN_MODEL_INPUT_MODALITIES`, `DIGITALOCEAN_CHAT_COMPLETION_MODELS`, `SCALEWAY_SERVERLESS_CHAT_MODELS`, `SCALEWAY_MODEL_INPUT_MODALITIES`, `UMANS_MODELS`, `UMANS_REASONING_EFFORTS`, `UMANS_GLM_REASONING_EFFORTS`, `UMANS_GLM_53_REASONING_EFFORTS`, `UMANS_TEXT_ONLY_MODELS`, `UMANS_MODEL_CONTEXT_WINDOWS`, `UMANS_MODEL_INPUT_MODALITIES`, `CLINE_PASS_MODELS`, `CLINE_PASS_MODEL_CONTEXT_WINDOWS`, `CLINE_PASS_TEXT_ONLY_MODELS`, `CLINE_PASS_MODEL_INPUT_MODALITIES`. Other declarations remain private; none becomes a new export of `registry.ts`. +- Own imports: **none** (all expressions use local declarations and built-ins). + +MODIFY `src/providers/registry.ts`: expected residual **2429 lines**. Over 400 intentionally; #b (060, L3) takes contracts and primary entry chunks; #c (070, L4) takes remaining entry chunks. + +| Registry stage | Original lines removed, cumulative | Residual body incl. spread placeholders | Header/import/re-export lines | Expected residual | +|---|---:|---:|---:|---:| +| #a / L2 | 814 | 2,412 | 17 | 2,429 | +| #b / L3 | 1,981 | 1,249 | 18 | 1,267 | +| #c / L4 | 3,029 | 205 | 14 | 219 | + +Accounting starts from 3,250 original physical lines. Original header 1–24 is replaced by the explicit one-statement-per-line headers in each Re-export block. Body removals: 814 model lines in #a; 319 contract lines + 848 entry lines in #b; 1,048 entry lines in #c. #b inserts four spread lines; #c inserts four more. Thus #b reduces the prior residual by 1,162; #c by 1,048. These counts include retained comments/blanks and are exact for the specified compact headers; formatting may change them but must not exceed 400 for a new leaf. All 1,897 original array-content lines are accounted for: 848 + 1,048 moved, plus the one retained Antigravity line at 1903. Final original residual is 219, not an unplanned #d. + + +## Re-export block + +No existing export moves in #a: all 11 public types, PROVIDER_REGISTRY and all 11 exported functions remain declared in place. Therefore the exact new public re-export block is **empty**, not export-star; private constants must not leak into the historical API. The complete expected residual import/re-export header is: + +```ts +import type { CodexAccountMode, FastWire, OcxProviderConfig } from "../types"; +import type { ProviderBaseUrlChoice } from "./base-url-choices"; +import { fastWireDeclarationError } from "./fastwire"; +import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models"; +import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "./antigravity-models"; +import { QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL, ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL, ALIBABA_CODING_BASE_URL_CHOICES, ALIBABA_CODING_INTL_BASE_URL, MOONSHOT_BASE_URL_CHOICES, MOONSHOT_INTL_BASE_URL } from "./base-url-choices"; +import { CURSOR_NO_VISION_MODELS, CURSOR_STATIC_MODELS, cursorModelContextWindows, cursorModelDisplayNames, cursorModelIds, cursorModelInputModalities, cursorModelReasoningEfforts } from "../adapters/cursor/discovery"; +import { cursorFastCapableBases } from "../adapters/cursor/catalog"; +import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts"; +import { isCanonicalOpenRouterTarget } from "./openrouter-routing"; +import { ANTHROPIC_MODELS, ANTHROPIC_MODEL_CONTEXT_WINDOWS, ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS, ANTHROPIC_MODEL_REASONING_EFFORTS, ZAI_GLM_53_MODELS, ZAI_GLM_5X_MODELS, ZAI_GLM_5X_SIDECAR_VISION_MODELS, ZAI_GLM_52_REASONING_EFFORTS, ZAI_GLM_53_REASONING_EFFORTS, ZAI_GLM_5X_REASONING_EFFORTS, MINIMAX_MODELS, MINIMAX_MODEL_CONTEXT_WINDOWS, MINIMAX_M3_REASONING_EFFORTS, MINIMAX_M3_REASONING_EFFORT_MAP, OPENAI_GPT56_MODELS, OPENAI_GPT56_PRO_MODELS, OPENAI_API_GPT56_CONTEXT_WINDOWS, OPENAI_API_GPT56_MAX_INPUT_TOKENS, OPENAI_API_GPT56_VIRTUAL_MODELS, OPENAI_API_GPT56_REASONING_EFFORTS, META_MUSE_REASONING_EFFORTS, META_MUSE_REASONING_EFFORT_MAP, META_MUSE_CONTEXT_WINDOW, META_MUSE_MODELS, OPENAI_DAYBREAK_MODELS, OPENAI_DAYBREAK_CONTEXT_WINDOWS, OPENAI_DAYBREAK_MAX_INPUT_TOKENS, OPENAI_DAYBREAK_REASONING_EFFORTS, OPENROUTER_GPT56_MODELS, XAI_MODELS, OPENROUTER_GPT56_CONTEXT_WINDOWS } from "./registry/frontier-models"; +import { THINKING_TOGGLE_EFFORTS, THINKING_TOGGLE_MAP, OPENCODE_GO_THINKING_TOGGLE_MODELS, ZHIPU_BIGMODEL_MODELS, ZHIPU_BIGMODEL_INPUT_MODALITIES, ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS, THINKING_BUDGET_EFFORTS, QWEN38_REASONING_EFFORTS, THINKING_BUDGET_MODELS, OPENCODE_GO_THINKING_BUDGET_MODELS, DEEPSEEK_THINKING_MODELS, DEEPSEEK_VISION_PREVIEW_MODEL, COMMAND_CODE_MODEL_INPUT_MODALITIES, OPENCODE_FREE_DEEPSEEK_MODELS, OPENCODE_ZEN_TEXT_ONLY_MODELS, deepseekThinkingEffortsFor, deepseekReasoningMapFor } from "./registry/reasoning-models"; +import { ALIBABA_TOKEN_PLAN_MODELS, ALIBABA_TOKEN_PLAN_QWEN_MODELS, ALIBABA_TOKEN_PLAN_INPUT_MODALITIES, ALIBABA_INTL_TOKEN_PLAN_MODELS, ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS, TENCENT_CODING_PLAN_MODELS, VOLCENGINE_ARK_MODELS, VOLCENGINE_DOUBAO_THINKING_MODELS, VOLCENGINE_CODING_PLAN_MODELS, VOLCENGINE_AGENT_PLAN_MODELS, VOLCENGINE_PLAN_INPUT_MODALITIES, VOLCENGINE_PLAN_TEXT_ONLY_MODELS, ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES } from "./registry/coding-plan-models"; +import { KIMI_K3_STANDARD_CONTEXT_WINDOW, KIMI_API_MODELS, KIMI_CODING_MODELS, KIMI_THINKING_MODELS, KIMI_CODING_NO_REASONING_MODELS, KIMI_API_NO_REASONING_MODELS, KIMI_CODING_K3_REASONING_EFFORTS, KIMI_CODING_K3_REASONING_EFFORT_MAP, KIMI_CODING_REASONING_EFFORTS, KIMI_CODING_DEFAULT_REASONING_EFFORTS, KIMI_CODING_REASONING_EFFORT_MAPS, KIMI_API_REASONING_EFFORTS, KIMI_LOCKED_PARAMETER_MODELS, KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS, KIMI_API_MODEL_CONTEXT_WINDOWS, KIMI_API_MODEL_INPUT_MODALITIES, KIMI_CODING_MODEL_CONTEXT_WINDOWS, KIMI_CODING_MODEL_INPUT_MODALITIES } from "./registry/kimi-models"; +import { NVIDIA_NIM_KIMI_THINKING_MODELS, NVIDIA_NIM_KIMI_MODELS, NVIDIA_NIM_VISION_INPUT_MODALITIES, NVIDIA_NIM_NO_VISION_MODELS } from "./registry/nim-models"; +import { NEURALWATT_REASONING_HISTORY_MODELS, BASETEN_MODEL_REASONING_EFFORTS, BASETEN_MODEL_REASONING_EFFORT_MAP, BASETEN_MODEL_DEFAULT_REASONING_EFFORTS, BASETEN_MODEL_INPUT_MODALITIES, DIGITALOCEAN_CHAT_COMPLETION_MODELS, SCALEWAY_SERVERLESS_CHAT_MODELS, SCALEWAY_MODEL_INPUT_MODALITIES, UMANS_MODELS, UMANS_REASONING_EFFORTS, UMANS_GLM_REASONING_EFFORTS, UMANS_GLM_53_REASONING_EFFORTS, UMANS_TEXT_ONLY_MODELS, UMANS_MODEL_CONTEXT_WINDOWS, UMANS_MODEL_INPUT_MODALITIES, CLINE_PASS_MODELS, CLINE_PASS_MODEL_CONTEXT_WINDOWS, CLINE_PASS_TEXT_ONLY_MODELS, CLINE_PASS_MODEL_INPUT_MODALITIES } from "./registry/gateway-models"; +``` + + +## Module-level state and cycles + +- `CLINE_PASS_IMAGE_MODELS` at `src/providers/registry.ts:1139–1151` has exactly one owner: `src/providers/registry/gateway-models.ts` from L2. It stays private there; its derived modality/text-only arrays stay with it. No setter, clone, lazy initializer, cache, or test hook is introduced. +- Every other top-level const is in the inventory. Model arrays/records are initialized once by their assigned leaf. Keep shared object identity, aliases (`KIMI_THINKING_MODELS` at 847, `KIMI_LOCKED_PARAMETER_MODELS` at 871), copies, and Object.fromEntries expressions unchanged. Readonly typing does not authorize freezing or cloning their values. +- `PROVIDER_REGISTRY` at 1158 remains one exported array in `registry.ts`. Entry leaves allocate each original entry object once; the facade spreads entry references in the historical sequence. The original validation loop at `src/providers/registry.ts:3064–3067` runs exactly once, after the complete array is constructed and before the facade import completes. It is a top-level effect, not a cache; never move it into each chunk or defer it. +- No top-level let, Map, WeakMap, lock or timer exists in either target. The `claimed` Set in `mergeRegistryStaticHeaders` at 3095 and callback-local Sets are invocation-local, not singleton state. No reset owner is needed. + +Dependency map: `src/router.ts:20`, `src/providers/derive.ts:8`, `src/config.ts:88`, and `src/codex/catalog/parsing.ts:14` consume the old boundary; it points to data leaves and contracts. Entry leaves point directly to their model leaves and existing vendor metadata owners, never to `../registry`. This is functional/data coupling; initialization/validation is the existing temporal coupling. No common mutable-state API is introduced. + +Existing type cycle: `registry.ts:2 → fastwire.ts:10 → registry.ts`. L2 leaves it unchanged; L3 moves contracts and changes only the type specifier in `src/providers/fastwire.ts:10` from `"./registry"` to `"./registry/contracts"`. This single adjacent source-file change is a required executor scope expansion for the parent to authorize, not performed by this documentation task. It reduces legacy-path importer count from 134 to 133; all other legacy consumers and all 78 test/support importers stay put. Do not pretend the literal unchanged-importer-count line in 002 can apply to this intentional one-edge repair. + +A second, pre-existing type-containing cycle is `registry.ts:4 → antigravity-models.ts:2 → codex/model-cache.ts:10 → codex/catalog.ts:3 → codex/catalog/parsing.ts:13 → providers/derive.ts:8 → registry.ts`. Keep the complete `google-antigravity` object at `registry.ts:1903` and its existing import in the facade, between the two gateway arrays. Moving it into an entry leaf would put that new leaf into the existing SCC. No new leaf imports Antigravity. The known vendor dependencies remain real shared owners (KIRO at src/providers/kiro-models.ts:1; Command Code at src/providers/command-code-efforts.ts:1; Cursor discovery/catalog at src/adapters/cursor/discovery.ts:1–8), not copied snapshots. A direct import of CatalogModel from parsing would still reach derive and would not fix this cycle. Strict all-graph zero-cycle acceptance needs a separately scoped type-owner repair; report this to the parent rather than silently expanding S02 or claiming the graph is globally acyclic. Compare baseline and tip graphs including erased type edges; no new SCC may contain a planned leaf. No lazy-import workaround. + +## Tests + +Resolved `rg -l` importer list below: 77 test files plus one test helper (78 files). Each is **unchanged** in every layer: it continues importing the historical facade, including the dynamic import at `tests/providers/qwen38-preserve-reasoning.test.ts:106` and child-process import text at `tests/adapters/openai/openai-provider-option-e2e.test.ts:261`. + +- `tests/adapters/adapter-tool-conformance.test.ts` — unchanged. +- `tests/adapters/anthropic/anthropic-hardening.test.ts` — unchanged. +- `tests/adapters/empty-tool-output-annotation.test.ts` — unchanged. +- `tests/adapters/google/antigravity-static-catalog.test.ts` — unchanged. +- `tests/adapters/google/gemini-37-flash-migration.test.ts` — unchanged. +- `tests/adapters/google/google-hardening.test.ts` — unchanged. +- `tests/adapters/openai/openai-api-virtual-models.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option-e2e.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option.test.ts` — unchanged. +- `tests/codex-integration/catalog-vision-sidecar-modalities.test.ts` — unchanged. +- `tests/codex-integration/codex-catalog.test.ts` — unchanged. +- `tests/codex-integration/codex-gather-authority.test.ts` — unchanged. +- `tests/codex-integration/compatibility-manifest.test.ts` — unchanged. +- `tests/gui/alibaba-intl-token-plan.test.ts` — unchanged. +- `tests/gui/provider-payload.test.ts` — unchanged. +- `tests/gui/qwen-cloud-endpoints.test.ts` — unchanged. +- `tests/gui/tencent-siliconflow-providers.test.ts` — unchanged. +- `tests/gui/volcengine-providers.test.ts` — unchanged. +- `tests/helpers/provider-registry-discovery.ts` — unchanged. +- `tests/images/gemini-inline.test.ts` — unchanged. +- `tests/providers/baseten-provider.test.ts` — unchanged. +- `tests/providers/chutes-provider.test.ts` — unchanged. +- `tests/providers/cline-pass-provider.test.ts` — unchanged. +- `tests/providers/cline-pass-reasoning-efforts.test.ts` — unchanged. +- `tests/providers/cline-provider.test.ts` — unchanged. +- `tests/providers/command-code-provider.test.ts` — unchanged. +- `tests/providers/commandcode-provider.test.ts` — unchanged. +- `tests/providers/cursor/cursor-display-names.test.ts` — unchanged. +- `tests/providers/cursor/cursor-fast-listing.test.ts` — unchanged. +- `tests/providers/cursor/cursor-fast-tier.test.ts` — unchanged. +- `tests/providers/deepinfra-provider.test.ts` — unchanged. +- `tests/providers/deepseek-inbound-wire.test.ts` — unchanged. +- `tests/providers/deepseek-reasoning-replay.test.ts` — unchanged. +- `tests/providers/deepseek-responses-item-id-repair.test.ts` — unchanged. +- `tests/providers/digitalocean-scaleway-provider.test.ts` — unchanged. +- `tests/providers/fast-row-ingress.test.ts` — unchanged. +- `tests/providers/featherless-provider.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-stream-contract.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-wire-defaults.test.ts` — unchanged. +- `tests/providers/hyperbolic-provider.test.ts` — unchanged. +- `tests/providers/kiro/kiro-adapter.test.ts` — unchanged. +- `tests/providers/meta-model-api-provider.test.ts` — unchanged. +- `tests/providers/meta-muse-oauth.test.ts` — unchanged. +- `tests/providers/mimo-effort.test.ts` — unchanged. +- `tests/providers/mimo-free-provider.test.ts` — unchanged. +- `tests/providers/mimo-token-plan-provider.test.ts` — unchanged. +- `tests/providers/model-rename-migration.test.ts` — unchanged. +- `tests/providers/moonshot-endpoints.test.ts` — unchanged. +- `tests/providers/muse-spark-web-search-compat.test.ts` — unchanged. +- `tests/providers/novita-provider.test.ts` — unchanged. +- `tests/providers/nscale-vultr-provider.test.ts` — unchanged. +- `tests/providers/nvidia-nim-hardening.test.ts` — unchanged. +- `tests/providers/ollama/ollama-native.test.ts` — unchanged. +- `tests/providers/opencode-free-provider.test.ts` — unchanged. +- `tests/providers/opencode-go-grok46-responses.test.ts` — unchanged. +- `tests/providers/opencode-go-luna-wire.test.ts` — unchanged. +- `tests/providers/opencode-go-muse-context.test.ts` — unchanged. +- `tests/providers/opencode-go-muse-vision.test.ts` — unchanged. +- `tests/providers/opencode-go-session-header.test.ts` — unchanged. +- `tests/providers/opencode-zen-rate-limit.test.ts` — unchanged. +- `tests/providers/provider-connection-test.test.ts` — unchanged. +- `tests/providers/provider-model-discovery-contract.test.ts` — unchanged. +- `tests/providers/provider-registry-parity.test.ts` — unchanged. +- `tests/providers/provider-static-model-discovery.test.ts` — unchanged. +- `tests/providers/qwen38-preserve-reasoning.test.ts` — unchanged. +- `tests/providers/sambanova-nebius-provider.test.ts` — unchanged. +- `tests/providers/xai/xai-transport.test.ts` — unchanged. +- `tests/providers/zhipu-bigmodel-provider.test.ts` — unchanged. +- `tests/responses/openai-responses-passthrough.test.ts` — unchanged. +- `tests/responses/responses-reasoning-summary-passthrough.test.ts` — unchanged. +- `tests/responses/responses-routed-web-search-fields.test.ts` — unchanged. +- `tests/responses/responses-stateless-dangling-call-repair.test.ts` — unchanged. +- `tests/responses/responses-terminal-repair.test.ts` — unchanged. +- `tests/routing/fastwire-policy.test.ts` — unchanged. +- `tests/routing/routing-capability-model-matching.test.ts` — unchanged. +- `tests/routing/routing-compatibility-auth-identity.test.ts` — unchanged. +- `tests/service/service-tier-capability.test.ts` — unchanged. +- `tests/vision/vision-sidecar-e2e.test.ts` — unchanged. + +Text-oracle classification: + +- Direct source-text readers of `src/providers/registry.ts`: **none found** by full-path, basename and segmented-path searches. `001_stale_check.md`'s count 1 is not accepted as a real oracle: `tests/routing/routing-compatibility-model-matching.test.ts:15` only mentions the source path in a comment, does not read it, and tests catalog model matching through other modules. Unchanged. This agrees with lane 012's inspected conclusion. +- `tests/lab/core-lab-boundary.test.ts:69` reads each transitively reached runtime source via `current`; it already follows re-exports/imports, so new data/destination leaves are automatically scanned. **Unchanged**, no retarget and no add-leaf-to-scan-list; leave PROTECTED at line 20 untouched. This is a graph-boundary oracle, not a provider-value text oracle. +- Fixture reads such as `tests/providers/nscale-vultr-provider.test.ts:28–29`, `tests/providers/commandcode-provider.test.ts:23`, and catalog-cache reads at `tests/codex-integration/codex-catalog.test.ts:3063` read JSON data, not the split TypeScript source. Unchanged. + +Guards to drive red once in the future implementation C phase: temporarily duplicate an entry id and then swap adjacent key-provider positions; `tests/providers/provider-registry-parity.test.ts:44–46` (uniqueness) and `:50–51` (ordered keys) must fail respectively. Restore the exact intended content and rerun. Also perturb one moved model modality in the gateway leaf and confirm the matching ClinePass/NIM test fails. No assertion removal, fixture regeneration to hide a mismatch, or weakened scan. For the recursive boundary guard, temporarily add a forbidden Lab edge to a new reachable runtime leaf (not a PROTECTED root), observe failure, remove it, and rerun. These are planned commands, not executed evidence. + +## Verification + +Instantiate `002_layer_map.md` → **Per-layer gate** at this layer's exact tip. This delegated turn is docs-only: do not run these now. Remote full-suite execution, branch creation and PR publication belong to the parent/executor, not this drafting task. + +```sh +bun run typecheck +bun test tests/providers +bun test tests/routing/fastwire-policy.test.ts tests/routing/routing-capability-model-matching.test.ts tests/routing/routing-compatibility-auth-identity.test.ts tests/service/service-tier-capability.test.ts +bun test tests/adapters/openai tests/adapters/google tests/adapters/anthropic/anthropic-hardening.test.ts tests/adapters/adapter-tool-conformance.test.ts tests/adapters/empty-tool-output-annotation.test.ts +bun test tests/codex-integration/codex-catalog.test.ts tests/codex-integration/catalog-vision-sidecar-modalities.test.ts tests/codex-integration/codex-gather-authority.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/gui/alibaba-intl-token-plan.test.ts tests/gui/provider-payload.test.ts tests/gui/qwen-cloud-endpoints.test.ts tests/gui/tencent-siliconflow-providers.test.ts tests/gui/volcengine-providers.test.ts +bun test tests/responses/openai-responses-passthrough.test.ts tests/responses/responses-reasoning-summary-passthrough.test.ts tests/responses/responses-routed-web-search-fields.test.ts tests/responses/responses-stateless-dangling-call-repair.test.ts tests/responses/responses-terminal-repair.test.ts tests/images/gemini-inline.test.ts tests/vision/vision-sidecar-e2e.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/providers/registry/frontier-models.ts src/providers/registry/reasoning-models.ts src/providers/registry/coding-plan-models.ts src/providers/registry/kimi-models.ts src/providers/registry/nim-models.ts src/providers/registry/gateway-models.ts src/providers/registry.ts +rg -n 'from "[^"]*/registry"' src gui/src scripts tests | wc -l +git diff --check +# Remote only, after parent confirms this checkout is dedicated to the layer: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-providers-registry-a && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The 002 grep is a trend signal, not an exact module-resolution count: it omits `./registry`, dynamic imports and type-only ownership corrections. Compare the resolved importer list as well: 134 baseline callers; 134 after L2, 133 from L3 solely because fastwire now imports contracts. Run no repository-wide local suite. Every local focused group above must show zero failures; typecheck/privacy/diff checks must exit zero. The remote pipeline's final `tail` exit status alone is not proof of Bun success: retain the complete log and Bun exit status (pipefail or PIPESTATUS in the executor shell), exact tested commit, and pass/fail totals. Record exact-head CI rollup before claiming PR-ready. No passes are claimed here. + +Static architecture verification is separate from typecheck: use the installed ast-grep import/export scan, resolve relative .ts/.tsx/index paths, include type-only edges and compare return paths to the baseline witnesses in Module-level state and cycles. Reject any new leaf-to-facade edge or new SCC; unresolved existing strict cycle constraints go back to the parent. Compare moved AST bodies/literal arrays with original spans (permit only import/export wiring, indentation, and array wrapper/spread scaffolding). Keep exported function signatures and original-path runtime export names identical. + +## Accept criteria + +1. Before implementation, the parent explicitly resolves the ≤500 changed-source-line contradiction; the fixed three registry parts are not claimed to satisfy that cap. +2. All 146 original top-level declarations have one inventory row and one owner; original exported name/type/signature sets are unchanged. +3. Exactly six new leaves in this layer, each ≤400 physical lines; residual is 2429 with the named successor layer when over 400. +4. All model literals, metadata maps, object aliases, entry field requiredness and original entry order match origin/dev; retained Antigravity remains between the two gateway arrays. +5. PROVIDER_REGISTRY is allocated once; the FastWire validation loop remains one eager post-construction loop; no new locks, caches, or state copies. +6. All 134 legacy module importers remain unchanged. Re-export statements do not stand in for local type/value imports. +7. No new leaf-to-facade/type cycle; baseline FastWire and Antigravity cycle dispositions are explicit. Do not mark a globally strict zero-cycle gate passed while a baseline witness remains. +8. All test dispositions and restored red-once checks are satisfied; instantiated local focused/privacy/type gates and remote full suite have fresh exact-tip evidence. +9. PR body uses the repository template with this complete four-layer map, correct parent base, own-layer verification, no Closes reference and no merge. + +## PR + +Title: `refactor(providers): extract private model metadata (split S02 L2/4)` + +Branch: `codex/split-providers-registry-a`. Base: `dev`. Closes: **none**. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), recording only this layer's exact-tip evidence. Review only this layer's diff. Placeholder PR numbers below are intentional planning references, not opened PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S02-L1 | separate OpenAI destination classification | `codex/split-providers-openai-tiers` | `dev` | destination predicates and migration parity | +| 2 | #TBD-S02-L2 | **Current: extract private model metadata** | `codex/split-providers-registry-a` | `dev` | model values and single ownership | +| 3 | #TBD-S02-L3 | extract registry contracts and primary entries | `codex/split-providers-registry-b` | `codex/split-providers-registry-a` | types, initial entries, FastWire import | +| 4 | #TBD-S02-L4 | finish ordered registry entry extraction | `codex/split-providers-registry-c` | `codex/split-providers-registry-b` | tail ordering and final size | + +Base: dev — no dependency on lower layers; this layer is the root of the chain 060 → 070 (transitively based on it), so any change here cascades into both layers with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). Publication is parent-owned; merges remain prohibited for this split train. diff --git a/devlog/_plan/260905_now_split_train/060_providers_registry_b.md b/devlog/_plan/260905_now_split_train/060_providers_registry_b.md new file mode 100644 index 0000000000..19e8fc47f2 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/060_providers_registry_b.md @@ -0,0 +1,461 @@ +# 060 — S02 providers L3/4 + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Classification: C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture determine size, compatibility and state ownership; parent owns loop/goal/orchestration. +- Goal: extract registry contracts and primary entries, preserving every historical export and observable behavior of `src/providers/registry.ts`. +- Non-goals: no model refresh, endpoint/auth-policy changes, validation redesign, caching, new runtime dependency, bug fix, generated metadata rewrite, repository-wide local test, merge, release or deployment. Existing behavior stays literal, including comments explaining it. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current task verifies documentation only, not runtime correctness. +- Stop: this plan is complete when the inventory, ownership, exact wiring, test disposition and count ledger are consistent; execution stops only after its own tip passes the instantiated gate and records exact-head CI. Do not defer a failing layer upward. +- Escalation: execution is conditional: 3,250 → ≤400 requires removing at least 2,850 original lines; three registry layers capped at 500 cannot remove that much even if additions are free. Ask the parent to explicitly waive the per-layer move-volume cap or expand 002; do not assert these three layers meet it. Also obtain authorization for the one FastWire type-import edit in L3 and disposition the pre-existing Antigravity type cycle under the strict cycle rule. + +Structural decision: the 3,250-line module combines contracts, private model metadata, ordered provider rows and lookup policy. Move the lowest-fan-in private model groups first, then contracts plus entry chunks, retaining the public facade and its lookup/validation code. Rejected alternatives: doing nothing/configuring cannot meet the line limit; deleting declarations would change behavior; changing all consumer imports would widen churn; a new provider framework or generic utils barrel is unnecessary. Existing `src/types.ts → src/types/*`, `src/config/*.ts`, and `src/codex/catalog.ts → src/codex/catalog/*` establish kebab-case co-located leaf convention. Keep legacy facades as explicit compatibility boundaries; no new index.ts or export-star barrel. + +## Symbol inventory + +Basis: `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549`. Every range in this document is an original-source line range, not the intermediate branch's shifted coordinates. `git diff origin/dev -- src/providers/registry.ts` was empty. + +Ranges were measured with `sg run --lang typescript --kind --json=compact src/providers/registry.ts`, taking column-zero export/lexical/function/interface/type-alias/class declarations. Imports are listed separately below; the inventory does not confuse nested declarations with ESM state. + +Consumer count = distinct `rg -l -w ''` files among resolved static/dynamic importers of this exact module under `src gui/src scripts tests` (`*.ts`/`*.tsx`), excluding the defining file. This is textual fan-in within the importer set, not call frequency. Private symbols have zero external import consumers; coincident names/comments elsewhere are excluded. Importer discovery starts with `rg -l 'registry' src gui/src scripts tests` and resolves each relative specifier, so other registries do not count. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ProviderAuthKind` | type | 25–25 | yes | 1 | registry/contracts.ts (L3) | +| `MetadataModelIdNormalize` | type | 26–26 | yes | 0 | registry/contracts.ts (L3) | +| `InboundWire` | type | 33–33 | yes | 7 | registry/contracts.ts (L3) | +| `ModelWireDefault` | type | 39–45 | yes | 2 | registry/contracts.ts (L3) | +| `ResponsesTerminalRepairPolicy` | interface | 47–50 | yes | 2 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryScalar` | type | 52–52 | yes | 1 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryPredicate` | type | 54–74 | yes | 1 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryFilter` | interface | 76–83 | yes | 2 | registry/contracts.ts (L3) | +| `ProviderModelDiscoverySharedSpec` | interface | 85–99 | no | 0 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryLocation` | type | 101–116 | no | 0 | registry/contracts.ts (L3) | +| `ProviderModelDiscoverySpec` | type | 122–122 | yes | 4 | registry/contracts.ts (L3) | +| `ProviderRegistryEntry` | interface | 124–330 | yes | 6 | registry/contracts.ts (L3) | +| `ProviderConfigSeed` | type | 332–342 | yes | 0 | registry/contracts.ts (L3) | +| `ANTHROPIC_MODELS` | const | 350–350 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_MODEL_CONTEXT_WINDOWS` | const | 351–351 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS` | const | 355–355 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_REASONING_EFFORTS` | const | 380–380 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_MODEL_REASONING_EFFORTS` | const | 381–383 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_53_MODELS` | const | 399–399 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_52_MODELS` | const | 400–400 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_MODELS` | const | 401–401 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_SIDECAR_VISION_MODELS` | const | 416–416 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_52_REASONING_EFFORTS` | const | 417–417 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_53_REASONING_EFFORTS` | const | 425–425 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_REASONING_EFFORTS` | const | 427–430 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_MODELS` | const | 433–439 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_MODEL_CONTEXT_WINDOWS` | const | 440–442 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_M3_REASONING_EFFORTS` | const | 443–443 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_M3_REASONING_EFFORT_MAP` | const | 444–452 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_GPT56_MODELS` | const | 453–453 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_GPT56_PRO_MODELS` | const | 454–454 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_CONTEXT_WINDOW` | const | 455–455 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_CONTEXT_WINDOWS` | const | 456–459 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_MAX_INPUT_TOKENS` | const | 460–463 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_VIRTUAL_MODELS` | const | 464–468 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_REASONING_EFFORTS` | const | 469–469 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_REASONING_EFFORTS` | const | 482–482 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_REASONING_EFFORT_MAP` | const | 490–492 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_CONTEXT_WINDOW` | const | 494–494 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_MODELS` | const | 495–495 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_MODELS` | const | 507–507 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_CONTEXT_WINDOWS` | const | 508–511 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_MAX_INPUT_TOKENS` | const | 512–515 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_REASONING_EFFORTS` | const | 524–526 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_MODELS` | const | 527–527 | no | 0 | registry/frontier-models.ts (L2) | +| `XAI_MODELS` | const | 528–537 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_CONTEXT_WINDOW` | const | 540–540 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_CONTEXT_WINDOWS` | const | 541–545 | no | 0 | registry/frontier-models.ts (L2) | +| `THINKING_TOGGLE_EFFORTS` | const | 553–553 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_TOGGLE_MAP` | const | 554–562 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_GO_THINKING_TOGGLE_MODELS` | const | 563–565 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_TEXT_MODELS` | const | 574–574 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_MODELS` | const | 575–575 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_INPUT_MODALITIES` | const | 576–579 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS` | const | 580–580 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_BUDGET_EFFORTS` | const | 581–581 | no | 0 | registry/reasoning-models.ts (L2) | +| `QWEN38_REASONING_EFFORTS` | const | 584–584 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_BUDGET_MODELS` | const | 585–588 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_GO_THINKING_BUDGET_MODELS` | const | 589–589 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_THINKING_MODELS` | const | 590–590 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_VISION_PREVIEW_MODEL` | const | 597–597 | no | 0 | registry/reasoning-models.ts (L2) | +| `COMMAND_CODE_IMAGE_MODELS` | const | 607–617 | no | 0 | registry/reasoning-models.ts (L2) | +| `COMMAND_CODE_MODEL_INPUT_MODALITIES` | const | 618–619 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_FREE_DEEPSEEK_MODELS` | const | 620–620 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_ZEN_TEXT_ONLY_MODELS` | const | 641–648 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_FLASH_THINKING_EFFORTS` | const | 672–672 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_PRO_THINKING_EFFORTS` | const | 673–673 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_PRO_REASONING_MAP` | const | 674–680 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_FLASH_REASONING_MAP` | const | 681–687 | no | 0 | registry/reasoning-models.ts (L2) | +| `isDeepseekFlashModel` | const | 695–696 | no | 0 | registry/reasoning-models.ts (L2) | +| `deepseekThinkingEffortsFor` | const | 697–698 | no | 0 | registry/reasoning-models.ts (L2) | +| `deepseekReasoningMapFor` | const | 699–700 | no | 0 | registry/reasoning-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_MODELS` | const | 705–708 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_QWEN_MODELS` | const | 709–711 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_INPUT_MODALITIES` | const | 712–721 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_MODELS` | const | 727–733 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS` | const | 734–736 | no | 0 | registry/coding-plan-models.ts (L2) | +| `TENCENT_CODING_PLAN_MODELS` | const | 743–743 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_ARK_MODELS` | const | 758–769 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_DOUBAO_THINKING_MODELS` | const | 770–774 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_CODING_PLAN_MODELS` | const | 775–785 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_AGENT_PLAN_MODELS` | const | 786–795 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_PLAN_INPUT_MODALITIES` | const | 796–802 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_PLAN_TEXT_ONLY_MODELS` | const | 806–814 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES` | const | 815–833 | no | 0 | registry/coding-plan-models.ts (L2) | +| `KIMI_K3_STANDARD_CONTEXT_WINDOW` | const | 841–841 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_K3_1M_CONTEXT_WINDOW` | const | 842–842 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_MODELS` | const | 843–843 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_LEGACY_API_MODELS` | const | 844–844 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODELS` | const | 845–845 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_MODELS` | const | 846–846 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_THINKING_MODELS` | const | 847–847 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_NO_REASONING_MODELS` | const | 848–848 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_NO_REASONING_MODELS` | const | 849–849 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_REASONING_EFFORTS` | const | 850–850 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_REASONING_EFFORT_MAP` | const | 851–858 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_REASONING_EFFORTS` | const | 859–861 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_DEFAULT_REASONING_EFFORTS` | const | 862–864 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_REASONING_EFFORT_MAPS` | const | 865–867 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_REASONING_EFFORTS` | const | 868–870 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_LOCKED_PARAMETER_MODELS` | const | 871–871 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS` | const | 872–872 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODEL_CONTEXT_WINDOWS` | const | 873–875 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODEL_INPUT_MODALITIES` | const | 876–876 | no | 0 | registry/kimi-models.ts (L2) | +| `NVIDIA_NIM_KIMI_THINKING_MODELS` | const | 881–883 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_KIMI_MODELS` | const | 884–887 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_VISION_MODELS` | const | 910–920 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_VISION_INPUT_MODALITIES` | const | 926–928 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_NO_VISION_MODELS` | const | 939–958 | no | 0 | registry/nim-models.ts (L2) | +| `KIMI_CODING_MODEL_CONTEXT_WINDOWS` | const | 959–961 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_MODEL_INPUT_MODALITIES` | const | 962–964 | no | 0 | registry/kimi-models.ts (L2) | +| `NEURALWATT_REASONING_HISTORY_MODELS` | const | 965–970 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_FULL_REASONING_EFFORTS` | const | 979–979 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_REASONING_EFFORTS` | const | 980–990 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_REASONING_EFFORT_MAP` | const | 991–1000 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_DEFAULT_REASONING_EFFORTS` | const | 1001–1006 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_INPUT_MODALITIES` | const | 1007–1012 | no | 0 | registry/gateway-models.ts (L2) | +| `DIGITALOCEAN_CHAT_COMPLETION_MODELS` | const | 1023–1053 | no | 0 | registry/gateway-models.ts (L2) | +| `SCALEWAY_SERVERLESS_CHAT_MODELS` | const | 1054–1069 | no | 0 | registry/gateway-models.ts (L2) | +| `SCALEWAY_MODEL_INPUT_MODALITIES` | const | 1070–1072 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODELS` | const | 1073–1082 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_REASONING_EFFORTS` | const | 1083–1083 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_GLM_REASONING_EFFORTS` | const | 1084–1084 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_GLM_53_REASONING_EFFORTS` | const | 1087–1087 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_TEXT_ONLY_MODELS` | const | 1092–1092 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODEL_CONTEXT_WINDOWS` | const | 1093–1104 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODEL_INPUT_MODALITIES` | const | 1105–1107 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODELS` | const | 1108–1123 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODEL_CONTEXT_WINDOWS` | const | 1124–1138 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_IMAGE_MODELS` | const | 1139–1151 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODALITY_KNOWN_MODELS` | const | 1152–1152 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_TEXT_ONLY_MODELS` | const | 1153–1153 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODEL_INPUT_MODALITIES` | const | 1154–1156 | no | 0 | registry/gateway-models.ts (L2) | +| `PROVIDER_REGISTRY` | const | 1158–3056 | yes | 62 | residual; element leaves in L3/L4 | +| `providerRegistryFastWireError` | function | 3058–3062 | yes | 1 | residual original file | +| `getProviderRegistryEntry` | function | 3069–3071 | yes | 58 | residual original file | +| `mergeRegistryStaticHeaders` | function | 3089–3101 | yes | 2 | residual original file | +| `registryModelServiceTierCapabilityApplies` | function | 3104–3110 | yes | 4 | residual original file | +| `normalizedProviderEndpoint` | function | 3112–3121 | no | 0 | residual original file | +| `providerMatchesRegistryTransport` | function | 3131–3145 | yes | 9 | residual original file | +| `registryEntryForProviderDestination` | function | 3159–3171 | yes | 8 | residual original file | +| `providerModelWireDefault` | function | 3179–3199 | yes | 3 | residual original file | +| `providerModelResponsesUpstreamStreaming` | function | 3202–3210 | yes | 1 | residual original file | +| `providerModelResponsesTerminalRepair` | function | 3213–3224 | yes | 2 | residual original file | +| `providerCodexAccountMode` | function | 3231–3237 | yes | 25 | residual original file | +| `effectiveGoogleMode` | function | 3244–3250 | yes | 4 | residual original file | + +Imports at `src/providers/registry.ts:1–23` are dependencies, not additional declared public symbols; see exact residual imports below. The top-level `for` at 3064–3067 is inventoried as an effect in Module-level state and cycles. The `PROVIDER_REGISTRY` declaration is not duplicated: its individual object literals are the entry units detailed below. + +## Leaf partition + +Source paths below are all NEW under `src/providers/registry/`. The ranges are cut boundaries including nearby comments/blanks; symbol ranges above exclude leading comments. Leaf counts include imports and typed array wrappers, using one import statement per physical line. Never shorten source comments to hit the limit. + +### `src/providers/registry/contracts.ts` + +- Original ranges: `25–343`. +- Symbols: `ProviderAuthKind`, `MetadataModelIdNormalize`, `InboundWire`, `ModelWireDefault`, `ResponsesTerminalRepairPolicy`, `ProviderModelDiscoveryScalar`, `ProviderModelDiscoveryPredicate`, `ProviderModelDiscoveryFilter`, `ProviderModelDiscoverySharedSpec`, `ProviderModelDiscoveryLocation`, `ProviderModelDiscoverySpec`, `ProviderRegistryEntry`, `ProviderConfigSeed`. +- Expected lines: **322** (≤400). + +Own imports (complete): + +```ts +import type { CodexAccountMode, FastWire, OcxProviderConfig } from "../../types"; +import type { ProviderBaseUrlChoice } from "../base-url-choices"; +``` + +Move the 13 declarations from 25–342 plus trailing separator at 343 unchanged. The private shared-spec and location types remain private; 11 public types are re-exported below. This owner imports only provider/wire types and the existing BaseUrlChoice type, not runtime registry code. + +### `src/providers/registry/entries-accounts.ts` + +- Original ranges: `1159–1488`. +- Symbols: `ACCOUNT_ENTRIES`. +- Expected lines: **341** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +import { ANTHROPIC_MODELS, ANTHROPIC_MODEL_CONTEXT_WINDOWS, ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS, ANTHROPIC_MODEL_REASONING_EFFORTS, XAI_MODELS } from "./frontier-models"; +import { DEEPSEEK_VISION_PREVIEW_MODEL, COMMAND_CODE_MODEL_INPUT_MODALITIES } from "./reasoning-models"; +import { KIMI_CODING_MODELS, KIMI_THINKING_MODELS, KIMI_CODING_NO_REASONING_MODELS, KIMI_CODING_REASONING_EFFORTS, KIMI_CODING_DEFAULT_REASONING_EFFORTS, KIMI_CODING_REASONING_EFFORT_MAPS, KIMI_LOCKED_PARAMETER_MODELS, KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS, KIMI_CODING_MODEL_CONTEXT_WINDOWS, KIMI_CODING_MODEL_INPUT_MODALITIES } from "./kimi-models"; +import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "../kiro-models"; +import { CURSOR_NO_VISION_MODELS, CURSOR_STATIC_MODELS, cursorModelContextWindows, cursorModelDisplayNames, cursorModelIds, cursorModelInputModalities, cursorModelReasoningEfforts } from "../../adapters/cursor/discovery"; +import { cursorFastCapableBases } from "../../adapters/cursor/catalog"; +import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "../command-code-efforts"; +``` + +`ACCOUNT_ENTRIES` = `export const ACCOUNT_ENTRIES: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 1159–1488, followed by `];`. Entry ids in order: `openai` (1159–1169), `cursor` (1170–1205), `xai` (1206–1329), `command-code` (1330–1361), `anthropic` (1362–1380), `anthropic-apikey` (1381–1398), `kimi` (1399–1430), `kiro` (1431–1449), `nous` (1450–1488). No sorting, mapping, cloning, default filling or conditional inclusion. + +### `src/providers/registry/entries-frontier.ts` + +- Original ranges: `1489–1757`. +- Symbols: `FRONTIER_ENTRIES`. +- Expected lines: **277** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +import { ZAI_GLM_52_REASONING_EFFORTS, ZAI_GLM_53_REASONING_EFFORTS, OPENAI_GPT56_MODELS, OPENAI_GPT56_PRO_MODELS, OPENAI_API_GPT56_CONTEXT_WINDOWS, OPENAI_API_GPT56_MAX_INPUT_TOKENS, OPENAI_API_GPT56_VIRTUAL_MODELS, OPENAI_API_GPT56_REASONING_EFFORTS, META_MUSE_REASONING_EFFORTS, META_MUSE_REASONING_EFFORT_MAP, META_MUSE_CONTEXT_WINDOW, META_MUSE_MODELS, OPENAI_DAYBREAK_MODELS, OPENAI_DAYBREAK_CONTEXT_WINDOWS, OPENAI_DAYBREAK_MAX_INPUT_TOKENS, OPENAI_DAYBREAK_REASONING_EFFORTS } from "./frontier-models"; +import { THINKING_TOGGLE_EFFORTS, THINKING_TOGGLE_MAP, OPENCODE_GO_THINKING_TOGGLE_MODELS, THINKING_BUDGET_EFFORTS, QWEN38_REASONING_EFFORTS, THINKING_BUDGET_MODELS, OPENCODE_GO_THINKING_BUDGET_MODELS, DEEPSEEK_THINKING_MODELS, DEEPSEEK_VISION_PREVIEW_MODEL, deepseekThinkingEffortsFor, deepseekReasoningMapFor } from "./reasoning-models"; +import { KIMI_K3_STANDARD_CONTEXT_WINDOW, KIMI_CODING_K3_REASONING_EFFORTS, KIMI_CODING_K3_REASONING_EFFORT_MAP } from "./kimi-models"; +import { NEURALWATT_REASONING_HISTORY_MODELS, UMANS_MODELS, UMANS_REASONING_EFFORTS, UMANS_GLM_REASONING_EFFORTS, UMANS_GLM_53_REASONING_EFFORTS, UMANS_TEXT_ONLY_MODELS, UMANS_MODEL_CONTEXT_WINDOWS, UMANS_MODEL_INPUT_MODALITIES } from "./gateway-models"; +``` + +`FRONTIER_ENTRIES` = `export const FRONTIER_ENTRIES: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 1489–1757, followed by `];`. Entry ids in order: `openai-apikey` (1489–1516), `meta-model` (1525–1557), `meta-muse` (1566–1584), `umans` (1585–1610), `opencode-go` (1611–1705), `neuralwatt` (1706–1757). No sorting, mapping, cloning, default filling or conditional inclusion. + +### `src/providers/registry/entries-gateways.ts` + +- Original ranges: `1758–1902, 1904–2007`. +- Symbols: `GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY`, `GATEWAY_ENTRIES_AFTER_ANTIGRAVITY`. +- Expected lines: **259** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +import { OPENROUTER_GPT56_MODELS, OPENROUTER_GPT56_CONTEXT_WINDOWS } from "./frontier-models"; +import { DEEPSEEK_THINKING_MODELS, DEEPSEEK_VISION_PREVIEW_MODEL, deepseekThinkingEffortsFor, deepseekReasoningMapFor } from "./reasoning-models"; +import { CLINE_PASS_MODELS, CLINE_PASS_MODEL_CONTEXT_WINDOWS, CLINE_PASS_TEXT_ONLY_MODELS, CLINE_PASS_MODEL_INPUT_MODALITIES } from "./gateway-models"; +import { isCanonicalOpenRouterTarget } from "../openrouter-routing"; +``` + +`GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY` = `export const GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 1758–1902, followed by `];`. Entry ids in order: `openrouter` (1758–1784), `cline-pass` (1785–1812), `cline` (1816–1834), `orcarouter` (1835–1867), `bizrouter` (1868–1879), `groq` (1880–1880), `google` (1883–1899), `google-vertex` (1902–1902). No sorting, mapping, cloning, default filling or conditional inclusion. + +`GATEWAY_ENTRIES_AFTER_ANTIGRAVITY` = `export const GATEWAY_ENTRIES_AFTER_ANTIGRAVITY: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 1904–2007, followed by `];`. Entry ids in order: `azure-openai` (1904–1904), `ollama` (1905–1905), `vllm` (1906–1906), `lm-studio` (1907–1907), `deepseek` (1908–2005), `cerebras` (2007–2007). No sorting, mapping, cloning, default filling or conditional inclusion. + +MODIFY `src/providers/registry.ts`: expected residual **1267 lines**. Over 400 intentionally; #c (070, L4) takes all remaining tail chunks. + +| Registry stage | Original lines removed, cumulative | Residual body incl. spread placeholders | Header/import/re-export lines | Expected residual | +|---|---:|---:|---:|---:| +| #a / L2 | 814 | 2,412 | 17 | 2,429 | +| #b / L3 | 1,981 | 1,249 | 18 | 1,267 | +| #c / L4 | 3,029 | 205 | 14 | 219 | + +Accounting starts from 3,250 original physical lines. Original header 1–24 is replaced by the explicit one-statement-per-line headers in each Re-export block. Body removals: 814 model lines in #a; 319 contract lines + 848 entry lines in #b; 1,048 entry lines in #c. #b inserts four spread lines; #c inserts four more. Thus #b reduces the prior residual by 1,162; #c by 1,048. These counts include retained comments/blanks and are exact for the specified compact headers; formatting may change them but must not exceed 400 for a new leaf. All 1,897 original array-content lines are accounted for: 848 + 1,048 moved, plus the one retained Antigravity line at 1903. Final original residual is 219, not an unplanned #d. + +Required adjacent executor change: `src/providers/fastwire.ts:10`, type-only import target `"./registry" → "./registry/contracts"`. Parent must approve this exact expansion; do not modify the FastWire implementation. All other consumers retain the original module path. + +The Antigravity row is deliberately retained at its original sequence point; do not merge the two gateway arrays around it. This avoids propagating the known Antigravity/catalog type cycle into a new entry leaf. Preserve the existing eager Cursor calculations and validation timing: no factories or async initialization. + +## Re-export block + +All 11 public types move in #b; the exact named type re-export is retained in #c. PROVIDER_REGISTRY and all 11 exported functions stay defined in the residual, so adding value re-exports for them would duplicate declarations. The complete expected residual import/re-export header is: + +```ts +import type { CodexAccountMode, OcxProviderConfig } from "../types"; +import type { InboundWire, ProviderRegistryEntry, ResponsesTerminalRepairPolicy } from "./registry/contracts"; +import { fastWireDeclarationError } from "./fastwire"; +import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "./antigravity-models"; +import { QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL, ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL, ALIBABA_CODING_BASE_URL_CHOICES, ALIBABA_CODING_INTL_BASE_URL, MOONSHOT_BASE_URL_CHOICES, MOONSHOT_INTL_BASE_URL } from "./base-url-choices"; +import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts"; +import { ZAI_GLM_53_MODELS, ZAI_GLM_5X_MODELS, ZAI_GLM_5X_SIDECAR_VISION_MODELS, ZAI_GLM_52_REASONING_EFFORTS, ZAI_GLM_53_REASONING_EFFORTS, ZAI_GLM_5X_REASONING_EFFORTS, MINIMAX_MODELS, MINIMAX_MODEL_CONTEXT_WINDOWS, MINIMAX_M3_REASONING_EFFORTS, MINIMAX_M3_REASONING_EFFORT_MAP } from "./registry/frontier-models"; +import { THINKING_TOGGLE_EFFORTS, THINKING_TOGGLE_MAP, ZHIPU_BIGMODEL_MODELS, ZHIPU_BIGMODEL_INPUT_MODALITIES, ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS, THINKING_BUDGET_EFFORTS, QWEN38_REASONING_EFFORTS, DEEPSEEK_THINKING_MODELS, DEEPSEEK_VISION_PREVIEW_MODEL, COMMAND_CODE_MODEL_INPUT_MODALITIES, OPENCODE_FREE_DEEPSEEK_MODELS, OPENCODE_ZEN_TEXT_ONLY_MODELS, deepseekThinkingEffortsFor, deepseekReasoningMapFor } from "./registry/reasoning-models"; +import { ALIBABA_TOKEN_PLAN_MODELS, ALIBABA_TOKEN_PLAN_QWEN_MODELS, ALIBABA_TOKEN_PLAN_INPUT_MODALITIES, ALIBABA_INTL_TOKEN_PLAN_MODELS, ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS, TENCENT_CODING_PLAN_MODELS, VOLCENGINE_ARK_MODELS, VOLCENGINE_DOUBAO_THINKING_MODELS, VOLCENGINE_CODING_PLAN_MODELS, VOLCENGINE_AGENT_PLAN_MODELS, VOLCENGINE_PLAN_INPUT_MODALITIES, VOLCENGINE_PLAN_TEXT_ONLY_MODELS, ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES } from "./registry/coding-plan-models"; +import { KIMI_API_MODELS, KIMI_CODING_MODELS, KIMI_THINKING_MODELS, KIMI_CODING_NO_REASONING_MODELS, KIMI_API_NO_REASONING_MODELS, KIMI_CODING_REASONING_EFFORTS, KIMI_CODING_DEFAULT_REASONING_EFFORTS, KIMI_CODING_REASONING_EFFORT_MAPS, KIMI_API_REASONING_EFFORTS, KIMI_LOCKED_PARAMETER_MODELS, KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS, KIMI_API_MODEL_CONTEXT_WINDOWS, KIMI_API_MODEL_INPUT_MODALITIES, KIMI_CODING_MODEL_CONTEXT_WINDOWS, KIMI_CODING_MODEL_INPUT_MODALITIES } from "./registry/kimi-models"; +import { NVIDIA_NIM_KIMI_THINKING_MODELS, NVIDIA_NIM_KIMI_MODELS, NVIDIA_NIM_VISION_INPUT_MODALITIES, NVIDIA_NIM_NO_VISION_MODELS } from "./registry/nim-models"; +import { BASETEN_MODEL_REASONING_EFFORTS, BASETEN_MODEL_REASONING_EFFORT_MAP, BASETEN_MODEL_DEFAULT_REASONING_EFFORTS, BASETEN_MODEL_INPUT_MODALITIES, DIGITALOCEAN_CHAT_COMPLETION_MODELS, SCALEWAY_SERVERLESS_CHAT_MODELS, SCALEWAY_MODEL_INPUT_MODALITIES } from "./registry/gateway-models"; +import { ACCOUNT_ENTRIES } from "./registry/entries-accounts"; +import { FRONTIER_ENTRIES } from "./registry/entries-frontier"; +import { GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY, GATEWAY_ENTRIES_AFTER_ANTIGRAVITY } from "./registry/entries-gateways"; + +export type { ProviderAuthKind, MetadataModelIdNormalize, InboundWire, ModelWireDefault, ResponsesTerminalRepairPolicy, ProviderModelDiscoveryScalar, ProviderModelDiscoveryPredicate, ProviderModelDiscoveryFilter, ProviderModelDiscoverySpec, ProviderRegistryEntry, ProviderConfigSeed } from "./registry/contracts"; +``` + +At each original chunk start, replace only its range with the corresponding named spread below. Replace original lines 1158–2007 with this exact prefix; append original lines 2008–3056 unchanged, including the original array terminator: + +```ts +export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ + ...ACCOUNT_ENTRIES, + ...FRONTIER_ENTRIES, + ...GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY, + { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] }, + ...GATEWAY_ENTRIES_AFTER_ANTIGRAVITY, +``` + +The Antigravity object above is the exact original line 1903. No new array is exported from the old path apart from the existing PROVIDER_REGISTRY binding. + +## Module-level state and cycles + +- `CLINE_PASS_IMAGE_MODELS` at `src/providers/registry.ts:1139–1151` has exactly one owner: `src/providers/registry/gateway-models.ts` from L2. It stays private there; its derived modality/text-only arrays stay with it. No setter, clone, lazy initializer, cache, or test hook is introduced. +- Every other top-level const is in the inventory. Model arrays/records are initialized once by their assigned leaf. Keep shared object identity, aliases (`KIMI_THINKING_MODELS` at 847, `KIMI_LOCKED_PARAMETER_MODELS` at 871), copies, and Object.fromEntries expressions unchanged. Readonly typing does not authorize freezing or cloning their values. +- `PROVIDER_REGISTRY` at 1158 remains one exported array in `registry.ts`. Entry leaves allocate each original entry object once; the facade spreads entry references in the historical sequence. The original validation loop at `src/providers/registry.ts:3064–3067` runs exactly once, after the complete array is constructed and before the facade import completes. It is a top-level effect, not a cache; never move it into each chunk or defer it. +- No top-level let, Map, WeakMap, lock or timer exists in either target. The `claimed` Set in `mergeRegistryStaticHeaders` at 3095 and callback-local Sets are invocation-local, not singleton state. No reset owner is needed. + +Dependency map: `src/router.ts:20`, `src/providers/derive.ts:8`, `src/config.ts:88`, and `src/codex/catalog/parsing.ts:14` consume the old boundary; it points to data leaves and contracts. Entry leaves point directly to their model leaves and existing vendor metadata owners, never to `../registry`. This is functional/data coupling; initialization/validation is the existing temporal coupling. No common mutable-state API is introduced. + +Existing type cycle: `registry.ts:2 → fastwire.ts:10 → registry.ts`. L2 leaves it unchanged; L3 moves contracts and changes only the type specifier in `src/providers/fastwire.ts:10` from `"./registry"` to `"./registry/contracts"`. This single adjacent source-file change is a required executor scope expansion for the parent to authorize, not performed by this documentation task. It reduces legacy-path importer count from 134 to 133; all other legacy consumers and all 78 test/support importers stay put. Do not pretend the literal unchanged-importer-count line in 002 can apply to this intentional one-edge repair. + +A second, pre-existing type-containing cycle is `registry.ts:4 → antigravity-models.ts:2 → codex/model-cache.ts:10 → codex/catalog.ts:3 → codex/catalog/parsing.ts:13 → providers/derive.ts:8 → registry.ts`. Keep the complete `google-antigravity` object at `registry.ts:1903` and its existing import in the facade, between the two gateway arrays. Moving it into an entry leaf would put that new leaf into the existing SCC. No new leaf imports Antigravity. The known vendor dependencies remain real shared owners (KIRO at src/providers/kiro-models.ts:1; Command Code at src/providers/command-code-efforts.ts:1; Cursor discovery/catalog at src/adapters/cursor/discovery.ts:1–8), not copied snapshots. A direct import of CatalogModel from parsing would still reach derive and would not fix this cycle. Strict all-graph zero-cycle acceptance needs a separately scoped type-owner repair; report this to the parent rather than silently expanding S02 or claiming the graph is globally acyclic. Compare baseline and tip graphs including erased type edges; no new SCC may contain a planned leaf. No lazy-import workaround. + +## Tests + +Resolved `rg -l` importer list below: 77 test files plus one test helper (78 files). Each is **unchanged** in every layer: it continues importing the historical facade, including the dynamic import at `tests/providers/qwen38-preserve-reasoning.test.ts:106` and child-process import text at `tests/adapters/openai/openai-provider-option-e2e.test.ts:261`. + +- `tests/adapters/adapter-tool-conformance.test.ts` — unchanged. +- `tests/adapters/anthropic/anthropic-hardening.test.ts` — unchanged. +- `tests/adapters/empty-tool-output-annotation.test.ts` — unchanged. +- `tests/adapters/google/antigravity-static-catalog.test.ts` — unchanged. +- `tests/adapters/google/gemini-37-flash-migration.test.ts` — unchanged. +- `tests/adapters/google/google-hardening.test.ts` — unchanged. +- `tests/adapters/openai/openai-api-virtual-models.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option-e2e.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option.test.ts` — unchanged. +- `tests/codex-integration/catalog-vision-sidecar-modalities.test.ts` — unchanged. +- `tests/codex-integration/codex-catalog.test.ts` — unchanged. +- `tests/codex-integration/codex-gather-authority.test.ts` — unchanged. +- `tests/codex-integration/compatibility-manifest.test.ts` — unchanged. +- `tests/gui/alibaba-intl-token-plan.test.ts` — unchanged. +- `tests/gui/provider-payload.test.ts` — unchanged. +- `tests/gui/qwen-cloud-endpoints.test.ts` — unchanged. +- `tests/gui/tencent-siliconflow-providers.test.ts` — unchanged. +- `tests/gui/volcengine-providers.test.ts` — unchanged. +- `tests/helpers/provider-registry-discovery.ts` — unchanged. +- `tests/images/gemini-inline.test.ts` — unchanged. +- `tests/providers/baseten-provider.test.ts` — unchanged. +- `tests/providers/chutes-provider.test.ts` — unchanged. +- `tests/providers/cline-pass-provider.test.ts` — unchanged. +- `tests/providers/cline-pass-reasoning-efforts.test.ts` — unchanged. +- `tests/providers/cline-provider.test.ts` — unchanged. +- `tests/providers/command-code-provider.test.ts` — unchanged. +- `tests/providers/commandcode-provider.test.ts` — unchanged. +- `tests/providers/cursor/cursor-display-names.test.ts` — unchanged. +- `tests/providers/cursor/cursor-fast-listing.test.ts` — unchanged. +- `tests/providers/cursor/cursor-fast-tier.test.ts` — unchanged. +- `tests/providers/deepinfra-provider.test.ts` — unchanged. +- `tests/providers/deepseek-inbound-wire.test.ts` — unchanged. +- `tests/providers/deepseek-reasoning-replay.test.ts` — unchanged. +- `tests/providers/deepseek-responses-item-id-repair.test.ts` — unchanged. +- `tests/providers/digitalocean-scaleway-provider.test.ts` — unchanged. +- `tests/providers/fast-row-ingress.test.ts` — unchanged. +- `tests/providers/featherless-provider.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-stream-contract.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-wire-defaults.test.ts` — unchanged. +- `tests/providers/hyperbolic-provider.test.ts` — unchanged. +- `tests/providers/kiro/kiro-adapter.test.ts` — unchanged. +- `tests/providers/meta-model-api-provider.test.ts` — unchanged. +- `tests/providers/meta-muse-oauth.test.ts` — unchanged. +- `tests/providers/mimo-effort.test.ts` — unchanged. +- `tests/providers/mimo-free-provider.test.ts` — unchanged. +- `tests/providers/mimo-token-plan-provider.test.ts` — unchanged. +- `tests/providers/model-rename-migration.test.ts` — unchanged. +- `tests/providers/moonshot-endpoints.test.ts` — unchanged. +- `tests/providers/muse-spark-web-search-compat.test.ts` — unchanged. +- `tests/providers/novita-provider.test.ts` — unchanged. +- `tests/providers/nscale-vultr-provider.test.ts` — unchanged. +- `tests/providers/nvidia-nim-hardening.test.ts` — unchanged. +- `tests/providers/ollama/ollama-native.test.ts` — unchanged. +- `tests/providers/opencode-free-provider.test.ts` — unchanged. +- `tests/providers/opencode-go-grok46-responses.test.ts` — unchanged. +- `tests/providers/opencode-go-luna-wire.test.ts` — unchanged. +- `tests/providers/opencode-go-muse-context.test.ts` — unchanged. +- `tests/providers/opencode-go-muse-vision.test.ts` — unchanged. +- `tests/providers/opencode-go-session-header.test.ts` — unchanged. +- `tests/providers/opencode-zen-rate-limit.test.ts` — unchanged. +- `tests/providers/provider-connection-test.test.ts` — unchanged. +- `tests/providers/provider-model-discovery-contract.test.ts` — unchanged. +- `tests/providers/provider-registry-parity.test.ts` — unchanged. +- `tests/providers/provider-static-model-discovery.test.ts` — unchanged. +- `tests/providers/qwen38-preserve-reasoning.test.ts` — unchanged. +- `tests/providers/sambanova-nebius-provider.test.ts` — unchanged. +- `tests/providers/xai/xai-transport.test.ts` — unchanged. +- `tests/providers/zhipu-bigmodel-provider.test.ts` — unchanged. +- `tests/responses/openai-responses-passthrough.test.ts` — unchanged. +- `tests/responses/responses-reasoning-summary-passthrough.test.ts` — unchanged. +- `tests/responses/responses-routed-web-search-fields.test.ts` — unchanged. +- `tests/responses/responses-stateless-dangling-call-repair.test.ts` — unchanged. +- `tests/responses/responses-terminal-repair.test.ts` — unchanged. +- `tests/routing/fastwire-policy.test.ts` — unchanged. +- `tests/routing/routing-capability-model-matching.test.ts` — unchanged. +- `tests/routing/routing-compatibility-auth-identity.test.ts` — unchanged. +- `tests/service/service-tier-capability.test.ts` — unchanged. +- `tests/vision/vision-sidecar-e2e.test.ts` — unchanged. + +Text-oracle classification: + +- Direct source-text readers of `src/providers/registry.ts`: **none found** by full-path, basename and segmented-path searches. `001_stale_check.md`'s count 1 is not accepted as a real oracle: `tests/routing/routing-compatibility-model-matching.test.ts:15` only mentions the source path in a comment, does not read it, and tests catalog model matching through other modules. Unchanged. This agrees with lane 012's inspected conclusion. +- `tests/lab/core-lab-boundary.test.ts:69` reads each transitively reached runtime source via `current`; it already follows re-exports/imports, so new data/destination leaves are automatically scanned. **Unchanged**, no retarget and no add-leaf-to-scan-list; leave PROTECTED at line 20 untouched. This is a graph-boundary oracle, not a provider-value text oracle. +- Fixture reads such as `tests/providers/nscale-vultr-provider.test.ts:28–29`, `tests/providers/commandcode-provider.test.ts:23`, and catalog-cache reads at `tests/codex-integration/codex-catalog.test.ts:3063` read JSON data, not the split TypeScript source. Unchanged. + +Guards to drive red once in the future implementation C phase: temporarily duplicate an entry id and then swap adjacent key-provider positions; `tests/providers/provider-registry-parity.test.ts:44–46` (uniqueness) and `:50–51` (ordered keys) must fail respectively. Restore the exact intended content and rerun. After entry extraction, perturb one moved entry field and confirm its existing provider parity assertion fails through the old import path. No assertion removal, fixture regeneration to hide a mismatch, or weakened scan. For the recursive boundary guard, temporarily add a forbidden Lab edge to a new reachable runtime leaf (not a PROTECTED root), observe failure, remove it, and rerun. These are planned commands, not executed evidence. + +## Verification + +Instantiate `002_layer_map.md` → **Per-layer gate** at this layer's exact tip. This delegated turn is docs-only: do not run these now. Remote full-suite execution, branch creation and PR publication belong to the parent/executor, not this drafting task. + +```sh +bun run typecheck +bun test tests/providers +bun test tests/routing/fastwire-policy.test.ts tests/routing/routing-capability-model-matching.test.ts tests/routing/routing-compatibility-auth-identity.test.ts tests/service/service-tier-capability.test.ts +bun test tests/adapters/openai tests/adapters/google tests/adapters/anthropic/anthropic-hardening.test.ts tests/adapters/adapter-tool-conformance.test.ts tests/adapters/empty-tool-output-annotation.test.ts +bun test tests/codex-integration/codex-catalog.test.ts tests/codex-integration/catalog-vision-sidecar-modalities.test.ts tests/codex-integration/codex-gather-authority.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/gui/alibaba-intl-token-plan.test.ts tests/gui/provider-payload.test.ts tests/gui/qwen-cloud-endpoints.test.ts tests/gui/tencent-siliconflow-providers.test.ts tests/gui/volcengine-providers.test.ts +bun test tests/responses/openai-responses-passthrough.test.ts tests/responses/responses-reasoning-summary-passthrough.test.ts tests/responses/responses-routed-web-search-fields.test.ts tests/responses/responses-stateless-dangling-call-repair.test.ts tests/responses/responses-terminal-repair.test.ts tests/images/gemini-inline.test.ts tests/vision/vision-sidecar-e2e.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/providers/registry/frontier-models.ts src/providers/registry/reasoning-models.ts src/providers/registry/coding-plan-models.ts src/providers/registry/kimi-models.ts src/providers/registry/nim-models.ts src/providers/registry/gateway-models.ts src/providers/registry/entries-accounts.ts src/providers/registry/entries-frontier.ts src/providers/registry/entries-gateways.ts src/providers/registry/contracts.ts src/providers/registry.ts +rg -n 'from "[^"]*/registry"' src gui/src scripts tests | wc -l +git diff --check +# Remote only, after parent confirms this checkout is dedicated to the layer: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-providers-registry-b && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The 002 grep is a trend signal, not an exact module-resolution count: it omits `./registry`, dynamic imports and type-only ownership corrections. Compare the resolved importer list as well: 134 baseline callers; 134 after L2, 133 from L3 solely because fastwire now imports contracts. Run no repository-wide local suite. Every local focused group above must show zero failures; typecheck/privacy/diff checks must exit zero. The remote pipeline's final `tail` exit status alone is not proof of Bun success: retain the complete log and Bun exit status (pipefail or PIPESTATUS in the executor shell), exact tested commit, and pass/fail totals. Record exact-head CI rollup before claiming PR-ready. No passes are claimed here. + +Static architecture verification is separate from typecheck: use the installed ast-grep import/export scan, resolve relative .ts/.tsx/index paths, include type-only edges and compare return paths to the baseline witnesses in Module-level state and cycles. Reject any new leaf-to-facade edge or new SCC; unresolved existing strict cycle constraints go back to the parent. Compare moved AST bodies/literal arrays with original spans (permit only import/export wiring, indentation, and array wrapper/spread scaffolding). Keep exported function signatures and original-path runtime export names identical. + +## Accept criteria + +1. Before implementation, the parent explicitly resolves the ≤500 changed-source-line contradiction; the fixed three registry parts are not claimed to satisfy that cap. +2. All 146 original top-level declarations have one inventory row and one owner; original exported name/type/signature sets are unchanged. +3. Exactly four new leaves (contracts plus three entry files) in this layer, each ≤400 physical lines; residual is 1267 with the named successor layer when over 400. +4. All model literals, metadata maps, object aliases, entry field requiredness and original entry order match origin/dev; retained Antigravity remains between the two gateway arrays. +5. PROVIDER_REGISTRY is allocated once; the FastWire validation loop remains one eager post-construction loop; no new locks, caches, or state copies. +6. Only the authorized FastWire type-import edge moves to contracts; 133 remaining legacy importers and all 78 test/support importers remain unchanged. Re-export statements do not stand in for local type/value imports. +7. No new leaf-to-facade/type cycle; baseline FastWire and Antigravity cycle dispositions are explicit. Do not mark a globally strict zero-cycle gate passed while a baseline witness remains. +8. All test dispositions and restored red-once checks are satisfied; instantiated local focused/privacy/type gates and remote full suite have fresh exact-tip evidence. +9. PR body uses the repository template with this complete four-layer map, correct parent base, own-layer verification, no Closes reference and no merge. + +## PR + +Title: `refactor(providers): extract registry contracts and primary entries (split S02 L3/4)` + +Branch: `codex/split-providers-registry-b`. Base: `codex/split-providers-registry-a`. Closes: **none**. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), recording only this layer's exact-tip evidence. Review only this layer's diff. Placeholder PR numbers below are intentional planning references, not opened PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S02-L1 | separate OpenAI destination classification | `codex/split-providers-openai-tiers` | `dev` | destination predicates and migration parity | +| 2 | #TBD-S02-L2 | extract private model metadata | `codex/split-providers-registry-a` | `dev` | model values and single ownership | +| 3 | #TBD-S02-L3 | **Current: extract registry contracts and primary entries** | `codex/split-providers-registry-b` | `codex/split-providers-registry-a` | types, initial entries, FastWire import | +| 4 | #TBD-S02-L4 | finish ordered registry entry extraction | `codex/split-providers-registry-c` | `codex/split-providers-registry-b` | tail ordering and final size | + +Depends on #TBD-S02-L2. A rewrite of the real parent `codex/split-providers-registry-a` requires cascading this layer and re-verifying its base (DEV-STACK-02). Publication is parent-owned; merges remain prohibited for this split train. diff --git a/devlog/_plan/260905_now_split_train/070_providers_registry_c.md b/devlog/_plan/260905_now_split_train/070_providers_registry_c.md new file mode 100644 index 0000000000..d0b75e4645 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/070_providers_registry_c.md @@ -0,0 +1,456 @@ +# 070 — S02 providers L4/4 + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Classification: C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture determine size, compatibility and state ownership; parent owns loop/goal/orchestration. +- Goal: finish ordered registry entry extraction, preserving every historical export and observable behavior of `src/providers/registry.ts`. +- Non-goals: no model refresh, endpoint/auth-policy changes, validation redesign, caching, new runtime dependency, bug fix, generated metadata rewrite, repository-wide local test, merge, release or deployment. Existing behavior stays literal, including comments explaining it. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current task verifies documentation only, not runtime correctness. +- Stop: this plan is complete when the inventory, ownership, exact wiring, test disposition and count ledger are consistent; execution stops only after its own tip passes the instantiated gate and records exact-head CI. Do not defer a failing layer upward. +- Escalation: execution is conditional: 3,250 → ≤400 requires removing at least 2,850 original lines; three registry layers capped at 500 cannot remove that much even if additions are free. Ask the parent to explicitly waive the per-layer move-volume cap or expand 002; do not assert these three layers meet it. Also obtain authorization for the one FastWire type-import edit in L3 and disposition the pre-existing Antigravity type cycle under the strict cycle rule. + +Structural decision: the 3,250-line module combines contracts, private model metadata, ordered provider rows and lookup policy. Move the lowest-fan-in private model groups first, then contracts plus entry chunks, retaining the public facade and its lookup/validation code. Rejected alternatives: doing nothing/configuring cannot meet the line limit; deleting declarations would change behavior; changing all consumer imports would widen churn; a new provider framework or generic utils barrel is unnecessary. Existing `src/types.ts → src/types/*`, `src/config/*.ts`, and `src/codex/catalog.ts → src/codex/catalog/*` establish kebab-case co-located leaf convention. Keep legacy facades as explicit compatibility boundaries; no new index.ts or export-star barrel. + +## Symbol inventory + +Basis: `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549`. Every range in this document is an original-source line range, not the intermediate branch's shifted coordinates. `git diff origin/dev -- src/providers/registry.ts` was empty. + +Ranges were measured with `sg run --lang typescript --kind --json=compact src/providers/registry.ts`, taking column-zero export/lexical/function/interface/type-alias/class declarations. Imports are listed separately below; the inventory does not confuse nested declarations with ESM state. + +Consumer count = distinct `rg -l -w ''` files among resolved static/dynamic importers of this exact module under `src gui/src scripts tests` (`*.ts`/`*.tsx`), excluding the defining file. This is textual fan-in within the importer set, not call frequency. Private symbols have zero external import consumers; coincident names/comments elsewhere are excluded. Importer discovery starts with `rg -l 'registry' src gui/src scripts tests` and resolves each relative specifier, so other registries do not count. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ProviderAuthKind` | type | 25–25 | yes | 1 | registry/contracts.ts (L3) | +| `MetadataModelIdNormalize` | type | 26–26 | yes | 0 | registry/contracts.ts (L3) | +| `InboundWire` | type | 33–33 | yes | 7 | registry/contracts.ts (L3) | +| `ModelWireDefault` | type | 39–45 | yes | 2 | registry/contracts.ts (L3) | +| `ResponsesTerminalRepairPolicy` | interface | 47–50 | yes | 2 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryScalar` | type | 52–52 | yes | 1 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryPredicate` | type | 54–74 | yes | 1 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryFilter` | interface | 76–83 | yes | 2 | registry/contracts.ts (L3) | +| `ProviderModelDiscoverySharedSpec` | interface | 85–99 | no | 0 | registry/contracts.ts (L3) | +| `ProviderModelDiscoveryLocation` | type | 101–116 | no | 0 | registry/contracts.ts (L3) | +| `ProviderModelDiscoverySpec` | type | 122–122 | yes | 4 | registry/contracts.ts (L3) | +| `ProviderRegistryEntry` | interface | 124–330 | yes | 6 | registry/contracts.ts (L3) | +| `ProviderConfigSeed` | type | 332–342 | yes | 0 | registry/contracts.ts (L3) | +| `ANTHROPIC_MODELS` | const | 350–350 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_MODEL_CONTEXT_WINDOWS` | const | 351–351 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS` | const | 355–355 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_REASONING_EFFORTS` | const | 380–380 | no | 0 | registry/frontier-models.ts (L2) | +| `ANTHROPIC_MODEL_REASONING_EFFORTS` | const | 381–383 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_53_MODELS` | const | 399–399 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_52_MODELS` | const | 400–400 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_MODELS` | const | 401–401 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_SIDECAR_VISION_MODELS` | const | 416–416 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_52_REASONING_EFFORTS` | const | 417–417 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_53_REASONING_EFFORTS` | const | 425–425 | no | 0 | registry/frontier-models.ts (L2) | +| `ZAI_GLM_5X_REASONING_EFFORTS` | const | 427–430 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_MODELS` | const | 433–439 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_MODEL_CONTEXT_WINDOWS` | const | 440–442 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_M3_REASONING_EFFORTS` | const | 443–443 | no | 0 | registry/frontier-models.ts (L2) | +| `MINIMAX_M3_REASONING_EFFORT_MAP` | const | 444–452 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_GPT56_MODELS` | const | 453–453 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_GPT56_PRO_MODELS` | const | 454–454 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_CONTEXT_WINDOW` | const | 455–455 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_CONTEXT_WINDOWS` | const | 456–459 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_MAX_INPUT_TOKENS` | const | 460–463 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_VIRTUAL_MODELS` | const | 464–468 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_API_GPT56_REASONING_EFFORTS` | const | 469–469 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_REASONING_EFFORTS` | const | 482–482 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_REASONING_EFFORT_MAP` | const | 490–492 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_CONTEXT_WINDOW` | const | 494–494 | no | 0 | registry/frontier-models.ts (L2) | +| `META_MUSE_MODELS` | const | 495–495 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_MODELS` | const | 507–507 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_CONTEXT_WINDOWS` | const | 508–511 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_MAX_INPUT_TOKENS` | const | 512–515 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENAI_DAYBREAK_REASONING_EFFORTS` | const | 524–526 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_MODELS` | const | 527–527 | no | 0 | registry/frontier-models.ts (L2) | +| `XAI_MODELS` | const | 528–537 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_CONTEXT_WINDOW` | const | 540–540 | no | 0 | registry/frontier-models.ts (L2) | +| `OPENROUTER_GPT56_CONTEXT_WINDOWS` | const | 541–545 | no | 0 | registry/frontier-models.ts (L2) | +| `THINKING_TOGGLE_EFFORTS` | const | 553–553 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_TOGGLE_MAP` | const | 554–562 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_GO_THINKING_TOGGLE_MODELS` | const | 563–565 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_TEXT_MODELS` | const | 574–574 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_MODELS` | const | 575–575 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_INPUT_MODALITIES` | const | 576–579 | no | 0 | registry/reasoning-models.ts (L2) | +| `ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS` | const | 580–580 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_BUDGET_EFFORTS` | const | 581–581 | no | 0 | registry/reasoning-models.ts (L2) | +| `QWEN38_REASONING_EFFORTS` | const | 584–584 | no | 0 | registry/reasoning-models.ts (L2) | +| `THINKING_BUDGET_MODELS` | const | 585–588 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_GO_THINKING_BUDGET_MODELS` | const | 589–589 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_THINKING_MODELS` | const | 590–590 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_VISION_PREVIEW_MODEL` | const | 597–597 | no | 0 | registry/reasoning-models.ts (L2) | +| `COMMAND_CODE_IMAGE_MODELS` | const | 607–617 | no | 0 | registry/reasoning-models.ts (L2) | +| `COMMAND_CODE_MODEL_INPUT_MODALITIES` | const | 618–619 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_FREE_DEEPSEEK_MODELS` | const | 620–620 | no | 0 | registry/reasoning-models.ts (L2) | +| `OPENCODE_ZEN_TEXT_ONLY_MODELS` | const | 641–648 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_FLASH_THINKING_EFFORTS` | const | 672–672 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_PRO_THINKING_EFFORTS` | const | 673–673 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_PRO_REASONING_MAP` | const | 674–680 | no | 0 | registry/reasoning-models.ts (L2) | +| `DEEPSEEK_FLASH_REASONING_MAP` | const | 681–687 | no | 0 | registry/reasoning-models.ts (L2) | +| `isDeepseekFlashModel` | const | 695–696 | no | 0 | registry/reasoning-models.ts (L2) | +| `deepseekThinkingEffortsFor` | const | 697–698 | no | 0 | registry/reasoning-models.ts (L2) | +| `deepseekReasoningMapFor` | const | 699–700 | no | 0 | registry/reasoning-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_MODELS` | const | 705–708 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_QWEN_MODELS` | const | 709–711 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_TOKEN_PLAN_INPUT_MODALITIES` | const | 712–721 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_MODELS` | const | 727–733 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS` | const | 734–736 | no | 0 | registry/coding-plan-models.ts (L2) | +| `TENCENT_CODING_PLAN_MODELS` | const | 743–743 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_ARK_MODELS` | const | 758–769 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_DOUBAO_THINKING_MODELS` | const | 770–774 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_CODING_PLAN_MODELS` | const | 775–785 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_AGENT_PLAN_MODELS` | const | 786–795 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_PLAN_INPUT_MODALITIES` | const | 796–802 | no | 0 | registry/coding-plan-models.ts (L2) | +| `VOLCENGINE_PLAN_TEXT_ONLY_MODELS` | const | 806–814 | no | 0 | registry/coding-plan-models.ts (L2) | +| `ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES` | const | 815–833 | no | 0 | registry/coding-plan-models.ts (L2) | +| `KIMI_K3_STANDARD_CONTEXT_WINDOW` | const | 841–841 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_K3_1M_CONTEXT_WINDOW` | const | 842–842 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_MODELS` | const | 843–843 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_LEGACY_API_MODELS` | const | 844–844 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODELS` | const | 845–845 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_MODELS` | const | 846–846 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_THINKING_MODELS` | const | 847–847 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_NO_REASONING_MODELS` | const | 848–848 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_NO_REASONING_MODELS` | const | 849–849 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_REASONING_EFFORTS` | const | 850–850 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_K3_REASONING_EFFORT_MAP` | const | 851–858 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_REASONING_EFFORTS` | const | 859–861 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_DEFAULT_REASONING_EFFORTS` | const | 862–864 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_REASONING_EFFORT_MAPS` | const | 865–867 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_REASONING_EFFORTS` | const | 868–870 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_LOCKED_PARAMETER_MODELS` | const | 871–871 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS` | const | 872–872 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODEL_CONTEXT_WINDOWS` | const | 873–875 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_API_MODEL_INPUT_MODALITIES` | const | 876–876 | no | 0 | registry/kimi-models.ts (L2) | +| `NVIDIA_NIM_KIMI_THINKING_MODELS` | const | 881–883 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_KIMI_MODELS` | const | 884–887 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_VISION_MODELS` | const | 910–920 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_VISION_INPUT_MODALITIES` | const | 926–928 | no | 0 | registry/nim-models.ts (L2) | +| `NVIDIA_NIM_NO_VISION_MODELS` | const | 939–958 | no | 0 | registry/nim-models.ts (L2) | +| `KIMI_CODING_MODEL_CONTEXT_WINDOWS` | const | 959–961 | no | 0 | registry/kimi-models.ts (L2) | +| `KIMI_CODING_MODEL_INPUT_MODALITIES` | const | 962–964 | no | 0 | registry/kimi-models.ts (L2) | +| `NEURALWATT_REASONING_HISTORY_MODELS` | const | 965–970 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_FULL_REASONING_EFFORTS` | const | 979–979 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_REASONING_EFFORTS` | const | 980–990 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_REASONING_EFFORT_MAP` | const | 991–1000 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_DEFAULT_REASONING_EFFORTS` | const | 1001–1006 | no | 0 | registry/gateway-models.ts (L2) | +| `BASETEN_MODEL_INPUT_MODALITIES` | const | 1007–1012 | no | 0 | registry/gateway-models.ts (L2) | +| `DIGITALOCEAN_CHAT_COMPLETION_MODELS` | const | 1023–1053 | no | 0 | registry/gateway-models.ts (L2) | +| `SCALEWAY_SERVERLESS_CHAT_MODELS` | const | 1054–1069 | no | 0 | registry/gateway-models.ts (L2) | +| `SCALEWAY_MODEL_INPUT_MODALITIES` | const | 1070–1072 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODELS` | const | 1073–1082 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_REASONING_EFFORTS` | const | 1083–1083 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_GLM_REASONING_EFFORTS` | const | 1084–1084 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_GLM_53_REASONING_EFFORTS` | const | 1087–1087 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_TEXT_ONLY_MODELS` | const | 1092–1092 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODEL_CONTEXT_WINDOWS` | const | 1093–1104 | no | 0 | registry/gateway-models.ts (L2) | +| `UMANS_MODEL_INPUT_MODALITIES` | const | 1105–1107 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODELS` | const | 1108–1123 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODEL_CONTEXT_WINDOWS` | const | 1124–1138 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_IMAGE_MODELS` | const | 1139–1151 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODALITY_KNOWN_MODELS` | const | 1152–1152 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_TEXT_ONLY_MODELS` | const | 1153–1153 | no | 0 | registry/gateway-models.ts (L2) | +| `CLINE_PASS_MODEL_INPUT_MODALITIES` | const | 1154–1156 | no | 0 | registry/gateway-models.ts (L2) | +| `PROVIDER_REGISTRY` | const | 1158–3056 | yes | 62 | residual; element leaves in L3/L4 | +| `providerRegistryFastWireError` | function | 3058–3062 | yes | 1 | residual original file | +| `getProviderRegistryEntry` | function | 3069–3071 | yes | 58 | residual original file | +| `mergeRegistryStaticHeaders` | function | 3089–3101 | yes | 2 | residual original file | +| `registryModelServiceTierCapabilityApplies` | function | 3104–3110 | yes | 4 | residual original file | +| `normalizedProviderEndpoint` | function | 3112–3121 | no | 0 | residual original file | +| `providerMatchesRegistryTransport` | function | 3131–3145 | yes | 9 | residual original file | +| `registryEntryForProviderDestination` | function | 3159–3171 | yes | 8 | residual original file | +| `providerModelWireDefault` | function | 3179–3199 | yes | 3 | residual original file | +| `providerModelResponsesUpstreamStreaming` | function | 3202–3210 | yes | 1 | residual original file | +| `providerModelResponsesTerminalRepair` | function | 3213–3224 | yes | 2 | residual original file | +| `providerCodexAccountMode` | function | 3231–3237 | yes | 25 | residual original file | +| `effectiveGoogleMode` | function | 3244–3250 | yes | 4 | residual original file | + +Imports at `src/providers/registry.ts:1–23` are dependencies, not additional declared public symbols; see exact residual imports below. The top-level `for` at 3064–3067 is inventoried as an effect in Module-level state and cycles. The `PROVIDER_REGISTRY` declaration is not duplicated: its individual object literals are the entry units detailed below. + +## Leaf partition + +Source paths below are all NEW under `src/providers/registry/`. The ranges are cut boundaries including nearby comments/blanks; symbol ranges above exclude leading comments. Leaf counts include imports and typed array wrappers, using one import statement per physical line. Never shorten source comments to hit the limit. + +### `src/providers/registry/entries-hosted.ts` + +- Original ranges: `2008–2346`. +- Symbols: `HOSTED_ENTRIES`. +- Expected lines: **346** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +import { DEEPSEEK_VISION_PREVIEW_MODEL, COMMAND_CODE_MODEL_INPUT_MODALITIES } from "./reasoning-models"; +import { BASETEN_MODEL_REASONING_EFFORTS, BASETEN_MODEL_REASONING_EFFORT_MAP, BASETEN_MODEL_DEFAULT_REASONING_EFFORTS, BASETEN_MODEL_INPUT_MODALITIES, DIGITALOCEAN_CHAT_COMPLETION_MODELS, SCALEWAY_SERVERLESS_CHAT_MODELS, SCALEWAY_MODEL_INPUT_MODALITIES } from "./gateway-models"; +import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "../command-code-efforts"; +``` + +`HOSTED_ENTRIES` = `export const HOSTED_ENTRIES: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 2008–2346, followed by `];`. Entry ids in order: `chutes` (2008–2041), `deepinfra` (2042–2062), `hyperbolic` (2063–2078), `nscale` (2079–2111), `vultr` (2112–2141), `baseten` (2142–2166), `commandcode` (2167–2204), `sambanova` (2205–2225), `nebius` (2226–2251), `digitalocean` (2252–2274), `scaleway` (2275–2299), `featherless` (2300–2346). No sorting, mapping, cloning, default filling or conditional inclusion. + +### `src/providers/registry/entries-regional.ts` + +- Original ranges: `2347–2664`. +- Symbols: `REGIONAL_ENTRIES`. +- Expected lines: **328** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +import { ZAI_GLM_53_MODELS, ZAI_GLM_5X_MODELS, ZAI_GLM_5X_SIDECAR_VISION_MODELS, ZAI_GLM_5X_REASONING_EFFORTS } from "./frontier-models"; +import { THINKING_TOGGLE_EFFORTS, THINKING_TOGGLE_MAP, ZHIPU_BIGMODEL_MODELS, ZHIPU_BIGMODEL_INPUT_MODALITIES, ZHIPU_BIGMODEL_THINKING_TOGGLE_MODELS, DEEPSEEK_THINKING_MODELS, deepseekThinkingEffortsFor, deepseekReasoningMapFor } from "./reasoning-models"; +import { TENCENT_CODING_PLAN_MODELS, VOLCENGINE_ARK_MODELS, VOLCENGINE_DOUBAO_THINKING_MODELS, VOLCENGINE_CODING_PLAN_MODELS, VOLCENGINE_AGENT_PLAN_MODELS, VOLCENGINE_PLAN_INPUT_MODALITIES, VOLCENGINE_PLAN_TEXT_ONLY_MODELS } from "./coding-plan-models"; +import { KIMI_API_MODELS, KIMI_API_NO_REASONING_MODELS, KIMI_API_REASONING_EFFORTS, KIMI_API_MODEL_CONTEXT_WINDOWS, KIMI_API_MODEL_INPUT_MODALITIES } from "./kimi-models"; +import { NVIDIA_NIM_KIMI_THINKING_MODELS, NVIDIA_NIM_KIMI_MODELS, NVIDIA_NIM_VISION_INPUT_MODALITIES, NVIDIA_NIM_NO_VISION_MODELS } from "./nim-models"; +import { QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL, ALIBABA_CODING_BASE_URL_CHOICES, ALIBABA_CODING_INTL_BASE_URL, MOONSHOT_BASE_URL_CHOICES, MOONSHOT_INTL_BASE_URL } from "../base-url-choices"; +``` + +`REGIONAL_ENTRIES` = `export const REGIONAL_ENTRIES: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 2347–2664, followed by `];`. Entry ids in order: `novita` (2347–2389), `together` (2391–2391), `fireworks` (2392–2392), `firepass` (2393–2397), `moonshot` (2398–2414), `huggingface` (2415–2415), `nvidia` (2424–2438), `venice` (2439–2439), `zai` (2448–2462), `zhipu-bigmodel` (2472–2508), `zhipu-bigmodel-coding` (2525–2544), `nanogpt` (2545–2545), `synthetic` (2546–2546), `siliconflow` (2551–2560), `qwen-cloud` (2563–2573), `tencent-coding-plan` (2574–2587), `volcengine` (2588–2620), `volcengine-coding-plan` (2621–2642), `volcengine-agent-plan` (2643–2660), `qianfan` (2662–2662), `alibaba` (2664–2664). No sorting, mapping, cloning, default filling or conditional inclusion. + +### `src/providers/registry/entries-plans.ts` + +- Original ranges: `2665–2924`. +- Symbols: `PLAN_ENTRIES`. +- Expected lines: **269** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +import { ZAI_GLM_52_REASONING_EFFORTS, ZAI_GLM_53_REASONING_EFFORTS, MINIMAX_MODELS, MINIMAX_MODEL_CONTEXT_WINDOWS, MINIMAX_M3_REASONING_EFFORTS, MINIMAX_M3_REASONING_EFFORT_MAP } from "./frontier-models"; +import { THINKING_BUDGET_EFFORTS, QWEN38_REASONING_EFFORTS, DEEPSEEK_THINKING_MODELS, DEEPSEEK_VISION_PREVIEW_MODEL, OPENCODE_FREE_DEEPSEEK_MODELS, OPENCODE_ZEN_TEXT_ONLY_MODELS, deepseekThinkingEffortsFor, deepseekReasoningMapFor } from "./reasoning-models"; +import { ALIBABA_TOKEN_PLAN_MODELS, ALIBABA_TOKEN_PLAN_QWEN_MODELS, ALIBABA_TOKEN_PLAN_INPUT_MODALITIES, ALIBABA_INTL_TOKEN_PLAN_MODELS, ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS, ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES } from "./coding-plan-models"; +import { KIMI_CODING_MODELS, KIMI_THINKING_MODELS, KIMI_CODING_NO_REASONING_MODELS, KIMI_CODING_REASONING_EFFORTS, KIMI_CODING_DEFAULT_REASONING_EFFORTS, KIMI_CODING_REASONING_EFFORT_MAPS, KIMI_LOCKED_PARAMETER_MODELS, KIMI_AUTO_TOOL_CHOICE_ONLY_MODELS, KIMI_CODING_MODEL_CONTEXT_WINDOWS, KIMI_CODING_MODEL_INPUT_MODALITIES } from "./kimi-models"; +import { ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL } from "../base-url-choices"; +``` + +`PLAN_ENTRIES` = `export const PLAN_ENTRIES: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 2665–2924, followed by `];`. Entry ids in order: `alibaba-token-plan` (2665–2695), `alibaba-token-plan-intl` (2696–2738), `parallel` (2742–2742), `zenmux` (2747–2750), `litellm` (2751–2758), `ollama-cloud` (2759–2801), `mistral` (2803–2803), `minimax` (2804–2826), `minimax-cn` (2827–2840), `kimi-code` (2841–2859), `opencode-zen` (2860–2884), `vercel-ai-gateway` (2885–2885), `opencode-free` (2886–2924). No sorting, mapping, cloning, default filling or conditional inclusion. + +### `src/providers/registry/entries-edge.ts` + +- Original ranges: `2925–3055`. +- Symbols: `EDGE_ENTRIES`. +- Expected lines: **135** (≤400). + +Own imports (complete): + +```ts +import type { ProviderRegistryEntry } from "./contracts"; +``` + +`EDGE_ENTRIES` = `export const EDGE_ENTRIES: readonly ProviderRegistryEntry[] = [`, followed by **verbatim** original lines 2925–3055, followed by `];`. Entry ids in order: `xiaomi` (2925–2925), `xiaomi-mimo` (2930–2943), `kilo` (2944–2944), `mimo-free` (2945–2960), `mimo` (2971–2992), `cloudflare-ai-gateway` (2993–2993), `cloudflare-workers-ai` (2994–3022), `github-copilot` (3025–3053), `gitlab-duo` (3055–3055). No sorting, mapping, cloning, default filling or conditional inclusion. + +MODIFY `src/providers/registry.ts`: expected residual **219 lines**. Under 400; no later registry split is required. Keep only the single Antigravity entry, ordered spreads, original validation and lookup policy. + +| Registry stage | Original lines removed, cumulative | Residual body incl. spread placeholders | Header/import/re-export lines | Expected residual | +|---|---:|---:|---:|---:| +| #a / L2 | 814 | 2,412 | 17 | 2,429 | +| #b / L3 | 1,981 | 1,249 | 18 | 1,267 | +| #c / L4 | 3,029 | 205 | 14 | 219 | + +Accounting starts from 3,250 original physical lines. Original header 1–24 is replaced by the explicit one-statement-per-line headers in each Re-export block. Body removals: 814 model lines in #a; 319 contract lines + 848 entry lines in #b; 1,048 entry lines in #c. #b inserts four spread lines; #c inserts four more. Thus #b reduces the prior residual by 1,162; #c by 1,048. These counts include retained comments/blanks and are exact for the specified compact headers; formatting may change them but must not exceed 400 for a new leaf. All 1,897 original array-content lines are accounted for: 848 + 1,048 moved, plus the one retained Antigravity line at 1903. Final original residual is 219, not an unplanned #d. + +The Antigravity row is deliberately retained at its original sequence point; do not merge the two gateway arrays around it. This avoids propagating the known Antigravity/catalog type cycle into a new entry leaf. Preserve the existing eager Cursor calculations and validation timing: no factories or async initialization. + +## Re-export block + +All 11 public types move in #b; the exact named type re-export is retained in #c. PROVIDER_REGISTRY and all 11 exported functions stay defined in the residual, so adding value re-exports for them would duplicate declarations. The complete expected residual import/re-export header is: + +```ts +import type { CodexAccountMode, OcxProviderConfig } from "../types"; +import type { InboundWire, ProviderRegistryEntry, ResponsesTerminalRepairPolicy } from "./registry/contracts"; +import { fastWireDeclarationError } from "./fastwire"; +import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "./antigravity-models"; +import { ACCOUNT_ENTRIES } from "./registry/entries-accounts"; +import { FRONTIER_ENTRIES } from "./registry/entries-frontier"; +import { GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY, GATEWAY_ENTRIES_AFTER_ANTIGRAVITY } from "./registry/entries-gateways"; +import { HOSTED_ENTRIES } from "./registry/entries-hosted"; +import { REGIONAL_ENTRIES } from "./registry/entries-regional"; +import { PLAN_ENTRIES } from "./registry/entries-plans"; +import { EDGE_ENTRIES } from "./registry/entries-edge"; + +export type { ProviderAuthKind, MetadataModelIdNormalize, InboundWire, ModelWireDefault, ResponsesTerminalRepairPolicy, ProviderModelDiscoveryScalar, ProviderModelDiscoveryPredicate, ProviderModelDiscoveryFilter, ProviderModelDiscoverySpec, ProviderRegistryEntry, ProviderConfigSeed } from "./registry/contracts"; +``` + +At each original chunk start, replace only its range with one `...CHUNK_NAME,` line. Exact ordered composition for the extracted region: + +```ts +export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ + ...ACCOUNT_ENTRIES, + ...FRONTIER_ENTRIES, + ...GATEWAY_ENTRIES_BEFORE_ANTIGRAVITY, + { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] }, + ...GATEWAY_ENTRIES_AFTER_ANTIGRAVITY, + ...HOSTED_ENTRIES, + ...REGIONAL_ENTRIES, + ...PLAN_ENTRIES, + ...EDGE_ENTRIES, +]; +``` + +The Antigravity object above is the exact original line 1903. No new array is exported from the old path apart from the existing PROVIDER_REGISTRY binding. + +## Module-level state and cycles + +- `CLINE_PASS_IMAGE_MODELS` at `src/providers/registry.ts:1139–1151` has exactly one owner: `src/providers/registry/gateway-models.ts` from L2. It stays private there; its derived modality/text-only arrays stay with it. No setter, clone, lazy initializer, cache, or test hook is introduced. +- Every other top-level const is in the inventory. Model arrays/records are initialized once by their assigned leaf. Keep shared object identity, aliases (`KIMI_THINKING_MODELS` at 847, `KIMI_LOCKED_PARAMETER_MODELS` at 871), copies, and Object.fromEntries expressions unchanged. Readonly typing does not authorize freezing or cloning their values. +- `PROVIDER_REGISTRY` at 1158 remains one exported array in `registry.ts`. Entry leaves allocate each original entry object once; the facade spreads entry references in the historical sequence. The original validation loop at `src/providers/registry.ts:3064–3067` runs exactly once, after the complete array is constructed and before the facade import completes. It is a top-level effect, not a cache; never move it into each chunk or defer it. +- No top-level let, Map, WeakMap, lock or timer exists in either target. The `claimed` Set in `mergeRegistryStaticHeaders` at 3095 and callback-local Sets are invocation-local, not singleton state. No reset owner is needed. + +Dependency map: `src/router.ts:20`, `src/providers/derive.ts:8`, `src/config.ts:88`, and `src/codex/catalog/parsing.ts:14` consume the old boundary; it points to data leaves and contracts. Entry leaves point directly to their model leaves and existing vendor metadata owners, never to `../registry`. This is functional/data coupling; initialization/validation is the existing temporal coupling. No common mutable-state API is introduced. + +Existing type cycle: `registry.ts:2 → fastwire.ts:10 → registry.ts`. L2 leaves it unchanged; L3 moves contracts and changes only the type specifier in `src/providers/fastwire.ts:10` from `"./registry"` to `"./registry/contracts"`. This single adjacent source-file change is a required executor scope expansion for the parent to authorize, not performed by this documentation task. It reduces legacy-path importer count from 134 to 133; all other legacy consumers and all 78 test/support importers stay put. Do not pretend the literal unchanged-importer-count line in 002 can apply to this intentional one-edge repair. + +A second, pre-existing type-containing cycle is `registry.ts:4 → antigravity-models.ts:2 → codex/model-cache.ts:10 → codex/catalog.ts:3 → codex/catalog/parsing.ts:13 → providers/derive.ts:8 → registry.ts`. Keep the complete `google-antigravity` object at `registry.ts:1903` and its existing import in the facade, between the two gateway arrays. Moving it into an entry leaf would put that new leaf into the existing SCC. No new leaf imports Antigravity. The known vendor dependencies remain real shared owners (KIRO at src/providers/kiro-models.ts:1; Command Code at src/providers/command-code-efforts.ts:1; Cursor discovery/catalog at src/adapters/cursor/discovery.ts:1–8), not copied snapshots. A direct import of CatalogModel from parsing would still reach derive and would not fix this cycle. Strict all-graph zero-cycle acceptance needs a separately scoped type-owner repair; report this to the parent rather than silently expanding S02 or claiming the graph is globally acyclic. Compare baseline and tip graphs including erased type edges; no new SCC may contain a planned leaf. No lazy-import workaround. + +## Tests + +Resolved `rg -l` importer list below: 77 test files plus one test helper (78 files). Each is **unchanged** in every layer: it continues importing the historical facade, including the dynamic import at `tests/providers/qwen38-preserve-reasoning.test.ts:106` and child-process import text at `tests/adapters/openai/openai-provider-option-e2e.test.ts:261`. + +- `tests/adapters/adapter-tool-conformance.test.ts` — unchanged. +- `tests/adapters/anthropic/anthropic-hardening.test.ts` — unchanged. +- `tests/adapters/empty-tool-output-annotation.test.ts` — unchanged. +- `tests/adapters/google/antigravity-static-catalog.test.ts` — unchanged. +- `tests/adapters/google/gemini-37-flash-migration.test.ts` — unchanged. +- `tests/adapters/google/google-hardening.test.ts` — unchanged. +- `tests/adapters/openai/openai-api-virtual-models.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option-e2e.test.ts` — unchanged. +- `tests/adapters/openai/openai-provider-option.test.ts` — unchanged. +- `tests/codex-integration/catalog-vision-sidecar-modalities.test.ts` — unchanged. +- `tests/codex-integration/codex-catalog.test.ts` — unchanged. +- `tests/codex-integration/codex-gather-authority.test.ts` — unchanged. +- `tests/codex-integration/compatibility-manifest.test.ts` — unchanged. +- `tests/gui/alibaba-intl-token-plan.test.ts` — unchanged. +- `tests/gui/provider-payload.test.ts` — unchanged. +- `tests/gui/qwen-cloud-endpoints.test.ts` — unchanged. +- `tests/gui/tencent-siliconflow-providers.test.ts` — unchanged. +- `tests/gui/volcengine-providers.test.ts` — unchanged. +- `tests/helpers/provider-registry-discovery.ts` — unchanged. +- `tests/images/gemini-inline.test.ts` — unchanged. +- `tests/providers/baseten-provider.test.ts` — unchanged. +- `tests/providers/chutes-provider.test.ts` — unchanged. +- `tests/providers/cline-pass-provider.test.ts` — unchanged. +- `tests/providers/cline-pass-reasoning-efforts.test.ts` — unchanged. +- `tests/providers/cline-provider.test.ts` — unchanged. +- `tests/providers/command-code-provider.test.ts` — unchanged. +- `tests/providers/commandcode-provider.test.ts` — unchanged. +- `tests/providers/cursor/cursor-display-names.test.ts` — unchanged. +- `tests/providers/cursor/cursor-fast-listing.test.ts` — unchanged. +- `tests/providers/cursor/cursor-fast-tier.test.ts` — unchanged. +- `tests/providers/deepinfra-provider.test.ts` — unchanged. +- `tests/providers/deepseek-inbound-wire.test.ts` — unchanged. +- `tests/providers/deepseek-reasoning-replay.test.ts` — unchanged. +- `tests/providers/deepseek-responses-item-id-repair.test.ts` — unchanged. +- `tests/providers/digitalocean-scaleway-provider.test.ts` — unchanged. +- `tests/providers/fast-row-ingress.test.ts` — unchanged. +- `tests/providers/featherless-provider.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-stream-contract.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-wire-defaults.test.ts` — unchanged. +- `tests/providers/hyperbolic-provider.test.ts` — unchanged. +- `tests/providers/kiro/kiro-adapter.test.ts` — unchanged. +- `tests/providers/meta-model-api-provider.test.ts` — unchanged. +- `tests/providers/meta-muse-oauth.test.ts` — unchanged. +- `tests/providers/mimo-effort.test.ts` — unchanged. +- `tests/providers/mimo-free-provider.test.ts` — unchanged. +- `tests/providers/mimo-token-plan-provider.test.ts` — unchanged. +- `tests/providers/model-rename-migration.test.ts` — unchanged. +- `tests/providers/moonshot-endpoints.test.ts` — unchanged. +- `tests/providers/muse-spark-web-search-compat.test.ts` — unchanged. +- `tests/providers/novita-provider.test.ts` — unchanged. +- `tests/providers/nscale-vultr-provider.test.ts` — unchanged. +- `tests/providers/nvidia-nim-hardening.test.ts` — unchanged. +- `tests/providers/ollama/ollama-native.test.ts` — unchanged. +- `tests/providers/opencode-free-provider.test.ts` — unchanged. +- `tests/providers/opencode-go-grok46-responses.test.ts` — unchanged. +- `tests/providers/opencode-go-luna-wire.test.ts` — unchanged. +- `tests/providers/opencode-go-muse-context.test.ts` — unchanged. +- `tests/providers/opencode-go-muse-vision.test.ts` — unchanged. +- `tests/providers/opencode-go-session-header.test.ts` — unchanged. +- `tests/providers/opencode-zen-rate-limit.test.ts` — unchanged. +- `tests/providers/provider-connection-test.test.ts` — unchanged. +- `tests/providers/provider-model-discovery-contract.test.ts` — unchanged. +- `tests/providers/provider-registry-parity.test.ts` — unchanged. +- `tests/providers/provider-static-model-discovery.test.ts` — unchanged. +- `tests/providers/qwen38-preserve-reasoning.test.ts` — unchanged. +- `tests/providers/sambanova-nebius-provider.test.ts` — unchanged. +- `tests/providers/xai/xai-transport.test.ts` — unchanged. +- `tests/providers/zhipu-bigmodel-provider.test.ts` — unchanged. +- `tests/responses/openai-responses-passthrough.test.ts` — unchanged. +- `tests/responses/responses-reasoning-summary-passthrough.test.ts` — unchanged. +- `tests/responses/responses-routed-web-search-fields.test.ts` — unchanged. +- `tests/responses/responses-stateless-dangling-call-repair.test.ts` — unchanged. +- `tests/responses/responses-terminal-repair.test.ts` — unchanged. +- `tests/routing/fastwire-policy.test.ts` — unchanged. +- `tests/routing/routing-capability-model-matching.test.ts` — unchanged. +- `tests/routing/routing-compatibility-auth-identity.test.ts` — unchanged. +- `tests/service/service-tier-capability.test.ts` — unchanged. +- `tests/vision/vision-sidecar-e2e.test.ts` — unchanged. + +Text-oracle classification: + +- Direct source-text readers of `src/providers/registry.ts`: **none found** by full-path, basename and segmented-path searches. `001_stale_check.md`'s count 1 is not accepted as a real oracle: `tests/routing/routing-compatibility-model-matching.test.ts:15` only mentions the source path in a comment, does not read it, and tests catalog model matching through other modules. Unchanged. This agrees with lane 012's inspected conclusion. +- `tests/lab/core-lab-boundary.test.ts:69` reads each transitively reached runtime source via `current`; it already follows re-exports/imports, so new data/destination leaves are automatically scanned. **Unchanged**, no retarget and no add-leaf-to-scan-list; leave PROTECTED at line 20 untouched. This is a graph-boundary oracle, not a provider-value text oracle. +- Fixture reads such as `tests/providers/nscale-vultr-provider.test.ts:28–29`, `tests/providers/commandcode-provider.test.ts:23`, and catalog-cache reads at `tests/codex-integration/codex-catalog.test.ts:3063` read JSON data, not the split TypeScript source. Unchanged. + +Guards to drive red once in the future implementation C phase: temporarily duplicate an entry id and then swap adjacent key-provider positions; `tests/providers/provider-registry-parity.test.ts:44–46` (uniqueness) and `:50–51` (ordered keys) must fail respectively. Restore the exact intended content and rerun. After entry extraction, perturb one moved entry field and confirm its existing provider parity assertion fails through the old import path. No assertion removal, fixture regeneration to hide a mismatch, or weakened scan. For the recursive boundary guard, temporarily add a forbidden Lab edge to a new reachable runtime leaf (not a PROTECTED root), observe failure, remove it, and rerun. These are planned commands, not executed evidence. + +## Verification + +Instantiate `002_layer_map.md` → **Per-layer gate** at this layer's exact tip. This delegated turn is docs-only: do not run these now. Remote full-suite execution, branch creation and PR publication belong to the parent/executor, not this drafting task. + +```sh +bun run typecheck +bun test tests/providers +bun test tests/routing/fastwire-policy.test.ts tests/routing/routing-capability-model-matching.test.ts tests/routing/routing-compatibility-auth-identity.test.ts tests/service/service-tier-capability.test.ts +bun test tests/adapters/openai tests/adapters/google tests/adapters/anthropic/anthropic-hardening.test.ts tests/adapters/adapter-tool-conformance.test.ts tests/adapters/empty-tool-output-annotation.test.ts +bun test tests/codex-integration/codex-catalog.test.ts tests/codex-integration/catalog-vision-sidecar-modalities.test.ts tests/codex-integration/codex-gather-authority.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/gui/alibaba-intl-token-plan.test.ts tests/gui/provider-payload.test.ts tests/gui/qwen-cloud-endpoints.test.ts tests/gui/tencent-siliconflow-providers.test.ts tests/gui/volcengine-providers.test.ts +bun test tests/responses/openai-responses-passthrough.test.ts tests/responses/responses-reasoning-summary-passthrough.test.ts tests/responses/responses-routed-web-search-fields.test.ts tests/responses/responses-stateless-dangling-call-repair.test.ts tests/responses/responses-terminal-repair.test.ts tests/images/gemini-inline.test.ts tests/vision/vision-sidecar-e2e.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/providers/registry/frontier-models.ts src/providers/registry/reasoning-models.ts src/providers/registry/coding-plan-models.ts src/providers/registry/kimi-models.ts src/providers/registry/nim-models.ts src/providers/registry/gateway-models.ts src/providers/registry/entries-accounts.ts src/providers/registry/entries-frontier.ts src/providers/registry/entries-gateways.ts src/providers/registry/entries-hosted.ts src/providers/registry/entries-regional.ts src/providers/registry/entries-plans.ts src/providers/registry/entries-edge.ts src/providers/registry/contracts.ts src/providers/registry.ts +rg -n 'from "[^"]*/registry"' src gui/src scripts tests | wc -l +git diff --check +# Remote only, after parent confirms this checkout is dedicated to the layer: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-providers-registry-c && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The 002 grep is a trend signal, not an exact module-resolution count: it omits `./registry`, dynamic imports and type-only ownership corrections. Compare the resolved importer list as well: 134 baseline callers; 134 after L2, 133 from L3 solely because fastwire now imports contracts. Run no repository-wide local suite. Every local focused group above must show zero failures; typecheck/privacy/diff checks must exit zero. The remote pipeline's final `tail` exit status alone is not proof of Bun success: retain the complete log and Bun exit status (pipefail or PIPESTATUS in the executor shell), exact tested commit, and pass/fail totals. Record exact-head CI rollup before claiming PR-ready. No passes are claimed here. + +Static architecture verification is separate from typecheck: use the installed ast-grep import/export scan, resolve relative .ts/.tsx/index paths, include type-only edges and compare return paths to the baseline witnesses in Module-level state and cycles. Reject any new leaf-to-facade edge or new SCC; unresolved existing strict cycle constraints go back to the parent. Compare moved AST bodies/literal arrays with original spans (permit only import/export wiring, indentation, and array wrapper/spread scaffolding). Keep exported function signatures and original-path runtime export names identical. + +## Accept criteria + +1. Before implementation, the parent explicitly resolves the ≤500 changed-source-line contradiction; the fixed three registry parts are not claimed to satisfy that cap. +2. All 146 original top-level declarations have one inventory row and one owner; original exported name/type/signature sets are unchanged. +3. Exactly four new entry leaves in this layer, each ≤400 physical lines; residual is 219 with the named successor layer when over 400. +4. All model literals, metadata maps, object aliases, entry field requiredness and original entry order match origin/dev; retained Antigravity remains between the two gateway arrays. +5. PROVIDER_REGISTRY is allocated once; the FastWire validation loop remains one eager post-construction loop; no new locks, caches, or state copies. +6. Only the authorized FastWire type-import edge moves to contracts; 133 remaining legacy importers and all 78 test/support importers remain unchanged. Re-export statements do not stand in for local type/value imports. +7. No new leaf-to-facade/type cycle; baseline FastWire and Antigravity cycle dispositions are explicit. Do not mark a globally strict zero-cycle gate passed while a baseline witness remains. +8. All test dispositions and restored red-once checks are satisfied; instantiated local focused/privacy/type gates and remote full suite have fresh exact-tip evidence. +9. PR body uses the repository template with this complete four-layer map, correct parent base, own-layer verification, no Closes reference and no merge. + +## PR + +Title: `refactor(providers): finish ordered registry entry extraction (split S02 L4/4)` + +Branch: `codex/split-providers-registry-c`. Base: `codex/split-providers-registry-b`. Closes: **none**. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), recording only this layer's exact-tip evidence. Review only this layer's diff. Placeholder PR numbers below are intentional planning references, not opened PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S02-L1 | separate OpenAI destination classification | `codex/split-providers-openai-tiers` | `dev` | destination predicates and migration parity | +| 2 | #TBD-S02-L2 | extract private model metadata | `codex/split-providers-registry-a` | `dev` | model values and single ownership | +| 3 | #TBD-S02-L3 | extract registry contracts and primary entries | `codex/split-providers-registry-b` | `codex/split-providers-registry-a` | types, initial entries, FastWire import | +| 4 | #TBD-S02-L4 | **Current: finish ordered registry entry extraction** | `codex/split-providers-registry-c` | `codex/split-providers-registry-b` | tail ordering and final size | + +Depends on #TBD-S02-L3. A rewrite of the real parent `codex/split-providers-registry-b` requires cascading this layer and re-verifying its base (DEV-STACK-02). Publication is parent-owned; merges remain prohibited for this split train. diff --git a/devlog/_plan/260905_now_split_train/080_adapters_anthropic_image_normalize.md b/devlog/_plan/260905_now_split_train/080_adapters_anthropic_image_normalize.md new file mode 100644 index 0000000000..454fe8a551 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/080_adapters_anthropic_image_normalize.md @@ -0,0 +1,225 @@ +# 080 — S03 L1/3: image normalization cache and codec + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Evidence basis: docs HEAD `4cc219549`; `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. All source ranges below are at that code basis, not hypothetical post-move line numbers. `git diff origin/dev -- src/adapters/anthropic.ts src/adapters/anthropic-image-normalize.ts` was empty. Read 000, 001, 002 and lane 014 before planning. This delegated C3 docs-only task does not run tests, mutate git, or own CXC orchestration. + +## Loop spec + +- Archetype: `pure-move`. +- Goal: split `src/adapters/anthropic-image-normalize.ts` (518 lines) into one cache/codec owner and the existing wire-neutral orchestration/public boundary, both ≤400 lines. +- Non-goals: no cache-key, TTL, eviction, tier, concurrency, image-quality, decode-guard, retry, overflow or callback semantics changes; no new configuration/dependencies; no edits to callers or the memory-store registry. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated below. +- Stop: standalone layer passes the gate and exact-head CI is recorded by its executor; never merge. For this delegated task, stop after the three requested docs are checked. +- Escalation: report source drift, any leaf >400, a new cycle, missing export, or need for a new mutation seam. In particular, 299 physically moved lines alone mean ≥598 raw added+deleted lines. The 002 ≤500 changed-source-lines constraint cannot be claimed satisfied under ordinary numstat counting. Parent must approve an explicit pure-move size exception or revise the layer map before execution; this document does not silently authorize either. + +Structural decision (cxc-dev-architecture): lane 014:403–416 identifies the cache/codec seam and shared hooks. Reject a cache-only extraction: `encodeCalls++` at source:328 would require a new cross-module mutator if its owner moved away from the encoder loop. Move the whole cache plus `processAt` together instead. Reject deletion/configuration: both would change behavior. Reuse the existing guard and memory-budget APIs, not a new image abstraction. + +Current map: `src/adapters/anthropic.ts:21`, `src/adapters/kiro-images.ts:2`, `src/server/claude-messages.ts:12`, and `src/lib/app-owned-memory-stores.ts:17–20` → original boundary → image guard / memory-budget core. Intended map: the same consumers → original boundary → codec → guard / memory-budget core. Feature-local blast radius, no external import migration. Sibling naming follows `anthropic-image-guard.ts`, `anthropic-output-schema.ts`, and `google-tool-schema.ts`; no convenience index barrel is added. The ingress ownership invariant remains the one in `structure/04_transports-and-sidecars.md:1404`. + +## Symbol inventory + +Declaration ranges came from `git show origin/dev:` parsed in memory with the installed `@babel/parser` TypeScript parser, cross-checked against `nl -ba` / `rg -n` source reads. Tables include every top-level function, variable, type and interface declaration; imports are dependencies, recorded separately below. Consumer count means distinct other files importing/re-exporting that binding from this exact module: `rg -l '' src gui/src scripts tests -g '*.ts' -g '*.tsx'` supplies candidates, then import specifiers are resolved and counted. Comments, fixture path strings, unrelated OAuth modules named anthropic, and same-file references are excluded. Private declarations have zero external consumers, not zero internal uses. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `TierSpec` | interface | 26–31 | yes | 0 | `anthropic-image-codec.ts` | +| `KiB` | const | 33–33 | no | 0 | `anthropic-image-codec.ts` | +| `MiB` | const | 34–34 | no | 0 | `anthropic-image-codec.ts` | +| `TIER_SPECS` | const | 41–48 | yes | 2 | `anthropic-image-codec.ts` | +| `TERMINAL_POS` | const | 49–49 | no | 0 | `anthropic-image-codec.ts` | +| `TIER0_COUNT` | const | 52–52 | no | 0 | `anthropic-image-codec.ts` | +| `TIER1_COUNT` | const | 53–53 | no | 0 | `anthropic-image-codec.ts` | +| `MAX_INPUT_BASE64_LENGTH` | const | 56–56 | yes | 0 | `anthropic-image-codec.ts` | +| `IMAGE_NORMALIZE_CONCURRENCY` | const | 64–64 | yes | 1 | `anthropic-image-codec.ts` | +| `MAX_INPUT_PIXELS` | const | 65–65 | yes | 0 | `anthropic-image-codec.ts` | +| `UNDECODABLE_TEXT` | const | 67–67 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `BOMB_TEXT` | const | 68–68 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `OVERFLOW_DROP_TEXT` | const | 69–69 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `PASSTHROUGH_MEDIA` | const | 72–72 | no | 0 | `anthropic-image-codec.ts` | +| `NormalizeOptions` | interface | 74–81 | yes | 1 | `anthropic-image-codec.ts` | +| `EncodeFn` | type | 83–87 | yes | 2 | `anthropic-image-codec.ts` | +| `ValidateFn` | type | 90–90 | yes | 0 | `anthropic-image-codec.ts` | +| `ProcessResult` | type | 92–95 | no | 0 | `anthropic-image-codec.ts` | +| `IMAGE_NORMALIZE_CACHE_MAX_BYTES` | const | 102–102 | yes | 1 | `anthropic-image-codec.ts` | +| `CACHE_MAX_ENTRIES` | const | 103–103 | no | 0 | `anthropic-image-codec.ts` | +| `CACHE_MAX_ENTRY_BYTES` | const | 104–104 | no | 0 | `anthropic-image-codec.ts` | +| `CacheValue` | type | 107–107 | no | 0 | `anthropic-image-codec.ts` | +| `CacheEntry` | interface | 108–113 | no | 0 | `anthropic-image-codec.ts` | +| `NormalizeCacheLimits` | interface | 114–118 | no | 0 | `anthropic-image-codec.ts` | +| `DEFAULT_CACHE_LIMITS` | const | 119–123 | no | 0 | `anthropic-image-codec.ts` | +| `cacheEncoder` | const | 124–124 | no | 0 | `anthropic-image-codec.ts` | +| `cache` | const | 125–125 | no | 0 | `anthropic-image-codec.ts` | +| `cacheLimits` | let | 126–126 | no | 0 | `anthropic-image-codec.ts` | +| `cacheBytes` | let | 127–127 | no | 0 | `anthropic-image-codec.ts` | +| `cacheMetadataBytes` | let | 128–128 | no | 0 | `anthropic-image-codec.ts` | +| `cacheSentinelEntries` | let | 129–129 | no | 0 | `anthropic-image-codec.ts` | +| `encodeCalls` | let | 130–130 | no | 0 | `anthropic-image-codec.ts` | +| `cacheEntry` | function | 132–141 | no | 0 | `anthropic-image-codec.ts` | +| `deleteCacheEntry` | function | 143–151 | no | 0 | `anthropic-image-codec.ts` | +| `cachePut` | function | 153–174 | no | 0 | `anthropic-image-codec.ts` | +| `cacheGet` | function | 177–185 | no | 0 | `anthropic-image-codec.ts` | +| `getNormalizeStatsForTests` | function | 188–204 | yes | 1 | `anthropic-image-codec.ts` | +| `resetNormalizeStateForTests` | function | 205–211 | yes | 5 | `anthropic-image-codec.ts` | +| `setNormalizeCacheLimitsForTests` | function | 213–216 | yes | 1 | `anthropic-image-codec.ts` | +| `anthropicImageNormalizeRetainedStoreSnapshot` | function | 218–232 | yes | 2 | `anthropic-image-codec.ts` | +| `evictOldestAnthropicImageNormalizeForBudget` | function | 234–237 | yes | 2 | `anthropic-image-codec.ts` | +| `bunImageEncode` | const | 240–252 | no | 0 | `anthropic-image-codec.ts` | +| `bunImageValidate` | const | 259–261 | no | 0 | `anthropic-image-codec.ts` | +| `mediaTypeOf` | function | 263–267 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `textify` | function | 269–271 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `replaceImage` | function | 273–275 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `initialPosition` | function | 277–280 | no | 0 | `anthropic-image-normalize.ts (residual)` | +| `processAt` | function | 291–347 | no | 0 | `anthropic-image-codec.ts` | +| `NormalizeTarget` | interface | 356–361 | yes | 2 | `anthropic-image-normalize.ts (residual)` | +| `NormalizeTargetsOptions` | interface | 363–374 | yes | 0 | `anthropic-image-normalize.ts (residual)` | +| `normalizeImageTargets` | function | 380–499 | yes | 2 | `anthropic-image-normalize.ts (residual)` | +| `normalizeAnthropicImages` | function | 505–518 | yes | 3 | `anthropic-image-normalize.ts (residual)` | + +The two original import declarations are guard values/type at source:17–22 and `enforceAppOwnedMemoryBudget` at source:23. The latter moves to the codec; the former stays for the residual's collection, sniffing, budget and wire-handle type. + +## Leaf partition + +New file: `src/adapters/anthropic-image-codec.ts`. + +- Move source spans **25–66 (42 lines), 71–261 (191), 282–347 (66)**, including their comments and blank lines: **299 lines**. +- Symbols: `TierSpec`, `KiB`, `MiB`, `TIER_SPECS`, `TERMINAL_POS`, `TIER0_COUNT`, `TIER1_COUNT`, `MAX_INPUT_BASE64_LENGTH`, `IMAGE_NORMALIZE_CONCURRENCY`, `MAX_INPUT_PIXELS`, `PASSTHROUGH_MEDIA`, `NormalizeOptions`, `EncodeFn`, `ValidateFn`, `ProcessResult`, `IMAGE_NORMALIZE_CACHE_MAX_BYTES`, `CACHE_MAX_ENTRIES`, `CACHE_MAX_ENTRY_BYTES`, `CacheValue`, `CacheEntry`, `NormalizeCacheLimits`, `DEFAULT_CACHE_LIMITS`, `cacheEncoder`, `cache`, `cacheLimits`, `cacheBytes`, `cacheMetadataBytes`, `cacheSentinelEntries`, `encodeCalls`, `cacheEntry`, `deleteCacheEntry`, `cachePut`, `cacheGet`, `getNormalizeStatsForTests`, `resetNormalizeStateForTests`, `setNormalizeCacheLimitsForTests`, `anthropicImageNormalizeRetainedStoreSnapshot`, `evictOldestAnthropicImageNormalizeForBudget`, `bunImageEncode`, `bunImageValidate`, `processAt`. +- Export the existing public symbols exactly as before. Additionally export only the internal bindings needed by the residual: `TERMINAL_POS`, `TIER0_COUNT`, `TIER1_COUNT`, `bunImageEncode`, `bunImageValidate`, `processAt`. Do not re-export those new internal seams through the old boundary. +- Its complete imports: + +```ts +import { sniffImageDimensions } from "./anthropic-image-guard"; +import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory"; +``` + +- Expected size **302** = 299 moved + two imports + one separating blank line. Export keywords do not add lines. Keep the cache and its encoder counter in this one file. +- No duplicate KiB/MiB, tier array, option type, or mutable cache exists in the residual. + +Residual `src/adapters/anthropic-image-normalize.ts`: retain the module contract header, three omission strings, `mediaTypeOf`, `textify`, `replaceImage`, `initialPosition`, `NormalizeTarget`, `NormalizeTargetsOptions`, `normalizeImageTargets`, and `normalizeAnthropicImages`. Expected **227 lines** = 518 − 299 − removed memory-budget import (1) + eight wiring lines and one separator (9). Both files fit; no #b layer is needed for image normalization. Aggregate expected total is 529 = original 518 + net wiring 11. Final formatting may change these estimates, but measured ≤400 is mandatory. + +The 120-line `normalizeImageTargets` function remains intact: its bounded first-pass worker pool, synchronous failure flag, wait-for-in-flight-settlement, then oldest-first demotion are not function-size cleanup targets in this pure-move train. + +## Re-export block + +Exact additions to the original path (five export lines, followed by three actual local imports): + +```ts +export type { TierSpec, NormalizeOptions, EncodeFn, ValidateFn } from "./anthropic-image-codec"; +export { TIER_SPECS, MAX_INPUT_BASE64_LENGTH, IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_PIXELS } from "./anthropic-image-codec"; +export { IMAGE_NORMALIZE_CACHE_MAX_BYTES } from "./anthropic-image-codec"; +export { getNormalizeStatsForTests, resetNormalizeStateForTests, setNormalizeCacheLimitsForTests } from "./anthropic-image-codec"; +export { anthropicImageNormalizeRetainedStoreSnapshot, evictOldestAnthropicImageNormalizeForBudget } from "./anthropic-image-codec"; + +import { bunImageEncode, bunImageValidate, processAt, TERMINAL_POS, TIER0_COUNT, TIER1_COUNT } from "./anthropic-image-codec"; +import { IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_BASE64_LENGTH, MAX_INPUT_PIXELS } from "./anthropic-image-codec"; +import type { NormalizeOptions } from "./anthropic-image-codec"; +``` + +Keep the remaining four exports defined inline: `NormalizeTarget`, `NormalizeTargetsOptions`, `normalizeImageTargets`, `normalizeAnthropicImages`. Thus all **18** original exported bindings remain importable; the six type/interface exports preserve type identity. A re-export alone never provides the residual's local binding. + +## Module-level state and cycles + +Single owner for each binding: + +| Binding at original line | Kind | Owner | +|---|---|---| +| PASSTHROUGH_MEDIA:72 | policy Set, no new mutator | anthropic-image-codec.ts | +| cacheEncoder:124 | TextEncoder singleton | anthropic-image-codec.ts | +| cache:125 | mutable Map | anthropic-image-codec.ts | +| cacheLimits:126 | mutable limit snapshot | anthropic-image-codec.ts | +| cacheBytes:127 | mutable counter | anthropic-image-codec.ts | +| cacheMetadataBytes:128 | mutable counter | anthropic-image-codec.ts | +| cacheSentinelEntries:129 | mutable counter | anthropic-image-codec.ts | +| encodeCalls:130 | mutable counter; increment at 328 | anthropic-image-codec.ts | + +`DEFAULT_CACHE_LIMITS:119–123`, `TIER_SPECS:41–48`, and the codec function objects also stay single-owned, never cloned. No other top-level let/Map/Set/WeakMap or lock exists. The residual's `entries`, `nextIndex`, `firstError`, `failed` at 393–404 stay invocation-local. Reset and eviction hooks still mutate the same cache as every normalizer. + +Cycle to avoid: residual → codec → residual (for EncodeFn, TierSpec, NormalizeOptions or policy constants). All those definitions move to codec, so it never imports the old boundary. Another forbidden edge is codec → app-owned-memory-stores → old boundary; import only `app-owned-memory.ts`, whose import scan has no outgoing imports, not the registry. The guard likewise has no imports. Lane G1 found no existing cycle for this module; actual new import/re-export edges must be checked during implementation, including type-only edges. Functional coupling through `processAt` is explicit; there is no exported mutable state. Preserve existing synchronous budget callback timing at source:172. + +## Tests + +Complete direct-import `rg -l` list, after filtering to the exact import path; every entry is **unchanged** (no retarget and no scan-list addition): + +```text +tests/adapters/anthropic/anthropic-image-normalize.test.ts:2–14 +tests/adapters/anthropic/anthropic-image-retry.test.ts:4 +tests/adapters/anthropic/anthropic-image-retry-e2e.test.ts:8 +tests/providers/kiro/kiro-images.test.ts:7 +tests/claude-integration/claude-native-passthrough.test.ts:359 +tests/codex-integration/app-owned-memory.test.ts:15 +``` + +The exact-path import population is **10 files**: six tests plus four production consumers listed above. Tests access cache hooks through the original module, exercising the preserved identity. + +Text-oracle tests reading this source: **none found**. Search used `rg -n 'anthropic-image-normalize|anthropic\\.ts' tests -g '*.ts'`, followed by `rg -n 'readFileSync|readFile\\(|Bun\\.file|source\\(' ` and inspection. The matches in layout JSON are test-location metadata, not source-body readers; `anthropic-pool-toggle-copy.test.ts:44,54,63,73` reads GUI files, not either S03 source. Therefore there is no source-read line to retarget and no source scan-list to extend. Do not invent a text oracle or weaken behavioral guards. + +C-phase guards to drive red once, then restore (not executed while drafting): + +- Cache identity/hit: `anthropic-image-normalize.test.ts:205` (N3) must fail if codec cache reads are temporarily bypassed. +- Accounting/eviction: same file:85,109,157 must fail if metadata/sentinel accounting or oldest-row eviction is temporarily bypassed. +- Keep the same file's concurrency/fatal-callback/order cases intact; temporarily reversing the demotion selection must trip oldest-first coverage. Restore original statements and run clean green. +- No mutation is committed; record exact mutation, failing assertion and restored green evidence. + +## Verification + +Implementation-only instantiation of 002 **Per-layer gate**; no commands here have been run as tests by the doc author. + +```sh +bun run typecheck +bun test tests/adapters/anthropic/anthropic-image-normalize.test.ts tests/adapters/anthropic/anthropic-image-retry.test.ts tests/adapters/anthropic/anthropic-image-retry-e2e.test.ts tests/providers/kiro/kiro-images.test.ts tests/claude-integration/claude-native-passthrough.test.ts tests/codex-integration/app-owned-memory.test.ts +bun run privacy:scan +wc -l src/adapters/anthropic-image-codec.ts src/adapters/anthropic-image-normalize.ts +rg -n 'from "[^"]*/anthropic-image-normalize"' src gui/src scripts tests +git diff --numstat +``` + +Pass conditions: typecheck/privacy exit 0; focused adapters/anthropic, providers/kiro, claude-integration and codex-integration tests 0 fail; both source files ≤400; original-path consumer set remains the same 10 files. The conditional 002 `tests/lab/core-lab-boundary.test.ts` gate is not activated by adapter-only edits; never change its PROTECTED roots. Stop if implementation unexpectedly touches src/server, src/router or src/lib, then apply that gate after parent scope approval. + +Full suite **only on lidge**, at this exact layer tip: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-anthropic-image-normalize && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The executor must also preserve the full-suite exit status (pipefail or unpiped run) and capture `git rev-parse HEAD`; a successful `tail` is not test proof. Record exact-head CI rollup independently. No remote work is performed during this delegated docs task. Perform import-graph/type-edge cycle verification against the direction above without installing new tooling. + +## Accept criteria + +1. Exactly the three documented source spans move; bodies, signatures, literal values, scheduling and callback order stay identical. +2. All 52 top-level declarations have one owner; all 18 original exports remain at the original path. +3. The cache, reset/stats/budget hooks and encode counter share one codec owner, with no public mutable holder and no counter wrapper added. +4. Measured codec and residual line counts are ≤400 (expected 302 and 227). +5. All six importing tests stay unchanged; no source-text oracle is silently omitted. +6. Retarget-free behavioral guards have documented red/restored-green evidence and every per-layer gate passes at the same tip. +7. No new import cycle, source caller migration, test-layout drift, credential behavior change or protected-root edit. +8. Parent resolves the raw >500-line size conflict before execution; absence of that decision blocks implementation, not the accuracy of this draft. + +## PR + +Title: `refactor(adapters): isolate the image normalization cache and codec (split S03 L1/3)` + +Base: `dev`. Branch: `codex/split-adapters-anthropic-image-normalize`. Closes: none. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S03 L1/3 | # | `codex/split-adapters-anthropic-image-normalize` | `dev` | Single cache/codec owner | +| S03 L2/3 | # | `codex/split-adapters-anthropic-a` | `codex/split-adapters-anthropic-image-normalize` | Private prompt-cache/reasoning/schema leaves | +| S03 L3/3 | # | `codex/split-adapters-anthropic-b` | `codex/split-adapters-anthropic-a` | Message conversion and response parsers | + +Fill repository PR template Summary, Verification and Checklist; include this map with L1 marked current. Review only the layer diff. L2 depends on this layer, so a lower-layer rewrite requires a parent-owned cascade through L2 and L3 and fresh exact-head checks. No merge is authorized by this plan. + +## P stale-check (2026-09-05, wp080) + +origin/dev 4dde2db97; `git diff --stat 445742966 origin/dev -- src/adapters/anthropic-image-normalize.ts` empty (518 lines). Anchors 25/66/71/124/125/130/261/282/291/347/380/505 confirmed by sed. Base `dev` (S03 bottom; 090/100 anthropic.ts #a/#b chain on this layer). Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1 on focused runs; CI hygiene requires a test change in the same PR. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-080.bBldI7/wt` (branch `codex/split-adapters-anthropic-image-normalize`, base origin/dev 4dde2db97). Executor: gpt-6-astra high (Mencius, 01a06f15-eda0-7490-b193-c3b9a2295835). +- Commits: 0fbddf27e (move: anthropic-image-codec.ts 304 lines, anthropic-image-normalize.ts 228; unused enforceAppOwnedMemoryBudget import dropped from residual) and c1d436738 (test: +15 — reset/stats hooks identical via both paths; residual has no cache state). Diff: 3 files, +327/−298. +- Local gate: typecheck 0; focused (6 files) 80 pass / 0 fail; privacy passed; 10 original-path consumers unchanged; residual has no cache/cacheBytes/encodeCalls declarations. +- Red-drives: (a) cacheGet → undefined fails :213 (encodeCalls 2 vs 1), restored 24/0; (b) cachePut skips sentinel accounting fails :91, restored 24/0. + +- Adversarial diff review (Huygens, gpt-6-astra high, 01a06f19-6c0d-74a2-b55b-f4402fa9591b): VERDICT: PASS first round (three spans byte-identical, residual reconstruction exact, 18 exports preserved with type identity, 7 state bindings single-owned at codec:100–106, zero cycles via scanImports, 3 files). +- lidge full suite at c1d436738: SUITE_EXIT=0, 18014 pass / 0 fail / 16 skip (/tmp/suite-split-080.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3567 (base dev, head c1d436738). CI rollup at record time: OPEN draft=false c1d436738 =1 =10 SKIPPED=2 SUCCESS=16 diff --git a/devlog/_plan/260905_now_split_train/090_adapters_anthropic_a.md b/devlog/_plan/260905_now_split_train/090_adapters_anthropic_a.md new file mode 100644 index 0000000000..40b78e1d97 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/090_adapters_anthropic_a.md @@ -0,0 +1,246 @@ +# 090 — S03 L2/3: private Anthropic request policies (#a) + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Evidence basis: docs HEAD `4cc219549`; `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. All source ranges below are at that code basis, not hypothetical post-move line numbers. `git diff origin/dev -- src/adapters/anthropic.ts src/adapters/anthropic-image-normalize.ts` was empty. Read 000, 001, 002 and lane 014 before planning. This delegated C3 docs-only task does not run tests, mutate git, or own CXC orchestration. + +## Loop spec + +- Archetype: `pure-move`. +- Goal: take the zero-external-consumer prompt-cache, reasoning and tool-schema policy leaves first; preserve all three public exports of `src/adapters/anthropic.ts`. +- Non-goals: no wire-body, auth/header, model-family, schema, cache-breakpoint or reasoning-budget changes; no function decomposition, new dependencies, public helper exports, or neighboring adapter cleanup. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated below. +- Stop: this layer's standalone checks and exact-head CI are recorded; never defer L2 correctness to L3 or merge. Parent owns all orchestration/branch work. +- Escalation: the planned residual is **1007** lines and is explicitly assigned to **100 / S03 L3 / #b**. There is also a raw diff-size conflict: 381 moved physical lines imply ≥762 added+deleted source lines before wiring. Even counting moved lines only, L3 needs 712. Parent must explicitly approve a size exception or revise 002 before implementation; this fixed three-document delegation does not edit the stack map or create extra layers. + +Structural decision: lane 014:164–177 identifies prompt caching, request compilation and event decoding as separate seams. This layer follows its recommended cache-policy-first split and the user's lowest-consumer-first rule: all moved bindings are private, with zero external importers. Public factory fan-in is 26 and public URL/error helpers each have one consumer; keep them untouched at their current boundary. Among tied zero-fan-in leaves, take dependency-free cache policy and reasoning/schema policy before message conversion and parser closure relocation. + +Reject deleting or configuring policy away: that changes wire semantics. Reject moving the 506-line whole factory to a leaf: it violates ≤400 and conceals the request/response boundary. Existing `anthropic-output-schema.ts` owns output schemas, not the different input-schema rules at source:808–868; do not merge those contracts. Adjacent `google-tool-schema.ts`, `anthropic-output-schema.ts` and `anthropic-image-guard.ts` establish descriptive sibling naming. + +Current map: registry/index + 24 tests → anthropic.ts → types, OAuth helpers, image leaves, schema helpers, identity, SSE and budgeting. Intended L2 map: same public dependents → anthropic.ts → prompt-cache / reasoning-policy / tool-schema leaves; policy leaves point directly to existing types/reasoning-effort/responses-tool-schema, never back to anthropic.ts. Blast radius: one adapter feature. Registry construction authority stays unchanged (`structure/10_adapter-registry.md`). + +## Symbol inventory + +Declaration ranges came from `git show origin/dev:` parsed in memory with the installed `@babel/parser` TypeScript parser, cross-checked against `nl -ba` / `rg -n` source reads. Tables include every top-level function, variable, type and interface declaration; imports are dependencies, recorded separately below. Consumer count means distinct other files importing/re-exporting that binding from this exact module: `rg -l '' src gui/src scripts tests -g '*.ts' -g '*.tsx'` supplies candidates, then import specifiers are resolved and counted. Comments, fixture path strings, unrelated OAuth modules named anthropic, and same-file references are excluded. Private declarations have zero external consumers, not zero internal uses. + +This is the full original **54-declaration** inventory, including symbols left for #b; do not add L3 owners prematurely. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `toAnthropicContentPart` | function | 34–43 | no | 0 | `residual → L3 anthropic-messages.ts` | +| `DEFAULT_MAX_TOKENS` | const | 46–46 | no | 0 | `anthropic-reasoning-policy.ts` | +| `REASONING_MAX_TOKENS_CEILING` | const | 48–48 | no | 0 | `anthropic-reasoning-policy.ts` | +| `ADAPTIVE_THINKING_CEILING` | const | 51–51 | no | 0 | `anthropic-reasoning-policy.ts` | +| `MIN_THINKING_BUDGET` | const | 53–53 | no | 0 | `anthropic-reasoning-policy.ts` | +| `OUTPUT_HEADROOM` | const | 55–55 | no | 0 | `anthropic-reasoning-policy.ts` | +| `OUTPUT_FLOOR` | const | 57–57 | no | 0 | `anthropic-reasoning-policy.ts` | +| `COMPAT_TOOL_PREFIX` | const | 58–58 | no | 0 | `anthropic.ts (residual)` | +| `CacheControl` | type | 59–59 | no | 0 | `anthropic-prompt-cache.ts` | +| `MAX_CACHE_BREAKPOINTS` | const | 60–60 | no | 0 | `anthropic-prompt-cache.ts` | +| `resolveCacheControl` | function | 62–66 | no | 0 | `anthropic-prompt-cache.ts` | +| `applyCacheControlToLast` | function | 79–83 | no | 0 | `anthropic-prompt-cache.ts` | +| `applyCacheControlToLastText` | function | 85–93 | no | 0 | `anthropic-prompt-cache.ts` | +| `PromptCachingOptions` | type | 95–98 | no | 0 | `anthropic-prompt-cache.ts` | +| `applyPromptCaching` | function | 101–166 | no | 0 | `anthropic-prompt-cache.ts` | +| `countBreakpoints` | function | 172–187 | no | 0 | `anthropic-prompt-cache.ts` | +| `enforceCacheControlLimit` | function | 189–214 | no | 0 | `anthropic-prompt-cache.ts` | +| `normalizeTtlOrdering` | function | 220–245 | no | 0 | `anthropic-prompt-cache.ts` | +| `isLikelyRealAnthropicThinkingSignature` | function | 247–251 | no | 0 | `residual → L3 anthropic-messages.ts` | +| `formatAnthropicErrorBody` | function | 258–268 | yes | 1 | `anthropic.ts (residual)` | +| `isAnthropicRecord` | function | 270–272 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `anthropicStructuralValueType` | function | 274–277 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `InvalidAnthropicShapeDiagnostic` | interface | 279–283 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `invalidAnthropicShapeEvent` | function | 290–301 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `extractAnthropicErrorDetail` | function | 303–321 | no | 0 | `anthropic.ts (residual)` | +| `usesNativeAnthropicEndpoint` | function | 323–329 | no | 0 | `anthropic.ts (residual)` | +| `anthropicMessagesUrl` | function | 332–341 | yes | 1 | `anthropic.ts (residual)` | +| `synthesizeToolUseId` | function | 343–345 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `usableToolUseId` | function | 353–355 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES` | const | 366–366 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `utf8BytesExceed` | function | 373–390 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `lastValidJsonObject` | function | 392–417 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `toolUseArguments` | function | 419–439 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `streamedToolArgumentsParse` | function | 447–456 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `anthropicKeyUsesBearer` | function | 458–460 | no | 0 | `anthropic.ts (residual)` | +| `reasoningBudget` | function | 463–473 | no | 0 | `anthropic-reasoning-policy.ts` | +| `ADAPTIVE_THINKING_FAMILY_MINIMUMS` | const | 482–486 | no | 0 | `anthropic-reasoning-policy.ts` | +| `claudeFamilyVersion` | function | 504–515 | no | 0 | `anthropic-reasoning-policy.ts` | +| `meetsFamilyMinimum` | function | 517–526 | no | 0 | `anthropic-reasoning-policy.ts` | +| `usesAdaptiveThinking` | function | 528–530 | no | 0 | `anthropic-reasoning-policy.ts` | +| `EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS` | const | 544–546 | no | 0 | `anthropic-reasoning-policy.ts` | +| `supportsExplicitThinkingDisable` | function | 548–550 | no | 0 | `anthropic-reasoning-policy.ts` | +| `adaptiveEffort` | function | 553–555 | no | 0 | `anthropic-reasoning-policy.ts` | +| `defaultReasoningEffort` | function | 557–567 | no | 0 | `anthropic-reasoning-policy.ts` | +| `usageFromAnthropic` | function | 569–585 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `mergeAnthropicUsage` | function | 587–596 | no | 0 | `residual → L3 anthropic-response-values.ts` | +| `buildToolNameTransforms` | function | 598–609 | no | 0 | `anthropic.ts (residual)` | +| `toAnthropicToolResult` | function | 611–630 | no | 0 | `residual → L3 anthropic-messages.ts` | +| `unrepresentableToolCallText` | function | 632–635 | no | 0 | `residual → L3 anthropic-messages.ts` | +| `orphanToolResultText` | function | 637–643 | no | 0 | `residual → L3 anthropic-messages.ts` | +| `messagesToAnthropicFormat` | function | 651–792 | no | 0 | `residual → L3 anthropic-messages.ts` | +| `toolsToAnthropicFormat` | function | 794–806 | no | 0 | `anthropic-tool-schema.ts` | +| `normalizeAnthropicInputSchema` | function | 808–868 | no | 0 | `anthropic-tool-schema.ts` | +| `createAnthropicAdapter` | function | 870–1375 | yes | 26 | `residual; L3 moves parser methods` | + +Original imports: `./base`:1; `./tool-call-id`:2; debug:3; types:4–17; OAuth:18; image:19; image guard:20; image normalization:21; output schema:22; responses-tool-schema:23; identity:24; redact:25; fingerprint:26; tool-catalog nudge:27; SSE:28; translator-budget:29; reasoning-effort:30; AgentRouter:31. New leaf imports are listed below. Remove only imports that become unused after the move; do not rewrite consumers. + +## Leaf partition + +All paths are siblings under `src/adapters/`; no new index or generic utilities. + +| New file | Exact original spans moved | Symbols | Expected lines | +|---|---|---|---:| +| `src/adapters/anthropic-prompt-cache.ts` | 59–245 = 187 | CacheControl, MAX_CACHE_BREAKPOINTS, resolveCacheControl, applyCacheControlToLast, applyCacheControlToLastText, PromptCachingOptions, applyPromptCaching, countBreakpoints, enforceCacheControlLimit, normalizeTtlOrdering | 187 | +| `src/adapters/anthropic-reasoning-policy.ts` | 45–57 = 13; 462–567 = 106 | DEFAULT_MAX_TOKENS, REASONING_MAX_TOKENS_CEILING, ADAPTIVE_THINKING_CEILING, MIN_THINKING_BUDGET, OUTPUT_HEADROOM, OUTPUT_FLOOR, reasoningBudget, ADAPTIVE_THINKING_FAMILY_MINIMUMS, claudeFamilyVersion, meetsFamilyMinimum, usesAdaptiveThinking, EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS, supportsExplicitThinkingDisable, adaptiveEffort, defaultReasoningEffort | 122 | +| `src/adapters/anthropic-tool-schema.ts` | 794–868 = 75 | toolsToAnthropicFormat, normalizeAnthropicInputSchema | 79 | + +Prompt-cache leaf imports: **none**. Export only `MAX_CACHE_BREAKPOINTS`, `resolveCacheControl`, `applyPromptCaching`, `enforceCacheControlLimit`, `normalizeTtlOrdering` to its internal caller. CacheControl and PromptCachingOptions remain private to that leaf. + +Reasoning-policy leaf complete imports (119 moved + 2 imports + blank = 122): + +```ts +import type { OcxProviderConfig } from "../types"; +import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort"; +``` + +Export its six numeric constants plus `reasoningBudget`, `usesAdaptiveThinking`, `supportsExplicitThinkingDisable`, `adaptiveEffort`, `defaultReasoningEffort`. Keep family tables and parsing/minimum helpers private. The factory still imports `modelRecordValue` directly for its configured output-token lookup at source:902. + +Tool-schema leaf complete imports (75 moved + 3 imports + blank = 79): + +```ts +import type { OcxParsedRequest } from "../types"; +import { isAllowedToolChoice, namespacedToolName, toolChoiceToolPredicate } from "../types"; +import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema"; +``` + +Export `toolsToAnthropicFormat` only to the adapter; `normalizeAnthropicInputSchema` stays leaf-private. + +Residual arithmetic: **1375 − 381 moved + up to 13 net wiring/format lines = 1007** expected upper budget. Imports that become unused can lower the actual count; they are not extra declarations moved. All leaves ≤400. The residual >400 is deliberate and assigned to **100_adapters_anthropic_b.md**, not treated as resolved here. L3's consistent ledger is **1007 − 712 + 10 = 305**. Combined file totals may grow by import lines; no behavior block is counted twice. + +Diff semantics: 381 is unique moved physical source, not raw git diff size. Preserved long functions are not split internally in this layer. No line wrapping/minification to manufacture compliance. + +## Re-export block + +**No new re-export statements in L2:** every moved binding was private. Adding `export { applyPromptCaching, ... }` at the original path would unnecessarily enlarge its public API. The exact new `export { ... } from "./leaf"` / `export type { ... }` block is therefore empty. + +Keep the existing exported definitions at the original boundary, with these unchanged signatures: + +```ts +export function formatAnthropicErrorBody(status: number, _headers: Headers, payloadText: string): string +export function anthropicMessagesUrl(baseUrl: string): string +export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetention?: "none" | "short" | "long"): ProviderAdapter +``` + +The snippets above identify signatures, not replacement implementations. These explicit local imports are required by the retained factory: + +```ts +import { MAX_CACHE_BREAKPOINTS, resolveCacheControl, applyPromptCaching, enforceCacheControlLimit, normalizeTtlOrdering } from "./anthropic-prompt-cache"; +import { DEFAULT_MAX_TOKENS, REASONING_MAX_TOKENS_CEILING, ADAPTIVE_THINKING_CEILING, MIN_THINKING_BUDGET, OUTPUT_HEADROOM, OUTPUT_FLOOR, reasoningBudget, usesAdaptiveThinking, supportsExplicitThinkingDisable, adaptiveEffort, defaultReasoningEffort } from "./anthropic-reasoning-policy"; +import { toolsToAnthropicFormat } from "./anthropic-tool-schema"; +``` + +Retain all other still-used source imports, dropping moved-only bindings such as `stripResponsesOnlyEncryptedMarker`. Leaf exports are implementation seams consumed directly, not convenience barrel exports. Re-exporting a name is not a local import. + +## Module-level state and cycles + +There is **no top-level let, Map, Set, WeakMap, timer, lock or mutable tracker** in anthropic.ts. The full top-level scan has 54 declarations. Constant scalar values move with their policies; `COMPAT_TOOL_PREFIX:58` stays in the original file. + +| Aggregate | Original range | Single owner after L2 | +|---|---|---| +| ADAPTIVE_THINKING_FAMILY_MINIMUMS | 482–486 | anthropic-reasoning-policy.ts | +| EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS | 544–546 | anthropic-reasoning-policy.ts | + +These tables are read-only by convention; retain object identity and do not introduce copies or writers. `Set` objects in message pairing (source:738,741) and input-schema required fields (834) are function-local, not module caches. Factory `isOAuth:871` and `toolNames:872` retain their per-adapter closure lifetime. + +Cycle risks: do not make any leaf import anthropic.ts for types/constants, and do not import the adapter registry. Type aliases required by cache policy stay with their leaf; other leaves use the existing ../types boundary. Existing lane G1 found no return path into anthropic.ts. Planned edges add only downstream functional dependencies. Reasoning and schema remain independent; neither imports the other. No dependency injection or validation wrapper is needed. Verify concrete type/runtime edges during implementation rather than assuming typecheck is cycle detection. + +## Tests + +Complete direct-import `rg -l` list for the exact `src/adapters/anthropic` module (line numbers are import sites, not source-text reads). Each of the **24 test files is unchanged**: + +```text +tests/adapters/adapter-usage.test.ts:3 +tests/adapters/openai/openai-chat-model-suffix.test.ts:3 +tests/adapters/buffered-response-shape-guards.test.ts:2 +tests/adapters/anthropic/anthropic-tool-schema.test.ts:2 +tests/adapters/anthropic/anthropic-compatible-stream.test.ts:2 +tests/adapters/anthropic/anthropic-error-stop-reason.test.ts:2 +tests/adapters/anthropic/anthropic-agentrouter-language-framing.test.ts:2 +tests/adapters/anthropic/anthropic-image-retry.test.ts:3 +tests/adapters/translator-budget.test.ts:3 +tests/adapters/anthropic/anthropic-empty-content.test.ts:2 +tests/adapters/anthropic/anthropic-tail-guard.test.ts:2 +tests/adapters/anthropic/anthropic-eof-tolerance.test.ts:2 +tests/adapters/anthropic/anthropic-stream-hardening.test.ts:2 +tests/adapters/anthropic/anthropic-hardening.test.ts:2 +tests/adapters/anthropic/anthropic-error-body.test.ts:3 +tests/adapters/anthropic/anthropic-reasoning.test.ts:2 +tests/adapters/anthropic/anthropic-thinking-signature.test.ts:3 +tests/adapters/identity-neutralize.test.ts:13 +tests/codex-integration/reasoning-effort.test.ts:3 +tests/providers/umans-provider.test.ts:2 +tests/responses/sse-null-data-frame.test.ts:2 +tests/responses/responses-parser-malformed-content.test.ts:4 +tests/clients/client-fingerprint.test.ts:8 +tests/claude-integration/claude-messages-endpoint.test.ts:8 +``` + +Production consumers are `src/adapters/registry.ts:1` and `src/index.ts:4` (public re-export). Exact-path fan-in is **26 files**, with `createAnthropicAdapter` in all 26, `formatAnthropicErrorBody` in one test, and `anthropicMessagesUrl` in one test. Do not count `src/oauth/index.ts:34`'s different `./anthropic` module or comments in provider/config-export/google sources. + +Text-oracle readers of this source: **none found**, consistent with lane 014:173. Verified by basename/exact-path `rg -n` across tests, then inspecting candidates for `readFileSync|readFile\\(|Bun\\.file|source\\(`. Layout JSON references are test-path metadata. The GUI-file reads in `anthropic-pool-toggle-copy.test.ts:44,54,63,73` do not read S03 code. Thus no retarget-to-leaf or add-leaf-to-scan-list action, and no source-read line to report. Behavioral imports remain unchanged so they exercise the original compatibility boundary. + +C-phase guard mutations, each temporary then restored (not executed in this docs task): + +- `tests/adapters/adapter-usage.test.ts:209–211,244–247`: suppress `applyPromptCaching` at its existing factory call site and confirm the explicit system/tool/penultimate-message breakpoint assertions fail, then restore. Lane 014's `anthropic-reasoning.test.ts:444` assertion checks top-level automatic caching, which is assigned outside the moved helper; it is not a sufficient red guard for this extraction. No existing direct mixed-TTL/excess-breakpoint assertion was found in the inspected focused files, so do not claim those specific branches were driven red. Preserve their code verbatim and record this coverage limitation rather than exporting private helpers only for tests. +- Same reasoning suite: perturb adaptive/disabled-thinking classification and confirm the relevant existing behavior assertions fail; restore both family tables unchanged. +- `tests/adapters/anthropic/anthropic-tool-schema.test.ts`: bypass root composition normalization and confirm the composition test fails, then restore. +- Do not create source-string assertions as a substitute for request-body behavior tests. + +## Verification + +Implementation-only 002 **Per-layer gate** (not run by this doc author): + +```sh +bun run typecheck +bun test tests/adapters/anthropic tests/adapters/adapter-usage.test.ts tests/adapters/openai/openai-chat-model-suffix.test.ts tests/adapters/buffered-response-shape-guards.test.ts tests/adapters/translator-budget.test.ts tests/adapters/identity-neutralize.test.ts tests/codex-integration/reasoning-effort.test.ts tests/providers/umans-provider.test.ts tests/responses/sse-null-data-frame.test.ts tests/responses/responses-parser-malformed-content.test.ts tests/clients/client-fingerprint.test.ts tests/claude-integration/claude-messages-endpoint.test.ts +bun run privacy:scan +wc -l src/adapters/anthropic-prompt-cache.ts src/adapters/anthropic-reasoning-policy.ts src/adapters/anthropic-tool-schema.ts src/adapters/anthropic.ts +rg -n 'from "[^"]*/adapters/anthropic"' src gui/src scripts tests +git diff --numstat +``` + +The `src/index.ts` public re-export and `src/adapters/registry.ts` sibling import require separate inspection (`rg -n 'adapters/anthropic|from "./anthropic"' src/index.ts src/adapters/registry.ts`); the combined exact-path set must stay at 26. Pass: typecheck/privacy exit 0, focused domains adapters/anthropic plus adapters/openai, codex-integration, providers, responses, clients, claude-integration at 0 failures, every new leaf ≤400, residual ≤1007 with #b explicitly pending. No src/server|src/router|src/lib file is touched, so the conditional core-lab test is not required here; never edit PROTECTED roots. + +Full suite only on lidge: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-anthropic-a && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Record the remote HEAD and full-suite real exit status (pipefail or unpiped run); tail alone does not prove success. Record the green exact-head CI rollup and a new-edge import-cycle check including type edges. Never run a repository-wide suite locally. The executor must have the same L1 base tip being reviewed and preserve its L1 gate evidence independently. + +## Accept criteria + +1. The full 54-declaration inventory remains accounted for; only the three listed private policy groups move. +2. Three new leaves measure ≤400; expected sizes are 187, 122 and 79. +3. Original exports remain exactly formatAnthropicErrorBody, anthropicMessagesUrl and createAnthropicAdapter; external consumer files remain the same 26. +4. Retained factory behavior and all moved bodies/literals/comments remain identical apart from imports/exports and whitespace. +5. No table duplication, upward import, cycle, public helper API, new dependency or source-oracle weakening. +6. The expected residual ≤1007 is explicitly pending #b, whose budget resolves it to ≤305; L2 alone is not claimed to finish anthropic.ts. +7. All focused and remote/exact-head gates pass with red/restored-green evidence for selected policy guards. +8. Parent resolves the 500-line accounting conflict before execution. This draft does not authorize extra branches, a cap waiver, or a merge. + +## PR + +Title: `refactor(adapters): extract private Anthropic request policies (split S03 L2/3)` + +Base: `codex/split-adapters-anthropic-image-normalize`. Branch: `codex/split-adapters-anthropic-a`. Closes: none. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S03 L1/3 | # | `codex/split-adapters-anthropic-image-normalize` | `dev` | Single cache/codec owner | +| S03 L2/3 | # | `codex/split-adapters-anthropic-a` | `codex/split-adapters-anthropic-image-normalize` | Private prompt-cache/reasoning/schema leaves | +| S03 L3/3 | # | `codex/split-adapters-anthropic-b` | `codex/split-adapters-anthropic-a` | Message conversion and response parsers | + +Fill Summary, Verification and Checklist from the repository PR template. Mark L2 current; depends on #. Review this layer's diff only. L1 changes require cascading L2 and L3; L2 changes require cascading L3, with fresh exact-head checks. Stack maintenance remains parent-owned, and no merge is authorized. diff --git a/devlog/_plan/260905_now_split_train/100_adapters_anthropic_b.md b/devlog/_plan/260905_now_split_train/100_adapters_anthropic_b.md new file mode 100644 index 0000000000..957edea1e0 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/100_adapters_anthropic_b.md @@ -0,0 +1,305 @@ +# 100 — S03 L3/3: Anthropic messages and response parser leaves (#b) + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Evidence basis: docs HEAD `4cc219549`; `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. All source ranges below are at that code basis, not hypothetical post-move line numbers. `git diff origin/dev -- src/adapters/anthropic.ts src/adapters/anthropic-image-normalize.ts` was empty. Read 000, 001, 002 and lane 014 before planning. This delegated C3 docs-only task does not run tests, mutate git, or own CXC orchestration. + +## Loop spec + +- Archetype: `pure-move`. +- Goal: finish the `anthropic.ts` split after #a by moving message translation, response-value helpers, and the two existing parser methods, leaving the original factory/buildRequest/public URL/error boundary ≤400 lines. +- Non-goals: no protocol fixes, body rewrites, buffering strategy change, auth/header change, new state context/class, provider snapshot, public export rename or caller migration. Do not split the stream's internal state machine. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated below. +- Stop: all S03 leaves and both originals ≤400, all own-tip gates/CI recorded, no merge. Parent owns lifecycle, stack and goal state. +- Escalation: **712** moved physical source lines in this layer exceed the 002 ≤500 constraint even before double-counting move additions/deletions. Parent must explicitly approve the pure-move size exception or allocate additional layers in 002. This doc is a complete partition proposal, not an assertion that the existing layer-count and diff-size constraints are simultaneously satisfiable. No map/branch expansion is authorized in this bounded task. + +Structural decision: `createAnthropicAdapter:870–1375` is 506 lines. Its `buildRequest:878–1035`, `parseStream:1037–1272` and `parseResponse:1274–1372` methods have separate lifetimes. Move the parser methods intact into small closure factories that capture the same provider and tool-name-transform object; do not move the whole 506-line factory into an oversized leaf. Reject a shared global parser state or new mutable context object: the stream state is already correctly invocation-local. Reject hoisting provider flags: the current methods read `provider.anthropicEofTolerance` at invocation time. + +L2 first extracted private zero-fan-in policies; L3 handles the more coupled message conversion and parser closure seams. Public helpers with external fan-in 1 and factory fan-in 26 remain at the original boundary, minimizing consumer churn. New private seams exist only to preserve the existing method closures, not as APIs exposed for testing. + +Map: registry/index + 24 tests → original factory → L2 policy leaves + new message/parser leaves. Stream and response → response-values; messages → existing types, image, identity, tool-call-id, tool-catalog nudge. No leaf → original boundary or registry edge. Existing OAuth tool-prefix construction stays in the original `buildToolNameTransforms:598–609`, shared by buildRequest and both parser factories. Feature-local blast radius. Sibling naming follows `google-errors.ts`, `google-tool-schema.ts`, `kiro-events.ts`, and the existing Anthropic image/schema leaves; no new convenience index. + +## Symbol inventory + +Declaration ranges came from `git show origin/dev:` parsed in memory with the installed `@babel/parser` TypeScript parser, cross-checked against `nl -ba` / `rg -n` source reads. Tables include every top-level function, variable, type and interface declaration; imports are dependencies, recorded separately below. Consumer count means distinct other files importing/re-exporting that binding from this exact module: `rg -l '' src gui/src scripts tests -g '*.ts' -g '*.tsx'` supplies candidates, then import specifiers are resolved and counted. Comments, fixture path strings, unrelated OAuth modules named anthropic, and same-file references are excluded. Private declarations have zero external consumers, not zero internal uses. + +All **54 original declarations** are repeated here for complete accounting. L2-owned rows are inherited, not moved again. Factory sub-method spans below are subsets of its 870–1375 range and must not be double-counted. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `toAnthropicContentPart` | function | 34–43 | no | 0 | `anthropic-messages.ts` | +| `DEFAULT_MAX_TOKENS` | const | 46–46 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `REASONING_MAX_TOKENS_CEILING` | const | 48–48 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `ADAPTIVE_THINKING_CEILING` | const | 51–51 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `MIN_THINKING_BUDGET` | const | 53–53 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `OUTPUT_HEADROOM` | const | 55–55 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `OUTPUT_FLOOR` | const | 57–57 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `COMPAT_TOOL_PREFIX` | const | 58–58 | no | 0 | `anthropic.ts (residual)` | +| `CacheControl` | type | 59–59 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `MAX_CACHE_BREAKPOINTS` | const | 60–60 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `resolveCacheControl` | function | 62–66 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `applyCacheControlToLast` | function | 79–83 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `applyCacheControlToLastText` | function | 85–93 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `PromptCachingOptions` | type | 95–98 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `applyPromptCaching` | function | 101–166 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `countBreakpoints` | function | 172–187 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `enforceCacheControlLimit` | function | 189–214 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `normalizeTtlOrdering` | function | 220–245 | no | 0 | `anthropic-prompt-cache.ts (L2 owner)` | +| `isLikelyRealAnthropicThinkingSignature` | function | 247–251 | no | 0 | `anthropic-messages.ts` | +| `formatAnthropicErrorBody` | function | 258–268 | yes | 1 | `anthropic.ts (residual)` | +| `isAnthropicRecord` | function | 270–272 | no | 0 | `anthropic-response-values.ts` | +| `anthropicStructuralValueType` | function | 274–277 | no | 0 | `anthropic-response-values.ts` | +| `InvalidAnthropicShapeDiagnostic` | interface | 279–283 | no | 0 | `anthropic-response-values.ts` | +| `invalidAnthropicShapeEvent` | function | 290–301 | no | 0 | `anthropic-response-values.ts` | +| `extractAnthropicErrorDetail` | function | 303–321 | no | 0 | `anthropic.ts (residual)` | +| `usesNativeAnthropicEndpoint` | function | 323–329 | no | 0 | `anthropic.ts (residual)` | +| `anthropicMessagesUrl` | function | 332–341 | yes | 1 | `anthropic.ts (residual)` | +| `synthesizeToolUseId` | function | 343–345 | no | 0 | `anthropic-response-values.ts` | +| `usableToolUseId` | function | 353–355 | no | 0 | `anthropic-response-values.ts` | +| `MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES` | const | 366–366 | no | 0 | `anthropic-response-values.ts` | +| `utf8BytesExceed` | function | 373–390 | no | 0 | `anthropic-response-values.ts` | +| `lastValidJsonObject` | function | 392–417 | no | 0 | `anthropic-response-values.ts` | +| `toolUseArguments` | function | 419–439 | no | 0 | `anthropic-response-values.ts` | +| `streamedToolArgumentsParse` | function | 447–456 | no | 0 | `anthropic-response-values.ts` | +| `anthropicKeyUsesBearer` | function | 458–460 | no | 0 | `anthropic.ts (residual)` | +| `reasoningBudget` | function | 463–473 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `ADAPTIVE_THINKING_FAMILY_MINIMUMS` | const | 482–486 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `claudeFamilyVersion` | function | 504–515 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `meetsFamilyMinimum` | function | 517–526 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `usesAdaptiveThinking` | function | 528–530 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS` | const | 544–546 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `supportsExplicitThinkingDisable` | function | 548–550 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `adaptiveEffort` | function | 553–555 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `defaultReasoningEffort` | function | 557–567 | no | 0 | `anthropic-reasoning-policy.ts (L2 owner)` | +| `usageFromAnthropic` | function | 569–585 | no | 0 | `anthropic-response-values.ts` | +| `mergeAnthropicUsage` | function | 587–596 | no | 0 | `anthropic-response-values.ts` | +| `buildToolNameTransforms` | function | 598–609 | no | 0 | `anthropic.ts (residual)` | +| `toAnthropicToolResult` | function | 611–630 | no | 0 | `anthropic-messages.ts` | +| `unrepresentableToolCallText` | function | 632–635 | no | 0 | `anthropic-messages.ts` | +| `orphanToolResultText` | function | 637–643 | no | 0 | `anthropic-messages.ts` | +| `messagesToAnthropicFormat` | function | 651–792 | no | 0 | `anthropic-messages.ts` | +| `toolsToAnthropicFormat` | function | 794–806 | no | 0 | `anthropic-tool-schema.ts (L2 owner)` | +| `normalizeAnthropicInputSchema` | function | 808–868 | no | 0 | `anthropic-tool-schema.ts (L2 owner)` | +| `createAnthropicAdapter` | function | 870–1375 | yes | 26 | `residual; parser methods → stream/response leaves` | + +Nested method relocation inventory (additional to, not replacing, the top-level inventory): + +| Original member | Original range | Captured outer bindings | New owner/export | +|---|---|---|---| +| createAnthropicAdapter.buildRequest | 878–1035 | provider, isOAuth, toolNames, cacheRetention | stays inline in anthropic.ts | +| createAnthropicAdapter.parseStream | 1037–1272 | provider, toolNames | anthropic-stream.ts / createAnthropicStreamParser | +| createAnthropicAdapter.parseResponse | 1274–1372 | provider, toolNames | anthropic-response.ts / createAnthropicResponseParser | + +The new factory functions have one production consumer each (the residual adapter); that is intended post-split fan-in, not an origin/dev count. + +## Leaf partition + +L2's three leaves remain unchanged at 187 / 122 / 79 expected lines. L1's codec/residual remain 302 / 227. L3 adds exactly **four** sibling files: + +1. `src/adapters/anthropic-messages.ts`: move **34–44 (11), 247–252 (6), 611–793 (183)** = **200** lines. Symbols: `toAnthropicContentPart`, `isLikelyRealAnthropicThinkingSignature`, `toAnthropicToolResult`, `unrepresentableToolCallText`, `orphanToolResultText`, `messagesToAnthropicFormat`. Export only `messagesToAnthropicFormat` to the adapter. Expected **207** = 200 + six imports + blank. + +```ts +import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxToolResultMessage } from "../types"; +import { namespacedToolName } from "../types"; +import { createToolCallIdAllocator } from "./tool-call-id"; +import { parseDataUrl } from "./image"; +import { identifyRoutedModel } from "./identity"; +import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge"; +``` + +Do not carry the original unused OcxMessage or ToolCallIdAllocator imports into this leaf. Keep allocator creation and reserve/allocate/lookup passes together inside the moved function. + +2. `src/adapters/anthropic-response-values.ts`: move **270–302 (33), 343–457 (115), 569–597 (29)** = **177** lines. Symbols: `isAnthropicRecord`, `anthropicStructuralValueType`, `InvalidAnthropicShapeDiagnostic`, `invalidAnthropicShapeEvent`, `synthesizeToolUseId`, `usableToolUseId`, `MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES`, `utf8BytesExceed`, `lastValidJsonObject`, `toolUseArguments`, `streamedToolArgumentsParse`, `usageFromAnthropic`, `mergeAnthropicUsage`. Export only the eight called from parser leaves: isAnthropicRecord, anthropicStructuralValueType, invalidAnthropicShapeEvent, usableToolUseId, toolUseArguments, streamedToolArgumentsParse, usageFromAnthropic, mergeAnthropicUsage. Expected **179** = 177 + one import + blank. + +```ts +import type { AdapterEvent, OcxUsage } from "../types"; +``` + +3. `src/adapters/anthropic-stream.ts`: move method **1037–1272 (236)** without modifying its body. Replace method syntax by the returned async generator shown below; one closure factory owns access to the existing provider/toolNames objects. Expected **245** = 236 moved + two closure wrapper lines + six imports + blank. This is not a stream state-machine decomposition. + +```ts +import type { ProviderAdapter } from "./base"; +import type { AdapterEvent, OcxProviderConfig } from "../types"; +import { debugDroppedFrame } from "../lib/debug"; +import { decodeServerSentEvents } from "../lib/sse-decoder"; +import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget"; +import { usableToolUseId, streamedToolArgumentsParse, usageFromAnthropic, mergeAnthropicUsage } from "./anthropic-response-values"; +``` + +Exact wrapper signature (body is the verbatim original 1038–1271): + +```ts +export function createAnthropicStreamParser(provider: OcxProviderConfig, toolNames: { fromWire: (name: string) => string }): ProviderAdapter["parseStream"] +``` + +Inside it return `async function* parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator` with that body. Close the returned function with `};`, then close the factory. Do not implement a second forwarding generator or eagerly allocate stream state in the outer factory. + +4. `src/adapters/anthropic-response.ts`: move method **1274–1372 (99)**, body unchanged. Expected **107** = 99 moved + two wrapper lines + five imports + blank. + +```ts +import type { ProviderAdapter } from "./base"; +import type { AdapterEvent, OcxProviderConfig } from "../types"; +import type { TranslatorBudget } from "../lib/translator-budget"; +import { retainTranslatedEventBatch } from "../lib/translator-budget"; +import { isAnthropicRecord, anthropicStructuralValueType, invalidAnthropicShapeEvent, usableToolUseId, toolUseArguments, usageFromAnthropic } from "./anthropic-response-values"; +``` + +Exact wrapper signature: + +```ts +export function createAnthropicResponseParser(provider: OcxProviderConfig, toolNames: { fromWire: (name: string) => string }): NonNullable +``` + +Return `async function parseResponse(response: Response, budget: TranslatorBudget): Promise` with original 1275–1371 body. `NonNullable` is needed because base.ts:66 declares the adapter member optional; do not widen this concrete factory's result to possibly undefined. Both factories use an inline structural type for toolNames instead of duplicating its producer or adding a contracts module. + +Residual `src/adapters/anthropic.ts` retains the original public functions `formatAnthropicErrorBody`, `anthropicMessagesUrl`, `createAnthropicAdapter`; private `extractAnthropicErrorDetail`, `usesNativeAnthropicEndpoint`, `anthropicKeyUsesBearer`, `COMPAT_TOOL_PREFIX`, `buildToolNameTransforms`; and the entire buildRequest method. + +Physical ledger: #a left ≤1007. #b removes **200 + 177 + 236 + 99 = 712**, reserves **10** net wiring/format lines, yielding **≤305**. Combined: **1375 − 381 − 712 + 13 + 10 = 305**. Final L3 leaves expected **207 / 179 / 245 / 107** (all ≤400). Source import cleanup may reduce the residual further. Neither factory-body method is counted twice as moved source. No #c remains necessary for size after this partition, but additional PR layers or a size exception are necessary to satisfy the conflicting changeset cap. + +## Re-export block + +No existing public binding moves out of anthropic.ts in L3, so the exact additional named re-export/type-re-export block is **empty**. Keep these three public functions defined there (original signatures and implementations, with factory wiring only): + +```ts +export function formatAnthropicErrorBody(status: number, _headers: Headers, payloadText: string): string +export function anthropicMessagesUrl(baseUrl: string): string +export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetention?: "none" | "short" | "long"): ProviderAdapter +``` + +Do not add public exports for any new private seam. The residual's new imports: + +```ts +import { messagesToAnthropicFormat } from "./anthropic-messages"; +import { createAnthropicStreamParser } from "./anthropic-stream"; +import { createAnthropicResponseParser } from "./anthropic-response"; +``` + +Keep L2's actual local imports as well: + +```ts +import { MAX_CACHE_BREAKPOINTS, resolveCacheControl, applyPromptCaching, enforceCacheControlLimit, normalizeTtlOrdering } from "./anthropic-prompt-cache"; +import { DEFAULT_MAX_TOKENS, REASONING_MAX_TOKENS_CEILING, ADAPTIVE_THINKING_CEILING, MIN_THINKING_BUDGET, OUTPUT_HEADROOM, OUTPUT_FLOOR, reasoningBudget, usesAdaptiveThinking, supportsExplicitThinkingDisable, adaptiveEffort, defaultReasoningEffort } from "./anthropic-reasoning-policy"; +import { toolsToAnthropicFormat } from "./anthropic-tool-schema"; +``` + +Replace only the two method properties in the returned adapter object: + +```ts +parseStream: createAnthropicStreamParser(provider, toolNames), +parseResponse: createAnthropicResponseParser(provider, toolNames), +``` + +No parser helper import is needed by the residual. Keep the original base/types, OAuth, image-limit/normalization, output-schema, redact, fingerprint, tool-choice, modelRecordValue and AgentRouter bindings still used by buildRequest/URL/error/tool-prefix code. Remove moved-only imports for message conversion, debug, SSE and translator-budget. Re-exports never substitute for local imports. + +## Module-level state and cycles + +No top-level let/Map/Set/WeakMap/lock in the original or proposed adapter leaves. Inherited reasoning tables at original 482–486 and 544–546 remain owned by the L2 reasoning-policy leaf. COMPAT_TOOL_PREFIX:58 stays original; MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES:366 moves only to response-values. Other scalar policy constants stay with their L2 owners. + +| Existing state | Original lines | Lifetime and owner after move | +|---|---|---| +| isOAuth, toolNames | 871–872 | per adapter, original constructor; same object passed into each parser factory | +| callIds | 657 onward | per messagesToAnthropicFormat call, messages leaf | +| requiredIds / seen | 738 / 741 | per assistant-message pairing, messages leaf | +| budgetEncoder; currentBlockType/currentToolCallId/currentToolCallName/currentToolCallJson; pendingUsage/pendingStopReason; emittedDone/sawVisibleText | 1043–1051 | allocated inside each parseStream invocation, stream leaf | +| emitDone closure | 1053–1073 | captures only that stream invocation's state, stream leaf | +| responseBytes / events / finishWithEvents | 1289–1298 | per buffered parse invocation, response leaf | + +No holder is copied into both leaves, and no per-stream variable is moved to module or outer-factory scope. Preserve tool-call budget open/close order, retained/transient accounting, cancellation `finally`, terminal error returns, and exact usage merge precedence. The original methods contain no `this` access; closure factories need no `.bind`, shared context object or callback adapter. + +Forbidden cycles: messages → anthropic (for its signature checker); stream/response → anthropic (for usage/tool-argument helpers); response-values → either parser. Move each helper to the downstream owner specified above; stream and response do not import one another. Existing types/base dependencies are imported directly; no leaf imports registry or the old public boundary. Type edges count in the cycle check. This is functional coupling; shared wire-format parsing stays single-owned, and request-state temporal coupling stays confined to one function. + +## Tests + +Complete direct-import `rg -l` list for the exact `src/adapters/anthropic` module (line numbers are import sites, not source-text reads). Each of the **24 test files is unchanged**: + +```text +tests/adapters/adapter-usage.test.ts:3 +tests/adapters/openai/openai-chat-model-suffix.test.ts:3 +tests/adapters/buffered-response-shape-guards.test.ts:2 +tests/adapters/anthropic/anthropic-tool-schema.test.ts:2 +tests/adapters/anthropic/anthropic-compatible-stream.test.ts:2 +tests/adapters/anthropic/anthropic-error-stop-reason.test.ts:2 +tests/adapters/anthropic/anthropic-agentrouter-language-framing.test.ts:2 +tests/adapters/anthropic/anthropic-image-retry.test.ts:3 +tests/adapters/translator-budget.test.ts:3 +tests/adapters/anthropic/anthropic-empty-content.test.ts:2 +tests/adapters/anthropic/anthropic-tail-guard.test.ts:2 +tests/adapters/anthropic/anthropic-eof-tolerance.test.ts:2 +tests/adapters/anthropic/anthropic-stream-hardening.test.ts:2 +tests/adapters/anthropic/anthropic-hardening.test.ts:2 +tests/adapters/anthropic/anthropic-error-body.test.ts:3 +tests/adapters/anthropic/anthropic-reasoning.test.ts:2 +tests/adapters/anthropic/anthropic-thinking-signature.test.ts:3 +tests/adapters/identity-neutralize.test.ts:13 +tests/codex-integration/reasoning-effort.test.ts:3 +tests/providers/umans-provider.test.ts:2 +tests/responses/sse-null-data-frame.test.ts:2 +tests/responses/responses-parser-malformed-content.test.ts:4 +tests/clients/client-fingerprint.test.ts:8 +tests/claude-integration/claude-messages-endpoint.test.ts:8 +``` + +Production consumers are `src/adapters/registry.ts:1` and `src/index.ts:4` (public re-export). Exact-path fan-in is **26 files**, with `createAnthropicAdapter` in all 26, `formatAnthropicErrorBody` in one test, and `anthropicMessagesUrl` in one test. Do not count `src/oauth/index.ts:34`'s different `./anthropic` module or comments in provider/config-export/google sources. + +Text-oracle readers of this source: **none found**, consistent with lane 014:173. Verified by basename/exact-path `rg -n` across tests, then inspecting candidates for `readFileSync|readFile\\(|Bun\\.file|source\\(`. Layout JSON references are test-path metadata. The GUI-file reads in `anthropic-pool-toggle-copy.test.ts:44,54,63,73` do not read S03 code. Thus no retarget-to-leaf or add-leaf-to-scan-list action, and no source-read line to report. Behavioral imports remain unchanged so they exercise the original compatibility boundary. + +Retain L2's explicit breakpoint assertions in adapter-usage.test.ts unchanged in L3. No new test filename, source-text oracle or layout-map entry is required. + +C-phase guards to drive red once, then restore (implementation only): + +- `anthropic-thinking-signature.test.ts`, `anthropic-tail-guard.test.ts`, `anthropic-hardening.test.ts`: bypass respectively the moved thinking-signature predicate, terminal user nudge, and call/result pairing. Each corresponding existing assertion must fail; restore all. +- `anthropic-error-stop-reason.test.ts`: suppress an error-terminal branch in each parser separately and verify that parser's case fails. Do not test just one wire mode. +- `anthropic-eof-tolerance.test.ts` and `anthropic-stream-hardening.test.ts`: perturb usable-ID/assembled-JSON validation, confirm existing invalid-tool and EOF cases fail, then restore. +- `tests/adapters/translator-budget.test.ts`: remove the terminal return after a stream budget error or the buffered release in a temporary mutation; matching budget/terminal tests must fail, then restore. +- Preserve tests through `createAnthropicAdapter`, not direct imports solely to expose internals. All byte/order-sensitive assertions remain at least as strict. + +## Verification + +Implementation-only 002 **Per-layer gate**, not executed by this doc author: + +```sh +bun run typecheck +bun test tests/adapters/anthropic tests/adapters/adapter-usage.test.ts tests/adapters/openai/openai-chat-model-suffix.test.ts tests/adapters/buffered-response-shape-guards.test.ts tests/adapters/translator-budget.test.ts tests/adapters/identity-neutralize.test.ts tests/codex-integration/reasoning-effort.test.ts tests/providers/umans-provider.test.ts tests/responses/sse-null-data-frame.test.ts tests/responses/responses-parser-malformed-content.test.ts tests/clients/client-fingerprint.test.ts tests/claude-integration/claude-messages-endpoint.test.ts +bun run privacy:scan +wc -l src/adapters/anthropic-image-codec.ts src/adapters/anthropic-image-normalize.ts src/adapters/anthropic-prompt-cache.ts src/adapters/anthropic-reasoning-policy.ts src/adapters/anthropic-tool-schema.ts src/adapters/anthropic-messages.ts src/adapters/anthropic-response-values.ts src/adapters/anthropic-stream.ts src/adapters/anthropic-response.ts src/adapters/anthropic.ts +rg -n 'from "[^"]*/adapters/anthropic"' src gui/src scripts tests +rg -n 'adapters/anthropic|from "./anthropic"' src/index.ts src/adapters/registry.ts +git diff --numstat +``` + +Expected: typecheck/privacy exit 0, focused adapters/anthropic plus adapters/openai, codex-integration, providers, responses, clients and claude-integration at 0 fail; original adapter consumer set unchanged at 26; every listed source file ≤400. Verify actual new import/re-export graph including type edges against the stated DAG. Conditional 002 core-lab gate is not activated for adapter-only edits; never change PROTECTED roots or introduce Lab edges. Any need to touch server/router/lib first escalates scope and then invokes that gate. + +Full suite only on lidge, not locally: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-anthropic-b && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Capture exact remote HEAD and true full-suite exit status using pipefail or an unpiped run; tail success is not suite success. Record the exact-head CI rollup and ensure this L3 tip contains the reviewed L2 tip. The doc author does not run remote commands or CI. + +## Accept criteria + +1. All original 54 top-level declarations have exactly one owner; all three original public exports and all 26 consumer files stay compatible. +2. The four new leaves are ≤400, expected 207/179/245/107. The residual is ≤305; inherited L1/L2 modules also remain ≤400. +3. Arithmetic matches #a: 381 lines moved there, 712 here, residual allowance 305; no new source file or final residual exceeds 400. +4. Methods move with bodies intact; only closure factory syntax, imports/exports and two property bindings change. No wrappers add a generator hop or alter promise/error timing. +5. Each stream/buffer invocation owns its state and cleanup; provider/toolNames captures preserve reference identity and late-read semantics. +6. Zero new upward/type/runtime cycles, duplicate helper/state owners, public seam exports, source-oracle weakening or caller migrations. +7. Existing behavioral tests, selected red/restored-green guards, focused checks, privacy, remote full suite and exact-head CI all pass for this tip. +8. Parent resolves the >500-line conflict before execution; this doc never claims the fixed L3 diff fits that cap or authorizes extra stack layers. +9. No merges, releases, orchestration commands or source/test edits occur as part of drafting these three documents. + +## PR + +Title: `refactor(adapters): separate Anthropic messages and response parsers (split S03 L3/3)` + +Base: `codex/split-adapters-anthropic-a`. Branch: `codex/split-adapters-anthropic-b`. Closes: none. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S03 L1/3 | # | `codex/split-adapters-anthropic-image-normalize` | `dev` | Single cache/codec owner | +| S03 L2/3 | # | `codex/split-adapters-anthropic-a` | `codex/split-adapters-anthropic-image-normalize` | Private prompt-cache/reasoning/schema leaves | +| S03 L3/3 | # | `codex/split-adapters-anthropic-b` | `codex/split-adapters-anthropic-a` | Message conversion and response parsers | + +Use the repository template's Summary, Verification and Checklist, mark L3 current, and state depends on #. Review only this layer's diff; L1/L2 maintain their own independent gates. Parent cascades any lower-layer change and refreshes exact-head checks. No merge is authorized. diff --git a/devlog/_plan/260905_now_split_train/105_cursor_desktop_executor_contract.md b/devlog/_plan/260905_now_split_train/105_cursor_desktop_executor_contract.md new file mode 100644 index 0000000000..958b2efbbb --- /dev/null +++ b/devlog/_plan/260905_now_split_train/105_cursor_desktop_executor_contract.md @@ -0,0 +1,352 @@ +# S04 L0 — desktop executor contract (105) + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Docs HEAD: `4cc219549eafbf9cd2efd651482fbfefd88944d5`. Fresh source basis: +`origin/dev = 4457429662bc98279d8b321e6f75d752f77e78e8`. +The seven inspected S04/companion source files are unchanged from `1362b1a38` +(`git diff 1362b1a38 origin/dev -- ` returned no delta). +All source ranges below are at the fresh origin/dev basis. + +Read: 003_parent_decisions.md TYPE-CYCLE-01 and PURE-MOVE-SIZE-01; +002_layer_map.md rows 105–150. This is the approved prerequisite's +implementation plan, not a claim that its code or PR has merged. The requested +“prerequisite landed as layer 105” pointer in 110 means assigned to this +roadmap layer; actual implementation receipts remain the parent's responsibility. + +## Loop spec + +- Archetype: **pure-move**; C3 dependency-boundary planning, docs-only delegated mode. +- Goal: remove the provider-type dependency on the desktop executor implementation + before the five S04 split layers introduce new leaves. Preserve the public type + at the original import path and preserve its exact five optional properties. +- Non-goals: no executor behavior, shell/spawn/timer/error changes, new runtime + imports, new validation, new package, extra config type, provider-file split, + S04 symbol repartitioning, or test implementation in this drafting task. +- Verifier: 002 **Per-layer gate**, instantiated below, as amended by 003: + pure-move non-move diff ≤150 lines; compare cycle delta, not unrelated baseline + cycles. S04 depth six is explicitly approved by TYPE-CYCLE-01. +- Stop: the parent has exact-tip export/type-resolution, graph, focused-test, + privacy and remote full-suite receipts plus green exact-head CI. No merge. +- Escalation: any runtime diff, changed type shape, new cycle, source drift or + additional companion edit requires parent direction; do not widen this layer. + +Structural decision: `src/types/provider.ts:701` currently imports a type +through `native-exec-desktop.ts`, whose type dependency on +`native-exec-tools.ts` (specifier at native-exec-desktop.ts:19) connects back +to tool-definitions via native-exec-tools.ts:25. Move the existing contract to a +dependency-free sibling; both provider and implementation consume that owner. +Rejected: only redirecting tool-naming to types/request, because +types/request.ts:3 still reaches provider; duplicating the interface, because +that introduces a second authority; runtime/lazy imports, because this is an +erased-type dependency and needs no runtime mechanism. Deletion/configuration +cannot preserve the contract. Reuse the exact existing declaration. + +Blast radius: one adapter implementation and one provider type-reference field, +plus one new contract. No package API changes. Existing kebab-case siblings +`native-exec-common.ts`, `native-exec-tools.ts`, and `claude-id.ts` establish +the naming/layout convention; no index barrel is added. + +## Symbol inventory + +Main source: `src/adapters/cursor/native-exec-desktop.ts`, 207 lines. +Inventory covers every owned top-level declaration; imports are dependencies. +Evidence: `git show origin/dev:src/adapters/cursor/native-exec-desktop.ts` +and `ast-grep run --lang typescript --kind --json=compact src/adapters/cursor/native-exec-desktop.ts`, +filtered to top-level declarations at source column zero (exported declarations +start after the export modifier in AST output). Ranges exclude leading comments. + +Counts are distinct external referencing files from +`rg -l -w '' src gui/src scripts tests`, excluding the defining file. +Private declarations have zero external bound consumers. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `DEFAULT_DESKTOP_TIMEOUT_MS` | const | 21–21 | no | 0 | native-exec-desktop.ts (residual) | +| `DesktopExecutorConfig` | interface | 28–37 | yes | 1 | desktop-executor-contract.ts | +| `desktopDepsFromConfig` | function | 45–55 | yes | 2 | native-exec-desktop.ts (residual) | +| `runComputerUse` | async function | 57–80 | no | 0 | native-exec-desktop.ts (residual) | +| `runRecordScreen` | async function | 82–111 | no | 0 | native-exec-desktop.ts (residual) | +| `computerUseError` | function | 113–117 | no | 0 | native-exec-desktop.ts (residual) | +| `recordScreenFailure` | function | 119–123 | no | 0 | native-exec-desktop.ts (residual) | +| `runExternalJson` | function | 130–207 | no | 0 | native-exec-desktop.ts (residual) | + +Exact `DesktopExecutorConfig` references: + +- Definition: native-exec-desktop.ts:28. +- Same-file annotations: :45, :57, :82, :130 (four local consumers). +- Only external consumer: src/types/provider.ts:701, inline import type. +- Zero direct test consumers of that type name. + +Companion edit only: the `desktopExecutor` property at src/types/provider.ts:701 +inside the existing `OcxProviderConfig` interface (:172–723). Change only its +import specifier; do not move or reinventory unrelated provider declarations. +The two external `desktopDepsFromConfig` consumers are +src/adapters/cursor/live-transport.ts:68 and +tests/providers/cursor/cursor-desktop-exec.test.ts:10. + +## Leaf partition + +### New `src/adapters/cursor/desktop-executor-contract.ts` + +Move native-exec-desktop.ts:23–37 verbatim: five leading documentation lines and +the ten-line exported interface. Expected size **15 lines**, no own imports, +no runtime values, no dependencies, no initialization. + +```ts +/** + * Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and + * cannot drive a screen itself; set these commands only when running on a host that can. Each + * command receives the request as JSON on stdin and must print a JSON result on stdout. + */ +export interface DesktopExecutorConfig { + /** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */ + computerUseCommand?: string; + /** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */ + recordScreenCommand?: string; + cwd?: string; + env?: Record; + /** Max time to wait for the external process. Default 30s. */ + timeoutMs?: number; +} +``` + +### Residual and companion + +- `native-exec-desktop.ts`: keep every other body and import. Remove the 15-line + slice, add the two one-line bindings below: **207 − 15 + 2 = 194 lines**. + No residual over 400 and no #b layer. +- `src/types/provider.ts`: one line replaced, **723 → 723**. This is an + explicitly approved companion type-reference edit, not a split target or a + claim that the provider file's pre-existing size debt is resolved. +- Leaf plus split residual: **15 + 194 = 209**, original 207 plus two glue lines. + Including companion: **932** total versus 930 before, net +2. +- PURE-MOVE-SIZE-01 accounting: 15 verbatim lines transferred; two added + import/re-export lines and one removed/one added consumer line give **4 raw + non-move changed lines**, well below 150. Record actual diff at execution; + do not count this docs file as runtime source churn. + +## Re-export block + +Keep every current export importable from native-exec-desktop.ts. The runtime +`desktopDepsFromConfig` declaration remains exported in place. + +```ts +export type { DesktopExecutorConfig } from "./desktop-executor-contract"; +``` + +Re-export binds nothing locally. Add the explicit erased local binding for +the four existing annotations; no value import: + +```ts +import type { DesktopExecutorConfig } from "./desktop-executor-contract"; +``` + +Exact provider.ts:701 replacement: + +```ts + desktopExecutor?: import("../adapters/cursor/desktop-executor-contract").DesktopExecutorConfig; +``` + +No field rename, requiredness change, alias type, duplicate interface, exported +runtime object, or consumer change beyond this one inline type specifier. + +## Module-level state and cycles + +The sole top-level value `DEFAULT_DESKTOP_TIMEOUT_MS` at :21 stays in +native-exec-desktop.ts. There is no top-level let, Map, Set, WeakMap, lock or +timer. stdout/stderr/settled at :140–142 and timer at :143–148 are invocation-local +inside runExternalJson and do not move. The new contract owns only the interface; +provider imports its type, never copies its fields. No lifecycle or state changes. + +Read-only resolver evidence, run during this drafting turn: + +1. Read files using `git show 4457429662bc98279d8b321e6f75d752f77e78e8:`; + resolve relative static from/re-export specifiers and literal inline + `import("...")` type specifiers, including erased types. Resolve exact paths, + .ts/.tsx/.mts/.mjs and index.ts against the basis tree. +2. Record the baseline provider edge and the tool-definitions return cycle. +3. In memory only, add the contract leaf, its desktop import/re-export and the + provider specifier replacement. Overlay all eleven leaf imports and moved + source ranges from docs 110–150; add each facade's exact planned imports and + re-exports. Conservatively retain baseline facade imports as an edge superset, + so a missing return path is not caused by dropping an unmodelled old import. +4. Breadth-first traverse from each of the twelve new leaves; fail if an edge + returns to that starting leaf. Assert provider no longer directly imports + native-exec-desktop and directly imports the contract instead. + +Observed output (resolver exit **0**, no production imports or tests executed): + +```text +basis provider->desktop: true +basis cycle: tool-definitions -> types -> provider -> native-exec-desktop -> native-exec-tools -> tool-definitions +overlay provider->desktop: false +overlay provider->contract: true +desktop-executor-contract.ts: no return cycle +tool-naming.ts: no return cycle +tool-schemas.ts: no return cycle +tool-guidance.ts: no return cycle +catalog-data.ts: no return cycle +image-format.ts: no return cycle +image-preparation.ts: no return cycle +tool-budget.ts: no return cycle +protobuf-event-state.ts: no return cycle +protobuf-tool-events.ts: no return cycle +patch-grammar.ts: no return cycle +structured-edit.ts: no return cycle +PASS: 12 planned leaves; provider edge removed; no new leaf closes a type/runtime cycle +``` + +The in-memory negative control also ran in this drafting turn: restoring only +the old provider specifier produced `tool-naming → types → provider → +native-exec-desktop → native-exec-tools → tool-definitions → tool-naming`. +This establishes that the resolver detects the exact cycle the prerequisite +removes; no source file was mutated for either check. + +Before: +`src/types.ts:112 → src/types/provider.ts:701 → native-exec-desktop.ts:19 → native-exec-tools.ts:25 → tool-definitions.ts:3 → src/types.ts`. + +After: +`provider.ts:701 → desktop-executor-contract.ts` and +`native-exec-desktop.ts → desktop-executor-contract.ts`; the contract has +zero outgoing edges, so it cannot return to the implementation. The runtime +dependency graph does not change: the interface, export type, import type and +provider inline type are erased. This is plan-overlay evidence, **not** evidence +of code already landed; the layer executor repeats the resolver against the +actual layer tip and each S04 tip. Per TYPE-CYCLE-01, unrelated pre-existing +cycles are baselined rather than repaired in this layer. + +## Tests + +Commands used for this inventory (read-only): + +```sh +rg -n -w DesktopExecutorConfig src gui/src scripts tests +rg -l -w DesktopExecutorConfig src gui/src scripts tests +rg -n 'native-exec-desktop' src gui/src scripts tests +rg -l 'readFileSync|Bun\.file|source\(' tests | xargs rg -n 'native-exec-desktop|types/provider|DesktopExecutorConfig' +``` + +- `tests/providers/cursor/cursor-desktop-exec.test.ts:10` — **unchanged**, + the sole direct test importer of native-exec-desktop.ts; it imports + desktopDepsFromConfig, not DesktopExecutorConfig. Preserve executor result, + unsupported-default, pipe-error and platform-shell assertions. +- No direct DesktopExecutorConfig test imports; typecheck covers provider.ts:701 + and the four desktop implementation annotations. Preserve the historical + type re-export as a separate structural/export check, not merely runtime tests. +- No explicitly named text-oracle reader of either touched source was found by + the source-reader candidate search. No retarget-to-leaf or add-leaf-to-scan-list. +- `tests/lab/core-lab-boundary.test.ts` is **not affected by this layer**: + its static traversal regex (:50) skips import type/export type, and its + traversal (:80) skips literal import() edges; its source read at :69 may still + visit the desktop implementation, but all runtime edges there are unchanged. + The new dependency-free type-only leaf is unreachable through the new erased + bindings. No src/server, src/router or src/lib source is changed, so 002's + conditional local Lab gate is not activated. Do not edit PROTECTED roots or + add the contract to a runtime scan list. Later S04 move layers retain their + own Lab checks. +- Guard to drive red once during implementation: in a disposable **in-memory** + graph overlay, restore provider.ts's old native-exec-desktop type specifier + while keeping the S04 leaf overlays. The resolver must again report the + tool-naming return cycle. Restore the contract edge and require zero new-leaf + cycles. Also assert exactly one DesktopExecutorConfig interface declaration + and both the compatibility re-export and local type import. No source mutant + is committed, no new test file/layout entry is required. + +## Verification + +Implementation-only commands, not run by this drafting delegate. Execute at +the layer's own tip in its dedicated worktree: + +```sh +bun run typecheck +bun test tests/providers/cursor/cursor-desktop-exec.test.ts +bun run privacy:scan +wc -l src/adapters/cursor/desktop-executor-contract.ts src/adapters/cursor/native-exec-desktop.ts +rg -n 'interface DesktopExecutorConfig|import type.*DesktopExecutorConfig|export type.*DesktopExecutorConfig|desktopExecutor\?: import' src/adapters/cursor src/types/provider.ts +rg -n 'native-exec-desktop' src/types/provider.ts +git diff -M --stat dev...HEAD +git diff --color-moved=dimmed-zebra dev...HEAD -- src/adapters/cursor src/types/provider.ts +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch -q origin codex/split-cursor-desktop-executor-contract && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test > /tmp/suite-split-cursor-desktop-executor-contract.log 2>&1; rc=$?; tail -15 /tmp/suite-split-cursor-desktop-executor-contract.log; echo SUITE_EXIT=$rc; exit $rc' +``` + +The negative `rg` for native-exec-desktop in provider.ts must produce **no +matches (exit 1)**; that is expected, not a failed implementation check. +All positive checks must find the exact expected bindings. Run the graph +resolver described above against actual tip sources, and with future S04 leaf +overlays, plus its in-memory negative control. Check interface body equivalence +including comments/optional properties and exactly one definition. Keep the two +desktopDepsFromConfig consumers unchanged; the type consumer deliberately moves +from implementation to contract. A generic importer-count-equal rule must not +reject that explicitly approved one-edge migration. + +Require local typecheck/privacy exit 0, focused test 0 failures, remote +SUITE_EXIT=0 with printed SHA equal to this PR head, full remote log retained, +and green exact-head CI rollup. Full suite never locally. Parent verifies remote +checkout ownership before the planned fetch/checkout; no unrelated dirty work +may be overwritten. No merge or push is performed by this delegate. + +## Accept criteria + +1. desktop-executor-contract.ts contains exactly the original :23–37 slice + (15 lines), one interface and zero imports/runtime declarations. +2. Native desktop residual is 194 lines, with every runtime declaration and body + unchanged, both exact erased bindings present and the historical type export + preserved. Provider companion remains 723 lines; only :701's type path changes. +3. The five properties retain identical optionality and types; all four local + annotation uses resolve to the sole contract owner. +4. Resolver records provider → desktop absent and provider → contract present; + all twelve planned leaves have no return cycle. Negative control restores a + detectable cycle; unrelated baseline cycles stay outside scope. +5. Runtime import edges and Lab protected roots/traversal stay unchanged; no + source-oracle retarget or runtime scan-list expansion. +6. Non-move source diff ≤150 under PURE-MOVE-SIZE-01; verbatim relocation proved + with move-aware diff and exactly-once inventory. No opportunistic changes. +7. Exact-tip typecheck, focused test, privacy, remote full-suite and CI receipts + satisfy 002; no local full suite or unauthorized merge. +8. Six-layer map follows 002: 105 on dev, then 110/120/130/140/150 bottom-up. + Every split residual/new leaf is ≤400. The provider companion's existing + 723-line file is not counted as a new split residual. + +## PR + +Title: `refactor(adapters-cursor): isolate desktop executor type contract (split S04 L0/5)` + +Branch: `codex/split-cursor-desktop-executor-contract`. Base: `dev`. Closes: **none**. +L0–L5 are six layers; the L0/5 label preserves 002's zero-based prerequisite +numbering and the existing five split-layer titles. + +Use every section of .github/PULL_REQUEST_TEMPLATE.md: Summary, Verification, +Checklist. Put the stack map in Summary and link move-aware diff guidance in +Verification per PURE-MOVE-SIZE-01. Replace placeholders with actual PR numbers +only when the parent opens them. + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 0 (105) | #TBD-S04-L0 | `codex/split-cursor-desktop-executor-contract` | `dev` | desktop-executor-contract | +| 1 | #TBD-S04-L1 | `codex/split-adapters-cursor-tool-definitions` | `codex/split-cursor-desktop-executor-contract` | tool-definitions | +| 2 | #TBD-S04-L2 | `codex/split-adapters-cursor-catalog` | `codex/split-cursor-desktop-executor-contract` | catalog | +| 3 | #TBD-S04-L3 | `codex/split-adapters-cursor-images` | `codex/split-cursor-desktop-executor-contract` | images | +| 4 | #TBD-S04-L4 | `codex/split-adapters-cursor-request-builder` | `codex/split-adapters-cursor-images` | request-builder | +| 5 | #TBD-S04-L5 | `codex/split-adapters-cursor-protobuf-events` | `codex/split-adapters-cursor-tool-definitions` | protobuf-events | + +Current layer: **L0 (105)**. Parent: `dev`. +Changes to parent `dev` require rebasing this layer and cascading only +through its actual dependency descendants, with exact-tip/base rechecks +(DEV-STACK-02); sibling layer numbering creates no dependency. Merge remains +parent-before-child and separately authorized, never part of this draft. +\n## Execution record (B, 2026-09-05)\n\n- Worktree: (node_modules symlinked to the primary checkout; the\n a2c0 app worktree's node_modules lacks bun-types/@bufbuild — noted for\n every later layer).\n- Executor: gpt-6-astra high (Carson, 01a06edc-7667-7b90-833c-e5562a3e9084).\n- Commit: e950b27138b20cc06e4d7b7a2268b9cf996a08e2 on\n (base origin/dev 445742966);\n 3 files, +18/−16; leaf 15 lines, residual 194, provider.ts 723.\n- Local gate (main agent re-ran after the symlink fix): error TS2688: Cannot find type definition file for 'bun-types'. + The file is in the program because: + Entry point of type library 'bun-types' specified in compilerOptions\n exit 0; bun test v1.4.0 (34cbb9a40)\n 14 pass / 0 fail; Privacy scan passed passed; On branch codex/260905-modular-debt-ledger-docs +nothing to commit, working tree clean clean.\n- Pushed to origin: .\n + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-105.LalCGy/wt` (node_modules symlinked to the primary checkout; the a2c0 app worktree's node_modules lacks bun-types/@bufbuild). +- Executor: gpt-6-astra high (Carson, 01a06edc-7667-7b90-833c-e5562a3e9084). Incident: a stray `bun scripts/test.ts` from the first executor worktree let the test-runner fixture commit `base.txt`/`seed` onto the branch; the worktree was recreated and the branch reset to e950b2713 before review. Lesson for later layers: executors must not launch `bun run test` at all. +- Commits: e950b2713 (move, 3 files +18/−16) and 97df51515 (regression guard in tests/providers/cursor/cursor-desktop-exec.test.ts: type parity via both paths, contract has no imports, provider.ts points at the contract — driven red once by restoring the old provider specifier: 14 pass / 1 fail, then green 15/15). Required because CI hygiene `missing_regression_test` rejects a src/ change with no test change. +- Local gate at 97df51515: `bun run typecheck` 0; focused 15 pass / 0 fail; `bun run privacy:scan` passed; leaf 15 lines, residual 194. +- Adversarial diff review (Kepler, gpt-6-astra high, 01a06ede-fa7c-7673-8d86-12f5031d6fd4): round 1 GO-WITH-FIXES(1: stray base.txt), round 2 VERDICT: PASS (byte-identical move, export parity, one provider line, no runtime import added, contract zero imports). +- lidge full suite at 97df51515: `SUITE_EXIT=0`, 18013 pass / 0 fail / 16 skip, log `/tmp/suite-split-cursor-desktop-executor-contract.log` on lidge. +- PR: https://github.com/lidge-jun/opencodex/pull/3557 (base dev, head 97df51515). CI rollup at record time: hygiene/enforce-target/gates/storage policy/api usage/keyring ubuntu+windows/npm-global ubuntu+windows/test 1-3 of 4 SUCCESS; test 4/4, macos 1/2, macos 2/2, keyring macos, npm-global macos, CodeRabbit still running. Final rollup to be re-read before the next S04 layer bases on this branch. diff --git a/devlog/_plan/260905_now_split_train/110_adapters_cursor_tool_definitions.md b/devlog/_plan/260905_now_split_train/110_adapters_cursor_tool_definitions.md new file mode 100644 index 0000000000..a73073c604 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/110_adapters_cursor_tool_definitions.md @@ -0,0 +1,291 @@ +# S04 L1/5 — tool-definitions + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Docs basis: `4cc219549`; source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. Every source line range below refers to `src/adapters/cursor/tool-definitions.ts` at that source commit, not a future leaf. Read alongside 000_plan.md, 001_stale_check.md, 002_layer_map.md, and ../260905_modular_debt_ledger/014_lane_adapters_media.md (lane 014; relevant file subsection). Status: diff-level plan only; no code, Git mutation, test run, or orchestration performed by this delegate. + +## Loop spec + +- Archetype: **pure-move**. Work class C3 structural planning, docs-only delegated mode; the parent owns all loop/goal state. +- Goal: move the inventoried responsibilities into the named sibling leaves, each ≤400 lines, preserving the original public import path and leaving 113 expected lines in the original. +- Non-goals: no exported rename/removal, no behavior or signature change, no dependency/tooling installation, no new validation, no changes to generated protobufs, native-exec ownership, live transport scheduling, registry policy, or unrelated files. No production-module execution or test run in this drafting task. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated in Verification below. Planned commands are for the layer executor; they are not results from this draft. +- Stop: parent records an independently verified, exact-tip layer with all accepts met and exact-head CI rollup; no merge. Stop implementation immediately on a changed signature, string/wire delta, duplicated state, cycle, unaccounted source-reader, or unsupported layer-size claim. +- Escalation: the explicit type-only-cycle prerequisite in Module-level state and cycles blocks implementation at this basis; source drift, required files outside this partition/test list, an actual behavior defect, or the sizing conflict below goes to the parent; do not repair it opportunistically. Unreleased security findings go only to approved scratch, never this public devlog. + +Implementation sizing escalation: this exact partition transfers 667 existing physical lines before import/export glue, already over 002's 500 changed-source-line bound even if moves are counted only once. Under additions + deletions it is at least 1334 lines. The fixed S04 five-layer map has no #b slot. Do not silently call this PR ≤500: the parent must either approve a documented move-only size exception or revise the layer topology (and obtain approval for extra layer docs) before implementation. This bounded draft does not alter 002 or invent a sixth branch. + +Structural decision and pre-change map: Wire identity/choice policy (118–268, 326–396, 608–619), schemas (44–116, 399–444, 541–606), and model guidance (446–536, 621–736) have separate inputs. Keep structured-tool advertisement and protobuf serialization in the original boundary. Rejected: moving guidance alone leaves 661 lines; deleting descriptions or rewriting tool policy changes behavior. Chosen: three sibling leaves, matching native-exec-fs.ts / native-exec-network.ts / native-exec-tools.ts. Existing boundary consumers include protobuf-request.ts:63, native-exec-mcp.ts:28, and live-transport.ts:79. Current edges are consumer → tool-definitions → ../../types, gen/agent_pb, ../exec-tool-result-normalize (lines 1–6); new edges are boundary → naming/schema/guidance, schema → naming, guidance → naming. Feature-local blast radius; no package API or registration changes. + +No-code alternatives: doing nothing leaves the requested size debt; deletion/configuration cannot preserve these existing behaviors while shortening their implementation; reuse means moving the current declarations, not inventing equivalent helpers. Owner search: `rg --files src/adapters/cursor`, `rg -n '' src gui/src scripts tests`, and the lane-014 seam audit. The named new siblings do not already exist. Existing stable imports are compatibility boundaries, not permission for new convenience barrels. + +## Symbol inventory + +AST evidence: `git show origin/dev:src/adapters/cursor/tool-definitions.ts`; working-tree bytes compared equal; `ast-grep run --lang typescript --kind --json=compact src/adapters/cursor/tool-definitions.ts` for lexical/variable/function/interface/type-alias/class declarations, filtered to top-level source starts. Ranges are inclusive, include an `export` modifier on the same line, and exclude preceding comments. 76 owned top-level declarations; imports are dependencies, not redeclared owned symbols. + +Consumer counting: `rg -l 'tool-definitions' src gui/src scripts tests` narrows candidates; resolve static `from` and dynamic `import()` relative specifiers to this exact file; then `rg -l -w '' ` counts distinct referencing consumer files. Count excludes the defining file. Private declarations have 0 external bound consumers; their local references move with the partition. This is a file count, not call-site count; do not reuse 001's broad basename heuristic as symbol fan-in. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `OCX_RESPONSES_TOOL_PROVIDER` | const | 8–8 | yes | 5 | `tool-naming.ts` | +| `CODEX_EXEC_COMMAND_TOOL` | const | 9–9 | yes | 0 | `tool-naming.ts` | +| `CODEX_SHELL_COMMAND_TOOL` | const | 10–10 | yes | 0 | `tool-naming.ts` | +| `CODEX_UNIFIED_EXEC_TOOL` | const | 12–12 | yes | 0 | `tool-naming.ts` | +| `CODEX_WAIT_TOOL` | const | 13–13 | yes | 0 | `tool-naming.ts` | +| `CODEX_APPLY_PATCH_TOOL` | const | 14–14 | yes | 1 | `tool-naming.ts` | +| `CODEX_TOOL_SEARCH_TOOL` | const | 15–15 | yes | 0 | `tool-naming.ts` | +| `CURSOR_EDIT_FILE_TOOL` | const | 16–16 | yes | 1 | `tool-naming.ts` | +| `CURSOR_MULTI_EDIT_TOOL` | const | 17–17 | yes | 2 | `tool-naming.ts` | +| `CURSOR_STRUCTURED_EDIT_TOOLS` | const | 18–18 | yes | 0 | `tool-naming.ts` | +| `CURSOR_EXEC_COMMAND_TOOL` | const | 19–19 | yes | 0 | `tool-naming.ts` | +| `CODEX_SHELL_BRIDGE_TOOL_NAMES` | const | 20–20 | yes | 0 | `tool-naming.ts` | +| `CURSOR_SHELL_ALIAS_SYSTEM_NOTE` | const | 21–22 | yes | 1 | `tool-guidance.ts` | +| `NEIGHBOR_AGENT_TOOL_NAMES` | const | 23–23 | no | 0 | `tool-guidance.ts` | +| `NEIGHBOR_AGENT_TOOL_ALIASES` | const | 24–30 | no | 0 | `tool-guidance.ts` | +| `CURSOR_GENERIC_TOOL_USE_USER_HINT` | const | 32–42 | yes | 0 | `tool-guidance.ts` | +| `CURSOR_EXEC_COMMAND_INPUT_SCHEMA` | const | 44–56 | yes | 1 | `tool-schemas.ts` | +| `CURSOR_EDIT_FILE_INPUT_SCHEMA` | const | 65–74 | yes | 1 | `tool-schemas.ts` | +| `CURSOR_MULTI_EDIT_INPUT_SCHEMA` | const | 77–97 | yes | 1 | `tool-schemas.ts` | +| `CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA` | const | 104–116 | yes | 0 | `tool-schemas.ts` | +| `isCodexShellBridgeToolName` | function | 118–120 | yes | 1 | `tool-naming.ts` | +| `resolveShellBridgeAliasKey` | function | 126–139 | yes | 1 | `tool-naming.ts` | +| `cursorToolChoiceAliases` | function | 141–147 | yes | 1 | `tool-naming.ts` | +| `catalogHasBareCodexShellBridge` | function | 149–153 | no | 0 | `tool-naming.ts` | +| `cursorToolChoiceMatches` | function | 162–177 | no | 0 | `tool-naming.ts` | +| `isBareCodexShellBridgeTool` | function | 179–181 | yes | 1 | `tool-naming.ts` | +| `isCursorResponsesProvider` | function | 183–185 | no | 0 | `tool-naming.ts` | +| `CURSOR_EXECUTION_PATH_TOOL_NAMES` | const | 187–191 | no | 0 | `tool-naming.ts` | +| `isCursorExecutionPathTool` | function | 194–197 | yes | 1 | `tool-naming.ts` | +| `isCursorWaitTool` | function | 200–202 | yes | 1 | `tool-naming.ts` | +| `isCursorCodeModeExecTool` | function | 208–214 | yes | 1 | `tool-naming.ts` | +| `cursorRequestUsesCodeMode` | function | 227–234 | yes | 3 | `tool-naming.ts` | +| `isBareCodexExecCommandTool` | function | 237–239 | no | 0 | `tool-naming.ts` | +| `cursorRequestHasShellAlias` | function | 241–243 | yes | 3 | `tool-naming.ts` | +| `cursorRequestHasExecutionPath` | function | 245–249 | no | 0 | `tool-naming.ts` | +| `cursorRequestAdvertisesApplyPatch` | function | 251–257 | yes | 2 | `tool-naming.ts` | +| `isCursorStructuredEditToolName` | function | 259–261 | yes | 2 | `tool-naming.ts` | +| `isCursorSyntheticStructuredEditTool` | function | 264–268 | yes | 2 | `tool-naming.ts` | +| `cursorStructuredEditTools` | function | 284–312 | yes | 3 | `tool-definitions.ts` (residual) | +| `cursorRequestAdvertisesStructuredEdits` | function | 319–324 | yes | 0 | `tool-definitions.ts` (residual) | +| `CURSOR_CLIENT_TOOL_WIRE_PREFIX` | const | 326–326 | no | 0 | `tool-naming.ts` | +| `CURSOR_PROXY_OWNED_BARE_TOOL_NAMES` | const | 327–336 | no | 0 | `tool-naming.ts` | +| `isCursorBareClientToolWireAliased` | function | 339–344 | no | 0 | `tool-naming.ts` | +| `cursorToolWireName` | function | 346–351 | yes | 4 | `tool-naming.ts` | +| `clientSemanticToolNameFromCursorWire` | function | 353–357 | no | 0 | `tool-naming.ts` | +| `CURSOR_MCP_DISPLAY_PREFIX` | const | 366–366 | no | 0 | `tool-naming.ts` | +| `normalizeCursorWireName` | function | 368–370 | yes | 1 | `tool-naming.ts` | +| `CURSOR_TEXT_TOOL_MARKER` | const | 382–385 | no | 0 | `tool-naming.ts` | +| `normalizeCursorTextToolMarkers` | function | 387–390 | yes | 1 | `tool-naming.ts` | +| `responsesToolNameFromCursorWire` | function | 392–396 | yes | 1 | `tool-naming.ts` | +| `cursorToolInputSchema` | function | 399–401 | yes | 1 | `tool-schemas.ts` | +| `cursorToolArgNormalizeSchema` | function | 408–413 | yes | 2 | `tool-schemas.ts` | +| `shellBridgeArgNormalizeSchema` | function | 415–444 | no | 0 | `tool-schemas.ts` | +| `isGenericToolUseCountDemoPrompt` | function | 446–461 | yes | 2 | `tool-guidance.ts` | +| `requestedCursorToolUseCount` | function | 463–478 | yes | 1 | `tool-guidance.ts` | +| `cursorGenericToolUseHint` | function | 480–489 | no | 0 | `tool-guidance.ts` | +| `activeTextMentionsGenericToolUseHint` | function | 491–495 | no | 0 | `tool-guidance.ts` | +| `shouldAppendCursorGenericToolUseHint` | function | 497–506 | yes | 0 | `tool-guidance.ts` | +| `appendCursorGenericToolUseHint` | function | 508–514 | yes | 2 | `tool-guidance.ts` | +| `shouldUseNativeExecOnlyForGenericToolUse` | function | 516–524 | yes | 0 | `tool-guidance.ts` | +| `cursorToolsForActivePrompt` | function | 526–536 | yes | 5 | `tool-guidance.ts` | +| `shellBridgeRequiredCommandKeys` | function | 541–553 | yes | 0 | `tool-schemas.ts` | +| `defaultShellBridgeArgNormalizeSchema` | function | 556–564 | yes | 1 | `tool-schemas.ts` | +| `cursorShellBridgeDropError` | function | 566–568 | yes | 1 | `tool-schemas.ts` | +| `nonEmptyShellBridgeCommandFromArgs` | function | 574–597 | yes | 1 | `tool-schemas.ts` | +| `cursorShellBridgeArgsValid` | function | 599–606 | yes | 1 | `tool-schemas.ts` | +| `cursorToolAllowedByChoice` | function | 608–619 | yes | 1 | `tool-naming.ts` | +| `quotedNames` | function | 621–623 | no | 0 | `tool-guidance.ts` | +| `advertisedCoversNeighbor` | function | 625–629 | no | 0 | `tool-guidance.ts` | +| `unavailableNeighborAgentToolNames` | function | 631–633 | no | 0 | `tool-guidance.ts` | +| `discoveryToolLabel` | function | 635–641 | no | 0 | `tool-guidance.ts` | +| `buildCursorToolGuidanceSystemNote` | function | 643–736 | yes | 3 | `tool-guidance.ts` | +| `encodeCursorInputSchema` | function | 738–743 | yes | 1 | `tool-definitions.ts` (residual) | +| `buildCursorToolDefinitions` | function | 745–760 | yes | 4 | `tool-definitions.ts` (residual) | +| `cursorMcpToolsEncodedSize` | function | 763–769 | yes | 2 | `tool-definitions.ts` (residual) | +| `cursorMcpToolEncodedSize` | function | 772–777 | yes | 1 | `tool-definitions.ts` (residual) | + +Resolved direct importers: 13 distinct files (8 production, 5 tests). Production paths: + +- `src/adapters/cursor.ts` — unchanged. +- `src/adapters/cursor/live-transport.ts` — unchanged. +- `src/adapters/cursor/native-exec-mcp.ts` — unchanged. +- `src/adapters/cursor/native-exec-tools.ts` — unchanged. +- `src/adapters/cursor/native-exec.ts` — unchanged. +- `src/adapters/cursor/protobuf-events.ts` — unchanged. +- `src/adapters/cursor/protobuf-request.ts` — unchanged. +- `src/adapters/cursor/request-builder.ts` — unchanged. + +## Leaf partition + +All paths below are new sibling files under `src/adapters/cursor/`, following the existing kebab-case native-exec-* and protobuf-* convention. Each symbol body and attached comment moves without rewriting. Physical slice accounting includes blank lines/comments; keep slice contents in their original relative order. Expected sizes use the exact compact import/re-export lines shown; multiline formatting consumes spare budget and must be recounted, especially catalog.ts. + +### `src/adapters/cursor/tool-naming.ts` + +- Transfer source slices: 8–20, 118–268, 326–396, 608–619 (247 physical lines). +- Symbols: `OCX_RESPONSES_TOOL_PROVIDER`, `CODEX_EXEC_COMMAND_TOOL`, `CODEX_SHELL_COMMAND_TOOL`, `CODEX_UNIFIED_EXEC_TOOL`, `CODEX_WAIT_TOOL`, `CODEX_APPLY_PATCH_TOOL`, `CODEX_TOOL_SEARCH_TOOL`, `CURSOR_EDIT_FILE_TOOL`, `CURSOR_MULTI_EDIT_TOOL`, `CURSOR_STRUCTURED_EDIT_TOOLS`, `CURSOR_EXEC_COMMAND_TOOL`, `CODEX_SHELL_BRIDGE_TOOL_NAMES`, `isCodexShellBridgeToolName`, `resolveShellBridgeAliasKey`, `cursorToolChoiceAliases`, `catalogHasBareCodexShellBridge`, `cursorToolChoiceMatches`, `isBareCodexShellBridgeTool`, `isCursorResponsesProvider`, `CURSOR_EXECUTION_PATH_TOOL_NAMES`, `isCursorExecutionPathTool`, `isCursorWaitTool`, `isCursorCodeModeExecTool`, `cursorRequestUsesCodeMode`, `isBareCodexExecCommandTool`, `cursorRequestHasShellAlias`, `cursorRequestHasExecutionPath`, `cursorRequestAdvertisesApplyPatch`, `isCursorStructuredEditToolName`, `isCursorSyntheticStructuredEditTool`, `CURSOR_CLIENT_TOOL_WIRE_PREFIX`, `CURSOR_PROXY_OWNED_BARE_TOOL_NAMES`, `isCursorBareClientToolWireAliased`, `cursorToolWireName`, `clientSemanticToolNameFromCursorWire`, `CURSOR_MCP_DISPLAY_PREFIX`, `normalizeCursorWireName`, `CURSOR_TEXT_TOOL_MARKER`, `normalizeCursorTextToolMarkers`, `responsesToolNameFromCursorWire`, `cursorToolAllowedByChoice`. +- Expected line count: 247 moved + 1 import lines = **248**, ≤400. +- Own imports: + +```ts +import { namespacedToolName, toolChoiceAliases, type OcxRequestOptions, type OcxTool } from "../../types"; +``` + +### `src/adapters/cursor/tool-schemas.ts` + +- Transfer source slices: 44–117, 398–444, 538–606 (190 physical lines). +- Symbols: `CURSOR_EXEC_COMMAND_INPUT_SCHEMA`, `CURSOR_EDIT_FILE_INPUT_SCHEMA`, `CURSOR_MULTI_EDIT_INPUT_SCHEMA`, `CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA`, `cursorToolInputSchema`, `cursorToolArgNormalizeSchema`, `shellBridgeArgNormalizeSchema`, `shellBridgeRequiredCommandKeys`, `defaultShellBridgeArgNormalizeSchema`, `cursorShellBridgeDropError`, `nonEmptyShellBridgeCommandFromArgs`, `cursorShellBridgeArgsValid`. +- Expected line count: 190 moved + 2 import lines = **192**, ≤400. +- Own imports: + +```ts +import type { OcxTool } from "../../types"; +import { CODEX_SHELL_COMMAND_TOOL, isBareCodexExecCommandTool, isBareCodexShellBridgeTool, isCodexShellBridgeToolName } from "./tool-naming"; +``` + +### `src/adapters/cursor/tool-guidance.ts` + +- Transfer source slices: 21–43, 446–536, 621–736 (230 physical lines). +- Symbols: `CURSOR_SHELL_ALIAS_SYSTEM_NOTE`, `NEIGHBOR_AGENT_TOOL_NAMES`, `NEIGHBOR_AGENT_TOOL_ALIASES`, `CURSOR_GENERIC_TOOL_USE_USER_HINT`, `isGenericToolUseCountDemoPrompt`, `requestedCursorToolUseCount`, `cursorGenericToolUseHint`, `activeTextMentionsGenericToolUseHint`, `shouldAppendCursorGenericToolUseHint`, `appendCursorGenericToolUseHint`, `shouldUseNativeExecOnlyForGenericToolUse`, `cursorToolsForActivePrompt`, `quotedNames`, `advertisedCoversNeighbor`, `unavailableNeighborAgentToolNames`, `discoveryToolLabel`, `buildCursorToolGuidanceSystemNote`. +- Expected line count: 230 moved + 3 import lines = **233**, ≤400. +- Own imports: + +```ts +import type { OcxRequestOptions, OcxTool } from "../../types"; +import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize"; +import { CODEX_SHELL_BRIDGE_TOOL_NAMES, CODEX_TOOL_SEARCH_TOOL, CODEX_UNIFIED_EXEC_TOOL, clientSemanticToolNameFromCursorWire, cursorRequestAdvertisesApplyPatch, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolAllowedByChoice, cursorToolWireName, isCodexShellBridgeToolName, isCursorExecutionPathTool, isCursorStructuredEditToolName } from "./tool-naming"; +``` + +### Residual `src/adapters/cursor/tool-definitions.ts` + +Retain: `cursorStructuredEditTools`, `cursorRequestAdvertisesStructuredEdits`, `encodeCursorInputSchema`, `buildCursorToolDefinitions`, `cursorMcpToolsEncodedSize`, `cursorMcpToolEncodedSize`. + +Remove only original imports at lines 4 and 6 (their bindings now live in leaves); retain lines 1–3 and 5. Insert the two local imports and three re-export lines below. + +Accounting: 777 − 667 moved − 2 net removed import lines + 2 local import lines + 3 re-export lines = **113** expected lines. All leaves plus residual total 786 = 777 original + 9 net import/export glue lines. No >400 residual and no #a/#b/#c part in this approved map. A size-policy escalation is not a hidden #b commitment; if the parent adds parts, re-plan lower-consumer leaves first and publish each intermediate residual count. + +Add `export` to the existing private declarations `isBareCodexExecCommandTool` (237), `cursorRequestHasExecutionPath` (245), and `clientSemanticToolNameFromCursorWire` (353) inside tool-naming.ts so sibling production leaves can use them. Do not re-export these new internal seams from tool-definitions.ts. All other currently private declarations remain private. + +## Re-export block + +Insert into the original file exactly these named lines; current exported declarations that stay local remain exported in place (`cursorStructuredEditTools`, `cursorRequestAdvertisesStructuredEdits`, `encodeCursorInputSchema`, `buildCursorToolDefinitions`, `cursorMcpToolsEncodedSize`, `cursorMcpToolEncodedSize`). Do not use export-star and do not re-export newly exposed internal-only seams. + +```ts +export { OCX_RESPONSES_TOOL_PROVIDER, CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL, CODEX_UNIFIED_EXEC_TOOL, CODEX_WAIT_TOOL, CODEX_APPLY_PATCH_TOOL, CODEX_TOOL_SEARCH_TOOL, CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, CURSOR_STRUCTURED_EDIT_TOOLS, CURSOR_EXEC_COMMAND_TOOL, CODEX_SHELL_BRIDGE_TOOL_NAMES, isCodexShellBridgeToolName, resolveShellBridgeAliasKey, cursorToolChoiceAliases, isBareCodexShellBridgeTool, isCursorExecutionPathTool, isCursorWaitTool, isCursorCodeModeExecTool, cursorRequestUsesCodeMode, cursorRequestHasShellAlias, cursorRequestAdvertisesApplyPatch, isCursorStructuredEditToolName, isCursorSyntheticStructuredEditTool, cursorToolWireName, normalizeCursorWireName, normalizeCursorTextToolMarkers, responsesToolNameFromCursorWire, cursorToolAllowedByChoice } from "./tool-naming"; +export { CURSOR_EXEC_COMMAND_INPUT_SCHEMA, CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, cursorToolInputSchema, cursorToolArgNormalizeSchema, shellBridgeRequiredCommandKeys, defaultShellBridgeArgNormalizeSchema, cursorShellBridgeDropError, nonEmptyShellBridgeCommandFromArgs, cursorShellBridgeArgsValid } from "./tool-schemas"; +export { CURSOR_SHELL_ALIAS_SYSTEM_NOTE, CURSOR_GENERIC_TOOL_USE_USER_HINT, isGenericToolUseCountDemoPrompt, requestedCursorToolUseCount, shouldAppendCursorGenericToolUseHint, appendCursorGenericToolUseHint, shouldUseNativeExecOnlyForGenericToolUse, cursorToolsForActivePrompt, buildCursorToolGuidanceSystemNote } from "./tool-guidance"; +``` + +Re-export binds nothing locally. The original needs these explicit leaf imports in addition to its retained original imports: + +```ts +import { CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, cursorRequestAdvertisesApplyPatch, cursorToolAllowedByChoice, cursorToolWireName, OCX_RESPONSES_TOOL_PROVIDER } from "./tool-naming"; +import { CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, cursorToolInputSchema } from "./tool-schemas"; +``` + +## Module-level state and cycles + +`CURSOR_PROXY_OWNED_BARE_TOOL_NAMES` at 327–336 is the only top-level Set; tool-naming.ts owns the sole allocation. `CURSOR_TEXT_TOOL_MARKER` at 382–385 is a global RegExp with mutable lastIndex; keep one instance in tool-naming.ts with normalizeCursorTextToolMarkers (387–390), no cloned regex. All top-level constants are assigned exactly once in the inventory, including the execution-path array (187–191), schema objects (44–116), neighbor lookup object (24–30), and hint string (32–42). No top-level let, Map, WeakMap, timer, or lock. Function-local Sets remain inside their moved functions. Naming must not import schemas/guidance/original: cursorToolAllowedByChoice moves with cursorToolChoiceMatches and cursorToolWireName, preventing naming → original → naming. Schema imports the existing deprecated predicate rather than rewriting its call. Guidance takes only naming predicates and the existing echo sentence; it never imports the original. Sequential/functional coupling stays explicit; no shared cache service is introduced. + +prerequisite landed as layer 105 (003 TYPE-CYCLE-01) + +The leaf direction listed in Loop spec is the allowed DAG. Sibling leaves import their canonical owner directly, never this original facade. Preserve initialization order for cross-constant references. Verify both runtime and type-only edges; a typecheck alone does not prove acyclicity. Compare the resolved import graph at the parent and tip; zero new cycles and no path from any new leaf back to the original are required. Existing external-format/provenance checks remain at the same trust boundary; do not reinterpret validation while relocating it. + +## Tests + +Exact direct-test list from `rg -l 'adapters/cursor/tool-definitions' tests`, with specifier resolution to discard comments/other basenames: + +- `tests/providers/cursor/cursor-request-builder.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-structured-edit.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-tool-choice.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-tool-definitions.test.ts` — **unchanged** import path and assertions. +- `tests/responses/responses-tool-conformance.test.ts` — **unchanged** import path and assertions. + +No test reads tool-definitions.ts as source. The basename/full-path source-reader search returns no source-body guard for this file. Runtime importers below remain unchanged, including responses-tool-conformance.test.ts. No retarget-to-leaf or add-leaf-to-scan-list operation is needed. + +Transitive source-reader exception: `tests/lab/core-lab-boundary.test.ts:69` reads each resolved source file while walking static imports/re-exports. A read-only replay of that walk from `src/server/responses/core.ts` reaches this target (413 visited files at the basis). Disposition: **unchanged**; new leaves are automatically included through named imports/re-exports, so no manual add-leaf-to-scan-list and no retarget. Never edit its PROTECTED roots (lines 20–28). At implementation time drive this guard red once with a temporary forbidden leaf edge to `../../lab/paths`, then restore and prove green; no forbidden edge may enter a commit. + +In C phase only, drive `tests/providers/cursor/cursor-tool-definitions.test.ts:43` red by temporarily breaking the ordinary bare-name alias predicate in tool-naming.ts, and `:328` red by temporarily removing the pinned-choice escape in tool-guidance.ts. Restore immediately, then rerun the focused files; never commit mutants or weaken assertions. Also retain schema byte-equivalence tests at :81 and :106 and guidance/code-mode assertions at :360 and :499. + +No test file is added by this plan, hence no test-layout manifest change. If extra regression coverage proves necessary, extend the existing focused files first and report scope expansion instead of silently creating new tests. + +## Verification + +Instantiate 002's Per-layer gate in this layer's dedicated worktree, not in the docs worktree. Nothing in this code fence was run by the drafting delegate. + +```sh +bun run typecheck +# Focused domain: providers/cursor (includes the direct Cursor tests listed above) +bun test tests/providers/cursor +bun test tests/adapters/adapter-tool-conformance.test.ts +bun test tests/responses/responses-tool-conformance.test.ts +# Transitive source-graph guard; justified even though only adapters files move +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/adapters/cursor/tool-naming.ts src/adapters/cursor/tool-schemas.ts src/adapters/cursor/tool-guidance.ts src/adapters/cursor/tool-definitions.ts +rg -n 'from "[^"]*/tool-definitions"' src gui/src scripts tests | wc -l +rg -l 'adapters/cursor/tool-definitions' tests +# Full suite: remote only; preserve pipeline failure rather than trusting tail's exit status +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-cursor-tool-definitions && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused named subset (for initial tight red/green and for an exact task manifest): + +```sh +bun test tests/adapters/adapter-tool-conformance.test.ts tests/providers/cursor/cursor-request-builder.test.ts tests/providers/cursor/cursor-structured-edit.test.ts tests/providers/cursor/cursor-tool-choice.test.ts tests/providers/cursor/cursor-tool-definitions.test.ts tests/responses/responses-tool-conformance.test.ts +``` + +Use the named subset for the temporary mutation checks, then the domain gate after restoration; do not rerun an unchanged passing check solely for confidence. Full suite is **never local**. Remote parent workflow must bind FETCH_HEAD/full-suite output to this exact PR head SHA, preserve a complete remote log as well as its summary, and ensure the remote checkout is exclusively owned before checkout; do not operate on unrelated dirty remote work. + +Importer proof: compare the 13-file resolved importer set above at parent and tip. Existing external consumer paths stay unchanged. New leaf imports are planned internal edges, not lost callers; count them separately. The simple 002 line-count command is supporting evidence only: multiline and dynamic imports require the resolved-file check. Export-name/type identity must be checked independently. Run a resolved runtime+type import-cycle scan with available repository tooling or a read-only resolver; do not install a dependency just for this split. Review `git diff --numstat codex/split-cursor-desktop-executor-contract...HEAD` with move-aware comparison and separately record raw additions + deletions; apply the sizing escalation above, not an unrecorded exception. Require green exact-head CI rollup, not merely an empty required-check list. + +## Accept criteria + +1. Source basis and parent branch are recorded; every owned top-level declaration in this table has exactly one post-move owner, with identical body/signature and attached explanatory comments. +2. All current 55 exports remain importable from `src/adapters/cursor/tool-definitions.ts`, with the same value/reference/type identity; no new internal-only export leaks through that original path. Residual local calls are bound by explicit imports. +3. Every planned leaf is ≤400 lines and residual is ≤400 (expected 113); actual `wc -l` agrees or the exact formatting delta is recorded. No omitted #b debt. +4. Schema payloads and emitted guidance strings remain byte-identical; namespace aliases, tool_choice pins, code-mode detection, and synthetic-edit provenance remain unchanged. +5. All 13 existing resolved importers remain; direct test imports/assertions and transitive source-reader semantics are preserved. Planned red mutations fail the named guards once, are removed, and the restored focused/domain checks pass with 0 failures. +6. Single-owner state allocations, allowed DAG edges, and no new runtime/type cycles are mechanically verified. Lab PROTECTED roots and optional-subsystem activation remain untouched. +7. Typecheck and privacy scan exit 0; remote-only full suite exits 0 at the exact layer SHA; exact-head CI rollup is green. No local full suite, no merge, and no unrelated changes. +8. Parent-to-tip size obeys the agreed 500-line metric or the parent explicitly resolves the documented exception/topology escalation before implementation; this draft itself is not evidence of an approved exception. + +## PR + +Title: `refactor(adapters-cursor): separate tool naming schemas and guidance (split S04 L1/5)` + +Branch: `codex/split-adapters-cursor-tool-definitions`. Base: `codex/split-cursor-desktop-executor-contract`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); paste the stack map below into Summary. Review only this layer's parent-to-tip diff. Replace PR placeholders with actual numbers when opened; no PR is created by this draft. + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 0 (105) | #TBD-S04-L0 | `codex/split-cursor-desktop-executor-contract` | `dev` | desktop-executor-contract | +| 1 | #TBD-S04-L1 | `codex/split-adapters-cursor-tool-definitions` | `codex/split-cursor-desktop-executor-contract` | tool-definitions | +| 2 | #TBD-S04-L2 | `codex/split-adapters-cursor-catalog` | `codex/split-cursor-desktop-executor-contract` | catalog | +| 3 | #TBD-S04-L3 | `codex/split-adapters-cursor-images` | `codex/split-cursor-desktop-executor-contract` | images | +| 4 | #TBD-S04-L4 | `codex/split-adapters-cursor-request-builder` | `codex/split-adapters-cursor-images` | request-builder | +| 5 | #TBD-S04-L5 | `codex/split-adapters-cursor-protobuf-events` | `codex/split-adapters-cursor-tool-definitions` | protobuf-events | + +Current layer: **L1**. Parent: `codex/split-cursor-desktop-executor-contract` (#TBD-S04-L0). +Changes to parent `codex/split-cursor-desktop-executor-contract` require rebasing this layer and cascading only +through its actual dependency descendants, with exact-tip/base rechecks +(DEV-STACK-02); sibling layer numbering creates no dependency. Merge remains +parent-before-child and separately authorized, never part of this draft. + +## P stale-check (2026-09-05, wp110) + +Base branch `codex/split-cursor-desktop-executor-contract` = 97df51515 (PR #3557, CI green). `git diff` of tool-definitions.ts between that tip and origin/dev is empty (777 lines); slice anchors 8/20/21/43/44/117/118/268/326/396/398/444/446/536/538/606/608/619/621/736/738 confirmed by sed. Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1 on focused runs; CI hygiene requires a test change in the same PR. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-110.CXPieV/wt` (branch `codex/split-adapters-cursor-tool-definitions`, base `codex/split-cursor-desktop-executor-contract` 97df51515). Executor: gpt-6-astra high (Russell, 01a06f21-e324-73c3-8b50-b69ae5b5e3c2). +- Commits: 5091dd604 (move: tool-naming 252, tool-schemas 195, tool-guidance 236, tool-definitions residual 112) and 73672ffd2 (test: cursor-tool-definitions.test.ts +13 — seam identity via both paths for naming/schemas/guidance; tool-naming has no ./tool-* import). Diff vs base: 5 files, +701/−670. +- Local gate: typecheck 0; focused (5 files) 203 pass / 0 fail; core-lab-boundary 17/0; privacy passed; 13 original-path importers unchanged; naming imports no sibling leaf. +- Red-drives: (a) bare-name predicate → :44 fails, restored; (b) pinned-choice escape → :340 fails, restored; (c) lab import in tool-naming → core-lab-boundary:288 fails with chain core → adapter-resolve → registry → cursor → tool-definitions → tool-naming → lab/paths, restored 17/0. + +- Adversarial diff review (Singer, gpt-6-astra high, 01a06f25-a703-7883-b091-93c43f87958e): GO-WITH-FIXES (blockers=0): all slices preserved (10 blank separators inserted, whitespace only), residual exact except original blank line 7, 55 exports identical, 3 internal seams not re-exported, runtime + type-inclusive graph zero new cycles (one pre-existing types→request→provider→mcp-config type cycle, TYPE-CYCLE-01). Non-blocking nit — quote-sensitive root guard — fixed in fdddbd3e1 (regex `from\s+["']\.\/tool-`), test 29/0. +- lidge full suite at 73672ffd2: SUITE_EXIT=0, 18014 pass / 0 fail / 16 skip; rerun at fdddbd3e1 recorded below. + +- lidge full suite at fdddbd3e1: SUITE_EXIT=0, 18014 pass / 0 fail / 16 skip (/tmp/suite-split-110.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3570 (base codex/split-cursor-desktop-executor-contract, head fdddbd3e1). CI rollup at record time: OPEN draft=false base=codex/split-cursor-desktop-executor-contract fdddbd3e1 =1 =10 CANCELLED=1 SKIPPED=2 SUCCESS=11 diff --git a/devlog/_plan/260905_now_split_train/120_adapters_cursor_catalog.md b/devlog/_plan/260905_now_split_train/120_adapters_cursor_catalog.md new file mode 100644 index 0000000000..f425931afb --- /dev/null +++ b/devlog/_plan/260905_now_split_train/120_adapters_cursor_catalog.md @@ -0,0 +1,210 @@ +# S04 L2/5 — catalog + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Docs basis: `4cc219549`; source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. Every source line range below refers to `src/adapters/cursor/catalog.ts` at that source commit, not a future leaf. Read alongside 000_plan.md, 001_stale_check.md, 002_layer_map.md, and ../260905_modular_debt_ledger/014_lane_adapters_media.md (lane 014; relevant file subsection). Status: diff-level plan only; no code, Git mutation, test run, or orchestration performed by this delegate. + +## Loop spec + +- Archetype: **pure-move**. Work class C3 structural planning, docs-only delegated mode; the parent owns all loop/goal state. +- Goal: move the inventoried responsibilities into the named sibling leaves, each ≤400 lines, preserving the original public import path and leaving 398 expected lines in the original. +- Non-goals: no exported rename/removal, no behavior or signature change, no dependency/tooling installation, no new validation, no changes to generated protobufs, native-exec ownership, live transport scheduling, registry policy, or unrelated files. No production-module execution or test run in this drafting task. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated in Verification below. Planned commands are for the layer executor; they are not results from this draft. +- Stop: parent records an independently verified, exact-tip layer with all accepts met and exact-head CI rollup; no merge. Stop implementation immediately on a changed signature, string/wire delta, duplicated state, cycle, unaccounted source-reader, or unsupported layer-size claim. +- Escalation: source drift, required files outside this partition/test list, an actual behavior defect, or the sizing conflict below goes to the parent; do not repair it opportunistically. Unreleased security findings go only to approved scratch, never this public devlog. + +Implementation sizing escalation: the move body is 322 lines (≤500 if counted once), but ordinary additions + deletions is at least 644 before glue. 002 does not define a move-discount metric. Parent must settle that metric or approve a move-only exception/revise topology before claiming the ≤500 changeset gate. This draft does not waive it. + +Structural decision and pre-change map: Static capability data/types occupy lines 7–328 and need no runtime dependency. Parser, selection, and live observation code occupy 330–716. Rejected: separate parser with setters in the original would introduce a reverse dependency unless additional seams moved; unnecessary here. Chosen: catalog-data.ts owns the existing ordered table and four supporting public types, while catalog.ts retains all parsing and live evidence. Pattern matches native-exec-common.ts and native-exec-tools.ts sibling naming. Current consumers include providers/registry.ts:21 and cursor/discovery.ts:8; current boundary → claude-id (1–5). Intended graph: consumers → catalog → catalog-data and claude-id. Feature boundary preserved across provider/server/catalog callers; no consumer retarget. + +No-code alternatives: doing nothing leaves the requested size debt; deletion/configuration cannot preserve these existing behaviors while shortening their implementation; reuse means moving the current declarations, not inventing equivalent helpers. Owner search: `rg --files src/adapters/cursor`, `rg -n '' src gui/src scripts tests`, and the lane-014 seam audit. The named new siblings do not already exist. Existing stable imports are compatibility boundaries, not permission for new convenience barrels. + +## Symbol inventory + +AST evidence: `git show origin/dev:src/adapters/cursor/catalog.ts`; working-tree bytes compared equal; `ast-grep run --lang typescript --kind --json=compact src/adapters/cursor/catalog.ts` for lexical/variable/function/interface/type-alias/class declarations, filtered to top-level source starts. Ranges are inclusive, include an `export` modifier on the same line, and exclude preceding comments. 42 owned top-level declarations; imports are dependencies, not redeclared owned symbols. + +Consumer counting: `rg -l 'catalog' src gui/src scripts tests` narrows candidates; resolve static `from` and dynamic `import()` relative specifiers to this exact file; then `rg -l -w '' ` counts distinct referencing consumer files. Count excludes the defining file. Private declarations have 0 external bound consumers; their local references move with the partition. This is a file count, not call-site count; do not reuse 001's broad basename heuristic as symbol fan-in. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `CursorVariantKind` | type | 24–24 | yes | 0 | `catalog-data.ts` | +| `CursorThinkingOrder` | type | 26–26 | yes | 0 | `catalog-data.ts` | +| `CursorVariantSpec` | interface | 28–35 | yes | 0 | `catalog-data.ts` | +| `CursorCapability` | interface | 37–53 | yes | 0 | `catalog-data.ts` | +| `K` | const | 55–55 | no | 0 | `catalog-data.ts` | +| `CONTEXT_200K` | const | 56–56 | no | 0 | `catalog-data.ts` | +| `CONTEXT_256K` | const | 57–57 | no | 0 | `catalog-data.ts` | +| `CONTEXT_272K` | const | 58–58 | no | 0 | `catalog-data.ts` | +| `CONTEXT_500K` | const | 59–59 | no | 0 | `catalog-data.ts` | +| `CONTEXT_1M` | const | 60–60 | no | 0 | `catalog-data.ts` | +| `CONTEXT_GEMINI` | const | 62–62 | no | 0 | `catalog-data.ts` | +| `FULL` | const | 64–64 | no | 0 | `catalog-data.ts` | +| `T` | const | 65–65 | no | 0 | `catalog-data.ts` | +| `E` | const | 66–66 | no | 0 | `catalog-data.ts` | +| `CURSOR_CAPABILITIES` | const | 74–328 | yes | 3 | `catalog-data.ts` | +| `LEVEL_TOKENS` | const | 330–330 | no | 0 | `catalog.ts` (residual) | +| `ParsedCursorVariantId` | interface | 332–340 | yes | 0 | `catalog.ts` (residual) | +| `stripLevelSuffix` | function | 342–358 | no | 0 | `catalog.ts` (residual) | +| `REAL_1M_WIRE_IDS` | const | 371–371 | no | 0 | `catalog.ts` (residual) | +| `parseCursorVariantId` | function | 373–438 | yes | 2 | `catalog.ts` (residual) | +| `finishParse` | function | 440–443 | no | 0 | `catalog.ts` (residual) | +| `defaultKindFor` | function | 445–447 | no | 0 | `catalog.ts` (residual) | +| `upgradeToFast` | function | 458–465 | yes | 1 | `catalog.ts` (residual) | +| `cursorFastCapableBases` | function | 468–473 | yes | 3 | `catalog.ts` (residual) | +| `cursorFastIdFor` | function | 487–494 | yes | 4 | `catalog.ts` (residual) | +| `normalizeRequestedEffort` | function | 496–499 | no | 0 | `catalog.ts` (residual) | +| `codexEffortRank` | function | 501–516 | no | 0 | `catalog.ts` (residual) | +| `cursorVariantEffort` | function | 519–531 | yes | 0 | `catalog.ts` (residual) | +| `CursorResolvedSelection` | interface | 533–541 | yes | 0 | `catalog.ts` (residual) | +| `CursorLiveClaudeWireIdentity` | type | 543–543 | no | 0 | `catalog.ts` (residual) | +| `composeWireId` | function | 550–577 | no | 0 | `catalog.ts` (residual) | +| `resolveCursorSelection` | function | 587–621 | yes | 6 | `catalog.ts` (residual) | +| `liveCursorMaxModeBases` | let | 629–629 | no | 0 | `catalog.ts` (residual) | +| `liveCursorClaudeWireIdentities` | let | 630–630 | no | 0 | `catalog.ts` (residual) | +| `recordLiveCursorClaudeModels` | function | 632–640 | yes | 3 | `catalog.ts` (residual) | +| `liveCursorClaudeWireIdentitiesForTests` | function | 642–644 | yes | 1 | `catalog.ts` (residual) | +| `resetLiveCursorClaudeWireIdentitiesForTests` | function | 646–648 | yes | 2 | `catalog.ts` (residual) | +| `recordLiveCursorMaxModeModels` | function | 650–657 | yes | 2 | `catalog.ts` (residual) | +| `liveCursorMaxModeBasesForTests` | function | 659–661 | yes | 0 | `catalog.ts` (residual) | +| `CursorUmbrellaRow` | interface | 663–670 | yes | 0 | `catalog.ts` (residual) | +| `cursorGrokFastSelection` | function | 678–695 | yes | 1 | `catalog.ts` (residual) | +| `cursorUmbrellaRows` | function | 702–716 | yes | 4 | `catalog.ts` (residual) | + +Resolved direct importers: 14 distinct files (7 production, 7 tests). Production paths: + +- `src/adapters/cursor/discovery.ts` — unchanged. +- `src/adapters/cursor/request-builder.ts` — unchanged. +- `src/claude/model-info.ts` — unchanged. +- `src/codex/catalog/provider-fetch.ts` — unchanged. +- `src/providers/registry.ts` — unchanged. +- `src/server/index.ts` — unchanged. +- `src/server/management/agent-settings-routes.ts` — unchanged. + +## Leaf partition + +All paths below are new sibling files under `src/adapters/cursor/`, following the existing kebab-case native-exec-* and protobuf-* convention. Each symbol body and attached comment moves without rewriting. Physical slice accounting includes blank lines/comments; keep slice contents in their original relative order. Expected sizes use the exact compact import/re-export lines shown; multiline formatting consumes spare budget and must be recounted, especially catalog.ts. + +### `src/adapters/cursor/catalog-data.ts` + +- Transfer source slices: 7–328 (322 physical lines). +- Symbols: `CursorVariantKind`, `CursorThinkingOrder`, `CursorVariantSpec`, `CursorCapability`, `K`, `CONTEXT_200K`, `CONTEXT_256K`, `CONTEXT_272K`, `CONTEXT_500K`, `CONTEXT_1M`, `CONTEXT_GEMINI`, `FULL`, `T`, `E`, `CURSOR_CAPABILITIES`. +- Expected line count: 322 moved + 0 import lines = **322**, ≤400. +- Own imports: none; standard Bun/JavaScript globals are not module imports. + +### Residual `src/adapters/cursor/catalog.ts` + +Retain: `LEVEL_TOKENS`, `ParsedCursorVariantId`, `stripLevelSuffix`, `REAL_1M_WIRE_IDS`, `parseCursorVariantId`, `finishParse`, `defaultKindFor`, `upgradeToFast`, `cursorFastCapableBases`, `cursorFastIdFor`, `normalizeRequestedEffort`, `codexEffortRank`, `cursorVariantEffort`, `CursorResolvedSelection`, `CursorLiveClaudeWireIdentity`, `composeWireId`, `resolveCursorSelection`, `liveCursorMaxModeBases`, `liveCursorClaudeWireIdentities`, `recordLiveCursorClaudeModels`, `liveCursorClaudeWireIdentitiesForTests`, `resetLiveCursorClaudeWireIdentitiesForTests`, `recordLiveCursorMaxModeModels`, `liveCursorMaxModeBasesForTests`, `CursorUmbrellaRow`, `cursorGrokFastSelection`, `cursorUmbrellaRows`. + +Keep original claude-id import lines 1–5. Move the explanatory header with the data (7–328), then insert the two local imports and two re-export lines below. This yields 398 lines; do not pad the near-limit residual with extra blank lines. + +Accounting: 716 − 322 moved + 2 local import lines + 2 re-export lines = **398** expected lines. All leaves plus residual total 720 = 716 original + 4 net import/export glue lines. No >400 residual and no #a/#b/#c part in this approved map. A size-policy escalation is not a hidden #b commitment; if the parent adds parts, re-plan lower-consumer leaves first and publish each intermediate residual count. + +No private declaration becomes an inter-leaf API. Preserve even the currently unused CONTEXT_256K (57); deleting it is not part of the pure move. + +## Re-export block + +Insert into the original file exactly these named lines; current exported declarations that stay local remain exported in place (`ParsedCursorVariantId`, `parseCursorVariantId`, `upgradeToFast`, `cursorFastCapableBases`, `cursorFastIdFor`, `cursorVariantEffort`, `CursorResolvedSelection`, `resolveCursorSelection`, `recordLiveCursorClaudeModels`, `liveCursorClaudeWireIdentitiesForTests`, `resetLiveCursorClaudeWireIdentitiesForTests`, `recordLiveCursorMaxModeModels`, `liveCursorMaxModeBasesForTests`, `CursorUmbrellaRow`, `cursorGrokFastSelection`, `cursorUmbrellaRows`). Do not use export-star and do not re-export newly exposed internal-only seams. + +```ts +export { CURSOR_CAPABILITIES } from "./catalog-data"; +export type { CursorVariantKind, CursorThinkingOrder, CursorVariantSpec, CursorCapability } from "./catalog-data"; +``` + +Re-export binds nothing locally. The original needs these explicit leaf imports in addition to its retained original imports: + +```ts +import { CURSOR_CAPABILITIES } from "./catalog-data"; +import type { CursorVariantKind, CursorVariantSpec } from "./catalog-data"; +``` + +## Module-level state and cycles + +`REAL_1M_WIRE_IDS` at 371 stays solely in catalog.ts with parseCursorVariantId. Both top-level mutable bindings stay in catalog.ts: `liveCursorMaxModeBases` (629; ReadonlySet initialized with new Set) and `liveCursorClaudeWireIdentities` (630; ReadonlyMap initialized with new Map). Their setters/getters/reset (632–661) and resolver reads (609, 618) remain colocated. The exported CURSOR_CAPABILITIES object (74–328), FULL array (64), and context constants have a single catalog-data.ts allocation; retain insertion order and alias references. No timer/lock/WeakMap. The data leaf imports nothing, including no type import from catalog.ts; the four types move with it. This prevents even a type-only catalog-data ↔ catalog cycle. Existing temporal live-observation coupling is unchanged, not replaced by a second registry or snapshot. + +Read-only graph check of this planned layer's new imports found no return cycle involving `catalog-data.ts`. The stack still inherits the **L1 type-only-cycle prerequisite** documented in 110_adapters_cursor_tool_definitions.md: `src/types.ts:112 → src/types/provider.ts:701 → native-exec-desktop.ts:19 → native-exec-tools.ts:25 → tool-definitions.ts → src/types.ts`. Do not claim whole-stack type acyclicity until the parent resolves that out-of-scope prerequisite; these later leaves do not repair it. The local partition/line accounting here remains conditional on a valid L1 parent. + +The leaf direction listed in Loop spec is the allowed DAG. Sibling leaves import their canonical owner directly, never this original facade. Preserve initialization order for cross-constant references. Verify both runtime and type-only edges; a typecheck alone does not prove acyclicity. Compare the resolved import graph at the parent and tip; zero new cycles and no path from any new leaf back to the original are required. Existing external-format/provenance checks remain at the same trust boundary; do not reinterpret validation while relocating it. + +## Tests + +Exact direct-test list from `rg -l 'adapters/cursor/catalog' tests`, with specifier resolution to discard comments/other basenames: + +- `tests/providers/cursor/cursor-catalog.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-display-names.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-fast-listing.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-fast-tier.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-static-catalog.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-umbrella-rows.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-uncallable-quarantine.test.ts` — **unchanged** import path and assertions. + +No source-text oracle for catalog.ts was found. Tests that call the legacy effort-map an oracle are behavioral wire-id comparisons, not readFileSync/Bun.file source readers. All seven direct test importers below remain unchanged; no retarget or scan-list additions. + +Transitive source-reader exception: `tests/lab/core-lab-boundary.test.ts:69` reads each resolved source file while walking static imports/re-exports. A read-only replay of that walk from `src/server/responses/core.ts` reaches this target (413 visited files at the basis). Disposition: **unchanged**; new leaves are automatically included through named imports/re-exports, so no manual add-leaf-to-scan-list and no retarget. Never edit its PROTECTED roots (lines 20–28). At implementation time drive this guard red once with a temporary forbidden leaf edge to `../../lab/paths`, then restore and prove green; no forbidden edge may enter a commit. + +In C phase, drive `tests/providers/cursor/cursor-catalog.test.ts:189` red by temporarily changing the Opus 5 fast ladder in catalog-data.ts; drive `:214` red by temporarily changing kimi-k3 maxModeVerified. Restore the exact table, then run all listed tests. Preserve live-reset identity assertion at :121 and live-evidence assertion at :223. + +No test file is added by this plan, hence no test-layout manifest change. If extra regression coverage proves necessary, extend the existing focused files first and report scope expansion instead of silently creating new tests. + +## Verification + +Instantiate 002's Per-layer gate in this layer's dedicated worktree, not in the docs worktree. Nothing in this code fence was run by the drafting delegate. + +```sh +bun run typecheck +# Focused domain: providers/cursor (includes the direct Cursor tests listed above) +bun test tests/providers/cursor + +# Transitive source-graph guard; justified even though only adapters files move +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/adapters/cursor/catalog-data.ts src/adapters/cursor/catalog.ts +rg -n 'from "[^"]*/catalog"' src gui/src scripts tests | wc -l +rg -l 'adapters/cursor/catalog' tests +# Full suite: remote only; preserve pipeline failure rather than trusting tail's exit status +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-cursor-catalog && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused named subset (for initial tight red/green and for an exact task manifest): + +```sh +bun test tests/providers/cursor/cursor-catalog.test.ts tests/providers/cursor/cursor-display-names.test.ts tests/providers/cursor/cursor-fast-listing.test.ts tests/providers/cursor/cursor-fast-tier.test.ts tests/providers/cursor/cursor-request-builder.test.ts tests/providers/cursor/cursor-static-catalog.test.ts tests/providers/cursor/cursor-umbrella-rows.test.ts tests/providers/cursor/cursor-uncallable-quarantine.test.ts +``` + +Use the named subset for the temporary mutation checks, then the domain gate after restoration; do not rerun an unchanged passing check solely for confidence. Full suite is **never local**. Remote parent workflow must bind FETCH_HEAD/full-suite output to this exact PR head SHA, preserve a complete remote log as well as its summary, and ensure the remote checkout is exclusively owned before checkout; do not operate on unrelated dirty remote work. + +Importer proof: compare the 14-file resolved importer set above at parent and tip. Existing external consumer paths stay unchanged. New leaf imports are planned internal edges, not lost callers; count them separately. The simple 002 line-count command is supporting evidence only: multiline and dynamic imports require the resolved-file check. Export-name/type identity must be checked independently. Run a resolved runtime+type import-cycle scan with available repository tooling or a read-only resolver; do not install a dependency just for this split. Review `git diff --numstat codex/split-cursor-desktop-executor-contract...HEAD` with move-aware comparison and separately record raw additions + deletions; apply the sizing escalation above, not an unrecorded exception. Require green exact-head CI rollup, not merely an empty required-check list. + +## Accept criteria + +1. Source basis and parent branch are recorded; every owned top-level declaration in this table has exactly one post-move owner, with identical body/signature and attached explanatory comments. +2. All current 21 exports remain importable from `src/adapters/cursor/catalog.ts`, with the same value/reference/type identity; no new internal-only export leaks through that original path. Residual local calls are bound by explicit imports. +3. Every planned leaf is ≤400 lines and residual is ≤400 (expected 398); actual `wc -l` agrees or the exact formatting delta is recorded. No omitted #b debt. +4. Object key order, all effort arrays, default variants, quarantines, windows, wirePrefix values, and live-state singletons are identical; old aliases and Max Mode evidence semantics remain unchanged. +5. All 14 existing resolved importers remain; direct test imports/assertions and transitive source-reader semantics are preserved. Planned red mutations fail the named guards once, are removed, and the restored focused/domain checks pass with 0 failures. +6. Single-owner state allocations, allowed DAG edges, and no new runtime/type cycles are mechanically verified. Lab PROTECTED roots and optional-subsystem activation remain untouched. +7. Typecheck and privacy scan exit 0; remote-only full suite exits 0 at the exact layer SHA; exact-head CI rollup is green. No local full suite, no merge, and no unrelated changes. +8. Parent-to-tip size obeys the agreed 500-line metric or the parent explicitly resolves the documented exception/topology escalation before implementation; this draft itself is not evidence of an approved exception. + +## PR + +Title: `refactor(adapters-cursor): isolate static Cursor capability data (split S04 L2/5)` + +Branch: `codex/split-adapters-cursor-catalog`. Base: `codex/split-cursor-desktop-executor-contract`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); paste the stack map below into Summary. Review only this layer's parent-to-tip diff. Replace PR placeholders with actual numbers when opened; no PR is created by this draft. + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 0 (105) | #TBD-S04-L0 | `codex/split-cursor-desktop-executor-contract` | `dev` | desktop-executor-contract | +| 1 | #TBD-S04-L1 | `codex/split-adapters-cursor-tool-definitions` | `codex/split-cursor-desktop-executor-contract` | tool-definitions | +| 2 | #TBD-S04-L2 | `codex/split-adapters-cursor-catalog` | `codex/split-cursor-desktop-executor-contract` | catalog | +| 3 | #TBD-S04-L3 | `codex/split-adapters-cursor-images` | `codex/split-cursor-desktop-executor-contract` | images | +| 4 | #TBD-S04-L4 | `codex/split-adapters-cursor-request-builder` | `codex/split-adapters-cursor-images` | request-builder | +| 5 | #TBD-S04-L5 | `codex/split-adapters-cursor-protobuf-events` | `codex/split-adapters-cursor-tool-definitions` | protobuf-events | + +Current layer: **L2**. Parent: `codex/split-cursor-desktop-executor-contract` (#TBD-S04-L0). +Changes to parent `codex/split-cursor-desktop-executor-contract` require rebasing this layer and cascading only +through its actual dependency descendants, with exact-tip/base rechecks +(DEV-STACK-02); sibling layer numbering creates no dependency. Merge remains +parent-before-child and separately authorized, never part of this draft. diff --git a/devlog/_plan/260905_now_split_train/130_adapters_cursor_images.md b/devlog/_plan/260905_now_split_train/130_adapters_cursor_images.md new file mode 100644 index 0000000000..f835d341ed --- /dev/null +++ b/devlog/_plan/260905_now_split_train/130_adapters_cursor_images.md @@ -0,0 +1,216 @@ +# S04 L3/5 — images + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Docs basis: `4cc219549`; source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. Every source line range below refers to `src/adapters/cursor/images.ts` at that source commit, not a future leaf. Read alongside 000_plan.md, 001_stale_check.md, 002_layer_map.md, and ../260905_modular_debt_ledger/014_lane_adapters_media.md (lane 014; relevant file subsection). Status: diff-level plan only; no code, Git mutation, test run, or orchestration performed by this delegate. + +## Loop spec + +- Archetype: **pure-move**. Work class C3 structural planning, docs-only delegated mode; the parent owns all loop/goal state. +- Goal: move the inventoried responsibilities into the named sibling leaves, each ≤400 lines, preserving the original public import path and leaving 327 expected lines in the original. +- Non-goals: no exported rename/removal, no behavior or signature change, no dependency/tooling installation, no new validation, no changes to generated protobufs, native-exec ownership, live transport scheduling, registry policy, or unrelated files. No production-module execution or test run in this drafting task. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated in Verification below. Planned commands are for the layer executor; they are not results from this draft. +- Stop: parent records an independently verified, exact-tip layer with all accepts met and exact-head CI rollup; no merge. Stop implementation immediately on a changed signature, string/wire delta, duplicated state, cycle, unaccounted source-reader, or unsupported layer-size claim. +- Escalation: source drift, required files outside this partition/test list, an actual behavior defect, or the sizing conflict below goes to the parent; do not repair it opportunistically. Unreleased security findings go only to approved scratch, never this public devlog. + +Implementation sizing escalation: the move body is 383 lines (≤500 if counted once), but ordinary additions + deletions is at least 766 before glue. 002 does not define a move-discount metric. Parent must settle that metric or approve a move-only exception/revise topology before claiming the ≤500 changeset gate. This draft does not waive it. + +Structural decision and pre-change map: Byte sniffers (172–197, 431–501) need no dependency. Decode/preparation policy (17–60, 71–91, 97–169, 299–425) depends only on those sniffers and Bun globals. Conversation traversal and SelectedImage/blob construction remain original. Rejected: moving sniffers alone leaves 600 lines; moving all image logic into one new file simply relocates debt. Chosen: image-format.ts and image-preparation.ts siblings, matching native-exec-fs.ts / native-exec-network.ts. Current consumers protobuf-request.ts:21, request-builder.ts:36, live-transport.ts:16, types.ts:5 → images → native-exec/gen/types (1–15). New graph: original → preparation → format; original → format and native-exec as before. No image fetch, media policy, blob-owner, or conversation-lifetime redesign. + +No-code alternatives: doing nothing leaves the requested size debt; deletion/configuration cannot preserve these existing behaviors while shortening their implementation; reuse means moving the current declarations, not inventing equivalent helpers. Owner search: `rg --files src/adapters/cursor`, `rg -n '' src gui/src scripts tests`, and the lane-014 seam audit. The named new siblings do not already exist. Existing stable imports are compatibility boundaries, not permission for new convenience barrels. + +## Symbol inventory + +AST evidence: `git show origin/dev:src/adapters/cursor/images.ts`; working-tree bytes compared equal; `ast-grep run --lang typescript --kind --json=compact src/adapters/cursor/images.ts` for lexical/variable/function/interface/type-alias/class declarations, filtered to top-level source starts. Ranges are inclusive, include an `export` modifier on the same line, and exclude preceding comments. 43 owned top-level declarations; imports are dependencies, not redeclared owned symbols. + +Consumer counting: `rg -l 'images' src gui/src scripts tests` narrows candidates; resolve static `from` and dynamic `import()` relative specifiers to this exact file; then `rg -l -w '' ` counts distinct referencing consumer files. Count excludes the defining file. Private declarations have 0 external bound consumers; their local references move with the partition. This is a file count, not call-site count; do not reuse 001's broad basename heuristic as symbol fan-in. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `MAX_CURSOR_IMAGE_BYTES` | const | 18–18 | yes | 1 | `image-preparation.ts` | +| `MAX_CURSOR_IMAGE_DECODE_BYTES` | const | 24–24 | yes | 1 | `image-preparation.ts` | +| `CURSOR_VISION_SOFT_MAX_BYTES` | const | 30–30 | yes | 1 | `image-preparation.ts` | +| `CURSOR_VISION_SOFT_MAX_BYTES_HIGH` | const | 33–33 | yes | 1 | `image-preparation.ts` | +| `CURSOR_VISION_MAX_EDGE` | const | 36–36 | yes | 0 | `image-preparation.ts` | +| `MAX_CURSOR_IMAGE_DECODE_EDGE` | const | 42–42 | yes | 1 | `image-preparation.ts` | +| `MAX_CURSOR_IMAGE_PIXELS` | const | 45–45 | yes | 1 | `image-preparation.ts` | +| `CURSOR_VISION_JPEG_QUALITIES_DEFAULT` | const | 47–47 | no | 0 | `image-preparation.ts` | +| `CURSOR_VISION_JPEG_QUALITIES_HIGH` | const | 48–48 | no | 0 | `image-preparation.ts` | +| `CURSOR_VISION_SOFT_MIN_EDGE` | const | 50–50 | no | 0 | `image-preparation.ts` | +| `CURSOR_VISION_SOFT_SHRINK` | const | 51–51 | no | 0 | `image-preparation.ts` | +| `CURSOR_VISION_PASSTHROUGH_MIME` | const | 53–59 | no | 0 | `image-preparation.ts` | +| `MAX_CURSOR_IMAGES` | const | 62–62 | yes | 1 | `images.ts` (residual) | +| `CURSOR_VISION_IMAGE_OMITTED` | const | 65–66 | yes | 1 | `image-preparation.ts` | +| `CURSOR_VISION_IMAGE_HISTORY_MARKER` | const | 69–69 | yes | 2 | `images.ts` (residual) | +| `CursorImageError` | class | 71–79 | yes | 1 | `image-preparation.ts` | +| `ResolvedCursorImage` | interface | 81–87 | yes | 1 | `image-preparation.ts` | +| `PrepareCursorImageOutcome` | type | 89–91 | yes | 0 | `image-preparation.ts` | +| `isImagePart` | function | 93–95 | no | 0 | `images.ts` (residual) | +| `estimatedBase64DecodedBytes` | function | 97–99 | no | 0 | `image-preparation.ts` | +| `isHighDetail` | function | 101–104 | no | 0 | `image-preparation.ts` | +| `softMaxBytesForDetail` | function | 106–108 | no | 0 | `image-preparation.ts` | +| `jpegQualitiesForDetail` | function | 110–112 | no | 0 | `image-preparation.ts` | +| `decodeCursorImageDataUrl` | function | 114–161 | yes | 1 | `image-preparation.ts` | +| `throwIfImagePhaseAborted` | function | 163–169 | no | 0 | `image-preparation.ts` | +| `sniffCursorImageFormat` | function | 172–197 | yes | 1 | `image-format.ts` | +| `extractCursorImageUrls` | function | 200–202 | yes | 1 | `images.ts` (residual) | +| `CursorImagePartRef` | interface | 204–207 | yes | 0 | `images.ts` (residual) | +| `extractCursorImageParts` | function | 210–224 | yes | 0 | `images.ts` (residual) | +| `resolveCursorImages` | function | 231–269 | yes | 1 | `images.ts` (residual) | +| `resolveCursorImageParts` | function | 271–280 | yes | 0 | `images.ts` (residual) | +| `cursorImageAttachmentPath` | function | 283–290 | yes | 0 | `images.ts` (residual) | +| `prepareCursorImageForWire` | function | 299–425 | yes | 1 | `image-preparation.ts` | +| `sniffCursorImageDimensions` | function | 431–501 | yes | 1 | `image-format.ts` | +| `buildSelectedImages` | function | 509–532 | yes | 1 | `images.ts` (residual) | +| `buildSelectedContext` | function | 538–545 | yes | 1 | `images.ts` (residual) | +| `resolveActiveCursorImages` | function | 551–562 | yes | 2 | `images.ts` (residual) | +| `imageDataUrlFromPrepared` | function | 564–566 | no | 0 | `images.ts` (residual) | +| `prepareCursorImageDataUrl` | function | 572–612 | yes | 0 | `images.ts` (residual) | +| `prepareCursorContentParts` | function | 614–641 | no | 0 | `images.ts` (residual) | +| `cursorVisionPrepareStartIndex` | function | 647–655 | yes | 1 | `images.ts` (residual) | +| `PreparedCursorRawMessages` | interface | 663–666 | yes | 0 | `images.ts` (residual) | +| `prepareCursorRawMessages` | function | 668–704 | yes | 2 | `images.ts` (residual) | + +Resolved direct importers: 6 distinct files (4 production, 2 tests). Production paths: + +- `src/adapters/cursor/live-transport.ts` — unchanged. +- `src/adapters/cursor/protobuf-request.ts` — unchanged. +- `src/adapters/cursor/request-builder.ts` — unchanged. +- `src/adapters/cursor/types.ts` — unchanged. + +## Leaf partition + +All paths below are new sibling files under `src/adapters/cursor/`, following the existing kebab-case native-exec-* and protobuf-* convention. Each symbol body and attached comment moves without rewriting. Physical slice accounting includes blank lines/comments; keep slice contents in their original relative order. Expected sizes use the exact compact import/re-export lines shown; multiline formatting consumes spare budget and must be recounted, especially catalog.ts. + +### `src/adapters/cursor/image-format.ts` + +- Transfer source slices: 171–198, 427–502 (104 physical lines). +- Symbols: `sniffCursorImageFormat`, `sniffCursorImageDimensions`. +- Expected line count: 104 moved + 0 import lines = **104**, ≤400. +- Own imports: none; standard Bun/JavaScript globals are not module imports. + +### `src/adapters/cursor/image-preparation.ts` + +- Transfer source slices: 17–60, 64–67, 71–92, 97–170, 292–426 (279 physical lines). +- Symbols: `MAX_CURSOR_IMAGE_BYTES`, `MAX_CURSOR_IMAGE_DECODE_BYTES`, `CURSOR_VISION_SOFT_MAX_BYTES`, `CURSOR_VISION_SOFT_MAX_BYTES_HIGH`, `CURSOR_VISION_MAX_EDGE`, `MAX_CURSOR_IMAGE_DECODE_EDGE`, `MAX_CURSOR_IMAGE_PIXELS`, `CURSOR_VISION_JPEG_QUALITIES_DEFAULT`, `CURSOR_VISION_JPEG_QUALITIES_HIGH`, `CURSOR_VISION_SOFT_MIN_EDGE`, `CURSOR_VISION_SOFT_SHRINK`, `CURSOR_VISION_PASSTHROUGH_MIME`, `CURSOR_VISION_IMAGE_OMITTED`, `CursorImageError`, `ResolvedCursorImage`, `PrepareCursorImageOutcome`, `estimatedBase64DecodedBytes`, `isHighDetail`, `softMaxBytesForDetail`, `jpegQualitiesForDetail`, `decodeCursorImageDataUrl`, `throwIfImagePhaseAborted`, `prepareCursorImageForWire`. +- Expected line count: 279 moved + 1 import lines = **280**, ≤400. +- Own imports: + +```ts +import { sniffCursorImageFormat, sniffCursorImageDimensions } from "./image-format"; +``` + +### Residual `src/adapters/cursor/images.ts` + +Retain: `MAX_CURSOR_IMAGES`, `CURSOR_VISION_IMAGE_HISTORY_MARKER`, `isImagePart`, `extractCursorImageUrls`, `CursorImagePartRef`, `extractCursorImageParts`, `resolveCursorImages`, `resolveCursorImageParts`, `cursorImageAttachmentPath`, `buildSelectedImages`, `buildSelectedContext`, `resolveActiveCursorImages`, `imageDataUrlFromPrepared`, `prepareCursorImageDataUrl`, `prepareCursorContentParts`, `cursorVisionPrepareStartIndex`, `PreparedCursorRawMessages`, `prepareCursorRawMessages`. + +Retain original imports 1–15: UUID creation, protobuf selected-context construction, Ocx message types, and native-exec blob ownership are still used in the residual. Add three local imports and three re-export lines. + +Accounting: 704 − 383 moved + 3 local import lines + 3 re-export lines = **327** expected lines. All leaves plus residual total 711 = 704 original + 7 net import/export glue lines. No >400 residual and no #a/#b/#c part in this approved map. A size-policy escalation is not a hidden #b commitment; if the parent adds parts, re-plan lower-consumer leaves first and publish each intermediate residual count. + +Export existing throwIfImagePhaseAborted (163–169) only from image-preparation.ts for residual traversal callers. Do not add it to images.ts public exports. ResolvedCursorImage and PrepareCursorImageOutcome move with preparation, so it never imports its own types back from images.ts. + +## Re-export block + +Insert into the original file exactly these named lines; current exported declarations that stay local remain exported in place (`MAX_CURSOR_IMAGES`, `CURSOR_VISION_IMAGE_HISTORY_MARKER`, `extractCursorImageUrls`, `CursorImagePartRef`, `extractCursorImageParts`, `resolveCursorImages`, `resolveCursorImageParts`, `cursorImageAttachmentPath`, `buildSelectedImages`, `buildSelectedContext`, `resolveActiveCursorImages`, `prepareCursorImageDataUrl`, `cursorVisionPrepareStartIndex`, `PreparedCursorRawMessages`, `prepareCursorRawMessages`). Do not use export-star and do not re-export newly exposed internal-only seams. + +```ts +export { sniffCursorImageFormat, sniffCursorImageDimensions } from "./image-format"; +export { MAX_CURSOR_IMAGE_BYTES, MAX_CURSOR_IMAGE_DECODE_BYTES, CURSOR_VISION_SOFT_MAX_BYTES, CURSOR_VISION_SOFT_MAX_BYTES_HIGH, CURSOR_VISION_MAX_EDGE, MAX_CURSOR_IMAGE_DECODE_EDGE, MAX_CURSOR_IMAGE_PIXELS, CURSOR_VISION_IMAGE_OMITTED, CursorImageError, decodeCursorImageDataUrl, prepareCursorImageForWire } from "./image-preparation"; +export type { ResolvedCursorImage, PrepareCursorImageOutcome } from "./image-preparation"; +``` + +Re-export binds nothing locally. The original needs these explicit leaf imports in addition to its retained original imports: + +```ts +import { sniffCursorImageDimensions } from "./image-format"; +import { MAX_CURSOR_IMAGE_BYTES, CURSOR_VISION_IMAGE_OMITTED, CursorImageError, decodeCursorImageDataUrl, throwIfImagePhaseAborted, prepareCursorImageForWire } from "./image-preparation"; +import type { ResolvedCursorImage } from "./image-preparation"; +``` + +## Module-level state and cycles + +`CURSOR_VISION_PASSTHROUGH_MIME` at 53–59 has exactly one owner: image-preparation.ts. Both JPEG quality arrays (47–48), soft resize thresholds (50–51), and caps (18–45) move with it. MAX_CURSOR_IMAGES (62) and history marker (69) remain original; omission marker (65–66) moves to preparation and is imported by the original. No module-level mutable Map, WeakMap, let, lock, timer, or cached Bun.Image. Native-exec.ts remains the sole blob-state authority; do not move or duplicate storeCursorBlob. Existing types.ts:5 imports ResolvedCursorImage from images.ts and remains unchanged. Preparation does not import ./types, native-exec, or images, avoiding preparation → images → preparation and types → images → types cycles. Format has zero imports. Boundary validation, abort timing, and error identity are preserved. + +Read-only graph check of this planned layer's new imports found no return cycle involving `image-format.ts`, `image-preparation.ts`. The stack still inherits the **L1 type-only-cycle prerequisite** documented in 110_adapters_cursor_tool_definitions.md: `src/types.ts:112 → src/types/provider.ts:701 → native-exec-desktop.ts:19 → native-exec-tools.ts:25 → tool-definitions.ts → src/types.ts`. Do not claim whole-stack type acyclicity until the parent resolves that out-of-scope prerequisite; these later leaves do not repair it. The local partition/line accounting here remains conditional on a valid L1 parent. + +The leaf direction listed in Loop spec is the allowed DAG. Sibling leaves import their canonical owner directly, never this original facade. Preserve initialization order for cross-constant references. Verify both runtime and type-only edges; a typecheck alone does not prove acyclicity. Compare the resolved import graph at the parent and tip; zero new cycles and no path from any new leaf back to the original are required. Existing external-format/provenance checks remain at the same trust boundary; do not reinterpret validation while relocating it. + +## Tests + +Exact direct-test list from `rg -l 'adapters/cursor/images' tests`, with specifier resolution to discard comments/other basenames: + +- `tests/providers/cursor/cursor-images.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-vision-wire-harness.test.ts` — **unchanged** import path and assertions. + +`rg -l 'readFileSync|Bun\\.file|source\\(' tests | xargs rg -n 'images'` produces a false-positive candidate `tests/providers/cursor/cursor-images.test.ts`: its actual file read is :47 (`Bun.file(pngPath).arrayBuffer()`), and :46 sets pngPath to `../../helpers/cursor-grumpy-fixture.png`. This reads a binary fixture, not images.ts. Disposition: unchanged, including fixture path. `tests/lib/credential-redirect-guard.test.ts:61` targets src/server/images.ts, a different file; unchanged. No test reads the target source file and no retarget-to-leaf/add-leaf-to-scan-list is warranted. This corrects 001's heuristic count of one without editing 001. + +Transitive source-reader exception: `tests/lab/core-lab-boundary.test.ts:69` reads each resolved source file while walking static imports/re-exports. A read-only replay of that walk from `src/server/responses/core.ts` reaches this target (413 visited files at the basis). Disposition: **unchanged**; new leaves are automatically included through named imports/re-exports, so no manual add-leaf-to-scan-list and no retarget. Never edit its PROTECTED roots (lines 20–28). At implementation time drive this guard red once with a temporary forbidden leaf edge to `../../lab/paths`, then restore and prove green; no forbidden edge may enter a commit. + +In C phase, drive `tests/providers/cursor/cursor-images.test.ts:68` red by temporarily disabling the inbound decoded-byte guard in image-preparation.ts; use :498 and :559 to drive dimension rejection red with a temporary sniff/limit mutation. Restore before green. Preserve original :47 fixture read, soft-cap/prep-before-cap :78, and historical/raw-message identity assertions. Do not weaken bomb, MIME, abort, or size assertions. + +No test file is added by this plan, hence no test-layout manifest change. If extra regression coverage proves necessary, extend the existing focused files first and report scope expansion instead of silently creating new tests. + +## Verification + +Instantiate 002's Per-layer gate in this layer's dedicated worktree, not in the docs worktree. Nothing in this code fence was run by the drafting delegate. + +```sh +bun run typecheck +# Focused domain: providers/cursor (includes the direct Cursor tests listed above) +bun test tests/providers/cursor +bun test tests/adapters/adapter-tool-conformance.test.ts +# Transitive source-graph guard; justified even though only adapters files move +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/adapters/cursor/image-format.ts src/adapters/cursor/image-preparation.ts src/adapters/cursor/images.ts +rg -n 'from "[^"]*/images"' src gui/src scripts tests | wc -l +rg -l 'adapters/cursor/images' tests +# Full suite: remote only; preserve pipeline failure rather than trusting tail's exit status +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-cursor-images && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused named subset (for initial tight red/green and for an exact task manifest): + +```sh +bun test tests/adapters/adapter-tool-conformance.test.ts tests/providers/cursor/cursor-images.test.ts tests/providers/cursor/cursor-request-builder.test.ts tests/providers/cursor/cursor-vision-wire-harness.test.ts +``` + +Use the named subset for the temporary mutation checks, then the domain gate after restoration; do not rerun an unchanged passing check solely for confidence. Full suite is **never local**. Remote parent workflow must bind FETCH_HEAD/full-suite output to this exact PR head SHA, preserve a complete remote log as well as its summary, and ensure the remote checkout is exclusively owned before checkout; do not operate on unrelated dirty remote work. + +Importer proof: compare the 6-file resolved importer set above at parent and tip. Existing external consumer paths stay unchanged. New leaf imports are planned internal edges, not lost callers; count them separately. The simple 002 line-count command is supporting evidence only: multiline and dynamic imports require the resolved-file check. Export-name/type identity must be checked independently. Run a resolved runtime+type import-cycle scan with available repository tooling or a read-only resolver; do not install a dependency just for this split. Review `git diff --numstat codex/split-cursor-desktop-executor-contract...HEAD` with move-aware comparison and separately record raw additions + deletions; apply the sizing escalation above, not an unrecorded exception. Require green exact-head CI rollup, not merely an empty required-check list. + +## Accept criteria + +1. Source basis and parent branch are recorded; every owned top-level declaration in this table has exactly one post-move owner, with identical body/signature and attached explanatory comments. +2. All current 30 exports remain importable from `src/adapters/cursor/images.ts`, with the same value/reference/type identity; no new internal-only export leaks through that original path. Residual local calls are bound by explicit imports. +3. Every planned leaf is ≤400 lines and residual is ≤400 (expected 327); actual `wc -l` agrees or the exact formatting delta is recorded. No omitted #b debt. +4. Same CursorImageError constructor identity, MIME allowlist, byte/pixel caps, output JPEG quality ladder, abort propagation, prepared-image object identity, history-window selection, and request-scoped blob writes. +5. All 6 existing resolved importers remain; direct test imports/assertions and transitive source-reader semantics are preserved. Planned red mutations fail the named guards once, are removed, and the restored focused/domain checks pass with 0 failures. +6. Single-owner state allocations, allowed DAG edges, and no new runtime/type cycles are mechanically verified. Lab PROTECTED roots and optional-subsystem activation remain untouched. +7. Typecheck and privacy scan exit 0; remote-only full suite exits 0 at the exact layer SHA; exact-head CI rollup is green. No local full suite, no merge, and no unrelated changes. +8. Parent-to-tip size obeys the agreed 500-line metric or the parent explicitly resolves the documented exception/topology escalation before implementation; this draft itself is not evidence of an approved exception. + +## PR + +Title: `refactor(adapters-cursor): separate image byte inspection and preparation (split S04 L3/5)` + +Branch: `codex/split-adapters-cursor-images`. Base: `codex/split-cursor-desktop-executor-contract`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); paste the stack map below into Summary. Review only this layer's parent-to-tip diff. Replace PR placeholders with actual numbers when opened; no PR is created by this draft. + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 0 (105) | #TBD-S04-L0 | `codex/split-cursor-desktop-executor-contract` | `dev` | desktop-executor-contract | +| 1 | #TBD-S04-L1 | `codex/split-adapters-cursor-tool-definitions` | `codex/split-cursor-desktop-executor-contract` | tool-definitions | +| 2 | #TBD-S04-L2 | `codex/split-adapters-cursor-catalog` | `codex/split-cursor-desktop-executor-contract` | catalog | +| 3 | #TBD-S04-L3 | `codex/split-adapters-cursor-images` | `codex/split-cursor-desktop-executor-contract` | images | +| 4 | #TBD-S04-L4 | `codex/split-adapters-cursor-request-builder` | `codex/split-adapters-cursor-images` | request-builder | +| 5 | #TBD-S04-L5 | `codex/split-adapters-cursor-protobuf-events` | `codex/split-adapters-cursor-tool-definitions` | protobuf-events | + +Current layer: **L3**. Parent: `codex/split-cursor-desktop-executor-contract` (#TBD-S04-L0). +Changes to parent `codex/split-cursor-desktop-executor-contract` require rebasing this layer and cascading only +through its actual dependency descendants, with exact-tip/base rechecks +(DEV-STACK-02); sibling layer numbering creates no dependency. Merge remains +parent-before-child and separately authorized, never part of this draft. diff --git a/devlog/_plan/260905_now_split_train/140_adapters_cursor_request_builder.md b/devlog/_plan/260905_now_split_train/140_adapters_cursor_request_builder.md new file mode 100644 index 0000000000..13caa3ec61 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/140_adapters_cursor_request_builder.md @@ -0,0 +1,202 @@ +# S04 L4/5 — request-builder + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Docs basis: `4cc219549`; source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. Every source line range below refers to `src/adapters/cursor/request-builder.ts` at that source commit, not a future leaf. Read alongside 000_plan.md, 001_stale_check.md, 002_layer_map.md, and ../260905_modular_debt_ledger/014_lane_adapters_media.md (lane 014; relevant file subsection). Status: diff-level plan only; no code, Git mutation, test run, or orchestration performed by this delegate. + +## Loop spec + +- Archetype: **pure-move**. Work class C3 structural planning, docs-only delegated mode; the parent owns all loop/goal state. +- Goal: move the inventoried responsibilities into the named sibling leaves, each ≤400 lines, preserving the original public import path and leaving 363 expected lines in the original. +- Non-goals: no exported rename/removal, no behavior or signature change, no dependency/tooling installation, no new validation, no changes to generated protobufs, native-exec ownership, live transport scheduling, registry policy, or unrelated files. No production-module execution or test run in this drafting task. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated in Verification below. Planned commands are for the layer executor; they are not results from this draft. +- Stop: parent records an independently verified, exact-tip layer with all accepts met and exact-head CI rollup; no merge. Stop implementation immediately on a changed signature, string/wire delta, duplicated state, cycle, unaccounted source-reader, or unsupported layer-size claim. +- Escalation: source drift, required files outside this partition/test list, an actual behavior defect, or the sizing conflict below goes to the parent; do not repair it opportunistically. Unreleased security findings go only to approved scratch, never this public devlog. + +Sizing: 145 moved lines, at least 290 additions + deletions before glue; the planned extraction fits the 500-line layer budget. Confirm actual parent-to-tip numstat at implementation time. + +Structural decision and pre-change map: Budget selection and catalog-limit wording (38–182) only need tool metadata, choice policy, and exact protobuf byte sizing. Request assembly (476–518), checkpoint lookup (401–474), identity and digest logic (325–399), and model selection remain original. Rejected: moving conversation/checkpoint code introduces unnecessary lifetime coupling. Chosen: tool-budget.ts sibling in the existing cursor flat layout. Current callers cursor.ts and live-transport.ts:17 → request-builder → catalog/tool-definitions/images/discovery/checkpoint-store/thread-continuity (1–36). New edge original → tool-budget → tool-definitions; no reverse edge or new mutable state. This is one local functional/sequential extraction; no provider contract changes. + +No-code alternatives: doing nothing leaves the requested size debt; deletion/configuration cannot preserve these existing behaviors while shortening their implementation; reuse means moving the current declarations, not inventing equivalent helpers. Owner search: `rg --files src/adapters/cursor`, `rg -n '' src gui/src scripts tests`, and the lane-014 seam audit. The named new siblings do not already exist. Existing stable imports are compatibility boundaries, not permission for new convenience barrels. + +## Symbol inventory + +AST evidence: `git show origin/dev:src/adapters/cursor/request-builder.ts`; working-tree bytes compared equal; `ast-grep run --lang typescript --kind --json=compact src/adapters/cursor/request-builder.ts` for lexical/variable/function/interface/type-alias/class declarations, filtered to top-level source starts. Ranges are inclusive, include an `export` modifier on the same line, and exclude preceding comments. 28 owned top-level declarations; imports are dependencies, not redeclared owned symbols. + +Consumer counting: `rg -l 'request-builder' src gui/src scripts tests` narrows candidates; resolve static `from` and dynamic `import()` relative specifiers to this exact file; then `rg -l -w '' ` counts distinct referencing consumer files. Count excludes the defining file. Private declarations have 0 external bound consumers; their local references move with the partition. This is a file count, not call-site count; do not reuse 001's broad basename heuristic as symbol fan-in. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `CURSOR_TOOL_COUNT_LIMIT` | const | 39–39 | yes | 1 | `tool-budget.ts` | +| `CURSOR_TOOL_BYTES_LIMIT` | const | 40–40 | yes | 1 | `tool-budget.ts` | +| `CursorToolBudgetResult` | interface | 42–45 | no | 0 | `tool-budget.ts` | +| `explicitlySelectedNames` | function | 47–50 | no | 0 | `tool-budget.ts` | +| `toolPriority` | function | 52–68 | no | 0 | `tool-budget.ts` | +| `isPinnedCursorTool` | function | 70–72 | no | 0 | `tool-budget.ts` | +| `applyCursorToolBudget` | function | 79–170 | yes | 2 | `tool-budget.ts` | +| `catalogLimitNote` | function | 172–181 | no | 0 | `tool-budget.ts` | +| `cursorFastRequested` | function | 191–193 | yes | 0 | `request-builder.ts` (residual) | +| `cursorRequestEmitsFastVariant` | function | 203–208 | yes | 2 | `request-builder.ts` (residual) | +| `normalizeCursorModelId` | function | 216–248 | no | 0 | `request-builder.ts` (residual) | +| `contentPartToText` | function | 250–266 | no | 0 | `request-builder.ts` (residual) | +| `toolResultToText` | function | 268–277 | no | 0 | `request-builder.ts` (residual) | +| `contentToText` | function | 279–285 | no | 0 | `request-builder.ts` (residual) | +| `requestMessage` | function | 287–310 | no | 0 | `request-builder.ts` (residual) | +| `cursorRequestMessagesFromRaw` | function | 316–323 | yes | 2 | `request-builder.ts` (residual) | +| `generatedCursorConversationId` | function | 325–327 | yes | 0 | `request-builder.ts` (residual) | +| `cursorConversationIdFromClientThread` | function | 330–339 | yes | 0 | `request-builder.ts` (residual) | +| `resolveCursorConversationId` | function | 347–362 | yes | 0 | `request-builder.ts` (residual) | +| `cursorClientThreadOwner` | function | 364–366 | yes | 1 | `request-builder.ts` (residual) | +| `updateFramed` | function | 368–374 | no | 0 | `request-builder.ts` (residual) | +| `cursorInstructionDigest` | function | 376–384 | yes | 2 | `request-builder.ts` (residual) | +| `cursorCoveredPrefixDigest` | function | 386–394 | yes | 2 | `request-builder.ts` (residual) | +| `CreateCursorRequestOptions` | interface | 396–399 | yes | 0 | `request-builder.ts` (residual) | +| `lookupPrefixSnapshot` | function | 401–420 | no | 0 | `request-builder.ts` (residual) | +| `lineageMismatch` | function | 422–436 | no | 0 | `request-builder.ts` (residual) | +| `resolveCursorCheckpoint` | function | 438–474 | no | 0 | `request-builder.ts` (residual) | +| `createCursorRequest` | function | 476–518 | yes | 10 | `request-builder.ts` (residual) | + +Resolved direct importers: 13 distinct files (2 production, 10 tests, 1 test helper). Production paths: + +- `src/adapters/cursor.ts` — unchanged. +- `src/adapters/cursor/live-transport.ts` — unchanged. + +## Leaf partition + +All paths below are new sibling files under `src/adapters/cursor/`, following the existing kebab-case native-exec-* and protobuf-* convention. Each symbol body and attached comment moves without rewriting. Physical slice accounting includes blank lines/comments; keep slice contents in their original relative order. Expected sizes use the exact compact import/re-export lines shown; multiline formatting consumes spare budget and must be recounted, especially catalog.ts. + +### `src/adapters/cursor/tool-budget.ts` + +- Transfer source slices: 38–182 (145 physical lines). +- Symbols: `CURSOR_TOOL_COUNT_LIMIT`, `CURSOR_TOOL_BYTES_LIMIT`, `CursorToolBudgetResult`, `explicitlySelectedNames`, `toolPriority`, `isPinnedCursorTool`, `applyCursorToolBudget`, `catalogLimitNote`. +- Expected line count: 145 moved + 2 import lines = **147**, ≤400. +- Own imports: + +```ts +import { isAllowedToolChoice, type OcxTool, type OcxToolChoice } from "../../types"; +import { cursorMcpToolEncodedSize, cursorMcpToolsEncodedSize, cursorToolAllowedByChoice, cursorToolChoiceAliases, cursorStructuredEditTools, cursorToolWireName, isCursorStructuredEditToolName, isBareCodexShellBridgeTool, isCursorExecutionPathTool, isCursorWaitTool } from "./tool-definitions"; +``` + +### Residual `src/adapters/cursor/request-builder.ts` + +Retain: `cursorFastRequested`, `cursorRequestEmitsFastVariant`, `normalizeCursorModelId`, `contentPartToText`, `toolResultToText`, `contentToText`, `requestMessage`, `cursorRequestMessagesFromRaw`, `generatedCursorConversationId`, `cursorConversationIdFromClientThread`, `resolveCursorConversationId`, `cursorClientThreadOwner`, `updateFramed`, `cursorInstructionDigest`, `cursorCoveredPrefixDigest`, `CreateCursorRequestOptions`, `lookupPrefixSnapshot`, `lineageMismatch`, `resolveCursorCheckpoint`, `createCursorRequest`. + +Replace original tool-definitions import block 16–28 (13 lines) with `import { cursorToolsForActivePrompt } from "./tool-definitions";` (1 line). Narrow line 10 to `import { namespacedToolName, toolChoiceAliases } from "../../types";` (same one line); do not opportunistically delete pre-existing unused toolChoiceAliases or OcxToolCall. Add one local import and one re-export below. Other imports stay. + +Accounting: 518 − 145 moved − 12 net removed import lines + 1 local import lines + 1 re-export lines = **363** expected lines. All leaves plus residual total 510 = 518 original − 8 net import/export glue lines. No >400 residual and no #a/#b/#c part in this approved map. A size-policy escalation is not a hidden #b commitment; if the parent adds parts, re-plan lower-consumer leaves first and publish each intermediate residual count. + +Export the existing private catalogLimitNote (172–181) from tool-budget.ts for createCursorRequest; do not re-export it from request-builder.ts. CursorToolBudgetResult remains private to the leaf; inference preserves the applyCursorToolBudget signature. + +## Re-export block + +Insert into the original file exactly these named lines; current exported declarations that stay local remain exported in place (`cursorFastRequested`, `cursorRequestEmitsFastVariant`, `cursorRequestMessagesFromRaw`, `generatedCursorConversationId`, `cursorConversationIdFromClientThread`, `resolveCursorConversationId`, `cursorClientThreadOwner`, `cursorInstructionDigest`, `cursorCoveredPrefixDigest`, `CreateCursorRequestOptions`, `createCursorRequest`). Do not use export-star and do not re-export newly exposed internal-only seams. + +```ts +export { CURSOR_TOOL_COUNT_LIMIT, CURSOR_TOOL_BYTES_LIMIT, applyCursorToolBudget } from "./tool-budget"; +``` + +Re-export binds nothing locally. The original needs these explicit leaf imports in addition to its retained original imports: + +```ts +import { applyCursorToolBudget, catalogLimitNote } from "./tool-budget"; +``` + +## Module-level state and cycles + +No top-level let, Map, Set, WeakMap, lock, timer, or cache in this source. CURSOR_TOOL_COUNT_LIMIT (39) and CURSOR_TOOL_BYTES_LIMIT (40) move once to tool-budget.ts. selectedNames, keptSet (99), candidate arrays, and byte counters remain per invocation within the moved function; never hoist them. Thread/checkpoint stores keep their existing owners and imports in the residual. tool-budget.ts imports only ../../types and ./tool-definitions; it must not import request-builder, discovery, checkpoint-store, or images. The exported limit constants are re-exported, not recreated. Existing budget→serialization functional coupling is retained. + +Read-only graph check of this planned layer's new imports found no return cycle involving `tool-budget.ts`. The stack still inherits the **L1 type-only-cycle prerequisite** documented in 110_adapters_cursor_tool_definitions.md: `src/types.ts:112 → src/types/provider.ts:701 → native-exec-desktop.ts:19 → native-exec-tools.ts:25 → tool-definitions.ts → src/types.ts`. Do not claim whole-stack type acyclicity until the parent resolves that out-of-scope prerequisite; these later leaves do not repair it. The local partition/line accounting here remains conditional on a valid L1 parent. + +The leaf direction listed in Loop spec is the allowed DAG. Sibling leaves import their canonical owner directly, never this original facade. Preserve initialization order for cross-constant references. Verify both runtime and type-only edges; a typecheck alone does not prove acyclicity. Compare the resolved import graph at the parent and tip; zero new cycles and no path from any new leaf back to the original are required. Existing external-format/provenance checks remain at the same trust boundary; do not reinterpret validation while relocating it. + +## Tests + +Exact direct-test list from `rg -l 'adapters/cursor/request-builder' tests`, with specifier resolution to discard comments/other basenames: + +- `tests/providers/cursor/cursor-default-catalog-suppression.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-effort-suffix.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-fast-tier.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-images.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-request-builder.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-structured-edit.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-tool-choice.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-ultra-mode.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-umbrella-rows.test.ts` — **unchanged** import path and assertions. +- `tests/responses/responses-state.test.ts` — **unchanged** import path and assertions. + +Direct test helper (not itself a runnable test): + +- `tests/helpers/adapter-conformance/wire-drivers.ts` — **unchanged**; exercised by `tests/adapters/adapter-tool-conformance.test.ts:15`. + +No source-text oracle reads request-builder.ts. responses-state.test.ts imports it at :22 but its readFileSync calls read persisted state/test files, not this source; unchanged. All direct tests and the wire-drivers.ts helper below keep the original path. No retarget or scan-list changes. + +Transitive source-reader exception: `tests/lab/core-lab-boundary.test.ts:69` reads each resolved source file while walking static imports/re-exports. A read-only replay of that walk from `src/server/responses/core.ts` reaches this target (413 visited files at the basis). Disposition: **unchanged**; new leaves are automatically included through named imports/re-exports, so no manual add-leaf-to-scan-list and no retarget. Never edit its PROTECTED roots (lines 20–28). At implementation time drive this guard red once with a temporary forbidden leaf edge to `../../lab/paths`, then restore and prove green; no forbidden edge may enter a commit. + +In C phase, drive `tests/providers/cursor/cursor-request-builder.test.ts:524` red by temporarily replacing actual byte measurement in tool-budget.ts with a wrong value; drive :693 red by temporarily lowering execution-path priority. Restore exact implementation before green. Keep :596/:616/:641/:675 priority cases and tests/providers/cursor/cursor-structured-edit.test.ts:122/:131. No mutants now. + +No test file is added by this plan, hence no test-layout manifest change. If extra regression coverage proves necessary, extend the existing focused files first and report scope expansion instead of silently creating new tests. + +## Verification + +Instantiate 002's Per-layer gate in this layer's dedicated worktree, not in the docs worktree. Nothing in this code fence was run by the drafting delegate. + +```sh +bun run typecheck +# Focused domain: providers/cursor (includes the direct Cursor tests listed above) +bun test tests/providers/cursor +bun test tests/adapters/adapter-tool-conformance.test.ts +bun test tests/responses/responses-state.test.ts +# Transitive source-graph guard; justified even though only adapters files move +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/adapters/cursor/tool-budget.ts src/adapters/cursor/request-builder.ts +rg -n 'from "[^"]*/request-builder"' src gui/src scripts tests | wc -l +rg -l 'adapters/cursor/request-builder' tests +# Full suite: remote only; preserve pipeline failure rather than trusting tail's exit status +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-cursor-request-builder && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused named subset (for initial tight red/green and for an exact task manifest): + +```sh +bun test tests/adapters/adapter-tool-conformance.test.ts tests/providers/cursor/cursor-default-catalog-suppression.test.ts tests/providers/cursor/cursor-effort-suffix.test.ts tests/providers/cursor/cursor-fast-tier.test.ts tests/providers/cursor/cursor-images.test.ts tests/providers/cursor/cursor-request-builder.test.ts tests/providers/cursor/cursor-structured-edit.test.ts tests/providers/cursor/cursor-tool-choice.test.ts tests/providers/cursor/cursor-ultra-mode.test.ts tests/providers/cursor/cursor-umbrella-rows.test.ts tests/responses/responses-state.test.ts +``` + +Use the named subset for the temporary mutation checks, then the domain gate after restoration; do not rerun an unchanged passing check solely for confidence. Full suite is **never local**. Remote parent workflow must bind FETCH_HEAD/full-suite output to this exact PR head SHA, preserve a complete remote log as well as its summary, and ensure the remote checkout is exclusively owned before checkout; do not operate on unrelated dirty remote work. + +Importer proof: compare the 13-file resolved importer set above at parent and tip. Existing external consumer paths stay unchanged. New leaf imports are planned internal edges, not lost callers; count them separately (tool-budget.ts newly imports tool-definitions.ts while request-builder still imports cursorToolsForActivePrompt, so tool-definitions fan-in gains one planned file at L4). The simple 002 line-count command is supporting evidence only: multiline and dynamic imports require the resolved-file check. Export-name/type identity must be checked independently. Run a resolved runtime+type import-cycle scan with available repository tooling or a read-only resolver; do not install a dependency just for this split. Review `git diff --numstat codex/split-adapters-cursor-images...HEAD` with move-aware comparison and separately record raw additions + deletions; apply the sizing escalation above, not an unrecorded exception. Require green exact-head CI rollup, not merely an empty required-check list. + +## Accept criteria + +1. Source basis and parent branch are recorded; every owned top-level declaration in this table has exactly one post-move owner, with identical body/signature and attached explanatory comments. +2. All current 14 exports remain importable from `src/adapters/cursor/request-builder.ts`, with the same value/reference/type identity; no new internal-only export leaks through that original path. Residual local calls are bound by explicit imports. +3. Every planned leaf is ≤400 lines and residual is ≤400 (expected 363); actual `wc -l` agrees or the exact formatting delta is recorded. No omitted #b debt. +4. Identical selected and omitted tool order, exact protobuf byte accounting, execution-path/wait pairing, synthetic-edit budgeting, and catalog-limit note text; no checkpoint or conversation identifier changes. +5. All 13 existing resolved importers remain; direct test imports/assertions and transitive source-reader semantics are preserved. Planned red mutations fail the named guards once, are removed, and the restored focused/domain checks pass with 0 failures. +6. Single-owner state allocations, allowed DAG edges, and no new runtime/type cycles are mechanically verified. Lab PROTECTED roots and optional-subsystem activation remain untouched. +7. Typecheck and privacy scan exit 0; remote-only full suite exits 0 at the exact layer SHA; exact-head CI rollup is green. No local full suite, no merge, and no unrelated changes. +8. Parent-to-tip size obeys the agreed 500-line metric or the parent explicitly resolves the documented exception/topology escalation before implementation; this draft itself is not evidence of an approved exception. + +## PR + +Title: `refactor(adapters-cursor): extract Cursor tool budget selection (split S04 L4/5)` + +Branch: `codex/split-adapters-cursor-request-builder`. Base: `codex/split-adapters-cursor-images`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); paste the stack map below into Summary. Review only this layer's parent-to-tip diff. Replace PR placeholders with actual numbers when opened; no PR is created by this draft. + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 0 (105) | #TBD-S04-L0 | `codex/split-cursor-desktop-executor-contract` | `dev` | desktop-executor-contract | +| 1 | #TBD-S04-L1 | `codex/split-adapters-cursor-tool-definitions` | `codex/split-cursor-desktop-executor-contract` | tool-definitions | +| 2 | #TBD-S04-L2 | `codex/split-adapters-cursor-catalog` | `codex/split-cursor-desktop-executor-contract` | catalog | +| 3 | #TBD-S04-L3 | `codex/split-adapters-cursor-images` | `codex/split-cursor-desktop-executor-contract` | images | +| 4 | #TBD-S04-L4 | `codex/split-adapters-cursor-request-builder` | `codex/split-adapters-cursor-images` | request-builder | +| 5 | #TBD-S04-L5 | `codex/split-adapters-cursor-protobuf-events` | `codex/split-adapters-cursor-tool-definitions` | protobuf-events | + +Current layer: **L4**. Parent: `codex/split-adapters-cursor-images` (#TBD-S04-L3). +Changes to parent `codex/split-adapters-cursor-images` require rebasing this layer and cascading only +through its actual dependency descendants, with exact-tip/base rechecks +(DEV-STACK-02); sibling layer numbering creates no dependency. Merge remains +parent-before-child and separately authorized, never part of this draft. diff --git a/devlog/_plan/260905_now_split_train/150_adapters_cursor_protobuf_events.md b/devlog/_plan/260905_now_split_train/150_adapters_cursor_protobuf_events.md new file mode 100644 index 0000000000..ff3de9cc82 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/150_adapters_cursor_protobuf_events.md @@ -0,0 +1,299 @@ +# S04 L5/5 — protobuf-events + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +Docs basis: `4cc219549`; source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. Every source line range below refers to `src/adapters/cursor/protobuf-events.ts` at that source commit, not a future leaf. Read alongside 000_plan.md, 001_stale_check.md, 002_layer_map.md, and ../260905_modular_debt_ledger/014_lane_adapters_media.md (lane 014; relevant file subsection). Status: diff-level plan only; no code, Git mutation, test run, or orchestration performed by this delegate. + +## Loop spec + +- Archetype: **pure-move**. Work class C3 structural planning, docs-only delegated mode; the parent owns all loop/goal state. +- Goal: move the inventoried responsibilities into the named sibling leaves, each ≤400 lines, preserving the original public import path and leaving 122 expected lines in the original. +- Non-goals: no exported rename/removal, no behavior or signature change, no dependency/tooling installation, no new validation, no changes to generated protobufs, native-exec ownership, live transport scheduling, registry policy, or unrelated files. No production-module execution or test run in this drafting task. +- Verifier: 002_layer_map.md **Per-layer gate**, instantiated in Verification below. Planned commands are for the layer executor; they are not results from this draft. +- Stop: parent records an independently verified, exact-tip layer with all accepts met and exact-head CI rollup; no merge. Stop implementation immediately on a changed signature, string/wire delta, duplicated state, cycle, unaccounted source-reader, or unsupported layer-size claim. +- Escalation: source drift, required files outside this partition/test list, an actual behavior defect, or the sizing conflict below goes to the parent; do not repair it opportunistically. Unreleased security findings go only to approved scratch, never this public devlog. + +Implementation sizing escalation: this exact partition transfers 1250 existing physical lines before import/export glue, already over 002's 500 changed-source-line bound even if moves are counted only once. Under additions + deletions it is at least 2500 lines. The fixed S04 five-layer map has no #b slot. Do not silently call this PR ≤500: the parent must either approve a documented move-only size exception or revise the layer topology (and obtain approval for extra layer docs) before implementation. This bounded draft does not alter 002 or invent a sixth branch. + +Structural decision and pre-change map: Patch grammar (362–365, 427–804) and structured edits (366–426, 805–1043) are stateless transforms. State factory/context usage (22–181, 202–271, 1338–1381) is separate from MCP argument/lifecycle handling (182–201, 272–361, 1044–1227). Dispatcher 1228–1336 stays original. Rejected: patch-only extraction leaves about 999 lines; a single patch/edit leaf exceeds 400. Chosen: four siblings using the existing protobuf-request / protobuf-events naming convention; no new index.ts. Current live-transport.ts:27/:55 → original → types, agent_pb, arg-codec, arg-normalize, tool-definitions, translator-budget (1–20). Intended: original dispatcher → state/tool-events; tool-events → state types, patch-grammar, structured-edit; structured-edit → patch-grammar. State never imports tool-events or dispatcher. Feature-local event contract and original public exports preserved. + +No-code alternatives: doing nothing leaves the requested size debt; deletion/configuration cannot preserve these existing behaviors while shortening their implementation; reuse means moving the current declarations, not inventing equivalent helpers. Owner search: `rg --files src/adapters/cursor`, `rg -n '' src gui/src scripts tests`, and the lane-014 seam audit. The named new siblings do not already exist. Existing stable imports are compatibility boundaries, not permission for new convenience barrels. + +## Symbol inventory + +AST evidence: `git show origin/dev:src/adapters/cursor/protobuf-events.ts`; working-tree bytes compared equal; `ast-grep run --lang typescript --kind --json=compact src/adapters/cursor/protobuf-events.ts` for lexical/variable/function/interface/type-alias/class declarations, filtered to top-level source starts. Ranges are inclusive, include an `export` modifier on the same line, and exclude preceding comments. 85 owned top-level declarations; imports are dependencies, not redeclared owned symbols. + +Consumer counting: `rg -l 'protobuf-events' src gui/src scripts tests` narrows candidates; resolve static `from` and dynamic `import()` relative specifiers to this exact file; then `rg -l -w '' ` counts distinct referencing consumer files. Count excludes the defining file. Private declarations have 0 external bound consumers; their local references move with the partition. This is a file count, not call-site count; do not reuse 001's broad basename heuristic as symbol fan-in. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `DEFAULT_CONTEXT_USAGE_MAX_ENTRIES` | const | 22–22 | no | 0 | `protobuf-event-state.ts` | +| `DEFAULT_CONTEXT_USAGE_TTL_MS` | const | 23–23 | no | 0 | `protobuf-event-state.ts` | +| `DEFAULT_MAX_CLIENT_TOOL_CALLS` | const | 24–24 | no | 0 | `protobuf-event-state.ts` | +| `CursorContextUsageControls` | interface | 26–34 | yes | 0 | `protobuf-event-state.ts` | +| `CursorContextUsageTracker` | interface | 36–44 | yes | 0 | `protobuf-event-state.ts` | +| `CursorContextUsageEntry` | interface | 46–49 | no | 0 | `protobuf-event-state.ts` | +| `createCursorContextUsageTracker` | function | 58–130 | yes | 5 | `protobuf-event-state.ts` | +| `CursorProtobufEventState` | interface | 132–179 | yes | 2 | `protobuf-event-state.ts` | +| `structuredEditCallIsOurs` | function | 195–200 | no | 0 | `protobuf-tool-events.ts` | +| `createCursorProtobufEventState` | function | 202–248 | yes | 7 | `protobuf-event-state.ts` | +| `observeContextTokens` | function | 250–254 | no | 0 | `protobuf-event-state.ts` | +| `reportableContextTokens` | function | 256–262 | yes | 1 | `protobuf-event-state.ts` | +| `usageFromContextTokens` | function | 264–270 | yes | 1 | `protobuf-event-state.ts` | +| `mcpArgsFromToolCall` | function | 273–277 | yes | 1 | `protobuf-tool-events.ts` | +| `mcpWireNameFromArgs` | function | 279–283 | no | 0 | `protobuf-tool-events.ts` | +| `mcpCursorWireName` | function | 285–287 | no | 0 | `protobuf-tool-events.ts` | +| `decodeMcpArgs` | function | 289–291 | no | 0 | `protobuf-tool-events.ts` | +| `resolveAdvertisedClientToolName` | function | 294–301 | no | 0 | `protobuf-tool-events.ts` | +| `toolSchemaForWireName` | function | 303–306 | no | 0 | `protobuf-tool-events.ts` | +| `decodeMcpArgsNormalized` | function | 308–314 | no | 0 | `protobuf-tool-events.ts` | +| `hasMcpArgBytes` | function | 316–318 | no | 0 | `protobuf-tool-events.ts` | +| `isCompleteJson` | function | 320–328 | no | 0 | `protobuf-tool-events.ts` | +| `normalizeJsonText` | function | 331–343 | no | 0 | `protobuf-tool-events.ts` | +| `resolveCompletedArgs` | function | 355–360 | no | 0 | `protobuf-tool-events.ts` | +| `PATCH_BEGIN` | const | 362–362 | no | 0 | `patch-grammar.ts` | +| `PATCH_END` | const | 363–363 | no | 0 | `patch-grammar.ts` | +| `GIT_HUNK_HEADER` | const | 364–364 | no | 0 | `patch-grammar.ts` | +| `MARKDOWN_FENCE` | const | 365–365 | no | 0 | `patch-grammar.ts` | +| `PATH_ARG_KEYS` | const | 366–366 | no | 0 | `structured-edit.ts` | +| `OLD_STRING_KEYS` | const | 367–367 | no | 0 | `structured-edit.ts` | +| `NEW_STRING_KEYS` | const | 368–368 | no | 0 | `structured-edit.ts` | +| `StructuredEditPair` | type | 370–370 | yes | 0 | `structured-edit.ts` | +| `lineBlockIndex` | function | 373–382 | no | 0 | `structured-edit.ts` | +| `replaceLineBlock` | function | 384–391 | no | 0 | `structured-edit.ts` | +| `foldSequentialStructuredEdits` | function | 399–425 | yes | 1 | `structured-edit.ts` | +| `GIT_NO_NEWLINE` | const | 427–427 | no | 0 | `patch-grammar.ts` | +| `GIT_META_PREFIX` | const | 428–428 | no | 0 | `patch-grammar.ts` | +| `GIT_FILE_HEADER` | const | 429–429 | no | 0 | `patch-grammar.ts` | +| `isCodexFileOpLine` | function | 431–435 | no | 0 | `patch-grammar.ts` | +| `canonicalizeCodexLine` | function | 437–460 | no | 0 | `patch-grammar.ts` | +| `isGitPreambleLine` | function | 462–467 | no | 0 | `patch-grammar.ts` | +| `unquoteGitPath` | function | 469–478 | no | 0 | `patch-grammar.ts` | +| `normalizePatchPath` | function | 481–485 | no | 0 | `patch-grammar.ts` | +| `parseDiffGitPaths` | function | 487–493 | no | 0 | `patch-grammar.ts` | +| `parseGitSidePath` | function | 495–501 | no | 0 | `patch-grammar.ts` | +| `isDevNull` | function | 503–505 | no | 0 | `patch-grammar.ts` | +| `rewriteHunkHeader` | function | 507–509 | no | 0 | `patch-grammar.ts` | +| `isFenceLine` | function | 511–513 | no | 0 | `patch-grammar.ts` | +| `isHunkBodyLine` | function | 515–523 | no | 0 | `patch-grammar.ts` | +| `rewriteCodexFileOpLine` | function | 525–532 | no | 0 | `patch-grammar.ts` | +| `normalizeAddFileBody` | function | 534–556 | no | 0 | `patch-grammar.ts` | +| `hasNonEmptyCodexOp` | function | 558–588 | no | 0 | `patch-grammar.ts` | +| `trimEmptyEdges` | function | 590–596 | no | 0 | `patch-grammar.ts` | +| `cleanHunkLines` | function | 598–612 | no | 0 | `patch-grammar.ts` | +| `isGitSectionStart` | function | 614–617 | no | 0 | `patch-grammar.ts` | +| `splitGitSections` | function | 619–633 | no | 0 | `patch-grammar.ts` | +| `isGitBinarySection` | function | 635–637 | no | 0 | `patch-grammar.ts` | +| `isGitCopySection` | function | 639–641 | no | 0 | `patch-grammar.ts` | +| `isGitEmptyRenameSection` | function | 643–649 | no | 0 | `patch-grammar.ts` | +| `isGitUntranslatableSection` | function | 651–653 | no | 0 | `patch-grammar.ts` | +| `convertGitSection` | function | 655–696 | no | 0 | `patch-grammar.ts` | +| `hasCodexFileOp` | function | 698–700 | no | 0 | `patch-grammar.ts` | +| `sanitizeCodexApplyPatch` | function | 703–749 | yes | 1 | `patch-grammar.ts` | +| `coercePatchInput` | function | 751–772 | no | 0 | `patch-grammar.ts` | +| `sanitizeEmittedApplyPatchArgs` | function | 774–803 | yes | 1 | `patch-grammar.ts` | +| `firstStringArg` | function | 805–811 | no | 0 | `structured-edit.ts` | +| `firstStringOrLines` | function | 813–822 | no | 0 | `structured-edit.ts` | +| `patchLines` | function | 825–829 | no | 0 | `structured-edit.ts` | +| `restoreFlushLeftIndent` | function | 837–856 | no | 0 | `structured-edit.ts` | +| `addFilePatch` | function | 858–864 | no | 0 | `structured-edit.ts` | +| `replacementHunk` | function | 867–892 | no | 0 | `structured-edit.ts` | +| `StructuredEditTranslation` | type | 902–904 | yes | 0 | `structured-edit.ts` | +| `translateStructuredEditCall` | function | 906–1042 | yes | 1 | `structured-edit.ts` | +| `mapSyntheticMcpExecToToolEvents` | function | 1044–1091 | yes | 4 | `protobuf-tool-events.ts` | +| `recordToolCall` | function | 1101–1118 | no | 0 | `protobuf-tool-events.ts` | +| `cursorFreeformWrapperValid` | function | 1126–1136 | no | 0 | `protobuf-tool-events.ts` | +| `dropInvalidFreeformCall` | function | 1138–1143 | no | 0 | `protobuf-tool-events.ts` | +| `dropShellBridgeCall` | function | 1145–1150 | no | 0 | `protobuf-tool-events.ts` | +| `dropStructuredEditCall` | function | 1152–1159 | no | 0 | `protobuf-tool-events.ts` | +| `commitToolCall` | function | 1161–1196 | no | 0 | `protobuf-tool-events.ts` | +| `bufferToolArgs` | function | 1204–1218 | no | 0 | `protobuf-tool-events.ts` | +| `endToolCall` | function | 1220–1226 | no | 0 | `protobuf-tool-events.ts` | +| `mapCursorProtobufServerMessage` | function | 1228–1336 | yes | 4 | `protobuf-events.ts` (residual) | +| `resolvedTurnUsage` | function | 1344–1357 | yes | 1 | `protobuf-event-state.ts` | +| `finalizeTurnEvents` | function | 1365–1381 | yes | 2 | `protobuf-event-state.ts` | + +Resolved direct importers: 9 distinct files (1 production, 8 tests). Production paths: + +- `src/adapters/cursor/live-transport.ts` — unchanged. + +## Leaf partition + +All paths below are new sibling files under `src/adapters/cursor/`, following the existing kebab-case native-exec-* and protobuf-* convention. Each symbol body and attached comment moves without rewriting. Physical slice accounting includes blank lines/comments; keep slice contents in their original relative order. Expected sizes use the exact compact import/re-export lines shown; multiline formatting consumes spare budget and must be recounted, especially catalog.ts. + +### `src/adapters/cursor/protobuf-event-state.ts` + +- Transfer source slices: 22–181, 202–271, 1338–1381 (274 physical lines). +- Symbols: `DEFAULT_CONTEXT_USAGE_MAX_ENTRIES`, `DEFAULT_CONTEXT_USAGE_TTL_MS`, `DEFAULT_MAX_CLIENT_TOOL_CALLS`, `CursorContextUsageControls`, `CursorContextUsageTracker`, `CursorContextUsageEntry`, `createCursorContextUsageTracker`, `CursorProtobufEventState`, `createCursorProtobufEventState`, `observeContextTokens`, `reportableContextTokens`, `usageFromContextTokens`, `resolvedTurnUsage`, `finalizeTurnEvents`. +- Expected line count: 274 moved + 3 import lines = **277**, ≤400. +- Own imports: + +```ts +import type { OcxUsage } from "../../types"; +import type { TranslatorBudget } from "../../lib/translator-budget"; +import type { CursorServerMessage } from "./types"; +``` + +### `src/adapters/cursor/protobuf-tool-events.ts` + +- Transfer source slices: 182–201, 272–361, 1044–1227 (294 physical lines). +- Symbols: `structuredEditCallIsOurs`, `mcpArgsFromToolCall`, `mcpWireNameFromArgs`, `mcpCursorWireName`, `decodeMcpArgs`, `resolveAdvertisedClientToolName`, `toolSchemaForWireName`, `decodeMcpArgsNormalized`, `hasMcpArgBytes`, `isCompleteJson`, `normalizeJsonText`, `resolveCompletedArgs`, `mapSyntheticMcpExecToToolEvents`, `recordToolCall`, `cursorFreeformWrapperValid`, `dropInvalidFreeformCall`, `dropShellBridgeCall`, `dropStructuredEditCall`, `commitToolCall`, `bufferToolArgs`, `endToolCall`. +- Expected line count: 294 moved + 8 import lines = **302**, ≤400. +- Own imports: + +```ts +import type { McpArgs, ToolCall } from "./gen/agent_pb"; +import { decodeCursorArgsMap } from "./arg-codec"; +import { normalizeArgKeys } from "./arg-normalize"; +import { CODEX_APPLY_PATCH_TOOL, cursorShellBridgeArgsValid, cursorShellBridgeDropError, defaultShellBridgeArgNormalizeSchema, isCodexShellBridgeToolName, normalizeCursorWireName, OCX_RESPONSES_TOOL_PROVIDER, resolveShellBridgeAliasKey, responsesToolNameFromCursorWire } from "./tool-definitions"; +import type { CursorServerMessage } from "./types"; +import type { CursorProtobufEventState } from "./protobuf-event-state"; +import { sanitizeEmittedApplyPatchArgs } from "./patch-grammar"; +import { translateStructuredEditCall } from "./structured-edit"; +``` + +### `src/adapters/cursor/patch-grammar.ts` + +- Transfer source slices: 362–365, 427–804 (382 physical lines). +- Symbols: `PATCH_BEGIN`, `PATCH_END`, `GIT_HUNK_HEADER`, `MARKDOWN_FENCE`, `GIT_NO_NEWLINE`, `GIT_META_PREFIX`, `GIT_FILE_HEADER`, `isCodexFileOpLine`, `canonicalizeCodexLine`, `isGitPreambleLine`, `unquoteGitPath`, `normalizePatchPath`, `parseDiffGitPaths`, `parseGitSidePath`, `isDevNull`, `rewriteHunkHeader`, `isFenceLine`, `isHunkBodyLine`, `rewriteCodexFileOpLine`, `normalizeAddFileBody`, `hasNonEmptyCodexOp`, `trimEmptyEdges`, `cleanHunkLines`, `isGitSectionStart`, `splitGitSections`, `isGitBinarySection`, `isGitCopySection`, `isGitEmptyRenameSection`, `isGitUntranslatableSection`, `convertGitSection`, `hasCodexFileOp`, `sanitizeCodexApplyPatch`, `coercePatchInput`, `sanitizeEmittedApplyPatchArgs`. +- Expected line count: 382 moved + 0 import lines = **382**, ≤400. +- Own imports: none; standard Bun/JavaScript globals are not module imports. + +### `src/adapters/cursor/structured-edit.ts` + +- Transfer source slices: 366–426, 805–1043 (300 physical lines). +- Symbols: `PATH_ARG_KEYS`, `OLD_STRING_KEYS`, `NEW_STRING_KEYS`, `StructuredEditPair`, `lineBlockIndex`, `replaceLineBlock`, `foldSequentialStructuredEdits`, `firstStringArg`, `firstStringOrLines`, `patchLines`, `restoreFlushLeftIndent`, `addFilePatch`, `replacementHunk`, `StructuredEditTranslation`, `translateStructuredEditCall`. +- Expected line count: 300 moved + 2 import lines = **302**, ≤400. +- Own imports: + +```ts +import { CURSOR_MULTI_EDIT_TOOL, isCursorStructuredEditToolName } from "./tool-definitions"; +import { PATCH_BEGIN, PATCH_END, normalizePatchPath } from "./patch-grammar"; +``` + +### Residual `src/adapters/cursor/protobuf-events.ts` + +Retain: `mapCursorProtobufServerMessage`. + +Replace all original imports at 1–20 with the five explicit imports below. The remaining original consists of dispatcher 1228–1336 and blank lines; add six named re-export lines. Count is 131 − 20 + 5 + 6 = 122. + +Accounting: 1381 − 1250 moved − 20 net removed import lines + 5 local import lines + 6 re-export lines = **122** expected lines. All leaves plus residual total 1385 = 1381 original + 4 net import/export glue lines. No >400 residual and no #a/#b/#c part in this approved map. A size-policy escalation is not a hidden #b commitment; if the parent adds parts, re-plan lower-consumer leaves first and publish each intermediate residual count. + +New leaf-only exports are existing declarations, not new helpers: protobuf-event-state exports observeContextTokens (250); protobuf-tool-events exports mcpCursorWireName (285), recordToolCall (1101), bufferToolArgs (1204), hasMcpArgBytes (316), cursorFreeformWrapperValid (1126), resolveCompletedArgs (355), commitToolCall (1161); patch-grammar exports PATCH_BEGIN (362), PATCH_END (363), normalizePatchPath (481). Keep these out of the original public export set. Preserve private decodeMcpArgs (289) even if presently unused; do not delete it during a move. + +## Re-export block + +Insert into the original file exactly these named lines; current exported declarations that stay local remain exported in place (`mapCursorProtobufServerMessage`). Do not use export-star and do not re-export newly exposed internal-only seams. + +```ts +export { createCursorContextUsageTracker, createCursorProtobufEventState, reportableContextTokens, usageFromContextTokens, resolvedTurnUsage, finalizeTurnEvents } from "./protobuf-event-state"; +export type { CursorContextUsageControls, CursorContextUsageTracker, CursorProtobufEventState } from "./protobuf-event-state"; +export { mcpArgsFromToolCall, mapSyntheticMcpExecToToolEvents } from "./protobuf-tool-events"; +export { sanitizeCodexApplyPatch, sanitizeEmittedApplyPatchArgs } from "./patch-grammar"; +export { foldSequentialStructuredEdits, translateStructuredEditCall } from "./structured-edit"; +export type { StructuredEditPair, StructuredEditTranslation } from "./structured-edit"; +``` + +Re-export binds nothing locally. The original needs these explicit leaf imports (this is the complete replacement import block, including retained external dependencies): + +```ts +import type { AgentServerMessage } from "./gen/agent_pb"; +import type { CursorServerMessage } from "./types"; +import { normalizeCursorTextToolMarkers } from "./tool-definitions"; +import { observeContextTokens, finalizeTurnEvents, type CursorProtobufEventState } from "./protobuf-event-state"; +import { mcpCursorWireName, mcpArgsFromToolCall, recordToolCall, bufferToolArgs, hasMcpArgBytes, cursorFreeformWrapperValid, resolveCompletedArgs, commitToolCall } from "./protobuf-tool-events"; +``` + +## Module-level state and cycles + +No top-level mutable let, Map, Set, WeakMap, lock, or timer. Context tracker entries Map is factory-local at :62 and stays inside createCursorContextUsageTracker in protobuf-event-state.ts. Request-local open/completed/client/freeform/provenance collections (:223–228) remain factory-local in that same leaf; do not hoist or clone them. TranslatorBudget remains caller-owned. State functions and tool-events receive the same object; no shared module singleton is introduced. Three default scalars (22–24) are owned by the state leaf; patch regexes and PATCH_BEGIN/PATCH_END by patch-grammar; edit key arrays by structured-edit. The state leaf includes finalizeTurnEvents and resolvedTurnUsage, which do not depend on tool handlers; this avoids state → tool-events → state. Tool-events owns record/commit/end together, avoiding commit → original dispatcher → tool-events. Structured-edit imports the existing normalizePatchPath and patch delimiters from patch-grammar; patch-grammar imports nothing and cannot depend on structured-edit. CursorServerMessage is imported from the existing ./types, not from the original event facade. + +Read-only graph check of this planned layer's new imports found no return cycle involving `protobuf-event-state.ts`, `protobuf-tool-events.ts`, `patch-grammar.ts`, `structured-edit.ts`. The stack still inherits the **L1 type-only-cycle prerequisite** documented in 110_adapters_cursor_tool_definitions.md: `src/types.ts:112 → src/types/provider.ts:701 → native-exec-desktop.ts:19 → native-exec-tools.ts:25 → tool-definitions.ts → src/types.ts`. Do not claim whole-stack type acyclicity until the parent resolves that out-of-scope prerequisite; these later leaves do not repair it. The local partition/line accounting here remains conditional on a valid L1 parent. + +The leaf direction listed in Loop spec is the allowed DAG. Sibling leaves import their canonical owner directly, never this original facade. Preserve initialization order for cross-constant references. Verify both runtime and type-only edges; a typecheck alone does not prove acyclicity. Compare the resolved import graph at the parent and tip; zero new cycles and no path from any new leaf back to the original are required. Existing external-format/provenance checks remain at the same trust boundary; do not reinterpret validation while relocating it. + +## Tests + +Exact direct-test list from `rg -l 'adapters/cursor/protobuf-events' tests`, with specifier resolution to discard comments/other basenames: + +- `tests/providers/cursor/cursor-interaction-query.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-live-transport.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-protobuf-events.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-structured-edit.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-tool-arg-decoding.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-tool-continuation.test.ts` — **unchanged** import path and assertions. +- `tests/providers/cursor/cursor-tool-finalize-race.test.ts` — **unchanged** import path and assertions. +- `tests/responses/responses-state.test.ts` — **unchanged** import path and assertions. + +No source-body oracle found for protobuf-events.ts. responses-state.test.ts imports createCursorContextUsageTracker at :23 but reads persisted state artifacts, not this file. Dynamic imports in cursor-interaction-query.test.ts:151/:165/:173/:190/:196 are ordinary API consumers. All tests below remain unchanged; no source retargets or scan-list changes. + +Transitive source-reader exception: `tests/lab/core-lab-boundary.test.ts:69` reads each resolved source file while walking static imports/re-exports. A read-only replay of that walk from `src/server/responses/core.ts` reaches this target (413 visited files at the basis). Disposition: **unchanged**; new leaves are automatically included through named imports/re-exports, so no manual add-leaf-to-scan-list and no retarget. Never edit its PROTECTED roots (lines 20–28). At implementation time drive this guard red once with a temporary forbidden leaf edge to `../../lab/paths`, then restore and prove green; no forbidden edge may enter a commit. + +In C phase, drive tests/providers/cursor/cursor-structured-edit.test.ts:296 red by temporarily restoring substring folding; :750 red by temporarily dropping the mixed-binary passthrough; :1419 red by temporarily converting the stateless native-exec branch without provenance. Restore immediately. Drive tests/providers/cursor/cursor-protobuf-events.test.ts:1022 red by temporarily removing dispatcher termination gating; preserve :277 atomic parallel emission and :925 absolute checkpoint totals. This validates moved ownership without weakening failure or translator-budget semantics. + +No test file is added by this plan, hence no test-layout manifest change. If extra regression coverage proves necessary, extend the existing focused files first and report scope expansion instead of silently creating new tests. + +## Verification + +Instantiate 002's Per-layer gate in this layer's dedicated worktree, not in the docs worktree. Nothing in this code fence was run by the drafting delegate. + +```sh +bun run typecheck +# Focused domain: providers/cursor (includes the direct Cursor tests listed above) +bun test tests/providers/cursor +bun test tests/adapters/adapter-tool-conformance.test.ts +bun test tests/responses/responses-state.test.ts +# Transitive source-graph guard; justified even though only adapters files move +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/adapters/cursor/protobuf-event-state.ts src/adapters/cursor/protobuf-tool-events.ts src/adapters/cursor/patch-grammar.ts src/adapters/cursor/structured-edit.ts src/adapters/cursor/protobuf-events.ts +rg -n 'from "[^"]*/protobuf-events"' src gui/src scripts tests | wc -l +rg -l 'adapters/cursor/protobuf-events' tests +# Full suite: remote only; preserve pipeline failure rather than trusting tail's exit status +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-cursor-protobuf-events && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused named subset (for initial tight red/green and for an exact task manifest): + +```sh +bun test tests/adapters/adapter-tool-conformance.test.ts tests/providers/cursor/cursor-interaction-query.test.ts tests/providers/cursor/cursor-live-transport.test.ts tests/providers/cursor/cursor-protobuf-events.test.ts tests/providers/cursor/cursor-structured-edit.test.ts tests/providers/cursor/cursor-tool-arg-decoding.test.ts tests/providers/cursor/cursor-tool-continuation.test.ts tests/providers/cursor/cursor-tool-finalize-race.test.ts tests/responses/responses-state.test.ts +``` + +Use the named subset for the temporary mutation checks, then the domain gate after restoration; do not rerun an unchanged passing check solely for confidence. Full suite is **never local**. Remote parent workflow must bind FETCH_HEAD/full-suite output to this exact PR head SHA, preserve a complete remote log as well as its summary, and ensure the remote checkout is exclusively owned before checkout; do not operate on unrelated dirty remote work. + +Importer proof: compare the 9-file resolved importer set above at parent and tip. Existing external consumer paths stay unchanged. New leaf imports are planned internal edges, not lost callers; count them separately. The simple 002 line-count command is supporting evidence only: multiline and dynamic imports require the resolved-file check. Export-name/type identity must be checked independently. Run a resolved runtime+type import-cycle scan with available repository tooling or a read-only resolver; do not install a dependency just for this split. Review `git diff --numstat codex/split-adapters-cursor-tool-definitions...HEAD` with move-aware comparison and separately record raw additions + deletions; apply the sizing escalation above, not an unrecorded exception. Require green exact-head CI rollup, not merely an empty required-check list. + +## Accept criteria + +1. Source basis and parent branch are recorded; every owned top-level declaration in this table has exactly one post-move owner, with identical body/signature and attached explanatory comments. +2. All current 18 exports remain importable from `src/adapters/cursor/protobuf-events.ts`, with the same value/reference/type identity; no new internal-only export leaks through that original path. Residual local calls are bound by explicit imports. +3. Every planned leaf is ≤400 lines and residual is ≤400 (expected 122); actual `wc -l` agrees or the exact formatting delta is recorded. No omitted #b debt. +4. Patch and structured-edit output strings/errors are byte-identical; request-local state identity, atomic tool start/delta/end order, late-native argument handling, provenance gates, terminal inertness, usage totals, and translator-budget reservations/close ordering remain unchanged. +5. All 9 existing resolved importers remain; direct test imports/assertions and transitive source-reader semantics are preserved. Planned red mutations fail the named guards once, are removed, and the restored focused/domain checks pass with 0 failures. +6. Single-owner state allocations, allowed DAG edges, and no new runtime/type cycles are mechanically verified. Lab PROTECTED roots and optional-subsystem activation remain untouched. +7. Typecheck and privacy scan exit 0; remote-only full suite exits 0 at the exact layer SHA; exact-head CI rollup is green. No local full suite, no merge, and no unrelated changes. +8. Parent-to-tip size obeys the agreed 500-line metric or the parent explicitly resolves the documented exception/topology escalation before implementation; this draft itself is not evidence of an approved exception. + +## PR + +Title: `refactor(adapters-cursor): separate patch translation and event-state seams (split S04 L5/5)` + +Branch: `codex/split-adapters-cursor-protobuf-events`. Base: `codex/split-adapters-cursor-tool-definitions`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); paste the stack map below into Summary. Review only this layer's parent-to-tip diff. Replace PR placeholders with actual numbers when opened; no PR is created by this draft. + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 0 (105) | #TBD-S04-L0 | `codex/split-cursor-desktop-executor-contract` | `dev` | desktop-executor-contract | +| 1 | #TBD-S04-L1 | `codex/split-adapters-cursor-tool-definitions` | `codex/split-cursor-desktop-executor-contract` | tool-definitions | +| 2 | #TBD-S04-L2 | `codex/split-adapters-cursor-catalog` | `codex/split-cursor-desktop-executor-contract` | catalog | +| 3 | #TBD-S04-L3 | `codex/split-adapters-cursor-images` | `codex/split-cursor-desktop-executor-contract` | images | +| 4 | #TBD-S04-L4 | `codex/split-adapters-cursor-request-builder` | `codex/split-adapters-cursor-images` | request-builder | +| 5 | #TBD-S04-L5 | `codex/split-adapters-cursor-protobuf-events` | `codex/split-adapters-cursor-tool-definitions` | protobuf-events | + +Current layer: **L5**. Parent: `codex/split-adapters-cursor-tool-definitions` (#TBD-S04-L1). +Changes to parent `codex/split-adapters-cursor-tool-definitions` require rebasing this layer and cascading only +through its actual dependency descendants, with exact-tip/base rechecks +(DEV-STACK-02); sibling layer numbering creates no dependency. Merge remains +parent-before-child and separately authorized, never part of this draft. diff --git a/devlog/_plan/260905_now_split_train/160_adapters_xai_tool_schema.md b/devlog/_plan/260905_now_split_train/160_adapters_xai_tool_schema.md new file mode 100644 index 0000000000..2e151744fe --- /dev/null +++ b/devlog/_plan/260905_now_split_train/160_adapters_xai_tool_schema.md @@ -0,0 +1,164 @@ +# 160 — S05 L1: xAI schema analysis + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**; C3 boundary planning, docs-only delegated work. Source basis `origin/dev:1362b1a38`; docs HEAD `4cc219549`. Inputs: 000, 001, 002 S05 row, and `../260905_modular_debt_ledger/014_lane_adapters_media.md` xAI section. +- Goal: reduce `src/adapters/xai-tool-schema.ts` from 436 to an expected 351 lines by moving provider-local pointer/value analysis to one 87-line leaf; all four existing public exports retain identity and original import paths. +- Non-goals: no schema-policy changes, budget changes, new validation, helper renames, provider host changes, generic schema library, function-body refactors, or code/test/git mutation in this drafting task. +- Structural decision: callers currently enter the one schema module. `src/adapters/openai-chat.ts:32`, `src/adapters/openai-responses.ts:25`, and `src/server/responses/core.ts:37` consume it; its only import is `../types` at source line 1. Split existing pure primitives, keeping orchestration/budgets in place. Reject doing nothing (436 >400) and a generic reuse/substitution (other schema compilers carry different policy). Consequence: callers → existing schema boundary → dependency-free analysis leaf; feature-local blast radius, zero caller migration. +- Verifier: 002 **Per-layer gate**, instantiated below, plus unchanged schema fixtures and source-body comparison. +- Stop: executor records passing gates and open exact-head green L1 PR; never merge. This delegation stops after the assigned document is written and statically checked. +- Escalation: source drift, an oracle not listed below, a new cycle, changed schema semantics, >400 residual/leaf, or >500 changed source lines requires parent reconciliation. No orchestration/loop/goal commands here. + +## Symbol inventory + +All source ranges below refer to `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`, not the docs HEAD. In-memory Babel TypeScript AST enumeration of `git show origin/dev:src/adapters/xai-tool-schema.ts` supplies inclusive declaration ranges (comments before declarations excluded). Every top-level definition is listed; import bindings are covered separately by the leaf import blocks. + +Consumer counts are distinct external files importing that exact symbol from the original module, not textual occurrences or calls. Reproduce candidates with `rg -l 'xai-tool-schema' src gui/src scripts tests -g '*.ts' -g '*.tsx'`, resolve relative import paths, then match imported names. This excludes unrelated same-basename modules and generic words such as `usage`; private definitions have zero external importers. There are 3 direct importer files and 29 definitions. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `isSchemaObject` | function | 3–5 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `isXaiSchemaTarget` | function | 7–15 | yes | 2 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XaiToolSchemaCompatibilityError` | class | 18–18 | yes | 2 | `src/adapters/xai-tool-schema.ts (residual)` | +| `stringRequiredFields` | function | 20–24 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XAI_VARIANT_MERGE_KEYS` | const | 27–37 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XAI_MAX_SCHEMA_DEPTH` | const | 46–46 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XAI_MAX_SCHEMA_NODES` | const | 47–47 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XAI_MAX_ROOT_VARIANTS` | const | 48–48 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XaiSchemaBudget` | interface | 51–54 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `createXaiSchemaBudget` | function | 57–59 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `decodeJsonPointerToken` | function | 61–63 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `lookupLocalJsonPointer` | function | 66–75 | yes | 1 | `src/adapters/xai-schema-analysis.ts` | +| `resolveXaiSchemaRefs` | function | 78–132 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `xaiVariantIsConcreteObject` | function | 135–138 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `variantProperties` | function | 140–142 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `xaiPropertyMergeIsLossless` | function | 150–164 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `xaiRequiredSetsMatch` | function | 166–169 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `xaiLiteralValues` | function | 172–177 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `xaiJsonTypeOf` | function | 180–187 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `xaiDeclaredTypes` | function | 190–196 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `xaiTypesOverlap` | function | 199–202 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `xaiSchemasAreProvablyDisjoint` | function | 209–226 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `xaiSchemasArePairwiseDisjoint` | function | 229–236 | no | 0 | `src/adapters/xai-schema-analysis.ts` | +| `uniqueXaiSchemas` | function | 239–249 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `mergeXaiAdditionalProperties` | function | 251–265 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `composeXaiObjectSchemas` | function | 268–292 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `XaiRootExpansion` | interface | 295–311 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `expandXaiRootObjectSchemas` | function | 313–341 | no | 0 | `src/adapters/xai-tool-schema.ts (residual)` | +| `normalizeXaiToolParameters` | function | 370–436 | yes | 2 | `src/adapters/xai-tool-schema.ts (residual)` | + +## Leaf partition + +Naming reuses provider-prefixed siblings `src/adapters/ollama-native-url.ts:9` and `src/adapters/kiro-thinking.ts:1`; no index barrel or generic utils module. Original entry stays the compatibility boundary required by cxc-dev §5. + +| New file | Symbols | Original source slices including attached comments/blanks | Expected physical lines | +|---|---|---|---:| +| `src/adapters/xai-schema-analysis.ts` | `isSchemaObject`, `decodeJsonPointerToken`, `lookupLocalJsonPointer`, `xaiLiteralValues`, `xaiJsonTypeOf`, `xaiDeclaredTypes`, `xaiTypesOverlap`, `xaiSchemasAreProvablyDisjoint`, `xaiSchemasArePairwiseDisjoint` | 3–6, 61–76, 171–237 | 87 | + +Own imports: **none**. Export `isSchemaObject`, `lookupLocalJsonPointer`, and `xaiSchemasArePairwiseDisjoint` from the leaf for real production callers; the other six definitions stay private. This is not test-only exposure. Keep the symbol bodies and pointer decoding order verbatim. + +Residual `src/adapters/xai-tool-schema.ts`: every row marked residual stays, including target detection, error class, budget definitions, ref resolver, composition, union expansion and normalizer. Arithmetic: 436 − (4 + 16 + 67) + 2 shim lines = **351**; leaf 87; aggregate 438 includes two new boundary lines. Single L1, no #b required. About 176 added/deleted source lines before metadata; verify actual diff remains ≤500. + +## Re-export block + +Insert exactly these boundary lines; all other existing exports remain inline: +```ts +export { lookupLocalJsonPointer } from "./xai-schema-analysis"; +import { isSchemaObject, lookupLocalJsonPointer, xaiSchemasArePairwiseDisjoint } from "./xai-schema-analysis"; +``` +The residual retains `import type { OcxProviderConfig } from "../types";`. It calls the locally imported pointer resolver at old line 91, predicate throughout, and disjointness analysis at 419. Re-export alone binds none of them. `isXaiSchemaTarget`, `XaiToolSchemaCompatibilityError`, and `normalizeXaiToolParameters` remain their original inline exports; no wildcard, duplicate export, wrapper, or alias. + +## Module-level state and cycles + +- `XAI_VARIANT_MERGE_KEYS` (27–37) remains owned only by the residual; constant-by-convention Set, never copied. `XAI_MAX_SCHEMA_DEPTH` (46), `XAI_MAX_SCHEMA_NODES` (47), `XAI_MAX_ROOT_VARIANTS` (48) also remain there. +- No top-level let, Map, WeakMap, timer or lock. The budget is allocated once per tool at 372 and shared by ref resolution (373) and root expansion (379); do not recreate it inside the leaf. +- Sets in disjointness at 190–195 and 213 are invocation-local, not module state. +- Existing lane G1 found no return-path cycle. New graph: boundary → analysis; analysis has no imports. Having analysis import `isSchemaObject` back from the boundary would create a cycle, so the predicate moves with its consumers. Existing `../types` type edge stays only in the residual. +- Coupling: existing provider-format coupling remains inside this feature; new edge is functional, with no lifecycle or shared-cache edge. + +## Tests + +Exact original-module direct-import query: +```sh +rg -l '["\x27][^"\x27]*adapters/xai-tool-schema(\.ts)?["\x27]' tests -g '*.ts' +``` +Result: **empty**. The schema suite tests through `createOpenAIChatAdapter`, not by importing this file; do not invent a direct importer. + +- `tests/providers/xai/xai-tool-schema.test.ts:4` — unchanged indirect behavioral oracle. Cases at 226, 251, 301, 326, 353 and 374 cover exclusivity, required promotion, mixed nesting, variant and node budgets. +- `tests/lib/reasoning-replay-scope-source.test.ts:33` reads **openai-chat.ts**, not this file — unchanged, no retarget. +- Filename-specific source-text oracles: **none** after `rg -l 'readFileSync|Bun\\.file|readFile\\(' tests -g '*.test.ts'` candidates were filtered by this basename and inspected. +- Generic transitive source reader: `tests/lab/core-lab-boundary.test.ts:69` in `firstLabPath`, invoked at 284 onward — unchanged; follows the new static import automatically. No add-leaf-to-scan-list or retarget needed. PROTECTED at 20 remains untouched. + +Drive red once during implementation, then restore: temporarily make the leaf pairwise-disjoint predicate always true and run the existing overlapping-oneOf case (test declaration 226); it must fail because a oneOf cannot become anyOf. Temporarily add a leaf→Lab static edge and confirm the transitive guard reports it, then remove it. This mutates only the disposable implementation worktree and is not part of this docs pass. Do not weaken assertions to obtain green. + +## Verification + +Implementation-only commands: none were run for this docs-only delegation. This instantiates `002_layer_map.md` → **Per-layer gate** (the `003` reference in 000 is stale). + +```sh +bun run typecheck +bun test tests/providers/xai/xai-tool-schema.test.ts tests/lib/reasoning-replay-scope-source.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/adapters/xai-schema-analysis.ts src/adapters/xai-tool-schema.ts +rg -l 'from "[^"]*/xai-tool-schema"' src gui/src scripts tests +git diff --check +git diff --numstat dev...HEAD -- src tests +``` + +Focused domains: `tests/providers/xai`, adapter/schema compatibility, and `tests/lib` source oracle. The original-path static importer list must retain 3 unique files after exact relative-path filtering (the raw basename rg can include unrelated modules). Keep exports/types resolvable; count alone is not proof. No protected-root edits are needed; the Lab guard is included because adapters are transitively reachable. Each listed leaf and residual must be ≤400 physical lines. Compare normalized AST bodies before/after, allowing only location, import/export modifiers and required import binding changes; preserve comments and exact error/wire literals. + +Run the resolved-relative-import/re-export graph walk from lane 014's G1, including type edges, at the layer tip; no return path from any new leaf to its old boundary or another leaf may appear. The Lab guard checks optional-subsystem reachability, not general cycles. + +Full suite is **never local**; executor uses the existing authorized remote checkout only after verifying its ownership, with pipeline failure propagation: +```sh +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-xai-tool-schema && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15"' +``` +Record remote HEAD equal to PR head, full-suite exit status and totals, local focused/typecheck/privacy results, and the complete exact-head CI rollup. A tail without the test exit status is not evidence. Re-run only invalidated checks after a lower-layer cascade; no merge/auto-merge. + +## Accept criteria + +1. All 29 definitions have exactly one owner matching the inventory; all moved bodies equal origin/dev modulo export modifiers. +2. The four original exports remain importable by identical names; `lookupLocalJsonPointer` resolves to the leaf binding, not a wrapper. +3. Original direct importer set remains exactly the three files listed in Loop spec; no protected-root edits. +4. One policy Set owner; one budget per normalization call; no new import cycle or Lab reachability. +5. Actual line counts ≤400 (planned leaf 87, residual 351); no #b debt; source diff ≤500. +6. Unchanged fixtures and both red probes recover to green; the instantiated local/remote/CI gates are recorded against the exact L1 head. +7. Implementation scope is original file + one leaf; existing tests remain behaviorally unchanged. Any extra production file requires escalation. + +## PR + +Title: `refactor(adapters): isolate xAI schema analysis (split S05 L1/3)` + +Branch: `codex/split-adapters-xai-tool-schema`. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Closes: none. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S05-L1 | xAI tool schema | `codex/split-adapters-xai-tool-schema` | `dev` | Schema-analysis extraction | +| 2 | #TBD-S05-L2 | Command Code | `codex/split-adapters-command-code` | `dev` | Wire messages and single-owner workspace cache | +| 3 | #TBD-S05-L3 | Ollama native | `codex/split-adapters-ollama-native` | `dev` | Request compilation and response translation | + +L1 is this PR; review only its diff. Use the repository PR template's Summary, Verification and Checklist sections, copying this stack map. Parent owns PR creation, push and approval requests; merge remains forbidden. + +## P stale-check (2026-09-05, wp160) + +origin/dev 24cc558d5; xai-tool-schema.ts unchanged since 445742966 (436 lines); anchors 3/6/61/76/171/237/370 confirmed by sed. Base `dev` (S05 independent). Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-160.sWXR9l/wt` (branch `codex/split-adapters-xai-tool-schema`, base origin/dev 24cc558d5). Executor: gpt-6-astra high (Helmholtz, 01a06f34-64cf-7152-9164-276539a08103). +- Commits: ac2be8606 (move: xai-schema-analysis.ts 86 lines, zero imports; xai-tool-schema.ts 351) and 8a404cb88 (test: xai-tool-schema.test.ts +14 — lookupLocalJsonPointer identity via both paths; pairwise-disjoint truth table; leaf has no imports). Diff: 3 files, +102/−87. Production importers unchanged (core.ts, openai-chat.ts, openai-responses.ts); the test is the only new importer. +- Local gate: typecheck 0; focused 14/0; core-lab-boundary 17/0; privacy passed. +- Red-drives: (a) pairwise-disjoint forced true → overlapping-oneOf case fails (oneOf → anyOf) plus the new leaf assertion, restored 14/0; (b) lab import in leaf → boundary chain core → xai-tool-schema → xai-schema-analysis → lab/paths, restored 17/0. + +- Adversarial diff review (Popper, gpt-6-astra high, 01a06f37-65d6-7c91-b813-c364df8d01e8): VERDICT: PASS (slices exact modulo the trailing blank line, residual byte-exact, 4 exports resolve with pointer identity, leaf zero imports, test non-tautological, 3 files). +- lidge full suite at 8a404cb88: SUITE_EXIT=0, 18018 pass / 0 fail / 16 skip (/tmp/suite-split-160.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3574 (base dev, head 8a404cb88). CI rollup at record time: OPEN draft=false 8a404cb88 =1 =19 SKIPPED=1 SUCCESS=5 diff --git a/devlog/_plan/260905_now_split_train/170_adapters_command_code.md b/devlog/_plan/260905_now_split_train/170_adapters_command_code.md new file mode 100644 index 0000000000..c9f928a413 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/170_adapters_command_code.md @@ -0,0 +1,177 @@ +# 170 — S05 L2: Command Code messages and workspace + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**; C3 structural planning, scoped docs-only delegation. Read 000/001/002 S05 and lane 014's Command Code audit. No code, test runs, git mutations, or parent-owned orchestration/loop/goal operations in this task. +- Goal: retain the adapter factory and transport in `src/adapters/command-code.ts`, reducing 637 lines to an expected **395**, with independently owned wire-message and workspace leaves. +- Non-goals: changing proprietary wire semantics, canonical model IDs, effort refresh/retry, cache TTL/capacity, filesystem/git collection behavior, credentials, diagnostics, or public API. +- Context/map: `src/adapters/registry.ts:5` and four test files import the module. Existing dependencies at lines 1–15 cover crypto, git/fs, types, budget, bounded body, debug, reasoning catalog, identity, tool nudge and image parsing. Intended graph: registry → original adapter boundary → messages and workspace leaves; the workspace leaf alone → existing git/fs imports; messages → existing image/types modules. Blast radius: adapter-local. +- Chosen structural move: extract the two existing helper clusters unchanged, retain request/stream/fetch closures. Do nothing leaves 637 >400; deleting/configuring cannot remove responsibilities; reusing another provider compiler would alter wire pairing. Reject moving only workspace metadata: 637 −116 + shims remains >500. The two cohesive leaves fit the residual cap without touching transport or inventing a shared framework. +- Verifier: 002 **Per-layer gate**, instantiated below, with cache identity/eviction and wire pairing checks. +- Stop: executor records exact-head green L2 PR, never merges; drafting delegate stops after this assigned document is statically complete. +- Escalation: actual changed source lines >500, new cycles, new source oracle, non-move edits, or a leaf/residual >400. The source diff is near the ceiling; do not spend that margin on reformatting. More layers or a size exception belong to the parent. + +## Symbol inventory + +Inclusive definition ranges were extracted with an in-memory Babel TypeScript AST from `git show origin/dev:src/adapters/command-code.ts`. Basis: `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549`. Imports are documented separately below. Consumer counts are distinct files importing this exact symbol through the original path: `rg -l 'command-code' src gui/src scripts tests -g '*.ts' -g '*.tsx'`, followed by relative-path/import-name filtering. Generic text matches (for example `usage`) and unrelated same-basename modules are excluded. Private definitions have zero external import consumers. 34 definitions; 5 static importer files. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `COMMAND_CODE_MODEL_ALIASES` | const | 19–24 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `canonicalCommandCodeModelId` | function | 26–28 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `toolResultText` | function | 31–34 | no | 0 | `src/adapters/command-code-messages.ts` | +| `mediaTypeFromUrl` | function | 37–44 | no | 0 | `src/adapters/command-code-messages.ts` | +| `wireImagePart` | function | 47–51 | no | 0 | `src/adapters/command-code-messages.ts` | +| `wireMessages` | function | 67–154 | no | 0 | `src/adapters/command-code-messages.ts` | +| `visibleTools` | function | 156–168 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `toolChoiceInstruction` | function | 170–182 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `wireTools` | function | 184–190 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `currentWorkingDirectory` | function | 192–194 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `MAX_WORKSPACE_STRUCTURE_ENTRIES` | const | 197–197 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `MAX_RECENT_COMMITS` | const | 199–199 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `MAX_RECENT_COMMIT_LENGTH` | const | 201–201 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `MAX_GIT_STATUS_LENGTH` | const | 203–203 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `WORKSPACE_METADATA_TTL_MS` | const | 205–205 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `MAX_WORKSPACE_METADATA_ENTRIES` | const | 207–207 | yes | 1 | `src/adapters/command-code-workspace.ts` | +| `projectSlug` | function | 210–212 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `GitWorkspaceInfo` | interface | 214–220 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `workspaceMetadataCache` | const | 222–222 | yes | 1 | `src/adapters/command-code-workspace.ts` | +| `pruneWorkspaceMetadataCache` | function | 228–247 | yes | 1 | `src/adapters/command-code-workspace.ts` | +| `execFile` | const | 249–249 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `gitWorkspaceInfo` | function | 252–283 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `commandCodeConfig` | function | 285–311 | no | 0 | `src/adapters/command-code-workspace.ts` | +| `usage` | function | 313–327 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `eventError` | function | 329–336 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `isMissingToolResultError` | function | 345–348 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `ndjson` | function | 350–387 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `decodeEventLine` | function | 409–422 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `stripEventFrame` | function | 425–427 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `isReasoningEffortRejection` | function | 429–431 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `requestWithoutReasoningEffort` | function | 433–442 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `fetchCommandCode` | function | 444–459 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `supportedCommandCodeEffort` | function | 461–484 | no | 0 | `src/adapters/command-code.ts (residual)` | +| `createCommandCodeAdapter` | function | 486–637 | yes | 4 | `src/adapters/command-code.ts (residual)` | + +## Leaf partition + +Use the existing provider-prefixed sibling convention (`src/adapters/ollama-native-url.ts:9`, `src/adapters/kiro-thinking.ts:1`), not a convenience index. All extraction exports serve the production factory; current public cache exports keep their original path. + +| New file | Symbols | Moved slice (comments included) | Expected lines | +|---|---|---|---:| +| `src/adapters/command-code-messages.ts` | `toolResultText`, `mediaTypeFromUrl`, `wireImagePart`, `wireMessages` | 30–154 (125 lines) | 129 | +| `src/adapters/command-code-workspace.ts` | `MAX_WORKSPACE_STRUCTURE_ENTRIES`, `MAX_RECENT_COMMITS`, `MAX_RECENT_COMMIT_LENGTH`, `MAX_GIT_STATUS_LENGTH`, `WORKSPACE_METADATA_TTL_MS`, `MAX_WORKSPACE_METADATA_ENTRIES`, `projectSlug`, `GitWorkspaceInfo`, `workspaceMetadataCache`, `pruneWorkspaceMetadataCache`, `execFile`, `gitWorkspaceInfo`, `commandCodeConfig` | 196–311 (116 lines) | 120 | + +Messages leaf own imports (three lines + separator): +```ts +import type { OcxContentPart, OcxMessage } from "../types"; +import { namespacedToolName } from "../types"; +import { parseDataUrl } from "./image"; +``` +Export only `wireMessages`; preserve its nested `closePendingCalls` and the tool-result/image-carrier ordering as a single body (67–154). + +Workspace leaf own imports (three lines + separator): +```ts +import { execFile as execFileCallback } from "node:child_process"; +import { promisify } from "node:util"; +import { opendir } from "node:fs/promises"; +``` +Keep the three existing cache exports; also export `projectSlug` and `commandCodeConfig` for the residual factory. `GitWorkspaceInfo` and all other helpers/constants stay private. + +Residual keeps model aliasing, `visibleTools`, `toolChoiceInstruction`, `wireTools`, `currentWorkingDirectory`, all event/framing/fetch/reasoning helpers, and `createCommandCodeAdapter`. Remove original imports at 2, 3, 4 and 15; trim `OcxContentPart`/`OcxMessage` from the type import at 5. Keep the other dependencies unchanged. + +Line ledger using the displayed single-line imports: 637 −125 −116 −4 old import lines +3 boundary lines = **395**. New leaves 129 +120; total 644 =637 +7 net import/separator lines. No #b is required. Planned changed-source estimate: 246 deletions +253 additions =499, including the retained type-import edit; verify actual numstat before declaring ready. Additional formatting can cross the ceiling. Tests/documentation do not justify hiding the actual total review diff. + +There are no #a/#b parts in approved S05. Within this layer, the messages leaf has zero external consumers; the workspace cache API has one test consumer, so move messages first, then workspace, while publishing one independently verified layer. Do not expose intermediate >400 state as completed debt. + +## Re-export block + +The residual adds exactly: +```ts +export { MAX_WORKSPACE_METADATA_ENTRIES, workspaceMetadataCache, pruneWorkspaceMetadataCache } from "./command-code-workspace"; +import { projectSlug, commandCodeConfig } from "./command-code-workspace"; +import { wireMessages } from "./command-code-messages"; +``` +`createCommandCodeAdapter` remains inline exported at the original boundary (old 486–637). No type was previously exported. Re-exporting cache names creates no local binding; the residual does not use those three names. It needs the explicit local imports above at old call sites 503, 507 and 528. Do not import the cache back into the residual to recreate or wrap it. + +## Module-level state and cycles + +- `workspaceMetadataCache` (222): sole allocation moves to workspace. `pruneWorkspaceMetadataCache` (228–247), TTL (205), cap (207), cache lookup/insert in `gitWorkspaceInfo` (255–281) all move together. Existing tests' `clear/set/delete` operations continue to hit that same object through the re-export. +- `execFile` (249) remains initialized once by `promisify` in workspace, not per request. Workspace policy constants (197–207) stay with that owner. +- `COMMAND_CODE_MODEL_ALIASES` (19–24) remains residual, immutable by type. No top-level let, WeakMap, lock or timer. The timeout controller/timer at 445–457 are fetch-local and untouched. +- Message arrays and pending carriers (68–74) remain invocation-local. Cache lifetime stays process/module-scoped; no duplicate instance or initialization/reset hook is added. +- Lane G1 reported no cycle. Leaves import existing downstream types/image/node APIs and never import `command-code.ts`, adapter registry or one another. Moving just `commandCodeConfig` while reading the cache from the residual would form a back-edge; the full workspace cluster avoids it. +- Coupling: workspace's existing externally exposed cache is a common-state contract preserved, not widened; only one owner mutates it in production. Message/factory edge is functional. No new validation boundary or defensive checks. + +## Tests + +Direct-import list from `rg -l 'adapters/command-code"' tests -g '*.ts'` (all unchanged): +- `tests/adapters/buffered-response-shape-guards.test.ts:3` — unchanged; keep its original-path import. +- `tests/providers/command-code-error-finish.test.ts:2` — unchanged; keep its original-path import. +- `tests/providers/command-code-provider.test.ts:2` — unchanged; keep its original-path import. +- `tests/providers/command-code-workspace-cache.test.ts:2` — unchanged; keep its original-path import. + +Additional indirect gates remain unchanged: +- `tests/adapters/adapter-registry-authority.test.ts` +- `tests/adapters/adapter-tool-conformance.test.ts` +- `tests/adapters/adapter-buffered-tool-conformance.test.ts` + +Filename-specific source-text readers: **none**. The O1 search is `rg -l 'readFileSync|Bun\\.file|readFile\\(' tests -g '*.test.ts' | xargs rg -l 'adapters/command-code|command-code.ts'`, followed by source inspection, not counting OAuth/config fixture reads as adapter source readers. Generic `tests/lab/core-lab-boundary.test.ts:69` reads transitive source; unchanged, automatically visits both static leaves. No retarget-to-leaf and no add-leaf-to-scan-list; PROTECTED at line 20 stays untouched. + +During C, drive the existing eviction guard red once by temporarily disconnecting pruning from the workspace-owned Map, using `tests/providers/command-code-workspace-cache.test.ts:19` (prune call 27), then restore. Also verify original/leaf `workspaceMetadataCache` strict identity in an in-memory import check; this requires no new committed test or API. Drive the Lab guard red with a temporary leaf→Lab edge and restore. Preserve the framing tests at `tests/adapters/buffered-response-shape-guards.test.ts:172`; their NDJSON code stays in the residual, so no oracle migration. + +## Verification + +Implementation-only commands: none were run for this docs-only delegation. This instantiates `002_layer_map.md` → **Per-layer gate** (the `003` reference in 000 is stale). + +```sh +bun run typecheck +bun test tests/providers/command-code-workspace-cache.test.ts tests/providers/command-code-provider.test.ts tests/providers/command-code-error-finish.test.ts tests/adapters/buffered-response-shape-guards.test.ts tests/adapters/adapter-registry-authority.test.ts tests/adapters/adapter-tool-conformance.test.ts tests/adapters/adapter-buffered-tool-conformance.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/adapters/command-code-workspace.ts src/adapters/command-code-messages.ts src/adapters/command-code.ts +rg -l 'from "[^"]*/command-code"' src gui/src scripts tests +git diff --check +git diff --numstat origin/dev...HEAD -- src tests +``` + +Focused domains: `tests/providers` Command Code files and `tests/adapters` framing/registry/tool conformance. The original-path static importer list must retain 5 unique files after exact relative-path filtering (the raw basename rg can include unrelated modules). Keep exports/types resolvable; count alone is not proof. No protected-root edits are needed; the Lab guard is included because adapters are transitively reachable. Each listed leaf and residual must be ≤400 physical lines. Compare normalized AST bodies before/after, allowing only location, import/export modifiers and required import binding changes; preserve comments and exact error/wire literals. + +Run the resolved-relative-import/re-export graph walk from lane 014's G1, including type edges, at the layer tip; no return path from any new leaf to its old boundary or another leaf may appear. The Lab guard checks optional-subsystem reachability, not general cycles. + +Full suite is **never local**; executor uses the existing authorized remote checkout only after verifying its ownership, with pipeline failure propagation: +```sh +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-command-code && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15"' +``` +Record remote HEAD equal to PR head, full-suite exit status and totals, local focused/typecheck/privacy results, and the complete exact-head CI rollup. A tail without the test exit status is not evidence. Re-run only invalidated checks after a lower-layer cascade; no merge/auto-merge. + + +## Accept criteria + +1. All 34 definitions have one inventory owner; extracted bodies are unchanged; retained NDJSON/fetch/reasoning/factory logic is untouched. +2. Original four exports remain available; the original and leaf cache bindings are strictly identical and TTL/cap/eviction ordering match. +3. Exactly five direct importer files remain at the old path; no caller migrations, new registry or wildcard exports. +4. Two leaves ≤400 (129 and 120 expected), residual ≤400 (395 expected); no #b; measured changed source lines ≤500 or parent approval is required before execution continues. +5. No type/runtime cycle, no new Lab path, no new module-level cache/timer; request framing/abort/error behavior remains unchanged. +6. Listed behavioral tests and red→green probes pass at L2; typecheck/privacy and remote full suite plus complete CI rollup prove that exact head. +7. L2 base is L1; any lower-layer edit cascades and invalidates affected evidence. Review this layer only; do not merge. + +## PR + +Title: `refactor(adapters): isolate Command Code messages and workspace (split S05 L2/3)` + +Branch: `codex/split-adapters-command-code`. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Closes: none. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S05-L1 | xAI tool schema | `codex/split-adapters-xai-tool-schema` | `dev` | Schema-analysis extraction | +| 2 | #TBD-S05-L2 | Command Code | `codex/split-adapters-command-code` | `dev` | Wire messages and single-owner workspace cache | +| 3 | #TBD-S05-L3 | Ollama native | `codex/split-adapters-ollama-native` | `dev` | Request compilation and response translation | + +L2 is this PR; review only its diff. Fill Summary, Verification and Checklist from the repository PR template and include the stack map. Parent owns git/PR operations; this document grants no merge permission. diff --git a/devlog/_plan/260905_now_split_train/180_adapters_ollama_native.md b/devlog/_plan/260905_now_split_train/180_adapters_ollama_native.md new file mode 100644 index 0000000000..9c69779f81 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/180_adapters_ollama_native.md @@ -0,0 +1,234 @@ +# 180 — S05 L3: Ollama native request and response leaves + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**; C3 structural planning with credential-sensitive behavior preserved in place. Source basis `origin/dev:1362b1a38`; docs HEAD `4cc219549`. Inputs read: 000/001/002 S05 and lane 014's Ollama-native audit. +- Goal: split the 1,131-line adapter into four provider-local leaves, each ≤400, leaving the existing factory and header policy in an expected 151-line original module. Preserve both public exports and all wire/event/error/budget semantics. +- Non-goals: new parsing algorithms, fresh state abstractions, rewritten function bodies, observers, changed validation/credentials, request lifecycle changes, test runs/code/git mutation during this delegated drafting task, and parent-owned orchestration/loop/goal commands. +- Structural map/context: registry (`src/adapters/registry.ts:11`) and six test files consume the public factory. Original dependencies 1–38 are base/types, crypto, reasoning, bounded body, diagnostics, translator budget, redaction, image parsing and URL policy. Intended direction: existing factory → request and stream; request → values; stream → events and values; events → values. Existing upstream types/libs and URL policy remain downstream. Blast radius: one adapter feature, not the registry. +- Decision: move already separate top-level definitions; retain `buildHeaders` and the factory's request-owned state. Reject splitting by arbitrary offsets or extracting factory methods with new state arguments: existing helper seams already accept the needed state. Do nothing/configure/delete cannot meet 400 lines; borrowing Command Code's permissive NDJSON decoder would change Ollama's terminal and budget contract. +- Verifier: 002 **Per-layer gate**, instantiated below, plus public-surface/ID/budget/abort fixtures and source-body identity checks. +- Stop: after parent resolves the size contradiction, implementation stops at exact-head green L3 PR, never merges. This drafting task stops after its one assigned document is statically verified. +- **Escalation required before implementation:** 002 gives this 1,131-line file one layer and caps a layer at ≤500 changed source lines. Merely reaching 400 requires moving at least 731 source lines before shims, already >500 even if moves count once; ordinary add+delete accounting is ≥1,462. This complete four-leaf design moves 954 original physical lines. It cannot truthfully satisfy the current one-layer size gate. Parent must explicitly approve a move-only size exception or revise 002 with additional #a/#b layers and their docs/branches. This delegate does not edit 002, add a fourth S05 layer, or treat the contradiction as resolved. + +## Symbol inventory + +Inclusive definition ranges were extracted with an in-memory Babel TypeScript AST from `git show origin/dev:src/adapters/ollama-native.ts`. Basis: `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549`. Imports are documented separately below. Consumer counts are distinct files importing this exact symbol through the original path: `rg -l 'ollama-native' src gui/src scripts tests -g '*.ts' -g '*.tsx'`, followed by relative-path/import-name filtering. Generic text matches (for example `usage`) and unrelated same-basename modules are excluded. Private definitions have zero external import consumers. 48 definitions; 7 static importer files. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `OllamaNativeMessage` | interface | 41–57 | yes | 0 | `src/adapters/ollama-native-request.ts` | +| `OllamaNativeTool` | interface | 59–66 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `PendingToolCall` | interface | 68–75 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `PendingToolBatch` | interface | 77–80 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `NativeStreamToolCall` | interface | 82–91 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `NativeStreamState` | interface | 93–102 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `JsonRecord` | type | 104–104 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `NativeReadResult` | type | 105–105 | no | 0 | `src/adapters/ollama-native-stream.ts` | +| `NATIVE_THINK_VALUES` | const | 107–107 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `NATIVE_TOOL_ID_MAX_LENGTH` | const | 108–108 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `NATIVE_TOOL_ID_CONTROL` | const | 109–109 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `isRecord` | function | 111–113 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `isFiniteNonNegativeInteger` | function | 115–117 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `validNativeToolCallId` | function | 125–134 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `mintNativeToolCallId` | function | 136–143 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `allocateNativeToolCallId` | function | 145–156 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `safeNativeString` | function | 158–162 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `errorDetail` | function | 164–174 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `nativeErrorEvent` | function | 176–189 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `malformedNativeEvent` | function | 191–200 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `translationBudgetEvent` | function | 202–211 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `wireModelId` | function | 213–219 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `assertObjectArguments` | function | 221–224 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `normalizedBase64` | function | 226–236 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `imageToBase64` | function | 238–250 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `contentToNative` | function | 252–272 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `assistantTextThinkingAndCalls` | function | 274–292 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `buildNativeMessages` | function | 294–409 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `buildNativeTools` | function | 411–444 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `nativeThink` | function | 446–497 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `nativeFormat` | function | 499–520 | no | 0 | `src/adapters/ollama-native-request.ts` | +| `usageFromNative` | function | 522–528 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `stopReasonFromNative` | function | 530–534 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `nativeMessageEvents` | function | 536–605 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `flushNativeStreamToolCalls` | function | 607–620 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `replaceNativeToolArguments` | function | 622–647 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `releaseNativeStateBuffers` | function | 649–651 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `nativeBodyMessage` | function | 653–656 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `nativeEventsFromResponsePayload` | function | 658–705 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `formatNativeErrorBody` | function | 707–733 | no | 0 | `src/adapters/ollama-native-values.ts` | +| `buildHeaders` | function | 735–784 | no | 0 | `src/adapters/ollama-native.ts (residual)` | +| `replaceLiveBuffer` | function | 786–801 | no | 0 | `src/adapters/ollama-native-stream.ts` | +| `readWithAbort` | function | 803–822 | no | 0 | `src/adapters/ollama-native-stream.ts` | +| `streamState` | function | 824–833 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `processNativeLine` | function | 835–892 | no | 0 | `src/adapters/ollama-native-events.ts` | +| `parseOllamaNativeStream` | function | 894–1014 | no | 0 | `src/adapters/ollama-native-stream.ts` | +| `parseOllamaNativeResponse` | function | 1016–1044 | no | 0 | `src/adapters/ollama-native-stream.ts` | +| `createOllamaNativeAdapter` | function | 1046–1131 | yes | 7 | `src/adapters/ollama-native.ts (residual)` | + +The dynamic import at `tests/providers/ollama/ollama-native-parser.test.ts:52` is another edge in an already-counted test file, not an eighth consumer. `OllamaNativeMessage` has zero direct importers but is still a public type and must remain exported. + +## Leaf partition + +Paths reuse provider-prefixed siblings, particularly existing `src/adapters/ollama-native-url.ts:9` and `src/adapters/kiro-thinking.ts:1`. No generic utility module, new index barrel, alternate adapter registry or new dependency. Table sizes preserve comments and blank lines in the specified disjoint slices; imports below are kept on the shown physical lines, followed by one blank. + +| New file | Exact symbol ownership | Original slices including comments/blanks | Moved lines + imports | Expected lines | +|---|---|---|---|---:| +| `src/adapters/ollama-native-request.ts` | `OllamaNativeMessage`, `OllamaNativeTool`, `PendingToolCall`, `PendingToolBatch`, `NATIVE_THINK_VALUES`, `wireModelId`, `normalizedBase64`, `imageToBase64`, `contentToNative`, `assistantTextThinkingAndCalls`, `buildNativeMessages`, `buildNativeTools`, `nativeThink`, `nativeFormat` | 40–81, 107, 213–220, 226–521 | 347 +8 | 355 | +| `src/adapters/ollama-native-values.ts` | `JsonRecord`, `NATIVE_TOOL_ID_MAX_LENGTH`, `NATIVE_TOOL_ID_CONTROL`, `isRecord`, `isFiniteNonNegativeInteger`, `validNativeToolCallId`, `mintNativeToolCallId`, `allocateNativeToolCallId`, `safeNativeString`, `errorDetail`, `nativeErrorEvent`, `malformedNativeEvent`, `translationBudgetEvent`, `assertObjectArguments`, `formatNativeErrorBody` | 104, 108–212, 221–225, 707–734 | 139 +4 | 143 | +| `src/adapters/ollama-native-events.ts` | `NativeStreamToolCall`, `NativeStreamState`, `usageFromNative`, `stopReasonFromNative`, `nativeMessageEvents`, `flushNativeStreamToolCalls`, `replaceNativeToolArguments`, `releaseNativeStateBuffers`, `nativeBodyMessage`, `nativeEventsFromResponsePayload`, `streamState`, `processNativeLine` | 82–103, 522–706, 824–893 | 277 +4 | 281 | +| `src/adapters/ollama-native-stream.ts` | `NativeReadResult`, `replaceLiveBuffer`, `readWithAbort`, `parseOllamaNativeStream`, `parseOllamaNativeResponse` | 105, 786–823, 894–1045 | 191 +6 | 197 | + +Request leaf own imports: +```ts +import type { OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxToolCall } from "../types"; +import { isAllowedToolChoice, modelInList, namespacedToolName, toolChoiceToolPredicate } from "../types"; +import { configuredReasoningEfforts, isReasoningEffortOmitted, mapReasoningEffort, reasoningEffortMapFor } from "../reasoning-effort"; +import { redactSecretString } from "../lib/redact"; +import { parseDataUrl } from "./image"; +import type { OllamaNativeEndpointKind } from "./ollama-native-url"; +import { isRecord, assertObjectArguments } from "./ollama-native-values"; +``` +Public type `OllamaNativeMessage` stays exported here; export the five actual factory dependencies `wireModelId`, `buildNativeMessages`, `buildNativeTools`, `nativeThink`, `nativeFormat`. Other request definitions remain private. + +Values leaf owns existing wire-value validation, ID allocation and error projection (not cross-provider helpers). Own imports: +```ts +import { randomUUID } from "node:crypto"; +import type { AdapterEvent, OcxUsage } from "../types"; +import { redactSecretString } from "../lib/redact"; +``` +Export `JsonRecord` as a type plus `isRecord`, `isFiniteNonNegativeInteger`, `validNativeToolCallId`, `allocateNativeToolCallId`, `nativeErrorEvent`, `malformedNativeEvent`, `translationBudgetEvent`, `assertObjectArguments`, `formatNativeErrorBody` for actual leaf/factory consumers. Keep the ID constants, minting implementation, safe-string and detail readers private. + +Events leaf own imports: +```ts +import type { AdapterEvent, OcxUsage } from "../types"; +import { isTranslatorBudgetExceededError, TRANSLATOR_MAX_SSE_EVENT_BYTES, TranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget"; +import { isRecord, isFiniteNonNegativeInteger, validNativeToolCallId, allocateNativeToolCallId, assertObjectArguments, nativeErrorEvent, malformedNativeEvent, translationBudgetEvent, type JsonRecord } from "./ollama-native-values"; +``` +Export `NativeStreamState` as a type and `nativeEventsFromResponsePayload`, `releaseNativeStateBuffers`, `streamState`, `processNativeLine` for the stream leaf. `NativeStreamToolCall` stays local; no new state object/factory is invented. + +Stream leaf own imports: +```ts +import type { AdapterEvent } from "../types"; +import { readBoundedResponseBytes } from "../lib/bounded-body"; +import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, TRANSLATOR_MAX_SSE_EVENT_BYTES, TranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget"; +import { malformedNativeEvent, translationBudgetEvent } from "./ollama-native-values"; +import { nativeEventsFromResponsePayload, releaseNativeStateBuffers, streamState, processNativeLine } from "./ollama-native-events"; +``` +Export only `parseOllamaNativeStream` and `parseOllamaNativeResponse`. No imported `NativeStreamState` is needed here: `streamState` already infers it; the type is exported by its owner as part of that leaf contract. + +Residual `src/adapters/ollama-native.ts`: `buildHeaders` (735–784), `createOllamaNativeAdapter` (1046–1131), and the boundary imports below. Source-slice arithmetic: 1,131 −954 =177 retained lines; replace the old 38-line import block with the 12 lines below, retaining the old separator = **151**. Leaf total 976; aggregate 1,127 =1,131 −38 +12 +22 leaf-import/separator lines. Formatting changes require fresh counts, never dropping comments to hit a threshold. + +This complete design leaves **zero residuals over 400**; it is not a claim that the single L3 satisfies the diff-size cap. No approved #b exists in 002. If the parent chooses reslicing instead of an exception, #a should first take the zero-external-consumer values foundation, then dependent request/events/stream leaves, preserving these exact owners. The parent must assign intermediate residual counts and enough layers to satisfy measured add+delete size; do not silently publish this complete design under an incomplete #a label. + +## Re-export block + +Replace imports 1–38 with these 12 lines (including the type re-export): +```ts +import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base"; +import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types"; +import { modelInList } from "../types"; +import { modelRecordValue } from "../reasoning-effort"; +import { debugProviderDiagnostic } from "../lib/debug"; +import type { TranslatorBudget } from "../lib/translator-budget"; +import { SENSITIVE_KEY_PATTERN } from "../lib/redact"; +import { ollamaNativeChatUrl, ollamaNativeEndpointKind, type OllamaNativeEndpointKind } from "./ollama-native-url"; +import { buildNativeMessages, buildNativeTools, nativeFormat, nativeThink, wireModelId } from "./ollama-native-request"; +import { formatNativeErrorBody } from "./ollama-native-values"; +import { parseOllamaNativeStream, parseOllamaNativeResponse } from "./ollama-native-stream"; +export type { OllamaNativeMessage } from "./ollama-native-request"; +``` +`createOllamaNativeAdapter` remains inline exported. These are the entire original public surface (one factory, one type); no value re-export is needed because the factory stays. Explicit value imports bind the functions used in that factory at old 1052, 1060–1062, 1088–1090 and 1113–1128. Re-exporting the type does not create a local binding, and the residual does not use that type. + +## Module-level state and cycles + +- `NATIVE_THINK_VALUES` (107), the only top-level Set, moves once to request; no mutation is added. `NATIVE_TOOL_ID_MAX_LENGTH` (108) and `NATIVE_TOOL_ID_CONTROL` (109) move once to values. The regex has no global/sticky flag and no new shared mutable state is introduced. +- No top-level let, Map, WeakMap, timer or lock. `NativeStreamState.toolCalls` is a type member (94), not an allocation. +- Factory closure stays intact: `requestAbortSignal` (1047), `requestAllowsParallelToolCalls` (1048), `issuedToolCallIds` Set (1049). `buildNativeMessages` still clears/reserves that same Set (307, 372); both response paths receive it (1117, 1126). No per-leaf or process-global substitute. +- State maps at 668 and 826 remain per parse invocation, with creation owned by events. Pending batch maps at 401 remain per message compilation in request. Live reader, decoder, residual, cancellation and abort-listener state (803–821, 905–912) stays invocation-local in stream; retain all finally/release ordering. +- Intended DAG: boundary → request/stream/values; request → values; stream → events/values; events → values. No leaf imports the boundary. Request-only types live in request, parser types in events, shared `JsonRecord` in values, and `NativeReadResult` in stream. Keeping shared predicates in the residual would create request↔boundary and events↔boundary cycles; moving them to values avoids those. +- Existing lane G1 found no cycle. Re-run its resolved graph including type edges after extraction; existing dependencies retain their direction. No lazy import escape hatch, new Lab edge or convenience barrel. +- Coupling: provider external-format coupling stays contained; explicit Set/budget parameters preserve existing temporal contract. Moving definitions does not authorize duplicating state, passing callbacks to break a cycle, or inventing an observer API. + +## Tests + +Exact direct static-import file list from `rg -l 'adapters/ollama-native"' tests -g '*.ts'`: +- `tests/providers/ollama/ollama-show-enrichment-v7.test.ts:371` — unchanged; keep its original-path import. +- `tests/providers/ollama/ollama-native-v4.test.ts:2` — unchanged; keep its original-path import. +- `tests/providers/ollama/ollama-native-parser.test.ts:2` — unchanged; keep its original-path import. +- `tests/providers/ollama/ollama-native-reasoning-wire.test.ts:2` — unchanged; keep its original-path import. +- `tests/providers/ollama/ollama-native-structured-output.test.ts:2` — unchanged; keep its original-path import. +- `tests/providers/ollama/ollama-native.test.ts:2` — unchanged; keep its original-path import. + +`tests/providers/ollama/ollama-native-parser.test.ts:52` dynamically imports the original module to check absence of observation machinery; unchanged. This is a runtime module-surface guard, **not** a source-text reader. Do not retarget it to a leaf. + +Additional unchanged indirect gates: `tests/adapters/adapter-registry-authority.test.ts`, `tests/adapters/adapter-tool-conformance.test.ts`, `tests/adapters/adapter-buffered-tool-conformance.test.ts`. + +Filename-specific source-text readers: **none** in O1 basename/path + readFileSync/Bun.file/readFile search. Generic source oracle: `tests/lab/core-lab-boundary.test.ts:69` reads transitive source and will automatically traverse all static leaf edges — unchanged. No retarget-to-leaf or add-leaf-to-scan-list. Preserve its PROTECTED roots at line 20. + +Drive guards red once during implementation and restore before final verification: +1. Existing observer-free guard at parser.test.ts:50–58: temporarily export the prohibited observation-sink name from the residual; the dynamic-import guard must fail. Remove the temporary export. +2. Budget/EOF parity: temporarily release the EOF residual before `processNativeLine` (old 974); `tests/providers/ollama/ollama-native-v4.test.ts:28` must fail. Restore the exact accounting order. +3. Temporarily import a Lab module from a new leaf; the generic transitive guard must report the path. Restore without touching PROTECTED or weakening the scan. + +Keep tool ID reuse/parallel policy, done:true validation, structured-output, reasoning omission, remote-image refusal, and transport/header tests through the public factory. Do not move credential policy out of `buildHeaders`, nor lower limits to make memory tests cheaper. Red runs happen only in the implementation worktree, not this drafting task. + +## Verification + +Implementation-only commands: none were run for this docs-only delegation. This instantiates `002_layer_map.md` → **Per-layer gate** (the `003` reference in 000 is stale). + +```sh +bun run typecheck +bun test tests/providers/ollama/ollama-native.test.ts tests/providers/ollama/ollama-native-parser.test.ts tests/providers/ollama/ollama-native-v4.test.ts tests/providers/ollama/ollama-native-reasoning-wire.test.ts tests/providers/ollama/ollama-native-structured-output.test.ts tests/providers/ollama/ollama-show-enrichment-v7.test.ts tests/adapters/adapter-registry-authority.test.ts tests/adapters/adapter-tool-conformance.test.ts tests/adapters/adapter-buffered-tool-conformance.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/adapters/ollama-native-request.ts src/adapters/ollama-native-values.ts src/adapters/ollama-native-events.ts src/adapters/ollama-native-stream.ts src/adapters/ollama-native.ts +rg -l 'from "[^"]*/ollama-native"' src gui/src scripts tests +git diff --check +git diff --numstat origin/dev...HEAD -- src tests +``` + +Focused domains: `tests/providers/ollama` and `tests/adapters` registry/tool conformance. The original-path static importer list must retain 7 unique files after exact relative-path filtering (the raw basename rg can include unrelated modules). Keep exports/types resolvable; count alone is not proof. No protected-root edits are needed; the Lab guard is included because adapters are transitively reachable. Each listed leaf and residual must be ≤400 physical lines. Compare normalized AST bodies before/after, allowing only location, import/export modifiers and required import binding changes; preserve comments and exact error/wire literals. + +Run the resolved-relative-import/re-export graph walk from lane 014's G1, including type edges, at the layer tip; no return path from any new leaf to its old boundary or another leaf may appear. The Lab guard checks optional-subsystem reachability, not general cycles. + +Full suite is **never local**; executor uses the existing authorized remote checkout only after verifying its ownership, with pipeline failure propagation: +```sh +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-adapters-ollama-native && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15"' +``` +Record remote HEAD equal to PR head, full-suite exit status and totals, local focused/typecheck/privacy results, and the complete exact-head CI rollup. A tail without the test exit status is not evidence. Re-run only invalidated checks after a lower-layer cascade; no merge/auto-merge. + + +Before these commands, resolve the one-layer/500-line contradiction in Loop spec; a passing suite is not a size-gate waiver. Preserve `buildHeaders` and URL policy ASTs exactly, and obtain the explicit security review required by `MAINTAINERS.md` if the actual implementation diff touches credential handling. No new dependency or general-purpose cycle checker installation is authorized. + +## Accept criteria + +1. Parent records either an explicit pure-move size exception for this L3 or an approved updated layer map with #a/#b ownership/residual accounting; without it, this plan is **blocked for implementation**, not ready. +2. All 48 definitions have exactly one owner; moved bodies/signatures/default arguments equal origin/dev; no new observer or validation behavior. +3. `createOllamaNativeAdapter` and `OllamaNativeMessage` remain importable from the original path; all seven static consumer files and the existing dynamic surface guard remain valid. +4. Four leaves ≤400 (355, 143, 281, 197 expected), residual ≤400 (151 expected); total line arithmetic is consistent and no residual debt is silently deferred. +5. Factory owns the single issued-ID Set and abort/parallel state; parser maps/readers/budget reservations keep original lifetimes and cleanup order. +6. No runtime/type cycle or new Lab reachability; no protected-root edits; credential/URL policy unchanged. +7. All focused fixtures and restored red probes, typecheck, privacy, remote full suite and complete CI rollup pass at the exact resolved layer head. +8. Branch/base and stack map reflect the parent-approved topology; no merge, auto-merge, unrelated source changes or code edits on the docs worktree. + +## PR + +Title: `refactor(adapters): isolate Ollama native request and response translation (split S05 L3/3)` + +Branch: `codex/split-adapters-ollama-native`. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Closes: none. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S05-L1 | xAI tool schema | `codex/split-adapters-xai-tool-schema` | `dev` | Schema-analysis extraction | +| 2 | #TBD-S05-L2 | Command Code | `codex/split-adapters-command-code` | `dev` | Wire messages and single-owner workspace cache | +| 3 | #TBD-S05-L3 | Ollama native | `codex/split-adapters-ollama-native` | `dev` | Request compilation and response translation | + +L3 is this PR. This is the assigned three-layer map, not an invented approval to exceed its size gate. Parent must reconcile it before PR publication if choosing reslicing. Fill the repository Summary, Verification and Checklist template sections with exact-head evidence and the resolved stack map; review only this layer's diff. diff --git a/devlog/_plan/260905_now_split_train/190_vision_index.md b/devlog/_plan/260905_now_split_train/190_vision_index.md new file mode 100644 index 0000000000..a3a70bb1b6 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/190_vision_index.md @@ -0,0 +1,283 @@ +# 190 — S06 L1/2: vision planning and image rewriting + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. C3 architecture planning, delegated docs-only; the parent owns phase/goal/orchestration. This document is not implementation or verification evidence. +- Goal: reduce `src/vision/index.ts` from 667 lines to at most 390 while preserving every original export, cache identity, auth-selection order, caption ordering, and raw Responses alignment. +- Non-goals: no new cache API, backend changes, model/default changes, validation changes, cache fixes, function-body cleanup, consumer migrations, new tests/tooling, merge, or release. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Stop when the implementation layer has its own passing gates and an open exact-head-green PR; never merge. +- Escalation: stop implementation for changed source basis, an uncovered oracle, a required behavior change, cycle, leaf/residual >400, or scope expansion. Report security findings only in ignored scratch, not here. This bounded drafting task writes only this document and `200_images_artifacts.md` and runs no tests or git mutations. +- Sizing conflict for parent resolution: 667→400 alone requires moving at least 267 lines, hence at least 534 raw added+deleted lines before wiring. The selected 287-line move is a documented exception proposal to cxc-dev's DEFAULT 500-line PR threshold, **not** a claim that it satisfies 002's ≤500 changed-source-line wording. Parent must accept a move-aware sizing exception or amend the topology with another layer before execution. No unassigned `#b` is silently introduced. + +Basis: docs HEAD `4cc219549`; `origin/dev` `1362b1a3841b4de20177e5d65865a513dd7936c4`. All source/test line citations are at that code basis. `git diff origin/dev -- src/vision/index.ts src/images/artifacts.ts` was empty. Read `000_plan.md`, `001_stale_check.md`, S06 rows and gate in `002_layer_map.md`, and `devlog/_plan/260905_modular_debt_ledger/014_lane_adapters_media.md` (the `src/vision/index.ts` section). The lane identifies the cache at :64–161, planner at :292–370, description execution at :536–637, and fallback at :645–667. Its cache-first suggestion is not mandatory: keeping cache plus executor together avoids exposing a mutable singleton merely to split it. + +Structural decision: current callers → `vision/index.ts` → eligibility, sidecar auth, reasoning, describe transports, memory budget. Intended callers → same boundary → `plan.ts` / `image-rewrite.ts`; `plan.ts` → `image-rewrite.ts`, eligibility, reasoning, auth; execution/cache remain in the boundary. Blast radius is one feature, with existing server and management clients unchanged. Direct source callers are `src/lib/app-owned-memory-stores.ts:24`, `src/server/chat-native.ts:20`, `src/server/management/config-routes.ts:67`, `src/server/management/vision-sidecar-options.ts:11`, `src/server/responses/{collaboration,compact,core,encrypted-payload}.ts:42/41/143/40`, and `src/web-search/index.ts:3`. + +Rejected alternatives: doing nothing/configuration/deletion cannot remove this structural debt while preserving behavior; a cache leaf with an exported mutable cache would split ownership; moving all execution first creates unnecessary state seams. Reuse existing eligibility, reasoning, timeout and describe owners. Searches for `planVisionSidecar`, `stripImagesInPlace`, `carriesImages`, and `syncRawBodyImageDescriptions` find their implementation only in this file. Sibling convention: `src/vision/{eligibility,reasoning,timeout-bounds}.ts` and `src/images/plan.ts`; no new convenience barrel. The existing public boundary intentionally retains logic plus named compatibility exports, as explicitly required by the train; do not turn this exception into a new internal barrel. + +## Symbol inventory + +Ranges include the declaration/export keyword through its closing token, excluding preceding comments. Enumerated with the installed Babel TypeScript parser over `git show origin/dev:src/vision/index.ts`, cross-checked against numbered source and `rg`. `consumers` is distinct external files using that symbol through this original boundary, counted with `rg -l -w ''` over the resolved importer list and checked against import bindings; private symbols have 0. Namespace/data strings and imports directly from `eligibility.ts` do not count. Boundary fan-in: **24 files = 9 source + 15 tests**. Leaf abbreviations: P=`src/vision/plan.ts`, R=`src/vision/image-rewrite.ts`, I=residual `src/vision/index.ts`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| DEFAULT_VISION_MODEL | const | 42–42 | no | 0 | P | +| DEFAULT_ANTHROPIC_VISION_MODEL | const | 43–43 | no | 0 | P | +| DEFAULT_REASONING | const | 44–44 | no | 0 | P | +| DEFAULT_MAX_DESCRIPTIONS_PER_TURN | const | 45–45 | yes | 1 | P | +| DESCRIPTION_CACHE_MAX_ENTRIES | const | 46–46 | no | 0 | I | +| VISION_DESCRIPTION_CACHE_MAX_BYTES | const | 47–47 | yes | 1 | I | +| descriptionEncoder | const TextEncoder | 48–48 | no | 0 | R (internal named export) | +| VISION_CONCURRENCY | const | 50–50 | no | 0 | I | +| DESC_MAX_CHARS | const | 52–52 | no | 0 | I | +| CONTEXT_MAX_CHARS | const | 54–54 | no | 0 | I | +| VisionDescriptionCache | interface | 56–62 | yes | 0 | I | +| BoundedLruDescriptionCache | class | 64–116 | no | 0 | I | +| descriptionCacheLimits | let | 118–121 | no | 0 | I | +| defaultDescriptionCache | function | 123–125 | no | 0 | I | +| descriptionCache | let | 127–127 | no | 0 | I | +| setVisionDescriptionCache | function | 130–132 | yes | 1 | I | +| resetVisionDescriptionCache | function | 134–136 | yes | 3 | I | +| setVisionDescriptionCacheLimitsForTests | function | 138–145 | yes | 1 | I | +| visionDescriptionRetainedStoreSnapshot | function | 147–157 | yes | 2 | I | +| evictOldestVisionDescriptionForBudget | function | 159–161 | yes | 2 | I | +| resolveMaxDescriptionsPerTurn | function | 164–169 | yes | 3 | P | +| isValidVisionTimeoutMs | function | 171–176 | yes | 1 | P | +| resolveVisionTimeoutMs | function | 179–181 | yes | 3 | P | +| runBounded | async function | 184–195 | no | 0 | I | +| clamp | function | 197–199 | no | 0 | I | +| AnthropicVisionProvider | interface | 201–204 | yes | 1 | P | +| findAnthropicVisionProvider | function | 210–214 | yes | 3 | P | +| resolveVisionBackend | function | 216–226 | yes | 1 | P | +| resolveOpenAiVisionModel | function | 229–234 | yes | 2 | P | +| resolveEffectiveVisionModel | function | 237–250 | yes | 1 | P | +| carriesImages | function | 253–255 | no | 0 | R (internal named export) | +| messagesHaveImage | function | 257–260 | no | 0 | P | +| shouldResolveOpenAiVisionSidecar | function | 262–272 | yes | 5 | P | +| VisionPlan | interface | 274–284 | yes | 2 | P | +| planVisionSidecar | function | 292–370 | yes | 7 | P | +| ImageJob | interface | 372–376 | no | 0 | I | +| renderDescription | function | 379–386 | no | 0 | I | +| IMAGE_OMITTED_TEXT | const | 388–388 | no | 0 | R | +| isPlainRecord | function | 390–392 | no | 0 | R | +| syncRawBodyImageDescriptions | function | 404–452 | no | 0 | R (internal named export) | +| sha256 | function | 454–456 | no | 0 | I | +| normalizedContext | function | 458–460 | no | 0 | I | +| descriptionIdentity | function | 462–483 | no | 0 | I | +| executeDescription | async function | 485–528 | no | 0 | I | +| describeImagesInPlace | async function | 536–637 | yes | 6 | I | +| stripImagesInPlace | function | 645–667 | yes | 6 | R | + +Existing re-export declarations are part of the contract, not new implementations: + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| describeImage | re-export | 21–21 | yes | 0 | existing ./describe | +| isModelTextOnly | aliased re-export | 24–24 | yes | 6 | existing ./eligibility | +| describeImageAnthropic, parseAnthropicVisionSSE | re-exports | 25–25 | yes | 1 each | existing ./anthropic-describe | +| BASELINE_VISION_MODELS, isModelVisionSidecarConsumer, isVisionEligibleModel, isVisionSidecarConsumer, modelAcceptsImageInput, visionBackendForCandidate, visionEligibleModelOptions | re-exports | 26–34 | yes | 0 each through index | existing ./eligibility | +| VisionCandidateModel, VisionModelOption, VisionSidecarBackend | type re-exports | 35–35 | yes | 0 each through index | existing ./eligibility | +| DEFAULT_VISION_TIMEOUT_MS | imported export | 36–40 | yes | 3 | existing ./timeout-bounds | +| MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS | imported exports | 36–40 | yes | 4 each | existing ./timeout-bounds | + +Import-declaration accounting (imported bindings are not definitions): :1 `createHash` stays I; :2 core types split between I/P/R; :3 `VisionReasoningEffort` goes P; :4 `describeImage`/`DescribeOutcome` stay I, `VisionSettings` goes P; :5–6 describe transports stay I; :7 eligibility and :8 reasoning go P; :9 unused `CodexAuthContext` is retained in I to avoid unrelated cleanup; :10 auth and :11 forward-sidecar type go P; :12 outcome-recorder, :13 memory-budget and :14 translator-budget stay I (R also needs the translator-budget type); :15–19 timeout imports go P, with direct named re-exports in I. + +## Leaf partition + +1. **`src/vision/plan.ts` — expected ≤210 lines.** Owns exactly P rows. Move original :42–45, :163–181, :201–250, :257–370, preserving comments and bodies: **187 original lines**, plus ≤23 import/separator lines. `messagesHaveImage` stays private; no duplicate type definition. Own imports: + + ```ts + import type { OcxConfig, OcxContentPart, OcxParsedRequest, OcxProviderConfig } from "../types"; + import type { VisionReasoningEffort } from "../reasoning-effort"; + import type { VisionSettings } from "./describe"; + import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar"; + import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility"; + import { normalizeVisionReasoningForModel } from "./reasoning"; + import { resolveSidecarAuth } from "../sidecar/auth"; + import { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds"; + import { carriesImages } from "./image-rewrite"; + ``` + +2. **`src/vision/image-rewrite.ts` — expected ≤110 lines.** Owns exactly R rows. Move :48, :252–256, :388–452, :639–667: **100 original lines**, plus ≤10 import/separator lines. `IMAGE_OMITTED_TEXT` and `isPlainRecord` remain private. Export `descriptionEncoder`, `carriesImages`, and `syncRawBodyImageDescriptions` only from this leaf for real internal consumers, not through the public boundary. Own imports: + + ```ts + import type { OcxContentPart, OcxParsedRequest, OcxTextContent } from "../types"; + import type { TranslatorBudget } from "../lib/translator-budget"; + ``` + +3. **Residual `src/vision/index.ts` — expected ≤390 lines.** Exactly I rows remain, including all cache/execution ownership. Arithmetic: 667 − 187 − 100 = 380 original lines; replace original :1–40 header with at most 50 lines of imports and compatibility exports → ≤390. No `#b` is needed for residual size. Move comments with their owners; do not collapse body formatting to hit the bound. Expected combined maximum is 710 = 390 + 210 + 110; the ≤43 net extra lines are import/export/separator allowance, not duplicated logic. + +In-memory physical-line accounting using these exact ranges and the import/export blocks below produced **P=200, R=106, residual=381** (687 total = 667 + 20 wiring lines). The larger bounds above leave formatting room; they are not measured implementation results. Adding `export` to the three cross-leaf helper declarations changes no line count. + +## Re-export block + +Exact compatibility exports in `src/vision/index.ts` (existing local exported declarations in I remain untouched): + +```ts +export { describeImage } from "./describe"; +export { isModelVisionSidecarConsumer as isModelTextOnly } from "./eligibility"; +export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe"; +export { + BASELINE_VISION_MODELS, + isModelVisionSidecarConsumer, + isVisionEligibleModel, + isVisionSidecarConsumer, + modelAcceptsImageInput, + visionBackendForCandidate, + visionEligibleModelOptions, +} from "./eligibility"; +export type { VisionCandidateModel, VisionModelOption, VisionSidecarBackend } from "./eligibility"; +export { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds"; +export { + DEFAULT_MAX_DESCRIPTIONS_PER_TURN, + resolveMaxDescriptionsPerTurn, + isValidVisionTimeoutMs, + resolveVisionTimeoutMs, + findAnthropicVisionProvider, + resolveVisionBackend, + resolveOpenAiVisionModel, + resolveEffectiveVisionModel, + shouldResolveOpenAiVisionSidecar, + planVisionSidecar, +} from "./plan"; +export type { AnthropicVisionProvider, VisionPlan } from "./plan"; +export { stripImagesInPlace } from "./image-rewrite"; +``` + +Re-exports bind nothing locally. Replacement residual imports, including the existing dependencies still needed by I: + +```ts +import { createHash } from "node:crypto"; +import type { OcxContentPart, OcxMessage, OcxParsedRequest, OcxTextContent } from "../types"; +import { describeImage, type DescribeOutcome } from "./describe"; +import { describeImageAnthropic } from "./anthropic-describe"; +import { describeImageRouted } from "./routed-describe"; +import type { CodexAuthContext } from "../codex/auth-context"; +import type { SidecarOutcomeRecorder } from "../web-search/executor"; +import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory"; +import type { TranslatorBudget } from "../lib/translator-budget"; +import type { VisionPlan } from "./plan"; +import { carriesImages, descriptionEncoder, syncRawBodyImageDescriptions } from "./image-rewrite"; +``` + +## Module-level state and cycles + +- `descriptionCacheLimits` (:118–121) and `descriptionCache` (:127) have exactly one owner: residual I, alongside setters (:130–145), snapshots/eviction (:147–161), and reads/writes (:572, :605). No new getter, exported live mutable binding, cache copy, or closure snapshot. +- `BoundedLruDescriptionCache.entries` (:65) and `.bytes` (:66) are instance fields, not module-level Maps; ownership remains in I. Default construction still occurs once at module load. `descriptionEncoder` (:48) moves once to R; I uses the same stateless encoder object for cache accounting (:85) and transient reservations (:628), while strip uses it inside R (:657). +- All other top-level consts are scalar policy values except `IMAGE_OMITTED_TEXT` (string); no top-level Set, WeakMap, lock, timer or other mutable collection exists. The `inFlight` Map (:565), counters and resolver closures remain request-local in `describeImagesInPlace`. +- Graph: I→P; I→R; P→R; R→types/translator-budget (type-only). Never R→P/I or P→I, even for types. P imports `VisionSettings` directly from `describe`, not I. Auth and reasoning are called at the same places in the moved planner; import relocation must not add auth reads at module load. +- Lane G1 found no cycle for this module. During implementation re-run the lane's in-memory resolved import-graph walk (including type edges) on I/P/R and require no return path; no new dependency installer or generated graph file. The new edge is functional; plan→rewrite uses the existing role predicate. Cache temporal coupling stays co-located, and raw-message synchronization remains sequentially after replacement. + +## Tests + +Exact direct-import `rg -l` list (all **unchanged**, original `../../src/vision` import path retained): + +```text +tests/claude-integration/claude-sidecar-override.test.ts +tests/cli/cli-models.test.ts +tests/codex-integration/app-owned-memory.test.ts +tests/gui/vision-sidecar-timeout-bounds.test.ts +tests/providers/nvidia-nim-hardening.test.ts +tests/routing/routing-capability-model-matching.test.ts +tests/vision/sidecar-auth.test.ts +tests/vision/sidecar-settings-vision-controls.test.ts +tests/vision/vision-anthropic.test.ts +tests/vision/vision-cache.test.ts +tests/vision/vision-fail-closed.test.ts +tests/vision/vision-reasoning-contract.test.ts +tests/vision/vision-routed.test.ts +tests/vision/vision-sidecar-e2e.test.ts +tests/vision/vision-text-only-predicate.test.ts +``` + +Reproduce with `rg -l 'from "[^" ]*/vision(/index)?(\.ts)?"' tests -g '*.test.ts' | sort`. Search full target paths and split path segments separately for source readers. **No target-specific body-text oracle was found**; `001`'s broad `index.ts` count of 47 is not 47 readers of this file. `tests/routing/routing-capability-model-matching.test.ts:14` is a source-location comment, not a source read. Reads in vision-reasoning-contract :177/:183 and sidecar-settings-vision-controls :65 are generated config JSON, not this module. + +Recursive source oracles that DO read this file, with exact read sites: + +| test | read site | disposition | +|---|---|---| +| `tests/lab/core-lab-boundary.test.ts` | :69 `readFileSync(current, "utf8")`; reached via `src/server/responses/core.ts:143` | unchanged; named imports/re-exports automatically include both new leaves; never edit PROTECTED (:20–27) | +| `tests/codex-integration/codex-history-reachability.test.ts` | :100 import scan and :114 mutator scan; recursive source enumeration :54–61 | unchanged; both leaves automatically scanned; no allowlist expansion | +| `tests/windows/windows-popup-fix.test.ts` | :139 `readFileSync(file, "utf8")`; recursive runtime enumeration :121–129 | unchanged; both leaves automatically scanned | + +There is no explicit scan list to extend and no retarget-to-leaf operation. Before/after implementation confirm these walkers actually include the new paths, rather than accepting an empty match set. Parent correction required outside this task's write scope: `002_layer_map.md`'s S06 thesis still says “47 text oracles retargeted”; replace that with zero target-specific retargets and the three recursive guards above after accepting this inventory. + +Guards to drive red once during C (not during this drafting task): change R's `carriesImages` to exclude `user` and require `vision-fail-closed.test.ts:18` to fail; temporarily suppress raw synchronization and require the raw-body cases in `vision-sidecar-e2e.test.ts` to fail; change P's cap resolver to lose explicit zero and require `vision-cache.test.ts:133` to fail. For the transitive boundary guard, temporarily add a direct Lab import to R and require core-lab-boundary's :284 case to fail, then restore it without changing PROTECTED. Never commit fault injections. Existing cache identity/LRU cases :166/:323/:347 stay unchanged. + +## Verification + +Draft validation actually performed: a read-only `bun -e` parser check matched all **46** definition rows against original start/end lines, confirmed the nine required headings in order, parsed every TypeScript snippet, and counted the proposed partition entirely in memory. `git diff --no-index --check /dev/null devlog/_plan/260905_now_split_train/190_vision_index.md` reported no whitespace errors. These are documentation checks, not tests or typechecking. + +Future executor only; **no commands below were run by this docs task**. Execute at this layer's exact tip, not L2's tip: + +```sh +bun run typecheck +bun test tests/vision +bun test tests/claude-integration/claude-sidecar-override.test.ts tests/cli/cli-models.test.ts tests/codex-integration/app-owned-memory.test.ts tests/gui/vision-sidecar-timeout-bounds.test.ts tests/providers/nvidia-nim-hardening.test.ts tests/routing/routing-capability-model-matching.test.ts +bun test tests/lab/core-lab-boundary.test.ts tests/codex-integration/codex-history-reachability.test.ts tests/windows/windows-popup-fix.test.ts +bun run privacy:scan +wc -l src/vision/plan.ts src/vision/image-rewrite.ts src/vision/index.ts +rg -l 'from "[^" ]*/vision(/index)?(\.ts)?"' src gui/src scripts tests -g '*.ts' -g '*.tsx' | sort +``` + +The final importer list must remain the same 24 files; symbol-import sets and all preexisting named/type exports must remain identical. Typecheck proves bindings resolve, not that unused public exports were preserved: compare the original AST export inventory against the post-split boundary explicitly. Run the lane G1 graph walk and archive zero return paths for all three owned modules. Core-lab testing is included despite no protected-file edit because `core.ts` already reaches these leaves. No GUI changes, so no GUI build/visual work is added. + +Full suite only on `lidge`, with the branch and exact fetched SHA recorded. Use pipefail and retain the suite log so `tail` cannot hide test failure (002's abbreviated pipeline alone does not preserve it): + +```sh +ssh lidge 'bash -lc '\''set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-vision-index && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tee /tmp/ocx-S06-L1-tests.log | tail -15'\''' +``` + +Require exit 0, zero failures, full log inspection, and equality with the PR head SHA. Parent coordinates exclusive use of that remote checkout. Before ready-for-review record exact-head CI rollup plus the focused results, privacy result, line counts, graph and public-export comparison. No local full suite. + +## Accept criteria + +1. Source basis is rechecked before moving; changes are confined to I, P, R and authorized layer documentation. Existing callers and tests retain import paths. +2. Every declaration above has exactly one owner; no body, signature, default, error text, cache key or authorization predicate changes. +3. New files are ≤210/110 and residual ≤390 (hard maximum 400 for every file), measured with `wc -l`; all 287 moved original lines are accounted for once. +4. All original runtime/type exports—including eligibility aliases and unused public symbols—remain importable from `src/vision`; internal encoder/predicate/sync exports are not added to that boundary. +5. Cache reset/eviction/insertion share the original singleton; raw `_rawBody` and parsed image replacement tests pass; no new module cycle or Lab reachability. +6. All three recursive source guards scan both leaves unchanged; specified fault injections fail their guards once and restored code passes. +7. Every per-layer gate above passes at the layer head, remote suite SHA matches PR head, and no merge/release occurs. +8. Parent resolves the >500 raw-line sizing conflict before execution; a changed topology requires a revised plan, not an implicit exception to 002. + +## PR + +Title: `refactor(vision): separate planning and image rewriting (split S06 L1/2)` + +Branch: `codex/split-vision-index`. Base: `dev`. Closes: none. + +Fill the repository PR template's Summary, Verification and Checklist. Include this full DEV-STACK-03 map; placeholders are for future PR numbers only: + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 2 | #TBD-S06-L2 | images artifacts | codex/split-images-artifacts | dev | storage/HTTPS leaves, original artifact API | +| 1 | #TBD-S06-L1 | vision ← you are here | codex/split-vision-index | dev | planning/rewrite leaves, co-located cache | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Review only this layer's diff. S06 groups execution order and PR navigation under `003_parent_decisions.md` STACK-INDEPENDENCE-01; both layers are independent PRs against dev. This train stops with open PRs and never merges. + +## P stale-check (2026-09-05, wp190) + +origin/dev 24cc558d5; src/vision/index.ts unchanged since 445742966 (667 lines); anchors 42/45/48/118/127/163/181/201/250/252/256/257/370/388/452/639/667 confirmed by sed. Base `dev` (S06 independent; 003 S06-ORACLE-01 already applied to 002). Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change. + +## A amendment (Arendt audit, GO-WITH-FIXES blockers=2 → folded) + +1. Sizing: the Loop-spec escalation and Accept-criteria wording about a 500-line raw cap are void; the binding gate is 003 PURE-MOVE-SIZE-01 (non-move diff ≤150; move-aware diff + exactly-once symbol inventory as evidence). Audit measured ≤100 non-move lines for this layer. +2. Test change: "all tests unchanged" applies to existing assertions and import paths only. This layer, like every layer in the train, extends one existing focused test (tests/vision/vision-cache.test.ts) with a seam-identity + zero-cycle guard so the CI hygiene rule `missing_regression_test` passes; that is the authorized test-change scope. No new test file, no layout-manifest edit. +Audit-verified structure: 46/46 declaration ranges, 17/17 header exports, P=187/R=100/I=340 partition covering 41–667 once, 38 boundary exports preserved, P imports exactly 15 bindings, R exactly 4, residual replacement imports cover all 16 used bindings (CodexAuthContext retained, pre-existing unused), no return path in a 347-module walk incl. type edges. Red-drive targets confirmed: vision-fail-closed:18, vision-sidecar-e2e:138/:228, vision-cache:133, core-lab-boundary:284 (needs a runtime Lab import in R). + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-190.whV1dc/wt` (branch `codex/split-vision-index`, base origin/dev 24cc558d5). Executor: gpt-6-astra high (Linnaeus, 01a06f41-7f42-73e1-b715-b9bf2a0ed5bb). +- Commits: d39ee6ee1 (move: plan.ts 200, image-rewrite.ts 106, index.ts 381) and 51f5a82d7 (test: vision-cache.test.ts +13 — seam identity for resolveMaxDescriptionsPerTurn and stripImagesInPlace via both paths; leaves have no back-edge to index/plan). Diff: 4 files, +338/−305. 24 original-path importers unchanged; 38 boundary exports preserved. +- Local gate: typecheck 0; guards (core-lab-boundary, codex-history-reachability, windows-popup-fix) 27/0; privacy passed. Focused tests/vision + 6 importers: 254 pass / 2 fail. The 2 failures (sidecar-settings-vision-filter "10. GET exposes only catalog rows…" and vision-reasoning-contract "native management rows expose vision-safe reasoning ladders") are **pre-existing and environment/order-dependent**: main agent reproduced the identical 2 failures on a pristine origin/dev worktree running `tests/vision` together (170 pass / 2 fail), while both files pass 19/0 when run alone on either tree. Not caused by this layer; lidge full suite is the arbiter. tests/gui/vision-sidecar-timeout-bounds.test.ts errors locally only because the backend node_modules symlink has no react (GUI deps). +- Red-drives: (a) carriesImages excludes user → vision-fail-closed:20 fails, restored 2/0; (b) resolveMaxDescriptionsPerTurn loses 0 → vision-cache:134 fails (8 vs 0), restored 15/0; (c) runtime lab import in image-rewrite → core-lab-boundary:288 chain core → vision/index → image-rewrite → lab/paths, restored 17/0. + +- Adversarial diff review (Gibbs, gpt-6-astra high, 01a06f46-ab23-7053-bcfa-b7ff0256810e): VERDICT: PASS (slices byte-exact, residual exact, 38/38 exports incl. 6 types and the isModelTextOnly alias, 348-module graph zero return paths, cache state single-owned, non-move ≤119). Qualification recorded: sidecar-settings-vision-filter case 10 does call the moved-but-byte-identical findAnthropicVisionProvider (plan.ts:45) via config-routes.ts:117; vision-reasoning-contract's ladder case does not touch moved code. Both failures are order-dependent on pristine dev and absent on the full remote suite. +- lidge full suite at 51f5a82d7: SUITE_EXIT=0, 18018 pass / 0 fail / 16 skip (/tmp/suite-split-190.log) — the arbiter for the two local order-dependent failures. +- PR: https://github.com/lidge-jun/opencodex/pull/3577 (base dev, head 51f5a82d7). CI rollup at record time: OPEN draft=false 51f5a82d7 =1 =5 SKIPPED=2 SUCCESS=21 diff --git a/devlog/_plan/260905_now_split_train/200_images_artifacts.md b/devlog/_plan/260905_now_split_train/200_images_artifacts.md new file mode 100644 index 0000000000..d1397734c3 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/200_images_artifacts.md @@ -0,0 +1,220 @@ +# 200 — S06 L2/2: artifact storage and pinned transfer + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. C3 structural planning; future implementation needs explicit security review of the moved destination-policy and retention boundary, without changing it. Parent owns orchestration/goal state. +- Goal: reduce `src/images/artifacts.ts` from 552 lines to ≤330 through focused storage and HTTPS leaves; retain every original API and image/video budget, permission, cancellation, redirect, pinning and retention behavior. +- Non-goals: no security fixes, new validation, URL-policy changes, budget changes, format changes, write-mode cleanup, new transport, framework/dependency, caller migrations, new tests/tooling, merge or release. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Stop at an independently passing layer with an open exact-head-green PR. No merge. +- Escalation: changed source basis, missing oracle, a body/signature change, unresolved cycle, >400 leaf/residual, extra file requirement, or a >500-line hard sizing interpretation must return to the parent. Findings requiring security work go only to ignored scratch. This task writes only the two assigned S06 docs; no code, tests, git mutation or cxc orchestration commands. +- Size note: the 249-line physical move is about 498 raw added+deleted lines before imports/re-exports. Wiring can exceed the DEFAULT 500-line threshold. Request the same move-aware sizing decision as L1; do not claim the 002 threshold passes by ignoring additions or deletions or invent an unassigned layer. + +Basis: docs HEAD `4cc219549`; code `origin/dev` `1362b1a3841b4de20177e5d65865a513dd7936c4`. Source/test citations below refer to that basis. Source matches the working tree. Read 000/001/002 plus `devlog/_plan/260905_modular_debt_ledger/014_lane_adapters_media.md`'s `src/images/artifacts.ts` section: paths :83–95, prune :140–173, pinned connect :302–331, image :361–416 and video :454–552. Current graph: Google adapter, image bridge and server → artifacts → config, filesystem, destination-policy and pinned-http. Intended graph: unchanged callers → artifacts → `artifact-store.ts` / `artifact-transfer.ts` → existing dependencies. Blast radius: image feature/public artifact boundary; no transport implementation is duplicated. + +Structural decision: split storage/retention and pinned transfer; keep image/video materialization and turn budgets together in the original boundary. Rejected: do nothing/delete/configure cannot resolve size with export preservation; moving video first while importing path helpers back from `artifacts.ts` creates a cycle; reimplementing pinned HTTP duplicates the canonical `src/lib/pinned-http.ts`. Searches for `timestampPrefix`, `connectPublicHttps`, `writeArtifactUnique` and `getArtifactsDir` locate this owner; keep the existing destination-policy and pinned-http APIs. Reuse sibling naming (`src/images/{fulfill-video,xai-video-client,synthetic-tool}.ts`) and the already-established leaf structure (`src/vision/timeout-bounds.ts`, `src/config/provider-name.ts`). No new internal index/barrel. + +Direct source dependents: `src/adapters/google.ts:4`, `src/images/fulfill.ts`, `src/images/fulfill-video.ts`, `src/images/loop.ts`, `src/server/images.ts:46`, and dynamic `src/server/index.ts:1800`. Existing path imports remain untouched. S06 L1 must already be this branch's base even though there is no direct import between the two source files. + +## Symbol inventory + +AST ranges from `git show origin/dev:src/images/artifacts.ts`, cross-checked with numbered source/`rg`. Ranges exclude leading comments. Consumer count = distinct external consumer files with `rg -l -w ''` among resolved static/dynamic/mock importers, with import bindings inspected. Include `tests/images/download-cap-default.test.ts:30–32`'s multiline template import with `?cap=…`, which a simple static-import regex misses. Boundary fan-in: **13 files = 6 source + 7 tests**, counting the mock consumer. S=`src/images/artifact-store.ts`; T=`src/images/artifact-transfer.ts`; A=residual `src/images/artifacts.ts`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| MAX_DECODED_BYTES_PER_IMAGE | const | 11–11 | no | 0 | A | +| MAX_DECODED_BYTES_PER_RESPONSE | const | 12–12 | no | 0 | A | +| MAX_DOWNLOAD_BYTES | const | 14–14 | yes | 1 | T | +| DOWNLOAD_IDLE_TIMEOUT_MS | const | 16–16 | yes | 0 | T | +| MAX_ENCODED_BYTES_PER_IMAGE | const | 25–25 | yes | 3 | A | +| DEFAULT_ARTIFACT_KEEP_COUNT | const | 28–28 | yes | 1 | S | +| ARTIFACT_HTTP_PREFIX | const | 31–31 | yes | 0 | S | +| ARTIFACT_ID_RE | const RegExp | 33–33 | no | 0 | S | +| BASE64_RE | const RegExp | 37–37 | no | 0 | A | +| ImageBudget | interface | 39–41 | yes | 1 | A | +| PinnedDownloadFn | type alias | 44–48 | yes | 1 | T | +| createImageBudget | function | 50–52 | yes | 5 | A | +| chargeImageBudget | function | 55–61 | yes | 0 | A | +| getArtifactsDir | function | 63–65 | yes | 0 | S | +| artifactHttpUrl | function | 71–77 | yes | 2 | S | +| resolveArtifactPath | function | 83–95 | yes | 1 | S | +| readArtifactBytes | function | 97–109 | yes | 0 | S | +| decodeValidatedImageBase64 | function | 115–132 | yes | 1 | A | +| pruneOldArtifacts | function | 140–173 | yes | 1 | S | +| timestampPrefix | function | 175–188 | no | 0 | S (internal named export) | +| writeArtifactUnique | async function | 196–213 | no | 0 | S (internal named export) | +| sniffImageExtension | function | 216–225 | yes | 1 | A | +| guessExtFromMagic | function | 227–233 | yes | 1 | A | +| pruneArtifacts | function | 236–238 | yes | 4 | S | +| materializeInlineImage | async function | 240–257 | yes | 6 | A | +| pinnedHttpsGet | function | 267–292 | yes | 1 | T | +| pickPinnedAddress | function | 294–296 | no | 0 | T | +| connectPublicHttps | async function | 302–331 | no | 0 | T (internal named export) | +| fetchPublicHttpsImage | async function | 340–359 | yes | 2 | T | +| downloadImageToArtifact | async function | 361–416 | yes | 3 | A | +| MAX_VIDEO_DOWNLOAD_BYTES | const | 418–418 | no | 0 | A | +| MAX_VIDEO_BYTES_PER_TURN | const | 420–420 | no | 0 | A | +| VideoBudget | interface | 422–426 | yes | 1 | A | +| createVideoBudget | function | 428–430 | yes | 2 | A | +| chargeVideoBudget | function | 433–437 | yes | 0 | A | +| guessVideoExtFromMagic | function | 439–447 | yes | 0 | A | +| downloadVideoToArtifact | async function | 454–552 | yes | 2 | A | +| PinnedAddress | type re-export | 9–9 | yes | 0 | existing ../lib/pinned-http via A | + +All seven import declarations: :1 `readdirSync/readFileSync/statSync/unlinkSync/existsSync` move to S; :2 `writeFile` shared by A/S, `mkdir/open/unlink` stay A; :3 `basename/resolve/sep` move to S, `join` needed by both A/S; :4 `getConfigDir` needed by both A/S; :5 destination assessment/resolution move to T; :6 `recordOwnedConfigPath` stays A; :7 `pinnedHttpGet` and `PinnedAddress` move to T, with the existing type re-export retained in A. No other top-level declaration or executable initializer exists. + +## Leaf partition + +1. **`src/images/artifact-store.ts` — expected ≤160 lines.** Owns exactly S rows. Move :27–33, :63–109, :134–213, :235–238, with comments: **138 original lines**, plus ≤22 import/separator lines. `timestampPrefix` and `writeArtifactUnique` acquire internal named exports only because A calls them. `ARTIFACT_ID_RE` remains private. Own imports: + + ```ts + import { readdirSync, readFileSync, statSync, unlinkSync, existsSync } from "node:fs"; + import { writeFile } from "node:fs/promises"; + import { basename, join, resolve, sep } from "node:path"; + import { getConfigDir } from "../config"; + ``` + +2. **`src/images/artifact-transfer.ts` — expected ≤125 lines.** Owns exactly T rows. Move :13–16, :43–48, :259–359: **111 original lines**, plus ≤14 import/separator lines. `connectPublicHttps` becomes an internal named export for A's video downloader; `pickPinnedAddress` remains private. Keep the cap, timeout, callback signature, response cancellation and error text verbatim. Own imports: + + ```ts + import { assessUrlDestination, resolvePublicAddresses } from "../lib/destination-policy"; + import { pinnedHttpGet, type PinnedAddress } from "../lib/pinned-http"; + ``` + +3. **Residual `src/images/artifacts.ts` — expected ≤330 lines.** Exactly A rows, image/video materialization, both budgets and both magic-format decisions remain. Arithmetic: 552 − 138 − 111 = 303 original lines; replace original :1–9 import/re-export header with ≤36 lines → ≤330. No `#b` is required for residual size. Combined allowance: ≤615 = 330 + 160 + 125, a maximum net +63 wiring/separator lines, with no duplicated bodies. Measure actual line counts, not a compressed formatting proxy. + +In-memory physical-line accounting using these ranges and the exact import/export blocks below produced **S=146, T=116, residual=320** (582 total = 552 + 30 wiring lines). The larger bounds reserve formatting room. This is plan accounting, not an implementation/test result; the three internal helpers need an `export` keyword but no additional source line. + +## Re-export block + +Exact exports added/retained at the original `src/images/artifacts.ts` path; all A-row local exports stay in place: + +```ts +export type { PinnedAddress } from "../lib/pinned-http"; +export { + DEFAULT_ARTIFACT_KEEP_COUNT, + ARTIFACT_HTTP_PREFIX, + getArtifactsDir, + artifactHttpUrl, + resolveArtifactPath, + readArtifactBytes, + pruneOldArtifacts, + pruneArtifacts, +} from "./artifact-store"; +export { + MAX_DOWNLOAD_BYTES, + DOWNLOAD_IDLE_TIMEOUT_MS, + pinnedHttpsGet, + fetchPublicHttpsImage, +} from "./artifact-transfer"; +export type { PinnedDownloadFn } from "./artifact-transfer"; +``` + +Explicit replacement imports for A (a re-export does not bind its name locally): + +```ts +import { mkdir, writeFile, open, unlink } from "node:fs/promises"; +import { join } from "node:path"; +import { getConfigDir } from "../config"; +import { recordOwnedConfigPath } from "../lib/config-ownership"; +import { getArtifactsDir, timestampPrefix, writeArtifactUnique } from "./artifact-store"; +import { MAX_DOWNLOAD_BYTES, connectPublicHttps, fetchPublicHttpsImage, type PinnedDownloadFn } from "./artifact-transfer"; +``` + +Do not re-export internal `connectPublicHttps`, `timestampPrefix`, or `writeArtifactUnique` from A. Do not retarget consumers to leaves: notably `tests/images/z-fulfill.test.ts:32` must still mock the original module. + +## Module-level state and cycles + +- No top-level `let`, Map, Set, WeakMap, cache, lock or timer exists. `ARTIFACT_ID_RE` (:33) belongs only to S; `BASE64_RE` (:37) stays only in A. Both regexes lack global/sticky flags. Scalar constants have one owner per the table; `MAX_DOWNLOAD_BYTES` must not be restated in A. +- Budgets (:39–41, :422–426) are per-call objects returned by their factories, not module-global state. Keep check+charge sequencing at :55–61 and :433–437. Video reader/file handle and cleanup state (:493–550) remain one local lifetime in A. Naming still calls `new Date()` and `crypto.randomUUID()` per write; no import-time timestamp or memoization. +- A→S and A→T only; S→config/node fs/path; T→destination-policy/pinned-http. Neither leaf imports A, the other leaf, `images/index.ts`, or vision. Exporting `getArtifactsDir` from S avoids S→A; T owns both constants and `PinnedDownloadFn` to avoid T→A type/constant back-edges. +- Lane G1 found no cycle. Future executor must run its in-memory resolved graph walk including type edges on A/S/T; no return path allowed. The existing config graph is not simplified in this move. New edges are functional; read/write/retention ordering and validation→DNS→pinned-connect sequencing stay inside their owners. +- Security review is required for a pure move across this boundary: prove policy calls at :316–321, HTTPS rejection, pinned transport options/defaults :322–329, image/non-2xx handling :348–357, and unique-write options :206 are unchanged. No additional defense or unrelated security finding belongs in this public planning document. + +## Tests + +`rg -l 'artifacts' tests -g '*.test.ts'`, filtered to actual imports/mock declarations (including multiline template imports), yields this complete consumer list. Every row stays **unchanged**: + +| test file | import/mock location | disposition | +|---|---|---| +| `tests/images/artifacts-prune.test.ts` | :12, :70 | unchanged; public retention/materialization exports | +| `tests/images/artifacts-ssrf.test.ts` | :9 | unchanged; destination/pinning behavior through public download | +| `tests/images/download-cap-default.test.ts` | :30–32 | unchanged; template-query dynamic import, mock before import | +| `tests/images/gemini-inline.test.ts` | :5 | unchanged; budget and inline materialization | +| `tests/images/pinned-https-get.test.ts` | :84, :115, :170, :234, :262, :310 | unchanged; dynamic imports, pinned transport contract | +| `tests/images/z-fulfill.test.ts` | :32 `mock.module` | unchanged; preserve the original mock boundary | +| `tests/server/server-images.test.ts` | :16, :2541 | unchanged; adapter/server API consumers | + +No target-specific body-text oracle was found for `artifacts.ts`; artifact file reads in `gemini-inline.test.ts:122` are generated image bytes, not source. Do not rewrite them into leaf-source assertions. General recursive source guards also read this file: + +| test | exact source read | disposition | +|---|---|---| +| `tests/codex-integration/codex-history-reachability.test.ts` | :100 and :114; recursive `src` discovery :54–61 | unchanged; S/T automatically included; no allowlist expansion | +| `tests/windows/windows-popup-fix.test.ts` | :139; recursive discovery :121–129 | unchanged; S/T automatically included | +| `tests/lab/core-lab-boundary.test.ts` | :69; traversal from protected roots | unchanged; existing core→adapter graph reaches artifact handling; named leaf edges are followed without changing PROTECTED | + +No explicit add-leaf-to-scan-list or retarget-to-leaf change is required; verify recursive discovery includes both new files. The `?cap=` query only refreshes the facade, not necessarily its new dependency; run that mock-bearing test in its own process as shown below and do not add query propagation or a production factory. If exact-head CI reveals cross-test contamination, escalate a test-only path/isolation adjustment with evidence rather than silently changing the production API. + +Drive guards red once in future C: temporarily remove T's `?? MAX_DOWNLOAD_BYTES` at original :327 and require `download-cap-default.test.ts:35` to fail; temporarily change S's non-positive retention early return at original :142 and require `artifacts-prune.test.ts:50` to fail. Confirm public-path pinned redirect/limit tests remain green after restoring the originals. For recursive coverage, inject a forbidden PowerShell argv literal in each leaf and require the Windows source scan to report that path, then restore it. No fault injection or tests are run in this drafting task. + +## Verification + +Draft validation actually performed: read-only `bun -e` parser checks matched all **37** definition rows to original start/end lines, checked the nine headings in order, parsed every TypeScript snippet, and counted proposed files in memory. `git diff --no-index --check /dev/null devlog/_plan/260905_now_split_train/200_images_artifacts.md` reported no whitespace errors. No tests, typecheck, code edits or git mutations were performed. + +Future executor commands at S06 L2's exact tip, not merely the already-verified parent tip: + +```sh +bun run typecheck +bun test tests/images/download-cap-default.test.ts +bun test tests/images/artifacts-prune.test.ts tests/images/artifacts-ssrf.test.ts tests/images/gemini-inline.test.ts tests/images/pinned-https-get.test.ts +bun test tests/images/z-fulfill.test.ts +bun test tests/images/loop.test.ts +bun test tests/images/loop-reasoning-replay.test.ts +bun test tests/images/z-handler-activation.test.ts +bun test tests/images/plan.test.ts tests/images/synthetic-tool.test.ts tests/images/xai-client.test.ts +bun test tests/server/server-images.test.ts +bun test tests/lab/core-lab-boundary.test.ts tests/codex-integration/codex-history-reachability.test.ts tests/windows/windows-popup-fix.test.ts +bun run privacy:scan +wc -l src/images/artifact-store.ts src/images/artifact-transfer.ts src/images/artifacts.ts +rg -n '(from|import|mock\.module).*artifacts|src/images/artifacts|from "\./artifacts"' src gui/src scripts tests -g '*.ts' -g '*.tsx' +``` + +Reconcile the final search to the same 13-file consumer inventory, including `download-cap-default` and `z-fulfill`. Use `rg -l -w` for the symbol-by-symbol check, inspect bindings for alias/type imports, and compare original AST exports to the post-split facade. A raw line count alone is not a file/import count. The commands include all 12 current `tests/images/*.test.ts` files, with mock-bearing bridge tests isolated; no repository-wide local suite. Run the lane G1 graph walk on A/S/T and require no return path. All direct tests above remain public-contract tests, not merely leaf unit tests. + +Full suite is remote-only. This instantiates 002 with `pipefail`/full-log retention to prevent `tail` masking a failing test process: + +```sh +ssh lidge 'bash -lc '\''set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-images-artifacts && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tee /tmp/ocx-S06-L2-tests.log | tail -15'\''' +``` + +Require zero failures, exit 0, full log inspection and remote SHA equality with the PR head. Parent coordinates exclusive remote-checkout use. Before ready-for-review capture full exact-head CI rollup, not an empty required-check list. Security review follows `MAINTAINERS.md`; no approval is claimed by this plan. No local full suite, browser or deployment work is required for the pure move. + +## Accept criteria + +1. Each top-level definition has one owner matching the inventory; only A/S/T and authorized layer docs change. Bodies, signatures, errors and defaults are unchanged. +2. S≤160, T≤125, A≤330 and all files ≤400; all 249 original moved lines accounted for once; no unassigned `#b` or formatting-only line compression. +3. Every original value/type export remains importable from `src/images/artifacts`; three new internal helper exports are not leaked through that path. +4. Original 13 consumer files and the fulfillment mock boundary are unchanged. Query-import cap test executes in a fresh process and passes. +5. Destination-policy/pinning, redirects, byte caps, image/video budgets, permissions, naming, unique-write retries, pruning and cleanup preserve the original contract; explicit security review is recorded. +6. No new cycle; recursive history/Windows/Lab guards discover the leaves; fault-injected cap/retention/source guards fail once and restored code passes. +7. Focused checks, typecheck, privacy and exact-head remote full suite/CI pass independently at L2, whose parent is the current L1 head. No merge/release. +8. Parent resolves raw diff sizing under 002 before implementation; cascading L1 changes invalidates L2 evidence until reverified. + +## PR + +Title: `refactor(images): separate artifact storage and pinned transfer (split S06 L2/2)` + +Branch: `codex/split-images-artifacts`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification and Checklist, including security-review evidence. DEV-STACK-03 map (future PR numbers remain placeholders): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 2 | #TBD-S06-L2 | images artifacts ← you are here | codex/split-images-artifacts | dev | storage/HTTPS leaves, original artifact API | +| 1 | #TBD-S06-L1 | vision | codex/split-vision-index | dev | planning/rewrite leaves, co-located cache | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Review only this layer's diff. S06 groups execution order and PR navigation under `003_parent_decisions.md` STACK-INDEPENDENCE-01; both layers are independent PRs against dev. This train stops with open PRs and never merges. diff --git a/devlog/_plan/260905_now_split_train/210_responses_parser.md b/devlog/_plan/260905_now_split_train/210_responses_parser.md new file mode 100644 index 0000000000..86ba142712 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/210_responses_parser.md @@ -0,0 +1,213 @@ +# S07 L1/4 — Responses parser leaves + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 boundary planning, docs-only delegated work. +- Goal: move content, tool-definition and text-format translation to named siblings while preserving the sole public `parseRequest` export. +- Non-goals: changing validation, tool catalog precedence, replay state, reasoning ownership, signatures, error strings, logging, or the body of `parseRequest`; no implementation, tests, Git mutations or orchestration in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. This is a proposed execution gate, not a claim it ran. +- Stop: documents are complete when declarations, imports, counts and consumers are accounted for. Implementation is **not ready** until the parent resolves the size conflict below; then stop at an exact-head green open PR, never merge. +- Escalation: `src/responses/parser.ts:398–861` is one **464-line function**. Pure declaration moves cannot put it in any <=400-line file. Proposed L1 leaves leave **561 lines**; a provisional **S07 L1#b parser request-body decomposition** must take the rest, but no such layer exists in 002. Parent must authorize statement/helper extraction and a topology amendment, or explicitly accept the remaining debt. Do not silently invent a fifth branch. L1 also moves 325 existing lines, so raw additions+deletions exceed the 500-line changeset guideline; parent must split the implementation further or approve a pure-move size exception. Neither exception is presumed here. + +Basis: docs HEAD `4cc219549`; all source coordinates below are `origin/dev` = `1362b1a38`. `git diff origin/dev -- src/responses/parser.ts` was empty. Lane evidence: `devlog/_plan/260905_modular_debt_ledger/011_lane_server_responses.md:193`. + +Structural map: `src/index.ts`, `src/lab/conformance/executor.ts`, `src/server/responses/{core,compact,encrypted-payload,collaboration}.ts` and 43 tests -> existing `parser.ts` -> schema/state/reasoning and synthetic-tool modules. Intended: those callers still -> `parser.ts` -> content/tools/text-format leaves; tools and format -> content predicate, never back to parser. Local feature blast radius; no package entry or request contract changes. Reject moving `parseRequest` whole to a new file: it merely relocates the violation. Deletion/configuration does not meet the split request. Reuse existing schema, synthetic-tool and tool-search owners, not a generic utility abstraction. + +## Symbol inventory + +Ranges are inclusive declarations (comments outside declarations are assigned separately below), measured with `sg run --lang ts --kind 'function_declaration,type_alias_declaration,interface_declaration,lexical_declaration,class_declaration' src/responses/parser.ts --json=compact`, cross-checked with `git show origin/dev:src/responses/parser.ts | nl -ba` and anchored `rg`. Imports at 1–23 are dependencies, not owned declarations. + +Consumer counts mean distinct external direct importer/re-exporter files in `src gui/src scripts tests` that reference the symbol, found by resolving literal `from`/`import()` paths and applying `rg -l -w SYMBOL` to those files. Not raw identifier occurrences or unrelated homonyms. Private symbols have zero external consumers. Module fan-in: **49** files (6 source + 43 tests). + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| isObj | function | 25–27 | no | 0 | parser-content.ts | +| replayThoughtSignatureMetadata | function | 36–42 | no | 0 | residual parser.ts | +| InputBlock | type | 44–49 | no | 0 | parser-content.ts | +| nonEmptyString | function | 52–54 | no | 0 | parser-content.ts | +| inputContentParts | function | 56–105 | no | 0 | parser-content.ts | +| OutputBlock | type | 107–107 | no | 0 | parser-content.ts | +| outputTextOf | function | 109–124 | no | 0 | parser-content.ts | +| mapToolChoice | function | 126–149 | no | 0 | parser-tools.ts | +| allowedToolName | function | 151–158 | no | 0 | parser-tools.ts | +| buildTools | function | 160–278 | no | 0 | parser-tools.ts | +| ensureAssistantPlaceholder | function | 280–286 | no | 0 | residual parser.ts | +| outputToToolResultContent | function | 293–314 | no | 0 | parser-content.ts | +| toolOutputContainsEncryptedContent | function | 316–318 | no | 0 | parser-content.ts | +| normalizeImageDetail | function | 324–326 | no | 0 | parser-content.ts | +| findToolById | function | 328–337 | no | 0 | residual parser.ts | +| attachPendingReasoningToCallOwner | function | 347–365 | no | 0 | residual parser.ts | +| REASONING_EFFORTS | const Set | 367–367 | no | 0 | residual parser.ts | +| customToolNamespaces | function | 379–396 | no | 0 | parser-tools.ts | +| parseRequest | function | 398–861 | yes | 49 | residual parser.ts | +| parseTextFormat | function | 869–883 | no | 0 | parser-text-format.ts | + +## Leaf partition + +Sibling convention inspected: `src/responses/tool-groups.ts`, `tool-search-compat.ts`, `provider-opaque-metadata.ts`; existing domain-named siblings and `src/config/*.ts` / `src/types/*.ts`, no new index barrel. + +1. `src/responses/parser-content.ts`: `isObj`, `InputBlock`, `nonEmptyString`, `inputContentParts`, `OutputBlock`, `outputTextOf`, `outputToToolResultContent`, `toolOutputContainsEncryptedContent`, `normalizeImageDetail`. Move original ranges **25–27, 44–124, 288–326** = 123 lines, including comments. Add one import and one separating blank = **125 lines**. Export only the five functions imported below; keep block types and remaining helpers private. + + ```ts + import type { OcxContentPart, OcxTextContent } from "../types"; + ``` + +2. `src/responses/parser-tools.ts`: `mapToolChoice`, `allowedToolName`, `buildTools`, `customToolNamespaces`. Move **126–278, 369–396** = 181 lines; five imports + one blank = **187 lines**. Export `mapToolChoice`, `buildTools`, `customToolNamespaces`; retain the nested callbacks in `buildTools` unchanged. + + ```ts + import type { OcxRequestOptions, OcxTool } from "../types"; + import { isObj } from "./parser-content"; + import { WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool"; + import { buildImageTool, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool"; + import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat"; + ``` + +3. `src/responses/parser-text-format.ts`: `parseTextFormat`, including its leading comment, **863–883** = 21 lines; two imports + one blank = **24 lines**. + + ```ts + import type { OcxRequestOptions } from "../types"; + import { isObj } from "./parser-content"; + ``` + +Residual: preserve all other text, including original imports 1–23, and add the three local import lines below: **883 - 325 + 3 = 561 lines**. Total planned footprint **125 + 187 + 24 + 561 = 897 = 883 + 14** import/spacing lines. This count does not hide comment removal or reformatting. All three leaf groups have zero current external consumers, so the lowest-churn private leaves move first. The provisional parent-owned L1#b must remove at least 161 net residual lines; reducing just the 464-line function below 400 is not sufficient to meet the entire file budget. No final #b count can honestly be committed until the parent authorizes and designs that non-whole-declaration seam. + +## Re-export block + +No existing export moves in this layer: `export function parseRequest(...)` remains at the original path with its exact signature and implementation. Therefore the exact added public re-export block is **empty**; do not export new private helpers from the public path merely to make a barrel. `src/index.ts:2` stays unchanged. The required explicit residual imports are: + +```ts +import { isObj, inputContentParts, outputTextOf, outputToToolResultContent, toolOutputContainsEncryptedContent } from "./parser-content"; +import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools"; +import { parseTextFormat } from "./parser-text-format"; +``` + +These are actual local bindings; an `export { ... } from` would not satisfy the call sites. Existing residual imports are deliberately retained in this pure move to avoid assuming unused runtime imports have no evaluation effects; pruning them is not bundled cleanup. + +## Module-level state and cycles + +- `REASONING_EFFORTS` at `src/responses/parser.ts:367` has exactly one owner, residual `parser.ts`; no copied Set in a leaf. +- No top-level `let`, Map, WeakMap, lock or timer. Sets/Maps at 380, 768–769, 778 and pending reasoning at 417 are request/function-local and remain in their original call lifetime. +- `isObj` is shared functional coupling: tools and text-format import its sole content owner. Having the content leaf import it from the old parser would create `parser -> content -> parser`; explicitly forbidden, including type-only back edges. +- Replay metadata lookup, prior-response-prefix lookup, schema validation and the single `Date.now()` stay in `parseRequest`'s existing sequence. Moving these into a state factory is outside this layer. +- Lane 011 reported no cycle in its static/type/literal-dynamic graph. Recheck the changed reachable graph at implementation tip; no new leaf may import `parser.ts`, `src/index.ts` or the server responses facade. The original Lab consumer is a downward consumer, not permission to import Lab from parser leaves. + +## Tests + +Direct importers: `rg -l 'responses/parser["\x27]' tests | sort`, **43 files**, all **unchanged**, importing the original public path: + +```text +tests/adapters/adapter-buffered-tool-conformance.test.ts +tests/adapters/adapter-tool-conformance.test.ts +tests/adapters/anthropic/anthropic-error-body.test.ts +tests/adapters/anthropic/anthropic-reasoning.test.ts +tests/adapters/anthropic/anthropic-thinking-signature.test.ts +tests/adapters/bridge-raw-reasoning-hidden.test.ts +tests/adapters/google/gemini-web-search.test.ts +tests/adapters/google/google-adapter.test.ts +tests/adapters/google/google-signature-history-roundtrip.test.ts +tests/claude-integration/claude-inbound.test.ts +tests/claude-integration/claude-sidecar-override.test.ts +tests/codex-integration/compatibility-manifest.test.ts +tests/codex-integration/multi-agent-compat.test.ts +tests/e2e-style/phase100-native-parity.test.ts +tests/providers/cursor/cursor-native-exec-policy.test.ts +tests/providers/cursor/cursor-request-builder.test.ts +tests/providers/cursor/cursor-tool-choice.test.ts +tests/providers/deepseek-reasoning-replay-gaps.test.ts +tests/providers/exa-web-search.test.ts +tests/providers/kiro/kiro-adapter.test.ts +tests/providers/kiro/kiro-reasoning-roundtrip.test.ts +tests/providers/nvidia-nim-hardening.test.ts +tests/providers/xai/xai-transport.test.ts +tests/providers/xai/xai-web-search.test.ts +tests/responses/chat-completions-endpoint.test.ts +tests/responses/responses-compaction.test.ts +tests/responses/responses-custom-tool-guidance.test.ts +tests/responses/responses-forward-posit-continuation.test.ts +tests/responses/responses-parser-agent-message.test.ts +tests/responses/responses-parser-malformed-content.test.ts +tests/responses/responses-parser.test.ts +tests/responses/responses-state.test.ts +tests/responses/responses-tool-conformance.test.ts +tests/vision/sidecar-abort.test.ts +tests/vision/vision-anthropic.test.ts +tests/vision/vision-cache.test.ts +tests/vision/vision-fail-closed.test.ts +tests/vision/vision-sidecar-e2e.test.ts +tests/web-search/web-search-anthropic.test.ts +tests/web-search/web-search-backend-union.test.ts +tests/web-search/web-search-timeout-contract.test.ts +tests/web-search/web-search-timeout-plan.test.ts +tests/web-search/web-search.test.ts +``` + +Source-oracle search: literal `parser.ts`, `responses/parser`, and segmented `repoPath`/`join` paths among `readFileSync`, `Bun.file`, `source(` readers found **no direct parser-text oracle**, matching lane 011. Two transitive source readers must still be preserved: + +| test and exact read | disposition | coverage | +|---|---|---| +| `tests/lab/core-lab-boundary.test.ts:69` (`readFileSync(current, "utf8")`) | unchanged | Runtime graph follows new imports automatically; do not edit PROTECTED roots. | +| `tests/codex-integration/compatibility-manifest.test.ts:61` (`readFileSync(current, "utf8")`) | unchanged | Reachable-source scanner includes all three leaves automatically. | + +No retarget-to-leaf or explicit add-leaf-to-scan-list is needed. At C, drive the graph guards red once using a temporary forbidden edge in a reachable new leaf (Lab edge for core-Lab guard; compatibility edge for compatibility guard), restore the exact file, then green. Also temporarily break image-detail normalization in `parser-content.ts` and verify the existing parser regression fails before restoring it. Do not add or weaken source-string assertions to bypass the size conflict. No new test file or test-layout entry is planned. + +## Verification + +Execution-only after parent disposition of L1; none run during drafting. The 002 gate is instantiated as: + +```sh +bun run typecheck +bun test tests/responses tests/adapters tests/claude-integration tests/codex-integration tests/providers tests/vision tests/web-search tests/e2e-style/phase100-native-parity.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/responses/parser-content.ts src/responses/parser-tools.ts src/responses/parser-text-format.ts src/responses/parser.ts +rg -n 'from "[^"]*/responses/parser"' src gui/src scripts tests | wc -l +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-responses-parser && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Original-path module fan-in remains 49 (all are static imports/re-exports here); additionally resolve the symbol imports, not just the printed line count. Require remote tested HEAD = pushed PR head and 0 failures; `pipefail` prevents `tail` hiding test failure. Capture full runner output as well as summary. Full suite is remote only. Inspect static imports/re-exports, type-only edges and literal dynamic imports from each new leaf for a return path to parser; no new cycle. Compare moved AST bodies ignoring only added `export` modifiers/import wiring. Record exact-head CI rollup before readiness. + +## Accept criteria + +1. All 20 owned declarations occur exactly once; original `parseRequest` remains importable through both old paths and keeps its body/signature. +2. Content/tools/format leaves measure <=400 (planned 125/187/24); sole Set owner stays residual. +3. No behavior, string, schema, timing or state-lifetime delta; new dependencies have no back edge or Lab path. +4. All listed behavioral tests retain their original imports and assertions; both graph guards fail under the temporary forbidden edge and pass after restoration. +5. Typecheck, privacy scan, focused tests, remote exact-head full suite and exact-head CI succeed with recorded evidence. +6. **Blocked until parent decision:** residual 561 cannot satisfy the 400-line terminal objective. Record approved L1#b topology and statement-level seam, or an explicit debt exception; never mark this row resolved just because private leaves moved. Resolve the >500 raw-diff issue at the same gate. + +## PR + +Title: `refactor(responses): isolate parser translation leaves (split S07 L1/4)` + +Branch: `codex/split-responses-parser`. Base: `dev`. Closes: none. +Use `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification and Checklist. This proposed stack remains the assigned four layers; the unresolved #b is not a fictitious open PR. Review only the current layer diff. No push/PR/merge is performed by this delegated drafting task. + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S07-L4 | codex/split-server-responses-collaboration | codex/split-responses-parser | Tool maps, roster rendering, insertion | +| 3 | #TBD-S07-L3 | codex/split-server-responses-agent-task-recovery | dev | Envelope codec ownership | +| 2 | #TBD-S07-L2 | codex/split-responses-namespace-tool-compat | dev | Restoration and alias contract | +| 1 | #TBD-S07-L1 | codex/split-responses-parser — this layer | dev | Private parser leaves; size escalation | + +Base: dev — no dependency on lower layers; this layer is the parent of 240 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). + +Merge requires separate user authorization. This delegated task performs no Git or PR mutation. + +## P stale-check (2026-09-05, wp210) + +origin/dev 526d4bf64; parser.ts unchanged since 445742966 (883 lines); anchors 25/27/44/124/126/278/288/326/369/396/398/863/883 confirmed by sed. Base `dev` (S07 bottom; 240 collaboration chains on it). RESIDUAL-FN-01 applies (003): the 561-line residual is accepted this layer because `parseRequest` alone is 464 lines; recorded as `RESOLVABLE_AFTER(design:L1-parse-request-extraction)` for the ledger. Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change (extend tests/responses/responses-parser.test.ts). + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-210.Hyb8ZV/wt` (branch `codex/split-responses-parser`, base origin/dev 526d4bf64/a594a7f21 — parser.ts identical). Executor: gpt-6-astra high (Noether, 01a06f50-337b-7851-87f0-bdd999315e1a). +- Commits: 0c554d540 (move: parser-content.ts 127, parser-tools.ts 188, parser-text-format.ts 24, parser.ts 561), 824ec33d5 (test: responses-parser.test.ts +13 — buildTools/parseTextFormat via leaves; leaves have no ./parser import), 3793fb032 (main agent: dropped the trailing blank line at parser.ts EOF flagged by `git diff --check`; residual 560). Diff: 5 files. +- Residual 560 > 400 is accepted under 003 RESIDUAL-FN-01 (parseRequest 398–861, 464 lines) → ledger verdict `RESOLVABLE_AFTER(design:L1-parse-request-extraction)`. +- Local gate: typecheck 0; focused (8 files) 166 pass / 0 fail; guards (core-lab-boundary + compatibility-manifest) 23/0; privacy passed; 49 original-path importers unchanged. +- Red-drives: (a) normalizeImageDetail identity → responses-parser.test.ts:610 fails (original vs high), restored; (b) lab import in parser-content → core-lab-boundary:288 chain core → parser → parser-content → lab/paths, restored; (c) compatibility import in parser-content → compatibility-manifest:191 fails, restored 23/0. + +- Adversarial diff review (Godel, gpt-6-astra high, 01a06f54-e186-7ab0-94c0-521de2a16468): VERDICT: PASS (slices exact, residual byte-identical incl. dropped line 862, parseRequest byte-identical over 464 lines, export inventory exactly [parseRequest], no new cycles). +- lidge full suite at 3793fb032: SUITE_EXIT=0, 18061 pass / 0 fail / 16 skip (/tmp/suite-split-210.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3580 (base dev, head 3793fb032). CI rollup at record time: OPEN draft=false 3793fb032 =1 =18 SKIPPED=2 SUCCESS=6 diff --git a/devlog/_plan/260905_now_split_train/220_responses_namespace_tool_compat.md b/devlog/_plan/260905_now_split_train/220_responses_namespace_tool_compat.md new file mode 100644 index 0000000000..65cb22da55 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/220_responses_namespace_tool_compat.md @@ -0,0 +1,141 @@ +# S07 L2/4 — Namespace call restoration + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 structural planning with explicit security-contract review of unchanged alias authorization. +- Goal: separate returned-call restoration and its alias contract from outbound namespace lowering, keeping every existing export at the original path. +- Non-goals: selector/auth policy changes, schema changes, new guards, tool identity renaming, behavior fixes, new dependencies or generic helpers. This delegated task writes only this plan; no code, tests, Git or orchestration actions. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. +- Stop: a fully checked plan here; implementation later stops at an exact-head green open PR, never merge. L1 readiness remains subject to 210's parent-owned escalation. +- Escalation: any changed authorization/collision outcome, duplicated alias owner, new cycle, >400 leaf/residual, or non-move diff. Do not expand the four-layer stack without parent approval. + +Basis: docs HEAD `4cc219549`; source `origin/dev` = `1362b1a38`, `src/responses/namespace-tool-compat.ts` = 435 lines, working tree identical. Lane: `devlog/_plan/260905_modular_debt_ledger/011_lane_server_responses.md:552`. + +Structural map: `src/adapters/openai-responses.ts:21`, `src/server/responses/core.ts:382` and one behavioral test -> namespace facade -> `../types` / `./tool-groups`. Intended: same callers -> residual lowering module -> dependency-free restoration/alias leaf. Local Responses-feature blast radius. The shared predicate and type definitions must move down with the restoration leaf to avoid an upward import. Reject a whole-file rename (does not reduce size), or copying the predicate/types (two owners); deletion/configuration cannot deliver this partition. + +## Symbol inventory + +Inclusive `origin/dev` declaration ranges from ast-grep declaration kinds plus numbered source/anchored `rg`. Imports 1–2 are dependencies, not owned declarations. Consumers count distinct direct importer/re-exporter files that match `rg -l -w SYMBOL`, after resolving this module's literal specifiers in `src gui/src scripts tests`. Private declarations have 0; unrelated homonyms are excluded. Fan-in **3** files. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| RoutedNamespaceToolIdentity | interface | 4–15 | yes | 0 | namespace-tool-restore.ts | +| RoutedNamespaceToolAliases | type | 17–17 | yes | 1 | namespace-tool-restore.ts | +| BUILTIN_FUNCTIONS_NAMESPACE | const | 19–19 | no | 0 | residual namespace-tool-compat.ts | +| isPlainObject | function | 21–23 | no | 0 | namespace-tool-restore.ts | +| namespaceIdentity | function | 25–27 | no | 0 | residual namespace-tool-compat.ts | +| isRepresentableName | function | 34–42 | no | 0 | residual namespace-tool-compat.ts | +| NamespaceGroup | type | 44–48 | no | 0 | residual namespace-tool-compat.ts | +| parseNamespaceGroup | function | 59–72 | no | 0 | residual namespace-tool-compat.ts | +| loweredIdentity | function | 79–83 | no | 0 | residual namespace-tool-compat.ts | +| loweredWireName | function | 85–87 | no | 0 | residual namespace-tool-compat.ts | +| addSelector | function | 89–97 | no | 0 | residual namespace-tool-compat.ts | +| NamespaceRewritePlan | type | 99–104 | no | 0 | residual namespace-tool-compat.ts | +| NamespaceToolCollisionError | class | 107–107 | yes | 1 | residual namespace-tool-compat.ts | +| buildRewritePlan | function | 109–159 | no | 0 | residual namespace-tool-compat.ts | +| rewriteToolList | function | 168–204 | no | 0 | residual namespace-tool-compat.ts | +| hasMalformedNamespace | function | 227–229 | no | 0 | residual namespace-tool-compat.ts | +| rewriteNamedSelector | function | 231–251 | no | 0 | residual namespace-tool-compat.ts | +| rewriteToolChoice | function | 253–267 | no | 0 | residual namespace-tool-compat.ts | +| authorizedAliases | function | 274–320 | no | 0 | residual namespace-tool-compat.ts | +| rewriteInputItem | function | 322–333 | no | 0 | residual namespace-tool-compat.ts | +| rewriteRoutedNamespaceToolsForUpstream | function | 343–377 | yes | 2 | residual namespace-tool-compat.ts | +| restoreRoutedNamespaceCalls | function | 379–414 | yes | 2 | namespace-tool-restore.ts | +| restoreRoutedNamespaceCallsInJson | function | 416–429 | yes | 2 | namespace-tool-restore.ts | +| createRoutedNamespaceCallRestoreRewrite | function | 431–435 | yes | 2 | namespace-tool-restore.ts | + +## Leaf partition + +One new sibling, `src/responses/namespace-tool-restore.ts`: `RoutedNamespaceToolIdentity`, `RoutedNamespaceToolAliases`, `isPlainObject`, `restoreRoutedNamespaceCalls`, `restoreRoutedNamespaceCallsInJson`, `createRoutedNamespaceCallRestoreRewrite`. Move ranges **4–17, 21–23, 379–435** = **74** existing lines. Two separating blanks = **76 lines**. Its own imports: **none**; standard language/JSON/Map types only. Export `isPlainObject` from the leaf for the residual, but not through the original public path. + +Residual: **435 - 74 + 4 = 365 lines**, with four exact wiring lines below; preserve all other source text and both original imports. Combined **76 + 365 = 441 = 435 + 6**. No #b required. Approximate raw diff before formatting: 74 deletions + 80 additions = 154, below 500. Existing sibling naming was checked against `src/responses/{custom-tool-compat,tool-search-compat,tool-groups,provider-opaque-metadata}.ts`; do not introduce a convenience index. + +Only the helper's export modifier changes; restore recursion stays in its owner and the types are not copied. The alias contract is colocated with the function that consumes it at the wire boundary rather than creating a type-only micro-file. Keep name/kind filtering in `authorizedAliases` unchanged in the residual. + +## Re-export block + +Add exactly these public compatibility exports: + +```ts +export type { RoutedNamespaceToolIdentity, RoutedNamespaceToolAliases } from "./namespace-tool-restore"; +export { restoreRoutedNamespaceCalls, restoreRoutedNamespaceCallsInJson, createRoutedNamespaceCallRestoreRewrite } from "./namespace-tool-restore"; +``` + +Keep the original exported `NamespaceToolCollisionError` and `rewriteRoutedNamespaceToolsForUpstream` declarations in place. Explicit residual local bindings, because the re-exports bind nothing locally: + +```ts +import type { RoutedNamespaceToolIdentity } from "./namespace-tool-restore"; +import { isPlainObject } from "./namespace-tool-restore"; +``` + +`RoutedNamespaceToolAliases` and restoration functions are not used by the residual; do not add unused local imports for them. Leaf recursion resolves locally. Existing adapter/core/test imports stay unchanged. + +## Module-level state and cycles + +No top-level `let`, Map, Set, WeakMap, lock or timer. `BUILTIN_FUNCTIONS_NAMESPACE` (`src/responses/namespace-tool-compat.ts:19`) remains a single immutable scalar in residual lowering. Type aliases for Maps are not allocations. Request-local Maps/Sets in `buildRewritePlan` (110–114), `authorizedAliases` (283) and rewrite entry (353) must remain per-call. Returned alias objects retain their identities and lifetime. + +Direction: residual -> restoration leaf. Leaf imports nothing, so it cannot create a direct or transitive back edge. In particular, importing alias types or `isPlainObject` from the old facade in the leaf would create a cycle, even if one edge were type-only. Functional coupling only; the existing caller-owned alias map is not a new shared store. Lane 011 found no existing cycle; recheck changed static/type/literal-dynamic edges at implementation HEAD. + +## Tests + +`rg -l 'responses/namespace-tool-compat["\x27]' tests | sort` returns one direct importer: + +```text +tests/responses/namespace-tool-compat.test.ts +``` + +Disposition: **unchanged**, old-path import at line 7; keep all assertions. Also unchanged, indirect transport coverage: `tests/responses/openai-responses-passthrough.test.ts` exercises lowering/restoration through the adapter/core. No new test file/test-layout change. + +Literal basename, full path and segmented `repoPath`/`join` search among source readers found no direct text oracle (lane 011 agrees). Transitive source readers: + +| test and exact read | disposition | action | +|---|---|---| +| `tests/lab/core-lab-boundary.test.ts:69` | unchanged | New restoration leaf reached from core through namespace module automatically. | +| `tests/codex-integration/compatibility-manifest.test.ts:61` | unchanged | Same import-graph discovery; no static filename list to extend. | + +No retarget-to-leaf or add-leaf-to-scan-list needed. Drive the restoration behavioral guard at `tests/responses/namespace-tool-compat.test.ts:482` red once by temporarily disabling restoration in the new leaf, then restore it and prove green. Preserve the authorization tests at lines 107, 294 and 306; they must not be moved into a weaker direct leaf-only test. For graph guard proof, temporarily inject a forbidden Lab/compatibility edge into the reachable leaf, verify each respective guard fails, remove it and verify green. PROTECTED roots remain byte-identical. + +## Verification + +Execution plan only; no tests run by this drafter: + +```sh +bun run typecheck +bun test tests/responses/namespace-tool-compat.test.ts tests/responses/openai-responses-passthrough.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/responses/namespace-tool-restore.ts src/responses/namespace-tool-compat.ts +rg -n 'from "[^"]*/namespace-tool-compat"' src gui/src scripts tests | wc -l +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-responses-namespace-tool-compat && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused domain: Responses, with reachable-boundary coverage in codex-integration/Lab. Fan-in remains 3; typecheck additionally proves named exports and type identity. Verify zero return edges from the dependency-free leaf; compare moved AST bodies ignoring only export modifiers. Remote tested SHA must equal PR head; retain full output and verify pipeline exit plus 0 failures, never use the tail alone as proof. Full suite remote only; exact-head CI rollup required before readiness. + +## Accept criteria + +1. All 24 declarations have one owner; all seven current exports remain importable from the old module with unchanged names/signatures and the same error constructor. +2. Leaf <=400 (76 planned); residual <=400 (365 planned); no duplicate predicate or alias interface. +3. Outbound authorization/collision logic is unchanged; recursion, copy-on-change identity, JSON error fallback and alias closure behavior are byte-preserved. +4. One direct test importer and indirect passthrough tests remain unchanged; red-once guard proof is recorded, including graph reachability without editing PROTECTED roots. +5. Typecheck, focused tests, privacy scan, remote exact-head full suite and exact-head CI are green; no new import cycle. +6. PR has the correct parent branch; parent layer's unresolved planning constraint is resolved before claiming stack readiness. + +## PR + +Title: `refactor(responses): separate namespace call restoration (split S07 L2/4)` + +Branch: `codex/split-responses-namespace-tool-compat`. Base: `dev`. Closes: none. Fill every `.github/PULL_REQUEST_TEMPLATE.md` section (Summary, Verification, Checklist). Review this layer's diff only. + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S07-L4 | codex/split-server-responses-collaboration | codex/split-responses-parser | Tool maps, roster rendering, insertion | +| 3 | #TBD-S07-L3 | codex/split-server-responses-agent-task-recovery | dev | Envelope codec ownership | +| 2 | #TBD-S07-L2 | codex/split-responses-namespace-tool-compat — this layer | dev | Restoration and alias contract | +| 1 | #TBD-S07-L1 | codex/split-responses-parser | dev | Private parser leaves; size escalation | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge requires separate user authorization. This delegated task performs no Git or PR mutation. diff --git a/devlog/_plan/260905_now_split_train/230_server_responses_agent_task_recovery.md b/devlog/_plan/260905_now_split_train/230_server_responses_agent_task_recovery.md new file mode 100644 index 0000000000..cd8b843b71 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/230_server_responses_agent_task_recovery.md @@ -0,0 +1,158 @@ +# S07 L3/4 — Agent-task envelope codec + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 boundary planning, with explicit security review of the unchanged credential/assignment boundary before implementation readiness. +- Goal: move envelope recognition, assignment validation and injection into one codec sibling while leaving admission, transport and cache orchestration in their existing owner. +- Non-goals: changing credentials, fixed endpoint, JWT policy, byte limits, plaintext handling, cache lifetime/key construction, retry/abort order, or security behavior. No code, test, Git or orchestration execution in this delegated docs task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. +- Stop: complete this bounded plan; execution later stops at an exact-head green open PR, not merge. Upstream L1's unresolved scope decision is inherited, not bypassed. +- Escalation: changed admission or validation order, duplicated cache/key/Set, a leaf >400, any cyclic import, non-move security change or required file scope expansion. Parent approval required; no extra layer silently added. + +Basis: docs HEAD `4cc219549`; source `origin/dev` = `1362b1a38`. Working-tree source matches the 498-line basis. Lane: `devlog/_plan/260905_modular_debt_ledger/011_lane_server_responses.md:434`. + +Structural map: `src/server/responses/core.ts:312` and five tests -> recovery module -> `agent-task-recovery-cache.ts`, `encrypted-payload.ts`, OAuth parsing, auth-cors, bounded body and crypto. Intended: recovery module -> new envelope codec -> existing encrypted-payload owner; admission and transport remain residual. This is a local server Responses-feature partition, not a new service. Reject a second recovery store and wholesale transport/admission extraction: neither is needed to meet 400, and both increase credential/lifetime review scope. Reuse `structurallyValidFernetTokens`, not a copied recognizer. Codec owns the existing unknown-input boundary checks, with no added internal validation. + +## Symbol inventory + +Inclusive declaration spans measured from ast-grep declaration kinds, checked against `git show origin/dev:src/server/responses/agent-task-recovery.ts | nl -ba` and anchored `rg`. Imports 1–11 are dependencies. Consumers = distinct external direct importer/re-exporter files whose resolved literal module path targets this file and whose contents match `rg -l -w SYMBOL`, across `src gui/src scripts tests`. Private declarations have 0 external consumers. Fan-in **6** files (core + five tests). + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| RECOVERY_ENDPOINT | const | 15–15 | no | 0 | residual agent-task-recovery.ts | +| RECOVERY_TOOL | const | 16–16 | no | 0 | residual agent-task-recovery.ts | +| RECOVERY_PROMPT | const | 17–20 | no | 0 | residual agent-task-recovery.ts | +| CODEX_ORIGINATORS | const Set | 21–27 | no | 0 | residual agent-task-recovery.ts | +| CODEX_OAUTH_CLIENT_ID | const | 28–28 | no | 0 | residual agent-task-recovery.ts | +| OPENAI_TOKEN_ISSUERS | const Set | 29–29 | no | 0 | residual agent-task-recovery.ts | +| OPENAI_TOKEN_AUDIENCE | const | 30–30 | no | 0 | residual agent-task-recovery.ts | +| MAX_CIPHERTEXT_BYTES | const | 31–31 | no | 0 | agent-task-envelope.ts | +| MAX_ASSIGNMENT_BYTES | const | 32–32 | no | 0 | agent-task-envelope.ts | +| MAX_RECOVERY_RESPONSE_BYTES | const | 33–33 | no | 0 | residual agent-task-recovery.ts | +| CACHE_SCOPE_KEY | const Buffer | 34–34 | no | 0 | residual agent-task-recovery.ts | +| AgentTaskRecoveryOptions | interface | 36–41 | yes | 0 | residual agent-task-recovery.ts | +| agentTaskRecoveryConfig | function | 43–58 | yes | 1 | residual agent-task-recovery.ts | +| AgentEnvelope | interface | 60–70 | no | 0 | agent-task-envelope.ts | +| ROUTING_HEADER | const RegExp | 72–72 | no | 0 | agent-task-envelope.ts | +| findEnvelope | function | 74–157 | no | 0 | agent-task-envelope.ts | +| stripMatchingEnvelope | function | 159–169 | no | 0 | agent-task-envelope.ts | +| validateAssignment | function | 171–178 | no | 0 | agent-task-envelope.ts | +| injectAssignment | function | 180–201 | no | 0 | agent-task-envelope.ts | +| RecoveryAdmission | interface | 203–206 | no | 0 | residual agent-task-recovery.ts | +| isNativeChatGptAccessToken | function | 208–233 | no | 0 | residual agent-task-recovery.ts | +| recoveryAdmission | function | 235–269 | no | 0 | residual agent-task-recovery.ts | +| AdmittedRecovery | interface | 271–275 | no | 0 | residual agent-task-recovery.ts | +| admittedRecovery | function | 277–301 | no | 0 | residual agent-task-recovery.ts | +| recoveryPayload | function | 303–332 | no | 0 | residual agent-task-recovery.ts | +| sseDataPayloads | function | 334–357 | no | 0 | residual agent-task-recovery.ts | +| assignmentFromRecoverySse | function | 359–415 | no | 0 | residual agent-task-recovery.ts | +| requestRecovery | function | 417–458 | no | 0 | residual agent-task-recovery.ts | +| recoverEncryptedAgentTask | function | 460–484 | yes | 1 | residual agent-task-recovery.ts | +| discardEncryptedAgentTaskRecovery | function | 486–494 | yes | 1 | residual agent-task-recovery.ts | +| resetAgentTaskRecoveryState | function | 496–498 | yes | 5 | residual agent-task-recovery.ts | + +## Leaf partition + +One new sibling: `src/server/responses/agent-task-envelope.ts`. Symbols: `MAX_CIPHERTEXT_BYTES`, `MAX_ASSIGNMENT_BYTES`, `AgentEnvelope`, `ROUTING_HEADER`, `findEnvelope`, `stripMatchingEnvelope`, `validateAssignment`, `injectAssignment`. Move original **31–32 and 60–201** = **144 lines**. Add one import and one blank = **146 lines**. Its own import: + +```ts +import { structurallyValidFernetTokens } from "./encrypted-payload"; +``` + +Export the internal leaf contract `AgentEnvelope`, `findEnvelope`, `validateAssignment`, `injectAssignment`; keep limits, regex and `stripMatchingEnvelope` private. The public facade does not expose them. `Buffer` remains the existing Bun global; no dependency or Node-only execution model is introduced. + +Residual `src/server/responses/agent-task-recovery.ts`: **498 - 144 + 2 = 356 lines**. All remaining source text is unchanged, including original imports; add only the two local imports below. Total **146 + 356 = 502 = 498 + 4**. Expected raw diff 144 deletions + 148 additions = 292 before formatter changes. No #b required. Lane 011 also identified response codecs, but moving them is unnecessary for this layer's file limit; leave them beside transport instead of expanding the diff. + +Convention/equivalent-owner search: existing `src/server/responses/{agent-task-recovery-cache,encrypted-payload,input-admission,context-overflow}.ts` are named sibling leaves. The existing recovery cache at `agent-task-recovery-cache.ts:21–23` is reused without edits; no generic helpers/index file, duplicate envelope parser or second cache. + +## Re-export block + +All five existing public declarations remain in the residual: `AgentTaskRecoveryOptions`, `agentTaskRecoveryConfig`, `recoverEncryptedAgentTask`, `discardEncryptedAgentTaskRecovery`, `resetAgentTaskRecoveryState`. The exact added public re-export block is **empty** because no public declaration moves. Do not widen the public API by re-exporting newly leaf-exported internals. + +Explicit local imports required by residual admission, payload, SSE and recovery functions: + +```ts +import type { AgentEnvelope } from "./agent-task-envelope"; +import { findEnvelope, validateAssignment, injectAssignment } from "./agent-task-envelope"; +``` + +Do not replace these with `export { ... } from`: that supplies no local bindings. The public reset function continues to invoke the existing cache reset; no facade wrapper replacement or alias change is planned. + +## Module-level state and cycles + +- `CODEX_ORIGINATORS` at `src/server/responses/agent-task-recovery.ts:21–27`: sole owner remains residual; same Set identity and members. +- `OPENAI_TOKEN_ISSUERS` at line 29: sole owner remains residual. +- `CACHE_SCOPE_KEY` at line 34: sole owner remains residual, exactly one `randomBytes(32)` per module initialization. It must not move into a function, leaf, or new reset hook. +- `ROUTING_HEADER` at line 72: sole owner becomes envelope leaf; preserve flags (no global/sticky state) and exact expression. Both matching/stripping functions use this one instance. +- No other top-level mutable Map/WeakMap/lock/timer. All scalar constants are assigned in the inventory. Request-local AbortController/timeout at 423–427 stay residual and retain `finally` cleanup. +- Existing cache Maps and flights live only in `agent-task-recovery-cache.ts:21–23`; no extraction duplicates them or changes waiter accounting. + +Potential cycle to reject: `recovery -> envelope -> recovery` if the leaf imports its type/limits from the original file. Move those definitions down instead. The actual dependency is `recovery -> envelope -> encrypted-payload -> parser`; parser must not import recovery. Retain L1's old parser boundary. Lane 011 reported no static/type/literal-dynamic cycle; implementation must rewalk this reachable chain, including types, and preserve core-Lab exclusion. Functional codec coupling and unchanged sequential admission -> cache -> request -> injection; no new common mutable state. + +## Tests + +Direct importers, from `rg -l 'responses/agent-task-recovery["\x27]' tests | sort`, all **unchanged**: + +```text +tests/routing/subagent-fallback-handle-responses.test.ts +tests/server/agent-task-recovery-combo.test.ts +tests/server/agent-task-recovery-fallback.test.ts +tests/server/agent-task-recovery-security.test.ts +tests/server/agent-task-recovery.test.ts +``` + +These imports exercise the public reset; actual recovery behavior is reached through server/core. Keep that integration path, not a weaker test-only export of internals. `tests/server/agent-task-recovery-cache.test.ts` imports the existing cache, not this file: run it unchanged as adjacent lifetime coverage. Helper `tests/helpers/agent-task-recovery.ts` is not a direct production-module importer and is not counted as one. + +Literal/segmented path and source-reader searches found no direct source-text oracle for this file, agreeing with lane 011. Transitive graph oracles still read it and its new leaf: + +| test and exact read | disposition | action | +|---|---|---| +| `tests/lab/core-lab-boundary.test.ts:69` | unchanged | Existing graph root core discovers the new envelope edge; PROTECTED roots untouched. | +| `tests/codex-integration/compatibility-manifest.test.ts:61` | unchanged | New leaf discovered through existing runtime-import traversal. | + +No retarget-to-leaf or add-leaf-to-scan-list required. C-phase guards to drive red once: temporarily disable both multiplicity checks moved from original lines 136–137 (`encryptedPartCount !== 1`, `ciphertextCount !== 1`); the duplicate-encrypted-part case at `tests/server/agent-task-recovery-security.test.ts:279` must reject the mutation. Restore both checks, then green. Keep cached-admission test at line 99 and real success case in `tests/server/agent-task-recovery.test.ts:149` green, proving the split neither bypasses admission nor denies everything. Temporarily add each graph guard's forbidden edge in the new reachable leaf, get red, restore, get green. No test file is newly introduced. + +## Verification + +Future implementation commands only; no test or scan execution in this docs task: + +```sh +bun run typecheck +bun test tests/server/agent-task-recovery.test.ts tests/server/agent-task-recovery-security.test.ts tests/server/agent-task-recovery-fallback.test.ts tests/server/agent-task-recovery-combo.test.ts tests/server/agent-task-recovery-cache.test.ts tests/routing/subagent-fallback-handle-responses.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/server/responses/agent-task-envelope.ts src/server/responses/agent-task-recovery.ts +rg -n 'from "[^"]*/agent-task-recovery"' src gui/src scripts tests | wc -l +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-server-responses-agent-task-recovery && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Domains: server, routing, reachable-boundary codex-integration/Lab. Importer count remains 6; typecheck validates unchanged API resolution. Compare moved AST bodies allowing only leaf export modifiers and dependency wiring. Rewalk static/type/literal-dynamic edges for return paths and preserve PROTECTED roots. Record explicit security review per MAINTAINERS.md, since this is an existing security-sensitive boundary even though behavior does not change. Remote full suite only, tested SHA = PR head, exit 0 and 0 failures with full output retained; tail alone is insufficient. Exact-head CI rollup required. + +## Accept criteria + +1. All 31 owned declarations accounted for exactly once; the five public exports and caller paths remain unchanged. +2. Envelope leaf <=400 (146 planned), residual <=400 (356 planned); no #b and no cache duplication. +3. Exactly one originator Set, issuer Set and process HMAC key remain in original owner; one routing regex lives with all its consumers in the leaf. +4. Endpoint, header allowlist, admission-before-cache ordering, byte limits, assignment exactness, abort and reset semantics are unchanged; moved declarations compare mechanically. +5. Listed tests remain intact; mutation guard fails then passes after restoration; no reachable Lab edge or newly introduced cycle. +6. Typecheck, focused tests, privacy scan, remote exact-head full suite, exact-head CI and explicit security review are recorded before PR readiness. + +## PR + +Title: `refactor(server-responses): isolate agent task envelope codec (split S07 L3/4)` + +Branch: `codex/split-server-responses-agent-task-recovery`. Base: `dev`. Closes: none. Fill Summary, Verification and Checklist from `.github/PULL_REQUEST_TEMPLATE.md`. Review this layer's diff only. + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S07-L4 | codex/split-server-responses-collaboration | codex/split-responses-parser | Tool maps, roster rendering, insertion | +| 3 | #TBD-S07-L3 | codex/split-server-responses-agent-task-recovery — this layer | dev | Envelope codec ownership | +| 2 | #TBD-S07-L2 | codex/split-responses-namespace-tool-compat | dev | Restoration and alias contract | +| 1 | #TBD-S07-L1 | codex/split-responses-parser | dev | Private parser leaves; size escalation | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge requires separate user authorization. This delegated task performs no Git or PR mutation. diff --git a/devlog/_plan/260905_now_split_train/240_server_responses_collaboration.md b/devlog/_plan/260905_now_split_train/240_server_responses_collaboration.md new file mode 100644 index 0000000000..dfca07133d --- /dev/null +++ b/devlog/_plan/260905_now_split_train/240_server_responses_collaboration.md @@ -0,0 +1,162 @@ +# S07 L4/4 — Collaboration leaves + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 boundary planning with explicit review of unchanged tool-authorization behavior. +- Goal: separate tool bridge maps, roster text rendering and developer-message insertion, keeping guidance orchestration and all public exports at the existing boundary. +- Non-goals: rewriting guidance strings, changing model/effort selection, config/catalog timing, tool authorization, budget accounting, raw/parsed insertion order, or existing dynamic import strategy. No code, tests, Git mutations or cxc orchestration in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. +- Stop: complete the plan; execution later stops at exact-head green open PR, never merge. Parent must resolve L1's scope conflict before stack readiness. +- Escalation: any changed runtime behavior or import side effect, cycle, >400 file, >500 changeset or required scope expansion. Do not prune unrelated unused imports as opportunistic cleanup. + +Basis: docs HEAD `4cc219549`; source `origin/dev` = `1362b1a38`, 622 lines, identical in working tree. Lane: `devlog/_plan/260905_modular_debt_ledger/011_lane_server_responses.md:314`. + +Structural map: `src/server/responses.ts:6–7`, `src/server/responses/core.ts` and two direct tests -> collaboration -> types/config/catalog/provider-slug/fallback/debug modules. There are also facade consumers listed under Tests. Intended: same callers -> collaboration -> tool-bridge-maps / subagent-roster-text / developer-message-insertion; only roster rendering is needed as a local binding. The feature facade remains the public compatibility boundary, not a new convenience barrel. Local server-feature blast radius. Reject lifting the entire guidance block into a fourth owner or new service: the three moves below meet 400 while staying within the changeset budget. Deletion/configuration cannot deliver the split. Reuse type helpers and existing catalog APIs; do not implement new tooling or model resolution. + +## Symbol inventory + +Inclusive origin/dev declaration spans from ast-grep plus numbered source/anchored `rg`. Imports 1–102 are existing dependencies, including many apparently unused bindings; preserve their source text in the residual rather than infer initialization safety. Consumers = distinct direct importer/re-exporter files in `src gui/src scripts tests` that match `rg -l -w SYMBOL` after literal-path resolution. Not transitive facade consumers or homonym counts. Module fan-in **4** files (2 source, 2 tests). + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| buildToolBridgeMaps | function | 105–231 | yes | 2 | tool-bridge-maps.ts | +| PROACTIVE_MULTI_AGENT_MODE_TEXT | const string | 235–241 | yes | 1 | residual collaboration.ts | +| isV1CollabSurface | function | 243–245 | yes | 1 | residual collaboration.ts | +| collabSurface | function | 249–270 | yes | 2 | residual collaboration.ts | +| MultiAgentGuidanceOptions | interface | 274–282 | yes | 1 | residual collaboration.ts | +| MultiAgentGuidanceDeps | interface | 286–293 | yes | 1 | residual collaboration.ts | +| defaultCollectCatalogState | function | 295–304 | no | 0 | residual collaboration.ts | +| resolveEffectiveSubagentRoster | function | 308–314 | yes | 1 | residual collaboration.ts | +| freshSubagentCatalogEntries | function | 329–346 | no | 0 | residual collaboration.ts | +| createRequestScopedSubagentRosterResolver | function | 349–356 | no | 0 | residual collaboration.ts | +| multiAgentGuidanceText | function | 360–497 | yes | 3 | residual collaboration.ts | +| V2_GUIDANCE_CHAR_BUDGET | const | 501–501 | yes | 1 | residual collaboration.ts | +| applyInjectionPlaceholders | function | 503–509 | yes | 0 | residual collaboration.ts | +| subagentRosterText | function | 513–525 | yes | 0 | subagent-roster-text.ts | +| isRecord | function | 529–531 | no | 0 | developer-message-insertion.ts | +| generatedDeveloperText | function | 533–540 | no | 0 | developer-message-insertion.ts | +| isGeneratedDeveloperItem | function | 542–544 | no | 0 | developer-message-insertion.ts | +| isDeveloperPrefixItem | function | 546–551 | no | 0 | developer-message-insertion.ts | +| leadingDeveloperPrefixLength | function | 553–557 | no | 0 | developer-message-insertion.ts | +| isConversationalItem | function | 559–564 | no | 0 | developer-message-insertion.ts | +| statefulRawInsertionIndex | function | 566–574 | no | 0 | developer-message-insertion.ts | +| injectDeveloperMessage | function | 576–622 | yes | 2 | developer-message-insertion.ts | + +## Leaf partition + +1. `src/server/responses/tool-bridge-maps.ts`: `buildToolBridgeMaps`, original **105–231** = 127 lines; three imports + one blank = **131 lines**. Keep all request-local maps, collision ordering and budget charges inside the function. + + ```ts + import { dottedToolName, namespacedToolName, toolChoiceToolPredicate } from "../../types"; + import type { OcxParsedRequest } from "../../types"; + import type { TranslatorBudget } from "../../lib/translator-budget"; + ``` + +2. `src/server/responses/developer-message-insertion.ts`: `isRecord`, `generatedDeveloperText`, `isGeneratedDeveloperItem`, `isDeveloperPrefixItem`, `leadingDeveloperPrefixLength`, `isConversationalItem`, `statefulRawInsertionIndex`, `injectDeveloperMessage`, original **529–622** = 94 lines; one import + one blank = **96 lines**. Only `injectDeveloperMessage` is exported. + + ```ts + import type { OcxParsedRequest } from "../../types"; + ``` + +3. `src/server/responses/subagent-roster-text.ts`: `subagentRosterText`, original **513–525** = **13 lines**; own imports **none**. This small pure renderer has a real local consumer and an existing exported contract, not a generic helper bucket. It is separate from config/catalog orchestration so it introduces no runtime catalog dependency. + +Residual: keep every other line, adding three named re-exports and one local import: **622 - (127 + 94 + 13) + 4 = 392 lines**. Total **131 + 96 + 13 + 392 = 632 = 622 + 10**. Raw diff estimate **234 deletions + 244 additions = 478** before formatter differences; do not widen it by pruning/reformatting the 102-line import prelude. No #b needed. Existing named sibling convention checked against `src/server/responses/{input-admission,encrypted-payload,agent-task-recovery-cache,context-overflow}.ts` and `src/config/*.ts` / `src/types/*.ts`. + +## Re-export block + +Exact added compatibility exports: + +```ts +export { buildToolBridgeMaps } from "./tool-bridge-maps"; +export { injectDeveloperMessage } from "./developer-message-insertion"; +export { subagentRosterText } from "./subagent-roster-text"; +``` + +Explicit local binding required by the retained `multiAgentGuidanceText` call at original line 444: + +```ts +import { subagentRosterText } from "./subagent-roster-text"; +``` + +No local imports of `buildToolBridgeMaps` or `injectDeveloperMessage`: the residual never calls them. Preserve the remaining nine exported declarations verbatim: `PROACTIVE_MULTI_AGENT_MODE_TEXT`, `isV1CollabSurface`, `collabSurface`, `MultiAgentGuidanceOptions`, `MultiAgentGuidanceDeps`, `resolveEffectiveSubagentRoster`, `multiAgentGuidanceText`, `V2_GUIDANCE_CHAR_BUDGET`, `applyInjectionPlaceholders`. Both type exports therefore remain direct declarations, not fabricated `export type ... from` lines. `src/server/responses.ts:6–7` stays unchanged, preserving the facade's deliberately smaller export set. + +## Module-level state and cycles + +No top-level mutable Map/Set/WeakMap/let/lock/timer. `PROACTIVE_MULTI_AGENT_MODE_TEXT` at `src/server/responses/collaboration.ts:235–241` and `V2_GUIDANCE_CHAR_BUDGET` at line 501 remain single immutable scalar owners in residual collaboration. + +The Maps/Sets at 115–121, 131, 203 and 208 remain local to each `buildToolBridgeMaps` call in its new owner; they are not promoted to module scope. The roster formatter's Set at 515 stays local to its call. The catalog snapshot captured at 353 and candidate Set at 418 stay request-local in the residual. No second roster snapshot, lazy cache or duplicated budget collector is introduced. + +No leaf imports collaboration or the `../responses` facade, even for types. Maps imports existing types and a type-only budget; insertion imports the existing parsed-request type; roster text imports nothing. Intended edges are functional and downward. A type import from collaboration into any leaf would create a prohibited facade cycle. Retain the existing dynamic imports at 302, 312, 330–331 and 352 in their original owner and timing; they are not a new cycle-avoidance hack. Lane 011 reported no cycle in the literal graph; recheck changed static/type/dynamic paths before readiness. Core remains a protected root with no new Lab reachability. + +## Tests + +Direct importer list from `rg -l 'responses/collaboration["\x27]' tests | sort` (**2**, both **unchanged**): + +```text +tests/routing/subagent-context-staleness.test.ts +tests/server/server-combo-failover-e2e.test.ts +``` + +The latter dynamically imports this module at line 1752; count it in fan-in even though 002's static `from` command misses it. Additional unchanged tests using collaboration exports through `src/server/responses.ts`, found by `rg -l 'buildToolBridgeMaps|injectDeveloperMessage|multiAgentGuidanceText|collabSurface' tests` and import inspection: + +```text +tests/adapters/adapter-buffered-tool-conformance.test.ts +tests/adapters/adapter-tool-conformance.test.ts +tests/codex-integration/effort-policy.test.ts +tests/codex-integration/multi-agent-compat.test.ts +tests/responses/responses-parser.test.ts +tests/responses/responses-state.test.ts +``` + +No direct source-text oracle was found by literal/segmented collaboration path search among source-reading tests (lane 011 agrees). Transitive source readers still apply: + +| test and exact read | disposition | action | +|---|---|---| +| `tests/lab/core-lab-boundary.test.ts:69` | unchanged | Existing runtime re-export/import traversal discovers all three leaves. PROTECTED roots untouched. | +| `tests/codex-integration/compatibility-manifest.test.ts:61` | unchanged | Leaves join the reachable scan without manual scan-list entries. | + +No retarget-to-leaf or explicit add-leaf-to-scan-list needed; no new test-layout entries. Drive insertion guard `tests/codex-integration/multi-agent-compat.test.ts:1125` red once by temporarily weakening the exact guidance predicate in the new insertion leaf, then restore and green. Also keep placement cases at 1029, 1043, 1075, 1092 and 1105 unchanged. Temporarily corrupt a tool namespace map result and confirm the existing adapter conformance assertions fail; restore the moved body. Red/green the two graph guards with temporary forbidden edges in a reachable leaf, without editing protected roots. Run the roster/staleness behavioral tests through their existing boundary, not a new test-only import. + +## Verification + +Future implementation gate only; no tests run during drafting: + +```sh +bun run typecheck +bun test tests/routing/subagent-context-staleness.test.ts tests/server/server-combo-failover-e2e.test.ts tests/adapters/adapter-buffered-tool-conformance.test.ts tests/adapters/adapter-tool-conformance.test.ts tests/codex-integration/effort-policy.test.ts tests/codex-integration/multi-agent-compat.test.ts tests/responses/responses-parser.test.ts tests/responses/responses-state.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/server/responses/tool-bridge-maps.ts src/server/responses/developer-message-insertion.ts src/server/responses/subagent-roster-text.ts src/server/responses/collaboration.ts +rg -n 'from "[^"]*/collaboration"' src gui/src scripts tests | wc -l +rg -n 'import\("[^"]*/collaboration"\)' src gui/src scripts tests +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-server-responses-collaboration && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Domains: routing, server, adapters, codex-integration, Responses and Lab boundary. Baseline **4 distinct consumer files**, including the dynamic import; a static line count alone is not the fan-in count (two re-export lines also share one facade file). Typecheck proves all old exports resolve. Compare all moved AST bodies, unchanged residual statements and retained dynamic imports; resolve new static/type/literal-dynamic edges and reject a return path. Record explicit authorization-boundary review for `buildToolBridgeMaps`. Remote full suite only; tested SHA must equal PR head, pipeline exit 0, 0 failures and retained complete output; exact-head CI rollup required before readiness. + +## Accept criteria + +1. All 22 declarations have exactly one owner; all 12 current exports and the existing facade subset retain names, signatures and import paths. +2. New files <=400 (131/96/13 planned), residual <=400 (392 planned); raw changeset stays <=500 after wiring/formatting or escalates before publication. +3. No global collectors, altered budget charging, extra catalog reads or changed dynamic import timing; no new return edge or Lab reachability. +4. Guidance text, roster rendering, authorization, replay deduplication and raw/parsed insertion ordering are byte/behavior preserved; no unrelated import pruning. +5. Direct and facade behavioral tests remain unchanged; designated mutation and graph guards prove red then green without weakening assertions. +6. Typecheck, focused tests, privacy scan, remote exact-head full suite, boundary review and exact-head CI evidence pass at this layer's own tip; base points to L3. + +## PR + +Title: `refactor(server-responses): separate collaboration map and insertion leaves (split S07 L4/4)` + +Branch: `codex/split-server-responses-collaboration`. Base: `codex/split-responses-parser`. Closes: none. Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification and Checklist. Depends on #TBD-S07-L1; review only this layer's diff. + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S07-L4 | codex/split-server-responses-collaboration — this layer | codex/split-responses-parser | Tool maps, roster rendering, insertion | +| 3 | #TBD-S07-L3 | codex/split-server-responses-agent-task-recovery | dev | Envelope codec ownership | +| 2 | #TBD-S07-L2 | codex/split-responses-namespace-tool-compat | dev | Restoration and alias contract | +| 1 | #TBD-S07-L1 | codex/split-responses-parser | dev | Private parser leaves; size escalation | + +DEV-STACK-02/03: cascade changes from the real parent `codex/split-responses-parser` (#TBD-S07-L1) into `codex/split-server-responses-collaboration` and refresh exact head/base evidence. No dependency on L2 or L3; no cascade obligation from either. Merge the parent before this layer, only with separate user authorization. No Git or PR mutation is performed by this delegated task. diff --git a/devlog/_plan/260905_now_split_train/250_claude_inbound.md b/devlog/_plan/260905_now_split_train/250_claude_inbound.md new file mode 100644 index 0000000000..1d68ca3629 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/250_claude_inbound.md @@ -0,0 +1,223 @@ +# S08 L1/2 — Claude inbound translation leaves + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architectural planning, docs-only delegated task. Parent owns orchestration, loop and goal state. This document is not permission to run them. +- Goal: split `src/claude/inbound.ts` (578 lines) into three small implementation leaves while preserving the original import surface and every translation result. +- Non-goals: changing classifier affinity, blocked-skill policy, output-schema acceptance, thinking effort, cache-key construction, validation, or exported signatures; no source/test edits or test execution during this planning task. +- Verifier: `002_layer_map.md`, **Per-layer gate**, instantiated below. `000_plan.md`'s reference to 003 is stale; 002 actually owns the gate. +- Stop: the implementation layer has its own exact-tip verification evidence and an open PR; never merge. Stop this delegated task after writing and statically checking its assigned documents. +- Escalation: stop implementation if the source basis drifts, any named export disappears, a leaf needs an upward import, the residual exceeds 400, or actual changed source lines exceed 500. Do not silently borrow S08 L2 for inbound leftovers. +- Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a38`. Both assigned source files were byte-compared with `git show origin/dev:` and match the working tree. All source coordinates below refer to that origin/dev snapshot. Lane evidence: `devlog/_plan/260905_modular_debt_ledger/011_lane_server_responses.md`, `src/claude/inbound.ts` subsection, identifies content/tool and directive seams and the two module-level Sets. + +Structural decision: the 578-line module combines boundary options/model resolution with content sequencing. Leaving it alone or merely configuring it cannot meet the size gate; deleting behavior is out of scope. Move existing declarations only. Keep the larger content/elision pipeline and cache construction together, and extract the smaller boundary-option groups. This avoids a larger content move whose additions plus deletions would threaten the 500-line layer budget. No generic utility or internal index barrel is introduced. + +Current map: `src/server/claude-messages.ts:13`, `src/lab/conformance/executor.ts:4`, and `src/claude/agents-inject.ts:22`, plus five behavioral tests, consume inbound. Inbound imports alias/context/Desktop resolution, the output-schema predicate, outbound WebSearch naming, shared config types, and crypto (`src/claude/inbound.ts:12–18`). Intended direction: these consumers → original compatibility facade/residual → `inbound/model-options.ts`, `inbound/content-options.ts`, `inbound/records.ts`; options leaves → records and their existing lower-level dependencies. Lab remains a consumer, never a dependency. Blast radius: one feature module, no wire/API changes. + +Convention evidence: `src/server/responses.ts:1–13` preserves original-path exports over a same-name subdirectory; `src/config/paths.ts:1–5` uses direct leaf imports. Use the same pattern, not an `inbound/index.ts`. The facade-plus-residual is explicitly required by this train, overriding the generic pure-barrel preference. + +## Symbol inventory + +Ranges are declaration starts/ends, not leading JSDoc. Obtained with `sg run --lang ts --kind function_declaration --json=compact`, plus `lexical_declaration`, `type_alias_declaration`, `interface_declaration`, and an anchored `rg` declaration scan. Imports at 12–18 are dependencies, not locally owned declarations. + +Consumer counts are distinct external source/test files referencing the symbol among verified importers of this exact module, not occurrence counts or same-named symbols from `src/chat/inbound.ts`. Method: `rg -l 'claude/inbound["\x27]' src gui/src scripts tests`, plus sibling `rg -n 'from "./inbound"' src/claude`, then `rg -l -w '' `. Private declarations have zero external consumers. Existing module fan-in is **8 files: 3 source + 5 test**. `R` means residual `src/claude/inbound.ts`; other target names are under `src/claude/inbound/`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| AnthropicRequestError | class | 20–20 | yes | 2 | records.ts | +| Rec | type | 22–22 | no | 0 | records.ts | +| isRec | function | 24–26 | no | 0 | records.ts | +| isClaudeClassifierModel | function | 28–31 | no | 0 | model-options.ts | +| configuredClassifierRoute | function | 47–56 | no | 0 | model-options.ts | +| resolveInboundModel | function | 59–89 | yes | 5 | model-options.ts | +| effortForThinkingBudget | function | 92–96 | yes | 1 | model-options.ts | +| OUTPUT_CONFIG_EFFORTS | const Set | 104–104 | no | 0 | model-options.ts | +| effortFromOutputConfig | function | 105–109 | yes | 0 | model-options.ts | +| formatFromOutputConfig | function | 111–120 | no | 0 | model-options.ts | +| systemToInstructions | function | 122–132 | no | 0 | content-options.ts | +| imageBlockToInputImage | function | 134–145 | no | 0 | R | +| toolResultOutput | function | 147–171 | no | 0 | R | +| pushUserMessage | function | 173–176 | no | 0 | R | +| DEFAULT_BLOCKED_SKILLS | const array | 186–186 | yes | 0 | R | +| effectiveBlockedSkillNames | function | 189–195 | yes | 1 | R | +| OCX_ROUTE_RE | const RegExp | 204–204 | no | 0 | model-options.ts | +| OCX_EFFORT_RE | const RegExp | 205–205 | no | 0 | model-options.ts | +| systemText | function | 207–217 | no | 0 | model-options.ts | +| extractOcxRouteDirective | function | 219–224 | yes | 2 | model-options.ts | +| extractOcxEffortDirective | function | 232–237 | yes | 2 | model-options.ts | +| SKILL_ELISION_MIN_CHARS | const number | 240–240 | no | 0 | R | +| SKILL_TEXT_MARKER | const string | 241–241 | no | 0 | R | +| SkillElisionContext | interface | 243–248 | no | 0 | R | +| NO_ELISION | const object containing Set | 250–250 | no | 0 | R | +| maybeElideSkillText | function | 259–270 | no | 0 | R | +| skillElisionStub | function | 272–276 | no | 0 | R | +| blockedSkillCallIds | function | 279–294 | no | 0 | R | +| systemMessageText | function | 303–311 | no | 0 | R | +| userMessageToItems | function | 313–357 | no | 0 | R | +| assistantMessageToItems | function | 359–392 | no | 0 | R | +| toolsToResponses | function | 394–416 | no | 0 | content-options.ts | +| toolChoiceToResponses | function | 418–438 | no | 0 | content-options.ts | +| canonicalJson | function | 441–448 | no | 0 | R | +| ClaudeCacheKeySource | exported type | 451–451 | yes | 1 | R | +| ClaudeInboundTranslation | exported interface | 453–456 | yes | 0 | R | +| anthropicToResponsesBody | function | 462–464 | yes | 2 | R | +| anthropicToResponsesTranslation | function | 471–578 | yes | 4 | R | + +## Leaf partition + +All counts include comments and allow import/export glue; these are planning ceilings to verify with `wc -l`, not claims about files already created. No #a/#b subdivision is needed for this layer. + +| new file | symbols | move slices at origin/dev | expected lines including glue | +|---|---|---|---:| +| `src/claude/inbound/records.ts` | AnthropicRequestError, Rec, isRec | 20–26 = 7 lines | 10 | +| `src/claude/inbound/model-options.ts` | isClaudeClassifierModel, configuredClassifierRoute, resolveInboundModel, effortForThinkingBudget, OUTPUT_CONFIG_EFFORTS, effortFromOutputConfig, formatFromOutputConfig, OCX_ROUTE_RE, OCX_EFFORT_RE, systemText, extractOcxRouteDirective, extractOcxEffortDirective | 28–120 + 197–237 = 134 lines | 144 | +| `src/claude/inbound/content-options.ts` | systemToInstructions, toolsToResponses, toolChoiceToResponses | 122–132 + 394–438 = 56 lines | 64 | + +Residual original expected **390 lines**, no #b: 578 − 197 moved − 5 obsolete import lines (13–17) + up to 14 lines of facade/import glue = 390. Leaf projections total 218; total projected footprint is 608, versus 578 originally. New code consists only of imports/exports and declaration visibility needed across leaves. Estimated source churn is approximately 450 lines; measure actual diff rather than relying on this estimate for the 500 gate. Existing long functions are not rewritten merely to meet the separate function-size guideline. + +Own imports of `records.ts`: none. `Rec` and `isRec` become leaf exports only; do not add them to the original public export surface. + +Own imports of `model-options.ts`: + +```ts +import type { OcxClaudeCodeConfig } from "../../types"; +import { isAnthropicOutputSchema } from "../../adapters/anthropic-output-schema"; +import { resolveAlias } from "../alias"; +import { stripOneMillionMarker } from "../context-windows"; +import { resolveDesktop3pAlias } from "../desktop-3p"; +import { isRec, type Rec } from "./records"; +``` + +Own imports of `content-options.ts`: + +```ts +import { isClaudeWebSearchToolName } from "../outbound"; +import { AnthropicRequestError, isRec, type Rec } from "./records"; +``` + +Move comments with their declarations. `formatFromOutputConfig`, `systemToInstructions`, `toolsToResponses`, and `toolChoiceToResponses` become direct leaf exports for their production caller, not facade exports or test-only APIs. Existing adapter/alias/outbound owners are reused; no replacement schema validator, alias registry, WebSearch recognizer, or new dependency is planned. + +## Re-export block + +Add these exact named re-exports to `src/claude/inbound.ts`: + +```ts +export { AnthropicRequestError } from "./inbound/records"; +export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, extractOcxRouteDirective, extractOcxEffortDirective } from "./inbound/model-options"; +``` + +The existing declarations continue exporting `DEFAULT_BLOCKED_SKILLS`, `effectiveBlockedSkillNames`, `ClaudeCacheKeySource`, `ClaudeInboundTranslation`, `anthropicToResponsesBody`, and `anthropicToResponsesTranslation`. No moved public type requires `export type`; the two public types stay local. All **12** current public identifiers remain importable at the original path. + +Re-exporting does not bind local names. The complete residual imports are: + +```ts +import type { OcxClaudeCodeConfig } from "../types"; +import { createHash } from "node:crypto"; +import { AnthropicRequestError, isRec, type Rec } from "./inbound/records"; +import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound/model-options"; +import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound/content-options"; +``` + +## Module-level state and cycles + +- `OUTPUT_CONFIG_EFFORTS`, origin `src/claude/inbound.ts:104`: one Set owned by `model-options.ts`; retain its allocation and membership semantics. Do not clone it into the residual. +- `NO_ELISION`, origin `:250`: the original residual remains the sole owner of this object and its `callIds` Set. `userMessageToItems`'s default stays attached to that same object. +- `DEFAULT_BLOCKED_SKILLS` (`:186`) is a publicly exported array, not a frozen value. Preserve its object identity and existing mutability in the residual; do not make a copy or freeze it as part of the move. +- `OCX_ROUTE_RE` / `OCX_EFFORT_RE` (`:204–205`) move to model-options with systemText; preserve the absence of global/sticky flags. `SKILL_ELISION_MIN_CHARS` / `SKILL_TEXT_MARKER` (`:240–241`) stay residual. +- No top-level let, Map, WeakMap, timer, or lock exists. Sets created inside effectiveBlockedSkillNames/blockedSkillCallIds (`:191`, `:280`) remain per invocation. +- The common error constructor is the key cycle seam: content-options must not import `AnthropicRequestError` from `../inbound`, and model-options must not import `Rec`/`isRec` from the residual. Both import records directly. This also preserves `instanceof AnthropicRequestError` at `src/server/claude-messages.ts:714`. +- Lane 011 found no cycle in its literal graph. New edges are functional/sequential coupling; the public array is an existing shared-identity contract, not a new shared store. The original module may import its leaves; leaves may not import the original or each other's consumer. `src/lab/conformance/executor.ts` continues importing the public original, never the reverse. + +## Tests + +Exact behavioral importer list from `rg -l 'claude/inbound["\x27]' tests` (sorted); all **unchanged**, including the dynamic/require site: + +| test file | import/use line | disposition | +|---|---:|---| +| `tests/adapters/anthropic/anthropic-reasoning.test.ts` | 5 | unchanged | +| `tests/claude-integration/claude-alias.test.ts` | 12 | unchanged | +| `tests/claude-integration/claude-inbound.test.ts` | 2; 512 require/type import | unchanged | +| `tests/clients/desktop-3p.test.ts` | 19 | unchanged | +| `tests/routing/routing-policy-surface-parity.test.ts` | 4 | unchanged | + +Direct text-oracle result: **none found** after `rg -n 'inbound.ts|claude/inbound' tests` and segmented-path/readFileSync/Bun.file/source-call inspection. The similarly named chat inbound imports are not this module. No direct oracle is retargeted and no existing literal scan list needs a leaf entry. + +Transitive source oracle: `tests/codex-integration/compatibility-manifest.test.ts:61` reads each reached file with `readFileSync(current, "utf8")`; root list at 182–188 includes server/index, whose `:178` import reaches claude-messages then inbound. **Unchanged**: its runtime re-export/import walker follows the three new leaves automatically. Drive this guard red once during implementation by temporarily adding a static named import/re-export from `../../compatibility/manifest` in `inbound/model-options.ts`, observe the forbidden-chain failure, then remove the probe and obtain green. Do not commit the probe. + +`tests/lab/core-lab-boundary.test.ts:69` is a generic reachable-graph reader, but the inspected protected roots do not currently reach inbound; do not claim it is a direct inbound text oracle or edit PROTECTED to make it one. Keep the roots untouched. No new tests are required for pure moves. If an executor needs a new test file, report the scope expansion and register it in both layout registries. + +Behavior guard sensitivity: in the existing inbound test, temporarily break the moved hosted WebSearch choice branch (`src/claude/inbound.ts:432` origin), confirm `tests/claude-integration/claude-inbound.test.ts:197` fails, then restore and rerun. The existing malformed-input cases at `:326` also check the shared error identity. These are future red/green instructions, not tests run by this docs task. + +## Verification + +Run only in the executor's dedicated layer worktree. Domains: claude-integration, clients, routing, adapters/anthropic, lab (conformance consumer), codex-integration (source graph). Instantiate 002's gate as follows: + +```sh +bun run typecheck +bun test tests/claude-integration tests/clients/desktop-3p.test.ts tests/routing/routing-policy-surface-parity.test.ts tests/adapters/anthropic/anthropic-reasoning.test.ts tests/lab/lab-conformance-harness.test.ts tests/lab/lab-conformance-runner-failures.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun run privacy:scan +wc -l src/claude/inbound.ts src/claude/inbound/records.ts src/claude/inbound/model-options.ts src/claude/inbound/content-options.ts +rg -n 'claude/inbound["\x27]|from "./inbound"' src gui/src scripts tests +git diff --check +git diff --numstat dev...HEAD -- src +``` + +Original-path importer identity/count must remain the same 8 files, not be diluted by new internal leaf imports. Compare the sorted importer list, not just a regex total. The protected-root test is conditional in 002 and is not required by this src/claude-only write set; run it if implementation expands to src/server/src/lib/src/router. Leaf-to-parent import scan must have no hits, and an import-graph comparison must find no newly introduced cycle (include type and literal dynamic edges); typecheck alone is not cycle proof. + +Full suite is **never local**. On the parent-approved remote checkout, use branch `codex/split-claude-inbound`: + +```sh +ssh lidge 'set -e; cd ~/ocx-ci/opencodex; git fetch origin codex/split-claude-inbound; git checkout -q FETCH_HEAD; git rev-parse HEAD; bun install --frozen-lockfile >/dev/null; bun run test' +``` + +Require the printed remote SHA to equal the recorded PR head and preserve the real test exit status (002's illustrative `| tail -15` alone can mask a failure). Record focused counts, remote full-suite result, privacy/typecheck exit statuses, red/green evidence, sizes, and green exact-head CI rollup. No implementation checks were executed while drafting this document; documentation verification is heading/inventory/path/count consistency only. Fresh read-only Node/ast-grep checks on 2026-09-05 confirmed nine ordered headings, all 38 exact declaration ranges, 12 public identifiers, all named test paths present, 14 relative import/re-export paths resolving to existing or explicitly planned files, and no trailing whitespace (exit 0). + +## Accept criteria + +1. Exactly three new leaves are added and no source outside this partition is changed; each leaf and residual is ≤400 lines (projected residual 390), with no hidden #b. +2. All 38 top-level declarations above have exactly one owner; the 12 original public identifiers and signatures are unchanged. Existing original-path importers remain 8 identical files. +3. Function bodies and serialized results are unchanged; only location, imports, exports and necessary relative paths change. Shared error identity and exported-array identity are preserved. +4. No leaf imports the residual, no new cycle is introduced, and no Lab/compatibility-catalog dependency enters the ordinary path. No protected-root edit is included. +5. Source-oracle walker still reaches moved code; planned sensitivity probes fail once and restored code passes. Existing behavioral import paths remain unchanged. +6. Every instantiated 002 gate passes at the layer SHA; no full suite runs locally and no previous/head-mismatched result substitutes for evidence. +7. Actual additions plus deletions of changed source are ≤500; if not, the parent must approve a revised slice before implementation proceeds. Open PR carries the stack map, all template sections and exact-head evidence; no merge is performed. + +## PR + +Title: `refactor(claude): separate inbound options from content translation (split S08 L1/2)` + +Branch: `codex/split-claude-inbound`. Base: `dev`. Closes: none. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. Review this layer's diff only. Stack map (DEV-STACK-03; placeholder numbers are intentional until PR creation): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 2 | #TBD-S08-L2 | server Claude messages | `codex/split-server-claude-messages` | `codex/split-claude-inbound` | native/body/count/replay ownership | +| 1 | #TBD-S08-L1 | inbound ← this layer | `codex/split-claude-inbound` | `dev` | option leaves and stable inbound exports | + +L2 depends on #TBD-S08-L1. Lower-layer changes require a parent-owned cascade and renewed exact-head verification; no cascade, push, PR creation, or merge is performed by this delegated docs task. + +## P stale-check (2026-09-05, wp250) + +origin/dev a594a7f21; inbound.ts unchanged since 445742966 (578 lines); anchors 20/26/28/120/122/132/197/237/394/438 confirmed by sed. Base `dev` (S08 bottom; 260 claude-messages chains on it). Naming: src/claude has no subdirectories today (all flat); the audit decides subdirectory `src/claude/inbound/` vs flat siblings `src/claude/inbound-records.ts`, `inbound-model-options.ts`, `inbound-content-options.ts` (there is already a flat `inbound-debug.ts` sibling). Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change (extend tests/claude-integration/claude-inbound.test.ts). + +## A amendment (Maxwell audit, GO-WITH-FIXES blockers=2 → folded; naming adopted) + +1. Size gate: the raw ≤500 wording in Loop spec / Accept criteria is void; 003 PURE-MOVE-SIZE-01 binds (197 relocated lines; non-move diff ≤150; move-aware diff + exactly-once ownership as evidence). +2. Error identity: the moved throw at inbound.ts:427 (toolChoiceToResponses, `tool_choice: { type: "tool" }` without a name) is not covered by the existing :326 cases. The required test extension in tests/claude-integration/claude-inbound.test.ts adds a valid request with that tool_choice shape and asserts the thrown value is an instance of the facade-exported AnthropicRequestError, plus seam identity via leaf vs facade and a no-back-edge check on the leaves. +3. Naming adopted: flat siblings `src/claude/inbound-records.ts`, `inbound-model-options.ts`, `inbound-content-options.ts` (matching the existing flat inbound-debug.ts). Leaf import paths become one level shallower (`../types`, `../adapters/anthropic-output-schema`, `./alias`, `./context-windows`, `./desktop-3p`, `./outbound`, `./inbound-records`); facade re-exports/imports use `./inbound-records`, `./inbound-model-options`, `./inbound-content-options`. +Audit-verified structure: 38/38 ranges; partition 7/134/56/381 covering 1–578 once; imports 13–17 all obsolete in the residual; leaf own-imports complete (model-options does not use AnthropicRequestError); residual keeps createHash and OcxClaudeCodeConfig; 12 public identifiers preserved; outbound/desktop-3p closures do not reach inbound. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-250.d5gu6i/wt` (branch `codex/split-claude-inbound`, base origin/dev a594a7f21). Executor: gpt-6-astra high (Kuhn, 01a06f5e-f95a-7b50-abea-1f4174183bfe). +- Commits: 2e6dfa6c5 (move: inbound-records.ts 7, inbound-model-options.ts 142, inbound-content-options.ts 60, inbound.ts 381) and c0fab2d74 (test: claude-inbound.test.ts +14 — tool_choice {type:"tool"} throws the facade AnthropicRequestError; leaf/facade identity; leaves have no ./inbound back-edge). Diff: 5 files, +228/−202; non-move 36 lines. +- Local gate: typecheck 0; focused (5 importer files + claude-messages-endpoint) 136+42 pass / 0 fail; guards (compatibility-manifest + core-lab-boundary) 23/0; privacy passed; 7 original-path importers unchanged. +- Red-drives: (a) hosted WebSearch tool_choice branch broken → claude-inbound.test.ts:211 (orig 208) fails, restored 32/0; (b) compatibility import in inbound-model-options → compatibility-manifest:191 chain server/index → claude-messages → inbound → inbound-model-options → compatibility/manifest, restored 23/0. + +- Adversarial diff review (Mill, gpt-6-astra high, 01a06f63-77fa-7002-9d29-5844130d06c7): VERDICT: PASS (slices exact, residual byte-exact, 12 public ids preserved / 6 internal seams not leaked, 347-file walk no cycle, new test reaches the moved throw at inbound-content-options.ts:49). +- lidge full suite at c0fab2d74: SUITE_EXIT=0 — 18061 pass / 0 fail / 16 skip, 7 serial files finished (/tmp/suite-split-250.log; the SSH pipe was killed after completion, totals read from the retained log). +- PR: https://github.com/lidge-jun/opencodex/pull/3583 (base dev, head c0fab2d74). CI rollup at record time: OPEN draft=false c0fab2d74 =1 =6 SKIPPED=2 SUCCESS=20 diff --git a/devlog/_plan/260905_now_split_train/260_server_claude_messages.md b/devlog/_plan/260905_now_split_train/260_server_claude_messages.md new file mode 100644 index 0000000000..7bcdebe2ed --- /dev/null +++ b/devlog/_plan/260905_now_split_train/260_server_claude_messages.md @@ -0,0 +1,264 @@ +# S08 L2/2 — Claude Messages transport and replay leaves + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 module-boundary planning with C4-level scrutiny of unchanged credential forwarding. Parent owns orchestration, loop and goal state; this task only writes its assigned documents. +- Goal: preserve every exported symbol and both Anthropic endpoint contracts while moving the 1,092-line `src/server/claude-messages.ts` into five implementation leaves and a small original-path entry/facade. +- Non-goals: changing admission, credentials, image normalization, routing, replay, budgets, timeout defaults, usage, error mapping, SSE ordering, or exported signatures; no implementation or tests during this task. +- Verifier: `002_layer_map.md`, **Per-layer gate**, instantiated below. The reference to 003 in 000 is stale; use 002. +- Stop: after the parent resolves the layer-size conflict, implementation ends only with its own exact-tip gates and an open PR; never merge. This delegated task stops after documentation consistency checks. +- **Escalation — implementation blocked on changeset size:** 002 gives this file one layer and requires ≤500 changed source lines. Reducing 1,092 to ≤400 requires removing at least 692 lines from the original even before adding any leaves. Thus no pure-move partition can satisfy both limits in this assigned L2. The complete endpoint partition below is a proposed diff, not a claim that this conflict is resolved. Parent must explicitly approve a move-only size exception, or revise the layer map with additional #a/#b parts. Do not silently reinterpret changed lines as only novel logic, invent an unassigned #b, or leave a >400 residual without a scheduled owner. +- Basis: docs HEAD `4cc219549`; code `origin/dev = 1362b1a38`, byte-equal to the working tree for this source. All source coordinates are origin/dev coordinates. Read `000_plan.md`, `001_stale_check.md`, S08 rows and gate in `002_layer_map.md`, and the `src/server/claude-messages.ts` subsection of `devlog/_plan/260905_modular_debt_ledger/011_lane_server_responses.md`. + +Structural decision: the existing file combines request reading, credential-gated native transport, bounded bodies, translated replay, and token counting. No-op/configuration cannot meet the size gate, and deleting code changes behavior. Choose declaration-only extraction at those existing seams; reject splitting the 364-line replay function into new state-passing stages, because that introduces closure/control-flow changes unnecessary for a pure move. Keep its body intact and budget its imports to stay ≤400. The five same-directory leaves use `src/server/claude-messages/`, following the facade/subdirectory pattern at `src/server/responses.ts:1–13`, not a new internal index barrel. + +Current map: `src/server/index.ts:178` and five behavioral tests consume this module. Its dependencies include inbound translation (`:13`), outbound (`:20–26`), auth-cors (`:38–43`), decompression (`:33`), logging (`:34–36`), responses replay (`:37`), routing (`:29–31`), image handling (`:11–12`), and request-scoped budgets (`:47–52`). Intended direction: original facade → replay → count-tokens/native/request-context; count-tokens → native/request-context; native → body/request-context; body → request-context; request-context → existing inbound/outbound/decompression owners. No leaf imports the facade. Public boundaries remain the same two routes and original TypeScript import path. Blast radius: one server feature. Runtime/transport context: `structure/01_runtime.md:10`, `structure/04_transports-and-sidecars.md:1407`. + +## Symbol inventory + +Every top-level owned declaration is listed (imports at 9–61 are listed per destination below). Exact ranges come from `sg run --lang ts --kind function_declaration --json=compact` plus lexical/type/interface kinds, checked against `git show origin/dev:src/server/claude-messages.ts | nl -ba`. Declaration ranges exclude leading comments. + +Consumers = distinct external files among the verified `rg -l '/claude-messages["\x27]' src gui/src scripts tests` importers, counted with `rg -l -w '' `. Private declarations are zero, rather than accidental matches for common names elsewhere. Fan-in: **6 files = 1 source + 5 tests**. Leaf names below are under `src/server/claude-messages/`; `R` means the original residual file. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| Rec | type | 63–63 | no | 0 | request-context.ts | +| decodeClaudeFastSelector | function | 73–79 | no | 0 | request-context.ts | +| isRec | function | 81–83 | no | 0 | request-context.ts | +| buildClaudeReplayConfig | function | 86–98 | yes | 1 | request-context.ts | +| claudeInboundDisabled | function | 100–105 | no | 0 | request-context.ts | +| readAnthropicBody | async function | 107–114 | no | 0 | request-context.ts | +| PASSTHROUGH_STRIP_HEADERS | const Set | 124–128 | no | 0 | native.ts | +| singleCredentialToken | function | 130–141 | no | 0 | native.ts | +| hasAnthropicNativeCredential | function | 143–148 | no | 0 | native.ts | +| wantsNativePassthrough | function | 150–168 | no | 0 | native.ts | +| shouldForwardNativeHeader | function | 170–176 | no | 0 | native.ts | +| uuidFromHex | function | 179–182 | no | 0 | request-context.ts | +| anthropicUsageToOcx | function | 184–201 | no | 0 | body.ts | +| PassthroughBodyGuard | interface | 204–211 | yes | 0 | body.ts | +| PassthroughCloseReason | type | 213–213 | no | 0 | body.ts | +| tapAnthropicSseForLog | function | 224–364 | yes | 1 | body.ts | +| anthropicNativePassthrough | async function | 366–459 | no | 0 | native.ts | +| DEFAULT_BODY_STALL_SEC | const number | 461–461 | no | 0 | body.ts | +| DEFAULT_BODY_MAX_BYTES | const number | 462–462 | no | 0 | body.ts | +| resolvePassthroughBodyGuard | function | 469–483 | yes | 1 | body.ts | +| BoundedPassthroughBody | type | 485–489 | no | 0 | body.ts | +| readBoundedPassthroughBody | async function | 497–552 | yes | 1 | body.ts | +| HeaderDeadlineFetchResult | type | 566–569 | yes | 0 | native.ts | +| fetchWithHeaderDeadline | async function | 571–589 | yes | 2 | native.ts | +| handleClaudeMessages | async function | 591–608 | yes | 4 | R | +| handleClaudeMessagesWithBudget | async function | 610–973 | no | 0 | replay.ts | +| estimateBase64AttachmentTokens | function | 978–983 | no | 0 | count-tokens.ts | +| estimateClaudeRequestTokens | function | 995–1035 | yes | 1 | count-tokens.ts | +| handleClaudeCountTokens | async function | 1037–1092 | yes | 1 | count-tokens.ts | + +## Leaf partition + +Counts are expected ceilings including moved comments and new import/export glue, to be measured during implementation. All five new leaves and the residual fit ≤400 without changing any function body. This is one proposed oversized pure-move layer, subject to the explicit size escalation above; no hidden #b is included. + +| new file | symbols | origin slices incl. attached comments | expected lines | +|---|---|---|---:| +| `src/server/claude-messages/request-context.ts` | Rec, decodeClaudeFastSelector, isRec, buildClaudeReplayConfig, claudeInboundDisabled, readAnthropicBody, uuidFromHex | 63–114 + 178–182 = 57 | 70 | +| `src/server/claude-messages/body.ts` | anthropicUsageToOcx, PassthroughBodyGuard, PassthroughCloseReason, tapAnthropicSseForLog, DEFAULT_BODY_STALL_SEC, DEFAULT_BODY_MAX_BYTES, resolvePassthroughBodyGuard, BoundedPassthroughBody, readBoundedPassthroughBody | 184–364 + 461–552 = 273 | 285 | +| `src/server/claude-messages/native.ts` | PASSTHROUGH_STRIP_HEADERS, singleCredentialToken, hasAnthropicNativeCredential, wantsNativePassthrough, shouldForwardNativeHeader, anthropicNativePassthrough, HeaderDeadlineFetchResult, fetchWithHeaderDeadline | 116–176 + 366–459 + 554–589 = 191 | 210 | +| `src/server/claude-messages/count-tokens.ts` | estimateBase64AttachmentTokens, estimateClaudeRequestTokens, handleClaudeCountTokens | 975–1092 = 118 | 135 | +| `src/server/claude-messages/replay.ts` | handleClaudeMessagesWithBudget | 610–973 = 364 | 399 | + +Residual original expected **45 lines**: keep header 1–8 and public wrapper 591–608 (26 source lines) plus up to 19 glue lines. Original 1,092 = 1,003 moved lines + 26 retained lines + 63 old import/spacing lines. Planned maximum footprint: 1,099 leaf lines + 45 residual = 1,144; 115 replacement glue lines versus 63 original import/spacing lines. Existing comments move, not disappear to game the limit. Replay gets ≤35 import/spacing lines; named imports from one module can share a line as existing source already does at :13/:34. If formatting expands it beyond 400, stop for a new partition decision rather than deleting comments or minifying the body. + +`request-context.ts` is request-boundary policy and normalization, not a runtime context/state object. Export its seven declarations only as needed by real callers; the facade re-exports only buildClaudeReplayConfig. Own imports: + +```ts +import type { OcxConfig } from "../../types"; +import { AnthropicRequestError, resolveInboundModel } from "../../claude/inbound"; +import { anthropicErrorResponse } from "../../claude/outbound"; +import { readJsonRequestBody } from "../request-decompress"; +import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget"; +``` + +`body.ts` exports its existing public functions/type plus `anthropicUsageToOcx` and `PassthroughCloseReason` directly for native.ts; keep BoundedPassthroughBody private (as today, inferred through the exported function). Own imports: + +```ts +import { sseFieldValue } from "../../lib/sse-decoder"; +import { idleDeadline } from "../../lib/abort"; +import type { OcxConfig } from "../../types"; +import type { RequestLogContext } from "../request-log"; +import { isRec, type Rec } from "./request-context"; +``` + +`native.ts` exports its existing public type/function plus wantsNativePassthrough and anthropicNativePassthrough to count/replay, not through the facade. Own imports: + +```ts +import { enforceAnthropicImageLimits } from "../../adapters/anthropic-image-guard"; +import { normalizeAnthropicImages } from "../../adapters/anthropic-image-normalize"; +import { resolveInboundModel } from "../../claude/inbound"; +import { anthropicErrorResponse } from "../../claude/outbound"; +import { clearableDeadline } from "../../lib/abort"; +import type { OcxConfig } from "../../types"; +import { addFinalRequestLog, type RequestLogContext } from "../request-log"; +import { isApiAuthRequired, isDataPlaneAdmissionSecret, isProxyAdmissionSecret, type RequestPolicyView } from "../auth-cors"; +import { isRec, type Rec } from "./request-context"; +import { anthropicUsageToOcx, tapAnthropicSseForLog, resolvePassthroughBodyGuard, readBoundedPassthroughBody, type PassthroughCloseReason } from "./body"; +``` + +`count-tokens.ts` keeps the two exported functions and nested sanitizeBlock/sanitizedMessages closures intact. Own imports: + +```ts +import { sniffImageDimensions } from "../../adapters/anthropic-image-guard"; +import { AnthropicRequestError, extractOcxRouteDirective, resolveInboundModel } from "../../claude/inbound"; +import { stripOneMillionMarker } from "../../claude/context-windows"; +import { captureClaudeInbound } from "../../claude/inbound-debug"; +import { anthropicErrorResponse } from "../../claude/outbound"; +import { estimateTokens } from "../../lib/token-estimate"; +import { createTranslatorBudget } from "../../lib/translator-budget"; +import type { OcxConfig } from "../../types"; +import type { RequestPolicyView } from "../auth-cors"; +import { parseFastOnlyRowId } from "../fast-row"; +import { claudeInboundDisabled, readAnthropicBody, decodeClaudeFastSelector, type Rec } from "./request-context"; +import { wantsNativePassthrough, anthropicNativePassthrough } from "./native"; +``` + +`replay.ts` exports handleClaudeMessagesWithBudget for the original wrapper. Its body is moved whole, including its existing two dynamic imports (not converted to eager imports). Own static imports, grouped one line per existing owner: + +```ts +import { FORWARD_HEADERS } from "../../adapters/openai-responses"; +import { AnthropicRequestError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../../claude/inbound"; +import { resolveDesktop3pAlias } from "../../claude/desktop-3p"; +import { recordDesktopRequest } from "../../claude/desktop-health"; +import { stripOneMillionMarker } from "../../claude/context-windows"; +import { captureClaudeInbound } from "../../claude/inbound-debug"; +import { anthropicErrorBody, anthropicErrorResponse, collectAnthropicMessage, responsesJsonToAnthropicMessage, responsesSseToAnthropicSse } from "../../claude/outbound"; +import { isTransientUpstreamStatus } from "../../lib/upstream-retry"; +import { resolveClientRetryAfter } from "../../lib/retry-after"; +import { NoEligiblePolicyCandidateError, routeModel } from "../../router"; +import { evidenceFromBody } from "../../routing/request-evidence"; +import { resolveWireProtocolOverride } from "../adapter-resolve"; +import type { OcxConfig } from "../../types"; +import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "../request-log"; +import { conversationIdFromClaudeMetadata } from "../request-log-conversation"; +import { responseWithDeferredRequestLog } from "../relay"; +import { handleResponses } from "../responses"; +import type { RequestPolicyView } from "../auth-cors"; +import type { AdmissionLease } from "../../lib/admission"; +import { tryClaimNativeMainProfileForTurn } from "../../codex/native-main-admission"; +import { CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE } from "../../codex/auth-context"; +import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget"; +import { parseRequestEffortRowId, type ParsedEffortRowId } from "../effort-row"; +import { parseSyntheticRowId, type ParsedFastRowId } from "../fast-row"; +import { claudeInboundDisabled, readAnthropicBody, decodeClaudeFastSelector, isRec, uuidFromHex, buildClaudeReplayConfig, type Rec } from "./request-context"; +import { wantsNativePassthrough, anthropicNativePassthrough } from "./native"; +import { estimateClaudeRequestTokens } from "./count-tokens"; +``` + +Dynamic path-only rewrites inside the otherwise unchanged body: origin `:761`, `import("./effort-policy")` → `import("../effort-policy")`; origin `:787`, `import("../codex/main-account")` → `import("../../codex/main-account")`. Their await position and gating stay identical. The existing parseRequestEffortRowId binding at origin :54 is unused by this function; retain or remove only as import bookkeeping, never substitute it for the current parseSyntheticRowId behavior. + +## Re-export block + +Add these exact lines at the original `src/server/claude-messages.ts` path: + +```ts +export { buildClaudeReplayConfig } from "./claude-messages/request-context"; +export { tapAnthropicSseForLog, resolvePassthroughBodyGuard, readBoundedPassthroughBody } from "./claude-messages/body"; +export type { PassthroughBodyGuard } from "./claude-messages/body"; +export { fetchWithHeaderDeadline } from "./claude-messages/native"; +export type { HeaderDeadlineFetchResult } from "./claude-messages/native"; +export { estimateClaudeRequestTokens, handleClaudeCountTokens } from "./claude-messages/count-tokens"; +``` + +`handleClaudeMessages` remains an exported local declaration with exactly its current signature/body. All **10** original exported identifiers are preserved (eight values and two types). Do not expose handleClaudeMessagesWithBudget or formerly private helpers through the facade. + +Explicit local imports needed by the retained wrapper (re-exports bind nothing): + +```ts +import type { OcxConfig } from "../types"; +import type { RequestLogContext } from "./request-log"; +import type { RequestPolicyView } from "./auth-cors"; +import type { AdmissionLease } from "../lib/admission"; +import { createTranslatorBudget, finalizeTranslatorBudgetResponse } from "../lib/translator-budget"; +import { handleClaudeMessagesWithBudget } from "./claude-messages/replay"; +``` + +## Module-level state and cycles + +- `PASSTHROUGH_STRIP_HEADERS`, origin `src/server/claude-messages.ts:124–128`: exactly one Set owner, native.ts. Keep the names and membership checks unchanged; no second header filter or copied Set in count/replay. +- `DEFAULT_BODY_STALL_SEC` (`:461`) and `DEFAULT_BODY_MAX_BYTES` (`:462`): sole owner body.ts, values unchanged. No other top-level let, Map, WeakMap, mutable store, lock, or timer exists. +- The SSE decoder, buffer, usageAcc, reader, settled/bodyBytes/controller, idle deadline and abort listener are closure-owned inside tapAnthropicSseForLog (`:230–309`), which moves whole to body.ts. Do not hoist them to module scope or duplicate settlement ownership. +- Native `logged` and finalize closure (`:378–383`) move whole with anthropicNativePassthrough. Replay's nativeLogged/finalizeNativeLog (`:818–823`) stay together in replay.ts. These are distinct per-request paths, not one shared module flag. +- readBoundedPassthroughBody's stalled/aborted flags and idle deadline (`:504–521`) move with its body/finally. The public wrapper still creates the TranslatorBudget (`:598`), finalizes returned responses, and disposes on thrown errors (`:604–607`); the count endpoint keeps its separate budget/finally (`:1046–1052`). No additional owner is introduced. +- Cycle trap: native needs body guard types/functions and usage conversion; putting these in the facade would create native → facade → replay → native. They live in body.ts instead. Body's Rec/isRec dependencies point only to request-context, not native or the facade. Request-context imports existing inbound, not the server entry. +- Another cycle trap: replay calls estimateClaudeRequestTokens (`:753`). Count-tokens may call native/request-context but must not import replay. The estimator and count endpoint can coexist in one leaf because that edge remains one-way. +- Lane 011 reported no literal-graph cycle. Preserve dynamic credential/effort edges and verify no new static, type-only or literal-dynamic return path. Existing request-context/logCtx passing is unchanged functional/sequential coupling; first-wins finalization is temporal coupling retained within its closure. This plan neither adds a global state container nor performs credential-policy cleanup. + +## Tests + +Exact behavioral importer list from `rg -l '/claude-messages["\x27]' tests` (sorted). All **unchanged** at the original path: + +| test file | import/use line | disposition | +|---|---:|---| +| `tests/claude-integration/claude-messages-endpoint.test.ts` | 17–24 | unchanged | +| `tests/claude-integration/claude-sidecar-override.test.ts` | 3 | unchanged | +| `tests/providers/cursor/cursor-effort-rows.test.ts` | 13 | unchanged | +| `tests/routing/routing-policy-surface-parity.test.ts` | 122 dynamic import | unchanged | +| `tests/server/fetch-header-timeout.test.ts` | 173 dynamic import | unchanged | + +Direct text oracles: **none found** by `rg -n 'claude-messages|claude-messages.ts' tests` and full/segmented-path read inspection, consistent with lane 011. In particular fetch-header-timeout imports the function; it does not read this source text. The comment in `tests/codex-integration/codex-auth-context.test.ts:2169` and comments in `tests/providers/deepseek-inbound-wire.test.ts:215` / `tests/providers/xai/xai-transport.test.ts:644` are not source reads. Do not invent a retargeting patch based on 001's coarse “1” textoracle summary. + +Transitive source oracle: `tests/codex-integration/compatibility-manifest.test.ts:61`, `readFileSync(current, "utf8")`, reached through its server/index root at :184 and `src/server/index.ts:178`. **Unchanged**: the import/re-export walker automatically discovers native/body/count/replay/request-context. No `retarget-to-leaf` or `add-leaf-to-scan-list` is needed. Guard sensitivity during implementation: temporarily add a static named re-export from `../../compatibility/manifest` in `src/server/claude-messages/replay.ts`; this test must fail with the forbidden chain. Remove the probe and rerun green. Do not commit it. + +`tests/lab/core-lab-boundary.test.ts:69` reads its protected reachable graph; :355 separately reads server/index for activation-order checks. The inspected PROTECTED roots do not reach claude-messages, so this is a mandatory server gate, not a direct source oracle to retarget. Leave PROTECTED and activation ordering untouched. Run existing boundary self-tests; do not add this facade to PROTECTED to manufacture coverage. + +Drive existing behavior guards red once after moves, then restore and obtain green: remove native.ts's deadline.clear() (origin :587) to trigger `tests/claude-integration/claude-messages-endpoint.test.ts:354`; alter body.ts's overflow comparison (origin :333) to trigger the A2 test at :468. Credential, native-main enrichment, fast/effort, count estimates, and cache provenance remain covered by the unchanged endpoint suite (:639, :767, :819, :888, :1180–1300) and the routing/cursor suites. No test is executed by this docs task. If new tests become necessary, the executor must account for both test-layout registries and request scope expansion rather than silently adding files. + +## Verification + +**Do not execute this layer until the changeset-size escalation is resolved.** Then run only in the dedicated layer worktree, based on the current L1 tip. Domains: claude-integration, server, providers/cursor, routing, codex-integration, lab. Instantiated 002 gate: + +```sh +bun run typecheck +bun test tests/claude-integration tests/server/fetch-header-timeout.test.ts tests/providers/cursor/cursor-effort-rows.test.ts tests/routing/routing-policy-surface-parity.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/server/claude-messages.ts src/server/claude-messages/request-context.ts src/server/claude-messages/body.ts src/server/claude-messages/native.ts src/server/claude-messages/count-tokens.ts src/server/claude-messages/replay.ts +rg -n '/claude-messages["\x27]|from "./claude-messages"' src gui/src scripts tests +git diff --check +git diff --numstat codex/split-claude-inbound...HEAD -- src +``` + +Compare the six original-path importer identities before/after, not merely a static `from` count that misses dynamic test imports. New leaf imports are additional dependencies, not a reason to accept missing public consumers. Verify the exact re-export set and both dynamic path rewrites. Scan each leaf's imports and compare the reachable graph for new cycles, including type/dynamic edges; typecheck is not a substitute. No standalone cycle tooling installation is authorized by this docs task. + +Full suite is **never local**. Parent-approved remote checkout and exact layer branch: + +```sh +ssh lidge 'set -e; cd ~/ocx-ci/opencodex; git fetch origin codex/split-server-claude-messages; git checkout -q FETCH_HEAD; git rev-parse HEAD; bun install --frozen-lockfile >/dev/null; bun run test' +``` + +Match the printed remote SHA with the recorded PR head; retain the actual test exit status rather than letting 002's illustrative `| tail -15` mask it. Save focused test counts, sensitivity red/green results, typecheck/privacy statuses, file sizes, full remote result, actual diff size and the authorized exception/revised map, plus exact-head CI rollup. No gates were run during planning; this document records instructions, not successful implementation. Fresh read-only Node/ast-grep checks on 2026-09-05 confirmed nine ordered headings, all 29 exact declaration ranges, 10 public identifiers, all named test paths present, 71 relative import/re-export paths resolving to existing or explicitly planned files, and no trailing whitespace (exit 0). + +## Accept criteria + +1. Parent explicitly resolves the incompatibility between one L2 and the 500-changed-source-line cap. Without recorded authorization/revised topology, this plan is not executable or review-ready. +2. Exactly the five proposed leaves contain the 28 moved declarations; handleClaudeMessages alone remains as the original local declaration. Every one of the 29 inventoried declarations has one owner. +3. Every new file and residual is ≤400 lines; expected residual 45 and replay ceiling 399. No undeclared #b remains and no body/comment compression is used to force the size gate. +4. All 10 original exported identifiers, parameter defaults, error-class identity, and six original importer files are preserved; internal-only names are not added to the facade. +5. Credential checks, dynamic import timing, request-policy propagation, budget ownership/disposal, log-tap placement, cancellation/timeout first-wins behavior, image normalization and token-estimation bodies remain unchanged. +6. No leaf imports the original facade; no new static/type/literal-dynamic cycle exists, no optional Lab/catalog dependency enters a protected runtime path, and PROTECTED is unchanged. +7. The transitive oracle still reads moved leaves and its temporary negative probe fails once; restored source passes the instantiated gates, remote full suite and CI at the exact layer head. Local full suite is never run. +8. PR base is codex/split-claude-inbound, its commits contain the current lower-layer tip, all template sections and the full two-layer map are filled, and no merge is performed. + +## PR + +Title: `refactor(server): separate Claude transport from translated replay (split S08 L2/2)` + +Branch: `codex/split-server-claude-messages`. Base: `codex/split-claude-inbound`. Closes: none. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. The Summary must disclose the parent-approved size exception or revised layer topology; do not claim compliance before it exists. Review only this layer's diff. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 2 | #TBD-S08-L2 | server Claude messages ← this layer | `codex/split-server-claude-messages` | `codex/split-claude-inbound` | native/body/count/replay ownership | +| 1 | #TBD-S08-L1 | inbound | `codex/split-claude-inbound` | `dev` | option leaves and stable inbound exports | + +Depends on #TBD-S08-L1. Parent owns any cascade after an L1 edit and must renew exact-head evidence. DEV-STACK-04 merge authorization is separate; this delegated task performs no Git mutation, push, PR creation, or merge. diff --git a/devlog/_plan/260905_now_split_train/270_server_system_env.md b/devlog/_plan/260905_now_split_train/270_server_system_env.md new file mode 100644 index 0000000000..becc61e7b1 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/270_server_system_env.md @@ -0,0 +1,200 @@ +# S09 L1/3 — system environment shell integration + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only delegated scope. Implementation touches an existing authentication boundary and requires the repository's explicit security review, without changing its behavior. +- Goal: extract shell-file/hook handling from `src/server/system-env.ts` (537 lines) while preserving every original export and keeping launchctl tracking and model derivation together. +- Non-goals: no auth-policy changes, launchctl behavior changes, hook-path fixes, export renames, dependency additions, or opportunistic unused-import cleanup. Do nothing/configure/delete cannot satisfy the size target; reuse the existing functions rather than introduce a second implementation. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. No verifier commands or implementation are run in this documentation task. +- Stop: one independently verified layer, original and leaf each <=400 lines, exact-head green CI recorded, PR open against `dev`; never merge. Stop this delegation after writing and checking the assigned document. +- Escalation: source drift, a changed auth result, an import cycle, >500 changed source lines, a weakened oracle, or a required file outside the executor's approved scope returns to the parent. Parent owns orchestration, loop and goal state. + +Evidence basis: docs HEAD `4cc219549`; `origin/dev` `1362b1a38`. All source ranges below refer to that code basis, verified identical to the working file. The older ref in 001 is not this document's source basis. Lane: `../260905_modular_debt_ledger/011_lane_server_responses.md:398` starts the system-env section. + +Structural decision: CLI and management consumers currently enter `system-env.ts`, which owns shell snapshots and launchctl state. Keep that compatibility boundary; move the shell snapshot's auth resolver with its shell writer, and let the residual call that leaf directly. Rejected: moving only `.zshrc` hooks (133–242) leaves about 427 lines; leaving the resolver in the residual creates `system-env -> shell -> system-env`. Blast radius is the server environment feature, not a new package or public API. + +## Symbol inventory + +Collected with `git show origin/dev:src/server/system-env.ts | rg -n '^(export )?(async )?(function|interface|type|const|let|class) |^}'`, checking multiline endings against numbered source. Imported bindings are dependencies listed in Leaf partition, not locally defined symbols. Consumers are distinct external files using the symbol through this module, found with `rg -l` on the old import path and symbol-name `rg` within that set; private declarations have zero external consumers (same-name declarations elsewhere do not count). `shell` = `src/server/system-env-shell.ts`; `residual` = original file. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| SystemEnvDeps | type | 26–31 | yes | 0 | shell | +| systemEnvAnthropicEnv | function | 39–52 | no | 0 | shell | +| systemEnvMarkerMode | function | 54–63 | no | 0 | shell; internal leaf export | +| getShellEnvFilePath | function | 71–73 | yes | 0 | shell | +| shellValue | function | 75–77 | no | 0 | shell | +| writeShellEnvFile | function | 79–127 | no | 0 | shell; internal leaf export | +| removeShellEnvFile | function | 129–131 | no | 0 | shell; internal leaf export | +| SHELL_HOOK_MARKER | const string | 138–138 | no | 0 | shell | +| SHELL_HOOK_LINE | const string | 139–139 | no | 0 | shell | +| installShellHook | function | 141–156 | yes | 0 | shell | +| uninstallShellHook | function | 158–184 | yes | 1 | shell | +| claudeCodeCliInstalled | function | 187–203 | yes | 1 | shell | +| reconcileShellHook | function | 218–242 | yes | 2 | shell | +| SYSTEM_ENV_NAMES | const tuple | 244–248 | no | 0 | residual | +| MANAGED_SYSTEM_ENV_NAMES | const Set | 250–261 | no | 0 | residual | +| SystemEnvTracking | interface | 263–269 | no | 0 | residual | +| SystemEnvResult | type | 271–271 | no | 0 | residual | +| RevertResult | type | 272–272 | no | 0 | residual | +| CleanupResult | type | 273–273 | no | 0 | residual | +| getSystemEnvTrackingPath | function | 275–277 | yes | 0 | residual | +| launchctlGetenv | function | 279–287 | yes | 0 | residual | +| readTracking | function | 289–304 | no | 0 | residual | +| setLaunchctlEnv | function | 306–308 | no | 0 | residual | +| unsetLaunchctlEnv | function | 310–312 | no | 0 | residual | +| ownedBaseUrl | function | 314–316 | no | 0 | residual | +| writeTracking | function | 318–327 | no | 0 | residual | +| rollbackInjectedKeys | function | 329–345 | no | 0 | residual | +| computeEffectiveModelEnv | async function | 351–365 | no | 0 | residual | +| injectSystemEnv | async function | 367–481 | yes | 3 | residual | +| applySystemEnvToggle | async function | 483–486 | yes | 10 | residual | +| revertSystemEnv | function | 488–519 | yes | 2 | residual | +| cleanStaleSystemEnv | async function | 521–537 | yes | 1 | residual | + +Old-path fan-in is 14 files: `src/cli/index.ts`, `src/server/management-api.ts`, `src/server/management/{agent-settings-routes,logs-usage-routes,combo-routes,config-routes,oauth-account-routes,provider-routes,shared,model-routes}.ts`, and the four direct test importers below. Keep all fourteen import sites unchanged. The namespace import in `claude-management-api.test.ts:8` consumes `applySystemEnvToggle` at line 355; do not count it as using all exports. + +## Leaf partition + +One new sibling, following the domain-prefixed naming used by `src/server/startup-health-cache.ts` and `src/server/proxy-liveness.ts`; no `index.ts` or convenience barrel. Search `rg -n 'system-env-shell|systemEnvMarkerMode|writeShellEnvFile|reconcileShellHook' src` finds the existing owner, not a pre-existing shell leaf. + +`src/server/system-env-shell.ts`: move original 15–242 including comments (228 physical lines). Own all thirteen `shell` rows above. Expected **238 lines**: 228 moved + nine one-line imports + one blank. Export `systemEnvMarkerMode`, `writeShellEnvFile`, and `removeShellEnvFile` only from the leaf for the residual's production calls; do not add them to the compatibility facade. Own imports: + +```ts +import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs"; +import { delimiter, join } from "node:path"; +import { getConfigDir } from "../config"; +import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows"; +import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect"; +import { resolveClaudeAuthMode } from "../claude/auth-mode"; +import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context"; +import type { OcxConfig } from "../types"; +import { recordOwnedConfigPath } from "../lib/config-ownership"; +``` + +Residual `src/server/system-env.ts`: expected **310 lines** = 537 − 229 (15–243, including separator) − 2 (old imports 7–8) + 4 compatibility/import statements below. Retain original 244–537, with identical bodies. Narrow fs/path/auth-detect imports to the names still used; retain `PROXY_MARKER`. Keep unrelated existing imports at 12–13 unchanged. The function at 351 still owns catalog-busy handling. No #b layer and no residual over 400. Formatting can change these estimates; actual `wc -l` must remain <=400. Estimated move-inclusive source diff stays below 500; measure before publishing. + +## Re-export block + +At the original path, add exactly these named re-exports and separate local bindings (one physical statement per line for the size estimate): + +```ts +export { getShellEnvFilePath, installShellHook, uninstallShellHook, claudeCodeCliInstalled, reconcileShellHook } from "./system-env-shell"; +export type { SystemEnvDeps } from "./system-env-shell"; +import { systemEnvMarkerMode, writeShellEnvFile, removeShellEnvFile } from "./system-env-shell"; +import type { SystemEnvDeps } from "./system-env-shell"; +``` + +Keep the six original exported implementations `getSystemEnvTrackingPath`, `launchctlGetenv`, `injectSystemEnv`, `applySystemEnvToggle`, `revertSystemEnv`, `cleanStaleSystemEnv` in place. Re-exporting alone does not bind `SystemEnvDeps` for the retained signature. No `export *`, aliases, or wrapper copies. This is preservation of an existing consumer boundary, not creation of an internal convenience barrel. + +## Module-level state and cycles + +- `MANAGED_SYSTEM_ENV_NAMES` at `src/server/system-env.ts:250–261`: exactly one Set owner, the residual; not exported or reconstructed in the shell leaf. `SYSTEM_ENV_NAMES` at 244–248 remains adjacent. All tracking read/write/rollback/revert operations stay with them. +- `SHELL_HOOK_MARKER` at 138 and `SHELL_HOOK_LINE` at 139 are immutable strings owned only by the leaf. No top-level let, Map, WeakMap, lock, timer or promise flight exists in this file. The Set at 46 is invocation-local, not a singleton. +- Existing external/temporal coupling is retained: injection writes launchctl values, then the shell snapshot, then caches/agent definitions, and finally tracking. The leaf performs no work at module load. Preserve snapshot timing and resolver calls; do not precompute auth state globally. +- Intended direction: old consumers → `system-env.ts` → `system-env-shell.ts` → existing config/auth/launcher-context owners. The leaf must never import the old facade, even for `SystemEnvDeps`; moving that type and the shared resolver eliminates the otherwise direct back-edge. Existing dynamic catalog/cache imports stay dynamic in the residual, not a newly invented cycle workaround. + +## Tests + +Direct importer command: `rg -l 'from .*server/system-env' tests --glob '*.ts'`. Exact list and disposition: + +- `tests/server/system-env.test.ts:5–9` — unchanged; preserve launchctl arguments, rollback, ownership, configured token, and lever expectations. +- `tests/claude-integration/claude-system-env-auto.test.ts:6` — unchanged; inject through old path and retain fs/auth spies. +- `tests/claude-integration/claude-shell-hook.test.ts:5` — unchanged; exercises PATH, LF/CRLF, idempotence and failure shape through the re-export. +- `tests/claude-integration/claude-management-api.test.ts:8` — unchanged; preserve old-path `spyOn(systemEnv, "applySystemEnvToggle")` at 355. + +Text/source-oracle inventory, distinguishing actual source reads from generated-file reads: + +| test / exact read site | disposition | +|---|---| +| `tests/codex-integration/model-visibility-management-api.test.ts:71` — `Bun.file(new URL("../../src/server/system-env.ts", import.meta.url)).text()`; assertion at 78 | unchanged; the original file retains `computeEffectiveModelEnv` and its catalog-busy branch | +| `tests/codex-integration/compatibility-manifest.test.ts:61` — transitive `readFileSync(current, "utf8")`, roots 182–190 include management-api/index | unchanged; automatic traversal follows named re-exports and direct imports into the new leaf; no manual scan-list edit | +| `tests/lab/core-lab-boundary.test.ts:69` — graph read; roots at 20–24, direct reads 278 and 336 | unchanged; PROTECTED roots never edited; any reachable new leaf must remain Lab-free | +| `tests/claude-integration/claude-shell-hook.test.ts:181` — reads `src/cli/index.ts`, not system-env | unchanged; CLI call sites and startup reconciliation count remain intact | + +001's coarse “2 textoracle” count is not two direct source reads: full basename searches also find comments and generated tracking-file assertions. The explicit system-env source read is line 71 above; generic graph walkers are listed separately. No oracle needs retargeting or weakening for this partition. + +Guards to drive red once during implementation C, then restore: remove the residual catalog-busy condition and run the model-visibility guard; perturb the moved hook's CRLF removal and run the existing CRLF test; temporarily add a forbidden Lab edge on a reachable protected graph and confirm the boundary guard fails without changing PROTECTED. Record mutations and red/green output, never commit the mutations. + +## Verification + +Commands below are the future layer-tip gate, not results from this docs-only task: + +```sh +bun run typecheck +bun test tests/server/system-env.test.ts tests/claude-integration/claude-system-env-auto.test.ts tests/claude-integration/claude-shell-hook.test.ts tests/claude-integration/claude-management-api.test.ts tests/codex-integration/model-visibility-management-api.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/server/system-env-shell.ts src/server/system-env.ts +rg -n 'from "[^"]*/system-env"' src gui/src scripts tests | wc -l +git diff --check +``` + +Focused domains: server, claude-integration, codex-integration, Lab boundary only. Compare the fourteen baseline importer sites and twelve-export surface (eleven functions plus one type), not an unqualified symbol-search count. Inspect static/type/literal-dynamic relative edges from the two modules for no new SCC/back-edge; typecheck alone does not prove absence of cycles. + +Full suite runs only on `lidge`, per 002. In the authorized dedicated remote checkout, use `ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-server-system-env && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test'`. Confirm remote HEAD equals the recorded local/PR tip before accepting output; retain the real test exit code and full log, not only the last pipeline command's status. Record exact-head CI rollup separately. No local full suite, merge or release. + +## Accept criteria + +1. Exactly the original 32 locally defined top-level symbols have one destination each; no copied function bodies or state. +2. Shell leaf <=400 and residual <=400; expected 238/310; measured layer source additions plus deletions <=500 or stop for parent re-slicing. +3. All twelve old exports remain importable, all fourteen old consumer sites are unchanged, and local leaf bindings compile. +4. Catalog-busy source assertion and old-path spies remain meaningful; recorded guard mutations fail then pass after restoration. +5. No new cycle, no eager Lab dependency and no PROTECTED changes; injection/revert sequencing and return values are unchanged. +6. Typecheck, focused tests, privacy scan, remote full suite and exact-head CI are green with recorded tip and output; explicit security review is recorded before review-ready status. +7. PR targets `dev`, includes the repository template and complete S09 map, and remains unmerged. + +## PR + +Title: `refactor(server): isolate shell environment integration (split S09 L1/3)` + +Branch: `codex/split-server-system-env`. Base: `dev`. Closes: none. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist; carry actual executed evidence, not this planned gate. DEV-STACK-03 body map (placeholder PR numbers intentionally pending publication): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 3 | #TBD-S09-L3 | lab-routes | codex/split-server-management-lab-routes | dev | public evidence route boundary | +| 2 | #TBD-S09-L2 | logs-usage-routes | codex/split-server-management-logs-usage-routes | codex/split-server-system-env | usage summary dispatch | +| 1 | #TBD-S09-L1 | system-env — this PR | codex/split-server-system-env | dev | shell snapshot/hook ownership | + +Base: dev — no dependency on lower layers; this layer is the parent of 280 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). + +Review only this layer's diff. Merging requires separate authorization; this train does not merge. + +## P stale-check (2026-09-05, wp270) + +origin/dev 760eddee1; system-env.ts unchanged since 445742966 (537 lines); anchors 7/8/12/13/15/138/139/242–244/250 confirmed by sed. Base `dev` (S09 bottom; 280 logs-usage-routes chains on it). src/server touched → core-lab-boundary gate mandatory; text oracle model-visibility-management-api.test.ts:71 reads the residual as source. Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change (extend tests/server/system-env.test.ts). + +## A amendment (Raman audit, GO-WITH-FIXES blockers=1 → folded) + +Size gate: the raw ≤500 clauses (Loop spec, :75, :140) are void; 003 PURE-MOVE-SIZE-01 binds (228 relocated lines; ≤150 non-move; audit measured ~23 non-move before the test edit). Lab roots citation corrected to core-lab-boundary.test.ts:20–28. +Audit-verified exact residual imports (retain the two already-unused providers imports verbatim; drop original 7–8): +```ts +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { getConfigDir } from "../config"; +import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows"; +import { PROXY_MARKER } from "../claude/auth-detect"; +import { isProxyAdmissionSecret } from "./auth-cors"; +import type { OcxConfig } from "../types"; +import { recordOwnedConfigPath } from "../lib/config-ownership"; +import { providerContextCap } from "../providers/context-cap"; +import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; +``` +plus the two leaf binding lines from the Re-export block. Structure verified: 32/32 ranges, 13 shell rows, leaf imports 23 bindings exact, leaf→residual references none, 12/12 exports, catalog_busy oracle string stays in the residual (:358), 344-module walk: no dependency reaches system-env. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-270.kvYBRy/wt` (branch `codex/split-server-system-env`, base origin/dev 760eddee1). Executor: gpt-6-astra high (Kant, 01a06f6c-d929-7910-85b4-7dfe4c2c48f6). +- Commits: a7e6ea6ee (move: system-env-shell.ts 238, system-env.ts 310) and 1cab08d40 (test: system-env.test.ts +16 — installShellHook/getShellEnvFilePath identity via both paths; leaf has no ./system-env import; residual still contains catalog_busy). Diff: 3 files, +261/−234; non-move 39 lines; 14 importers unchanged. +- Local gate: typecheck 0; focused (5 files) 84 pass / 0 fail; guards (core-lab-boundary + compatibility-manifest) 23/0; privacy passed. +- Red-drives: (a) CRLF handling removed → claude-shell-hook.test.ts:127 fails (9/2), restored 11/0; (b) catalog_busy string replaced → model-visibility-management-api.test.ts:78 fails + new seam test, restored 39/0; (c) lab import in leaf → core-lab-boundary:288 chain management-api → system-env → system-env-shell → lab/paths, restored 23/0. + +- Adversarial diff review (Erdos, gpt-6-astra high, 01a06f70-ebde-7052-8b83-d383aef7d5fb): VERDICT: PASS (slice byte-exact, residual exact, 12/12 exports, 3 seams not leaked, 345-module walk no cycle, catalog_busy at residual :131, leaf Lab-free under PROTECTED management-api.ts). +- lidge full suite at 1cab08d40: SUITE_EXIT=0, 18066 pass / 0 fail / 16 skip (/tmp/suite-split-270.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3585 (base dev, head 1cab08d40). CI rollup at record time: OPEN draft=false 1cab08d40 =1 =16 SKIPPED=2 SUCCESS=8 diff --git a/devlog/_plan/260905_now_split_train/280_server_management_logs_usage_routes.md b/devlog/_plan/260905_now_split_train/280_server_management_logs_usage_routes.md new file mode 100644 index 0000000000..16c0477b26 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/280_server_management_logs_usage_routes.md @@ -0,0 +1,140 @@ +# S09 L2/3 — usage summary route extraction + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 module split, docs-only delegation. +- Goal: extract the usage-summary route and its three private helpers, making both new leaf and residual <=400 lines while preserving `handleLogsUsageRoutes` and route behavior. +- Non-goals: no cache-policy or response changes, log/storage rewrites, storage execution, cleanup of inherited unrelated imports, additional dispatch layers, new dependencies or export renames. Do nothing/configure/delete cannot remove the source-size debt; reuse the existing summary/aggregate cache owners. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. This task writes only this plan; no test runs. +- Stop: isolated L2 verified at its own tip, exact-head CI green and PR open against L1, never merged. Parent alone owns orchestration/loop/goal state. +- Escalation: source drift, >500 changed source lines, changed error/cache behavior, unexpected cycles, unavailable remote verification, or additional write scope returns to the parent. `src/server/management/route-registry.ts` owner metadata is a required implementation companion file; if the executor is limited to the original plus leaf, obtain that scope before implementing. + +Source basis: `origin/dev` `1362b1a38`, docs HEAD `4cc219549`; numbered working source is identical. Lane 011 in `../260905_modular_debt_ledger/` identifies this seam at lines 363–372 and 820. Core map: `management-api.ts:61` → `handleLogsUsageRoutes` → logs/debug, usage projection caches, storage jobs. Chosen direction adds a direct usage leaf under the same management feature. Rejected: moving all route families into three leaves increases churn unnecessarily; moving just the three helpers leaves the file oversized. Public HTTP contracts and the old handler signature remain stable. + +## Symbol inventory + +Evidence command: `git show origin/dev:src/server/management/logs-usage-routes.ts | rg -n '^(export )?(async )?function |^}'`; numbered source supplies exact closing lines. Every locally defined top-level declaration is listed (import bindings are dependencies, not definitions). Consumers count distinct importing files via `rg -l` on the module path, followed by symbol checks; private symbols have zero external consumers. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| nextLocalMidnight | function | 86–90 | no | 0 | src/server/management/usage-routes.ts | +| usageSummaryExpiresAt | function | 92–98 | no | 0 | src/server/management/usage-routes.ts | +| refreshedUsageSummary | generic function | 100–103 | no | 0 | src/server/management/usage-routes.ts | +| handleLogsUsageRoutes | async function | 105–569 | yes | 1: src/server/management-api.ts:61 | residual; move only 169–320 body branch into handleUsageRoutes | + +No top-level type, class, variable, enum or state declaration is hidden below the handler. The new `handleUsageRoutes(ctx: ManagementContext): Promise` is a production extraction wrapper, not an additional export from the old path. + +## Leaf partition + +New `src/server/management/usage-routes.ts`, following sibling `routing-analytics-routes.ts` and `request-history-routes.ts`. Search `rg -n 'handleUsageRoutes|usageSummaryExpiresAt|nextLocalMidnight|refreshedUsageSummary' src` confirms the three definitions live in the current source and no `handleUsageRoutes` exists. The existing `usage-summary-cache.ts` / `usage-aggregate-cache.ts` remain the storage owners; do not absorb or clone them. + +- Symbols: private `nextLocalMidnight`, `usageSummaryExpiresAt`, `refreshedUsageSummary`; exported leaf-only `handleUsageRoutes` enclosing the complete original `if` branch at 169–320, then `return null`. +- Move 86–104 (19 lines including separator) and 169–321 (153 lines including separator). Expected **185 lines** = 172 moved + eight import/blank lines + five wrapper lines (signature, destructuring, separator, fallback, closing brace). +- Own imports, each on one line for the estimate: + +```ts +import { currentUsageLogRevision, usageLogIdentityKey, usageLogRevisionKey } from "../../usage/log"; +import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, rangeWindow, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary"; +import { userCostOverlayVersion } from "../../usage/user-cost-overlays"; +import { jsonResponse } from "../auth-cors"; +import type { ManagementContext } from "./context"; +import { discardUsageSummaryCacheEntry, getUsageSummaryCacheEntry, setUsageSummaryCacheEntry } from "./usage-summary-cache"; +import { getFilteredUsageAggregate, getUsageAggregate } from "./usage-aggregate-cache"; +``` + +Residual `src/server/management/logs-usage-routes.ts`: expected **388 lines** = 569 − 172 moved − 13 moved-only import lines (48–52, 54, 70, 79–84) + four import/delegation lines. Keep log/debug dispatch at original 108–167 first; replace the usage block at its original position with the three-line delegation below; keep storage dispatch at 322–566 after it and the final null. Leave unrelated inherited unused imports alone. No #b follows, no residual exceeds 400. Reformatting must respect the measured limit rather than rely on this estimate. + +Required metadata move: `src/server/management/route-registry.ts:204`, **only** the GET `/api/usage` row's `module` changes from `server/management/logs-usage-routes` to `server/management/usage-routes`. Keep method, path, mutates and exemptions unchanged. This is necessary for owner-source reconciliation, not a public endpoint change. Estimated move-inclusive source diff is <400 lines before small assertions; the hard publishing check remains <=500. + +## Re-export block + +The exact additional `export { ... } from ...` / `export type { ... }` block is **empty**: none of the original exported declarations moves. Retain exactly the existing declaration `export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise` at the original path. Do not re-export the newly introduced private-feature handler or expose the three former private helpers. + +Explicit local import and delegation, since an export would not bind a local name: + +```ts +import { handleUsageRoutes } from "./usage-routes"; +``` + +```ts + const usageResponse = await handleUsageRoutes(ctx); + if (usageResponse) return usageResponse; + +``` + +The leaf starts by destructuring `{ req, url, config } = ctx`, uses the original positive `/api/usage` + GET guard unchanged, and returns null for all other requests. No parent call-site or public export migration is needed. + +## Module-level state and cycles + +No top-level let, Map, Set, WeakMap, lock, timer or flight exists in the original. Every variable at 106–567 is invocation-local. `usage-summary-cache.ts` and `usage-aggregate-cache.ts` retain their own singleton lifetimes; moving imports must not create a second cache, reset it, or introduce a warm-loop owner in the leaf. + +Functional direction: facade → usage route → existing cache/usage owners and auth response helper. `ManagementContext` remains owned by `context.ts`; no leaf import from `logs-usage-routes.ts`, `shared.ts` for convenience, or `management-api.ts`. This avoids the wrapper→leaf→wrapper cycle. Retain the original imported dependency semantics; no eager Lab edge or changed auth gate. + +Temporal invariants at `src/server/management/logs-usage-routes.ts:169–320`: capture `now` once, bypass cache for filters, validate identity/read-size/overlay/timezone/freshness, invalidate before rebuilding, publish all range/surface cache entries only after aggregate consistency checks, return the same `read_failed` fallback. These statements move verbatim; extraction does not rename data fields, retry, parallelize or add catches. + +## Tests + +Direct-import search `rg -l 'from .*logs-usage-routes|import\(.*logs-usage-routes' tests --glob '*.ts'`: **empty**. Basename mentions in comments (`management-route-registry.test.ts:156`, helper lines 11/24) are not imports. Tests reach the function through `handleManagementAPI`. + +- `tests/server/api-usage.test.ts` — unchanged runtime assertions through management API; cache-module imports at 16–17 must stay on existing owners. Preserve filters, TTL/midnight, revision/read limits, timezone/overlay, concurrent aggregation and failure response coverage. +- `tests/server/api-key-attribution.test.ts` and `tests/server/server-auth.test.ts` — unchanged indirect route consumers; run relevant domains remotely and preserve the auth/attribution contract. + +Every identified source oracle and its read site: + +| test / exact read site | disposition | +|---|---| +| `tests/server/management-route-registry.test.ts:63,93,115` calls `scanRoutes`; actual source read is `tests/helpers/management-route-scan.ts:112` | add-leaf-to-scan-list **automatically**: sibling discovery at test 47–50 includes `src/server/management/usage-routes.ts`; no weakening or scanner implementation change | +| `tests/server/management-route-registry.test.ts:81` reads `src/${route.module}.ts` | retarget-to-leaf `src/server/management/usage-routes.ts` for GET `/api/usage` via the exact registry row change at 204; other rows unchanged | +| `tests/codex-integration/compatibility-manifest.test.ts:61`, roots 182–190 | unchanged; automatic static graph traversal includes the new leaf through management-api | +| `tests/lab/core-lab-boundary.test.ts:69`, direct reads 278/336 | unchanged; protected graph and mounting assertions retain original roots | + +No direct basename source read exists; the lane's “none found” does not exempt generic scanner reads. The unrelated CLI headless-parity scanner reads only `gui/src` at `tests/cli/cli-headless-parity.test.ts:216–218`, so it does not require a retarget for this extraction. + +Drive red once during implementation C: leave `/api/usage`'s owner on the old module after moving the guard and require the registry owner/count tests to fail, then apply the metadata retarget and pass; change the moved GET guard to POST and require focused usage assertions to fail, then restore. Do not weaken unknown-method handling, count reconciliation or the three PROTECTED roots. Avoid adding a new test file unless necessary; if one is added, both layout manifests are required scope. + +## Verification + +Future gate at L2 tip (not executed while drafting): + +```sh +bun run typecheck +bun test tests/server/api-usage.test.ts tests/server/api-key-attribution.test.ts tests/server/management-route-registry.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/server/management/usage-routes.ts src/server/management/logs-usage-routes.ts +rg -n 'from "[^"]*/logs-usage-routes"' src gui/src scripts tests | wc -l +git diff --check +``` + +Focused domains: server usage/registry/attribution, codex-integration graph and Lab boundary. Original importer count remains **1** and original export count **1**. Diff-check the original usage body against the leaf; inspect relative static, type and literal-dynamic edges from the moved imports for no new SCC/back-edge (a successful typecheck alone is insufficient). + +Full suite only remotely: `ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-server-management-logs-usage-routes && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test'`. In that dedicated checkout verify HEAD equals the exact recorded PR tip; retain full test log and actual test exit status. Capture exact-head CI rollup independently; no local full suite. Confirm L2 contains the latest L1 tip and PR base is L1, not dev. + +## Accept criteria + +1. Three private definitions move once; original handler and its signature remain exported at the same path with its sole importer unchanged. +2. One leaf <=400, residual <=400 (expected 185/388); measured layer source additions plus deletions <=500, otherwise escalate. +3. All `/api/usage` response fields, error fallbacks, cache ownership and sequencing are identical; log/debug precede it and storage routes follow it. +4. Exactly one registry owner row retargets; scanner automatically sees the leaf; stale-owner and route-method mutations fail before restored guards pass. +5. No cycle, no duplicated state, no new Lab reachability and no changes to protected roots or authorization behavior. +6. Typecheck, focused tests, privacy scan, exact-tip remote full suite and exact-head CI pass with recorded evidence; security-sensitive management-boundary review is explicit before review-ready status. +7. Correct L1 ancestry/base, complete template and map, and an open unmerged L2 PR. + +## PR + +Title: `refactor(server): isolate usage summary route dispatch (split S09 L2/3)` + +Branch: `codex/split-server-management-logs-usage-routes`. Base: `codex/split-server-system-env`. Closes: none. + +Use every Summary, Verification and Checklist section of `.github/PULL_REQUEST_TEMPLATE.md`. DEV-STACK-03 map (replace PR-number placeholders when published): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 3 | #TBD-S09-L3 | lab-routes | codex/split-server-management-lab-routes | dev | public evidence route boundary | +| 2 | #TBD-S09-L2 | logs-usage-routes — this PR | codex/split-server-management-logs-usage-routes | codex/split-server-system-env | usage summary dispatch | +| 1 | #TBD-S09-L1 | system-env | codex/split-server-system-env | dev | shell snapshot/hook ownership | + +Depends on #TBD-S09-L1 (`codex/split-server-system-env`) only. Review this layer's diff only. Cascade and re-verify this layer after its real parent `codex/split-server-system-env` changes; independent L3 has no cascade obligation. Bottom-up merging of this dependency chain needs separate authorization; this train never merges. diff --git a/devlog/_plan/260905_now_split_train/290_server_management_lab_routes.md b/devlog/_plan/260905_now_split_train/290_server_management_lab_routes.md new file mode 100644 index 0000000000..5f2605720b --- /dev/null +++ b/devlog/_plan/260905_now_split_train/290_server_management_lab_routes.md @@ -0,0 +1,179 @@ +# S09 L3/3 — optional Lab public-evidence route boundary + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 module-boundary planning under a docs-only delegation. +- Goal: separate public-evidence routes from query dispatch, preserving the optional mount and every existing HTTP/error contract; all resulting files <=400 lines. +- Non-goals: no new Lab activation, public-evidence validation changes, query changes, parser unification, weakened body limit, eager import from core, exported handler rename, security fixes or merge. Do nothing/configure/delete cannot meet the size target. Reuse current query/public services and `ManagementContext`; introduce no generic helper framework. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; not executed in this drafting task. +- Stop: standalone L3 with exact-head green evidence and an open PR against L2, never merge. Parent owns loop/goal/orchestration; this delegation writes only its assigned plan. +- Escalation: changed input/error behavior, a new import cycle, >500 changed source lines, source drift, non-green gate, or scope expansion goes to the parent. Registry metadata changes specified below are necessary companion implementation scope; do not silently skip them if only the original and leaves are authorized. + +Basis: docs HEAD `4cc219549`, code `origin/dev` `1362b1a38`; all ranges are origin/dev ranges and working source is identical. Lane 011 in `../260905_modular_debt_ledger/` records this file at 374–384 and optional mounting at 782. It has **562 newline characters, 563 physical lines**: its final `}` is not newline terminated. + +Structural decision: management-api's namespace-gated dynamic import at `src/server/management-api.ts:123–132` leads to one Lab route module with query parsing and public writes. Chosen: retain query dispatch in that boundary, extract public handling plus a small shared error-mapping leaf. Rejected: a public leaf importing errors from the original creates a facade↔leaf cycle; duplicating error mapping creates two owners. All new dependencies stay inside the optional Lab management feature; no new package or convenience index. + +## Symbol inventory + +Collected with `git show origin/dev:src/server/management/lab-routes.ts | rg -n '^(export )?(async )?(function|const|let|type|interface|class) |^}'` and numbered end-line inspection. Locally defined top-level declarations only; imported bindings are listed under own imports below. Consumers are distinct external importing files from `rg -l`, filtered to actual symbol uses; same-name helpers elsewhere are not consumers. `public` = `src/server/management/lab-public-routes.ts`; `errors` = `src/server/management/lab-route-errors.ts`; residual = original file. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| parseQueryInt | function | 59–65 | no | 0 | residual | +| errorResponse | function | 67–74 | no | 0 | errors; internal leaf export | +| projectionErrorResponse | function | 76–87 | no | 0 | errors; internal leaf export | +| parseLimit | function | 89–112 | no | 0 | residual | +| parseRange | function | 114–131 | no | 0 | residual | +| parseLayer | function | 133–140 | no | 0 | residual | +| parseVerdict | function | 142–149 | no | 0 | residual | +| parseEventKind | function | 151–158 | no | 0 | residual | +| parseOutcome | function | 160–167 | no | 0 | residual | +| parseExecutionMode | function | 169–176 | no | 0 | residual | +| rejectUnsafeId | function | 178–186 | no | 0 | residual | +| decodePathSegment | function | 188–194 | no | 0 | residual | +| paginatedEnvelope | generic function | 196–202 | no | 0 | residual | +| MAX_PUBLIC_REQUEST_BYTES | const number | 204–204 | no | 0 | public | +| readBoundedPublicJson | async function | 206–243 | no | 0 | public | +| publicEventIds | function | 245–258 | no | 0 | public | +| publicBundleValue | function | 260–269 | no | 0 | public | +| publicErrorResponse | function | 271–281 | no | 0 | public | +| handleLabRoutes | async function | 283–563 | yes | 1: src/server/management-api.ts:131–132 (dynamic) | residual; 287–350 branch group moves to handleLabPublicRoutes | + +## Leaf partition + +Sibling filenames deliberately follow existing `src/server/management/{routing-analytics-routes,storage-log-guard-routes,body}.ts`. `rg -n 'lab-public-routes|lab-route-errors|handleLabPublicRoutes' src` finds no existing owners; `errorResponse`/`projectionErrorResponse` remain Lab-specific rather than being folded into unrelated generic response helpers. No new subfolder, because the route scanner discovers direct `.ts` siblings only. + +1. `src/server/management/lab-route-errors.ts`: `errorResponse` (67–74) and `projectionErrorResponse` (76–87), unchanged bodies, exported only for production use by the facade and public leaf. Move 67–87 (21 lines). Expected **25 lines** with these three imports and a blank: + +```ts +import { InvalidCursorError, LabProjectionIncompatibleError, LabProjectionUnavailableError } from "../../lab/query"; +import { jsonResponse } from "../auth-cors"; +import type { ManagementContext } from "./context"; +``` + +2. `src/server/management/lab-public-routes.ts`: `MAX_PUBLIC_REQUEST_BYTES`, `readBoundedPublicJson`, `publicEventIds`, `publicBundleValue`, `publicErrorResponse` (204–281, 78 lines), and new leaf-only `handleLabPublicRoutes(ctx: ManagementContext): Promise` containing original 287–350 (64 lines). Keep the GET community branch and complete POST group, including its unknown-path `return null`. Add a final null for other methods. Expected **160 lines** = 142 moved + 13 import/blank lines + five wrapper lines; formatting allowance must remain under 400. Own imports (preserve original nine-line public import block at 47–55): + +```ts +import { + exportLocalPublicEvidence, + importCommunityEvidenceValue, + listCommunityEvidenceContext, + parseStrictPublicJson, + previewLocalPublicEvidence, + summarizePublicEvidenceVerification, + PublicEvidenceValidationError, +} from "../../lab/public"; +import { jsonResponse } from "../auth-cors"; +import type { ManagementContext } from "./context"; +import { errorResponse, projectionErrorResponse } from "./lab-route-errors"; +``` + +Residual `src/server/management/lab-routes.ts`: expected **389 newlines / 390 physical lines** before optionally normalizing its final newline (then 390/390). Accounting: 562 − 22 (67–88) − 79 (204–282) − 65 (287–351) − 9 (47–55 public imports) − 3 (30–32 error-class imports) + 5 (two imports, two delegation statements and one blank). Keep constants/query imports needed for reads, `jsonResponse`, `ManagementContext`, query validators, prefix check, GET narrowing and all query branches. No #b and no residual over 400. + +At original 287, after the unchanged prefix gate and before the unchanged `req.method !== "GET"` gate, replace the moved public group with the two delegation statements below plus a separator. The public wrapper destructures `{ url, req, config } = ctx`; it does not modify ctx or reparse a consumed request body. All original route guards, validation statements and catches move verbatim. + +Required metadata retarget in `src/server/management/route-registry.ts`: GET `/api/lab/public/community` at 185 and POST `/api/lab/public/community/import`, `/api/lab/public/export`, `/api/lab/public/preview`, `/api/lab/public/verify` at 189–192 get `module: "server/management/lab-public-routes"`. The eleven query/regex route owners stay on `server/management/lab-routes` (eight literal reads at 180–184 and 186–188, three regex reads at 319–321); methods, paths, mutation flags, mechanisms, exemptions and owner docs do not change. Estimated source additions plus deletions <400; actual gate remains <=500. + +## Re-export block + +The exact additional `export { ... } from ...` / `export type { ... }` block is **empty**: `handleLabRoutes` is the only existing export and its implementation/signature stay at the original path. Keep `export async function handleLabRoutes(ctx: ManagementContext): Promise` unchanged. Do not expose the new public handler or error helpers from the compatibility boundary. + +Required local imports: + +```ts +import { errorResponse, projectionErrorResponse } from "./lab-route-errors"; +import { handleLabPublicRoutes } from "./lab-public-routes"; +``` + +Replacement at original 287–351: + +```ts + const publicResponse = await handleLabPublicRoutes(ctx); + if (publicResponse) return publicResponse; + +``` + +These are direct imports, not re-exports masquerading as bindings. Unknown public POST requests still fall through to the existing non-GET return-null; GET requests still reach original query dispatch after public community handling. No change to management-api's import string or automation-first ordering. + +## Module-level state and cycles + +`MAX_PUBLIC_REQUEST_BYTES` at `src/server/management/lab-routes.ts:204` is an immutable scalar, owned solely by the public leaf. There is **no** top-level let, Map, Set, WeakMap, lock, timer or flight in the original. Streaming `chunks`, `total`, `reader` and `offset` at 220–240 remain request-local; no body bytes or request state become shared. + +Graph: namespace gate → dynamic `lab-routes` → `lab-public-routes` → `lab-route-errors`; residual also → `lab-route-errors`; both leaves → existing Lab query/public services and auth response helper. Neither leaf imports `lab-routes.ts` or `management-api.ts`, including via type imports. `ManagementContext` stays in `context.ts`. Error mapping is functional coupling, not a shared mutable error registry. + +Preserve existing optional boundary at `management-api.ts:123–132` exactly. Static imports between these Lab-only modules are acceptable only behind that existing dynamic gate. Never add these leaves to the eager core chain or change `tests/lab/core-lab-boundary.test.ts` PROTECTED roots. No new lazy import is introduced to conceal a cycle. + +Preserve transport validation at its current boundary: 2 MiB length and streamed-byte limits (204–243), reader cancellation, strict JSON parser identity, exact top-level body keys (245–269), error-class identity and retry header (271–280). Do not relocate these checks into services or add duplicate validation. + +## Tests + +Direct-import query `rg -l 'from .*management/lab-routes|import\(.*management/lab-routes' tests --glob '*.ts'` finds **no executed import**. `tests/lab/core-lab-boundary.test.ts:333` contains a quoted sample dynamic import to test the scanner, not an actual module load. Actual production importer is management-api at 131. + +Indirect runtime tests, all unchanged and exercised through `handleManagementAPI`: + +- `tests/lab/lab-public-api-json.test.ts:2` — strict/duplicate-key JSON, declared and streamed size limits. +- `tests/lab/lab-public-surfaces.test.ts:18` — public preview/export/verify/community/import and file effects within fixtures. +- `tests/lab/lab-read-surfaces.test.ts:43` — query endpoints, range/cursor validation, errors and read-only contract. +- `tests/lab/lab-passive-production-surfaces.test.ts:10` — production signal query validation. + +Source oracles (the lane's basename-only “none found” misses the generic route scanner): + +| test / exact read site | disposition | +|---|---| +| `tests/server/management-route-registry.test.ts:63,93,115`; helper `tests/helpers/management-route-scan.ts:112` reads each route file | add-leaf-to-scan-list automatically via sibling enumeration at test 47–50: `src/server/management/lab-public-routes.ts` and `src/server/management/lab-route-errors.ts`; errors leaf must yield zero routes | +| `tests/server/management-route-registry.test.ts:81` reads declared owner file | retarget-to-leaf `src/server/management/lab-public-routes.ts` for precisely five public rows through registry metadata; all original query owners unchanged | +| `tests/lab/core-lab-boundary.test.ts:69,278,336` graph/direct reads; quoted sample at 333 | unchanged; no retargeting PROTECTED. Optional-module name remains a non-direct-Lab-import example; the real mounting gate remains dynamic | +| `tests/codex-integration/compatibility-manifest.test.ts:61`, roots 182–190 | unchanged; walker deliberately skips dynamic edges at 68, so the public leaf must remain outside the eager traversal | + +Keep the positive public guard strings and the outer POST guard together: the scanner narrows method by brace context. Keep original residual non-GET early return before read-path guards. No hand-entered scan exemptions, concatenated whole-repo text or dropped per-module count checks. + +Guards to drive red once in implementation C: keep one moved public row's old owner and require registry owner/count failure, then restore the specified owner; reduce the public body bound or bypass duplicate-key parsing in a temporary mutation and require the existing JSON/body-limit test to fail, then restore; add a temporary direct Lab import in a protected graph and observe boundary failure without editing PROTECTED. Record red/green outputs; do not ship mutations. No new test-file registration is needed unless coverage proves insufficient, in which case scope includes both layout manifests. + +## Verification + +Future layer-tip gate, not commands run in this documentation task: + +```sh +bun run typecheck +bun test tests/lab/lab-public-api-json.test.ts tests/lab/lab-public-surfaces.test.ts tests/lab/lab-read-surfaces.test.ts tests/lab/lab-passive-production-surfaces.test.ts tests/server/management-route-registry.test.ts tests/codex-integration/compatibility-manifest.test.ts +bun test tests/lab/core-lab-boundary.test.ts +bun run privacy:scan +wc -l src/server/management/lab-route-errors.ts src/server/management/lab-public-routes.ts src/server/management/lab-routes.ts +rg -n 'import\("\./management/lab-routes"\)' src/server/management-api.ts +git diff --check +``` + +Focused domains: Lab public/read surfaces, server registry and codex-integration graph. The ordinary 002 `from`-only importer command reports zero here because the real import is dynamic; the specialized command above must retain exactly one real production import. Original export count remains one. Audit direct, type and literal-dynamic edges from both leaves for no new SCC/back-edge; keep query/public/error import direction explicit, not inferred from typecheck success. + +Full suite only on `lidge`: `ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-server-management-lab-routes && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test'`. Confirm the dedicated remote checkout's HEAD equals the recorded local/PR tip; save actual exit code and full log. Record exact-head CI, verify L2 ancestry and PR base separately. No local full suite and no merge. + +## Accept criteria + +1. Every one of nineteen original definitions has exactly one owner; only `handleLabRoutes` remains public at the old path, with its single dynamic consumer unchanged. +2. Two leaves <=400 and residual <=400 (expected 25/160/390 physical lines); measured move-inclusive source changes <=500 or escalate. +3. Namespace/automation gating, GET narrowing, unknown-route nulls, HTTP status/body/header shapes, strict JSON and body bounds are unchanged. +4. Exactly five public registry owner fields retarget; eleven query/regex owners stay; per-module scanner counts and method resolution remain strict, with recorded red/green evidence. +5. No duplicated state, no new import cycle, no eager core→Lab edge and no PROTECTED root edits. +6. Typecheck, focused tests, privacy scan, exact-tip remote full suite and exact-head CI pass with recorded evidence; explicit security-boundary review precedes review-ready status. +7. L3 includes the current L2 tip, targets the L2 branch, carries complete PR template/map, and remains open and unmerged. + +## PR + +Title: `refactor(server): isolate Lab public evidence routes (split S09 L3/3)` + +Branch: `codex/split-server-management-lab-routes`. Base: `dev`. Closes: none. + +Use `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification and Checklist in full, including explicit security review. DEV-STACK-03 map (PR-number placeholders are replaced at publication): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 3 | #TBD-S09-L3 | lab-routes — this PR | codex/split-server-management-lab-routes | dev | public evidence route boundary | +| 2 | #TBD-S09-L2 | logs-usage-routes | codex/split-server-management-logs-usage-routes | codex/split-server-system-env | usage summary dispatch | +| 1 | #TBD-S09-L1 | system-env | codex/split-server-system-env | dev | shell snapshot/hook ownership | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Review only this layer's diff. Merging requires separate authorization; this train never merges. diff --git a/devlog/_plan/260905_now_split_train/300_codex_prompt_layers_a.md b/devlog/_plan/260905_now_split_train/300_codex_prompt_layers_a.md new file mode 100644 index 0000000000..3711273e58 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/300_codex_prompt_layers_a.md @@ -0,0 +1,378 @@ +## Loop spec + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +- Archetype: **pure-move**. Work class: **C3**, bounded docs-only subagent; parent owns orchestration, loop, goal, and execution worktrees. No cxc state commands here. +- Non-goals: no behavior fixes, parser changes, hash framing changes, new cache/state, durability rewrite, function-body refactor, public rename/removal, test weakening, caller import migration, or operational writes. +- Goal: move the low-consumer path/byte/TOML dependency leaves first while every current export remains available at the original path; leave the documented remainder for 310 #b. +- Verifier: **002 “Per-layer gate”**, instantiated below; current delegation verifies the two documents only, without test runs. +- Stop: five leaves and the compatible residual are independently verifiable; L1 implementation may start only after S10-SIZE-01 is resolved. +- Escalation: stale source coordinates, behavior change, missing export, required edits outside S10, source-oracle uncertainty, cycle requiring a redesign, or the unsatisfied ≤500-line limit go to the parent. Do not edit 000/001/002 or add layers yourself. + +Source/audit basis: docs HEAD `4cc219549`; pinned code `1362b1a38`; `000_plan.md`, `001_stale_check.md`, `002_layer_map.md` S10 rows 300/310; lane evidence `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:143–155`. The opening tip recorded in 000/001 is historical; this document's source coordinates use the pinned code above. + +Structural decision (cxc-dev-architecture): a 1,652-line feature currently combines inventory, byte codecs, TOML edits, read projections, probe admission, and writes. Reject leaving it intact (misses the size goal), deleting/configuring behavior (not a pure move), widening `features.ts` (explicit boundary at source lines 4–8), or routing leaves through a new internal barrel (creates back-edges). Choose cohesive leaves in `src/codex/prompt-layers/` with a stable original-path compatibility facade. Reuse existing `prompt-journal.ts` and `prompt-lock.ts`, without moving or duplicating their durability/lock implementation. + +Convention evidence: `src/config.ts:129` re-exports `./config/paths`; `src/config.ts:162` re-exports `./config/rebase-provenance`; `src/types/*.ts` and `src/codex/log-guard/*.ts` use focused sibling/subfolder leaves. This is the existing compatibility-boundary convention, not a new convenience `index.ts`. + +Current map: `src/server/management/codex-prompt-routes.ts:26–49`, `src/server/management/context.ts:9`, and 6 tests → `prompt-layers.ts` → config/home/path helpers, marker, journal, lock, Node fs/path/crypto. Intended map: same external imports → original facade → read/transform/transaction leaves → those same dependencies. Blast radius: local Codex prompt feature; no HTTP route, DTO, CLI, auth, persistence format, or public signature change. Tests keep importing the facade. + +Ordering is dependency-first among low-fan-in seams. L1 takes `toml-edit` (0 external importers), `revision` (1), and `paths`/`encoding`/`toml-read` (2 each), installing their prerequisite leaves together. L2 takes higher-fan-in `inventory` (3), `store` (3), `snapshot` (6), then `transaction` (1), `fingerprint` (1), and `adoption` (2). Those last low-fan-in operations cannot move earlier without also moving their snapshot/store dependencies or creating facade return edges. Original callers are not retargeted, so low consumer count is not used to justify export removal. + +**S10-SIZE-01 — unresolved execution gate:** 002 says every layer stays ≤500 changed source lines, but two pure-move layers must remove at least `1652 - 400 = 1252` original lines, before adding leaves/imports. Even counting a moved line only once, `2 × 500 < 1252`; normal added+deleted diff accounting is larger. This concrete partition moves 518 original lines in L1 and 913 in L2. The parent must approve a documented pure-move size exception or revise 002's layer count before implementation. This delegated task does not grant that exception, add a third layer, or edit 002. The two documents remain the requested feasible **file partition**, not a claim that the current per-PR size budget is satisfiable. + +## Symbol inventory + +Ranges are inclusive declaration spans at `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`, not line numbers after L1. Read with `git show origin/dev:src/codex/prompt-layers.ts | nl -ba`; `git diff origin/dev -- src/codex/prompt-layers.ts` was empty. The installed TypeScript package exposes version metadata rather than the compiler AST API, so declaration endpoints were obtained with installed ast-grep, cross-checked against `rg -n '^(export )?(function|const|let|interface|type|class|enum) '`. + +There are **89 declarations plus the existing export-alias statement at line 505**, all inventoried below. Imports at 29–46 are dependency bindings, listed in Leaf partition rather than counted as locally owned declarations. Consumer counts are **distinct external files importing this binding from the original facade**, not textual hits of homonyms such as `Paths` or `commit`. Method: `rg -l 'from.*prompt-layers"' src gui/src scripts tests` finds 8 files (2 runtime, 6 tests); ast-grep `import_statement` selects their facade imports, and `rg -w ` counts matching import blocks. The alias is counted under `readFileBytes`. Private declarations have 0 external consumers; zero is not a deletion license. Comments mentioning `WriteError`, `adoptDeveloperInstructions`, and `salvageProjection` in the route test are excluded. + +In the table, leaf names expand to `src/codex/prompt-layers/.ts`; `residual` means `src/codex/prompt-layers.ts`. L2 targets remain in the original file through L1. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `LayerClass` | type | 53–58 | yes | 0 | `inventory.ts` (L2; retain for #b) | +| `ToggleId` | type | 60–65 | yes | 0 | `inventory.ts` (L2; retain for #b) | +| `LayerDescriptor` | interface | 67–76 | yes | 0 | `inventory.ts` (L2; retain for #b) | +| `LAYER_INVENTORY` | const | 88–122 | yes | 3 | `inventory.ts` (L2; retain for #b) | +| `TOGGLE_KEYS` | const | 130–136 | no | 0 | `inventory.ts` (L2; retain for #b) | +| `TOGGLE_IDS` | const | 138–138 | yes | 1 | `inventory.ts` (L2; retain for #b) | +| `isToggleId` | function | 140–142 | yes | 1 | `inventory.ts` (L2; retain for #b) | +| `Paths` | interface | 148–152 | yes | 2 | `paths.ts` (L1) | +| `activeCodexHome` | function | 154–163 | no | 0 | `paths.ts` (L1) | +| `activeConfigPath` | function | 165–167 | yes | 0 | `paths.ts` (L1) | +| `activeStorePath` | function | 169–171 | yes | 0 | `paths.ts` (L1) | +| `activeBaseVariantDir` | function | 181–183 | yes | 0 | `paths.ts` (L1) | +| `PROBE_INSTRUCTION_FILES` | const | 191–191 | no | 0 | `fingerprint.ts` (L2; retain for #b) | +| `probeInstructionFilenames` | function | 203–213 | no | 0 | `fingerprint.ts` (L2; retain for #b) | +| `rootArrayEntries` | function | 237–242 | no | 0 | `toml-read.ts` (L1) | +| `PARSE_FAILED` | const | 249–249 | no | 0 | `toml-read.ts` (L1) | +| `rootValue` | function | 252–262 | no | 0 | `toml-read.ts` (L1) | +| `scanRootArrayEntries` | function | 272–296 | no | 0 | `toml-read.ts` (L1) | +| `probeProjectDocDirs` | function | 313–337 | no | 0 | `fingerprint.ts` (L2; retain for #b) | +| `projectRootMarkers` | function | 340–345 | no | 0 | `fingerprint.ts` (L2; retain for #b) | +| `hasRootKey` | function | 354–358 | no | 0 | `toml-read.ts` (L1) | +| `scanHasRootKey` | function | 361–364 | no | 0 | `toml-read.ts` (L1) | +| `updateFingerprintField` | function | 384–388 | no | 0 | `revision.ts` (L1) | +| `journalPathFor` | function | 390–392 | no | 0 | `paths.ts` (L1) | +| `lockPathFor` | function | 394–396 | no | 0 | `paths.ts` (L1) | +| `CharacterFinding` | interface | 404–409 | yes | 0 | `encoding.ts` (L1) | +| `normalizeBody` | function | 412–414 | yes | 2 | `encoding.ts` (L1) | +| `findInvalidCharacter` | function | 417–440 | yes | 2 | `encoding.ts` (L1) | +| `encodeBasicString` | function | 448–450 | yes | 1 | `encoding.ts` (L1) | +| `decodeBasicString` | function | 458–477 | yes | 1 | `encoding.ts` (L1) | +| `readFileOrNull` | function | 484–491 | alias readFileBytes (505) | 0 | `revision.ts` (L1) | +| `computeRevision` | function | 493–503 | yes | 1 | `revision.ts` (L1) | +| `TABLE_HEADER` | const | 513–513 | no | 0 | `toml-read.ts` (L1) | +| `rootLines` | function | 516–520 | no | 0 | `toml-read.ts` (L1) | +| `tableLines` | function | 523–531 | no | 0 | `toml-read.ts` (L1) | +| `boolInLines` | function | 533–541 | no | 0 | `toml-read.ts` (L1) | +| `DEV_INSTRUCTIONS_KEY` | const | 555–555 | no | 0 | `toml-read.ts` (L1) | +| `CANONICAL_LINE` | const | 556–556 | no | 0 | `toml-read.ts` (L1) | +| `ANY_DEV_INSTRUCTIONS` | const | 557–557 | no | 0 | `toml-read.ts` (L1) | +| `Ownership` | type | 559–567 | yes | 0 | `toml-read.ts` (L1) | +| `inspectOwnership` | function | 569–582 | yes | 2 | `toml-read.ts` (L1) | +| `CustomLayer` | interface | 588–594 | yes | 2 | `store.ts` (L2; retain for #b) | +| `LAYER_ID` | const | 596–596 | no | 0 | `store.ts` (L2; retain for #b) | +| `isCustomLayer` | function | 598–605 | no | 0 | `store.ts` (L2; retain for #b) | +| `parseStore` | function | 608–622 | yes | 1 | `store.ts` (L2; retain for #b) | +| `composeProjection` | function | 625–627 | yes | 2 | `store.ts` (L2; retain for #b) | +| `ToggleState` | interface | 633–645 | yes | 0 | `snapshot.ts` (L2; retain for #b) | +| `Drift` | type | 647–652 | yes | 0 | `snapshot.ts` (L2; retain for #b) | +| `BaseVariant` | interface | 655–660 | yes | 0 | `snapshot.ts` (L2; retain for #b) | +| `BaseSelection` | type | 678–678 | yes | 1 | `snapshot.ts` (L2; retain for #b) | +| `PromptLayerSnapshot` | interface | 680–694 | yes | 1 | `snapshot.ts` (L2; retain for #b) | +| `readToggle` | function | 696–713 | no | 0 | `snapshot.ts` (L2; retain for #b) | +| `readModelInstructionsFile` | function | 715–733 | no | 0 | `snapshot.ts` (L2; retain for #b) | +| `BASE_VARIANT_ID` | const | 736–736 | no | 0 | `snapshot.ts` (L2; retain for #b) | +| `readBaseVariants` | function | 746–776 | yes | 1 | `snapshot.ts` (L2; retain for #b) | +| `resolveBaseSelection` | function | 785–805 | yes | 0 | `snapshot.ts` (L2; retain for #b) | +| `readPromptLayers` | function | 811–855 | yes | 6 | `snapshot.ts` (L2; retain for #b) | +| `computePromptProbeStateFingerprint` | function | 887–942 | yes | 1 | `fingerprint.ts` (L2; retain for #b) | +| `probeSkillManifests` | function | 960–974 | no | 0 | `fingerprint.ts` (L2; retain for #b) | +| `WriteError` | type | 980–993 | yes | 1 | `transaction.ts` (L2; retain for #b) | +| `WriteResult` | type | 995–997 | yes | 1 | `transaction.ts` (L2; retain for #b) | +| `dominantEol` | function | 1000–1005 | no | 0 | `toml-edit.ts` (L1) | +| `splitLines` | function | 1007–1009 | no | 0 | `toml-edit.ts` (L1) | +| `splitBom` | function | 1023–1027 | no | 0 | `toml-edit.ts` (L1) | +| `joinLines` | function | 1029–1032 | no | 0 | `toml-edit.ts` (L1) | +| `firstTableIndex` | function | 1034–1037 | no | 0 | `toml-edit.ts` (L1) | +| `setRootBool` | function | 1040–1056 | no | 0 | `toml-edit.ts` (L1) | +| `setRootString` | function | 1065–1081 | no | 0 | `toml-edit.ts` (L1) | +| `setTableBool` | function | 1084–1108 | no | 0 | `toml-edit.ts` (L1) | +| `setProjection` | function | 1115–1142 | no | 0 | `toml-edit.ts` (L1) | +| `serializeStore` | function | 1144–1146 | no | 0 | `store.ts` (L2; retain for #b) | +| `Mutation` | interface | 1148–1151 | no | 0 | `transaction.ts` (L2; retain for #b) | +| `commit` | function | 1158–1272 | no | 0 | `transaction.ts` (L2; retain for #b) | +| `rollback` | function | 1275–1294 | no | 0 | `transaction.ts` (L2; retain for #b) | +| `setToggle` | function | 1297–1306 | yes | 2 | residual | +| `selectBaseVariant` | function | 1316–1335 | yes | 2 | residual | +| `MAX_BASE_VARIANTS` | const | 1338–1338 | yes | 2 | residual | +| `writeBaseVariant` | function | 1351–1434 | yes | 2 | residual | +| `newBaseVariantId` | function | 1436–1442 | no | 0 | residual | +| `writeCustomLayers` | function | 1445–1466 | yes | 2 | residual | +| `AdoptPreview` | interface | 1476–1484 | yes | 0 | `adoption.ts` (L2; retain for #b) | +| `newLayerId` | function | 1486–1492 | no | 0 | `store.ts` (L2; retain for #b) | +| `previewAdopt` | function | 1498–1536 | yes | 2 | `adoption.ts` (L2; retain for #b) | +| `adoptDeveloperInstructions` | function | 1539–1565 | yes | 2 | `adoption.ts` (L2; retain for #b) | +| `removeUnownedProjection` | function | 1568–1579 | no | 0 | `toml-edit.ts` (L1) | +| `SalvagePreview` | interface | 1589–1595 | yes | 0 | `adoption.ts` (L2; retain for #b) | +| `UNRECOVERABLE` | const | 1597–1604 | no | 0 | `adoption.ts` (L2; retain for #b) | +| `previewSalvage` | function | 1606–1620 | yes | 2 | `adoption.ts` (L2; retain for #b) | +| `salvageProjection` | function | 1627–1652 | yes | 2 | `adoption.ts` (L2; retain for #b) | +| `readFileBytes` | export alias of `readFileOrNull` | 505–505 | yes | 0 | `revision.ts` (L1); preserve alias exactly | + +## Leaf partition + +This layer creates the following five files. No files are created by this planning turn outside its assigned two Markdown documents. + +### src/codex/prompt-layers/encoding.ts + +- Move original ranges `src/codex/prompt-layers.ts:398–478` including comments and blank lines: 81 lines. +- Symbols: `CharacterFinding`, `normalizeBody`, `findInvalidCharacter`, `encodeBasicString`, `decodeBasicString`. +- Expected length: **81 lines**, including 0 one-line imports; limit 400. +- Own imports: none; do not add a facade import. + +### src/codex/prompt-layers/revision.ts + +- Move original ranges `src/codex/prompt-layers.ts:366–389`, `src/codex/prompt-layers.ts:479–506` including comments and blank lines: 52 lines. +- Symbols: `updateFingerprintField`, `readFileOrNull`, `computeRevision`; retain the existing `readFileOrNull as readFileBytes` alias at original line 505. +- Expected length: **55 lines**, including 2 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { existsSync, readFileSync } from "node:fs"; +import { createHash, type Hash } from "node:crypto"; +``` + +### src/codex/prompt-layers/paths.ts + +- Move original ranges `src/codex/prompt-layers.ts:144–184`, `src/codex/prompt-layers.ts:390–397` including comments and blank lines: 49 lines. +- Symbols: `Paths`, `activeCodexHome`, `activeConfigPath`, `activeStorePath`, `activeBaseVariantDir`, `journalPathFor`, `lockPathFor`. +- Expected length: **54 lines**, including 4 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { realpathSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { expandUserPath } from "../../config"; +import { CODEX_CONFIG_PATH } from "../paths"; +``` + +### src/codex/prompt-layers/toml-read.ts + +- Move original ranges `src/codex/prompt-layers.ts:215–296`, `src/codex/prompt-layers.ts:347–364`, `src/codex/prompt-layers.ts:507–583` including comments and blank lines: 177 lines. +- Symbols: `rootArrayEntries`, `PARSE_FAILED`, `rootValue`, `scanRootArrayEntries`, `hasRootKey`, `scanHasRootKey`, `TABLE_HEADER`, `rootLines`, `tableLines`, `boolInLines`, `DEV_INSTRUCTIONS_KEY`, `CANONICAL_LINE`, `ANY_DEV_INSTRUCTIONS`, `Ownership`, `inspectOwnership`. +- Expected length: **180 lines**, including 2 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { OCX_SECTION_MARKER } from "../injected-marker"; +import { decodeBasicString } from "./encoding"; +``` + +### src/codex/prompt-layers/toml-edit.ts + +- Move original ranges `src/codex/prompt-layers.ts:999–1143`, `src/codex/prompt-layers.ts:1567–1580` including comments and blank lines: 159 lines. +- Symbols: `dominantEol`, `splitLines`, `splitBom`, `joinLines`, `firstTableIndex`, `setRootBool`, `setRootString`, `setTableBool`, `setProjection`, `removeUnownedProjection`. +- Expected length: **163 lines**, including 3 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { OCX_SECTION_MARKER } from "../injected-marker"; +import { encodeBasicString } from "./encoding"; +import { TABLE_HEADER, ANY_DEV_INSTRUCTIONS, DEV_INSTRUCTIONS_KEY } from "./toml-read"; +``` + +The residual retains every L2 declaration in the inventory, plus the six final mutation declarations. **Residual >400 is intentional only for L1; 310_codex_prompt_layers_b.md takes the rest.** + +| Stage | Original lines extracted this layer | New leaves this layer (expected total) | Original residual | +|---|---:|---:|---:| +| Basis | 0 | 0 | 1652 | +| L1 / 300 #a | 518 | 533 across 5 files | 1146 | +| L2 / 310 #b | 913 | 953 across 6 files | 234 | + +Accounting uses inclusive source chunks (comments retained), one-line import/export statements as shown, and one blank line after each non-empty leaf import block. L1: `1652 - 518 - 2 + 14 = 1146`: remove the two obsolete facade imports at old 33/35, add five local imports + seven re-exports + two separator lines. L2: `1146 - 913 - 13 + 4 + 10 = 234`: reduce the remaining sixteen old import lines to three; grow five leaf-local import lines to nine; add ten named re-export lines. Original retained content is lines 1–47, 297, 365, and 1296–1467 with imports rewritten; the final implementation body chunk is 172 lines. Total completed source estimate: `533 + 953 + 234 = 1720`; the +68 lines over 1652 are import/re-export/spacing overhead, not duplicated bodies. Expected counts are formatting estimates, but the ≤400 final cap is mechanical. + +Only cross-leaf production dependencies gain named leaf exports: paths → `journalPathFor, lockPathFor`; revision → `readFileOrNull, updateFingerprintField`; toml-read → `rootArrayEntries, hasRootKey, rootLines, tableLines, boolInLines, TABLE_HEADER, DEV_INSTRUCTIONS_KEY, ANY_DEV_INSTRUCTIONS`; toml-edit → `setRootBool, setRootString, setTableBool, setProjection, removeUnownedProjection`; inventory → `TOGGLE_KEYS`; store → `serializeStore, newLayerId`; snapshot → `BASE_VARIANT_ID`; transaction → `commit`. Keep all other original private declarations private. `readFileOrNull` keeps its declaration name and the existing leaf alias `export { readFileOrNull as readFileBytes };`. None of these extra internal names is added to the original facade's public surface. + +## Re-export block + +Insert these seven lines; the remaining exports stay as direct declarations until #b. + +```ts +export { activeConfigPath, activeStorePath, activeBaseVariantDir } from "./prompt-layers/paths"; +export type { Paths } from "./prompt-layers/paths"; +export { computeRevision, readFileBytes } from "./prompt-layers/revision"; +export { normalizeBody, findInvalidCharacter, encodeBasicString, decodeBasicString } from "./prompt-layers/encoding"; +export type { CharacterFinding } from "./prompt-layers/encoding"; +export { inspectOwnership } from "./prompt-layers/toml-read"; +export type { Ownership } from "./prompt-layers/toml-read"; +``` + +Re-exports bind nothing locally. Add these explicit local imports for the retained code: + +```ts +import { activeConfigPath, activeStorePath, activeBaseVariantDir, journalPathFor, lockPathFor, type Paths } from "./prompt-layers/paths"; +import { readFileOrNull, computeRevision, updateFingerprintField } from "./prompt-layers/revision"; +import { normalizeBody, findInvalidCharacter, decodeBasicString } from "./prompt-layers/encoding"; +import { rootArrayEntries, hasRootKey, rootLines, tableLines, boolInLines, inspectOwnership } from "./prompt-layers/toml-read"; +import { setRootBool, setRootString, setTableBool, setProjection, removeUnownedProjection } from "./prompt-layers/toml-edit"; +``` + +Remove original `CODEX_CONFIG_PATH` import at 33 and `OCX_SECTION_MARKER` import at 35; remove `realpathSync` and type `Hash` from the remaining multi-binding imports at 29/31. Retain the other original imports: filesystem reads, `dirname/join/resolve`, `createHash/randomBytes`, `expandUserPath`, `resolveCodexHomeDir`, journal functions/types, and lock functions are still used by the L2 residual. + +## Module-level state and cycles + +All coordinates below are in `origin/dev:src/codex/prompt-layers.ts`. + +| Top-level state/constant | Line(s) | Single owner after S10 | Preservation | +|---|---|---|---| +| `LAYER_INVENTORY` | 88–122 | `prompt-layers/inventory.ts` (L2) | same shallow `Object.freeze`, same rows/order and reference identity | +| `TOGGLE_KEYS` | 130–136 | `prompt-layers/inventory.ts` (L2) | one object; export internally for readers/writers, never copy | +| `TOGGLE_IDS` | 138 | `prompt-layers/inventory.ts` (L2) | same frozen derivation after `TOGGLE_KEYS` | +| `PROBE_INSTRUCTION_FILES` | 191 | `prompt-layers/fingerprint.ts` (L2) | same tuple/order | +| `PARSE_FAILED` | 249 | `prompt-layers/toml-read.ts` (L1) | unique Symbol stays beside every identity comparison, not recreated | +| `TABLE_HEADER` | 513 | `prompt-layers/toml-read.ts` (L1) | one non-global RegExp shared with edits | +| `DEV_INSTRUCTIONS_KEY` | 555 | `prompt-layers/toml-read.ts` (L1) | same literal, edit leaf imports it | +| `CANONICAL_LINE` | 556 | `prompt-layers/toml-read.ts` (L1) | remains private non-global RegExp | +| `ANY_DEV_INSTRUCTIONS` | 557 | `prompt-layers/toml-read.ts` (L1) | one non-global RegExp shared with edits | +| `LAYER_ID` | 596 | `prompt-layers/store.ts` (L2) | validator-local owner | +| `BASE_VARIANT_ID` | 736 | `prompt-layers/snapshot.ts` (L2) | same object; residual writer imports it directly | +| `MAX_BASE_VARIANTS` | 1338 | original residual | retain exported constant and cap, no snapshot back-import | +| `UNRECOVERABLE` | 1597–1604 | `prompt-layers/adoption.ts` (L2) | same frozen list shared by previews | + +There is **no top-level `let`, Map, Set, WeakMap, cached path, or acquired lock handle** in this file. `new Set` at 619, 1437, and 1487 is call-local; hashes at 494/892 and `acquired/handle` at 1172/1174 are call-local. The existing filesystem lock stays owned by `src/codex/prompt-lock.ts`; commit at 1158–1272 moves whole to `transaction.ts`, with its acquisition/recovery/check/rollback/release sequence intact. It does not introduce another mutex or move lock acquisition to module evaluation. + +Path resolution stays call-time (154–183). Pure reads (811–855) never acquire a lock, repair, recover, or write; the journal/lock leaves are not dependencies of `snapshot.ts`. The length-framed revision and probe fields share `revision.ts:updateFingerprintField` (384–388), not copied implementations. Journal hashBytes remains a different existing contract in `prompt-journal.ts`; do not substitute one hash for the other. + +Cycle prevention and coupling classification: + +- Current audited graph has no return path (lane 013:151); the eight direct consumers confirmed by rg include no reverse imports from the current dependencies. +- L1 direction: residual → paths/revision/encoding/toml-read/toml-edit; toml-edit → toml-read + encoding; toml-read → encoding. No leaf imports `../prompt-layers` or `./index`. +- L2 direction: residual → inventory/store/snapshot/transaction; adoption → transaction + store + TOML leaves; transaction → snapshot; fingerprint → snapshot + paths + revision + toml-read; snapshot → inventory/store + L1 leaves. Every leaf imports its exact lower owner, including types. +- Moving commit alone while leaving `readPromptLayers` behind would form `facade → transaction → facade`; L2 moves snapshot first within the same layer. `WriteResult` moves with transaction and imports `PromptLayerSnapshot` from snapshot, never from facade. +- Moving fingerprint alone while importing `readBaseVariants/resolveBaseSelection` through the facade has the same problem; they move together in L2. Snapshot exports `BASE_VARIANT_ID` internally so the remaining writer never makes snapshot import its caller. +- These edges are functional/sequential coupling. Existing file-before-config and clear-key-before-delete ordering at 1391–1409/1423–1431 is temporal coupling, preserved rather than redesigned. No new common mutable state or content coupling is introduced. Existing internal declarations become leaf exports only when another production owner actually needs them; they do not become new facade exports. + +Future static check: capture ast-grep `import_statement` and `export_statement` edges for the facade and all eleven leaves, resolve relative paths, include type-only edges, and require a DFS/SCC result with no cycle containing a split module. The explicit adjacency above is the expected graph. Check transitive return paths through unchanged config/home/journal/lock dependencies as well; do not install a new analysis dependency or claim typecheck alone detects cycles. + +## Tests + +Direct importer list from `rg -l 'from.*prompt-layers"' tests` (all remain **unchanged**, importing the original facade): + +| Test file | Import line at origin/dev | Disposition | +|---|---:|---| +| `tests/codex-integration/codex-prompt-layers.test.ts` | 20 | unchanged | +| `tests/codex-integration/codex-prompt-layers-read.test.ts` | 16 | unchanged | +| `tests/codex-integration/codex-prompt-layers-write.test.ts` | 15 | unchanged | +| `tests/codex-integration/codex-prompt-base-variants.test.ts` | 16 | unchanged | +| `tests/codex-integration/codex-prompt-adopt.test.ts` | 19 | unchanged | +| `tests/codex-integration/codex-prompt-route.test.ts` | 14 | unchanged | + +**Exact-path text-oracle readers of `src/codex/prompt-layers.ts`: none.** Reproducing 001's broad read-function/basename intersection returns the following 3 files, but inspection confirms each reads fixture data, not this source. This resolves the apparent disagreement with lane 013:152 rather than inventing retargets. + +| Broad-search candidate | Exact read site and actual target | Disposition | +|---|---|---| +| `tests/codex-integration/codex-prompt-layers-write.test.ts` | line 37 `readFileSync(path, "utf8")`: fixture config/store through local helper | unchanged; no retarget-to-leaf | +| `tests/codex-integration/codex-prompt-layers-read.test.ts` | lines 203/204 `Bun.file(paths.configPath/storePath).text()`; line 217 `Bun.file(nested).exists()` | unchanged; fixture files, no source reader | +| `tests/codex-integration/codex-prompt-adopt.test.ts` | lines 75/93/111/178 read fixture config; line 163 reads salvage backup | unchanged; no retarget-to-leaf | + +Other importer read sites: `codex-prompt-base-variants.test.ts:34` reads fixture files; `codex-prompt-route.test.ts:70` reads fixtures, while its actual source guards read `src/server/management/codex-prompt-routes.ts` at **806** and `src/codex/prompt-text-probe.ts` at **811/820**. All unchanged: neither source is split in S10. There is no S10 retarget-to-leaf and no existing explicit source scan list requiring add-leaf-to-scan-list. + +`tests/lab/core-lab-boundary.test.ts:69` is a generic graph reader (not an explicit prompt-source oracle). Its import/re-export traversal discovers reachable leaves automatically; keep its `PROTECTED` roots and assertions unchanged. Run it as an extra boundary check because the facade is consumed from management code, even though 002 only mandates it when server/router/lib paths themselves are touched. Do not weaken the graph or manufacture an exemption. + +The future executor drives the named guards red once by a temporary mutation in its own isolated layer worktree, records the actual expected failure, restores the mutation, then runs green. No test, mutation, or red/green exercise is executed during this planning task. + +Guards to drive red once in L1: + +1. `tests/codex-integration/codex-prompt-layers.test.ts:138–146`: temporarily break one encoding escape in `prompt-layers/encoding.ts`; grammar/round-trip assertions must fail. +2. `tests/codex-integration/codex-prompt-layers-write.test.ts:280`: temporarily return an empty BOM from the moved `splitBom` in `prompt-layers/toml-edit.ts`; byte-zero assertion must fail. Also preserve existing root/table placement cases at 57/85/95 and CRLF case at 104. +3. `tests/codex-integration/codex-prompt-layers-read.test.ts:112`: temporarily relax adjacency in `prompt-layers/toml-read.ts:inspectOwnership`; marker-two-lines-up guard must fail. + +These mutation exercises add no permanent tests or new layout-manifest entries. Keep existing source guards at route-test 806/811/820 unchanged. + +## Verification + +This instantiates **002_layer_map.md → Per-layer gate**, not the stale “003” reference in 000. These are future execution commands, **not checks run in this docs-only delegation**. Domain: `tests/codex-integration`; additional graph guard: `tests/lab/core-lab-boundary.test.ts`. + +```sh +bun run typecheck +bun test tests/codex-integration/codex-prompt-layers.test.ts \ + tests/codex-integration/codex-prompt-layers-read.test.ts \ + tests/codex-integration/codex-prompt-layers-write.test.ts \ + tests/codex-integration/codex-prompt-base-variants.test.ts \ + tests/codex-integration/codex-prompt-adopt.test.ts \ + tests/codex-integration/codex-prompt-route.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/codex/prompt-layers/encoding.ts \ + src/codex/prompt-layers/revision.ts \ + src/codex/prompt-layers/paths.ts \ + src/codex/prompt-layers/toml-read.ts \ + src/codex/prompt-layers/toml-edit.ts \ + src/codex/prompt-layers.ts +rg -n 'from "[^"]*/prompt-layers"' src gui/src scripts tests | wc -l +``` + +Require each executed command's real exit code 0 and focused tests 0 failures. The external importer baseline is **8** (6 tests + 2 runtime), unaffected by new leaf-local imports. Compare the 44-name facade export surface against origin/dev, including types, zero-consumer names, and `readFileBytes`. Verify declaration bodies with AST/moved-code diff after stripping only import/export linkage changes. Compare the exact moved source ranges and require the acyclic edge result described above. Allow only this layer's explicitly recorded 1,146-line residual; 310 #b owns its remaining extraction. + +Full suite **never locally**. Only the authorized executor uses the 002 remote workspace on `lidge`, verifies that its fetched branch SHA equals the PR head, and runs this gate under Bash pipefail so `tail` cannot mask a test failure: + +```sh +ssh lidge "bash -lc 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-prompt-layers-a && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15'" +``` + +Before dispatch, the parent/executor must check that the shared remote CI checkout is not occupied by another stack; this docs task reserves nothing. Record exact-head CI rollup in the owning layer doc and fill the repository PR template with actual verification evidence. No merge, release, source dogfood, or running-service restart is included. + +## Accept criteria + +1. Parent records a resolution of **S10-SIZE-01** before implementation; no claim that two ≤500-line layers can satisfy the current 1,652→≤400 goal. +2. The source basis is rechecked at execution; every one of the 89 declarations and the line-505 alias has exactly one owner in this inventory. No body, branch, signature, identifier, error string, persistence byte format, or ordered operation changes. +3. Exactly five new leaves match this partition, each ≤400; residual expected 1146 and explicitly assigned to 310 #b. +4. All **44 existing exported names**, including 16 types and the `readFileBytes` alias, remain importable from `src/codex/prompt-layers.ts`; no private helper leaks through that facade. External importer count stays **8**. +5. Every needed residual binding is explicitly imported; no leaf imports the facade, no type-only cycle, no second lock/journal/constant owner, no newly reachable Lab code. +6. All six existing importer tests remain facade-based; the broad “3 textoracle” count is reconciled against actual read sites. Named guards have recorded red→restored-green evidence during execution, not invented passing results. +7. Instantiated typecheck, focused tests, privacy scan, boundary guard, size/import checks, static cycle inspection, remote full-suite exit, and exact-head CI are recorded before the PR is review-ready. No local full suite. +8. Future source diff touches only the original and this layer's planned leaves unless the parent explicitly expands scope. This delegation itself writes only 300/310 Markdown, runs no tests, mutates no git state, and invokes no orchestration/loop/goal commands. + +## PR + +Title: `refactor(codex): extract prompt byte and TOML leaves (split S10 L1/2)` + +Branch: `codex/split-codex-prompt-layers-a`. +Base: `dev`. +Closes: **none**. + +DEV-STACK-03 map for the PR body (PR numbers intentionally unassigned placeholders): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 2 | #TBD-S10-L2 | codex prompt #b | `codex/split-codex-prompt-layers-b` | `codex/split-codex-prompt-layers-a` | read snapshot, fingerprint, single transaction owner, final size cap | +| 1 | #TBD-S10-L1 | codex prompt #a ← you are here | `codex/split-codex-prompt-layers-a` | `dev` | byte codecs, paths, TOML leaves; preserve facade | + +Review this layer's diff only. Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, and Checklist; cite S10-SIZE-01 and the parent's recorded resolution before marking ready. Each layer needs its own actual checks and exact-head CI. Cascade parent edits to L2 before publishing any update; merge remains bottom-up and separately user-authorized. This planning task creates no branch or PR. + +## P stale-check (2026-09-05, wp300) + +origin/dev 3c920af5f; prompt-layers.ts unchanged since 445742966 (1652 lines); 25 slice anchors confirmed by sed. Base `dev` (S10 bottom; 310 #b chains on it). Subdirectory `src/codex/prompt-layers/` follows existing precedent (src/codex/catalog/, src/codex/log-guard/). 003 INTERMEDIATE-RESIDUAL-01 applies: the 1146 residual after #a is bounded by #b (→ 234). Text oracles: three prompt-layers tests read source (per 001) — the audit must list their exact read sites and dispositions. Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change. + +## A amendment (Lovelace audit, GO-WITH-FIXES blockers=1 → folded) + +1. Test change: "all importer tests unchanged" applies to existing assertions and import paths. The authorized test change for CI hygiene is one appended test in tests/codex-integration/codex-prompt-layers.test.ts: seam identity (facade vs leaf) for computeRevision, encodeBasicString/decodeBasicString, inspectOwnership; a decodeBasicString round-trip via the encoding leaf; and a readFileSync+repoPath guard that no leaf under src/codex/prompt-layers/ matches /from\s+["']\.\.\/prompt-layers["']/. +2. Text oracles: the "3 tests read source" claim (001 broad count, plan:356) is false — audit verified none of the codex-prompt-* tests reads prompt-layers.ts as source (all reads are fixture config/store files; codex-prompt-route.test.ts:806/811/820 read codex-prompt-routes.ts and prompt-text-probe.ts, untouched). No retarget. +3. Residual imports: also drop `readFileSync` from the node:fs import (its only use, :487, moves to revision.ts). S10-SIZE-01 is resolved by 003 PURE-MOVE-SIZE-01 (stale plan:20 wording void). +Audit-verified structure: 89/90 declaration ranges exact, 518 disjoint extracted lines, leaf own-imports complete and minimal, DAG toml-edit → toml-read → encoding (+ toml-edit → encoding), paths/revision standalone, 23/23 residual bindings covered, 44/44 exports preserved, 6 test importers exact. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-300.dQlJzd/wt` (branch `codex/split-codex-prompt-layers-a`, base origin/dev 3c920af5f). Executor: gpt-6-astra high (Plato, 01a06f7a-8a4e-7e10-a991-aa39a3799f4a). +- Commits: baef8af7f (move: encoding 81, revision 56, paths 55, toml-read 182, toml-edit 164, prompt-layers.ts residual 1146), f2c9b29aa (test: codex-prompt-layers.test.ts — seam identity, decode round-trip, no back-edge), 82e069c9f (main agent: trimmed the trailing blank line at each leaf EOF flagged by `git diff --check`; leaves 80/54/55/181/163). Diff: 7 files. +- Residual 1146 > 400 is the planned intermediate state (003 INTERMEDIATE-RESIDUAL-01; #b layer 310 → 234). +- Local gate: typecheck 0; focused (6 files) 205 pass / 0 fail; core-lab-boundary 17/0; privacy passed; 8 original-path importers unchanged; 89 declarations single-owned; 44/44 exports. +- Red-drives: (a) decodeBasicString identity → drift/adopt-preview tests + seam test fail, restored; (b) setProjection marker broken → custom-layers write test fails, restored; (c) lab import in paths.ts → management-api transitive guard fails via codex-prompt-routes → prompt-layers → paths → lab/paths, restored 17/0. + +- Adversarial diff review (Anscombe, gpt-6-astra high, 01a06f7f-3ae0-7e32-80cb-8c84cb0284c4): VERDICT: PASS (slices exact modulo 5 inserted blank separators between slices, residual reconstruction exact at 1146, 44/44 exports incl. readFileBytes === revision.readFileOrNull, 17 seams not leaked, DAG per plan, no cycle; #b starting numbers match — 310 line-219 estimates off by one per leaf). +- lidge full suite at 82e069c9f: SUITE_EXIT=0, 18067 pass / 0 fail / 16 skip (/tmp/suite-split-300.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3590 (base dev, head 82e069c9f). CI rollup at record time: OPEN draft=false 82e069c9f =1 =5 CANCELLED=1 SUCCESS=2 diff --git a/devlog/_plan/260905_now_split_train/310_codex_prompt_layers_b.md b/devlog/_plan/260905_now_split_train/310_codex_prompt_layers_b.md new file mode 100644 index 0000000000..555b2346f7 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/310_codex_prompt_layers_b.md @@ -0,0 +1,417 @@ +## Loop spec + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +- Archetype: **pure-move**. Work class: **C3**, bounded docs-only subagent; parent owns orchestration, loop, goal, and execution worktrees. No cxc state commands here. +- Non-goals: no behavior fixes, parser changes, hash framing changes, new cache/state, durability rewrite, function-body refactor, public rename/removal, test weakening, caller import migration, or operational writes. +- Goal: consume L1's pure leaves, separate read snapshot/probe/transaction/adoption owners, and bring the original file and every new leaf to ≤400 lines. +- Verifier: **002 “Per-layer gate”**, instantiated below; current delegation verifies the two documents only, without test runs. +- Stop: six additional leaves, acyclic ownership, 44 preserved facade exports, and final size gates are independently verifiable; implementation remains gated on S10-SIZE-01. +- Escalation: stale source coordinates, behavior change, missing export, required edits outside S10, source-oracle uncertainty, cycle requiring a redesign, or the unsatisfied ≤500-line limit go to the parent. Do not edit 000/001/002 or add layers yourself. + +Source/audit basis: docs HEAD `4cc219549`; pinned code `1362b1a38`; `000_plan.md`, `001_stale_check.md`, `002_layer_map.md` S10 rows 300/310; lane evidence `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:143–155`. The opening tip recorded in 000/001 is historical; this document's source coordinates use the pinned code above. + +Structural decision (cxc-dev-architecture): a 1,652-line feature currently combines inventory, byte codecs, TOML edits, read projections, probe admission, and writes. Reject leaving it intact (misses the size goal), deleting/configuring behavior (not a pure move), widening `features.ts` (explicit boundary at source lines 4–8), or routing leaves through a new internal barrel (creates back-edges). Choose cohesive leaves in `src/codex/prompt-layers/` with a stable original-path compatibility facade. Reuse existing `prompt-journal.ts` and `prompt-lock.ts`, without moving or duplicating their durability/lock implementation. + +Convention evidence: `src/config.ts:129` re-exports `./config/paths`; `src/config.ts:162` re-exports `./config/rebase-provenance`; `src/types/*.ts` and `src/codex/log-guard/*.ts` use focused sibling/subfolder leaves. This is the existing compatibility-boundary convention, not a new convenience `index.ts`. + +Current map: `src/server/management/codex-prompt-routes.ts:26–49`, `src/server/management/context.ts:9`, and 6 tests → `prompt-layers.ts` → config/home/path helpers, marker, journal, lock, Node fs/path/crypto. Intended map: same external imports → original facade → read/transform/transaction leaves → those same dependencies. Blast radius: local Codex prompt feature; no HTTP route, DTO, CLI, auth, persistence format, or public signature change. Tests keep importing the facade. + +Ordering is dependency-first among low-fan-in seams. L1 takes `toml-edit` (0 external importers), `revision` (1), and `paths`/`encoding`/`toml-read` (2 each), installing their prerequisite leaves together. L2 takes higher-fan-in `inventory` (3), `store` (3), `snapshot` (6), then `transaction` (1), `fingerprint` (1), and `adoption` (2). Those last low-fan-in operations cannot move earlier without also moving their snapshot/store dependencies or creating facade return edges. Original callers are not retargeted, so low consumer count is not used to justify export removal. + +**S10-SIZE-01 — unresolved execution gate:** 002 says every layer stays ≤500 changed source lines, but two pure-move layers must remove at least `1652 - 400 = 1252` original lines, before adding leaves/imports. Even counting a moved line only once, `2 × 500 < 1252`; normal added+deleted diff accounting is larger. This concrete partition moves 518 original lines in L1 and 913 in L2. The parent must approve a documented pure-move size exception or revise 002's layer count before implementation. This delegated task does not grant that exception, add a third layer, or edit 002. The two documents remain the requested feasible **file partition**, not a claim that the current per-PR size budget is satisfiable. + +## Symbol inventory + +Ranges are inclusive declaration spans at `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`, not line numbers after L1. Read with `git show origin/dev:src/codex/prompt-layers.ts | nl -ba`; `git diff origin/dev -- src/codex/prompt-layers.ts` was empty. The installed TypeScript package exposes version metadata rather than the compiler AST API, so declaration endpoints were obtained with installed ast-grep, cross-checked against `rg -n '^(export )?(function|const|let|interface|type|class|enum) '`. + +There are **89 declarations plus the existing export-alias statement at line 505**, all inventoried below. Imports at 29–46 are dependency bindings, listed in Leaf partition rather than counted as locally owned declarations. Consumer counts are **distinct external files importing this binding from the original facade**, not textual hits of homonyms such as `Paths` or `commit`. Method: `rg -l 'from.*prompt-layers"' src gui/src scripts tests` finds 8 files (2 runtime, 6 tests); ast-grep `import_statement` selects their facade imports, and `rg -w ` counts matching import blocks. The alias is counted under `readFileBytes`. Private declarations have 0 external consumers; zero is not a deletion license. Comments mentioning `WriteError`, `adoptDeveloperInstructions`, and `salvageProjection` in the route test are excluded. + +In the table, leaf names expand to `src/codex/prompt-layers/.ts`; `residual` means `src/codex/prompt-layers.ts`. L2 targets remain in the original file through L1. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `LayerClass` | type | 53–58 | yes | 0 | `inventory.ts` (L2) | +| `ToggleId` | type | 60–65 | yes | 0 | `inventory.ts` (L2) | +| `LayerDescriptor` | interface | 67–76 | yes | 0 | `inventory.ts` (L2) | +| `LAYER_INVENTORY` | const | 88–122 | yes | 3 | `inventory.ts` (L2) | +| `TOGGLE_KEYS` | const | 130–136 | no | 0 | `inventory.ts` (L2) | +| `TOGGLE_IDS` | const | 138–138 | yes | 1 | `inventory.ts` (L2) | +| `isToggleId` | function | 140–142 | yes | 1 | `inventory.ts` (L2) | +| `Paths` | interface | 148–152 | yes | 2 | `paths.ts` (L1) | +| `activeCodexHome` | function | 154–163 | no | 0 | `paths.ts` (L1) | +| `activeConfigPath` | function | 165–167 | yes | 0 | `paths.ts` (L1) | +| `activeStorePath` | function | 169–171 | yes | 0 | `paths.ts` (L1) | +| `activeBaseVariantDir` | function | 181–183 | yes | 0 | `paths.ts` (L1) | +| `PROBE_INSTRUCTION_FILES` | const | 191–191 | no | 0 | `fingerprint.ts` (L2) | +| `probeInstructionFilenames` | function | 203–213 | no | 0 | `fingerprint.ts` (L2) | +| `rootArrayEntries` | function | 237–242 | no | 0 | `toml-read.ts` (L1) | +| `PARSE_FAILED` | const | 249–249 | no | 0 | `toml-read.ts` (L1) | +| `rootValue` | function | 252–262 | no | 0 | `toml-read.ts` (L1) | +| `scanRootArrayEntries` | function | 272–296 | no | 0 | `toml-read.ts` (L1) | +| `probeProjectDocDirs` | function | 313–337 | no | 0 | `fingerprint.ts` (L2) | +| `projectRootMarkers` | function | 340–345 | no | 0 | `fingerprint.ts` (L2) | +| `hasRootKey` | function | 354–358 | no | 0 | `toml-read.ts` (L1) | +| `scanHasRootKey` | function | 361–364 | no | 0 | `toml-read.ts` (L1) | +| `updateFingerprintField` | function | 384–388 | no | 0 | `revision.ts` (L1) | +| `journalPathFor` | function | 390–392 | no | 0 | `paths.ts` (L1) | +| `lockPathFor` | function | 394–396 | no | 0 | `paths.ts` (L1) | +| `CharacterFinding` | interface | 404–409 | yes | 0 | `encoding.ts` (L1) | +| `normalizeBody` | function | 412–414 | yes | 2 | `encoding.ts` (L1) | +| `findInvalidCharacter` | function | 417–440 | yes | 2 | `encoding.ts` (L1) | +| `encodeBasicString` | function | 448–450 | yes | 1 | `encoding.ts` (L1) | +| `decodeBasicString` | function | 458–477 | yes | 1 | `encoding.ts` (L1) | +| `readFileOrNull` | function | 484–491 | alias readFileBytes (505) | 0 | `revision.ts` (L1) | +| `computeRevision` | function | 493–503 | yes | 1 | `revision.ts` (L1) | +| `TABLE_HEADER` | const | 513–513 | no | 0 | `toml-read.ts` (L1) | +| `rootLines` | function | 516–520 | no | 0 | `toml-read.ts` (L1) | +| `tableLines` | function | 523–531 | no | 0 | `toml-read.ts` (L1) | +| `boolInLines` | function | 533–541 | no | 0 | `toml-read.ts` (L1) | +| `DEV_INSTRUCTIONS_KEY` | const | 555–555 | no | 0 | `toml-read.ts` (L1) | +| `CANONICAL_LINE` | const | 556–556 | no | 0 | `toml-read.ts` (L1) | +| `ANY_DEV_INSTRUCTIONS` | const | 557–557 | no | 0 | `toml-read.ts` (L1) | +| `Ownership` | type | 559–567 | yes | 0 | `toml-read.ts` (L1) | +| `inspectOwnership` | function | 569–582 | yes | 2 | `toml-read.ts` (L1) | +| `CustomLayer` | interface | 588–594 | yes | 2 | `store.ts` (L2) | +| `LAYER_ID` | const | 596–596 | no | 0 | `store.ts` (L2) | +| `isCustomLayer` | function | 598–605 | no | 0 | `store.ts` (L2) | +| `parseStore` | function | 608–622 | yes | 1 | `store.ts` (L2) | +| `composeProjection` | function | 625–627 | yes | 2 | `store.ts` (L2) | +| `ToggleState` | interface | 633–645 | yes | 0 | `snapshot.ts` (L2) | +| `Drift` | type | 647–652 | yes | 0 | `snapshot.ts` (L2) | +| `BaseVariant` | interface | 655–660 | yes | 0 | `snapshot.ts` (L2) | +| `BaseSelection` | type | 678–678 | yes | 1 | `snapshot.ts` (L2) | +| `PromptLayerSnapshot` | interface | 680–694 | yes | 1 | `snapshot.ts` (L2) | +| `readToggle` | function | 696–713 | no | 0 | `snapshot.ts` (L2) | +| `readModelInstructionsFile` | function | 715–733 | no | 0 | `snapshot.ts` (L2) | +| `BASE_VARIANT_ID` | const | 736–736 | no | 0 | `snapshot.ts` (L2) | +| `readBaseVariants` | function | 746–776 | yes | 1 | `snapshot.ts` (L2) | +| `resolveBaseSelection` | function | 785–805 | yes | 0 | `snapshot.ts` (L2) | +| `readPromptLayers` | function | 811–855 | yes | 6 | `snapshot.ts` (L2) | +| `computePromptProbeStateFingerprint` | function | 887–942 | yes | 1 | `fingerprint.ts` (L2) | +| `probeSkillManifests` | function | 960–974 | no | 0 | `fingerprint.ts` (L2) | +| `WriteError` | type | 980–993 | yes | 1 | `transaction.ts` (L2) | +| `WriteResult` | type | 995–997 | yes | 1 | `transaction.ts` (L2) | +| `dominantEol` | function | 1000–1005 | no | 0 | `toml-edit.ts` (L1) | +| `splitLines` | function | 1007–1009 | no | 0 | `toml-edit.ts` (L1) | +| `splitBom` | function | 1023–1027 | no | 0 | `toml-edit.ts` (L1) | +| `joinLines` | function | 1029–1032 | no | 0 | `toml-edit.ts` (L1) | +| `firstTableIndex` | function | 1034–1037 | no | 0 | `toml-edit.ts` (L1) | +| `setRootBool` | function | 1040–1056 | no | 0 | `toml-edit.ts` (L1) | +| `setRootString` | function | 1065–1081 | no | 0 | `toml-edit.ts` (L1) | +| `setTableBool` | function | 1084–1108 | no | 0 | `toml-edit.ts` (L1) | +| `setProjection` | function | 1115–1142 | no | 0 | `toml-edit.ts` (L1) | +| `serializeStore` | function | 1144–1146 | no | 0 | `store.ts` (L2) | +| `Mutation` | interface | 1148–1151 | no | 0 | `transaction.ts` (L2) | +| `commit` | function | 1158–1272 | no | 0 | `transaction.ts` (L2) | +| `rollback` | function | 1275–1294 | no | 0 | `transaction.ts` (L2) | +| `setToggle` | function | 1297–1306 | yes | 2 | residual | +| `selectBaseVariant` | function | 1316–1335 | yes | 2 | residual | +| `MAX_BASE_VARIANTS` | const | 1338–1338 | yes | 2 | residual | +| `writeBaseVariant` | function | 1351–1434 | yes | 2 | residual | +| `newBaseVariantId` | function | 1436–1442 | no | 0 | residual | +| `writeCustomLayers` | function | 1445–1466 | yes | 2 | residual | +| `AdoptPreview` | interface | 1476–1484 | yes | 0 | `adoption.ts` (L2) | +| `newLayerId` | function | 1486–1492 | no | 0 | `store.ts` (L2) | +| `previewAdopt` | function | 1498–1536 | yes | 2 | `adoption.ts` (L2) | +| `adoptDeveloperInstructions` | function | 1539–1565 | yes | 2 | `adoption.ts` (L2) | +| `removeUnownedProjection` | function | 1568–1579 | no | 0 | `toml-edit.ts` (L1) | +| `SalvagePreview` | interface | 1589–1595 | yes | 0 | `adoption.ts` (L2) | +| `UNRECOVERABLE` | const | 1597–1604 | no | 0 | `adoption.ts` (L2) | +| `previewSalvage` | function | 1606–1620 | yes | 2 | `adoption.ts` (L2) | +| `salvageProjection` | function | 1627–1652 | yes | 2 | `adoption.ts` (L2) | +| `readFileBytes` | export alias of `readFileOrNull` | 505–505 | yes | 0 | `revision.ts` (L1); preserve alias exactly | + +## Leaf partition + +Prerequisite: all five L1 leaves in 300 exist unchanged. This layer creates the six files below; it does not move L1 bodies again. + +### src/codex/prompt-layers/inventory.ts + +- Move original ranges `src/codex/prompt-layers.ts:48–143` including comments and blank lines: 96 lines. +- Symbols: `LayerClass`, `ToggleId`, `LayerDescriptor`, `LAYER_INVENTORY`, `TOGGLE_KEYS`, `TOGGLE_IDS`, `isToggleId`. +- Expected length: **96 lines**, including 0 one-line imports; limit 400. +- Own imports: none; do not add a facade import. + +### src/codex/prompt-layers/store.ts + +- Move original ranges `src/codex/prompt-layers.ts:584–628`, `src/codex/prompt-layers.ts:1144–1147`, `src/codex/prompt-layers.ts:1486–1493` including comments and blank lines: 57 lines. +- Symbols: `CustomLayer`, `LAYER_ID`, `isCustomLayer`, `parseStore`, `composeProjection`, `serializeStore`, `newLayerId`. +- Expected length: **59 lines**, including 1 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { randomBytes } from "node:crypto"; +``` + +### src/codex/prompt-layers/snapshot.ts + +- Move original ranges `src/codex/prompt-layers.ts:629–856` including comments and blank lines: 228 lines. +- Symbols: `ToggleState`, `Drift`, `BaseVariant`, `BaseSelection`, `PromptLayerSnapshot`, `readToggle`, `readModelInstructionsFile`, `BASE_VARIANT_ID`, `readBaseVariants`, `resolveBaseSelection`, `readPromptLayers`. +- Expected length: **238 lines**, including 9 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { existsSync, readdirSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { expandUserPath } from "../../config"; +import { activeConfigPath, activeStorePath, activeBaseVariantDir, type Paths } from "./paths"; +import { LAYER_INVENTORY, TOGGLE_KEYS, TOGGLE_IDS, type ToggleId } from "./inventory"; +import { readFileOrNull, computeRevision } from "./revision"; +import { decodeBasicString } from "./encoding"; +import { rootLines, tableLines, boolInLines, inspectOwnership } from "./toml-read"; +import { parseStore, composeProjection, type CustomLayer } from "./store"; +``` + +### src/codex/prompt-layers/fingerprint.ts + +- Move original ranges `src/codex/prompt-layers.ts:185–214`, `src/codex/prompt-layers.ts:298–346`, `src/codex/prompt-layers.ts:857–975` including comments and blank lines: 198 lines. +- Symbols: `PROBE_INSTRUCTION_FILES`, `probeInstructionFilenames`, `probeProjectDocDirs`, `projectRootMarkers`, `computePromptProbeStateFingerprint`, `probeSkillManifests`. +- Expected length: **208 lines**, including 9 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { existsSync, readdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { createHash } from "node:crypto"; +import { expandUserPath } from "../../config"; +import { resolveCodexHomeDir } from "../home"; +import { activeConfigPath, activeStorePath, activeBaseVariantDir, type Paths } from "./paths"; +import { readFileOrNull, computeRevision, updateFingerprintField } from "./revision"; +import { readBaseVariants, resolveBaseSelection } from "./snapshot"; +import { rootArrayEntries, hasRootKey } from "./toml-read"; +``` + +### src/codex/prompt-layers/transaction.ts + +- Move original ranges `src/codex/prompt-layers.ts:976–998`, `src/codex/prompt-layers.ts:1148–1295` including comments and blank lines: 171 lines. +- Symbols: `WriteError`, `WriteResult`, `Mutation`, `commit`, `rollback`. +- Expected length: **178 lines**, including 6 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { existsSync } from "node:fs"; +import { activeConfigPath, activeStorePath, journalPathFor, lockPathFor, type Paths } from "./paths"; +import { readFileOrNull, computeRevision } from "./revision"; +import { readPromptLayers, type PromptLayerSnapshot } from "./snapshot"; +import { durableWrite, durableDelete, encodeJournal, ensureDir, hashBytes, recoverIfNeeded as recoverJournal, type JournalRecord } from "../prompt-journal"; +import { release, stillHeld, tryAcquire } from "../prompt-lock"; +``` + +### src/codex/prompt-layers/adoption.ts + +- Move original ranges `src/codex/prompt-layers.ts:1468–1485`, `src/codex/prompt-layers.ts:1494–1566`, `src/codex/prompt-layers.ts:1581–1652` including comments and blank lines: 163 lines. +- Symbols: `AdoptPreview`, `previewAdopt`, `adoptDeveloperInstructions`, `SalvagePreview`, `UNRECOVERABLE`, `previewSalvage`, `salvageProjection`. +- Expected length: **174 lines**, including 10 one-line imports and one separating blank line; limit 400. +- Own imports: + +```ts +import { randomBytes } from "node:crypto"; +import { dirname } from "node:path"; +import { activeConfigPath, activeStorePath, type Paths } from "./paths"; +import { readFileOrNull } from "./revision"; +import { decodeBasicString, normalizeBody, findInvalidCharacter } from "./encoding"; +import { inspectOwnership } from "./toml-read"; +import { composeProjection, serializeStore, newLayerId, type CustomLayer } from "./store"; +import { removeUnownedProjection, setProjection } from "./toml-edit"; +import { commit, type WriteResult } from "./transaction"; +import { durableWriteExclusive } from "../prompt-journal"; +``` + +Inherited L1 leaf sizes: `encoding.ts` 81, `revision.ts` 55, `paths.ts` 54, `toml-read.ts` 180, `toml-edit.ts` 163. Final residual keeps `setToggle`, `selectBaseVariant`, `MAX_BASE_VARIANTS`, `writeBaseVariant`, `newBaseVariantId`, and `writeCustomLayers` (original 1296–1467). All other declarations have exactly one leaf owner. No #c layer is presumed. + +| Stage | Original lines extracted this layer | New leaves this layer (expected total) | Original residual | +|---|---:|---:|---:| +| Basis | 0 | 0 | 1652 | +| L1 / 300 #a | 518 | 533 across 5 files | 1146 | +| L2 / 310 #b | 913 | 953 across 6 files | 234 | + +Accounting uses inclusive source chunks (comments retained), one-line import/export statements as shown, and one blank line after each non-empty leaf import block. L1: `1652 - 518 - 2 + 14 = 1146`: remove the two obsolete facade imports at old 33/35, add five local imports + seven re-exports + two separator lines. L2: `1146 - 913 - 13 + 4 + 10 = 234`: reduce the remaining sixteen old import lines to three; grow five leaf-local import lines to nine; add ten named re-export lines. Original retained content is lines 1–47, 297, 365, and 1296–1467 with imports rewritten; the final implementation body chunk is 172 lines. Total completed source estimate: `533 + 953 + 234 = 1720`; the +68 lines over 1652 are import/re-export/spacing overhead, not duplicated bodies. Expected counts are formatting estimates, but the ≤400 final cap is mechanical. + +Only cross-leaf production dependencies gain named leaf exports: paths → `journalPathFor, lockPathFor`; revision → `readFileOrNull, updateFingerprintField`; toml-read → `rootArrayEntries, hasRootKey, rootLines, tableLines, boolInLines, TABLE_HEADER, DEV_INSTRUCTIONS_KEY, ANY_DEV_INSTRUCTIONS`; toml-edit → `setRootBool, setRootString, setTableBool, setProjection, removeUnownedProjection`; inventory → `TOGGLE_KEYS`; store → `serializeStore, newLayerId`; snapshot → `BASE_VARIANT_ID`; transaction → `commit`. Keep all other original private declarations private. `readFileOrNull` keeps its declaration name and the existing leaf alias `export { readFileOrNull as readFileBytes };`. None of these extra internal names is added to the original facade's public surface. + +## Re-export block + +The complete final block is below, including all seven L1 lines. The five direct exported mutation/limit declarations remain unchanged in the residual; `newBaseVariantId` remains private. + +```ts +export { activeConfigPath, activeStorePath, activeBaseVariantDir } from "./prompt-layers/paths"; +export type { Paths } from "./prompt-layers/paths"; +export { computeRevision, readFileBytes } from "./prompt-layers/revision"; +export { normalizeBody, findInvalidCharacter, encodeBasicString, decodeBasicString } from "./prompt-layers/encoding"; +export type { CharacterFinding } from "./prompt-layers/encoding"; +export { inspectOwnership } from "./prompt-layers/toml-read"; +export type { Ownership } from "./prompt-layers/toml-read"; +export { LAYER_INVENTORY, TOGGLE_IDS, isToggleId } from "./prompt-layers/inventory"; +export type { LayerClass, ToggleId, LayerDescriptor } from "./prompt-layers/inventory"; +export { parseStore, composeProjection } from "./prompt-layers/store"; +export type { CustomLayer } from "./prompt-layers/store"; +export { readBaseVariants, resolveBaseSelection, readPromptLayers } from "./prompt-layers/snapshot"; +export type { ToggleState, Drift, BaseVariant, BaseSelection, PromptLayerSnapshot } from "./prompt-layers/snapshot"; +export { computePromptProbeStateFingerprint } from "./prompt-layers/fingerprint"; +export type { WriteError, WriteResult } from "./prompt-layers/transaction"; +export { previewAdopt, adoptDeveloperInstructions, previewSalvage, salvageProjection } from "./prompt-layers/adoption"; +export type { AdoptPreview, SalvagePreview } from "./prompt-layers/adoption"; +``` + +Re-exports bind nothing locally. Replace the residual import section with these exact imports: + +```ts +import { join, resolve } from "node:path"; +import { randomBytes } from "node:crypto"; +import { durableWrite, durableDelete, ensureDir } from "./prompt-journal"; +import { activeConfigPath, activeBaseVariantDir, type Paths } from "./prompt-layers/paths"; +import { readFileOrNull } from "./prompt-layers/revision"; +import { normalizeBody, findInvalidCharacter } from "./prompt-layers/encoding"; +import { inspectOwnership } from "./prompt-layers/toml-read"; +import { setRootBool, setRootString, setTableBool, setProjection } from "./prompt-layers/toml-edit"; +import { isToggleId, TOGGLE_KEYS } from "./prompt-layers/inventory"; +import { composeProjection, serializeStore, type CustomLayer } from "./prompt-layers/store"; +import { readBaseVariants, resolveBaseSelection, readPromptLayers, BASE_VARIANT_ID, type BaseSelection, type BaseVariant } from "./prompt-layers/snapshot"; +import { commit, type WriteResult } from "./prompt-layers/transaction"; +``` + +Remove all other old imports from the original file. In particular, `snapshot.ts` must not obtain `Paths`, `CustomLayer`, or `ToggleId` from the facade, and `transaction.ts` must not obtain `PromptLayerSnapshot` from it. + +## Module-level state and cycles + +All coordinates below are in `origin/dev:src/codex/prompt-layers.ts`. + +| Top-level state/constant | Line(s) | Single owner after S10 | Preservation | +|---|---|---|---| +| `LAYER_INVENTORY` | 88–122 | `prompt-layers/inventory.ts` (L2) | same shallow `Object.freeze`, same rows/order and reference identity | +| `TOGGLE_KEYS` | 130–136 | `prompt-layers/inventory.ts` (L2) | one object; export internally for readers/writers, never copy | +| `TOGGLE_IDS` | 138 | `prompt-layers/inventory.ts` (L2) | same frozen derivation after `TOGGLE_KEYS` | +| `PROBE_INSTRUCTION_FILES` | 191 | `prompt-layers/fingerprint.ts` (L2) | same tuple/order | +| `PARSE_FAILED` | 249 | `prompt-layers/toml-read.ts` (L1) | unique Symbol stays beside every identity comparison, not recreated | +| `TABLE_HEADER` | 513 | `prompt-layers/toml-read.ts` (L1) | one non-global RegExp shared with edits | +| `DEV_INSTRUCTIONS_KEY` | 555 | `prompt-layers/toml-read.ts` (L1) | same literal, edit leaf imports it | +| `CANONICAL_LINE` | 556 | `prompt-layers/toml-read.ts` (L1) | remains private non-global RegExp | +| `ANY_DEV_INSTRUCTIONS` | 557 | `prompt-layers/toml-read.ts` (L1) | one non-global RegExp shared with edits | +| `LAYER_ID` | 596 | `prompt-layers/store.ts` (L2) | validator-local owner | +| `BASE_VARIANT_ID` | 736 | `prompt-layers/snapshot.ts` (L2) | same object; residual writer imports it directly | +| `MAX_BASE_VARIANTS` | 1338 | original residual | retain exported constant and cap, no snapshot back-import | +| `UNRECOVERABLE` | 1597–1604 | `prompt-layers/adoption.ts` (L2) | same frozen list shared by previews | + +There is **no top-level `let`, Map, Set, WeakMap, cached path, or acquired lock handle** in this file. `new Set` at 619, 1437, and 1487 is call-local; hashes at 494/892 and `acquired/handle` at 1172/1174 are call-local. The existing filesystem lock stays owned by `src/codex/prompt-lock.ts`; commit at 1158–1272 moves whole to `transaction.ts`, with its acquisition/recovery/check/rollback/release sequence intact. It does not introduce another mutex or move lock acquisition to module evaluation. + +Path resolution stays call-time (154–183). Pure reads (811–855) never acquire a lock, repair, recover, or write; the journal/lock leaves are not dependencies of `snapshot.ts`. The length-framed revision and probe fields share `revision.ts:updateFingerprintField` (384–388), not copied implementations. Journal hashBytes remains a different existing contract in `prompt-journal.ts`; do not substitute one hash for the other. + +Cycle prevention and coupling classification: + +- Current audited graph has no return path (lane 013:151); the eight direct consumers confirmed by rg include no reverse imports from the current dependencies. +- L1 direction: residual → paths/revision/encoding/toml-read/toml-edit; toml-edit → toml-read + encoding; toml-read → encoding. No leaf imports `../prompt-layers` or `./index`. +- L2 direction: residual → inventory/store/snapshot/transaction; adoption → transaction + store + TOML leaves; transaction → snapshot; fingerprint → snapshot + paths + revision + toml-read; snapshot → inventory/store + L1 leaves. Every leaf imports its exact lower owner, including types. +- Moving commit alone while leaving `readPromptLayers` behind would form `facade → transaction → facade`; L2 moves snapshot first within the same layer. `WriteResult` moves with transaction and imports `PromptLayerSnapshot` from snapshot, never from facade. +- Moving fingerprint alone while importing `readBaseVariants/resolveBaseSelection` through the facade has the same problem; they move together in L2. Snapshot exports `BASE_VARIANT_ID` internally so the remaining writer never makes snapshot import its caller. +- These edges are functional/sequential coupling. Existing file-before-config and clear-key-before-delete ordering at 1391–1409/1423–1431 is temporal coupling, preserved rather than redesigned. No new common mutable state or content coupling is introduced. Existing internal declarations become leaf exports only when another production owner actually needs them; they do not become new facade exports. + +Future static check: capture ast-grep `import_statement` and `export_statement` edges for the facade and all eleven leaves, resolve relative paths, include type-only edges, and require a DFS/SCC result with no cycle containing a split module. The explicit adjacency above is the expected graph. Check transitive return paths through unchanged config/home/journal/lock dependencies as well; do not install a new analysis dependency or claim typecheck alone detects cycles. + +## Tests + +Direct importer list from `rg -l 'from.*prompt-layers"' tests` (all remain **unchanged**, importing the original facade): + +| Test file | Import line at origin/dev | Disposition | +|---|---:|---| +| `tests/codex-integration/codex-prompt-layers.test.ts` | 20 | unchanged | +| `tests/codex-integration/codex-prompt-layers-read.test.ts` | 16 | unchanged | +| `tests/codex-integration/codex-prompt-layers-write.test.ts` | 15 | unchanged | +| `tests/codex-integration/codex-prompt-base-variants.test.ts` | 16 | unchanged | +| `tests/codex-integration/codex-prompt-adopt.test.ts` | 19 | unchanged | +| `tests/codex-integration/codex-prompt-route.test.ts` | 14 | unchanged | + +**Exact-path text-oracle readers of `src/codex/prompt-layers.ts`: none.** Reproducing 001's broad read-function/basename intersection returns the following 3 files, but inspection confirms each reads fixture data, not this source. This resolves the apparent disagreement with lane 013:152 rather than inventing retargets. + +| Broad-search candidate | Exact read site and actual target | Disposition | +|---|---|---| +| `tests/codex-integration/codex-prompt-layers-write.test.ts` | line 37 `readFileSync(path, "utf8")`: fixture config/store through local helper | unchanged; no retarget-to-leaf | +| `tests/codex-integration/codex-prompt-layers-read.test.ts` | lines 203/204 `Bun.file(paths.configPath/storePath).text()`; line 217 `Bun.file(nested).exists()` | unchanged; fixture files, no source reader | +| `tests/codex-integration/codex-prompt-adopt.test.ts` | lines 75/93/111/178 read fixture config; line 163 reads salvage backup | unchanged; no retarget-to-leaf | + +Other importer read sites: `codex-prompt-base-variants.test.ts:34` reads fixture files; `codex-prompt-route.test.ts:70` reads fixtures, while its actual source guards read `src/server/management/codex-prompt-routes.ts` at **806** and `src/codex/prompt-text-probe.ts` at **811/820**. All unchanged: neither source is split in S10. There is no S10 retarget-to-leaf and no existing explicit source scan list requiring add-leaf-to-scan-list. + +`tests/lab/core-lab-boundary.test.ts:69` is a generic graph reader (not an explicit prompt-source oracle). Its import/re-export traversal discovers reachable leaves automatically; keep its `PROTECTED` roots and assertions unchanged. Run it as an extra boundary check because the facade is consumed from management code, even though 002 only mandates it when server/router/lib paths themselves are touched. Do not weaken the graph or manufacture an exemption. + +The future executor drives the named guards red once by a temporary mutation in its own isolated layer worktree, records the actual expected failure, restores the mutation, then runs green. No test, mutation, or red/green exercise is executed during this planning task. + +Guards to drive red once in L2: + +1. `tests/codex-integration/codex-prompt-layers-write.test.ts:117`: temporarily disable revision rejection in `prompt-layers/transaction.ts:commit`; stale-revision/no-write guard must fail. Preserve lock/refusal cases at 215/224/252 and rollback at 337. +2. `tests/codex-integration/codex-prompt-route.test.ts:1090`: temporarily remove length framing in the shared `revision.ts:updateFingerprintField` in the isolated worktree; the field-boundary collision guard must fail. Restore L1 byte content afterward; no L1 change remains in L2. +3. `tests/codex-integration/codex-prompt-route.test.ts:1410`: temporarily omit the skill-manifest fields in `fingerprint.ts`; manifest-edit invalidation must fail. Keep quoted-key, parent-directory, and parser-failure cases at 1278/1322/1365. +4. `tests/codex-integration/codex-prompt-base-variants.test.ts:131`: temporarily omit clearing a selected variant's config key in the residual writer; live-delete guard must fail. +5. `tests/codex-integration/codex-prompt-layers-read.test.ts:193` and `codex-prompt-adopt.test.ts:154` remain read-purity/backup characterization coverage; their expected byte/mode checks must not be altered. + +Transaction/journal code can carry config bytes containing credentials. Preserve existing privacy and fail-closed checks exactly; any newly found vulnerability belongs in ignored scratch, not this public plan. No new disclosure or behavior repair is part of the move. + +## Verification + +This instantiates **002_layer_map.md → Per-layer gate**, not the stale “003” reference in 000. These are future execution commands, **not checks run in this docs-only delegation**. Domain: `tests/codex-integration`; additional graph guard: `tests/lab/core-lab-boundary.test.ts`. + +```sh +bun run typecheck +bun test tests/codex-integration/codex-prompt-layers.test.ts \ + tests/codex-integration/codex-prompt-layers-read.test.ts \ + tests/codex-integration/codex-prompt-layers-write.test.ts \ + tests/codex-integration/codex-prompt-base-variants.test.ts \ + tests/codex-integration/codex-prompt-adopt.test.ts \ + tests/codex-integration/codex-prompt-route.test.ts \ + tests/codex-integration/codex-prompt-journal.test.ts \ + tests/codex-integration/codex-prompt-lock.test.ts \ + tests/codex-integration/codex-prompt-text-probe.test.ts +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/codex/prompt-layers/encoding.ts \ + src/codex/prompt-layers/revision.ts \ + src/codex/prompt-layers/paths.ts \ + src/codex/prompt-layers/toml-read.ts \ + src/codex/prompt-layers/toml-edit.ts \ + src/codex/prompt-layers/inventory.ts \ + src/codex/prompt-layers/store.ts \ + src/codex/prompt-layers/snapshot.ts \ + src/codex/prompt-layers/fingerprint.ts \ + src/codex/prompt-layers/transaction.ts \ + src/codex/prompt-layers/adoption.ts \ + src/codex/prompt-layers.ts +rg -n 'from "[^"]*/prompt-layers"' src gui/src scripts tests | wc -l +``` + +Require each executed command's real exit code 0 and focused tests 0 failures. The external importer baseline is **8** (6 tests + 2 runtime), unaffected by new leaf-local imports. Compare the 44-name facade export surface against origin/dev, including types, zero-consumer names, and `readFileBytes`. Verify declaration bodies with AST/moved-code diff after stripping only import/export linkage changes. Compare the exact moved source ranges and require the acyclic edge result described above. All eleven leaves and the estimated 234-line original must be ≤400; no later S10 layer is assumed. + +Full suite **never locally**. Only the authorized executor uses the 002 remote workspace on `lidge`, verifies that its fetched branch SHA equals the PR head, and runs this gate under Bash pipefail so `tail` cannot mask a test failure: + +```sh +ssh lidge "bash -lc 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-prompt-layers-b && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15'" +``` + +Before dispatch, the parent/executor must check that the shared remote CI checkout is not occupied by another stack; this docs task reserves nothing. Record exact-head CI rollup in the owning layer doc and fill the repository PR template with actual verification evidence. No merge, release, source dogfood, or running-service restart is included. + +## Accept criteria + +1. Parent records a resolution of **S10-SIZE-01** before implementation; no claim that two ≤500-line layers can satisfy the current 1,652→≤400 goal. +2. The source basis is rechecked at execution; every one of the 89 declarations and the line-505 alias has exactly one owner in this inventory. No body, branch, signature, identifier, error string, persistence byte format, or ordered operation changes. +3. Exactly six additional leaves match this partition; all eleven leaves and the residual are ≤400 (residual expected 234). L1 leaves are unchanged in the committed L2 delta. +4. All **44 existing exported names**, including 16 types and the `readFileBytes` alias, remain importable from `src/codex/prompt-layers.ts`; no private helper leaks through that facade. External importer count stays **8**. +5. Every needed residual binding is explicitly imported; no leaf imports the facade, no type-only cycle, no second lock/journal/constant owner, no newly reachable Lab code. +6. All six existing importer tests remain facade-based; the broad “3 textoracle” count is reconciled against actual read sites. Named guards have recorded red→restored-green evidence during execution, not invented passing results. +7. Instantiated typecheck, focused tests, privacy scan, boundary guard, size/import checks, static cycle inspection, remote full-suite exit, and exact-head CI are recorded before the PR is review-ready. No local full suite. +8. Future source diff touches only the original and this layer's planned leaves unless the parent explicitly expands scope. This delegation itself writes only 300/310 Markdown, runs no tests, mutates no git state, and invokes no orchestration/loop/goal commands. + +## PR + +Title: `refactor(codex): isolate prompt reads and journaled commits (split S10 L2/2)` + +Branch: `codex/split-codex-prompt-layers-b`. +Base: `codex/split-codex-prompt-layers-a`. +Closes: **none**. + +DEV-STACK-03 map for the PR body (PR numbers intentionally unassigned placeholders): + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 2 | #TBD-S10-L2 | codex prompt #b ← you are here | `codex/split-codex-prompt-layers-b` | `codex/split-codex-prompt-layers-a` | read snapshot, fingerprint, single transaction owner, final size cap | +| 1 | #TBD-S10-L1 | codex prompt #a | `codex/split-codex-prompt-layers-a` | `dev` | byte codecs, paths, TOML leaves; preserve facade | + +Depends on #TBD-S10-L1. Review this layer's diff only. Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, and Checklist; cite S10-SIZE-01 and the parent's recorded resolution before marking ready. Each layer needs its own actual checks and exact-head CI. Cascade parent edits to L2 before publishing any update; merge remains bottom-up and separately user-authorized. This planning task creates no branch or PR. diff --git a/devlog/_plan/260905_now_split_train/320_combos_types.md b/devlog/_plan/260905_now_split_train/320_combos_types.md new file mode 100644 index 0000000000..20b11b44cc --- /dev/null +++ b/devlog/_plan/260905_now_split_train/320_combos_types.md @@ -0,0 +1,193 @@ +# 320 — S11 L1/5: src/combos/types.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Planning class: C3, bounded docs-only delegation; auth/provenance implementation retains C4 security care where noted below. +- Non-goals: Do not change native-alias admission, validation order/messages, whitespace normalization, default effort, target weights, or the selector persisted for a native alias. +- Goal: Move the namespace/identifier/selector primitives into one dependency-only leaf; leave alias/schema validation and normalized config construction at the existing boundary. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated in Verification below (the 000 reference to 003 is stale; 002 is authoritative). +- Stop: this delegated turn stops after writing and statically checking this plan; no source edits, tests, git mutations, orchestration, loop or goal commands. The later executor stops on any changed behavior, missing binding, cycle, oversized leaf, failing guard or basis drift. Layer execution ends only at an open PR with recorded green exact-head CI; never merge. +- Escalation: send any extra file/layer requirement or boundary change to the parent. Do not expand this layer into adjacent cleanup or add an unplanned #b. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source line references below are to that source snapshot. `git diff --numstat origin/dev -- src/combos/types.ts` is empty. Lane audit: `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:716`. No implementation proof is claimed here. + +## Symbol inventory + +Every top-level declaration is listed, including private declarations and import bindings. Inclusive start–end spans were extracted with `sg run --lang ts --kind --json=compact src/combos/types.ts` and checked against `git show origin/dev:src/combos/types.ts` with numbered lines. Nested declarations are intentionally not top-level rows. + +Consumers = unique **direct importing/re-exporting files**, not identifier occurrences or callers inside this module. Start from `rg -l -F 'types' src gui/src scripts tests`, inspect import/re-export clauses, resolve each relative specifier to this exact file, then intersect each named binding with `rg -l -w '' src gui/src scripts tests`. Private declarations have zero external consumers; same-spelling symbols elsewhere are not consumers. Type-only imports count. Imported bindings themselves are local, not exports. Baseline: 19 direct files; test-only leaf imports for new identity assertions do not replace any original import. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `isCodexReasoningEffort` | import binding(s) | 1–1 | no | 0 (local imports) | residual | +| `SUPPORTED_NATIVE_OPENAI_SLUGS` | import binding(s) | 2–2 | no | 0 (local imports) | residual; identifiers.ts | +| `OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxConfig, OcxProviderConfig` | import binding(s) | 3–11 | no | 0 (local imports) | residual; identifiers.ts (only its three needed types) | +| `COMBO_NAMESPACE` | const | 13–13 | yes | 3 | `src/combos/identifiers.ts` | +| `preservesPhysicalComboProvider` | function | 15–20 | yes | 1 | `src/combos/identifiers.ts` | +| `COMBO_ID_PATTERN` | const | 22–22 | no | 0 | `src/combos/identifiers.ts` | +| `COMBO_ALIAS_PATTERN` | const | 28–28 | no | 0 | `src/combos/types.ts (residual)` | +| `NATIVE_OPENAI_FAMILY_PATTERN` | const | 30–30 | no | 0 | `src/combos/types.ts (residual)` | +| `ComboValidationIssue` | interface | 32–35 | yes | 0 | `src/combos/types.ts (residual)` | +| `NormalizedComboConfig` | interface | 37–52 | yes | 10 | `src/combos/types.ts (residual)` | +| `isNativeAliasCombo` | function | 55–61 | yes | 1 | `src/combos/identifiers.ts` | +| `targetKey` | function | 63–65 | yes | 3 | `src/combos/identifiers.ts` | +| `parseComboModelId` | function | 67–72 | yes | 1 | `src/combos/identifiers.ts` | +| `comboModelId` | function | 74–76 | yes | 3 | `src/combos/identifiers.ts` | +| `comboPublicModelId` | function | 79–82 | yes | 4 | `src/combos/identifiers.ts` | +| `comboDisabledModelId` | function | 88–93 | yes | 1 | `src/combos/identifiers.ts` | +| `comboDisabledModelSelectors` | function | 96–103 | yes | 1 | `src/combos/identifiers.ts` | +| `resolveComboId` | function | 109–123 | yes | 3 | `src/combos/identifiers.ts` | +| `comboAliasIssues` | function | 129–168 | yes | 1 | `src/combos/types.ts (residual)` | +| `ComboValidationOptions` | interface | 170–176 | yes | 0 | `src/combos/types.ts (residual)` | +| `comboConfigIssues` | function | 178–353 | yes | 2 | `src/combos/types.ts (residual)` | +| `comboConfigError` | function | 355–362 | yes | 1 | `src/combos/types.ts (residual)` | +| `normalizeComboConfig` | function | 364–382 | yes | 1 | `src/combos/types.ts (residual)` | +| `comboDefaultEffort` | function | 384–394 | yes | 1 | `src/combos/types.ts (residual)` | +| `isValidComboId` | function | 396–398 | yes | 1 | `src/combos/identifiers.ts` | +| `listComboIds` | function | 400–402 | yes | 1 | `src/combos/types.ts (residual)` | +| `listLiveComboTargetKeys` | function | 404–414 | yes | 1 | `src/combos/types.ts (residual)` | +| `getCombo` | function | 416–423 | yes | 2 | `src/combos/types.ts (residual)` | + +## Leaf partition + +Structural decision: The 423-line boundary mixes identifiers with schema issue collection. Reject deleting or configuring away live exports, and reject moving only comboConfigIssues: it calls isValidComboId, targetKey and comboAliasIssues, so that move alone would create a reverse import. Choose one identifiers leaf, preserving src/combos/types.ts and the existing src/combos/index.ts facade. Existing sibling names src/combos/request.ts and src/combos/resolve.ts support the short concern name identifiers.ts. Blast radius: local combo feature, with unchanged callers in config, catalog, routing and management. + +Pre-change/intended map: Current: src/config.ts:50, src/combos/resolve.ts:6 and src/combos/index.ts:1 → types.ts → ../reasoning-effort, ../codex/catalog/native-models, ../types. Intended: the same callers → types.ts → identifiers.ts → native-models / shared types; types.ts retains reasoning-effort and its native-slug check. identifiers.ts never imports ./types or ./index. Existing index → types → identifiers is a compatibility path, not a new convenience barrel. + +The 001 note's basename-only fanin 946 is not a usable importer count for types.ts. Path-resolved rg candidates identify 19 direct importer/re-exporter files (20 statements), not every unrelated types module. Count downstream index.ts users separately; do not migrate them. + +### `src/combos/identifiers.ts` — 89 expected lines + +Move source bands `src/combos/types.ts:13`–22, `src/combos/types.ts:54`–124, `src/combos/types.ts:396`–399 (85 physical lines including existing inter-declaration comments/blanks). Symbols: `COMBO_NAMESPACE`, `preservesPhysicalComboProvider`, `COMBO_ID_PATTERN`, `isNativeAliasCombo`, `targetKey`, `parseComboModelId`, `comboModelId`, `comboPublicModelId`, `comboDisabledModelId`, `comboDisabledModelSelectors`, `resolveComboId`, `isValidComboId`. + +Keep existing exported declarations exported. All other private declarations stay private. + +Own imports (complete): + +```ts +import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models"; +import type { OcxComboConfig, OcxComboTarget, OcxConfig } from "../types"; +``` + +### Residual `src/combos/types.ts` — 332 expected lines + +Keep these declarations: `COMBO_ALIAS_PATTERN`, `NATIVE_OPENAI_FAMILY_PATTERN`, `ComboValidationIssue`, `NormalizedComboConfig`, `comboAliasIssues`, `ComboValidationOptions`, `comboConfigIssues`, `comboConfigError`, `normalizeComboConfig`, `comboDefaultEffort`, `listComboIds`, `listLiveComboTargetKeys`, `getCombo`. + +Accounting: 423 original − 85 moved − 12 replaced import/header lines + 4 explicit import lines + 1 named re-export lines + 1 separator = **332**. Each leaf estimate is its source-band count + own import lines + two header/separator lines. These are physical-line estimates using the compact exact import blocks below, not a claim of measured implementation output. Preserve comments, allow readable multiline imports, and remeasure after formatting; no file may exceed 400. No residual >400 and no #b required by file length. No #a/#b/#c parts are added in this five-layer map. Original function bodies over 50 lines remain unchanged as an explicit pure-move exception; splitting their logic is out of scope. + +## Re-export block + +Insert at the existing feature boundary, using named re-exports only. This is preservation of an established path, not a new internal index barrel. Re-exports create no local bindings. + +```ts +export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers"; +``` + +Retain these current exports as declarations in the original file (not copies): `ComboValidationIssue`, `NormalizedComboConfig`, `comboAliasIssues`, `ComboValidationOptions`, `comboConfigIssues`, `comboConfigError`, `normalizeComboConfig`, `comboDefaultEffort`, `listComboIds`, `listLiveComboTargetKeys`, `getCombo`. Together with the block above this preserves the complete old type/value export set; leaf-private API is not added to the facade. + +Explicit residual imports (replace the old import block): + +```ts +import { isCodexReasoningEffort } from "../reasoning-effort"; +import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models"; +import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types"; +import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers"; +``` + +## Module-level state and cycles + +No top-level let, Map, Set, WeakMap, lock or flight is created here. COMBO_ID_PATTERN (22) moves to identifiers.ts; COMBO_ALIAS_PATTERN (28) and NATIVE_OPENAI_FAMILY_PATTERN (30) stay in the original. They are non-global regular expressions, not mutable shared cursors. COMBO_NAMESPACE (13) has one leaf definition. SUPPORTED_NATIVE_OPENAI_SLUGS is imported at line 2; its single owner remains src/codex/catalog/native-models.ts, even though both facade and leaf import it. The Set at line 302 and the Set in listLiveComboTargetKeys at 407 are invocation-local and stay with their functions. The tempting leaf → ./types cycle is avoided by importing Ocx types directly from ../types and moving all identifier-to-identifier callees together. + +Lane 013 reported no static return-path cycle for this source. This plan's new local graph is acyclic by the dependency direction above; this is not a substitute for the executor's fresh whole-relative-graph return-path scan. Include type-only imports/re-exports, not merely runtime imports. New edges are Functional/Sequential coupling, not shared mutable Common state; preserve existing invocation ordering rather than adding locks or global owners. No leaf imports `./types` or any facade that routes back into itself. No lazy import workaround. + +## Tests + +Direct importer list, reproduced by `rg -l -F 'src/combos/types' tests` (all **unchanged**, including import path and existing assertions): + +- `tests/codex-integration/codex-catalog.test.ts` — unchanged. + +Text-oracle inventory: **none found** for this exact source path. Inspected basename/path matches and segmented `repoPath` forms for `readFileSync`, `Bun.file` and source-reader helpers, consistent with lane 013. There is therefore no source-read line to retarget and no explicit scan-list entry to add. A basename occurrence in `tests/fixtures/test-layout-expected.json` is test registration, not a source read. Generic recursive import-graph coverage is unchanged and discovers imports naturally. If implementation finds a computed/path-list source oracle not captured here, stop and extend the inventory with its exact read line before moving code; do not weaken it. + +Broader unchanged behavior coverage through src/combos/index.ts: tests/codex-integration/combos.test.ts, tests/routing/combo-management-api.test.ts and tests/providers/provider-id-rewrite.test.ts. Preserve tests/codex-integration/combos.test.ts:179 (model ID spelling), :193 (native disable selectors), :205 (canonical-before-alias precedence), :869 (alias validation), :916 (ordered issue rows), and :1030 (physical combo provider). Add a moved-export identity assertion to tests/codex-integration/combos.test.ts: reuse the existing ../../src/combos public entry (add a namespace import there if needed) and import the leaf via ../../src/combos/identifiers and compare all 11 moved public values with toBe. Drive that guard red once using a temporary wrapper for comboModelId at the facade, restore the named re-export, then prove green. Do not make the behavioral tests bypass the facade. + +These red-once mutations are future disposable-worktree verification steps, never persistent changes. They were not performed during drafting. Extend existing test files only; no new test file or test-layout entry is planned. `tests/lab/core-lab-boundary.test.ts` PROTECTED roots are never edited. + +## Verification + +Future implementation commands only; **none run in this docs-only task**. Execute against this layer's own tip, domains **codex-integration, routing, providers**, not the eventual stack top. + +```sh +bun run typecheck +bun test tests/codex-integration/codex-catalog.test.ts tests/codex-integration/combos.test.ts tests/routing/combo-management-api.test.ts tests/providers/provider-id-rewrite.test.ts +bun run privacy:scan +wc -l src/combos/identifiers.ts src/combos/types.ts +rg -l -F 'combos/types' src gui/src scripts tests +# Resolve relative import/re-export paths and compare the original consumer file set. +# Full suite: lidge only, no local full-suite invocation; keep the full exit status/log. +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-combos-types && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test' +``` + +For the 002 importer gate, the expected **existing** direct consumer set is 19: `src/codex/account-namespaces.ts`, `src/codex/catalog/aggregation.ts`, `src/codex/catalog/bundled.ts`, `src/codex/catalog/effort.ts`, `src/codex/catalog/metadata.ts`, `src/codex/catalog/parsing.ts`, `src/codex/catalog/provider-fetch.ts`, `src/codex/catalog/sync.ts`, `src/combos/failover.ts`, `src/combos/index.ts`, `src/combos/request.ts`, `src/combos/resolve.ts`, `src/config.ts`, `src/lib/state-store-registrations.ts`, `src/router.ts`, `src/server/effort-row.ts`, `src/server/fast-row.ts`, `src/server/management/model-routes.ts`, `tests/codex-integration/codex-catalog.test.ts`. The rg line above is a candidate list, not the count: same-directory imports and aliases require the path resolution described in Symbol inventory. Compare file sets, not statement counts; added leaf imports in identity tests are intentional. No original consumer migrates away from this boundary. Typecheck must still resolve every old export. + +Cycle verification: repeat lane 013 SG-GRAPH using `sg run --lang ts --kind import_statement --json=compact src` and `sg run --lang ts --kind export_statement --json=compact src`; resolve relative .ts/.tsx/index targets, include type edges, and search for a return path to the original or any new leaf. Require no new return path; record the scoped graph result. Do not install a new dependency tool for this layer. + +The 002 conditional Lab gate is not triggered by these planned source paths (none is src/server, src/router.ts or src/lib). If the implementation touches one of those paths, that is an expansion requiring parent approval and `bun test tests/lab/core-lab-boundary.test.ts`; keep PROTECTED unchanged. All new leaves must stay free of a transitive Lab dependency regardless. + +Record red then green for the guard named in Tests, typecheck exit 0, focused tests 0 failures, privacy scan exit 0, actual per-file line counts, full-suite exit 0 on lidge, the exact tested SHA and CI rollup. The remote worktree is parent-coordinated; confirm ownership before checkout and require its tested SHA to equal the PR head. Do not mask test exit status with an unguarded tail pipeline. Revalidate after any cascade. + +## Accept criteria + +1. Source still matches the stated basis or the plan is refreshed for every changed symbol before extraction. The actual source diff remains at most 500 added-plus-deleted lines; otherwise escalate before publication. +2. Every inventory declaration has exactly one owner; all function bodies/signatures and constant/type definitions are moved verbatim, apart from the necessary export modifiers and import paths. No public export is renamed, deleted, wrapped or newly invented. +3. Every current export remains importable from `src/combos/types.ts`; moved values pass identity guards where applicable, and residual references are satisfied by real imports, not a re-export-only assumption. +4. Actual 1 new leaves and the residual are each ≤400 physical lines. Record counts rather than relying on these estimates. No hidden #b or unplanned source file is required. +5. Native aliases still use canonical disable selectors; alias validation keeps its exact issue order; listLiveComboTargetKeys still sees the same normalized targets. +6. State/constant ownership matches this plan; fresh relative-import graph reports no new cycle, including type-only edges, and no new Lab reachability. +7. Existing tests/imports/source guards are retained without weakening; the specified guard is demonstrated red once and restored green. All instantiated 002 gates and exact-head CI are green with recorded evidence. +8. PR uses the template, correct base and complete five-layer map. No merge, release, deployment, dependency installation on the user's running service, or unrelated code change is included. + +## PR + +Title: `refactor(combos): isolate combo identifiers from validation (split S11 L1/5)` + +Branch: `codex/split-combos-types`. Base: `dev`. Closes: **none**. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. Put the measured move size and any parent-approved exception in Summary, evidence tied to this PR head in Verification, and include the stack map below. Review this layer's diff only. PR numbers are intentionally unassigned planning placeholders, not existing PR claims. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S11-L1 | **L1 — this layer** | `codex/split-combos-types` | `dev` | isolate combo identifiers from validation | +| 2 | #TBD-S11-L2 | L2 | `codex/split-codex-subagent-defaults` | `dev` | isolate format-preserving subagent TOML lexing | +| 3 | #TBD-S11-L3 | L3 | `codex/split-codex-cli-install-provenance` | `dev` | separate install evidence from classification | +| 4 | #TBD-S11-L4 | L4 | `codex/split-routing-trace` | `dev` | separate trace contracts and evidence codecs | +| 5 | #TBD-S11-L5 | L5 | `codex/split-oauth-github-copilot` | `dev` | isolate GitHub device grant transport | + +Base: dev — no dependency on the layers below; no cascade obligation. + +DEV-STACK-04: merges remain separately authorized; this task performs none. + +## P stale-check (2026-09-05, wp320) + +origin/dev 3c920af5f; combos/types.ts unchanged since 445742966 (423 lines); anchors 13/22/54/124/396/399 confirmed by sed. Base `dev` (S11 independent). The plan already names the CI-hygiene test change (identity assertion in tests/codex-integration/combos.test.ts). Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1. + +## A amendment (Darwin audit, GO-WITH-FIXES blockers=2 → folded) + +1. Cycle gate: the executor/reviewer graph walk must include inline `import("…")` type edges (src/types/provider.ts:695/:701 at HEAD) in addition to import/export statements; compare against base — pre-existing type cycles (types → provider → mcp-config → types; types → provider → native-exec-desktop → … → tool-definitions → types) are unchanged and permitted (003 TYPE-CYCLE-01). The new leaf must not join any. +2. Size gate: acceptance criterion 1 (raw ≤500) is void; 003 PURE-MOVE-SIZE-01 binds (85 relocated lines; ≤150 non-move; audit measured ~22 before test edits). +3. Test anchors at base: alias validation starts at combos.test.ts:873 (not 869); ordered rows at :920. +Audit-verified: 28/28 inventory rows; leaf = exactly 12 declarations (11 public + COMBO_ID_PATTERN) with 2 minimal imports; residual imports exact (comboModelId/parseComboModelId/resolveComboId unused by residual); 22 exports (19 values + 3 interfaces), combos/index.ts uses named re-exports; 19 direct importer files / 20 statements; only codex-catalog.test.ts imports src/combos/types directly (type-only). + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-320.whwuzf/wt` (branch `codex/split-combos-types`, base origin/dev 3c920af5f). Executor: gpt-6-astra high (Nietzsche, 01a06f8a-a1ff-71f3-9510-256f8d2dc3b2). +- Commits: 093c4efd6 (move: identifiers.ts 90, types.ts 333), aa695d933 (test: combos.test.ts +25 — 11-value identity facade vs leaf; leaf has no ./types or ./index import), 0c914bf26 (main agent: trimmed the EOF blank → identifiers.ts 89). Diff: 3 files. +- Local gate: typecheck 0; focused (combos, codex-catalog, combo-management-api, provider-id-rewrite) 372 pass / 0 fail; core-lab-boundary 17/0; privacy passed; 19 direct importers unchanged. +- Cycle gate (executor script incl. inline import() type edges): 18 files / 31 relative + 2 inline edges walked; no path returns to types.ts, index.ts or the leaf; 3 pre-existing type cycles unchanged (TYPE-CYCLE-01). +- Red-drives: (a) facade wrapper for comboModelId → identity test :1223 fails, restored; (b) comboDisabledModelSelectors broken → combos.test.ts:203 (test :193) fails, restored. + +- Adversarial diff review (Avicenna, gpt-6-astra high, 01a06f8e-9394-7dd1-8738-3bf5a1227f77): VERDICT: PASS (slices exact, residual exact, 22/22 exports, index.ts 18 names intact, walk incl. inline type imports base 19/34 → HEAD 20/38 with no new cycle, test non-tautological). +- lidge full suite at 0c914bf26: SUITE_EXIT=0, 18067 pass / 0 fail / 16 skip (/tmp/suite-split-320.log). +- PR: https://github.com/lidge-jun/opencodex/pull/3594 (base dev, head 0c914bf26). CI rollup at record time: OPEN draft=false 0c914bf26 =1 =9 SKIPPED=2 SUCCESS=17 diff --git a/devlog/_plan/260905_now_split_train/330_codex_subagent_defaults.md b/devlog/_plan/260905_now_split_train/330_codex_subagent_defaults.md new file mode 100644 index 0000000000..79662e2069 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/330_codex_subagent_defaults.md @@ -0,0 +1,183 @@ +# 330 — S11 L2/5: src/codex/subagent-defaults.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Planning class: C3, bounded docs-only delegation; auth/provenance implementation retains C4 security care where noted below. +- Non-goals: No TOML parser replacement, reserialization, generic utility reuse, marker changes, overwrite-policy changes or function-body cleanup. Keep comments, unknown keys, CRLF/LF choice and original bytes on rejection. +- Goal: Extract physical-line scanning, TOML key decoding and scalar string encoding into a pure source leaf while retaining managed-ownership analysis and the transform at the original path. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated in Verification below (the 000 reference to 003 is stale; 002 is authoritative). +- Stop: this delegated turn stops after writing and statically checking this plan; no source edits, tests, git mutations, orchestration, loop or goal commands. The later executor stops on any changed behavior, missing binding, cycle, oversized leaf, failing guard or basis drift. Layer execution ends only at an open PR with recorded green exact-head CI; never merge. +- Escalation: send any extra file/layer requirement or boundary change to the parent. Do not expand this layer into adjacent cleanup or add an unplanned #b. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source line references below are to that source snapshot. `git diff --numstat origin/dev -- src/codex/subagent-defaults.ts` is empty. Lane audit: `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:546`. No implementation proof is claimed here. + +## Symbol inventory + +Every top-level declaration is listed, including private declarations and import bindings. Inclusive start–end spans were extracted with `sg run --lang ts --kind --json=compact src/codex/subagent-defaults.ts` and checked against `git show origin/dev:src/codex/subagent-defaults.ts` with numbered lines. Nested declarations are intentionally not top-level rows. + +Consumers = unique **direct importing/re-exporting files**, not identifier occurrences or callers inside this module. Start from `rg -l -F 'subagent-defaults' src gui/src scripts tests`, inspect import/re-export clauses, resolve each relative specifier to this exact file, then intersect each named binding with `rg -l -w '' src gui/src scripts tests`. Private declarations have zero external consumers; same-spelling symbols elsewhere are not consumers. Type-only imports count. Imported bindings themselves are local, not exports. Baseline: 6 direct files; test-only leaf imports for new identity assertions do not replace any original import. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `MANAGED_SUBAGENT_DEFAULT_MARKER` | const | 10–10 | yes | 5 | `src/codex/subagent-defaults.ts (residual)` | +| `MANAGED_AGENTS_TABLE_MARKER` | const | 11–11 | yes | 5 | `src/codex/subagent-defaults.ts (residual)` | +| `ManagedSubagentDefaultKey` | type alias | 13–15 | yes | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `ManagedSubagentDefaults` | interface | 17–20 | yes | 1 | `src/codex/subagent-defaults.ts (residual)` | +| `ManagedSubagentDefaultsConflict` | interface | 22–26 | yes | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `ManagedSubagentDefaultsTransformResult` | type alias | 28–41 | yes | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `SourceLine` | interface | 43–48 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `TargetDefinition` | interface | 50–54 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `TomlShape` | interface | 56–61 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `TARGET_KEYS` | const | 63–66 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `splitSourceLines` | function | 68–87 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `MultilineStringKind` | type alias | 89–89 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `markStructuralLines` | function | 96–170 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `joinSourceLines` | function | 172–174 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `dominantEol` | function | 176–184 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `decodeTomlBasicKey` | function | 187–209 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `canonicalKeySegment` | function | 211–215 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `KEY_SEGMENT` | const | 217–217 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `EXACT_TABLE_HEADER` | const | 218–218 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `ARRAY_TABLE_HEADER` | const | 219–219 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `DOTTED_TABLE_HEADER` | const | 220–220 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `KEY_ASSIGNMENT` | const | 221–221 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `DOTTED_ASSIGNMENT` | const | 222–222 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `ANY_TABLE_HEADER` | const | 223–223 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `exactAgentsHeader` | function | 225–229 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `arrayAgentsHeader` | function | 231–235 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `dottedAgentsHeader` | function | 237–242 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `isAnyTableHeader` | function | 244–246 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `assignmentKeyAt` | function | 248–252 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `markerLine` | function | 254–256 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `targetKeyAt` | function | 258–264 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `dottedAssignmentAt` | function | 266–274 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `dottedTargetAt` | function | 276–282 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `analyzeToml` | function | 284–366 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `quotedTomlString` | function | 368–373 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `containsLoneSurrogate` | function | 375–387 | no | 0 | `src/codex/subagent-defaults-source.ts` | +| `replaceManagedString` | function | 389–396 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `insertedLines` | function | 398–407 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `invalidInput` | function | 409–411 | no | 0 | `src/codex/subagent-defaults.ts (residual)` | +| `transformManagedSubagentDefaults` | function | 419–550 | yes | 2 | `src/codex/subagent-defaults.ts (residual)` | + +## Leaf partition + +Structural decision: The 550-line file has a standalone lexical layer. Reject a whole-function move of analyzeToml: it depends on ownership markers and target policy shared with the transform. Reject reusing similarly named dominantEol/canonicalKeySegment from unrelated injectors: their co-owned grammar is not this scanner's contract. Choose a zero-dependency subagent-defaults-source.ts sibling, following src/codex/prompt-text-probe.ts and other hyphenated concern siblings. Blast radius: Codex configuration feature; inject.ts remains the only production importer. + +Pre-change/intended map: Current: src/codex/inject.ts:75 and five tests → subagent-defaults.ts (no imports). Intended: those callers → subagent-defaults.ts → subagent-defaults-source.ts (no imports). TARGET_KEYS, ownership markers, TomlShape, TargetDefinition, analyzeToml and transformation stay together. SourceLine moves down and is type-imported upward; the lexical leaf never imports the facade or policy types. + +The leaf's eleven declarations are currently private and have zero external import consumers. Only eight become named leaf exports for production residual imports; markStructuralLines, MultilineStringKind and decodeTomlBasicKey remain leaf-private. All seven existing public declarations remain verbatim in the original file. + +### `src/codex/subagent-defaults-source.ts` — 180 expected lines + +Move source bands `src/codex/subagent-defaults.ts:43`–49, `src/codex/subagent-defaults.ts:68`–217, `src/codex/subagent-defaults.ts:368`–388 (178 physical lines including existing inter-declaration comments/blanks). Symbols: `SourceLine`, `splitSourceLines`, `MultilineStringKind`, `markStructuralLines`, `joinSourceLines`, `dominantEol`, `decodeTomlBasicKey`, `canonicalKeySegment`, `KEY_SEGMENT`, `quotedTomlString`, `containsLoneSurrogate`. + +Keep existing exported declarations exported. Add the `export` modifier (without changing a body/signature) only to these formerly private declarations needed by another production module: `SourceLine`, `splitSourceLines`, `joinSourceLines`, `dominantEol`, `canonicalKeySegment`, `KEY_SEGMENT`, `quotedTomlString`, `containsLoneSurrogate`. Every other private declaration stays private; none of the new internal exports is added to the facade. + +Own imports (complete): + +```ts +// None: this leaf has no imports. +``` + +### Residual `src/codex/subagent-defaults.ts` — 375 expected lines + +Keep these declarations: `MANAGED_SUBAGENT_DEFAULT_MARKER`, `MANAGED_AGENTS_TABLE_MARKER`, `ManagedSubagentDefaultKey`, `ManagedSubagentDefaults`, `ManagedSubagentDefaultsConflict`, `ManagedSubagentDefaultsTransformResult`, `TargetDefinition`, `TomlShape`, `TARGET_KEYS`, `EXACT_TABLE_HEADER`, `ARRAY_TABLE_HEADER`, `DOTTED_TABLE_HEADER`, `KEY_ASSIGNMENT`, `DOTTED_ASSIGNMENT`, `ANY_TABLE_HEADER`, `exactAgentsHeader`, `arrayAgentsHeader`, `dottedAgentsHeader`, `isAnyTableHeader`, `assignmentKeyAt`, `markerLine`, `targetKeyAt`, `dottedAssignmentAt`, `dottedTargetAt`, `analyzeToml`, `replaceManagedString`, `insertedLines`, `invalidInput`, `transformManagedSubagentDefaults`. + +Accounting: 550 original − 178 moved − 0 replaced import/header lines + 2 explicit import lines + 0 named re-export lines + 1 separator = **375**. Each leaf estimate is its source-band count + own import lines + two header/separator lines. These are physical-line estimates using the compact exact import blocks below, not a claim of measured implementation output. Preserve comments, allow readable multiline imports, and remeasure after formatting; no file may exceed 400. No residual >400 and no #b required by file length. No #a/#b/#c parts are added in this five-layer map. Original function bodies over 50 lines remain unchanged as an explicit pure-move exception; splitting their logic is out of scope. + +## Re-export block + +Insert at the existing feature boundary, using named re-exports only. This is preservation of an established path, not a new internal index barrel. Re-exports create no local bindings. + +```ts +// No public declaration moves in this layer; add no re-export statements. +``` + +Retain these current exports as declarations in the original file (not copies): `MANAGED_SUBAGENT_DEFAULT_MARKER`, `MANAGED_AGENTS_TABLE_MARKER`, `ManagedSubagentDefaultKey`, `ManagedSubagentDefaults`, `ManagedSubagentDefaultsConflict`, `ManagedSubagentDefaultsTransformResult`, `transformManagedSubagentDefaults`. Together with the block above this preserves the complete old type/value export set; leaf-private API is not added to the facade. + +Explicit residual imports (add alongside any unchanged original imports): + +```ts +import { splitSourceLines, joinSourceLines, dominantEol, canonicalKeySegment, KEY_SEGMENT, quotedTomlString, containsLoneSurrogate } from "./subagent-defaults-source"; +import type { SourceLine } from "./subagent-defaults-source"; +``` + +## Module-level state and cycles + +No module-level let, Map, Set, WeakMap, lock or flight. TARGET_KEYS at 63–66 stays in the residual as the sole readonly target-policy array. KEY_SEGMENT at 217 moves with canonicalKeySegment; regexes at 218–223 stay and import KEY_SEGMENT. Source scanner state multiline/squareDepth/curlyDepth (97–99) remains invocation-local. analyzeToml's definitions Map at 315 and transform's desired Map at 449 remain per invocation in the residual. No new cache or shared parser instance. Moving SourceLine and all scalar encoders together avoids even a type-only leaf → facade return edge. + +Lane 013 reported no static return-path cycle for this source. This plan's new local graph is acyclic by the dependency direction above; this is not a substitute for the executor's fresh whole-relative-graph return-path scan. Include type-only imports/re-exports, not merely runtime imports. New edges are Functional/Sequential coupling, not shared mutable Common state; preserve existing invocation ordering rather than adding locks or global owners. No leaf imports `./subagent-defaults` or any facade that routes back into itself. No lazy import workaround. + +## Tests + +Direct importer list, reproduced by `rg -l -F 'src/codex/subagent-defaults' tests` (all **unchanged**, including import path and existing assertions): + +- `tests/codex-integration/codex-inject-integration.test.ts` — unchanged. +- `tests/codex-integration/codex-inject.test.ts` — unchanged. +- `tests/codex-integration/codex-journal.test.ts` — unchanged. +- `tests/codex-integration/codex-sync-api.test.ts` — unchanged. +- `tests/routing/subagent-defaults.test.ts` — unchanged. + +Text-oracle inventory: **none found** for this exact source path. Inspected basename/path matches and segmented `repoPath` forms for `readFileSync`, `Bun.file` and source-reader helpers, consistent with lane 013. There is therefore no source-read line to retarget and no explicit scan-list entry to add. A basename occurrence in `tests/fixtures/test-layout-expected.json` is test registration, not a source read. Generic recursive import-graph coverage is unchanged and discovers imports naturally. If implementation finds a computed/path-list source oracle not captured here, stop and extend the inventory with its exact read line before moving code; do not weaken it. + +All existing test imports remain unchanged. In particular tests/routing/subagent-defaults.test.ts:39 must retain exact comment/sibling/table ordering, :61 CRLF preservation, :231 escaped-key recognition, :262 escaped table names, :275 nested arrays and :294 multiline arrays. No public exports move in this layer, so no vacuous facade identity test is added. The existing CRLF guard at :61 is the red-once guard for this extraction: temporarily make dominantEol return LF for a CRLF fixture in the leaf, observe that focused test fail, restore its original body, then verify green. Keep markStructuralLines and scalar Unicode tests reachable through transformManagedSubagentDefaults; do not export scanner internals through the public facade for tests. + +These red-once mutations are future disposable-worktree verification steps, never persistent changes. They were not performed during drafting. Extend existing test files only; no new test file or test-layout entry is planned. `tests/lab/core-lab-boundary.test.ts` PROTECTED roots are never edited. + +## Verification + +Future implementation commands only; **none run in this docs-only task**. Execute against this layer's own tip, domains **routing, codex-integration**, not the eventual stack top. + +```sh +bun run typecheck +bun test tests/routing/subagent-defaults.test.ts tests/codex-integration/codex-inject-integration.test.ts tests/codex-integration/codex-journal.test.ts tests/codex-integration/codex-sync-api.test.ts tests/codex-integration/codex-inject.test.ts +bun run privacy:scan +wc -l src/codex/subagent-defaults-source.ts src/codex/subagent-defaults.ts +rg -l -F 'codex/subagent-defaults' src gui/src scripts tests +# Resolve relative import/re-export paths and compare the original consumer file set. +# Full suite: lidge only, no local full-suite invocation; keep the full exit status/log. +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-subagent-defaults && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test' +``` + +For the 002 importer gate, the expected **existing** direct consumer set is 6: `src/codex/inject.ts`, `tests/codex-integration/codex-inject-integration.test.ts`, `tests/codex-integration/codex-inject.test.ts`, `tests/codex-integration/codex-journal.test.ts`, `tests/codex-integration/codex-sync-api.test.ts`, `tests/routing/subagent-defaults.test.ts`. The rg line above is a candidate list, not the count: same-directory imports and aliases require the path resolution described in Symbol inventory. Compare file sets, not statement counts; added leaf imports in identity tests are intentional. No original consumer migrates away from this boundary. Typecheck must still resolve every old export. + +Cycle verification: repeat lane 013 SG-GRAPH using `sg run --lang ts --kind import_statement --json=compact src` and `sg run --lang ts --kind export_statement --json=compact src`; resolve relative .ts/.tsx/index targets, include type edges, and search for a return path to the original or any new leaf. Require no new return path; record the scoped graph result. Do not install a new dependency tool for this layer. + +The 002 conditional Lab gate is not triggered by these planned source paths (none is src/server, src/router.ts or src/lib). If the implementation touches one of those paths, that is an expansion requiring parent approval and `bun test tests/lab/core-lab-boundary.test.ts`; keep PROTECTED unchanged. All new leaves must stay free of a transitive Lab dependency regardless. + +Record red then green for the guard named in Tests, typecheck exit 0, focused tests 0 failures, privacy scan exit 0, actual per-file line counts, full-suite exit 0 on lidge, the exact tested SHA and CI rollup. The remote worktree is parent-coordinated; confirm ownership before checkout and require its tested SHA to equal the PR head. Do not mask test exit status with an unguarded tail pipeline. Revalidate after any cascade. + +## Accept criteria + +1. Source still matches the stated basis or the plan is refreshed for every changed symbol before extraction. The actual source diff remains at most 500 added-plus-deleted lines; otherwise escalate before publication. +2. Every inventory declaration has exactly one owner; all function bodies/signatures and constant/type definitions are moved verbatim, apart from the necessary export modifiers and import paths. No public export is renamed, deleted, wrapped or newly invented. +3. Every current export remains importable from `src/codex/subagent-defaults.ts`; moved values pass identity guards where applicable, and residual references are satisfied by real imports, not a re-export-only assumption. +4. Actual 1 new leaves and the residual are each ≤400 physical lines. Record counts rather than relying on these estimates. No hidden #b or unplanned source file is required. +5. Unmarked values still produce the same conflicts; malformed/ambiguous input remains byte-for-byte unchanged; quoted keys, multiline strings, nested arrays and CRLF edits remain identical. +6. State/constant ownership matches this plan; fresh relative-import graph reports no new cycle, including type-only edges, and no new Lab reachability. +7. Existing tests/imports/source guards are retained without weakening; the specified guard is demonstrated red once and restored green. All instantiated 002 gates and exact-head CI are green with recorded evidence. +8. PR uses the template, correct base and complete five-layer map. No merge, release, deployment, dependency installation on the user's running service, or unrelated code change is included. + +## PR + +Title: `refactor(codex): isolate format-preserving subagent TOML lexing (split S11 L2/5)` + +Branch: `codex/split-codex-subagent-defaults`. Base: `dev`. Closes: **none**. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. Put the measured move size and any parent-approved exception in Summary, evidence tied to this PR head in Verification, and include the stack map below. Review this layer's diff only. PR numbers are intentionally unassigned planning placeholders, not existing PR claims. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S11-L1 | L1 | `codex/split-combos-types` | `dev` | isolate combo identifiers from validation | +| 2 | #TBD-S11-L2 | **L2 — this layer** | `codex/split-codex-subagent-defaults` | `dev` | isolate format-preserving subagent TOML lexing | +| 3 | #TBD-S11-L3 | L3 | `codex/split-codex-cli-install-provenance` | `dev` | separate install evidence from classification | +| 4 | #TBD-S11-L4 | L4 | `codex/split-routing-trace` | `dev` | separate trace contracts and evidence codecs | +| 5 | #TBD-S11-L5 | L5 | `codex/split-oauth-github-copilot` | `dev` | isolate GitHub device grant transport | + +Base: dev — no dependency on the layers below; no cascade obligation. + +DEV-STACK-04: merges remain separately authorized; this task performs none. diff --git a/devlog/_plan/260905_now_split_train/340_codex_cli_install_provenance.md b/devlog/_plan/260905_now_split_train/340_codex_cli_install_provenance.md new file mode 100644 index 0000000000..b4a23a28f3 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/340_codex_cli_install_provenance.md @@ -0,0 +1,230 @@ +# 340 — S11 L3/5: src/codex/cli-install-provenance.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Planning class: C3, bounded docs-only delegation; auth/provenance implementation retains C4 security care where noted below. +- Non-goals: No updater policy changes, real process probing, mutation, new Windows inspection, altered file-read flags/bounds, changed report fields, changed digest domains or changes to the dependency-injection API. +- Goal: Split dependency contracts, path ownership observations and bounded manifest reads from the public install-classification coordinator. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated in Verification below (the 000 reference to 003 is stale; 002 is authoritative). +- Stop: this delegated turn stops after writing and statically checking this plan; no source edits, tests, git mutations, orchestration, loop or goal commands. The later executor stops on any changed behavior, missing binding, cycle, oversized leaf, failing guard or basis drift. Layer execution ends only at an open PR with recorded green exact-head CI; never merge. +- Escalation: send any extra file/layer requirement or boundary change to the parent. Execution also requires the parent to resolve the 500-line diff-size contradiction below. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source line references below are to that source snapshot. `git diff --numstat origin/dev -- src/codex/cli-install-provenance.ts` is empty. Lane audit: `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:335`. No implementation proof is claimed here. + +## Symbol inventory + +Every top-level declaration is listed, including private declarations and import bindings. Inclusive start–end spans were extracted with `sg run --lang ts --kind --json=compact src/codex/cli-install-provenance.ts` and checked against `git show origin/dev:src/codex/cli-install-provenance.ts` with numbered lines. Nested declarations are intentionally not top-level rows. + +Consumers = unique **direct importing/re-exporting files**, not identifier occurrences or callers inside this module. Start from `rg -l -F 'cli-install-provenance' src gui/src scripts tests`, inspect import/re-export clauses, resolve each relative specifier to this exact file, then intersect each named binding with `rg -l -w '' src gui/src scripts tests`. Private declarations have zero external consumers; same-spelling symbols elsewhere are not consumers. Type-only imports count. Imported bindings themselves are local, not exports. Baseline: 3 direct files; test-only leaf imports for new identity assertions do not replace any original import. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `createHash` | import binding(s) | 1–1 | no | 0 (local imports) | cli-install-provenance-files.ts | +| `closeSync, fsConstants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync` | import binding(s) | 2–12 | no | 0 (local imports) | files/paths/types leaves; residual statSync | +| `posix, win32` | import binding(s) | 13–13 | no | 0 (local imports) | paths leaf; residual win32 | +| `getConfigDir` | import binding(s) | 14–14 | no | 0 (local imports) | residual | +| `parseStrictSemver` | import binding(s) | 15–15 | no | 0 (local imports) | files leaf | +| `CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS` | import binding(s) | 16–16 | no | 0 (local imports) | paths leaf | +| `isSpawnableCodexCandidate` | import binding(s) | 17–17 | no | 0 (local imports) | paths leaf | +| `codexRuntimeStatePath, parsePersistedCodexRuntime` | import binding(s) | 18–21 | no | 0 (local imports) | residual | +| `inspectCodexShimBackingForCommand, isLocalAbsoluteInspectionPath, isVersionManagerOwnedCodexPath, CodexShimBackingForCommand` | import binding(s) | 22–27 | no | 0 (local imports) | residual/paths/types (exact imports below) | +| `CODEX_PACKAGE` | const | 29–29 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `MAX_MANIFEST_BYTES` | const | 30–30 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `MAX_RUNTIME_STATE_BYTES` | const | 31–31 | no | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `MAX_MANIFEST_ANCESTORS` | const | 32–32 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `CodexCliInstallKind` | type alias | 34–39 | yes | 0 | `src/codex/cli-install-provenance-types.ts` | +| `CodexCliInstallReason` | type alias | 41–54 | yes | 0 | `src/codex/cli-install-provenance-types.ts` | +| `CodexCliCandidateSource` | type alias | 56–56 | yes | 0 | `src/codex/cli-install-provenance-types.ts` | +| `CodexCliInstallEvidence` | type alias | 57–64 | yes | 0 | `src/codex/cli-install-provenance-types.ts` | +| `ReadOnlyCodexRuntimeCandidate` | interface | 66–70 | yes | 0 | `src/codex/cli-install-provenance-types.ts` | +| `CodexCliInstallReport` | interface | 72–91 | yes | 2 | `src/codex/cli-install-provenance-types.ts` | +| `CodexCliInstallProvenanceDeps` | interface | 93–108 | yes | 3 | `src/codex/cli-install-provenance-types.ts` | +| `PackageManifestEvidence` | interface | 110–116 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `sha256` | function | 118–124 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `validatedVersion` | function | 126–129 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `publicExecutableLocation` | function | 131–136 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `freezeReport` | function | 138–142 | no | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `unknownReport` | function | 144–166 | no | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `unknownWindowsReport` | function | 168–177 | no | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `readPersistedCandidate` | function | 179–205 | no | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `observeCodexRuntimeCandidateReadOnly` | function | 211–224 | yes | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `pathTools` | function | 226–228 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `isWindowsPlatform` | function | 230–232 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `isSafeLocalInspectionPath` | function | 234–240 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `caseInsensitiveEnv` | function | 242–245 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `resolveCandidateCommandPath` | function | 247–288 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `canonicalize` | function | 290–298 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `normalizePath` | function | 300–306 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `samePath` | function | 308–310 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `isAppBundledCodexPath` | function | 312–321 | yes | 1 | `src/codex/cli-install-provenance-paths.ts` | +| `isCodexCliUpdateVersionManagerPath` | function | 324–348 | yes | 1 | `src/codex/cli-install-provenance-paths.ts` | +| `configuredVersionManagerRoots` | function | 350–369 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `isWithinConfiguredVersionManagerRoot` | function | 371–377 | no | 0 | `src/codex/cli-install-provenance-paths.ts` | +| `readBoundedFile` | function | 379–459 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `manifestCandidates` | function | 461–484 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `manifestBinPath` | function | 486–495 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `findCodexPackageManifest` | function | 497–527 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `launcherIsLinkedToManifest` | function | 529–543 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `isProvenGlobalNpmLayout` | function | 545–571 | no | 0 | `src/codex/cli-install-provenance-files.ts` | +| `shimReport` | function | 573–581 | no | 0 | `src/codex/cli-install-provenance.ts (residual)` | +| `inspectCodexCliInstall` | function | 584–795 | yes | 2 | `src/codex/cli-install-provenance.ts (residual)` | + +## Leaf partition + +Structural decision: The 795-line file combines bounded IO, path policy and report classification. Reject extracting readBoundedFile alone: it would not bring the residual below 400, and a files leaf importing facade-owned path helpers would create a cycle. Choose three same-directory concern siblings, following src/codex/history-manifest.ts and src/codex/history-provider.ts. Move the existing dependency/report contracts first, path helpers second, then the file evidence cluster; the outer coordinator remains original. Blast radius: Codex CLI update feature. This is C4-care during later implementation because it moves filesystem/provenance checks; explicit security review under MAINTAINERS.md:60 is still required. + +Pre-change/intended map: Current: src/cli/codex-cli-update.ts:1 and two test files → cli-install-provenance.ts → config, strict-semver, update launch policy, exec-invocation, runtime and shim. Intended local order: facade → files → paths → types; facade also imports paths/types directly. types uses only type imports from node:fs and ./shim. files owns the manifest digest/read boundary; paths owns path canonicalization and version-manager classification; runtime observation and report assembly stay in facade. None of these leaves imports cli-install-provenance.ts. + +Sizing escalation: 454 existing physical lines move, so the source additions-plus-deletions lower bound is 908 before import rewiring and guards. That cannot satisfy a literal 500-line changed-source cap in 002 while preserving this five-layer S11 map. The parent must explicitly accept a pure-move size exception or revise 002 with extra parts/stacks before implementation. This document is a complete proposed partition, not a claim that the cap is met. No #b layer is silently invented; the planned single-layer residual is already below 400. + +### `src/codex/cli-install-provenance-types.ts` — 79 expected lines + +Move source bands `src/codex/cli-install-provenance.ts:34`–108 (75 physical lines including existing inter-declaration comments/blanks). Symbols: `CodexCliInstallKind`, `CodexCliInstallReason`, `CodexCliCandidateSource`, `CodexCliInstallEvidence`, `ReadOnlyCodexRuntimeCandidate`, `CodexCliInstallReport`, `CodexCliInstallProvenanceDeps`. + +Keep existing exported declarations exported. All other private declarations stay private. + +Own imports (complete): + +```ts +import type { lstatSync, statSync } from "node:fs"; +import type { CodexShimBackingForCommand } from "./shim"; +``` + +### `src/codex/cli-install-provenance-paths.ts` — 168 expected lines + +Move source bands `src/codex/cli-install-provenance.ts:131`–137, `src/codex/cli-install-provenance.ts:226`–378 (160 physical lines including existing inter-declaration comments/blanks). Symbols: `publicExecutableLocation`, `pathTools`, `isWindowsPlatform`, `isSafeLocalInspectionPath`, `caseInsensitiveEnv`, `resolveCandidateCommandPath`, `canonicalize`, `normalizePath`, `samePath`, `isAppBundledCodexPath`, `isCodexCliUpdateVersionManagerPath`, `configuredVersionManagerRoots`, `isWithinConfiguredVersionManagerRoot`. + +Keep existing exported declarations exported. Add the `export` modifier (without changing a body/signature) only to these formerly private declarations needed by another production module: `publicExecutableLocation`, `pathTools`, `isWindowsPlatform`, `isSafeLocalInspectionPath`, `resolveCandidateCommandPath`, `canonicalize`, `normalizePath`, `samePath`, `configuredVersionManagerRoots`, `isWithinConfiguredVersionManagerRoot`. Every other private declaration stays private; none of the new internal exports is added to the facade. + +Own imports (complete): + +```ts +import { existsSync, lstatSync, statSync, realpathSync } from "node:fs"; +import { posix, win32 } from "node:path"; +import { CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS } from "../update/codex-cli-update-launch-policy.mjs"; +import { isSpawnableCodexCandidate } from "./exec-invocation"; +import { isLocalAbsoluteInspectionPath, isVersionManagerOwnedCodexPath } from "./shim"; +import type { CodexCliInstallProvenanceDeps } from "./cli-install-provenance-types"; +``` + +### `src/codex/cli-install-provenance-files.ts` — 226 expected lines + +Move source bands `src/codex/cli-install-provenance.ts:29`–30, `src/codex/cli-install-provenance.ts:32`–33, `src/codex/cli-install-provenance.ts:110`–130, `src/codex/cli-install-provenance.ts:379`–572 (219 physical lines including existing inter-declaration comments/blanks). Symbols: `CODEX_PACKAGE`, `MAX_MANIFEST_BYTES`, `MAX_MANIFEST_ANCESTORS`, `PackageManifestEvidence`, `sha256`, `validatedVersion`, `readBoundedFile`, `manifestCandidates`, `manifestBinPath`, `findCodexPackageManifest`, `launcherIsLinkedToManifest`, `isProvenGlobalNpmLayout`. + +Keep existing exported declarations exported. Add the `export` modifier (without changing a body/signature) only to these formerly private declarations needed by another production module: `validatedVersion`, `readBoundedFile`, `findCodexPackageManifest`, `launcherIsLinkedToManifest`, `isProvenGlobalNpmLayout`. Every other private declaration stays private; none of the new internal exports is added to the facade. + +Own imports (complete): + +```ts +import { createHash } from "node:crypto"; +import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readSync } from "node:fs"; +import { parseStrictSemver } from "../lib/strict-semver"; +import type { CodexCliInstallProvenanceDeps } from "./cli-install-provenance-types"; +import { pathTools, isSafeLocalInspectionPath, canonicalize, normalizePath, samePath } from "./cli-install-provenance-paths"; +``` + +### Residual `src/codex/cli-install-provenance.ts` — 324 expected lines + +Keep these declarations: `MAX_RUNTIME_STATE_BYTES`, `freezeReport`, `unknownReport`, `unknownWindowsReport`, `readPersistedCandidate`, `observeCodexRuntimeCandidateReadOnly`, `shimReport`, `inspectCodexCliInstall`. + +Accounting: 795 original − 454 moved − 28 replaced import/header lines + 8 explicit import lines + 2 named re-export lines + 1 separator = **324**. Each leaf estimate is its source-band count + own import lines + two header/separator lines. These are physical-line estimates using the compact exact import blocks below, not a claim of measured implementation output. Preserve comments, allow readable multiline imports, and remeasure after formatting; no file may exceed 400. No residual >400 and no #b required by file length. No #a/#b/#c parts are added in this five-layer map. Original function bodies over 50 lines remain unchanged as an explicit pure-move exception; splitting their logic is out of scope. + +## Re-export block + +Insert at the existing feature boundary, using named re-exports only. This is preservation of an established path, not a new internal index barrel. Re-exports create no local bindings. + +```ts +export type { CodexCliInstallKind, CodexCliInstallReason, CodexCliCandidateSource, CodexCliInstallEvidence, ReadOnlyCodexRuntimeCandidate, CodexCliInstallReport, CodexCliInstallProvenanceDeps } from "./cli-install-provenance-types"; +export { isAppBundledCodexPath, isCodexCliUpdateVersionManagerPath } from "./cli-install-provenance-paths"; +``` + +Retain these current exports as declarations in the original file (not copies): `observeCodexRuntimeCandidateReadOnly`, `inspectCodexCliInstall`. Together with the block above this preserves the complete old type/value export set; leaf-private API is not added to the facade. + +Explicit residual imports (replace the old import block): + +```ts +import { statSync } from "node:fs"; +import { win32 } from "node:path"; +import { getConfigDir } from "../config"; +import { codexRuntimeStatePath, parsePersistedCodexRuntime } from "./runtime"; +import { inspectCodexShimBackingForCommand, isLocalAbsoluteInspectionPath, type CodexShimBackingForCommand } from "./shim"; +import type { CodexCliInstallReport, CodexCliInstallReason, CodexCliInstallEvidence, CodexCliInstallProvenanceDeps, ReadOnlyCodexRuntimeCandidate } from "./cli-install-provenance-types"; +import { publicExecutableLocation, pathTools, isWindowsPlatform, isSafeLocalInspectionPath, resolveCandidateCommandPath, canonicalize, isAppBundledCodexPath, isCodexCliUpdateVersionManagerPath, configuredVersionManagerRoots, isWithinConfiguredVersionManagerRoot } from "./cli-install-provenance-paths"; +import { validatedVersion, readBoundedFile, findCodexPackageManifest, launcherIsLinkedToManifest, isProvenGlobalNpmLayout } from "./cli-install-provenance-files"; +``` + +## Module-level state and cycles + +Lane 013 and the top-level inventory identify no mutable module state: no top-level let, Map, Set, WeakMap, lock or flight. CODEX_PACKAGE (29), MAX_MANIFEST_BYTES (30), MAX_MANIFEST_ANCESTORS (32) move to files; MAX_RUNTIME_STATE_BYTES (31) stays with readPersistedCandidate. Sets at 368 and 483 are fresh per invocation and stay in paths/files respectively. File descriptor fd at 402 belongs to each readBoundedFile invocation, including its finally close, and moves as one whole function. Do not split or duplicate that ownership. Preserve dependency lookup inside calls; never hoist deps.env/deps.platform/deps.stat or process.env captures into module state. files → facade for isSafeLocalInspectionPath or types would form a new cycle; the explicit paths/types leaves remove that edge, including type-only imports. + +Lane 013 reported no static return-path cycle for this source. This plan's new local graph is acyclic by the dependency direction above; this is not a substitute for the executor's fresh whole-relative-graph return-path scan. Include type-only imports/re-exports, not merely runtime imports. New edges are Functional/Sequential coupling, not shared mutable Common state; preserve existing invocation ordering rather than adding locks or global owners. No leaf imports `./cli-install-provenance` or any facade that routes back into itself. No lazy import workaround. + +## Tests + +Direct importer list, reproduced by `rg -l -F 'src/codex/cli-install-provenance' tests` (all **unchanged**, including import path and existing assertions): + +- `tests/cli/cli-codex-cli-update.test.ts` — unchanged. +- `tests/codex-integration/codex-cli-install-provenance.test.ts` — unchanged. + +Text-oracle inventory: **none found** for this exact source path. Inspected basename/path matches and segmented `repoPath` forms for `readFileSync`, `Bun.file` and source-reader helpers, consistent with lane 013. There is therefore no source-read line to retarget and no explicit scan-list entry to add. A basename occurrence in `tests/fixtures/test-layout-expected.json` is test registration, not a source read. Generic recursive import-graph coverage is unchanged and discovers imports naturally. If implementation finds a computed/path-list source oracle not captured here, stop and extend the inventory with its exact read line before moving code; do not weaken it. + +Keep both direct-import test files unchanged in their behavioral cases. Preserve tests/codex-integration/codex-cli-install-provenance.test.ts:62 (Windows no-filesystem calls), :111 (no persisted-state read on Windows), :125 (lexical report-only classifications), and :146 (version-manager layout discrimination), plus all existing injected and native filesystem fixtures. Add named-export identity assertions for isAppBundledCodexPath and isCodexCliUpdateVersionManagerPath to that existing test using ../../src/codex/cli-install-provenance-paths. Drive the identity guard red with a temporary facade wrapper for isAppBundledCodexPath, restore, then green. Typecheck covers the moved public contracts; audit the exact seven exported type names against the old boundary. Do not add exports for sha256 or private filesystem types solely for tests. + +These red-once mutations are future disposable-worktree verification steps, never persistent changes. They were not performed during drafting. Extend existing test files only; no new test file or test-layout entry is planned. `tests/lab/core-lab-boundary.test.ts` PROTECTED roots are never edited. + +## Verification + +Future implementation commands only; **none run in this docs-only task**. Execute against this layer's own tip, domains **codex-integration, cli**, not the eventual stack top. + +```sh +bun run typecheck +bun test tests/codex-integration/codex-cli-install-provenance.test.ts tests/cli/cli-codex-cli-update.test.ts +bun run privacy:scan +wc -l src/codex/cli-install-provenance-types.ts src/codex/cli-install-provenance-paths.ts src/codex/cli-install-provenance-files.ts src/codex/cli-install-provenance.ts +rg -l -F 'codex/cli-install-provenance' src gui/src scripts tests +# Resolve relative import/re-export paths and compare the original consumer file set. +# Full suite: lidge only, no local full-suite invocation; keep the full exit status/log. +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-cli-install-provenance && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test' +``` + +For the 002 importer gate, the expected **existing** direct consumer set is 3: `src/cli/codex-cli-update.ts`, `tests/cli/cli-codex-cli-update.test.ts`, `tests/codex-integration/codex-cli-install-provenance.test.ts`. The rg line above is a candidate list, not the count: same-directory imports and aliases require the path resolution described in Symbol inventory. Compare file sets, not statement counts; added leaf imports in identity tests are intentional. No original consumer migrates away from this boundary. Typecheck must still resolve every old export. + +Cycle verification: repeat lane 013 SG-GRAPH using `sg run --lang ts --kind import_statement --json=compact src` and `sg run --lang ts --kind export_statement --json=compact src`; resolve relative .ts/.tsx/index targets, include type edges, and search for a return path to the original or any new leaf. Require no new return path; record the scoped graph result. Do not install a new dependency tool for this layer. + +The 002 conditional Lab gate is not triggered by these planned source paths (none is src/server, src/router.ts or src/lib). If the implementation touches one of those paths, that is an expansion requiring parent approval and `bun test tests/lab/core-lab-boundary.test.ts`; keep PROTECTED unchanged. All new leaves must stay free of a transitive Lab dependency regardless. + +Record red then green for the guard named in Tests, typecheck exit 0, focused tests 0 failures, privacy scan exit 0, actual per-file line counts, full-suite exit 0 on lidge, the exact tested SHA and CI rollup. The remote worktree is parent-coordinated; confirm ownership before checkout and require its tested SHA to equal the PR head. Do not mask test exit status with an unguarded tail pipeline. Revalidate after any cascade. + +## Accept criteria + +1. Source still matches the stated basis or the plan is refreshed for every changed symbol before extraction. Parent size disposition is recorded before implementation; absent that decision the layer is not executable. +2. Every inventory declaration has exactly one owner; all function bodies/signatures and constant/type definitions are moved verbatim, apart from the necessary export modifiers and import paths. No public export is renamed, deleted, wrapped or newly invented. +3. Every current export remains importable from `src/codex/cli-install-provenance.ts`; moved values pass identity guards where applicable, and residual references are satisfied by real imports, not a re-export-only assumption. +4. Actual 3 new leaves and the residual are each ≤400 physical lines. Record counts rather than relying on these estimates. No hidden #b or unplanned source file is required. +5. Reports retain identical freezing, classification reasons, selectionAttested/managed values and injected dependency behavior; no additional filesystem access or process execution is introduced. +6. State/constant ownership matches this plan; fresh relative-import graph reports no new cycle, including type-only edges, and no new Lab reachability. +7. Existing tests/imports/source guards are retained without weakening; the specified guard is demonstrated red once and restored green. All instantiated 002 gates and exact-head CI are green with recorded evidence. +8. PR uses the template, correct base and complete five-layer map. No merge, release, deployment, dependency installation on the user's running service, or unrelated code change is included. + +## PR + +Title: `refactor(codex): separate install evidence from classification (split S11 L3/5)` + +Branch: `codex/split-codex-cli-install-provenance`. Base: `dev`. Closes: **none**. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. Put the measured move size and any parent-approved exception in Summary, evidence tied to this PR head in Verification, and include the stack map below. Review this layer's diff only. PR numbers are intentionally unassigned planning placeholders, not existing PR claims. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S11-L1 | L1 | `codex/split-combos-types` | `dev` | isolate combo identifiers from validation | +| 2 | #TBD-S11-L2 | L2 | `codex/split-codex-subagent-defaults` | `dev` | isolate format-preserving subagent TOML lexing | +| 3 | #TBD-S11-L3 | **L3 — this layer** | `codex/split-codex-cli-install-provenance` | `dev` | separate install evidence from classification | +| 4 | #TBD-S11-L4 | L4 | `codex/split-routing-trace` | `dev` | separate trace contracts and evidence codecs | +| 5 | #TBD-S11-L5 | L5 | `codex/split-oauth-github-copilot` | `dev` | isolate GitHub device grant transport | + +Base: dev — no dependency on the layers below; no cascade obligation. + +DEV-STACK-04: merges remain separately authorized; this task performs none. diff --git a/devlog/_plan/260905_now_split_train/350_routing_trace.md b/devlog/_plan/260905_now_split_train/350_routing_trace.md new file mode 100644 index 0000000000..2d1d9445f0 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/350_routing_trace.md @@ -0,0 +1,204 @@ +# 350 — S11 L4/5: src/routing/trace.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Planning class: C3, bounded docs-only delegation; auth/provenance implementation retains C4 security care where noted below. +- Non-goals: No change to trace wire version, selected-candidate retention, truncation flags, candidate/requirement limits, byte-budget fallback, random decision IDs, or evidence whitelist/normalization behavior. +- Goal: Separate wire DTOs/limits and evidence codecs from trace building, deterministic byte budgeting and persisted-row normalization. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated in Verification below (the 000 reference to 003 is stale; 002 is authoritative). +- Stop: this delegated turn stops after writing and statically checking this plan; no source edits, tests, git mutations, orchestration, loop or goal commands. The later executor stops on any changed behavior, missing binding, cycle, oversized leaf, failing guard or basis drift. Layer execution ends only at an open PR with recorded green exact-head CI; never merge. +- Escalation: send any extra file/layer requirement or boundary change to the parent. Execution also requires the parent to resolve the 500-line diff-size contradiction below. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source line references below are to that source snapshot. `git diff --numstat origin/dev -- src/routing/trace.ts` is empty. Lane audit: `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:359`. No implementation proof is claimed here. + +## Symbol inventory + +Every top-level declaration is listed, including private declarations and import bindings. Inclusive start–end spans were extracted with `sg run --lang ts --kind --json=compact src/routing/trace.ts` and checked against `git show origin/dev:src/routing/trace.ts` with numbered lines. Nested declarations are intentionally not top-level rows. + +Consumers = unique **direct importing/re-exporting files**, not identifier occurrences or callers inside this module. Start from `rg -l -F 'trace' src gui/src scripts tests`, inspect import/re-export clauses, resolve each relative specifier to this exact file, then intersect each named binding with `rg -l -w '' src gui/src scripts tests`. Private declarations have zero external consumers; same-spelling symbols elsewhere are not consumers. Type-only imports count. Imported bindings themselves are local, not exports. Baseline: 14 direct files; test-only leaf imports for new identity assertions do not replace any original import. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `randomBytes` | import binding(s) | 17–17 | no | 0 (local imports) | residual | +| `RouteDecisionKind` | type alias | 19–25 | yes | 1 | `src/routing/trace-contracts.ts` | +| `Unknownable` | type alias | 27–27 | yes | 1 | `src/routing/trace-contracts.ts` | +| `RouteRequirementEvidence` | interface | 29–35 | yes | 1 | `src/routing/trace-contracts.ts` | +| `RouteExclusionReason` | interface | 37–41 | yes | 1 | `src/routing/trace-contracts.ts` | +| `RouteCapabilityEvidence` | interface | 43–54 | yes | 2 | `src/routing/trace-contracts.ts` | +| `RouteHealthEvidence` | interface | 56–65 | yes | 2 | `src/routing/trace-contracts.ts` | +| `RouteQuotaEvidence` | interface | 67–78 | yes | 2 | `src/routing/trace-contracts.ts` | +| `RouteCostCapOutcome` | type alias | 85–89 | yes | 0 | `src/routing/trace-contracts.ts` | +| `RouteCostEvidence` | interface | 91–103 | yes | 2 | `src/routing/trace-contracts.ts` | +| `RouteCompatibilitySuiteTrace` | interface | 105–117 | yes | 0 | `src/routing/trace-contracts.ts` | +| `RouteCompatibilityEvidence` | interface | 119–124 | yes | 2 | `src/routing/trace-contracts.ts` | +| `RouteScoreEvidence` | interface | 126–137 | yes | 1 | `src/routing/trace-contracts.ts` | +| `RouteCandidateTrace` | interface | 139–151 | yes | 1 | `src/routing/trace-contracts.ts` | +| `RouteDecisionTraceV1` | interface | 153–177 | yes | 7 | `src/routing/trace-contracts.ts` | +| `MAX_TRACE_CANDIDATES` | const | 179–179 | yes | 1 | `src/routing/trace-contracts.ts` | +| `MAX_EXCLUSIONS_PER_CANDIDATE` | const | 180–180 | yes | 1 | `src/routing/trace-contracts.ts` | +| `MAX_REQUIREMENTS` | const | 181–181 | yes | 2 | `src/routing/trace-contracts.ts` | +| `MAX_TRACE_STRING` | const | 182–182 | yes | 1 | `src/routing/trace-contracts.ts` | +| `MAX_TRACE_BYTES` | const | 183–183 | yes | 1 | `src/routing/trace-contracts.ts` | +| `ROUTE_KINDS` | const | 185–192 | no | 0 | `src/routing/trace.ts (residual)` | +| `REQUIREMENT_OUTCOMES` | const | 194–194 | no | 0 | `src/routing/trace-evidence.ts` | +| `capString` | function | 197–201 | no | 0 | `src/routing/trace-evidence.ts` | +| `isPlainRecord` | function | 203–205 | no | 0 | `src/routing/trace-evidence.ts` | +| `finiteNumber` | function | 207–209 | no | 0 | `src/routing/trace-evidence.ts` | +| `unknownable` | function | 211–216 | no | 0 | `src/routing/trace-evidence.ts` | +| `TraceCandidateInput` | interface | 218–230 | yes | 1 | `src/routing/trace-contracts.ts` | +| `TraceBuildInput` | interface | 232–248 | yes | 0 | `src/routing/trace-contracts.ts` | +| `ParseCaps` | interface | 250–256 | no | 0 | `src/routing/trace-contracts.ts` | +| `buildCandidate` | function | 259–287 | no | 0 | `src/routing/trace.ts (residual)` | +| `buildRequirement` | function | 290–305 | no | 0 | `src/routing/trace.ts (residual)` | +| `buildRouteDecisionTrace` | function | 311–384 | yes | 3 | `src/routing/trace.ts (residual)` | +| `serializedByteLength` | function | 387–389 | no | 0 | `src/routing/trace.ts (residual)` | +| `enforceByteBudget` | function | 392–443 | no | 0 | `src/routing/trace.ts (residual)` | +| `parseExclusion` | function | 446–457 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseRequirement` | function | 460–484 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseCapability` | function | 487–525 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseHealth` | function | 528–540 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseQuota` | function | 543–558 | no | 0 | `src/routing/trace-evidence.ts` | +| `COST_CAP_OUTCOMES` | const | 560–565 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseCost` | function | 568–584 | no | 0 | `src/routing/trace-evidence.ts` | +| `MAX_COMPATIBILITY_SUITES` | const | 586–586 | no | 0 | `src/routing/trace-evidence.ts` | +| `COMPATIBILITY_OUTCOMES` | const | 587–587 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseCompatibility` | function | 589–623 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseScore` | function | 626–635 | no | 0 | `src/routing/trace-evidence.ts` | +| `parseCandidate` | function | 638–675 | no | 0 | `src/routing/trace-evidence.ts` | +| `normalizeRouteDecisionTrace` | function | 682–776 | yes | 5 | `src/routing/trace.ts (residual)` | + +## Leaf partition + +Structural decision: The 776-line module has self-contained contracts and shared evidence parsing used by both builder and normalizer. Reject extracting only normalizeRouteDecisionTrace: it depends on parsers also used by buildCandidate and would create a facade return edge or duplicate code. Choose trace-contracts.ts and trace-evidence.ts siblings, matching src/routing/request-evidence.ts and domain-named routing modules. Keep both public coordinators and byte-budget enforcement in trace.ts. Blast radius: routing evidence contract used by routing, usage hydration and request logs; no Lab implementation dependency is introduced. + +Pre-change/intended map: Current: src/routing/evaluator.ts:10, src/routing/quota.ts:22, src/router.ts:39, src/usage/log.ts:10 and src/server/request-log.ts:16 → trace.ts → node:crypto only. Intended: same callers → trace.ts → trace-evidence.ts → trace-contracts.ts; trace.ts also imports contracts directly. node:crypto remains only in trace.ts. Evidence codecs consume shared contracts and never import the build/normalization facade. ParseCaps is an internal downward contract, exported only from its leaf and not added to the old public API. + +Sizing escalation: the partition moves 461 existing lines, so source additions-plus-deletions are at least 922 before rewiring. The fixed S11 L4 cannot meet a literal 500 changed-line cap. Parent disposition (explicit pure-move exception or revised layer map) is required before execution. Do not report the one-way moved-line count as a passing diff-size check. No extra #b is assumed by this document. + +### `src/routing/trace-contracts.ts` — 208 expected lines + +Move source bands `src/routing/trace.ts:19`–184, `src/routing/trace.ts:218`–257 (206 physical lines including existing inter-declaration comments/blanks). Symbols: `RouteDecisionKind`, `Unknownable`, `RouteRequirementEvidence`, `RouteExclusionReason`, `RouteCapabilityEvidence`, `RouteHealthEvidence`, `RouteQuotaEvidence`, `RouteCostCapOutcome`, `RouteCostEvidence`, `RouteCompatibilitySuiteTrace`, `RouteCompatibilityEvidence`, `RouteScoreEvidence`, `RouteCandidateTrace`, `RouteDecisionTraceV1`, `MAX_TRACE_CANDIDATES`, `MAX_EXCLUSIONS_PER_CANDIDATE`, `MAX_REQUIREMENTS`, `MAX_TRACE_STRING`, `MAX_TRACE_BYTES`, `TraceCandidateInput`, `TraceBuildInput`, `ParseCaps`. + +Keep existing exported declarations exported. Add the `export` modifier (without changing a body/signature) only to these formerly private declarations needed by another production module: `ParseCaps`. Every other private declaration stays private; none of the new internal exports is added to the facade. + +Own imports (complete): + +```ts +// None: this leaf has no imports. +``` + +### `src/routing/trace-evidence.ts` — 259 expected lines + +Move source bands `src/routing/trace.ts:194`–194, `src/routing/trace.ts:196`–217, `src/routing/trace.ts:445`–676 (255 physical lines including existing inter-declaration comments/blanks). Symbols: `REQUIREMENT_OUTCOMES`, `capString`, `isPlainRecord`, `finiteNumber`, `unknownable`, `parseExclusion`, `parseRequirement`, `parseCapability`, `parseHealth`, `parseQuota`, `COST_CAP_OUTCOMES`, `parseCost`, `MAX_COMPATIBILITY_SUITES`, `COMPATIBILITY_OUTCOMES`, `parseCompatibility`, `parseScore`, `parseCandidate`. + +Keep existing exported declarations exported. Add the `export` modifier (without changing a body/signature) only to these formerly private declarations needed by another production module: `capString`, `isPlainRecord`, `finiteNumber`, `parseRequirement`, `parseCapability`, `parseHealth`, `parseQuota`, `parseCost`, `parseCompatibility`, `parseCandidate`. Every other private declaration stays private; none of the new internal exports is added to the facade. + +Own imports (complete): + +```ts +import type { Unknownable, RouteRequirementEvidence, RouteExclusionReason, RouteCapabilityEvidence, RouteHealthEvidence, RouteQuotaEvidence, RouteCostCapOutcome, RouteCostEvidence, RouteCompatibilitySuiteTrace, RouteCompatibilityEvidence, RouteScoreEvidence, RouteCandidateTrace, ParseCaps } from "./trace-contracts"; +import { MAX_TRACE_STRING, MAX_EXCLUSIONS_PER_CANDIDATE } from "./trace-contracts"; +``` + +### Residual `src/routing/trace.ts` — 321 expected lines + +Keep these declarations: `ROUTE_KINDS`, `buildCandidate`, `buildRequirement`, `buildRouteDecisionTrace`, `serializedByteLength`, `enforceByteBudget`, `normalizeRouteDecisionTrace`. + +Accounting: 776 original − 461 moved − 0 replaced import/header lines + 3 explicit import lines + 2 named re-export lines + 1 separator = **321**. Each leaf estimate is its source-band count + own import lines + two header/separator lines. These are physical-line estimates using the compact exact import blocks below, not a claim of measured implementation output. Preserve comments, allow readable multiline imports, and remeasure after formatting; no file may exceed 400. No residual >400 and no #b required by file length. No #a/#b/#c parts are added in this five-layer map. Original function bodies over 50 lines remain unchanged as an explicit pure-move exception; splitting their logic is out of scope. + +## Re-export block + +Insert at the existing feature boundary, using named re-exports only. This is preservation of an established path, not a new internal index barrel. Re-exports create no local bindings. + +```ts +export { MAX_TRACE_CANDIDATES, MAX_EXCLUSIONS_PER_CANDIDATE, MAX_REQUIREMENTS, MAX_TRACE_STRING, MAX_TRACE_BYTES } from "./trace-contracts"; +export type { RouteDecisionKind, Unknownable, RouteRequirementEvidence, RouteExclusionReason, RouteCapabilityEvidence, RouteHealthEvidence, RouteQuotaEvidence, RouteCostCapOutcome, RouteCostEvidence, RouteCompatibilitySuiteTrace, RouteCompatibilityEvidence, RouteScoreEvidence, RouteCandidateTrace, RouteDecisionTraceV1, TraceCandidateInput, TraceBuildInput } from "./trace-contracts"; +``` + +Retain these current exports as declarations in the original file (not copies): `buildRouteDecisionTrace`, `normalizeRouteDecisionTrace`. Together with the block above this preserves the complete old type/value export set; leaf-private API is not added to the facade. + +Explicit residual imports (add alongside any unchanged original imports): + +```ts +import type { RouteCandidateTrace, RouteDecisionTraceV1, RouteDecisionKind, RouteRequirementEvidence, TraceCandidateInput, TraceBuildInput, ParseCaps } from "./trace-contracts"; +import { MAX_TRACE_CANDIDATES, MAX_EXCLUSIONS_PER_CANDIDATE, MAX_REQUIREMENTS, MAX_TRACE_STRING, MAX_TRACE_BYTES } from "./trace-contracts"; +import { capString, isPlainRecord, finiteNumber, parseCapability, parseHealth, parseQuota, parseCost, parseCompatibility, parseCandidate, parseRequirement } from "./trace-evidence"; +``` + +## Module-level state and cycles + +ROUTE_KINDS Set at 185–192 stays in trace.ts with normalizeRouteDecisionTrace. REQUIREMENT_OUTCOMES at 194 moves to trace-evidence.ts with parseRequirement. COST_CAP_OUTCOMES at 560–565 and COMPATIBILITY_OUTCOMES at 587 move to that same leaf; neither is exported. MAX_COMPATIBILITY_SUITES at 586 moves with parseCompatibility. The five public numeric limits at 179–183 have one owner in trace-contracts.ts and are re-exported, never copied. There is no module-level let, WeakMap, lock, flight or timer. budget/caps/truncated objects are call-local. Builder → parser → facade would be a cycle if ParseCaps or limits stayed only in the facade; moving them into contracts prevents that edge. + +Lane 013 reported no static return-path cycle for this source. This plan's new local graph is acyclic by the dependency direction above; this is not a substitute for the executor's fresh whole-relative-graph return-path scan. Include type-only imports/re-exports, not merely runtime imports. New edges are Functional/Sequential coupling, not shared mutable Common state; preserve existing invocation ordering rather than adding locks or global owners. No leaf imports `./trace` or any facade that routes back into itself. No lazy import workaround. + +## Tests + +Direct importer list, reproduced by `rg -l -F 'src/routing/trace' tests` (all **unchanged**, including import path and existing assertions): + +- `tests/routing/routing-compatibility.test.ts` — unchanged. +- `tests/routing/routing-policy-fallback.test.ts` — unchanged. +- `tests/server/route-decision-trace.test.ts` — unchanged. +- `tests/usage/cost-cap-unknown-evidence.test.ts` — unchanged. + +Text-oracle inventory: **none found** for this exact source path. Inspected basename/path matches and segmented `repoPath` forms for `readFileSync`, `Bun.file` and source-reader helpers, consistent with lane 013. There is therefore no source-read line to retarget and no explicit scan-list entry to add. A basename occurrence in `tests/fixtures/test-layout-expected.json` is test registration, not a source read. Generic recursive import-graph coverage is unchanged and discovers imports naturally. If implementation finds a computed/path-list source oracle not captured here, stop and extend the inventory with its exact read line before moving code; do not weaken it. + +All four direct-import test files retain behavioral imports through trace.ts. Preserve tests/server/route-decision-trace.test.ts:186 exactly: selected-candidate index/model and UTF-8 byte budget are the initial pure-move oracle. Also keep :227 (retained reasoning-effort reads), :259 (sparse evidence), :338 (whitelisted evidence) and :383 (usage/request-log roundtrip). Add assertions to the existing trace test comparing all five public numeric limits with ../../src/routing/trace-contracts; add a bound test using a long provider string so the facade demonstrably uses the moved MAX_TRACE_STRING. Drive the latter guard red once by temporarily bypassing capString's slice in trace-evidence.ts, restore, then green. Preserve the existing byte-budget test unchanged rather than adjusting an expected limit to make extraction pass. + +These red-once mutations are future disposable-worktree verification steps, never persistent changes. They were not performed during drafting. Extend existing test files only; no new test file or test-layout entry is planned. `tests/lab/core-lab-boundary.test.ts` PROTECTED roots are never edited. + +## Verification + +Future implementation commands only; **none run in this docs-only task**. Execute against this layer's own tip, domains **server, routing, usage**, not the eventual stack top. + +```sh +bun run typecheck +bun test tests/server/route-decision-trace.test.ts tests/routing/routing-policy-fallback.test.ts tests/routing/routing-compatibility.test.ts tests/usage/cost-cap-unknown-evidence.test.ts +bun run privacy:scan +wc -l src/routing/trace-contracts.ts src/routing/trace-evidence.ts src/routing/trace.ts +rg -l -F 'routing/trace' src gui/src scripts tests +# Resolve relative import/re-export paths and compare the original consumer file set. +# Full suite: lidge only, no local full-suite invocation; keep the full exit status/log. +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-routing-trace && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test' +``` + +For the 002 importer gate, the expected **existing** direct consumer set is 14: `src/router.ts`, `src/routing/capability.ts`, `src/routing/compatibility/policy.ts`, `src/routing/cost.ts`, `src/routing/evaluator.ts`, `src/routing/health.ts`, `src/routing/quota.ts`, `src/server/request-log.ts`, `src/server/responses/policy-fallback.ts`, `src/usage/log.ts`, `tests/routing/routing-compatibility.test.ts`, `tests/routing/routing-policy-fallback.test.ts`, `tests/server/route-decision-trace.test.ts`, `tests/usage/cost-cap-unknown-evidence.test.ts`. The rg line above is a candidate list, not the count: same-directory imports and aliases require the path resolution described in Symbol inventory. Compare file sets, not statement counts; added leaf imports in identity tests are intentional. No original consumer migrates away from this boundary. Typecheck must still resolve every old export. + +Cycle verification: repeat lane 013 SG-GRAPH using `sg run --lang ts --kind import_statement --json=compact src` and `sg run --lang ts --kind export_statement --json=compact src`; resolve relative .ts/.tsx/index targets, include type edges, and search for a return path to the original or any new leaf. Require no new return path; record the scoped graph result. Do not install a new dependency tool for this layer. + +The 002 conditional Lab gate is not triggered by these planned source paths (none is src/server, src/router.ts or src/lib). If the implementation touches one of those paths, that is an expansion requiring parent approval and `bun test tests/lab/core-lab-boundary.test.ts`; keep PROTECTED unchanged. All new leaves must stay free of a transitive Lab dependency regardless. + +Record red then green for the guard named in Tests, typecheck exit 0, focused tests 0 failures, privacy scan exit 0, actual per-file line counts, full-suite exit 0 on lidge, the exact tested SHA and CI rollup. The remote worktree is parent-coordinated; confirm ownership before checkout and require its tested SHA to equal the PR head. Do not mask test exit status with an unguarded tail pipeline. Revalidate after any cascade. + +## Accept criteria + +1. Source still matches the stated basis or the plan is refreshed for every changed symbol before extraction. Parent size disposition is recorded before implementation; absent that decision the layer is not executable. +2. Every inventory declaration has exactly one owner; all function bodies/signatures and constant/type definitions are moved verbatim, apart from the necessary export modifiers and import paths. No public export is renamed, deleted, wrapped or newly invented. +3. Every current export remains importable from `src/routing/trace.ts`; moved values pass identity guards where applicable, and residual references are satisfied by real imports, not a re-export-only assumption. +4. Actual 2 new leaves and the residual are each ≤400 physical lines. Record counts rather than relying on these estimates. No hidden #b or unplanned source file is required. +5. The old runtime export set is exactly five numeric limits plus the two public functions; all sixteen public types remain importable; codec-private sets and ParseCaps do not leak through the facade. +6. State/constant ownership matches this plan; fresh relative-import graph reports no new cycle, including type-only edges, and no new Lab reachability. +7. Existing tests/imports/source guards are retained without weakening; the specified guard is demonstrated red once and restored green. All instantiated 002 gates and exact-head CI are green with recorded evidence. +8. PR uses the template, correct base and complete five-layer map. No merge, release, deployment, dependency installation on the user's running service, or unrelated code change is included. + +## PR + +Title: `refactor(routing): separate trace contracts and evidence codecs (split S11 L4/5)` + +Branch: `codex/split-routing-trace`. Base: `dev`. Closes: **none**. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. Put the measured move size and any parent-approved exception in Summary, evidence tied to this PR head in Verification, and include the stack map below. Review this layer's diff only. PR numbers are intentionally unassigned planning placeholders, not existing PR claims. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S11-L1 | L1 | `codex/split-combos-types` | `dev` | isolate combo identifiers from validation | +| 2 | #TBD-S11-L2 | L2 | `codex/split-codex-subagent-defaults` | `dev` | isolate format-preserving subagent TOML lexing | +| 3 | #TBD-S11-L3 | L3 | `codex/split-codex-cli-install-provenance` | `dev` | separate install evidence from classification | +| 4 | #TBD-S11-L4 | **L4 — this layer** | `codex/split-routing-trace` | `dev` | separate trace contracts and evidence codecs | +| 5 | #TBD-S11-L5 | L5 | `codex/split-oauth-github-copilot` | `dev` | isolate GitHub device grant transport | + +Base: dev — no dependency on the layers below; no cascade obligation. + +DEV-STACK-04: merges remain separately authorized; this task performs none. diff --git a/devlog/_plan/260905_now_split_train/360_oauth_github_copilot.md b/devlog/_plan/260905_now_split_train/360_oauth_github_copilot.md new file mode 100644 index 0000000000..671718696b --- /dev/null +++ b/devlog/_plan/260905_now_split_train/360_oauth_github_copilot.md @@ -0,0 +1,175 @@ +# 360 — S11 L5/5: src/oauth/github-copilot.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**. Planning class: C3, bounded docs-only delegation; auth/provenance implementation retains C4 security care where noted below. +- Non-goals: No live login, token refresh, credential writes, timer redesign, endpoint/allowlist changes, altered cancellation/cadence, retry changes or new validation. Preserve public constant values and header object identity. +- Goal: Move GitHub device authorization, polling and refresh-grant transport into one leaf; retain Copilot token exchange, identity projection and public login/refresh orchestration at the old boundary. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated in Verification below (the 000 reference to 003 is stale; 002 is authoritative). +- Stop: this delegated turn stops after writing and statically checking this plan; no source edits, tests, git mutations, orchestration, loop or goal commands. The later executor stops on any changed behavior, missing binding, cycle, oversized leaf, failing guard or basis drift. Layer execution ends only at an open PR with recorded green exact-head CI; never merge. +- Escalation: send any extra file/layer requirement or boundary change to the parent. Do not expand this layer into adjacent cleanup or add an unplanned #b. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source line references below are to that source snapshot. `git diff --numstat origin/dev -- src/oauth/github-copilot.ts` is empty. Lane audit: `devlog/_plan/260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md:691`. No implementation proof is claimed here. + +## Symbol inventory + +Every top-level declaration is listed, including private declarations and import bindings. Inclusive start–end spans were extracted with `sg run --lang ts --kind --json=compact src/oauth/github-copilot.ts` and checked against `git show origin/dev:src/oauth/github-copilot.ts` with numbered lines. Nested declarations are intentionally not top-level rows. + +Consumers = unique **direct importing/re-exporting files**, not identifier occurrences or callers inside this module. Start from `rg -l -F 'github-copilot' src gui/src scripts tests`, inspect import/re-export clauses, resolve each relative specifier to this exact file, then intersect each named binding with `rg -l -w '' src gui/src scripts tests`. Private declarations have zero external consumers; same-spelling symbols elsewhere are not consumers. Type-only imports count. Imported bindings themselves are local, not exports. Baseline: 6 direct files; test-only leaf imports for new identity assertions do not replace any original import. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `OAuthController, OAuthCredentials` | import binding(s) | 7–7 | no | 0 (local imports) | residual | +| `GITHUB_COPILOT_OAUTH_CLIENT_ID` | const | 10–10 | yes | 0 | `src/oauth/github-copilot-device.ts` | +| `GITHUB_COPILOT_DEFAULT_API_BASE` | const | 11–11 | yes | 1 | `src/oauth/github-copilot.ts (residual)` | +| `GITHUB_DEVICE_VERIFY_ORIGIN` | const | 12–12 | yes | 0 | `src/oauth/github-copilot-device.ts` | +| `GITHUB_DEVICE_VERIFY_PATH` | const | 13–13 | yes | 0 | `src/oauth/github-copilot-device.ts` | +| `DEVICE_CODE_URL` | const | 15–15 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `ACCESS_TOKEN_URL` | const | 16–16 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `COPILOT_TOKEN_URL` | const | 17–17 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `GITHUB_USER_URL` | const | 18–18 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `OAUTH_SCOPE` | const | 20–20 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `DEFAULT_POLL_INTERVAL_MS` | const | 21–21 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `DEFAULT_DEVICE_FLOW_TTL_MS` | const | 22–22 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `OAUTH_EXPIRY_SKEW_MS` | const | 23–23 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `MIN_POLL_MS` | const | 24–24 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `TERMINAL_OAUTH_ERROR_CODES` | const | 26–26 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `IDENTITY_RETRY_DELAY_MS` | const | 27–27 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `GITHUB_COPILOT_EDITOR_HEADERS` | const | 30–36 | yes | 1 | `src/oauth/github-copilot.ts (residual)` | +| `DeviceAuthorizationResponse` | interface | 38–47 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `GithubTokenResponse` | interface | 49–57 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `CopilotTokenResponse` | interface | 59–64 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `GithubUserResponse` | interface | 66–70 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `sleep` | function | 72–81 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `githubCopilotHttpError` | function | 84–86 | yes | 1 | `src/oauth/github-copilot-device.ts` | +| `buildGithubDeviceVerifyUrl` | function | 88–94 | yes | 1 | `src/oauth/github-copilot-device.ts` | +| `isAllowedGithubDeviceVerifyUrl` | function | 100–112 | yes | 1 | `src/oauth/github-copilot-device.ts` | +| `validateCopilotApiBaseUrl` | function | 118–139 | yes | 4 | `src/oauth/github-copilot.ts (residual)` | +| `resolveCopilotApiBaseUrl` | function | 141–143 | yes | 3 | `src/oauth/github-copilot.ts (residual)` | +| `requestDeviceAuthorization` | function | 145–181 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `pollGithubDeviceToken` | function | 183–245 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `refreshGithubAccessToken` | function | 247–279 | no | 0 | `src/oauth/github-copilot-device.ts` | +| `exchangeCopilotToken` | function | 281–313 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `fetchGithubIdentityOnce` | function | 315–340 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `fetchGithubIdentity` | function | 348–362 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `credentialsFromGithubAccess` | function | 370–388 | no | 0 | `src/oauth/github-copilot.ts (residual)` | +| `loginGithubCopilot` | function | 390–411 | yes | 2 | `src/oauth/github-copilot.ts (residual)` | +| `refreshGithubCopilotToken` | function | 419–428 | yes | 2 | `src/oauth/github-copilot.ts (residual)` | + +## Leaf partition + +Structural decision: The 428-line OAuth module has a device-grant cluster and a Copilot exchange/identity cluster. Reject moving only pollGithubDeviceToken: it shares sleep, GitHub token response shape, access endpoint and status-only errors with other functions. Move the complete device/refresh cluster and its shared stateless primitives into github-copilot-device.ts, exporting only the internal operations the residual actually calls. This follows src/oauth/chatgpt-device.ts and src/oauth/kiro-credentials.ts. Blast radius: GitHub Copilot OAuth feature; later implementation needs C4 auth care and explicit security review (MAINTAINERS.md:60). + +Pre-change/intended map: Current: src/oauth/index.ts:40, src/oauth/store.ts:30, src/providers/github-copilot-transport.ts:2 and src/server/responses/core.ts:140 → github-copilot.ts → OAuth types only. Intended: the same callers → github-copilot.ts → github-copilot-device.ts (zero imports); OAuthController/OAuthCredentials remain type-only imports in the facade. sleep and githubCopilotHttpError move down because both clusters call them. Editor headers/API origin validation remain residual and are not needed by the device leaf; therefore no return edge. + +The leaf deliberately includes refreshGithubAccessToken with polling because both own ACCESS_TOKEN_URL and GithubTokenResponse. The public refreshGithubCopilotToken stays in the facade, so durable-grant dispatch, parallel exchange/identity lookup and identity-required persistence behavior retain their existing caller boundary. + +### `src/oauth/github-copilot-device.ts` — 214 expected lines + +Move source bands `src/oauth/github-copilot.ts:9`–10, `src/oauth/github-copilot.ts:12`–16, `src/oauth/github-copilot.ts:20`–22, `src/oauth/github-copilot.ts:24`–26, `src/oauth/github-copilot.ts:38`–58, `src/oauth/github-copilot.ts:72`–113, `src/oauth/github-copilot.ts:145`–280 (212 physical lines including existing inter-declaration comments/blanks). Symbols: `GITHUB_COPILOT_OAUTH_CLIENT_ID`, `GITHUB_DEVICE_VERIFY_ORIGIN`, `GITHUB_DEVICE_VERIFY_PATH`, `DEVICE_CODE_URL`, `ACCESS_TOKEN_URL`, `OAUTH_SCOPE`, `DEFAULT_POLL_INTERVAL_MS`, `DEFAULT_DEVICE_FLOW_TTL_MS`, `MIN_POLL_MS`, `TERMINAL_OAUTH_ERROR_CODES`, `DeviceAuthorizationResponse`, `GithubTokenResponse`, `sleep`, `githubCopilotHttpError`, `buildGithubDeviceVerifyUrl`, `isAllowedGithubDeviceVerifyUrl`, `requestDeviceAuthorization`, `pollGithubDeviceToken`, `refreshGithubAccessToken`. + +Keep existing exported declarations exported. Add the `export` modifier (without changing a body/signature) only to these formerly private declarations needed by another production module: `requestDeviceAuthorization`, `pollGithubDeviceToken`, `refreshGithubAccessToken`, `sleep`. Every other private declaration stays private; none of the new internal exports is added to the facade. + +Own imports (complete): + +```ts +// None: this leaf has no imports. +``` + +### Residual `src/oauth/github-copilot.ts` — 219 expected lines + +Keep these declarations: `GITHUB_COPILOT_DEFAULT_API_BASE`, `COPILOT_TOKEN_URL`, `GITHUB_USER_URL`, `OAUTH_EXPIRY_SKEW_MS`, `IDENTITY_RETRY_DELAY_MS`, `GITHUB_COPILOT_EDITOR_HEADERS`, `CopilotTokenResponse`, `GithubUserResponse`, `validateCopilotApiBaseUrl`, `resolveCopilotApiBaseUrl`, `exchangeCopilotToken`, `fetchGithubIdentityOnce`, `fetchGithubIdentity`, `credentialsFromGithubAccess`, `loginGithubCopilot`, `refreshGithubCopilotToken`. + +Accounting: 428 original − 212 moved − 0 replaced import/header lines + 1 explicit import lines + 1 named re-export lines + 1 separator = **219**. Each leaf estimate is its source-band count + own import lines + two header/separator lines. These are physical-line estimates using the compact exact import blocks below, not a claim of measured implementation output. Preserve comments, allow readable multiline imports, and remeasure after formatting; no file may exceed 400. No residual >400 and no #b required by file length. No #a/#b/#c parts are added in this five-layer map. Original function bodies over 50 lines remain unchanged as an explicit pure-move exception; splitting their logic is out of scope. + +## Re-export block + +Insert at the existing feature boundary, using named re-exports only. This is preservation of an established path, not a new internal index barrel. Re-exports create no local bindings. + +```ts +export { GITHUB_COPILOT_OAUTH_CLIENT_ID, GITHUB_DEVICE_VERIFY_ORIGIN, GITHUB_DEVICE_VERIFY_PATH, githubCopilotHttpError, buildGithubDeviceVerifyUrl, isAllowedGithubDeviceVerifyUrl } from "./github-copilot-device"; +``` + +Retain these current exports as declarations in the original file (not copies): `GITHUB_COPILOT_DEFAULT_API_BASE`, `GITHUB_COPILOT_EDITOR_HEADERS`, `validateCopilotApiBaseUrl`, `resolveCopilotApiBaseUrl`, `loginGithubCopilot`, `refreshGithubCopilotToken`. Together with the block above this preserves the complete old type/value export set; leaf-private API is not added to the facade. + +Explicit residual imports (add alongside any unchanged original imports): + +```ts +import { requestDeviceAuthorization, pollGithubDeviceToken, refreshGithubAccessToken, sleep, githubCopilotHttpError, isAllowedGithubDeviceVerifyUrl } from "./github-copilot-device"; +``` + +## Module-level state and cycles + +TERMINAL_OAUTH_ERROR_CODES Set at 26 has exactly one owner: github-copilot-device.ts alongside refreshGithubAccessToken. It remains private, with identical members and construction timing relative to its dependent code. GITHUB_COPILOT_EDITOR_HEADERS (30–36), the sole shared object here, stays in the facade; do not clone or freeze it as part of this move. Client ID and verification origin/path move and re-export by binding. All other top-level constants have the owners shown in the inventory. sleep's t timer (75), poll deadline (189) and waitMs (190) remain invocation-local, with unchanged abort listeners. There is no global lock/cache/flight. Moving sleep/error helpers together with the device transport avoids leaf → facade cycles; do not create a general OAuth utilities module. + +Lane 013 reported no static return-path cycle for this source. This plan's new local graph is acyclic by the dependency direction above; this is not a substitute for the executor's fresh whole-relative-graph return-path scan. Include type-only imports/re-exports, not merely runtime imports. New edges are Functional/Sequential coupling, not shared mutable Common state; preserve existing invocation ordering rather than adding locks or global owners. No leaf imports `./github-copilot` or any facade that routes back into itself. No lazy import workaround. + +## Tests + +Direct importer list, reproduced by `rg -l -F 'src/oauth/github-copilot' tests` (all **unchanged**, including import path and existing assertions): + +- `tests/oauth/generic-oauth-failover.test.ts` — unchanged. +- `tests/providers/github-copilot/github-copilot-oauth.test.ts` — unchanged. + +Text-oracle inventory: **none found** for this exact source path. Inspected basename/path matches and segmented `repoPath` forms for `readFileSync`, `Bun.file` and source-reader helpers, consistent with lane 013. There is therefore no source-read line to retarget and no explicit scan-list entry to add. A basename occurrence in `tests/fixtures/test-layout-expected.json` is test registration, not a source read. Generic recursive import-graph coverage is unchanged and discovers imports naturally. If implementation finds a computed/path-list source oracle not captured here, stop and extend the inventory with its exact read line before moving code; do not weaken it. + +The two direct-import test files keep their existing behavioral imports unchanged; run the entire tests/providers/github-copilot domain for transport/account-origin integration. Preserve tests/providers/github-copilot/github-copilot-oauth.test.ts:47 URL rejection, :69 status-only error assertions, :129 slow_down cadence, :162 refresh failure privacy, :183 cancellation, :248 durable access-grant re-exchange and :263 terminal error allowlisting. Add an identity test there comparing the six moved public exports against ../../../src/oauth/github-copilot-device. Drive red once by replacing the facade's githubCopilotHttpError re-export with a temporary wrapper, restore, then green. Fetch remains read dynamically from globalThis during calls; do not capture it at module load and invalidate the existing fetch-mock tests. No test is converted into a real OAuth request. + +These red-once mutations are future disposable-worktree verification steps, never persistent changes. They were not performed during drafting. Extend existing test files only; no new test file or test-layout entry is planned. `tests/lab/core-lab-boundary.test.ts` PROTECTED roots are never edited. + +## Verification + +Future implementation commands only; **none run in this docs-only task**. Execute against this layer's own tip, domains **providers/github-copilot, oauth**, not the eventual stack top. + +```sh +bun run typecheck +bun test tests/providers/github-copilot tests/oauth/generic-oauth-failover.test.ts +bun run privacy:scan +wc -l src/oauth/github-copilot-device.ts src/oauth/github-copilot.ts +rg -l -F 'oauth/github-copilot' src gui/src scripts tests +# Resolve relative import/re-export paths and compare the original consumer file set. +# Full suite: lidge only, no local full-suite invocation; keep the full exit status/log. +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-oauth-github-copilot && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test' +``` + +For the 002 importer gate, the expected **existing** direct consumer set is 6: `src/oauth/index.ts`, `src/oauth/store.ts`, `src/providers/github-copilot-transport.ts`, `src/server/responses/core.ts`, `tests/oauth/generic-oauth-failover.test.ts`, `tests/providers/github-copilot/github-copilot-oauth.test.ts`. The rg line above is a candidate list, not the count: same-directory imports and aliases require the path resolution described in Symbol inventory. Compare file sets, not statement counts; added leaf imports in identity tests are intentional. No original consumer migrates away from this boundary. Typecheck must still resolve every old export. + +Cycle verification: repeat lane 013 SG-GRAPH using `sg run --lang ts --kind import_statement --json=compact src` and `sg run --lang ts --kind export_statement --json=compact src`; resolve relative .ts/.tsx/index targets, include type edges, and search for a return path to the original or any new leaf. Require no new return path; record the scoped graph result. Do not install a new dependency tool for this layer. + +The 002 conditional Lab gate is not triggered by these planned source paths (none is src/server, src/router.ts or src/lib). If the implementation touches one of those paths, that is an expansion requiring parent approval and `bun test tests/lab/core-lab-boundary.test.ts`; keep PROTECTED unchanged. All new leaves must stay free of a transitive Lab dependency regardless. + +Record red then green for the guard named in Tests, typecheck exit 0, focused tests 0 failures, privacy scan exit 0, actual per-file line counts, full-suite exit 0 on lidge, the exact tested SHA and CI rollup. The remote worktree is parent-coordinated; confirm ownership before checkout and require its tested SHA to equal the PR head. Do not mask test exit status with an unguarded tail pipeline. Revalidate after any cascade. + +## Accept criteria + +1. Source still matches the stated basis or the plan is refreshed for every changed symbol before extraction. The actual source diff remains at most 500 added-plus-deleted lines; otherwise escalate before publication. +2. Every inventory declaration has exactly one owner; all function bodies/signatures and constant/type definitions are moved verbatim, apart from the necessary export modifiers and import paths. No public export is renamed, deleted, wrapped or newly invented. +3. Every current export remains importable from `src/oauth/github-copilot.ts`; moved values pass identity guards where applicable, and residual references are satisfied by real imports, not a re-export-only assumption. +4. Actual 1 new leaves and the residual are each ≤400 physical lines. Record counts rather than relying on these estimates. No hidden #b or unplanned source file is required. +5. The 12 public runtime exports keep identical names and bindings/behavior; the editor-header object remains single-owned, device polling waits before each request, and no credential persistence or network action is performed while drafting. +6. State/constant ownership matches this plan; fresh relative-import graph reports no new cycle, including type-only edges, and no new Lab reachability. +7. Existing tests/imports/source guards are retained without weakening; the specified guard is demonstrated red once and restored green. All instantiated 002 gates and exact-head CI are green with recorded evidence. +8. PR uses the template, correct base and complete five-layer map. No merge, release, deployment, dependency installation on the user's running service, or unrelated code change is included. + +## PR + +Title: `refactor(oauth): isolate GitHub device grant transport (split S11 L5/5)` + +Branch: `codex/split-oauth-github-copilot`. Base: `dev`. Closes: **none**. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with Summary, Verification and Checklist. Put the measured move size and any parent-approved exception in Summary, evidence tied to this PR head in Verification, and include the stack map below. Review this layer's diff only. PR numbers are intentionally unassigned planning placeholders, not existing PR claims. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S11-L1 | L1 | `codex/split-combos-types` | `dev` | isolate combo identifiers from validation | +| 2 | #TBD-S11-L2 | L2 | `codex/split-codex-subagent-defaults` | `dev` | isolate format-preserving subagent TOML lexing | +| 3 | #TBD-S11-L3 | L3 | `codex/split-codex-cli-install-provenance` | `dev` | separate install evidence from classification | +| 4 | #TBD-S11-L4 | L4 | `codex/split-routing-trace` | `dev` | separate trace contracts and evidence codecs | +| 5 | #TBD-S11-L5 | **L5 — this layer** | `codex/split-oauth-github-copilot` | `dev` | isolate GitHub device grant transport | + +Base: dev — no dependency on the layers below; no cascade obligation. + +DEV-STACK-04: merges remain separately authorized; this task performs none. diff --git a/devlog/_plan/260905_now_split_train/370_codex_log_guard_inspect.md b/devlog/_plan/260905_now_split_train/370_codex_log_guard_inspect.md new file mode 100644 index 0000000000..c51905db59 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/370_codex_log_guard_inspect.md @@ -0,0 +1,179 @@ +# S12 L1 — Codex Log Guard inspection schema leaf + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. C3 boundary planning, docs-only delegated execution; the parent owns orchestration and goal state. +- Goal: reduce `src/codex/log-guard/inspect.ts` from 524 to an expected 392 lines by extracting exact SQLite schema recognition. Preserve every existing export and all runtime behavior. +- Non-goals: no metric/cache redesign, SQL changes, signature changes, filesystem writes during inspection, new dependencies, caller migration, dead-code cleanup, or function-length cleanup. Existing >50-line functions remain intact under the pure-move constraint. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. No implementation commands or tests run in this drafting task. +- Stop: this layer has an open PR, exact-head CI/full-suite evidence, and the numbered accept criteria satisfied. Never merge. Stop the delegated drafting task after this document is checked. +- Escalation: source drift, changed exports, a new cycle, a leaf/residual >400 lines, >500 added+deleted source lines, weakened test coverage, or behavior changes require the parent's revised plan; do not expand the write scope. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a38`. All source ranges below refer to that code basis. The working-tree file was byte-compared with `git show origin/dev:src/codex/log-guard/inspect.ts`. Input audit: `../260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md`, section for this file, especially `inspect.ts:288` and `inspect.ts:399`. + +## Symbol inventory + +Ranges were checked with `sg run --lang ts --kind --json=compact` and top-level `rg`. Imports are dependencies, not locally owned declarations; they are covered below. Consumer counts are distinct external files from `rg -l -w '' src gui/src scripts tests`, excluding the declaration file and unrelated same-name bindings. Private symbols have zero external consumers; e.g. other `ColumnRow`, `pragmaNumber`, and `fileSize` declarations are not consumers. `R` means the residual original file; `S` means `src/codex/log-guard/inspect-schema.ts`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| IMMUTABLE_READONLY_FLAGS | const | 12–12 | no | 0 | R | +| KNOWN_LOG_LEVELS | const Set | 13–13 | no | 0 | R | +| MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES | const | 16–16 | no | 0 | R | +| CurrentLogColumn | interface | 18–24 | no | 0 | S | +| CURRENT_LOG_SCHEMA | const array | 29–42 | no | 0 | S | +| CURRENT_LOG_TABLE_SQL | const | 44–57 | no | 0 | S | +| CURRENT_LOG_INDEX_SQL | const object | 59–66 | no | 0 | S | +| CodexLogGuardCapabilityReason | type | 68–72 | yes | 0 | R | +| CodexLogGuardSchemaState | type | 74–79 | yes | 0 | R | +| CodexLogGuardCapability | type | 81–83 | yes | 0 | R | +| CodexLogGuardMetrics | interface | 85–96 | yes | 0 | R | +| CodexLogGuardInspection | interface | 105–125 | yes | 3 | R | +| ColumnRow | interface | 127–134 | no | 0 | S | +| SchemaObjectRow | interface | 135–135 | no | 0 | S | +| CountRow | interface | 136–136 | no | 0 | R | +| LevelRow | interface | 137–137 | no | 0 | R | +| TargetCountRow | interface | 138–138 | no | 0 | R | +| EstimatedBytesRow | interface | 139–139 | no | 0 | R | +| CanonicalTargetState | type | 141–141 | no | 0 | R | +| canonicalTargetState | function | 143–149 | no | 0 | R | +| fileSize | function | 151–158 | no | 0 | R | +| InspectionCacheEntry | type | 180–183 | no | 0 | R | +| inspectionCache | let | 185–185 | no | 0 | R | +| inspectionCacheKey | function | 187–215 | no | 0 | R | +| resetCodexLogGuardInspectionCache | function | 218–220 | yes | 1 | R | +| capabilityFor | function | 222–225 | no | 0 | R | +| unavailableInspection | function | 227–244 | no | 0 | R | +| normalizeDeclaredType | function | 246–248 | no | 0 | S | +| normalizeDefault | function | 250–252 | no | 0 | S | +| normalizeSchemaSql | function | 254–256 | no | 0 | S | +| sameColumns | function | 258–269 | no | 0 | S | +| hasCurrentLogsSchema | function | 283–286 | yes | 2 | S | +| hasCurrentLogsTable | function | 288–319 | no | 0 | S | +| pragmaNumber | function | 321–324 | no | 0 | R | +| readMetrics | function | 326–375 | no | 0 | R | +| inspectCodexLogs | function | 383–397 | yes | 4 | R | +| inspectCodexLogsUncached | function | 399–524 | no | 0 | R | + +## Leaf partition + +Structural map: `src/cli/codex-log-guard-doctor.ts:1`, `protection.ts:6`, `maintenance.ts:6`, and the two direct test files below → `inspect.ts` → `../paths`, filesystem/URL functions and SQLite. Intended edge: the same consumers → `inspect.ts` → `inspect-schema.ts` → SQLite **type only**. Blast radius: local Log Guard feature; CLI/API contracts do not change. + +Decision: the size pressure and exact-schema seam justify one extraction. Reject no-op/configuration because neither reduces structural size; reject deletion because it changes the contract; reuse the existing canonical predicate, not a second validator. Reject moving the whole inspector/cache to a new facade because that increases churn and risks splitting cache ownership. Keep metrics in the residual: moving schema alone meets the limit. This is a compatibility re-export on the existing entry, not a new convenience barrel. + +Naming follows sibling `src/codex/log-guard/path-safety.ts` and `sqlite-errors.ts`, and the purpose-qualified siblings `src/config/provider-validation.ts` and `src/server/responses/agent-task-recovery-cache.ts`. `rg --files` confirmed `inspect-schema.ts` does not already exist. + +- New `src/codex/log-guard/inspect-schema.ts`, expected **136 lines**: all `S` symbols above. Move inclusive blocks **18–67, 127–135, 246–320**, including their comments/blanks: 50 + 9 + 75 = **134 moved lines**. Add the following import and one blank line. Export `ColumnRow` and `hasCurrentLogsTable` only from this leaf for the residual's existing query, and retain the export on `hasCurrentLogsSchema`; all other declarations stay leaf-private. + + ```ts + import type { Database } from "bun:sqlite"; + ``` + +- Residual `src/codex/log-guard/inspect.ts`, expected **392 lines**: 524 − 134 + 2 binding/re-export lines below. All `R` symbols remain. Existing imports `statSync`, `join`, `resolve`, `pathToFileURL`, `Database`, `constants`, `getCodexHome`, `resolveCodexSqliteHome`, and `CodexSqliteHomeDeps` remain necessary. No `#b` layer is needed. + +Total after split: 528 lines = 524 original + 4 import/re-export/blank lines. Expected source diff: 134 removed + 138 added = **272**, below 500. Formatting may vary, but actual counts must still satisfy the hard limits. + +## Re-export block + +Add exactly these statements to the original file (one physical line each for the count above): + +```ts +export { hasCurrentLogsSchema } from "./inspect-schema"; +import { hasCurrentLogsTable, type ColumnRow } from "./inspect-schema"; +``` + +The import binds the names still used at original lines 483 and 485; the re-export binds nothing. `hasCurrentLogsSchema` has no residual local use. Keep local exported declarations for `CodexLogGuardCapabilityReason`, `CodexLogGuardSchemaState`, `CodexLogGuardCapability`, `CodexLogGuardMetrics`, `CodexLogGuardInspection`, `resetCodexLogGuardInspectionCache`, and `inspectCodexLogs`. Do not re-export the newly leaf-visible `ColumnRow` or `hasCurrentLogsTable` from the original path. The original export set remains exactly five types/interfaces and three functions. + +## Module-level state and cycles + +- `KNOWN_LOG_LEVELS` (`inspect.ts:13`): one read-only-in-practice Set owner, residual `inspect.ts`; stays with `readMetrics`. +- `inspectionCache` (`inspect.ts:185`): sole mutable memo owner, residual `inspect.ts`; `InspectionCacheEntry`, `inspectionCacheKey`, reset and lookup/publication remain colocated. Preserve the DB/WAL/SHM dev/ino/size/mtimeNs/ctimeNs identity at lines 187–215 and memoized `generatedAt` at 393–395. +- Schema array/object constants (`inspect.ts:29`, `:59`) move once to `inspect-schema.ts`; neither is mutated. SQL string at `:44` moves with them. Read-only flags at `:12` and 64 MiB threshold at `:16` remain residual constants. +- `byName` Map at `:307` is call-local, not a second module singleton. No top-level lock/WeakMap or other mutable owner exists. +- The leaf imports no residual types or facade. Moving `ColumnRow` together with the predicate avoids `inspect → schema → inspect`, including a type-only cycle. Current audit found no static cycle through this file; the proposed leaf has no project dependency, so it cannot add one. Check import and re-export edges, not just runtime value imports. +- Coupling stays functional for the predicate and sequential for query rows. Memoization's temporal behavior stays within one owner; no shared mutable cache API is introduced. + +## Tests + +Direct importer list from `rg -l 'log-guard/inspect' tests` (2 files), both **unchanged**: + +- `tests/codex-integration/codex-log-guard-inspect.test.ts:17`. +- `tests/codex-integration/codex-log-guard-doctor.test.ts:4` (type import). + +Direct source-text oracle readers: **none found**. Search covered full/segmented Log Guard paths and basename `inspect.ts`, followed by read-site inspection. `001_stale_check.md`'s basename heuristic reports one, but `tests/codex-integration/native-grok-toggle.test.ts:343` actually reads `src/grok/inspect.ts`; leave it unchanged, do not retarget it to Log Guard. No `retarget-to-leaf` or explicit `add-leaf-to-scan-list` action is needed. Generic import-graph traversal naturally reaches the new leaf through the re-export/import; never change protected roots in `tests/lab/core-lab-boundary.test.ts`. + +Preserve runtime guards in `codex-log-guard-inspect.test.ts`: unknown schema `:189`, views `:209`, column metadata `:233`, table DDL `:267`, canonical indexes `:301`, unrelated triggers `:319`, zero writes `:173`, privacy `:139`, size gate `:120`, cache invalidation `:434`, cache reset `:460`, inode replacement `:471`. Preserve downstream protection's locked exact-schema check (`codex-log-guard-protection.test.ts:331`) and maintenance's schema refusal (`codex-log-guard-maintenance.test.ts:177`). No new test file/layout registration is required. + +During implementation C, drive the moved schema guard red once: temporarily bypass the canonical-index comparison in the leaf; `requires every canonical Codex logs index` at `:301` must fail. Restore precisely that temporary change, then run the focused set green. Do not alter fixture assertions or claim a red run in this docs-only task. + +## Verification + +Future implementation gate, **not executed during drafting**. Run at the layer tip in its dedicated worktree: + +```sh +bun run typecheck +bun test tests/codex-integration/codex-log-guard-*.test.ts tests/server/api-codex-log-guard*.test.ts +bun run privacy:scan +wc -l src/codex/log-guard/inspect-schema.ts src/codex/log-guard/inspect.ts +rg -l 'log-guard/inspect"|from "\./inspect"' src gui/src scripts tests +git diff --check +git diff --numstat dev...HEAD -- src/codex/log-guard +``` + +Focused domains are `codex-integration` and `server`; the wildcard is only the named Log Guard files, not a repository-wide suite. Baseline direct original-path importer count is **5 files** (3 source, 2 tests); retain the same importer set. Count files with `rg -l`, not physical import-block lines; `inspect-schema` is a different basename. Check the exact eight original exports and the leaf's one-way static edges. The conditional 002 core/Lab test is not triggered by this `src/codex`-only source change; if the scope expands into `src/server`, `src/router`, or `src/lib`, escalate and include that test without changing its roots. + +Full suite only on the designated remote host, after the parent publishes this exact branch: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-log-guard-inspect && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Record the remote checked-out SHA and actual test exit status/full result; a successful `tail` is not proof of a passing suite. Use pipeline-status preservation when executing. All checks must pass at the same PR head; collect the complete exact-head CI rollup. Do not run a full suite locally or rerun passing unchanged checks. + +## Accept criteria + +1. Source diff changes only the original file and `inspect-schema.ts`; declarations/bodies and comments move as specified, with only necessary module exports/imports added. +2. Actual new leaf and residual are each ≤400 lines; expected 136/392. Added+deleted source lines ≤500; no `#b` residual remains. +3. All 37 owned top-level declarations in the inventory have exactly one owner; all eight original exports resolve from `inspect.ts`; all five original-path consumer files are unchanged. +4. Exactly one `inspectionCache` and one `KNOWN_LOG_LEVELS` remain; the leaf imports neither `inspect.ts` nor another S12 entry. +5. The canonical-index guard has fresh red/restored-green evidence; focused tests, typecheck, privacy scan, remote full suite, and exact-head CI pass without weakening assertions. +6. PR base is `dev`, stack map/template sections are complete, and PR stays open/unmerged. No downstream layer is required for correctness. + +## PR + +Title: `refactor(codex): isolate log guard schema recognition (split S12 L1/3)` + +Branch: `codex/split-codex-log-guard-inspect`. Base: `dev`. Closes: none. + +Use `.github/PULL_REQUEST_TEMPLATE.md` with all **Summary**, **Verification**, and **Checklist** sections filled. Summary: exact schema extraction with unchanged inspection/mutation contract. Verification: actual commands, SHA-bound outcomes and guard evidence; never copy planned commands as completed. Scope remains pure move; no UI changes. + +| # | PR | Layer | Base | Review focus | +|---|---|---|---|---| +| 3 | # | codex/split-codex-log-guard-maintenance | codex/split-codex-log-guard-inspect | Compaction measurements | +| 2 | # | codex/split-codex-log-guard-protection | codex/split-codex-log-guard-inspect | Owned trigger SQL/observation | +| 1 | # | codex/split-codex-log-guard-inspect — this PR | dev | Exact schema recognition | + +Review this layer's diff only. This layer's only parent is `dev`; changes to `dev` require rebasing/reverifying this layer per DEV-STACK-02. Protection and maintenance are independent children of inspection under STACK-INDEPENDENCE-01, not a linear chain. Merge parents before children only after separate authorization; this train never merges. + +## P stale-check (2026-09-05, wp370) + +origin/dev 3c920af5f; inspect.ts unchanged since 445742966 (524 lines); anchors 18/67/127/135/246/320/483/485 confirmed by sed. Base `dev` (S12 bottom; 380/390 chain on it). Executor rules: no bun run test; OCX_TEST_NO_QUEUE=1; CI hygiene requires a test change (extend tests/codex-integration/codex-log-guard-inspect.test.ts with a seam identity + zero-back-edge guard). + +## A amendment (Tesla audit, GO-WITH-FIXES blockers=1 → folded) + +"Unchanged consumer files" (Tests, accept criterion) applies to existing imports and assertions and to the five original-path importers; the one authorized change is an appended test in tests/codex-integration/codex-log-guard-inspect.test.ts (hasCurrentLogsSchema identity facade vs leaf; leaf has no ./inspect import). Size gate: 003 PURE-MOVE-SIZE-01. Audit-verified: 37/37 ranges; leaf = exactly the 12 S declarations, only a bun:sqlite type import; residual uses exactly ColumnRow (:483) and hasCurrentLogsTable (:485); 8 exports preserved; 5 importers; red-drive :301 depends on the moved comparison at :310. + +## Execution record (B/C/D, 2026-09-05) + +- Executor worktree: `/tmp/ocx-split-370.DfVIYS/wt` (branch `codex/split-codex-log-guard-inspect`, base origin/dev 593978db0; inspect.ts identical to 3c920af5f). Executor: gpt-6-astra high (Halley, 01a06f96-ba8f-7460-a1ae-a8ae4d0abaf1). +- Commits: 247dc38d7 (move: inspect-schema.ts 137, inspect.ts 392) and 5c1a398da (test: codex-log-guard-inspect.test.ts +11 — hasCurrentLogsSchema identity; leaf has no ./inspect import). Diff: 3 files, +150/−134. 5 original-path importers unchanged; git diff --check clean. +- Local gate: typecheck 0; focused (inspect, doctor, protection, maintenance) 48 pass / 0 fail; core-lab-boundary 17/0; privacy passed. +- Red-drive: canonical-index comparison bypassed → 'requires every canonical Codex logs index' :314 fails (compatible vs unsupported), restored 1/0. + +- Adversarial diff review (Banach, gpt-6-astra high, 01a06f99-b6f2-79f3-9de7-ef3941e1d3fd): VERDICT: PASS (slices exact modulo separators, residual byte-exact, 8/8 exports, cache/levels single-owned at :128/:15, 3 files, test non-tautological). +- lidge full suite at 5c1a398da: SUITE_EXIT=1 — 18195 pass / **4 fail** / 16 skip. All four failures are **upstream dev breakage from 593978db0 (#3588 auto-activate quota reset windows)**, not this layer: management-route-registry ×3 (undeclared `GET /api/quota-resets` in src/server/management/quota-reset-routes.ts) and quota-reset-notify "a real rollover reaches a webhook". Reproduced identically on the pristine parent 593978db0 on lidge (same 4 fails, 0 in log-guard). This layer touches only src/codex/log-guard; its own focused suites are 48/0 and the layer diff cannot influence the route registry or quota notifier. + +- PR: https://github.com/lidge-jun/opencodex/pull/3599 (base dev, head 5c1a398da). CI rollup at record time: =1 =5 CANCELLED=1 SKIPPED=1 SUCCESS=2. Expectation: the `test` shards will show the same 4 upstream failures as dev@593978db0 until #3588's follow-up lands; re-read CI after that before stacking 380/390 on this branch. diff --git a/devlog/_plan/260905_now_split_train/380_codex_log_guard_protection.md b/devlog/_plan/260905_now_split_train/380_codex_log_guard_protection.md new file mode 100644 index 0000000000..eeb88156a2 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/380_codex_log_guard_protection.md @@ -0,0 +1,174 @@ +# S12 L2 — Codex Log Guard owned-trigger leaf + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. C3 boundary planning; this delegated task writes documentation only and does not own orchestration/goal state. +- Goal: reduce `src/codex/log-guard/protection.ts` from 489 to an expected 379 lines by moving trigger SQL and observation together, preserving status, mutation, and compensation behavior. +- Non-goals: no filter/policy changes, lock movement, transaction edits, type/signature renames, caller migration, dependency additions, or cleanup of currently unused declarations. Existing long functions remain intact under the pure-move constraint. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; implementation verification is future work, not performed during drafting. +- Stop: independently passing layer, open PR, exact-head CI/full-suite evidence and all accept criteria. Never merge. The drafting task stops after document verification. +- Escalation: upstream/export drift, a new cycle, any behavior change, >400-line output or >500 added+deleted source lines, or a required file outside this plan. Parent decides any revised partition or scope. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a38`. Line references are to this source, byte-compared with the working-tree file. Input audit: `../260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md`, this file's section, anchored at `protection.ts:296` and `:399`. + +## Symbol inventory + +Ranges: ast-grep top-level function/interface/type/lexical declarations, cross-checked with `rg`. Import-only bindings are dependencies listed below. Counts: distinct external files found by `rg -l -w '' src gui/src scripts tests`, excluding the owner and unrelated same-name bindings. Non-exported declarations have no external consumers; namesakes such as `ColumnRow`, `processRefusal`, or `CURRENT_LOG_COLUMNS` do not count. For the forwarded `CodexLogGuardMode`, count only consumers importing through this file (not directly through `policy.ts`). `R` = residual original; `T` = `src/codex/log-guard/protection-triggers.ts`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| CodexLogGuardMode | type re-export | 20–20 | yes | 1 | policy.ts, forwarded by R | +| IMMUTABLE_READONLY_FLAGS | const | 22–22 | no | 0 | R | +| COMPAT_TRIGGER | const | 23–23 | no | 0 | T | +| QUIET_TRIGGER | const | 24–24 | no | 0 | T | +| OWNED_TRIGGER_NAMES | const array | 25–25 | no | 0 | T | +| CURRENT_LOG_COLUMNS | const array | 27–40 | no | 0 | R | +| targetOrDescendant | function | 73–76 | no | 0 | T | +| anyTargetOrDescendant | function | 78–80 | no | 0 | T | +| COMPAT_TRIGGER_SQL | const | 82–101 | no | 0 | T | +| QUIET_TRIGGER_SQL | const | 103–108 | no | 0 | T | +| SQL_BY_MODE | const object | 110–113 | no | 0 | T | +| CodexLogGuardObservedMode | type | 115–115 | yes | 0 | T | +| CodexLogGuardProtectionState | type | 116–116 | yes | 0 | R | +| CodexLogGuardProtectionSummary | interface | 118–122 | yes | 0 | R | +| CodexLogGuardStatus | type | 124–126 | yes | 4 | R | +| CodexLogGuardMutationError | type | 128–136 | yes | 0 | R | +| CodexLogGuardMutationResult | type | 138–140 | yes | 1 | R | +| CodexLogGuardProtectionDeps | interface | 142–152 | yes | 2 | R | +| TriggerRow | interface | 154–157 | no | 0 | T | +| ColumnRow | interface | 158–158 | no | 0 | R | +| OwnedTriggerSnapshot | interface | 159–159 | no | 0 | R | +| LockedMutationResult | type | 161–163 | no | 0 | R | +| normalizeSql | function | 165–167 | no | 0 | T | +| expectedSql | function | 169–171 | no | 0 | T | +| ownedModeForRow | function | 173–177 | no | 0 | T | +| queryReservedTriggers | function | 179–184 | no | 0 | T | +| observeTriggers | function | 186–193 | no | 0 | T | +| exactCurrentSchema | function | 195–201 | no | 0 | R | +| openReadOnly | function | 208–211 | no | 0 | R | +| openReadWrite | function | 213–217 | no | 0 | R | +| databasePathIsSafe | function | 219–227 | no | 0 | R | +| protectionSummary | function | 229–245 | no | 0 | R | +| inspectionDeps | function | 247–249 | no | 0 | R | +| getCodexLogGuardProtectionStatus | function | 251–274 | yes | 5 | R | +| successfulMutationStatus | function | 276–288 | no | 0 | R | +| processRefusal | function | 290–294 | no | 0 | R | +| mutateOwnedTrigger | function | 296–350 | no | 0 | R | +| restoreOwnedTriggers | function | 352–397 | no | 0 | R | +| performMutation | function | 399–468 | no | 0 | R | +| protectCodexLogs | function | 470–475 | yes | 3 | R | +| unprotectCodexLogs | function | 477–481 | yes | 3 | R | +| repairCodexLogGuardProtection | function | 483–489 | yes | 2 | R | + +## Leaf partition + +Structural map: doctor `src/cli/codex-log-guard-doctor.ts:5`, management `context.ts:3` and `storage-log-guard-routes.ts:13`, plus six test files → `protection.ts` → paths, inspection, lock, path-safety, SQLite error classifier, policy, processes. Intended edge: same callers → residual `protection.ts` → `protection-triggers.ts`; the leaf depends only on SQLite and policy **types**, not on the inspector or mutation owner. The residual continues importing the L1-compatible `./inspect` path. Blast radius: local feature. + +Decision: extract one cohesive canonical trigger definition/recognition owner. No-op/configure cannot solve size; deletion changes behavior; reuse the existing SQL/predicates rather than inventing another trigger API. Reject moving lock-scoped mutation/rollback because that increases transaction-state coupling. Reject exporting only SQL while leaving normalization elsewhere because ownership checks and compensation must use the same definitions. The leaf's several internal exports are needed by existing production call sites, not solely for testing. + +Use the existing same-directory, purpose-qualified convention: `path-safety.ts`, `sqlite-errors.ts`, and `src/server/responses/agent-task-recovery-cache.ts`. `rg --files` found no existing `protection-triggers.ts` owner. + +- New `src/codex/log-guard/protection-triggers.ts`, expected **115 lines**, all `T` symbols above. Move blocks **23–26, 42–115, 154–157, 165–194** (4 + 74 + 4 + 30 = **112 lines**), retaining policy comments and SQL byte contents. Add two imports and one blank line: + + ```ts + import type { Database } from "bun:sqlite"; + import type { CodexLogGuardMode } from "./policy"; + ``` + + Export `SQL_BY_MODE`, `normalizeSql`, `ownedModeForRow`, `queryReservedTriggers`, `observeTriggers`, and the already-exported `CodexLogGuardObservedMode`. `TriggerRow` remains leaf-private; callers infer query results, while `OwnedTriggerSnapshot` remains the structurally compatible residual type. Other `T` symbols stay private. + +- Residual `src/codex/log-guard/protection.ts`, expected **379 lines** = 489 − 112 + 2 new binding/re-export lines. All `R` symbols remain. Keep its existing filesystem/URL/SQLite, paths, inspect, lock, path-safety, sqlite-errors, policy, and processes imports. No `#b` layer is needed. + +Total 494 = 489 original + 5 import/re-export/blank lines. Expected source diff 112 removed + 117 added = **229**, below 500. Multiline formatting is allowed only while actual line/diff limits stay satisfied. + +## Re-export block + +The original path retains this existing policy forwarding (no duplicate line) and gains only the observed-mode forwarding plus a local import: + +```ts +export type { CodexLogGuardMode } from "./policy"; +export type { CodexLogGuardObservedMode } from "./protection-triggers"; +import { SQL_BY_MODE, normalizeSql, ownedModeForRow, queryReservedTriggers, observeTriggers, type CodexLogGuardObservedMode } from "./protection-triggers"; +``` + +The first statement is equivalent to original line 20's `export { type CodexLogGuardMode } from "./policy"`; retaining that spelling also preserves the count. The existing local `CodexLogGuardMode` type import in lines 10–14 remains mandatory. The new import binds observed-mode references, status observation at `:263`, mutation at `:318`–336, and compensation at `:369`–385; the type re-export alone cannot do that. + +Keep local exported declarations for `CodexLogGuardProtectionState`, `CodexLogGuardProtectionSummary`, `CodexLogGuardStatus`, `CodexLogGuardMutationError`, `CodexLogGuardMutationResult`, `CodexLogGuardProtectionDeps`, `getCodexLogGuardProtectionStatus`, `protectCodexLogs`, `unprotectCodexLogs`, and `repairCodexLogGuardProtection`. Preserve the original twelve-name export set (eight types including forwarded mode, four functions); do not forward new internal helper exports through the original path. + +## Module-level state and cycles + +- No top-level `let`, Map, Set, WeakMap, lock, or in-flight mutable state is created in this file. `OWNED_TRIGGER_NAMES` at `:25` and `SQL_BY_MODE` at `:110` move as single-owner lookup constants to the leaf. `COMPAT_TRIGGER`, `QUIET_TRIGGER`, both SQL strings, and their construction helpers move together in original initialization order. +- `CURRENT_LOG_COLUMNS` (`:27`) stays residual even though unused; deleting or merging its namesake in maintenance is not this pure move. Read-only flags at `:22` also stay residual. +- `unique` Set at `:191` moves with `observeTriggers` but remains call-local. The compensation Map at `:383`, DB handles and `transactionOpen` at `:300`/`:301` and `:356`/`:357`, and `locked`/`effectiveMode` at `:424`/`:429` stay per invocation in the residual. +- The lock remains owned by existing `lock.ts` and invoked by `performMutation`; hold it through trigger commit, desired-state write, and compensation. Preserve repair's mode resolution inside that same lock (`:431`–448). +- Keeping `CodexLogGuardObservedMode` in the residual while the leaf imported it would create a type-only `protection ↔ triggers` cycle. Move its definition to the leaf and re-export it. Leaf types depend on `policy.ts`, which does not import protection; no leaf → residual edge is allowed. Inspection remains downward through the stable L1 path; no maintenance edge is added. +- SQL/observation coupling is functional with one canonical definition owner. Transaction/config-write temporal coupling deliberately stays colocated; no common mutable state is introduced. No error handling or filesystem validation changes accompany the move. + +## Tests + +`rg -l 'log-guard/protection' tests` returns these six files, each **unchanged**: + +- `tests/codex-integration/codex-log-guard-protection.test.ts:12`. +- `tests/codex-integration/codex-log-guard-coderabbit.test.ts:13`. +- `tests/codex-integration/codex-log-guard-status-zero-write.test.ts:7`. +- `tests/codex-integration/codex-log-guard-doctor-coderabbit.test.ts:4`. +- `tests/codex-integration/codex-log-guard-doctor-protection.test.ts:4`. +- `tests/server/api-codex-log-guard-protection.test.ts:7`. + +Direct source-text oracle readers: **none found** after basename/full/segmented path searches and read-site filtering. `readFileSync` in `codex-log-guard-status-zero-write.test.ts:86` reads a fixture WAL, not protection source; keep it unchanged. There are no `retarget-to-leaf` or explicit `add-leaf-to-scan-list` actions. Graph scans follow imports automatically; do not edit protected Lab roots. No new test files or layout entries are planned. + +Keep the protection tests for compat filters (`:110`), descendants (`:134`), Repair/Disable ordering (`:164`), quiet mode (`:194`), collisions (`:236`), drift (`:251`), selective removal (`:288`), rollback (`:301`), disable after schema change (`:311`), locked schema recheck (`:331`). Preserve multi-trigger compensation in `codex-log-guard-coderabbit.test.ts:185`, multi-trigger unprotect at `:164`, and both zero-write status tests (`codex-log-guard-status-zero-write.test.ts:53`, `:74`). + +C-phase guard to drive red once: temporarily make the moved `targetOrDescendant` match only the exact target; the unchanged descendant-filter test at `codex-log-guard-protection.test.ts:134` must fail. Restore the temporary change and run the full focused set green. No such mutation or test run belongs to this drafting task. + +## Verification + +Future implementation, not executed here; dedicated layer worktree and exact tip: + +```sh +bun run typecheck +bun test tests/codex-integration/codex-log-guard-*.test.ts tests/server/api-codex-log-guard*.test.ts +bun run privacy:scan +wc -l src/codex/log-guard/protection-triggers.ts src/codex/log-guard/protection.ts +rg -l 'log-guard/protection"' src gui/src scripts tests +git diff --check +git diff --numstat codex/split-codex-log-guard-inspect...HEAD -- src/codex/log-guard +``` + +Domains: `codex-integration` and `server`, limited to Log Guard paths. Original-path importer baseline: **9 files** = 3 source + 6 tests. Match the exact original basename so `protection-triggers` does not inflate the count. Verify all twelve old exports, not merely the value exports. Inspect import/re-export edges to confirm no leaf → protection path, including type edges. No server/router/lib implementation file is touched, so 002's conditional core/Lab test is not triggered; scope expansion requires escalation and that guard, with protected roots unchanged. + +Remote full-suite command, parent-owned execution after publishing: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-log-guard-protection && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Capture checked-out SHA and actual test exit status, preserving pipeline status; `tail` success is not a test result. Require a green complete exact-head CI rollup and remote full suite on that SHA. Never run full tests locally or defer this layer's checks to L3. + +## Accept criteria + +1. Only `protection.ts` and `protection-triggers.ts` change in the source diff; all 41 local declarations plus the existing policy type forwarding are accounted for. +2. New leaf/residual each ≤400 lines (expected 115/379), total source additions+deletions ≤500; no residual `#b` work remains. +3. All twelve original exports resolve from the old path; all nine original importer files remain unchanged. Leaf helpers are not newly re-exported through that path. +4. Trigger SQL, normalization, ownership detection, SQL construction order and selective deletion/compensation are unchanged. There is no duplicate SQL_BY_MODE owner or new cycle. +5. Fresh descendant-guard red/restored-green evidence and all per-layer typecheck, focused, privacy, remote-full-suite and exact-head CI gates pass. +6. PR base is the L1 branch, latest lower-layer commit is contained, all template sections/stack map are present, and the independently valid PR remains open/unmerged. + +## PR + +Title: `refactor(codex): isolate owned log guard trigger definitions (split S12 L2/3)` + +Branch: `codex/split-codex-log-guard-protection`. Base: `codex/split-codex-log-guard-inspect`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` **Summary**, **Verification**, and **Checklist**. Summary names the pure trigger extraction and preserved lock/rollback contract. Verification records actual SHA-bound outcomes, not this planned command list. Keep the security-sensitive checklist review explicit for unchanged SQL/mutation boundaries; no UI changes. + +| # | PR | Layer | Base | Review focus | +|---|---|---|---|---| +| 3 | # | codex/split-codex-log-guard-maintenance | codex/split-codex-log-guard-inspect | Compaction measurements | +| 2 | # | codex/split-codex-log-guard-protection — this PR | codex/split-codex-log-guard-inspect | Owned trigger SQL/observation | +| 1 | # | codex/split-codex-log-guard-inspect | dev | Exact schema recognition | + +Depends on #. Review this layer's diff only. Cascade/reverify this layer when its real parent `codex/split-codex-log-guard-inspect` changes (DEV-STACK-02). Maintenance is an independent sibling, not a child of protection (STACK-INDEPENDENCE-01). Merge parents before children only after separate authorization; this train never merges. diff --git a/devlog/_plan/260905_now_split_train/390_codex_log_guard_maintenance.md b/devlog/_plan/260905_now_split_train/390_codex_log_guard_maintenance.md new file mode 100644 index 0000000000..ce7aed0e88 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/390_codex_log_guard_maintenance.md @@ -0,0 +1,151 @@ +# S12 L3 — Codex Log Guard compaction measurement leaf + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. C3 structural planning, documentation-only delegated execution; no parent orchestration/goal commands. +- Goal: reduce `src/codex/log-guard/maintenance.ts` from 403 to an expected 329 lines by extracting SQLite measurements/checkpoint checks while keeping the admission and compaction loop together. +- Non-goals: no budget changes, reclaim/report semantics changes, lock/transaction movement, file-identity changes, new dependencies, dead-code cleanup, or caller/signature changes. Existing long functions remain intact under the pure-move constraint. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. All runtime checks are future implementation work, not run while drafting. +- Stop: standalone valid layer with open PR, exact-head CI/full-suite evidence and all accept criteria; never merge. This delegated task ends once this document is checked. +- Escalation: source drift, >400-line output, >500 added+deleted source lines, cycle, behavior change, export change, or necessary file outside the stated plan; parent must revise scope first. + +Basis: docs HEAD `4cc219549`; source `origin/dev = 1362b1a38`. Line references are from the real source, byte-compared with the working tree. Input lane audit: `../260905_modular_debt_ledger/013_lane_providers_codex_oauth_routing.md`, maintenance section (`maintenance.ts:223` and `:367`). + +## Symbol inventory + +Ranges use ast-grep top-level declarations and `rg` cross-checks. Imported bindings are dependencies, listed below, not locally owned declarations. Counts are distinct external files from `rg -l -w '' src gui/src scripts tests`, excluding the owner and unrelated same-name bindings. Private helpers such as `measure`, `runCompaction`, and `pragmaNumber` have zero external binding consumers despite lexical namesakes elsewhere. `R` = residual original file; `M` = `src/codex/log-guard/maintenance-measure.ts`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| CURRENT_LOG_COLUMNS | const array | 12–25 | no | 0 | R | +| DEFAULT_BATCH_BYTES | const | 34–34 | no | 0 | R | +| DEFAULT_MAX_BYTES_PER_RUN | const | 35–35 | no | 0 | R | +| MAX_ITERATIONS | const | 36–36 | no | 0 | R | +| pagesForBytes | function | 39–42 | no | 0 | R | +| CompactStopReason | type | 44–44 | no | 0 | R | +| CodexLogGuardCompactionMeasure | interface | 46–53 | yes | 0 | M | +| CodexLogGuardCompactionReport | interface | 55–73 | yes | 0 | R | +| CodexLogGuardCompactionError | type | 75–83 | yes | 0 | R | +| CodexLogGuardCompactionResult | type | 85–91 | yes | 1 | R | +| CodexLogGuardMaintenanceDeps | interface | 93–105 | yes | 3 | R | +| ColumnRow | interface | 107–107 | no | 0 | R | +| CheckpointRow | interface | 108–112 | no | 0 | M | +| DatabaseFileIdentity | interface | 114–118 | no | 0 | R | +| databasePathIdentity | function | 120–130 | no | 0 | R | +| databasePathIsSafe | function | 132–134 | no | 0 | R | +| databasePathStillMatches | function | 136–145 | no | 0 | R | +| exactCurrentSchema | function | 147–152 | no | 0 | R | +| pragmaNumber | function | 154–160 | no | 0 | M | +| defaultQuickCheck | function | 162–167 | no | 0 | M | +| quickCheckIsOk | function | 169–171 | no | 0 | M | +| processRefusal | function | 173–179 | no | 0 | R | +| checkpointFull | function | 181–193 | no | 0 | M | +| measure | function | 195–221 | no | 0 | M | +| runCompaction | function | 223–365 | no | 0 | R | +| compactCodexLogs | function | 367–403 | yes | 3 | R | + +## Leaf partition + +Structural map: `src/server/management/context.ts:4`, `storage-log-guard-routes.ts:5`, and three test files below → `maintenance.ts` → paths, user-identity, inspect, lock, path-safety, sqlite-errors, processes, filesystem and SQLite. Intended edge: same consumers → residual `maintenance.ts` → `maintenance-measure.ts` → filesystem and SQLite type. The residual continues using the L1-compatible `./inspect` path. Blast radius: local Log Guard feature; no API change. + +Decision: extract the measurement/checkpoint sub-seam of the audited compaction loop, not the whole transaction. Reject no-op/configuration because they do not reduce size; reject deleting the unused column declarations just to cross 400 because that is cleanup, not the agreed split. Reuse existing helper bodies. Reject moving `runCompaction` alone because it would require a much larger dependency/type boundary and more churn. This split moves the lowest-consumer helpers and one unconsumed public measure type first; no later part is required. + +Naming uses the existing same-directory purpose-qualified convention (`path-safety.ts`, `sqlite-errors.ts`) and parallels `src/server/responses/agent-task-recovery-cache.ts`. `rg --files` found no `maintenance-measure.ts`; keep the measurement-specific `pragmaNumber` separate from inspection's namesake because their error behavior and signatures differ. + +- New `src/codex/log-guard/maintenance-measure.ts`, expected **79 lines**: all `M` symbols. Move inclusive blocks **46–54, 108–113, 154–172, 181–222**, including blanks/comments: 9 + 6 + 19 + 42 = **76 moved lines**. Add these two imports and one blank line: + + ```ts + import { statSync } from "node:fs"; + import type { Database } from "bun:sqlite"; + ``` + + Export existing `CodexLogGuardCompactionMeasure` and the production-used helpers `pragmaNumber`, `defaultQuickCheck`, `quickCheckIsOk`, `checkpointFull`, `measure`. `CheckpointRow` remains private. + +- Residual `src/codex/log-guard/maintenance.ts`, expected **329 lines** = 403 − 76 + 2 new import/re-export lines. All `R` symbols remain. Remove only `statSync` from its first import (retain `lstatSync`, `realpathSync`). Keep `Database`, `sqliteConstants`, paths, user-identity, inspect, lock and lock type, path-safety, sqlite-errors, processes and process type imports. No `#b` follows. + +Total 408 = 403 original + 5 binding/import/blank lines. Source diff estimate including the changed filesystem import is 77 removed + 82 added = **159**, below 500. Formatting may vary only within the actual line/diff limits. + +## Re-export block + +Add these exact one-line statements to the original module: + +```ts +export type { CodexLogGuardCompactionMeasure } from "./maintenance-measure"; +import { pragmaNumber, defaultQuickCheck, quickCheckIsOk, checkpointFull, measure, type CodexLogGuardCompactionMeasure } from "./maintenance-measure"; +``` + +The local measure type binds the residual report interface at original lines 57–58; the helper imports bind the existing calls in `runCompaction`. Re-export alone binds neither. Keep local exported declarations for `CodexLogGuardCompactionReport`, `CodexLogGuardCompactionError`, `CodexLogGuardCompactionResult`, `CodexLogGuardMaintenanceDeps`, and `compactCodexLogs`. Original export set stays five types/interfaces plus one function. Do not re-export the newly leaf-visible helpers from the old path. + +## Module-level state and cycles + +- No top-level mutable `let`, Map, Set, WeakMap, lock, DB handle or in-flight owner exists in maintenance. `CURRENT_LOG_COLUMNS` at `:12` is an existing read-only tuple retained even though unused; no deduplication with protection's namesake. +- `DEFAULT_BATCH_BYTES` (`:34`), `DEFAULT_MAX_BYTES_PER_RUN` (`:35`), and `MAX_ITERATIONS` (`:36`) stay in the residual with `pagesForBytes` and the loop. Byte budgets and iteration count are unchanged. +- `db`, `probeOpen`, `reportBusyPartial` (`:227`–229), counters (`:271`–274), `finish` (`:276`) and `locked` (`:383`) remain call-local in the residual, not moved into module state. Existing `lock.ts` is the sole lock owner. Keep process rechecks before and inside the lock and the identity recheck immediately after open (`:231`–240). +- Leaf owns `CodexLogGuardCompactionMeasure` and `CheckpointRow`. Importing the measure type back from `maintenance.ts` would form a type-only cycle; move it with the helpers and re-export instead. The leaf has no project dependency, so it cannot add a project cycle. No leaf → maintenance/inspect/protection edge, no dynamic import workaround. +- Measurement is functional/sequential coupling. Temporal ordering remains in `runCompaction`: schema/auto-vacuum admission, quick-check, write-lock probe, FULL checkpoint, bounded vacuum batches, final measurement/quick-check. `checkpointFull` can write checkpoint state but is called at exactly the original sites with the original handle; moving its definition must not move execution or resource ownership. + +## Tests + +`rg -l 'log-guard/maintenance' tests` returns these three files, all **unchanged**: + +- `tests/codex-integration/codex-log-guard-maintenance-coderabbit.test.ts:16` (static import). +- `tests/codex-integration/codex-log-guard-maintenance.test.ts` (dynamic imports at `:125`, `:149`, `:162`, `:178`, `:194`, `:211`, `:221`, `:241`, `:259`, `:276`, `:294`; preserve all). +- `tests/server/api-codex-log-guard-compact.test.ts:7` (type import). + +Direct source-text oracle readers: **none found** by basename/full/segmented path search plus read-site filtering. Dynamic import path pins above execute the module rather than reading its source; unchanged original-path exports satisfy them. No `retarget-to-leaf` or explicit `add-leaf-to-scan-list` action is needed. Generic import-graph scans reach the leaf automatically. No new test file/layout registration; protected Lab roots remain untouched. + +Preserve runtime guards in `codex-log-guard-maintenance.test.ts`: row/trigger preservation `:124`, no-op `:148`, incremental-only `:161`, unknown schema `:177`, process/lock refusals `:193`/`:210`, pre/post quick checks `:220`/`:240`, per-run budget `:258`, real-page-size budget `:275`, logical vs physical reporting `:293`. Preserve coderabbit regressions for replacement identity `:97`, second process check `:115`, iteration budget `:131`, initial busy checkpoint `:147`, committed-batch partial success `:174`, thrown busy partial success `:221`. + +C-phase red guard: temporarily make the moved `quickCheckIsOk` always return true. The unchanged pre-maintenance quick-check test at `codex-log-guard-maintenance.test.ts:220` must fail. Restore the exact temporary change and run focused checks green. Do not execute tests or mutations during this documentation task. + +## Verification + +Future implementation in the dedicated layer worktree, not run here: + +```sh +bun run typecheck +bun test tests/codex-integration/codex-log-guard-*.test.ts tests/server/api-codex-log-guard*.test.ts +bun run privacy:scan +wc -l src/codex/log-guard/maintenance-measure.ts src/codex/log-guard/maintenance.ts +rg -l 'log-guard/maintenance"' src gui/src scripts tests +git diff --check +git diff --numstat codex/split-codex-log-guard-inspect...HEAD -- src/codex/log-guard +``` + +Focused domains `codex-integration` and `server`, bounded to Log Guard filenames. Baseline **5 direct importer files** = 2 source + 3 tests, counting dynamic imports once per file; retain the same set and all six original exports. Match the original basename exactly so `maintenance-measure` is not mistaken for an old-path consumer. Inspect all import/re-export edges: the leaf has only built-in imports and no project return path. With only `src/codex` source touched, 002's conditional server/router/lib core-Lab test is not triggered; an expansion triggers escalation and that test without root edits. + +Remote full suite after parent publication, never locally: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-codex-log-guard-maintenance && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Record checked-out SHA and real `bun run test` exit status with pipeline-status preservation; success of `tail` alone is insufficient. Require remote full suite and complete exact-head CI rollup green on the PR head. This layer is independently gated even though it is the stack top; do not rerun passing checks on unchanged code. + +## Accept criteria + +1. Source diff is limited to `maintenance.ts` and `maintenance-measure.ts`; all 26 declarations are assigned exactly once and bodies/signatures are unchanged apart from necessary module exports/imports. +2. Leaf/residual each ≤400 lines (expected 79/329); source diff additions+deletions ≤500; no `#b` layer or over-400 residual remains. +3. All six existing exports remain importable from maintenance; all five original-path consumer files and eleven dynamic import sites remain unchanged. +4. No duplicated type, singleton or import cycle. Database/lock ownership, checkpoint execution order, identity checks, budgets, partial-success/error reports and quick-check behavior are preserved. +5. Quick-check guard has fresh red/restored-green proof; typecheck, focused tests, privacy scan, remote full suite and complete exact-head CI pass. +6. PR is based on L2, contains the current parent commit, includes template/stack map evidence, stands alone for review, and remains open/unmerged. + +## PR + +Title: `refactor(codex): isolate log guard compaction measurements (split S12 L3/3)` + +Branch: `codex/split-codex-log-guard-maintenance`. Base: `codex/split-codex-log-guard-inspect`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` **Summary**, **Verification**, **Checklist**. Explain the measurement extraction and unchanged compaction/admission semantics; report actual SHA-bound checks and guard evidence, not planned work as completed. Explicitly review the unchanged checkpoint/foreign-database boundary in the checklist. No UI changes. + +| # | PR | Layer | Base | Review focus | +|---|---|---|---|---| +| 3 | # | codex/split-codex-log-guard-maintenance — this PR | codex/split-codex-log-guard-inspect | Compaction measurements | +| 2 | # | codex/split-codex-log-guard-protection | codex/split-codex-log-guard-inspect | Owned trigger SQL/observation | +| 1 | # | codex/split-codex-log-guard-inspect | dev | Exact schema recognition | + +Depends on #. Review this layer's diff only. Cascade/reverify this layer when its real parent `codex/split-codex-log-guard-inspect` changes (DEV-STACK-02). Protection is an independent sibling, not this layer's parent (STACK-INDEPENDENCE-01). Merge parents before children only after separate authorization; this train never merges. diff --git a/devlog/_plan/260905_now_split_train/400_clients_config_export_a.md b/devlog/_plan/260905_now_split_train/400_clients_config_export_a.md new file mode 100644 index 0000000000..acc2755308 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/400_clients_config_export_a.md @@ -0,0 +1,544 @@ +# 400 — S13 L1/5: extract low-fanout client formats and dependency foundations + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Existing split implementation history; aggregate delivery pending. Original PR is not individually merged. Current/latest below means at that historical checkpoint; older blocked/pending snapshots do not override bbf8d3cd or authorize resumption. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`, C3 implementation with explicit security regression review for the relocated admission/credential helpers. Main owns orchestration and goal state. +- Goal: extract low-fanout client formats and dependency foundations, preserving the original public import path and behavior. +- Non-goals: behavior fixes, exported renames, signature changes, new validation, changed credentials/admission policy, changed config paths, new framework, caller migration, merges or releases. Preserve function bodies verbatim, including >50-line functions; function redesign is not this pure-move train. +- Verifier: this document's authoritative Verification section; every layer must pass independently at its actual tip. All tests run on `ssh lidge`, never locally. +- Stop: exact-tip acceptance evidence and CI recorded, then close the work phase; do not merge. The earlier docs-only drafting pass is historical. +- Size gate: the binding `003_parent_decisions.md` PURE-MOVE-SIZE-01 resolves the original 500-line churn conflict. Non-move changes must stay **≤150 lines**, with move-aware diff review and unique-owner evidence for every inventory symbol. Raw added+deleted churn is not claimed to meet 500. Stale source, a leaf >400, any new cycle, any behavioral difference, or non-move changes above the bound stop implementation. + +Inventory basis (historical): task docs HEAD `4cc219549`; code `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. The drafting pass read 000, 001, S13 rows/Per-layer gate of 002, and the relevant records in `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md`. Source was read with `git show origin/dev:`; `git diff origin/dev -- src/clients/config-export.ts src/cli/opencode.ts src/cli/minimax.ts src/integrations/state.ts` was empty. Current execution uses the dev base in the PR section; the inventoried source bytes were checked unchanged. + +Structural decision (cxc-dev §1/§5, architecture ARCH-MAP-01/ARCH-DECISION-01): 1990 lines mix distinct concerns. Reject deleting/configuring the feature (does not preserve behavior), and generic helpers/index barrels (do not establish ownership). Reuse every existing algorithm and lower-level dependency; only relocate declarations. Inspected conventions: `src/config/paths.ts`, `src/config/process-state.ts`, `src/cli/launcher-context.ts`, `src/cli/account-extended.ts`, `src/integrations/ownership-policy.ts`. Use the domain subfolder `src/clients/config-export/` without an index barrel. The original remains an existing compatibility boundary, not an internal import shortcut. + +Structural map: 33 direct source/test/fixture consumer files. Production dependents: `src/integrations/state.ts`, `src/integrations/ownership.ts`, `src/integrations/merge.ts`, `src/integrations/registry.ts`, `src/integrations/owned-refresh.ts`, `src/integrations/config-io.ts`, `src/integrations/ownership-policy.ts`, `src/integrations/writer.ts`, `src/server/management/model-routes.ts`, `src/server/management/model-rows.ts`, `src/cli/export-command.ts`, `src/cli/minimax.ts`, `src/cli/opencode.ts`. Current direction is dependents → original → existing imported owners; intended direction is dependents → original → concern leaves → existing owners. Leaf imports are fully enumerated below; no leaf → original edge. Blast radius: client/CLI integration feature, with public consumers unchanged. `structure/09_client-integrations.md:11` identifies builders and classification as single authorities; no parallel implementation is introduced. + +## Symbol inventory + +Exact syntax spans at `origin/dev:src/clients/config-export.ts` (leading comments excluded). Reproduce: `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration,variable_declaration,class_declaration' --json=compact src/clients/config-export.ts`, filtering declarations enclosed by another declaration. Consumers = distinct direct importer/re-exporter files per symbol, resolved by literal module path then counted with `rg -l -w '' `. Dynamic dispatch destructuring counts too. Private declarations have 0 external consumers, not 0 local calls. Imported bindings are covered by the leaf imports; export-only declarations are noted below. L2 repeats the complete basis inventory and marks L1-owned rows already moved. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ManagedFragment` | interface | 43–46 | yes | 2 | `src/clients/config-export/contracts.ts` (L1) | +| `ManagedContribution` | interface | 49–52 | yes | 5 | `src/clients/config-export/contracts.ts` (L1) | +| `BuildContribution` | type | 54–54 | yes | 0 | `src/clients/config-export/contracts.ts` (L1) | +| `OpencodeLaunchEnv` | interface | 56–58 | yes | 1 | `src/clients/config-export/contracts.ts` (L1) | +| `OpencodeCatalogModel` | interface | 61–76 | yes | 1 | `src/clients/config-export/contracts.ts` (L1) | +| `OpencodeModelEntry` | interface | 78–81 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeModelVariant` | interface | 90–93 | yes | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeV2ModelEntry` | interface | 95–97 | yes | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeProviderConnection` | interface | 100–104 | yes | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeProviderBlock` | interface | 107–112 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeV2ProviderBlock` | interface | 115–120 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeProviderBlocks` | interface | 127–130 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OpencodeGeneratedConfig` | interface | 132–138 | yes | 4 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OPENCODE_PROVIDER_ID` | const | 141–141 | yes | 11 | `src/clients/config-export/constants.ts` (L1) | +| `OPENCODE_CONFIG_SCHEMA` | const | 143–143 | yes | 2 | `src/clients/config-export/constants.ts` (L1) | +| `OPENCODE_PROVIDER_NPM` | const | 149–149 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OPENCODE_V2_PROVIDER_PACKAGE` | const | 161–161 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OPENCODE_PROVIDER_NAME` | const | 164–164 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `OPENCODE_API_KEY_ENV` | const | 171–171 | yes | 3 | `src/clients/config-export/constants.ts` (L1) | +| `OPENCODE_API_KEY_ENV_REF` | const | 174–174 | yes | 2 | `src/clients/config-export/constants.ts` (L1) | +| `HERMES_API_KEY_ENV` | const | 180–180 | yes | 0 | `src/clients/config-export/constants.ts` (L1) | +| `HERMES_API_KEY_ENV_REF` | const | 181–181 | yes | 2 | `src/clients/config-export/constants.ts` (L1) | +| `OPENCLAW_API_KEY_ENV` | const | 184–184 | yes | 0 | `src/clients/config-export/constants.ts` (L1) | +| `OPENCLAW_API_KEY_ENV_REF` | const | 185–185 | yes | 2 | `src/clients/config-export/constants.ts` (L1) | +| `LOOPBACK_API_KEY_PLACEHOLDER` | const | 193–193 | yes | 9 | `src/clients/config-export/constants.ts` (L1) | +| `GAJAE_API_KEY_ENV` | const | 200–200 | yes | 2 | `src/clients/config-export/constants.ts` (L1) | +| `PI_API_DIALECT` | const | 203–203 | no | 0 | `src/clients/config-export/constants.ts` (L1) | +| `SCHEMA_REQUIRED_OUTPUT_BUDGET` | const | 217–217 | yes | 2 | `src/clients/config-export/constants.ts` (L1) | +| `OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG` | const | 220–225 | yes | 1 | `src/clients/config-export/constants.ts` (L1) | +| `opencodeGlobalConfigPath` | function | 231–237 | yes | 3 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `OMP_PROFILE_NAME_RE` | const | 239–239 | no | 0 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `OMP_WINDOWS_RESERVED_PROFILE_RE` | const | 240–240 | no | 0 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `ompProfileName` | function | 242–258 | no | 0 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `piAgentDir` | function | 270–274 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `piConfigPath` | function | 277–279 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `ompAgentDir` | function | 282–293 | yes | 1 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `ompModelsConfigPath` | function | 296–301 | yes | 4 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `opencodeProxyBaseUrl` | function | 304–316 | yes | 4 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `hermesHomeDir` | function | 322–330 | yes | 1 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `hermesConfigPath` | function | 332–334 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `ClientPathError` | class | 350–350 | yes | 12 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `absoluteClientPath` | function | 352–363 | no | 0 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `openclawEffectiveHome` | function | 372–375 | no | 0 | `src/clients/config-export/openclaw-paths.ts` (L2; deferred) | +| `openclawHomeDir` | function | 393–413 | yes | 2 | `src/clients/config-export/openclaw-paths.ts` (L2; deferred) | +| `openclawConfigPath` | function | 427–457 | yes | 2 | `src/clients/config-export/openclaw-paths.ts` (L2; deferred) | +| `kimiHomeDir` | function | 459–462 | yes | 1 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `kimiConfigPath` | function | 464–466 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `gajaeHomeDir` | function | 468–470 | yes | 1 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `gajaeConfigPath` | function | 472–474 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `dshHomeDir` | function | 477–492 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `dshConfigPath` | function | 494–496 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `mcodeHomeDir` | function | 503–509 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `mcodeConfigPath` | function | 511–513 | yes | 3 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `zcodeHomeDir` | function | 521–525 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `zcodeConfigPath` | function | 527–529 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `primeAgentDir` | function | 540–544 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `primeConfigPath` | function | 547–549 | yes | 2 | `src/clients/config-export/paths.ts` (L2; deferred) | +| `asideHomeDir` | function | 558–560 | yes | 1 | `src/clients/config-export/aside-paths.ts` (L2; deferred) | +| `asideCurrentAccountId` | function | 584–612 | no | 0 | `src/clients/config-export/aside-paths.ts` (L2; deferred) | +| `asideAccountDir` | function | 619–622 | yes | 2 | `src/clients/config-export/aside-paths.ts` (L2; deferred) | +| `asideConfigPath` | function | 625–627 | yes | 2 | `src/clients/config-export/aside-paths.ts` (L2; deferred) | +| `ExportModel` | interface | 634–647 | yes | 18 | `src/clients/config-export/contracts.ts` (L1) | +| `ExportContext` | interface | 649–658 | yes | 8 | `src/clients/config-export/contracts.ts` (L1) | +| `ExportClientId` | type | 660–672 | yes | 3 | `src/clients/config-export/contracts.ts` (L1) | +| `ExportClientSpec` | interface | 674–713 | yes | 0 | `src/clients/config-export/contracts.ts` (L1) | +| `authoritativeContextWindow` | function | 719–725 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `outputBudgetFor` | function | 728–730 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `CLIENT_INPUT_MODALITIES` | const | 761–764 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `inputModalitiesForClient` | function | 767–779 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `dshInputModalities` | function | 782–791 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `exportModelLabel` | function | 798–805 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `opencodeProviderConnection` | function | 808–818 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `opencodeEffortVariants` | function | 833–840 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `opencodeProviderBlocks` | function | 855–894 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `opencodeProviderBlock` | function | 897–903 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `opencodeV2ProviderBlock` | function | 906–912 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `buildOpencodeProviderBlockFromCatalog` | function | 919–926 | yes | 1 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `normalizeExportModels` | function | 934–943 | yes | 2 | `src/clients/config-export/model-metadata.ts` (L1) | +| `buildOpencodeClientConfig` | function | 953–962 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `PiModelEntry` | interface | 964–979 | yes | 0 | `src/clients/config-export/contracts.ts` (L1) | +| `PiProviderBlock` | interface | 981–986 | yes | 0 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `PiGeneratedConfig` | interface | 988–990 | yes | 6 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `OmpModelEntry` | interface | 997–1006 | yes | 0 | `src/clients/config-export/omp.ts` (L1) | +| `OmpProviderBlock` | interface | 1008–1013 | yes | 0 | `src/clients/config-export/omp.ts` (L1) | +| `OmpGeneratedConfig` | interface | 1015–1017 | yes | 0 | `src/clients/config-export/omp.ts` (L1) | +| `OMP_EFFORT_VOCABULARY` | const | 1023–1023 | no | 0 | `src/clients/config-export/omp.ts` (L1) | +| `ompEfforts` | function | 1025–1034 | no | 0 | `src/clients/config-export/omp.ts` (L1) | +| `HermesProviderBlock` | interface | 1041–1049 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `HermesModelEntry` | interface | 1052–1054 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `HermesGeneratedConfig` | interface | 1056–1058 | yes | 4 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `OpenclawModelEntry` | interface | 1060–1064 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `OpenclawProviderBlock` | interface | 1066–1072 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `OpenclawGeneratedConfig` | interface | 1075–1080 | yes | 2 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `KimiProviderBlock` | interface | 1082–1086 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `KimiModelBlock` | interface | 1095–1100 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `KimiGeneratedConfig` | interface | 1102–1105 | yes | 2 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `GajaeModelEntry` | interface | 1107–1113 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `GajaeProviderBlock` | interface | 1116–1121 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `GajaeGeneratedConfig` | interface | 1123–1125 | yes | 3 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `DshReasoningEffort` | type | 1127–1127 | yes | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `DshWireReasoningEffort` | type | 1128–1128 | yes | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `DshModelEntry` | interface | 1130–1136 | yes | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `DshProviderBlock` | interface | 1138–1144 | yes | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `DshGeneratedConfig` | interface | 1146–1150 | yes | 2 | `src/clients/config-export/dsh.ts` (L1) | +| `McodeProviderBlock` | interface | 1152–1163 | yes | 0 | `src/clients/config-export/mcode.ts` (L1) | +| `McodeModelEntry` | interface | 1165–1170 | yes | 0 | `src/clients/config-export/mcode.ts` (L1) | +| `McodeGeneratedConfig` | interface | 1172–1174 | yes | 2 | `src/clients/config-export/mcode.ts` (L1) | +| `ZcodeModelEntry` | interface | 1183–1187 | yes | 0 | `src/clients/config-export/zcode.ts` (L1) | +| `ZcodeProviderBlock` | interface | 1189–1200 | yes | 0 | `src/clients/config-export/zcode.ts` (L1) | +| `ZcodeGeneratedConfig` | interface | 1202–1204 | yes | 1 | `src/clients/config-export/zcode.ts` (L1) | +| `buildPiClientConfig` | function | 1229–1276 | no | 0 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `buildOmpClientConfig` | function | 1283–1321 | no | 0 | `src/clients/config-export/omp.ts` (L1) | +| `proxyAdmissionHeaders` | function | 1324–1326 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `buildHermesClientConfig` | function | 1328–1349 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `buildOpenclawClientConfig` | function | 1351–1375 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `kimiModelAlias` | function | 1378–1380 | yes | 1 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `buildKimiClientConfig` | function | 1382–1407 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `buildGajaeClientConfig` | function | 1409–1438 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `DSH_EFFORT_ORDER` | const | 1440–1440 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `dshReasoningEfforts` | function | 1442–1462 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `isKnownSafeDshCombo` | function | 1464–1483 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `buildDshClientConfig` | function | 1485–1516 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `buildMcodeClientConfig` | function | 1527–1559 | no | 0 | `src/clients/config-export/mcode.ts` (L1) | +| `buildZcodeClientConfig` | function | 1570–1606 | no | 0 | `src/clients/config-export/zcode.ts` (L1) | +| `summarizeOpencode` | function | 1614–1617 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `summarizePi` | function | 1619–1622 | no | 0 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `summarizeOmp` | function | 1624–1627 | no | 0 | `src/clients/config-export/omp.ts` (L1) | +| `summarizeHermes` | function | 1629–1633 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `summarizeOpenclaw` | function | 1635–1638 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `summarizeKimi` | function | 1640–1645 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `summarizeGajae` | function | 1647–1650 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `summarizeDsh` | function | 1652–1655 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `summarizeMcode` | function | 1657–1660 | no | 0 | `src/clients/config-export/mcode.ts` (L1) | +| `summarizeZcode` | function | 1662–1665 | no | 0 | `src/clients/config-export/zcode.ts` (L1) | +| `singleFragment` | function | 1668–1670 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1) | +| `buildOpencodeContribution` | function | 1672–1684 | no | 0 | `src/clients/config-export/opencode.ts` (L2; deferred) | +| `buildPiContribution` | function | 1686–1689 | no | 0 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `buildOmpContribution` | function | 1691–1694 | no | 0 | `src/clients/config-export/omp.ts` (L1) | +| `buildHermesContribution` | function | 1696–1699 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `buildOpenclawContribution` | function | 1701–1704 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2; deferred) | +| `buildKimiContribution` | function | 1711–1720 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `buildGajaeContribution` | function | 1722–1725 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2; deferred) | +| `buildDshContribution` | function | 1727–1730 | no | 0 | `src/clients/config-export/dsh.ts` (L1) | +| `buildMcodeContribution` | function | 1732–1735 | no | 0 | `src/clients/config-export/mcode.ts` (L1) | +| `buildZcodeContribution` | function | 1737–1740 | no | 0 | `src/clients/config-export/zcode.ts` (L1) | +| `buildPrimeContribution` | function | 1755–1758 | no | 0 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `buildAsideContribution` | function | 1778–1781 | no | 0 | `src/clients/config-export/pi.ts` (L2; deferred) | +| `EXPORT_CLIENTS` | const | 1783–1954 | yes | 15 | `src/clients/config-export.ts` (residual) | +| `EXPORT_CLIENT_IDS` | const | 1956–1956 | yes | 7 | `src/clients/config-export.ts` (residual) | +| `isExportClientId` | function | 1958–1960 | yes | 3 | `src/clients/config-export.ts` (residual) | +| `buildClientConfig` | function | 1963–1965 | yes | 9 | `src/clients/config-export.ts` (residual) | +| `buildClientConfigText` | function | 1973–1985 | yes | 8 | `src/clients/config-export.ts` (residual) | +| `buildClientContribution` | function | 1988–1990 | yes | 5 | `src/clients/config-export.ts` (residual) | + +Export-only declaration: `ConfigFormat` at `src/clients/config-export.ts:32` remains forwarded from `../integrations/serialize`, not redefined. + +## Leaf partition + +Part a moves the lowest-fanout format leaves first: `omp` (sum of external symbol consumers 0), `zcode` (1), `dsh` (2), `mcode` (2). Part b takes the higher-fanout families and paths. The three shared foundations move with part a because even its lowest-fanout clients need them: leaving types/constants/model rules in the original would create facade back-imports. No external caller changes paths. PiModelEntry (0 consumers) moves with shared contracts because OmpModelEntry extends it. The larger Pi document type/builders remain for part b. + +Line-budget convention: each declaration carries immediately preceding comments/whitespace, from previous declaration end+1. One explicit exception: the blank separator at original line 33, immediately after the import/export header, stays in the facade; the first moved block starts at line 34. This gives 707 moved original lines and the contracts projection below. Moving line 33 as well would instead give 708 moved lines and a 151-line contracts leaf. Counts include those blocks, the exact one-line imports shown, one header line and one separator. These are projected implementation counts, not measurements of files already written. Do not discard comments to meet limits. Adding an export keyword does not add a line. All new files are ≤400. + +### `src/clients/config-export/contracts.ts` — expected 150 lines + +Symbols: `ManagedFragment`, `ManagedContribution`, `BuildContribution`, `OpencodeLaunchEnv`, `OpencodeCatalogModel`, `ExportModel`, `ExportContext`, `ExportClientId`, `ExportClientSpec`, `PiModelEntry`. + +Own imports: + +```ts +import type { OcxConfig } from "../../types"; +import type { ConfigFormat } from "../../integrations/serialize"; +``` + +Leaf exports: `ManagedFragment`, `ManagedContribution`, `BuildContribution`, `OpencodeLaunchEnv`, `OpencodeCatalogModel`, `ExportModel`, `ExportContext`, `ExportClientId`, `ExportClientSpec`, `PiModelEntry`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/constants.ts` — expected 69 lines + +Symbols: `OPENCODE_PROVIDER_ID`, `OPENCODE_CONFIG_SCHEMA`, `OPENCODE_API_KEY_ENV`, `OPENCODE_API_KEY_ENV_REF`, `HERMES_API_KEY_ENV`, `HERMES_API_KEY_ENV_REF`, `OPENCLAW_API_KEY_ENV`, `OPENCLAW_API_KEY_ENV_REF`, `LOOPBACK_API_KEY_PLACEHOLDER`, `GAJAE_API_KEY_ENV`, `PI_API_DIALECT`, `SCHEMA_REQUIRED_OUTPUT_BUDGET`, `OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG`. + +Own imports: + +```ts +import type { OcxConfig } from "../../types"; +``` + +Leaf exports: `OPENCODE_PROVIDER_ID`, `OPENCODE_CONFIG_SCHEMA`, `OPENCODE_API_KEY_ENV`, `OPENCODE_API_KEY_ENV_REF`, `HERMES_API_KEY_ENV`, `HERMES_API_KEY_ENV_REF`, `OPENCLAW_API_KEY_ENV`, `OPENCLAW_API_KEY_ENV_REF`, `LOOPBACK_API_KEY_PLACEHOLDER`, `GAJAE_API_KEY_ENV`, `PI_API_DIALECT`, `SCHEMA_REQUIRED_OUTPUT_BUDGET`, `OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/model-metadata.ts` — expected 113 lines + +Symbols: `authoritativeContextWindow`, `outputBudgetFor`, `CLIENT_INPUT_MODALITIES`, `inputModalitiesForClient`, `exportModelLabel`, `normalizeExportModels`, `proxyAdmissionHeaders`, `singleFragment`. + +Own imports: + +```ts +import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants"; +import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts"; +import type { OcxConfig } from "../../types"; +import { shouldInjectApiAuthHeader } from "../../codex/inject"; +``` + +Leaf exports: `authoritativeContextWindow`, `outputBudgetFor`, `inputModalitiesForClient`, `exportModelLabel`, `normalizeExportModels`, `proxyAdmissionHeaders`, `singleFragment`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/omp.ts` — expected 104 lines + +Symbols: `OmpModelEntry`, `OmpProviderBlock`, `OmpGeneratedConfig`, `OMP_EFFORT_VOCABULARY`, `ompEfforts`, `buildOmpClientConfig`, `summarizeOmp`, `buildOmpContribution`. + +Own imports: + +```ts +import type { PiModelEntry, ExportModel, ExportContext, ManagedContribution } from "./contracts"; +import { PI_API_DIALECT, OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; +import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, outputBudgetFor, singleFragment } from "./model-metadata"; +``` + +Leaf exports: `OmpModelEntry`, `OmpProviderBlock`, `OmpGeneratedConfig`, `buildOmpClientConfig`, `summarizeOmp`, `buildOmpContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/zcode.ts` — expected 92 lines + +Symbols: `ZcodeModelEntry`, `ZcodeProviderBlock`, `ZcodeGeneratedConfig`, `buildZcodeClientConfig`, `summarizeZcode`, `buildZcodeContribution`. + +Own imports: + +```ts +import type { ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment } from "./model-metadata"; +import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; +``` + +Leaf exports: `ZcodeModelEntry`, `ZcodeProviderBlock`, `ZcodeGeneratedConfig`, `buildZcodeClientConfig`, `summarizeZcode`, `buildZcodeContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/dsh.ts` — expected 132 lines + +Symbols: `dshInputModalities`, `DshReasoningEffort`, `DshWireReasoningEffort`, `DshModelEntry`, `DshProviderBlock`, `DshGeneratedConfig`, `DSH_EFFORT_ORDER`, `dshReasoningEfforts`, `isKnownSafeDshCombo`, `buildDshClientConfig`, `summarizeDsh`, `buildDshContribution`. + +Own imports: + +```ts +import type { ExportModel, ExportContext, ManagedContribution } from "./contracts"; +import type { OcxConfig } from "../../types"; +import { providerCodexAccountMode } from "../../providers/registry"; +import { normalizeExportModels, authoritativeContextWindow, exportModelLabel, singleFragment } from "./model-metadata"; +import { OPENCODE_PROVIDER_ID } from "./constants"; +``` + +Leaf exports: `DshReasoningEffort`, `DshWireReasoningEffort`, `DshModelEntry`, `DshProviderBlock`, `DshGeneratedConfig`, `buildDshClientConfig`, `summarizeDsh`, `buildDshContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/mcode.ts` — expected 83 lines + +Symbols: `McodeProviderBlock`, `McodeModelEntry`, `McodeGeneratedConfig`, `buildMcodeClientConfig`, `summarizeMcode`, `buildMcodeContribution`. + +Own imports: + +```ts +import type { ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata"; +import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort"; +import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; +``` + +Leaf exports: `McodeProviderBlock`, `McodeModelEntry`, `McodeGeneratedConfig`, `buildMcodeClientConfig`, `summarizeMcode`, `buildMcodeContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +Residual `src/clients/config-export.ts`: expected **1299 lines**. It remains >400 intentionally; **410 / S13 L2 / #b** takes all deferred inventory rows. + +Retained declarations after this layer: `OpencodeModelEntry`, `OpencodeModelVariant`, `OpencodeV2ModelEntry`, `OpencodeProviderConnection`, `OpencodeProviderBlock`, `OpencodeV2ProviderBlock`, `OpencodeProviderBlocks`, `OpencodeGeneratedConfig`, `OPENCODE_PROVIDER_NPM`, `OPENCODE_V2_PROVIDER_PACKAGE`, `OPENCODE_PROVIDER_NAME`, `opencodeGlobalConfigPath`, `OMP_PROFILE_NAME_RE`, `OMP_WINDOWS_RESERVED_PROFILE_RE`, `ompProfileName`, `piAgentDir`, `piConfigPath`, `ompAgentDir`, `ompModelsConfigPath`, `opencodeProxyBaseUrl`, `hermesHomeDir`, `hermesConfigPath`, `ClientPathError`, `absoluteClientPath`, `openclawEffectiveHome`, `openclawHomeDir`, `openclawConfigPath`, `kimiHomeDir`, `kimiConfigPath`, `gajaeHomeDir`, `gajaeConfigPath`, `dshHomeDir`, `dshConfigPath`, `mcodeHomeDir`, `mcodeConfigPath`, `zcodeHomeDir`, `zcodeConfigPath`, `primeAgentDir`, `primeConfigPath`, `asideHomeDir`, `asideCurrentAccountId`, `asideAccountDir`, `asideConfigPath`, `opencodeProviderConnection`, `opencodeEffortVariants`, `opencodeProviderBlocks`, `opencodeProviderBlock`, `opencodeV2ProviderBlock`, `buildOpencodeProviderBlockFromCatalog`, `buildOpencodeClientConfig`, `PiProviderBlock`, `PiGeneratedConfig`, `HermesProviderBlock`, `HermesModelEntry`, `HermesGeneratedConfig`, `OpenclawModelEntry`, `OpenclawProviderBlock`, `OpenclawGeneratedConfig`, `KimiProviderBlock`, `KimiModelBlock`, `KimiGeneratedConfig`, `GajaeModelEntry`, `GajaeProviderBlock`, `GajaeGeneratedConfig`, `buildPiClientConfig`, `buildHermesClientConfig`, `buildOpenclawClientConfig`, `kimiModelAlias`, `buildKimiClientConfig`, `buildGajaeClientConfig`, `summarizeOpencode`, `summarizePi`, `summarizeHermes`, `summarizeOpenclaw`, `summarizeKimi`, `summarizeGajae`, `buildOpencodeContribution`, `buildPiContribution`, `buildHermesContribution`, `buildOpenclawContribution`, `buildKimiContribution`, `buildGajaeContribution`, `buildPrimeContribution`, `buildAsideContribution`, `EXPORT_CLIENTS`, `EXPORT_CLIENT_IDS`, `isExportClientId`, `buildClientConfig`, `buildClientConfigText`, `buildClientContribution`. + +Projection before unused-import pruning: 1990 original − 707 cumulative moved original lines + 16 facade glue = 1299. Across a/b: 707 + 1,041 = 1,748 moved body/trivia lines; 242 retained original lines; 1,748 + 242 = 1,990. The projected final glue is 31 lines, giving 273; L1's 16 glue lines are replaced by L2's 31, not both counted. These are estimates, not acceptance measurements: remove the now-unused provider import and measure actual import/forward/separator lines in B/C, recording the reconciled residual and leaf counts before advancing. + +## Re-export block + +Exact forwards in the original path follow. Other public declarations remain exported in place. No wildcard, alias, wrapper, signature change or duplicate definition. + +```ts +export type { ConfigFormat } from "../integrations/serialize"; +export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry } from "./config-export/contracts"; +export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants"; +export { normalizeExportModels } from "./config-export/model-metadata"; +export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp"; +export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode"; +export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh"; +export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode"; +``` + +Explicit residual local imports (re-export binds nothing locally): + +```ts +import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts"; +import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, GAJAE_API_KEY_ENV, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants"; +import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata"; +import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp"; +import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh"; +import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode"; +import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode"; +``` + +Retain original external imports still used by the residual; prune only proven-unused bindings. Specifically remove the `providerCodexAccountMode` import and remove only `sanitizeCodexReasoningEfforts` from the reasoning-effort import, retaining `canonicalizeReasoningEfforts`. Keep both existing imports from `../codex/inject`: the residual still uses `shouldInjectApiAuthHeader` and `standaloneCodexRoutingTarget`. New leaves import one another directly. + +## Module-level state and cycles + +`CLIENT_INPUT_MODALITIES` at `src/clients/config-export.ts:761–764` owns two allowlist Sets in `config-export/model-metadata.ts`; never copy them into each client. `OMP_EFFORT_VOCABULARY` at `:1023` belongs only to `config-export/omp.ts`. No top-level let, Map, WeakMap, timer or lock exists. Function-local seen/offered Sets remain per-call. The exported default-config object at `:220–225` moves once to constants.ts; preserve object identity. `EXPORT_CLIENTS` at `:1783–1954` and derived `EXPORT_CLIENT_IDS` at `:1956` remain initialized once in the residual; preserve order. + +Lane 016's AST import BFS found no return path through the original. The partition avoids new return imports, including type-only ones. Risk: original → client leaf → original. Shared contracts/constants/model rules therefore move down in L1. `contracts.ts → ../../integrations/serialize` preserves ConfigFormat's actual owner; do not substitute config-io (which imports the original facade). OpenClaw/Aside paths import paths.ts for the single constructor/absolute-path rule; paths.ts imports no path sibling. Only the residual registry composes all client builders. Private builders/summarizers become explicit leaf exports for that production registry; no duplicated closures. + +Coupling classification: existing config-schema coupling stays with format owners; sequential/functional coupling is explicit through parameters. No new common mutable state or temporal startup constraint. Existing auth/ownership checks are moved verbatim. Before execution rerun lane 016 method G against the actual layer base (relative static imports, re-exports, type-only edges and literal dynamic imports); any new return path is escalation, not permission for a lazy-import workaround. + +## Tests + +Discovery: `rg -l 'src/clients/config-export' tests --glob '*.ts'`, followed by import/source-read inspection. Every direct test/fixture importer is listed below, with disposition **unchanged** (old public path): + +- `tests/ci-workflows/dsh-path-contract.test.ts` — unchanged. +- `tests/ci-workflows/dsh-writer-lock.test.ts` — unchanged. +- `tests/cli/cli-help.test.ts` — unchanged. +- `tests/clients/client-export-modality-enum.test.ts` — unchanged. +- `tests/clients/integrations-state.test.ts` — unchanged. +- `tests/clients/integrations-writer.test.ts` — unchanged. +- `tests/clients/omp-path-contract.test.ts` — unchanged. +- `tests/clients/pi-path-contract.test.ts` — unchanged. +- `tests/clients/prime-client.test.ts` — unchanged. +- `tests/clients/sync-client-integrations.test.ts` — unchanged. +- `tests/config/client-config-export-new-clients.test.ts` — unchanged. +- `tests/config/client-config-export.test.ts` — original import/assertions retained; facade identity and fixed-byte regressions added. +- `tests/config/client-config-new-clients.test.ts` — unchanged. +- `tests/gui/integrations-invariants.test.ts` — unchanged. +- `tests/providers/aside-client.test.ts` — unchanged. +- `tests/providers/minimax-clients.test.ts` — unchanged. +- `tests/providers/zcode-client.test.ts` — unchanged. +- `tests/server/management-client-config-route.test.ts` — unchanged. +- `tests/server/management-integration-journal-delete.test.ts` — unchanged. +- `tests/server/management-integration-routes.test.ts` — unchanged. + +No source-text reader of src/clients/config-export.ts was found. `tests/config/client-config-export.test.ts:58` and `tests/server/management-client-config-route.test.ts:416` mention it in comments, not source reads. No retarget-to-leaf or add-leaf-to-scan-list action. Preserve baked serialized fixtures unchanged. + +C-phase red proof: temporarily treat incompatible audio-only input as text in the moved metadata function and observe `tests/clients/client-export-modality-enum.test.ts:96` fail; restore. Temporarily retain none in the moved MCode effort list and observe `tests/providers/minimax-clients.test.ts:117` fail; restore. + +The implementation uses the existing focused test file; no new test file was needed. Original assertions remain intact. The recorded red/restored-green proof applies to the unchanged tested source and test blobs; repeat it if those change. Never commit mutation probes. A future new test file requires both layout registry entries. + +## Verification + +Run the following Bash recipe from this session's bound checkout while its FSM is at C, after committing and publishing the layer head. The session identifier below belongs to this task; another task must use its own latest SessionStart binding. All Bun commands run on `lidge`, inside a fresh temporary clone. No shared seed checkout is switched. The local commands only validate identity, transport the verifier and retain output. + +```bash +set -euo pipefail +wp400_root=$(git rev-parse --show-toplevel) +wp400_expected=$(git rev-parse HEAD) +wp400_status=$(git status --porcelain) +test -z "$wp400_status" +wp400_log="$wp400_root/.codexclaw/evidence/01a06e97-b9d8-7250-8204-bb788338c288/wp400-remote-check-$wp400_expected.log" +mkdir -p "$(dirname "$wp400_log")" +cxc receipt test --cwd "$wp400_root" --session 01a06e97-b9d8-7250-8204-bb788338c288 -- bash -c ' +set -euo pipefail +test "$(git rev-parse HEAD)" = "$1" +local_status=$(git status --porcelain) +test -z "$local_status" +ssh lidge bash -s -- "$1" 2>&1 | tee "$2" +test "$(git rev-parse HEAD)" = "$1" +local_status=$(git status --porcelain) +test -z "$local_status" +' -- "$wp400_expected" "$wp400_log" <<'REMOTE' +set -euo pipefail +expected=${1:?expected SHA required} +[[ "$expected" =~ ^[0-9a-f]{40}$ ]] +run_dir=$(mktemp -d /tmp/ocx-wp400.XXXXXX) +printf 'RETAINED_RUN_DIR=%s\n' "$run_dir" +git clone --no-checkout https://github.com/lidge-jun/opencodex.git "$run_dir/repo" +cd "$run_dir/repo" +git fetch origin refs/heads/codex/split-clients-config-export-a +test "$(git rev-parse FETCH_HEAD)" = "$expected" +git checkout --detach "$expected" +bun --version +bun install --frozen-lockfile +(cd gui && bun install --frozen-lockfile) +tree_status=$(git status --porcelain) +test -z "$tree_status" +printf 'CHECKOUT=%s\nHEAD=%s\n' "$PWD" "$(git rev-parse HEAD)" +unset OCX_TEST_NO_QUEUE +bun run typecheck +bun test tests/ci-workflows/dsh-path-contract.test.ts tests/ci-workflows/dsh-writer-lock.test.ts tests/cli/cli-help.test.ts tests/clients/client-export-modality-enum.test.ts tests/clients/integrations-state.test.ts tests/clients/integrations-writer.test.ts tests/clients/omp-path-contract.test.ts tests/clients/pi-path-contract.test.ts tests/clients/prime-client.test.ts tests/clients/sync-client-integrations.test.ts tests/config/client-config-export-new-clients.test.ts tests/config/client-config-export.test.ts tests/config/client-config-new-clients.test.ts tests/gui/integrations-invariants.test.ts tests/providers/aside-client.test.ts tests/providers/minimax-clients.test.ts tests/providers/zcode-client.test.ts tests/server/management-client-config-route.test.ts tests/server/management-integration-journal-delete.test.ts tests/server/management-integration-routes.test.ts tests/cli/cli-export-command.test.ts +bun run privacy:scan +if bun run test; then + test_rc=0 +else + test_rc=$? +fi +printf 'SUITE_EXIT=%s\n' "$test_rc" +if [ "$test_rc" -ne 0 ]; then exit "$test_rc"; fi +test "$(git rev-parse HEAD)" = "$expected" +tree_status=$(git status --porcelain) +test -z "$tree_status" +printf 'VERIFIED_HEAD=%s\n' "$expected" +REMOTE +``` + +The local pipeline propagates SSH and log-write failure to the receipt producer. Remote commands stop on failure; full-suite status is printed and returned. Final remote HEAD and Git status must still match the clean layer head. Keep the temporary clone and full output as evidence. A receipt proves the command actually run, not this prose; require fresh current-head CI and independent review before closure. + +Local read-only structural checks are `git diff --check`, `wc -l` for the eight source files, and importer discovery with `rg -n 'clients/config-export' src gui/src scripts tests`. Resolve actual import edges when comparing consumers; a grep count alone is insufficient. New leaves must have no facade return edge, including type-only and literal dynamic imports. The full suite includes the core/Lab guard; do not weaken its protected roots. This layer's own source delta does not touch those protected files. + +## Accept criteria + +1. Apply PURE-MOVE-SIZE-01: ≤150 non-move changed lines, move-aware diff evidence, and exactly one implementation owner for each inventory symbol. No claim that literal added+deleted churn meets 500. +2. Every inventory declaration has exactly one implementation owner. Preserve all original export names/signatures and value/type importability; do not extract L1 declarations a second time. +3. Every new leaf is ≤400 lines. Residual target is 1299, with the sole >400 carry explicitly assigned to 410 / #b. Measure actual files and explain drift before proceeding. +4. Preserve function bodies, branch order, literals, serialized bytes/key order, class/object identity and state initialization. Only moves, explicit imports and named forwards change source structure. +5. Old-path consumers and assertions remain intact. Record the exact red/restored-green evidence named under Tests; no guard deletion, skipping, weakened assertions or empty-facade source scans. +6. Singleton state/allowlists each have one owner; no leaf imports the original even for types; resolved static/re-export/type/dynamic-literal graph has no new cycles. +7. Typecheck, focused checks, privacy, remote full suite and exact-head CI pass at this layer tip independently of later layers. No full local suite and no merge. +8. Diff stays within the original/new leaves and genuinely required existing focused tests. New tests, SoT edits, new topology or unrelated code require parent scope approval. + +## PR + +Title: `refactor(clients): extract low-fanout client formats and dependency foundations (split S13 L1/5)` + +Branch: `codex/split-clients-config-export-a`. Current replanned base: `dev`, pinned `be81013fab6d83ff630ca5f38e7881678a303871` after prerequisite #3610 landed. Closes: none. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), including the size-gate disposition and DEV-STACK-03 map below. This layer is PR #3611; placeholder rows refer only to future layers. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #3611 | 400 — this layer | `codex/split-clients-config-export-a` | `dev` | extract low-fanout client formats and dependency foundations | +| 2 | #TBD-S13-L2 | 410 | `codex/split-clients-config-export-b` | `codex/split-clients-config-export-a` | finish client path and format partitions | +| 3 | #TBD-S13-L3 | 420 | `codex/split-cli-opencode` | `codex/split-clients-config-export-b` | separate OpenCode config and catalog from launch | +| 4 | #TBD-S13-L4 | 430 | `codex/split-cli-minimax` | `codex/split-cli-opencode` | isolate MMX protocol and termination owners | +| 5 | #TBD-S13-L5 | 440 | `codex/split-integrations-state` | `codex/split-clients-config-export-b` | separate classification from state reads | + +Bottom S13 layer against `dev`. The former prerequisite #3610 has landed and the retarget/restack is complete. Review this layer's diff only. No S13 child had been published at this checkpoint. Future base changes require normal scoped restack and fresh verification; no open-prerequisite retargeting action remains. Merging stays out of scope. + +## P stale-check (2026-09-05, wp400) + +Historical stale check at origin/dev 3191fe1aa: config-export.ts unchanged since 445742966 (1990 lines). Base `dev` (S13 bottom; 410 #b, 420, 430, 440 chain on it). The planned subdirectory mirrors the src/codex/prompt-layers/ precedent from L300. 003 INTERMEDIATE-RESIDUAL-01 applies. Known upstream failures were management-route-registry ×3 and quota-reset-notify ×1. The earlier OCX_TEST_NO_QUEUE=1 instruction is withdrawn: it contaminates lock tests and must be unset for remote verification. No local suites; CI hygiene requires a test change. + +## A audit synthesis (2026-09-05, wp400) + +### Current execution authority + +The current branch is `codex/split-clients-config-export-a`, PR #3611, base `dev` at `be81013fab6d83ff630ca5f38e7881678a303871`. That SHA is the integration base, not the layer head. The restack has already been performed. For current verification, use the clean layer HEAD and the isolated recipe above; do not repeat the retired parent rebase below. #3610 has landed as `5ab8aa9a2d9d2a3926469f9d8c82387b43c6d0e9`. + +### Historical replan record — not executable + +The original `244663568` split on the `850afb2e9` foundation passed focused checks but failed the full suite on four inherited quota/route cases. A temporary stack used the then-open #3610 head `afdd38ff43c64696153372fc2e27a38aff208c73`; its older foundation omitted four intervening dev commits, an explicitly recorded and audited tradeoff. The historical review anchors `7953e6d4` and `7d4a37544` belong to that retired parent arrangement. + +After #3610 landed, the user authorized continued scoped verification and repair. Main replayed only its own commits onto the current dev base, preserving old refs and using `--no-update-refs` plus an exact-old-head lease. The actual `412dcba4` tree was identity-checked against the audited dev-base overlay. Its remote gates passed. Subsequent documentation repairs require a new current-head receipt; the historical hashes here are evidence anchors, never checkout or retargeting instructions. + +The complete initial roadmap remains in the preserved documentation branch, at immutable local commit `dc44b08cafbbd45da81f940f1e8c00a9e5f61ce1`. Only this layer's governing documents are carried in its PR; other roadmap documents are read from that preserved ref. Implementation and receipts remain in the existing a2c0 directory, never in a replacement managed worktree. + +Hooke (`01a06f9f-f57f-7fc3-9261-b07f291929be`, requested gpt-6-astra high) returned GO-WITH-FIXES with zero blockers, then PASS after the two documentation corrections above. The read-only audit matched all 153 inventory ranges, assigned all 63 moved declarations uniquely, checked seven leaf and seven facade import lists, and preserved 96 public exports (47 types, 49 values). Its dependency traversal reported no return path from the external owners to the facade at base `3191fe1aa`. These are plan-audit results, not implementation or test results. + +Accepted findings: replace the stale raw-churn escalation with the binding ≤150 non-move gate; explicitly retain original blank line 33 and mark projected line counts as pre-pruning estimates. No blocker was rebutted. Re-review confirmed both closures at docs HEAD `38ad3cf5a` plus the working diff. `git diff --check` exited 0 after those edits; no local test suite was run. + +Operational audit by Wegener (`01a06fa6-5e3c-7840-8172-8587e853dcc7`, explicitly `model=gpt-6-astra`, `reasoning_effort=high`) found two blockers: checkout-local source identity was incompatible with the prior separate execution tree, and the remote recipe switched a shared checkout. Both were accepted and folded into 003 WORKTREE-EVIDENCE-01 and 000. Re-audit returned PASS, with no blocker to entering B. A documentation-only delta must not stand in for implementation evidence from another checkout. The unsafe shared-checkout command has been removed; Verification now contains the isolated recipe. The actual runner received independent exact-SHA, clean-tree and failure-propagation review; changes to that recipe require the same checks. Approval of the plan is not proof that remote verification passed. + +## B implementation record (2026-09-05) + +### Historical temporary-stack checkpoint + +At historical review anchor `7953e6d4e18b0e7c90c0c5cdb0a4256c22a25dd0`, the integration base was `afdd38ff43c64696153372fc2e27a38aff208c73` and PR #3611 temporarily targeted the then-open #3610 branch. That arrangement is retired. Only our branch was rebased/pushed; the exact-old-head lease protected publication and `--no-update-refs` preserved the old snapshots. + +Independent reviewer Heisenberg confirmed identical blob IDs for all nine source/test paths versus244, exactly those nine paths plus three documents in the parent-relative diff, and an actual-tree traversal of4979edges/349facade-reachable files with no new return cycle or unresolved reachable import. Static verdict PASS; not a runtime-pass claim. This documentation checkpoint adds no source/test changes after that review anchor. + +The prerequisite itself was separately verified in remote `/tmp/ocx-wp400.T036h8/repo` at exactafdd38ff: typecheck0,440focusedpass/0fail, privacy0, full suite SUITE_EXIT=0 and final HEAD/clean-tree checks passed. Full output: `wp400-prerequisite-check.log` in the session evidence directory. No local suite ran. Fresh resulting-head verification and GitHub CI for #3611 are still required. + +Franklin (`01a06fac-95ee-77a0-8916-f7546c2b8996`, explicitly gpt-6-astra high) implemented only the approved source/test paths in a2c0 and handed them back without Git mutations or local tests. Main inspected the diff and measured all leaves. Source owner search and the A inventory were reused; no new algorithm or parallel implementation was introduced. + +| File | Change and impact | Measured lines | +|---|---|---:| +| `src/clients/config-export.ts` | Retains dispatch/compatibility exports, imports moved owners; caller paths unchanged | 1298 | +| `src/clients/config-export/contracts.ts` | Canonical shared types, no runtime behavior | 150 | +| `src/clients/config-export/constants.ts` | Single constant/default-object owner | 69 | +| `src/clients/config-export/model-metadata.ts` | Existing normalization/modality/admission helpers moved intact | 113 | +| `src/clients/config-export/omp.ts` | Existing OMP builder, summary and owned fragment | 104 | +| `src/clients/config-export/zcode.ts` | Existing ZCode builder, summary and owned fragment | 92 | +| `src/clients/config-export/dsh.ts` | Existing DSH builder, summary and owned fragment | 132 | +| `src/clients/config-export/mcode.ts` | Existing MCode builder, summary and owned fragment | 83 | +| `tests/config/client-config-export.test.ts` | Adds identity and independent fixed-byte/fragment assertions; original assertions/fixtures unchanged | 919 | + +The worker's AST inventory reports 153 unique owners (63 moved, 90 retained), 96 public exports (47 types, 49 values), 707 moved original lines and 109 non-move lines (36 leaf glue + 18 facade additions + 3 removals + 52 test additions). Actual facade size is one below the estimate because the unused provider import was removed. `git diff --check` passed. The existing test file was already over 400; its scoped extension is not a claim to resolve test-file debt. Final independent graph/syntax and runtime gates remain pending; the worker's combined graph/syntax command hit an AST no-match exit and did not establish a pass. + +Verification runner: `.codexclaw/evidence/01a06e97-b9d8-7250-8204-bb788338c288/wp400-check.sh` invokes the reviewed `wp400-remote-check.sh` only over SSH. Wegener closed the pre-C hold after four clean-tree substitutions were changed to standalone Git-status assignments and the no-queue override was removed. Both scripts pass `bash -n`; runtime success is not implied. + +Baseline evidence: isolated remote `/tmp/ocx-wp400.4dKWtB/repo`, exact base `850afb2e9f84979c87e914b248de482f44b34cd6`; typecheck, 440 focused tests across 21 files and privacy scan passed. Full suite exited 1. The initial runner mistakenly exported OCX_TEST_NO_QUEUE=1, inducing four lock-test failures in addition to the known upstream route-registry/rollover failures. That run is contaminated and cannot certify all gates. The variable is now explicitly unset; corrected verification is required. Full baseline output is retained as `wp400-base-check.log` in the same evidence directory. No local suite was run. + +## Latest verification checkpoint + +Current layer head is `bbf8d3cd25ccf70eb595bc7982f63528d060c1bd`, base dev at be81013fab6d83ff630ca5f38e7881678a303871, PR #3611 ready for review. All nine source/test blobs remain unchanged from the reviewed split. Three CodeRabbit document findings were fixed; all three threads are resolved. The S04 correction distinguishes six members from actual depth three, rather than retaining an obsolete depth-six exception. The isolated verifier and in-receipt identity guards received independent review. + +The complete documented Bash recipe ran at this head and produced a fresh clean receipt: typecheck,442focusedtests,privacy and full18402pass/16skip/0fail, SUITE_EXIT=0. Evidence is in the bound a2c0 session directory, including wp400-remote-check-bbf8d3cd25ccf70eb595bc7982f63528d060c1bd.log and test-receipt.json. Prior receipts are archived by head. Final CI snapshot contains36reportedchecks with no pending/failed logical checks; configured skips remain explicit and obsolete cancellations have same-head successful replacements. All review threads are resolved. cxc completed exact-head-full-gates and closed WP400 through D, then entered P for WP450. Evidence includes ci-bbf8d3cd25ccf70eb595bc7982f63528d060c1bd.json and the head-named receipt archive. No merge occurred. Scoped CI reruns/repairs are authorized and no local suite or merge occurred. + +## Central evidence journal — historical snapshots, not execution instructions + +### Earlier resume checkpoint + +Historical state at that checkpoint: the user explicitly granted full scoped authority after the CI-restart question. Host goal is ACTIVE; do not repeat that permission stop. #3610 externally landed and the PR auto-retargeteddev. Main performed C→P→A→B→C, audited the new candidate with Hooke, and rebased only own commits onto pinnedbe81013fab6d83ff630ca5f38e7881678a303871 using --no-update-refs and exact-old-head force-with-lease. Current clean a2c0 head is412dcba4d617bd2c6c5961a1ada9484b859d700f. Heisenberg confirmed identical9source/test blobs and exact12-path scope; security/contract review remains valid. + +Fresh412remote typecheck,442focusedtests,privacy and full18402pass/16skip/0fail all passed. Receipt is clean and bound to412 with epochc-20260905044615-f4039d. Per-head output iswp400-remote-check-412dcba4d617bd2c6c5961a1ada9484b859d700f.log; previous7d4receipt was archived verbatim before producer reuse. Current hostedCIrun33945457034 is the target, with no failure/cancellation at this checkpoint. Its remaining checks are awaited; no D claim yet. The old-base retry was cancelled by Main as superseded by this restack, not treated as a new permission blocker. + +The resumed goal was subsequently marked BLOCKED on a new condition: three consecutive goal turns confirmed hosted CI cancelled by the account with no replacement run or rerun confirmation. The former four baseline failures are resolved; successful7d4remote proof remains valid. No D close, CI waiver, merge or local suite was fabricated to end the loop. Resume on explicit rerun direction or fresh external replacement evidence. + +The user resumed until completion. Actual FSM path was C→P→A→B→C, without a false D. Main chose an explicit prerequisite stack on #3610/afdd38ff and accepted the older basis in writing. Hooke candidate-graph and Wegener operational audits passed. Only our branch was rebased, with --no-update-refs and an exact-old-head lease; original244/audit67 refs remain. + +Final clean a2c0 head: `7d4a37544b0df016cb7ba45d193d2fb9f0ad00a1`; #3611 targets the open prerequisite branch. Heisenberg independently verified identical nine source/test blobs, exact parent-relative scope and actual graph at7953e6d4. The subsequent7d4change is documentation only. + +Both prerequisiteafdd and our7d4head passed isolated remote typecheck, focused tests, privacy and the full suite. At7d4:442focusedpass; full-suite lanes total18200pass/16skip/0fail; SUITE_EXIT=0; final remote HEAD/clean-tree checks passed. The actual `cxc receipt test` command exited0 and created `test-receipt.json` bound to7d4, dirtyfalse and check epochc-20260905042631-92d875. Full log: `wp400-remote-check-7d4a37544b0df016cb7ba45d193d2fb9f0ad00a1.log`. No local suite or merge occurred. + +Hosted CI is still incomplete, not a source-test pass: current-head run33944657511 was cancelled. Job101248560357 has no runner/steps and its check annotation says “The run was canceled by @lidge-jun.” The run list shows no newer replacement at7d4. Missing checks include test2/4,test3/4,macos1/2,macos2/2,npm-globalmacos and a cancelled enforce-target. Do not silently override this account-initiated cancellation or count it as green. Exact-head-full-gates and c-5 remain open; no D close. + +### Historical original-head C checkpoint (not a D close) + +Host goal subsequently marked BLOCKED after three consecutive goal turns hit the same full-gate failure. Last fetched dev55395a9dc leaves route-registry.ts, management-api.ts and quota-reset-notify.test.ts unchanged from850; #3610 remains open/draft/unmerged. Additional update-test diagnostic scope has been requested, not granted. Source/PR244663568 remains clean and draft. No D transition, success receipt, merge or local suite was used to bypass this condition. Resume after the prerequisite is incorporated into a stable base, or after an authorized diagnostic/stabilization plan is agreed. The read-only #3610 watch has ended; no background follow-up is promised while blocked. + +Code is committed and pushed at `24466356836dd567120d3d3f4e8d09574f2182d3`, PR #3611 open/draft against dev. Source and FSM remain in a2c0; this worktree is only the central documentation record, not the receipt checkout. The detailed per-file B record is in the code head's copy of this document. + +- Seven leaves: contracts150, constants69, model-metadata113, omp104, zcode92, dsh132, mcode83. Facade1990→1298; WP410 remains required. Existing test file gained52lines without removing original assertions. +- Franklin implemented with explicit gpt-6-astra high. Fresh reviewer Heisenberg independently verified153uniqueowners (63moved/90retained), all96exports (47types/49values),118localbindings and4982edges/349reachablefiles with no new return cycle. Credential references, admission predicate and DSH filtering remain unchanged. Verdict PASS. +- Remote `/tmp/ocx-wp400.dg2OKt/repo` at that head: typecheck0,442focusedpass/0fail, privacy0. Full suite exited1 with exactly4baseline failures: management-route-registry×3 and quota-reset-notify enabled rollover×1. No passing receipt was written; C remains open. +- The older OCX_TEST_NO_QUEUE=1 instruction is withdrawn. It induced4lock-test failures in the initial baseline. The corrected runner unsets it and uses standalone Git-status assignments. Unchanged baseline lock tests then gave41pass/2skip/0fail; the corrected current full run has no lock-test failures. +- Two remote mutation proofs: audio-only→text triggered the named omission failure, then restored8pass/0fail; MCode retaining none triggered the named none-only failure, then restored19pass/0fail. Both red exits1; final remote HEAD unchanged and gitstatusclean. No mutant was committed or pushed. +- No local suite, merge, auto-merge or direct integration-branch push occurred. + +Evidence in a2c0 `.codexclaw/evidence/01a06e97-b9d8-7250-8204-bb788338c288/`: `wp400-remote-check.log`, `wp400-red-green.log`, `wp400-baseline-lock-correction.log`, `wp400-current-checkpoint.md`. Goalplan ledger records source-extraction and focused-and-redgreen tasks done; exact-head-full-gates stays pending. + +Fix PR #3610 remains open/draft. Last audited head `afdd38ff43c64696153372fc2e27a38aff208c73` causally addresses the4failures, but combined runtime/CI proof is pending. Its older foundation lacks4commits in WP400's850base; replaying onto it would change8sourcefiles in the verification tree. Main retains the current branch and watches the upstream fix, then will re-audit/restack onto a verified base containing both sets. No old receipt can certify a future head. diff --git a/devlog/_plan/260905_now_split_train/410_clients_config_export_b.md b/devlog/_plan/260905_now_split_train/410_clients_config_export_b.md new file mode 100644 index 0000000000..698bf5c463 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/410_clients_config_export_b.md @@ -0,0 +1,433 @@ +# 410 — S13 L2/5: finish client path and format partitions + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. Bounded delegated **docs-only C3** task; parent owns orchestration, loop and goal state. +- Goal: finish client path and format partitions, preserving the original public import path and behavior. +- Non-goals: behavior fixes, exported renames, signature changes, new validation, changed credentials/admission policy, changed config paths, new framework, caller migration, merges or releases. Preserve function bodies verbatim, including >50-line functions; function redesign is not this pure-move train. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; every layer must pass independently at its actual tip. Full suite on `ssh lidge` only, never locally. +- Stop: exact-tip acceptance evidence recorded; do not merge. This drafting task stops after document checks and runs no tests, code entrypoints, or Git mutations. +- Escalation: parent must resolve the 002 size-budget contradiction before execution. This layer moves **1041 original lines** including attached comments/whitespace: plain added+deleted churn is at least **2082 lines** before glue. Even a move-count-once interpretation fails: #a moves 707 lines and #b 1,041. At least 1,590 original lines must leave a 1,990-line file to reach 400, so two 500-line layers cannot meet that target. Request an explicit pure-move churn exception or a parent-approved topology expansion; do not silently waive the gate or edit 002. Stale source, a leaf >400, any new cycle, or any behavioral difference also stops implementation. + +Basis: task docs HEAD `4cc219549`; code `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. Read 000, 001, S13 rows/Per-layer gate of 002, and the relevant records in `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md`. Source was read with `git show origin/dev:`; `git diff origin/dev -- src/clients/config-export.ts src/cli/opencode.ts src/cli/minimax.ts src/integrations/state.ts` was empty. Older tips in 000/001 are historical, not this plan's code basis. + +Structural decision (cxc-dev §1/§5, architecture ARCH-MAP-01/ARCH-DECISION-01): 1990 lines mix distinct concerns. Reject deleting/configuring the feature (does not preserve behavior), and generic helpers/index barrels (do not establish ownership). Reuse every existing algorithm and lower-level dependency; only relocate declarations. Inspected conventions: `src/config/paths.ts`, `src/config/process-state.ts`, `src/cli/launcher-context.ts`, `src/cli/account-extended.ts`, `src/integrations/ownership-policy.ts`. Use the domain subfolder `src/clients/config-export/` without an index barrel. The original remains an existing compatibility boundary, not an internal import shortcut. + +Structural map: 33 direct source/test/fixture consumer files. Production dependents: `src/integrations/state.ts`, `src/integrations/ownership.ts`, `src/integrations/merge.ts`, `src/integrations/registry.ts`, `src/integrations/owned-refresh.ts`, `src/integrations/config-io.ts`, `src/integrations/ownership-policy.ts`, `src/integrations/writer.ts`, `src/server/management/model-routes.ts`, `src/server/management/model-rows.ts`, `src/cli/export-command.ts`, `src/cli/minimax.ts`, `src/cli/opencode.ts`. Current direction is dependents → original → existing imported owners; intended direction is dependents → original → concern leaves → existing owners. Leaf imports are fully enumerated below; no leaf → original edge. Blast radius: client/CLI integration feature, with public consumers unchanged. `structure/09_client-integrations.md:11` identifies builders and classification as single authorities; no parallel implementation is introduced. + +## Symbol inventory + +Exact syntax spans at `origin/dev:src/clients/config-export.ts` (leading comments excluded). Reproduce: `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration,variable_declaration,class_declaration' --json=compact src/clients/config-export.ts`, filtering declarations enclosed by another declaration. Consumers = distinct direct importer/re-exporter files per symbol, resolved by literal module path then counted with `rg -l -w '' `. Dynamic dispatch destructuring counts too. Private declarations have 0 external consumers, not 0 local calls. Imported bindings are covered by the leaf imports; export-only declarations are noted below. L2 repeats the complete basis inventory and marks L1-owned rows already moved. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ManagedFragment` | interface | 43–46 | yes | 2 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `ManagedContribution` | interface | 49–52 | yes | 5 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `BuildContribution` | type | 54–54 | yes | 0 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `OpencodeLaunchEnv` | interface | 56–58 | yes | 1 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `OpencodeCatalogModel` | interface | 61–76 | yes | 1 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `OpencodeModelEntry` | interface | 78–81 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeModelVariant` | interface | 90–93 | yes | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeV2ModelEntry` | interface | 95–97 | yes | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeProviderConnection` | interface | 100–104 | yes | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeProviderBlock` | interface | 107–112 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeV2ProviderBlock` | interface | 115–120 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeProviderBlocks` | interface | 127–130 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `OpencodeGeneratedConfig` | interface | 132–138 | yes | 4 | `src/clients/config-export/opencode.ts` (L2) | +| `OPENCODE_PROVIDER_ID` | const | 141–141 | yes | 11 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `OPENCODE_CONFIG_SCHEMA` | const | 143–143 | yes | 2 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `OPENCODE_PROVIDER_NPM` | const | 149–149 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `OPENCODE_V2_PROVIDER_PACKAGE` | const | 161–161 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `OPENCODE_PROVIDER_NAME` | const | 164–164 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `OPENCODE_API_KEY_ENV` | const | 171–171 | yes | 3 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `OPENCODE_API_KEY_ENV_REF` | const | 174–174 | yes | 2 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `HERMES_API_KEY_ENV` | const | 180–180 | yes | 0 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `HERMES_API_KEY_ENV_REF` | const | 181–181 | yes | 2 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `OPENCLAW_API_KEY_ENV` | const | 184–184 | yes | 0 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `OPENCLAW_API_KEY_ENV_REF` | const | 185–185 | yes | 2 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `LOOPBACK_API_KEY_PLACEHOLDER` | const | 193–193 | yes | 9 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `GAJAE_API_KEY_ENV` | const | 200–200 | yes | 2 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `PI_API_DIALECT` | const | 203–203 | no | 0 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `SCHEMA_REQUIRED_OUTPUT_BUDGET` | const | 217–217 | yes | 2 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG` | const | 220–225 | yes | 1 | `src/clients/config-export/constants.ts` (L1; already moved) | +| `opencodeGlobalConfigPath` | function | 231–237 | yes | 3 | `src/clients/config-export/paths.ts` (L2) | +| `OMP_PROFILE_NAME_RE` | const | 239–239 | no | 0 | `src/clients/config-export/paths.ts` (L2) | +| `OMP_WINDOWS_RESERVED_PROFILE_RE` | const | 240–240 | no | 0 | `src/clients/config-export/paths.ts` (L2) | +| `ompProfileName` | function | 242–258 | no | 0 | `src/clients/config-export/paths.ts` (L2) | +| `piAgentDir` | function | 270–274 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `piConfigPath` | function | 277–279 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `ompAgentDir` | function | 282–293 | yes | 1 | `src/clients/config-export/paths.ts` (L2) | +| `ompModelsConfigPath` | function | 296–301 | yes | 4 | `src/clients/config-export/paths.ts` (L2) | +| `opencodeProxyBaseUrl` | function | 304–316 | yes | 4 | `src/clients/config-export/opencode.ts` (L2) | +| `hermesHomeDir` | function | 322–330 | yes | 1 | `src/clients/config-export/paths.ts` (L2) | +| `hermesConfigPath` | function | 332–334 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `ClientPathError` | class | 350–350 | yes | 12 | `src/clients/config-export/paths.ts` (L2) | +| `absoluteClientPath` | function | 352–363 | no | 0 | `src/clients/config-export/paths.ts` (L2) | +| `openclawEffectiveHome` | function | 372–375 | no | 0 | `src/clients/config-export/openclaw-paths.ts` (L2) | +| `openclawHomeDir` | function | 393–413 | yes | 2 | `src/clients/config-export/openclaw-paths.ts` (L2) | +| `openclawConfigPath` | function | 427–457 | yes | 2 | `src/clients/config-export/openclaw-paths.ts` (L2) | +| `kimiHomeDir` | function | 459–462 | yes | 1 | `src/clients/config-export/paths.ts` (L2) | +| `kimiConfigPath` | function | 464–466 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `gajaeHomeDir` | function | 468–470 | yes | 1 | `src/clients/config-export/paths.ts` (L2) | +| `gajaeConfigPath` | function | 472–474 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `dshHomeDir` | function | 477–492 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `dshConfigPath` | function | 494–496 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `mcodeHomeDir` | function | 503–509 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `mcodeConfigPath` | function | 511–513 | yes | 3 | `src/clients/config-export/paths.ts` (L2) | +| `zcodeHomeDir` | function | 521–525 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `zcodeConfigPath` | function | 527–529 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `primeAgentDir` | function | 540–544 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `primeConfigPath` | function | 547–549 | yes | 2 | `src/clients/config-export/paths.ts` (L2) | +| `asideHomeDir` | function | 558–560 | yes | 1 | `src/clients/config-export/aside-paths.ts` (L2) | +| `asideCurrentAccountId` | function | 584–612 | no | 0 | `src/clients/config-export/aside-paths.ts` (L2) | +| `asideAccountDir` | function | 619–622 | yes | 2 | `src/clients/config-export/aside-paths.ts` (L2) | +| `asideConfigPath` | function | 625–627 | yes | 2 | `src/clients/config-export/aside-paths.ts` (L2) | +| `ExportModel` | interface | 634–647 | yes | 18 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `ExportContext` | interface | 649–658 | yes | 8 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `ExportClientId` | type | 660–672 | yes | 3 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `ExportClientSpec` | interface | 674–713 | yes | 0 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `authoritativeContextWindow` | function | 719–725 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `outputBudgetFor` | function | 728–730 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `CLIENT_INPUT_MODALITIES` | const | 761–764 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `inputModalitiesForClient` | function | 767–779 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `dshInputModalities` | function | 782–791 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `exportModelLabel` | function | 798–805 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `opencodeProviderConnection` | function | 808–818 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `opencodeEffortVariants` | function | 833–840 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `opencodeProviderBlocks` | function | 855–894 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `opencodeProviderBlock` | function | 897–903 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `opencodeV2ProviderBlock` | function | 906–912 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `buildOpencodeProviderBlockFromCatalog` | function | 919–926 | yes | 1 | `src/clients/config-export/opencode.ts` (L2) | +| `normalizeExportModels` | function | 934–943 | yes | 2 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `buildOpencodeClientConfig` | function | 953–962 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `PiModelEntry` | interface | 964–979 | yes | 0 | `src/clients/config-export/contracts.ts` (L1; already moved) | +| `PiProviderBlock` | interface | 981–986 | yes | 0 | `src/clients/config-export/pi.ts` (L2) | +| `PiGeneratedConfig` | interface | 988–990 | yes | 6 | `src/clients/config-export/pi.ts` (L2) | +| `OmpModelEntry` | interface | 997–1006 | yes | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `OmpProviderBlock` | interface | 1008–1013 | yes | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `OmpGeneratedConfig` | interface | 1015–1017 | yes | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `OMP_EFFORT_VOCABULARY` | const | 1023–1023 | no | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `ompEfforts` | function | 1025–1034 | no | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `HermesProviderBlock` | interface | 1041–1049 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `HermesModelEntry` | interface | 1052–1054 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `HermesGeneratedConfig` | interface | 1056–1058 | yes | 4 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `OpenclawModelEntry` | interface | 1060–1064 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `OpenclawProviderBlock` | interface | 1066–1072 | yes | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `OpenclawGeneratedConfig` | interface | 1075–1080 | yes | 2 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `KimiProviderBlock` | interface | 1082–1086 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `KimiModelBlock` | interface | 1095–1100 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `KimiGeneratedConfig` | interface | 1102–1105 | yes | 2 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `GajaeModelEntry` | interface | 1107–1113 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `GajaeProviderBlock` | interface | 1116–1121 | yes | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `GajaeGeneratedConfig` | interface | 1123–1125 | yes | 3 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `DshReasoningEffort` | type | 1127–1127 | yes | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `DshWireReasoningEffort` | type | 1128–1128 | yes | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `DshModelEntry` | interface | 1130–1136 | yes | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `DshProviderBlock` | interface | 1138–1144 | yes | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `DshGeneratedConfig` | interface | 1146–1150 | yes | 2 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `McodeProviderBlock` | interface | 1152–1163 | yes | 0 | `src/clients/config-export/mcode.ts` (L1; already moved) | +| `McodeModelEntry` | interface | 1165–1170 | yes | 0 | `src/clients/config-export/mcode.ts` (L1; already moved) | +| `McodeGeneratedConfig` | interface | 1172–1174 | yes | 2 | `src/clients/config-export/mcode.ts` (L1; already moved) | +| `ZcodeModelEntry` | interface | 1183–1187 | yes | 0 | `src/clients/config-export/zcode.ts` (L1; already moved) | +| `ZcodeProviderBlock` | interface | 1189–1200 | yes | 0 | `src/clients/config-export/zcode.ts` (L1; already moved) | +| `ZcodeGeneratedConfig` | interface | 1202–1204 | yes | 1 | `src/clients/config-export/zcode.ts` (L1; already moved) | +| `buildPiClientConfig` | function | 1229–1276 | no | 0 | `src/clients/config-export/pi.ts` (L2) | +| `buildOmpClientConfig` | function | 1283–1321 | no | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `proxyAdmissionHeaders` | function | 1324–1326 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `buildHermesClientConfig` | function | 1328–1349 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `buildOpenclawClientConfig` | function | 1351–1375 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `kimiModelAlias` | function | 1378–1380 | yes | 1 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `buildKimiClientConfig` | function | 1382–1407 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `buildGajaeClientConfig` | function | 1409–1438 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `DSH_EFFORT_ORDER` | const | 1440–1440 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `dshReasoningEfforts` | function | 1442–1462 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `isKnownSafeDshCombo` | function | 1464–1483 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `buildDshClientConfig` | function | 1485–1516 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `buildMcodeClientConfig` | function | 1527–1559 | no | 0 | `src/clients/config-export/mcode.ts` (L1; already moved) | +| `buildZcodeClientConfig` | function | 1570–1606 | no | 0 | `src/clients/config-export/zcode.ts` (L1; already moved) | +| `summarizeOpencode` | function | 1614–1617 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `summarizePi` | function | 1619–1622 | no | 0 | `src/clients/config-export/pi.ts` (L2) | +| `summarizeOmp` | function | 1624–1627 | no | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `summarizeHermes` | function | 1629–1633 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `summarizeOpenclaw` | function | 1635–1638 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `summarizeKimi` | function | 1640–1645 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `summarizeGajae` | function | 1647–1650 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `summarizeDsh` | function | 1652–1655 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `summarizeMcode` | function | 1657–1660 | no | 0 | `src/clients/config-export/mcode.ts` (L1; already moved) | +| `summarizeZcode` | function | 1662–1665 | no | 0 | `src/clients/config-export/zcode.ts` (L1; already moved) | +| `singleFragment` | function | 1668–1670 | no | 0 | `src/clients/config-export/model-metadata.ts` (L1; already moved) | +| `buildOpencodeContribution` | function | 1672–1684 | no | 0 | `src/clients/config-export/opencode.ts` (L2) | +| `buildPiContribution` | function | 1686–1689 | no | 0 | `src/clients/config-export/pi.ts` (L2) | +| `buildOmpContribution` | function | 1691–1694 | no | 0 | `src/clients/config-export/omp.ts` (L1; already moved) | +| `buildHermesContribution` | function | 1696–1699 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `buildOpenclawContribution` | function | 1701–1704 | no | 0 | `src/clients/config-export/hermes-openclaw.ts` (L2) | +| `buildKimiContribution` | function | 1711–1720 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `buildGajaeContribution` | function | 1722–1725 | no | 0 | `src/clients/config-export/kimi-gajae.ts` (L2) | +| `buildDshContribution` | function | 1727–1730 | no | 0 | `src/clients/config-export/dsh.ts` (L1; already moved) | +| `buildMcodeContribution` | function | 1732–1735 | no | 0 | `src/clients/config-export/mcode.ts` (L1; already moved) | +| `buildZcodeContribution` | function | 1737–1740 | no | 0 | `src/clients/config-export/zcode.ts` (L1; already moved) | +| `buildPrimeContribution` | function | 1755–1758 | no | 0 | `src/clients/config-export/pi.ts` (L2) | +| `buildAsideContribution` | function | 1778–1781 | no | 0 | `src/clients/config-export/pi.ts` (L2) | +| `EXPORT_CLIENTS` | const | 1783–1954 | yes | 15 | `src/clients/config-export.ts` (residual) | +| `EXPORT_CLIENT_IDS` | const | 1956–1956 | yes | 7 | `src/clients/config-export.ts` (residual) | +| `isExportClientId` | function | 1958–1960 | yes | 3 | `src/clients/config-export.ts` (residual) | +| `buildClientConfig` | function | 1963–1965 | yes | 9 | `src/clients/config-export.ts` (residual) | +| `buildClientConfigText` | function | 1973–1985 | yes | 8 | `src/clients/config-export.ts` (residual) | +| `buildClientContribution` | function | 1988–1990 | yes | 5 | `src/clients/config-export.ts` (residual) | + +Export-only declaration: `ConfigFormat` at `src/clients/config-export.ts:32` remains forwarded from `../integrations/serialize`, not redefined. + +## Leaf partition + +Part a moves the lowest-fanout format leaves first: `omp` (sum of external symbol consumers 0), `zcode` (1), `dsh` (2), `mcode` (2). Part b takes the higher-fanout families and paths. The three shared foundations move with part a because even its lowest-fanout clients need them: leaving types/constants/model rules in the original would create facade back-imports. No external caller changes paths. PiModelEntry (0 consumers) moves with shared contracts because OmpModelEntry extends it. The larger Pi document type/builders remain for part b. + +Line-budget convention: each declaration carries immediately preceding comments/whitespace, from previous declaration end+1 (first declaration starts after the import/export header). Counts include those blocks, the exact one-line imports shown, one header line and one separator. These are conservative projected implementation counts, not measurements of files already written. Do not discard comments to meet limits. Adding an export keyword does not add a line. All new files are ≤400. + +### `src/clients/config-export/paths.ts` — expected 221 lines + +Symbols: `opencodeGlobalConfigPath`, `OMP_PROFILE_NAME_RE`, `OMP_WINDOWS_RESERVED_PROFILE_RE`, `ompProfileName`, `piAgentDir`, `piConfigPath`, `ompAgentDir`, `ompModelsConfigPath`, `hermesHomeDir`, `hermesConfigPath`, `ClientPathError`, `absoluteClientPath`, `kimiHomeDir`, `kimiConfigPath`, `gajaeHomeDir`, `gajaeConfigPath`, `dshHomeDir`, `dshConfigPath`, `mcodeHomeDir`, `mcodeConfigPath`, `zcodeHomeDir`, `zcodeConfigPath`, `primeAgentDir`, `primeConfigPath`. + +Own imports: + +```ts +import type { OpencodeLaunchEnv } from "./contracts"; +import { homedir } from "node:os"; +import { join, isAbsolute, resolve } from "node:path"; +import { existsSync } from "node:fs"; +``` + +Leaf exports: `opencodeGlobalConfigPath`, `piAgentDir`, `piConfigPath`, `ompAgentDir`, `ompModelsConfigPath`, `hermesHomeDir`, `hermesConfigPath`, `ClientPathError`, `absoluteClientPath`, `kimiHomeDir`, `kimiConfigPath`, `gajaeHomeDir`, `gajaeConfigPath`, `dshHomeDir`, `dshConfigPath`, `mcodeHomeDir`, `mcodeConfigPath`, `zcodeHomeDir`, `zcodeConfigPath`, `primeAgentDir`, `primeConfigPath`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/openclaw-paths.ts` — expected 101 lines + +Symbols: `openclawEffectiveHome`, `openclawHomeDir`, `openclawConfigPath`. + +Own imports: + +```ts +import type { OpencodeLaunchEnv } from "./contracts"; +import { absoluteClientPath } from "./paths"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { existsSync } from "node:fs"; +``` + +Leaf exports: `openclawHomeDir`, `openclawConfigPath`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/aside-paths.ts` — expected 85 lines + +Symbols: `asideHomeDir`, `asideCurrentAccountId`, `asideAccountDir`, `asideConfigPath`. + +Own imports: + +```ts +import type { OpencodeLaunchEnv } from "./contracts"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { readFileSync } from "node:fs"; +import { ClientPathError } from "./paths"; +``` + +Leaf exports: `asideHomeDir`, `asideAccountDir`, `asideConfigPath`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/opencode.ts` — expected 272 lines + +Symbols: `OpencodeModelEntry`, `OpencodeModelVariant`, `OpencodeV2ModelEntry`, `OpencodeProviderConnection`, `OpencodeProviderBlock`, `OpencodeV2ProviderBlock`, `OpencodeProviderBlocks`, `OpencodeGeneratedConfig`, `OPENCODE_PROVIDER_NPM`, `OPENCODE_V2_PROVIDER_PACKAGE`, `OPENCODE_PROVIDER_NAME`, `opencodeProxyBaseUrl`, `opencodeProviderConnection`, `opencodeEffortVariants`, `opencodeProviderBlocks`, `opencodeProviderBlock`, `opencodeV2ProviderBlock`, `buildOpencodeProviderBlockFromCatalog`, `buildOpencodeClientConfig`, `summarizeOpencode`, `buildOpencodeContribution`. + +Own imports: + +```ts +import type { OcxConfig } from "../../types"; +import { standaloneCodexRoutingTarget, shouldInjectApiAuthHeader } from "../../codex/inject"; +import { probeHostname } from "../../server/proxy-liveness"; +import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID } from "./constants"; +import type { OpencodeCatalogModel, ExportContext, ManagedContribution } from "./contracts"; +import { canonicalizeReasoningEfforts } from "../../reasoning-effort"; +import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels } from "./model-metadata"; +``` + +Leaf exports: `OpencodeModelEntry`, `OpencodeModelVariant`, `OpencodeV2ModelEntry`, `OpencodeProviderConnection`, `OpencodeProviderBlock`, `OpencodeV2ProviderBlock`, `OpencodeProviderBlocks`, `OpencodeGeneratedConfig`, `opencodeProxyBaseUrl`, `opencodeProviderBlocks`, `opencodeV2ProviderBlock`, `buildOpencodeProviderBlockFromCatalog`, `buildOpencodeClientConfig`, `summarizeOpencode`, `buildOpencodeContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/pi.ts` — expected 139 lines + +Symbols: `PiProviderBlock`, `PiGeneratedConfig`, `buildPiClientConfig`, `summarizePi`, `buildPiContribution`, `buildPrimeContribution`, `buildAsideContribution`. + +Own imports: + +```ts +import type { PiModelEntry, ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, outputBudgetFor, singleFragment } from "./model-metadata"; +import { OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; +``` + +Leaf exports: `PiProviderBlock`, `PiGeneratedConfig`, `buildPiClientConfig`, `summarizePi`, `buildPiContribution`, `buildPrimeContribution`, `buildAsideContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/hermes-openclaw.ts` — expected 121 lines + +Symbols: `HermesProviderBlock`, `HermesModelEntry`, `HermesGeneratedConfig`, `OpenclawModelEntry`, `OpenclawProviderBlock`, `OpenclawGeneratedConfig`, `buildHermesClientConfig`, `buildOpenclawClientConfig`, `summarizeHermes`, `summarizeOpenclaw`, `buildHermesContribution`, `buildOpenclawContribution`. + +Own imports: + +```ts +import type { ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, proxyAdmissionHeaders, authoritativeContextWindow, exportModelLabel, singleFragment } from "./model-metadata"; +import { HERMES_API_KEY_ENV_REF, OPENCODE_PROVIDER_ID, OPENCLAW_API_KEY_ENV_REF } from "./constants"; +``` + +Leaf exports: `HermesProviderBlock`, `HermesModelEntry`, `HermesGeneratedConfig`, `OpenclawModelEntry`, `OpenclawProviderBlock`, `OpenclawGeneratedConfig`, `buildHermesClientConfig`, `buildOpenclawClientConfig`, `summarizeHermes`, `summarizeOpenclaw`, `buildHermesContribution`, `buildOpenclawContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/clients/config-export/kimi-gajae.ts` — expected 146 lines + +Symbols: `KimiProviderBlock`, `KimiModelBlock`, `KimiGeneratedConfig`, `GajaeModelEntry`, `GajaeProviderBlock`, `GajaeGeneratedConfig`, `kimiModelAlias`, `buildKimiClientConfig`, `buildGajaeClientConfig`, `summarizeKimi`, `summarizeGajae`, `buildKimiContribution`, `buildGajaeContribution`. + +Own imports: + +```ts +import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV } from "./constants"; +import type { ExportContext, ManagedContribution, ManagedFragment } from "./contracts"; +import { normalizeExportModels, authoritativeContextWindow, inputModalitiesForClient, exportModelLabel, outputBudgetFor, singleFragment } from "./model-metadata"; +``` + +Leaf exports: `KimiProviderBlock`, `KimiModelBlock`, `KimiGeneratedConfig`, `GajaeModelEntry`, `GajaeProviderBlock`, `GajaeGeneratedConfig`, `kimiModelAlias`, `buildKimiClientConfig`, `buildGajaeClientConfig`, `summarizeKimi`, `summarizeGajae`, `buildKimiContribution`, `buildGajaeContribution`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +Residual `src/clients/config-export.ts`: expected **273 lines**. Part a leaves 1,299; part b removes 1,041 additional original lines and replaces staged glue with final glue. No #c remains. + +Retained declarations after this layer: `EXPORT_CLIENTS`, `EXPORT_CLIENT_IDS`, `isExportClientId`, `buildClientConfig`, `buildClientConfigText`, `buildClientContribution`. + +Arithmetic: 1990 original − 1748 cumulative moved original lines + 31 facade glue = 273. Across a/b: 707 + 1,041 = 1,748 moved body/trivia lines; 242 retained original lines; 1,748 + 242 = 1,990. Final glue is 31 lines, giving 273; L1's 16 glue lines are replaced by L2's 31, not both counted. + +## Re-export block + +Exact forwards in the original path follow. Other public declarations remain exported in place. No wildcard, alias, wrapper, signature change or duplicate definition. + +```ts +export type { ConfigFormat } from "../integrations/serialize"; +export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry } from "./config-export/contracts"; +export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants"; +export { normalizeExportModels } from "./config-export/model-metadata"; +export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp"; +export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode"; +export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh"; +export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode"; +export { opencodeGlobalConfigPath, piAgentDir, piConfigPath, ompAgentDir, ompModelsConfigPath, hermesHomeDir, hermesConfigPath, ClientPathError, kimiHomeDir, kimiConfigPath, gajaeHomeDir, gajaeConfigPath, dshHomeDir, dshConfigPath, mcodeHomeDir, mcodeConfigPath, zcodeHomeDir, zcodeConfigPath, primeAgentDir, primeConfigPath } from "./config-export/paths"; +export { openclawHomeDir, openclawConfigPath } from "./config-export/openclaw-paths"; +export { asideHomeDir, asideAccountDir, asideConfigPath } from "./config-export/aside-paths"; +export { opencodeProxyBaseUrl, opencodeProviderBlocks, opencodeV2ProviderBlock, buildOpencodeProviderBlockFromCatalog } from "./config-export/opencode"; +export type { OpencodeModelEntry, OpencodeModelVariant, OpencodeV2ModelEntry, OpencodeProviderConnection, OpencodeProviderBlock, OpencodeV2ProviderBlock, OpencodeProviderBlocks, OpencodeGeneratedConfig } from "./config-export/opencode"; +export type { PiProviderBlock, PiGeneratedConfig } from "./config-export/pi"; +export type { HermesProviderBlock, HermesModelEntry, HermesGeneratedConfig, OpenclawModelEntry, OpenclawProviderBlock, OpenclawGeneratedConfig } from "./config-export/hermes-openclaw"; +export { kimiModelAlias } from "./config-export/kimi-gajae"; +export type { KimiProviderBlock, KimiModelBlock, KimiGeneratedConfig, GajaeModelEntry, GajaeProviderBlock, GajaeGeneratedConfig } from "./config-export/kimi-gajae"; +``` + +Explicit residual local imports (re-export binds nothing locally): + +```ts +import type { ExportClientId, ExportClientSpec, ExportContext, ManagedContribution } from "./config-export/contracts"; +import { opencodeGlobalConfigPath, piConfigPath, ompModelsConfigPath, hermesConfigPath, kimiConfigPath, gajaeConfigPath, dshConfigPath, mcodeConfigPath, zcodeConfigPath, primeConfigPath } from "./config-export/paths"; +import { OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV, GAJAE_API_KEY_ENV } from "./config-export/constants"; +import { buildOpencodeClientConfig, summarizeOpencode, buildOpencodeContribution } from "./config-export/opencode"; +import { buildPiClientConfig, summarizePi, buildPiContribution, buildPrimeContribution, buildAsideContribution } from "./config-export/pi"; +import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp"; +import { buildHermesClientConfig, summarizeHermes, buildHermesContribution, buildOpenclawClientConfig, summarizeOpenclaw, buildOpenclawContribution } from "./config-export/hermes-openclaw"; +import { openclawConfigPath } from "./config-export/openclaw-paths"; +import { buildKimiClientConfig, summarizeKimi, buildKimiContribution, buildGajaeClientConfig, summarizeGajae, buildGajaeContribution } from "./config-export/kimi-gajae"; +import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh"; +import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode"; +import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode"; +import { asideConfigPath } from "./config-export/aside-paths"; +``` + +Retain original external imports still used by the residual; prune only proven-unused bindings. New leaves import one another directly. This is the cumulative block, including L1 forwards; replace the staged block instead of appending duplicate forwards. + +## Module-level state and cycles + +L1 already owns `CLIENT_INPUT_MODALITIES` (`src/clients/config-export.ts:761–764`) in `config-export/model-metadata.ts` and `OMP_EFFORT_VOCABULARY` (`:1023`) in `config-export/omp.ts`; do not recreate them. No top-level let/Map/WeakMap/timer/lock exists. `EXPORT_CLIENTS` (`:1783–1954`) and `EXPORT_CLIENT_IDS` (`:1956`) remain in the original registry, preserving identity/key order/one-time evaluation. `ClientPathError` (`:350`) moves once to `config-export/paths.ts` so every existing instanceof check sees the same constructor. Non-global `OMP_PROFILE_NAME_RE`/`OMP_WINDOWS_RESERVED_PROFILE_RE` (`:239–240`) stay with ompProfileName. + +Lane 016's AST import BFS found no return path through the original. The partition avoids new return imports, including type-only ones. Risk: original → client leaf → original. Shared contracts/constants/model rules therefore move down in L1. `contracts.ts → ../../integrations/serialize` preserves ConfigFormat's actual owner; do not substitute config-io (which imports the original facade). OpenClaw/Aside paths import paths.ts for the single constructor/absolute-path rule; paths.ts imports no path sibling. Only the residual registry composes all client builders. Private builders/summarizers become explicit leaf exports for that production registry; no duplicated closures. + +Coupling classification: existing config-schema coupling stays with format owners; sequential/functional coupling is explicit through parameters. No new common mutable state or temporal startup constraint. Existing auth/ownership checks are moved verbatim. Before execution rerun lane 016 method G against the actual layer base (relative static imports, re-exports, type-only edges and literal dynamic imports); any new return path is escalation, not permission for a lazy-import workaround. + +## Tests + +Discovery: `rg -l 'src/clients/config-export' tests --glob '*.ts'`, followed by import/source-read inspection. Every direct test/fixture importer is listed below, with disposition **unchanged** (old public path): + +- `tests/ci-workflows/dsh-path-contract.test.ts` — unchanged. +- `tests/ci-workflows/dsh-writer-lock.test.ts` — unchanged. +- `tests/cli/cli-help.test.ts` — unchanged. +- `tests/clients/client-export-modality-enum.test.ts` — unchanged. +- `tests/clients/integrations-state.test.ts` — unchanged. +- `tests/clients/integrations-writer.test.ts` — unchanged. +- `tests/clients/omp-path-contract.test.ts` — unchanged. +- `tests/clients/pi-path-contract.test.ts` — unchanged. +- `tests/clients/prime-client.test.ts` — unchanged. +- `tests/clients/sync-client-integrations.test.ts` — unchanged. +- `tests/config/client-config-export-new-clients.test.ts` — unchanged. +- `tests/config/client-config-export.test.ts` — unchanged. +- `tests/config/client-config-new-clients.test.ts` — unchanged. +- `tests/gui/integrations-invariants.test.ts` — unchanged. +- `tests/providers/aside-client.test.ts` — unchanged. +- `tests/providers/minimax-clients.test.ts` — unchanged. +- `tests/providers/zcode-client.test.ts` — unchanged. +- `tests/server/management-client-config-route.test.ts` — unchanged. +- `tests/server/management-integration-journal-delete.test.ts` — unchanged. +- `tests/server/management-integration-routes.test.ts` — unchanged. + +No source-text reader of src/clients/config-export.ts was found. `tests/config/client-config-export.test.ts:58` and `tests/server/management-client-config-route.test.ts:416` mention it in comments, not source reads. No retarget-to-leaf or add-leaf-to-scan-list action. Preserve baked serialized fixtures unchanged. + +C-phase red proof: temporarily treat incompatible audio-only input as text in the moved metadata function and observe `tests/clients/client-export-modality-enum.test.ts:96` fail; restore. Run all existing Pi/OMP/DSH path-contract tests unchanged against the final facade; no replacement expected paths. + +These are future implementation checks, not tests run by this docs author. No new test file is required. Facade/leaf identity assertions may be added in an existing focused test; if a new test file is required, parent must explicitly expand scope to include both test-layout registry files (`scripts/test-layout/layout.json`, `tests/fixtures/test-layout-expected.json`). Never commit red-proof mutations. + +## Verification + +Future implementation gate only, in the dedicated layer worktree at its actual tip. Domains: ci-workflows, cli, clients, config, gui, providers, server. Explicit source-reader and subprocess coverage is not replaced by test:changed. + +```sh +bun run typecheck +bun test tests/ci-workflows/dsh-path-contract.test.ts tests/ci-workflows/dsh-writer-lock.test.ts tests/cli/cli-help.test.ts tests/clients/client-export-modality-enum.test.ts tests/clients/integrations-state.test.ts tests/clients/integrations-writer.test.ts tests/clients/omp-path-contract.test.ts tests/clients/pi-path-contract.test.ts tests/clients/prime-client.test.ts tests/clients/sync-client-integrations.test.ts tests/config/client-config-export-new-clients.test.ts tests/config/client-config-export.test.ts tests/config/client-config-new-clients.test.ts tests/gui/integrations-invariants.test.ts tests/providers/aside-client.test.ts tests/providers/minimax-clients.test.ts tests/providers/zcode-client.test.ts tests/server/management-client-config-route.test.ts tests/server/management-integration-journal-delete.test.ts tests/server/management-integration-routes.test.ts tests/cli/cli-export-command.test.ts +bun run privacy:scan +wc -l src/clients/config-export/paths.ts src/clients/config-export/openclaw-paths.ts src/clients/config-export/aside-paths.ts src/clients/config-export/opencode.ts src/clients/config-export/pi.ts src/clients/config-export/hermes-openclaw.ts src/clients/config-export/kimi-gajae.ts src/clients/config-export.ts +# Compare resolved old-path consumer identities/counts with the list in this plan +rg -n 'clients/config-export' src gui/src scripts tests +# Full suite on lidge only; parent serializes access to this shared remote checkout +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-clients-config-export-b && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test' +``` + +The remote command intentionally keeps bun run test last, preserving its exit code instead of masking failure behind tail. Parent records remote HEAD and full output. Every command exits 0; focused/full tests report 0 failures. Delivery requires a green exact-head GitHub CI rollup, not an empty required-check list. + +Per 002, `bun test tests/lab/core-lab-boundary.test.ts` is conditional on source edits under `src/server|src/router|src/lib`: **not applicable** to this approved layer touch set. Do not edit its PROTECTED roots. If implementation expands into those directories, parent must approve scope and run that guard explicitly. Preserve the 33 original direct consumer files; new facade-to-leaf imports are not caller churn. The grep is a discovery list, not by itself a proof of consumer identity: resolve relative and dynamic paths as in the inventory method. Repeat lane 016 method G on the final imports to prove zero new cycles; typecheck alone is not a cycle detector. + +Drafting verification is document-only: required heading order, complete symbol ranges/ownership, projected line arithmetic, export coverage, referenced test paths, unique leaf paths and assigned-file scope. No test, typecheck, privacy scan or remote command above was executed in this drafting task. + +## Accept criteria + +1. Parent resolves the 500-line budget definition/exception or revises topology before implementation; no claim that literal added+deleted churn passes. +2. Every inventory declaration has exactly one implementation owner. Preserve all original export names/signatures and value/type importability; do not extract L1 declarations a second time. +3. Every new leaf is ≤400 lines. Residual target is 273, ≤400. Measure actual files and explain drift before proceeding. +4. Preserve function bodies, branch order, literals, serialized bytes/key order, class/object identity and state initialization. Only moves, explicit imports and named forwards change source structure. +5. Old-path consumers and assertions remain intact. Record the exact red/restored-green evidence named under Tests; no guard deletion, skipping, weakened assertions or empty-facade source scans. +6. Singleton state/allowlists each have one owner; no leaf imports the original even for types; resolved static/re-export/type/dynamic-literal graph has no new cycles. +7. Typecheck, focused checks, privacy, remote full suite and exact-head CI pass at this layer tip independently of later layers. No full local suite and no merge. +8. Diff stays within the original/new leaves and genuinely required existing focused tests. New tests, SoT edits, new topology or unrelated code require parent scope approval. + +## PR + +Title: `refactor(clients): finish client path and format partitions (split S13 L2/5)` + +Branch: `codex/split-clients-config-export-b`. Base: `codex/split-clients-config-export-a`. Closes: none. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), including the size-gate disposition and DEV-STACK-03 map below. This draft creates no PR; placeholder PR numbers are intentional. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S13-L1 | 400 | `codex/split-clients-config-export-a` | `dev` | extract low-fanout client formats and dependency foundations | +| 2 | #TBD-S13-L2 | 410 — this layer | `codex/split-clients-config-export-b` | `codex/split-clients-config-export-a` | finish client path and format partitions | +| 3 | #TBD-S13-L3 | 420 | `codex/split-cli-opencode` | `codex/split-clients-config-export-b` | separate OpenCode config and catalog from launch | +| 4 | #TBD-S13-L4 | 430 | `codex/split-cli-minimax` | `codex/split-cli-opencode` | isolate MMX protocol and termination owners | +| 5 | #TBD-S13-L5 | 440 | `codex/split-integrations-state` | `codex/split-clients-config-export-b` | separate classification from state reads | + +Depends on #TBD-S13-L1. Review this layer's diff only. Cascade this layer only from its real parent `codex/split-clients-config-export-a`, then re-verify its tip/base ref while preserving checkout ownership. Bottom-up merging remains a separate user-authorized action and is out of scope. diff --git a/devlog/_plan/260905_now_split_train/420_cli_opencode.md b/devlog/_plan/260905_now_split_train/420_cli_opencode.md new file mode 100644 index 0000000000..434e277cd0 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/420_cli_opencode.md @@ -0,0 +1,220 @@ +# 420 — S13 L3/5: separate OpenCode config and catalog from launch + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. Bounded delegated **docs-only C3** task; parent owns orchestration, loop and goal state. +- Goal: separate OpenCode config and catalog from launch, preserving the original public import path and behavior. +- Non-goals: behavior fixes, exported renames, signature changes, new validation, changed credentials/admission policy, changed config paths, new framework, caller migration, merges or releases. Preserve function bodies verbatim, including >50-line functions; function redesign is not this pure-move train. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; every layer must pass independently at its actual tip. Full suite on `ssh lidge` only, never locally. +- Stop: exact-tip acceptance evidence recorded; do not merge. This drafting task stops after document checks and runs no tests, code entrypoints, or Git mutations. +- Escalation: parent must resolve the 002 size-budget contradiction before execution. This layer moves **441 original lines** including attached comments/whitespace: plain added+deleted churn is at least **882 lines** before glue. If 500 means moved-once lines, this layer fits; ordinary additions plus deletions do not. Request an explicit pure-move churn exception or a parent-approved topology expansion; do not silently waive the gate or edit 002. Stale source, a leaf >400, any new cycle, or any behavioral difference also stops implementation. + +Basis: task docs HEAD `4cc219549`; code `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. Read 000, 001, S13 rows/Per-layer gate of 002, and the relevant records in `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md`. Source was read with `git show origin/dev:`; `git diff origin/dev -- src/clients/config-export.ts src/cli/opencode.ts src/cli/minimax.ts src/integrations/state.ts` was empty. Older tips in 000/001 are historical, not this plan's code basis. + +Structural decision (cxc-dev §1/§5, architecture ARCH-MAP-01/ARCH-DECISION-01): 682 lines mix distinct concerns. Reject deleting/configuring the feature (does not preserve behavior), and generic helpers/index barrels (do not establish ownership). Reuse every existing algorithm and lower-level dependency; only relocate declarations. Inspected conventions: `src/config/paths.ts`, `src/config/process-state.ts`, `src/cli/launcher-context.ts`, `src/cli/account-extended.ts`, `src/integrations/ownership-policy.ts`. Use named siblings in the existing directory. The original remains an existing compatibility boundary, not an internal import shortcut. + +Structural map: 5 direct source/test/fixture consumer files. Production dependents: `src/cli/export-command.ts`, `src/cli/minimax.ts`, `src/cli/dispatch.ts`. Current direction is dependents → original → existing imported owners; intended direction is dependents → original → concern leaves → existing owners. Leaf imports are fully enumerated below; no leaf → original edge. Blast radius: client/CLI integration feature, with public consumers unchanged. `structure/09_client-integrations.md:11` identifies builders and classification as single authorities; no parallel implementation is introduced. + +## Symbol inventory + +Exact syntax spans at `origin/dev:src/cli/opencode.ts` (leading comments excluded). Reproduce: `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration,variable_declaration,class_declaration' --json=compact src/cli/opencode.ts`, filtering declarations enclosed by another declaration. Consumers = distinct direct importer/re-exporter files per symbol, resolved by literal module path then counted with `rg -l -w '' `. Dynamic dispatch destructuring counts too. Private declarations have 0 external consumers, not 0 local calls. Imported bindings are covered by the leaf imports; export-only declarations are noted below. L2 repeats the complete basis inventory and marks L1-owned rows already moved. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `OpencodeRoutedModel` | interface | 78–87 | yes | 0 | `src/cli/opencode-catalog.ts` (L3) | +| `OpencodeProxyModelRow` | interface | 90–103 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `PROJECT_CONFIG_FILENAMES` | const | 105–105 | no | 0 | `src/cli/opencode-config.ts` (L3) | +| `OPENCODE_CONFIG_CONTENT_ENV` | const | 111–111 | yes | 1 | `src/cli/opencode.ts` (residual) | +| `isRecord` | function | 113–115 | no | 0 | `src/cli/opencode-config.ts` (L3) | +| `stripJsonComments` | function | 121–158 | no | 0 | `src/cli/opencode-config.ts` (L3) | +| `stripTrailingCommas` | function | 161–185 | no | 0 | `src/cli/opencode-config.ts` (L3) | +| `parseJsonc` | function | 192–198 | yes | 1 | `src/cli/opencode-config.ts` (L3) | +| `opencodeModelKey` | function | 201–203 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `opencodeLaunchNativeSlugs` | function | 209–212 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `opencodeLaunchCatalog` | function | 215–240 | no | 0 | `src/cli/opencode-catalog.ts` (L3) | +| `buildOpencodeProviderBlock` | function | 243–257 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `buildOpencodeV2ProviderBlock` | function | 263–277 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `buildOpencodeProviderBlocksFromCatalog` | function | 284–291 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `opencodeBlocks` | function | 293–300 | no | 0 | `src/cli/opencode-catalog.ts` (L3) | +| `OPENCODE_PROXY_MODELS_TIMEOUT_MS` | const | 303–303 | yes | 0 | `src/cli/opencode-catalog.ts` (L3) | +| `fetchOpencodeProxyModels` | function | 306–366 | yes | 1 | `src/cli/opencode-catalog.ts` (L3) | +| `opencodeCatalogFromProxyRows` | function | 372–401 | yes | 2 | `src/cli/opencode-catalog.ts` (L3) | +| `OpencodeRuntimeConfigError` | type | 403–403 | yes | 0 | `src/cli/opencode-config.ts` (L3) | +| `isOpencodeRuntimeConfigError` | function | 406–410 | yes | 1 | `src/cli/opencode-config.ts` (L3) | +| `mergeOpencodeRuntimeConfig` | function | 417–457 | yes | 1 | `src/cli/opencode-config.ts` (L3) | +| `buildOpencodeConfig` | function | 460–476 | yes | 1 | `src/cli/opencode.ts` (residual) | +| `serializeOpencodeRuntimeConfig` | function | 479–481 | yes | 1 | `src/cli/opencode-config.ts` (L3) | +| `findGitRoot` | function | 483–491 | no | 0 | `src/cli/opencode-config.ts` (L3) | +| `configFileDefinesProvider` | function | 498–509 | no | 0 | `src/cli/opencode-config.ts` (L3) | +| `opencodeProviderOverridePath` | function | 515–536 | yes | 1 | `src/cli/opencode-config.ts` (L3) | +| `projectConfigOverridesProvider` | function | 539–541 | yes | 1 | `src/cli/opencode-config.ts` (L3) | +| `serviceTokenLookupEnv` | function | 543–546 | no | 0 | `src/cli/opencode.ts` (residual) | +| `opencodeProxyStartEnv` | function | 553–558 | yes | 2 | `src/cli/opencode.ts` (residual) | +| `buildOpencodeEnv` | function | 566–578 | yes | 1 | `src/cli/opencode.ts` (residual) | +| `opencodeApiKey` | function | 584–590 | yes | 1 | `src/cli/opencode.ts` (residual) | +| `ensureProxyForOpencode` | function | 592–614 | no | 0 | `src/cli/opencode.ts` (residual) | +| `OPENCODE_INSTALL_HINT` | const | 616–616 | no | 0 | `src/cli/opencode.ts` (residual) | +| `opencodeNotFoundHint` | function | 623–629 | yes | 1 | `src/cli/opencode.ts` (residual) | +| `cmdOpencode` | function | 631–682 | yes | 1 | `src/cli/opencode.ts` (residual) | + +Export-only statements: `src/cli/opencode.ts:56–66` (9 values) and `:67–75` (7 types) remain forwarded from `../clients/config-export` exactly as shown below. + +## Leaf partition + +Keep launch/read orchestration in the original; no source-scanned spawn site or process singleton moves. This is a pure relocation, not a new adapter abstraction. + +Line-budget convention: each declaration carries immediately preceding comments/whitespace, from previous declaration end+1 (first declaration starts after the import/export header). Counts include those blocks, the exact one-line imports shown, one header line and one separator. These are conservative projected implementation counts, not measurements of files already written. Do not discard comments to meet limits. Adding an export keyword does not add a line. All new files are ≤400. + +### `src/cli/opencode-config.ts` — expected 217 lines + +Symbols: `PROJECT_CONFIG_FILENAMES`, `isRecord`, `stripJsonComments`, `stripTrailingCommas`, `parseJsonc`, `OpencodeRuntimeConfigError`, `isOpencodeRuntimeConfigError`, `mergeOpencodeRuntimeConfig`, `serializeOpencodeRuntimeConfig`, `findGitRoot`, `configFileDefinesProvider`, `opencodeProviderOverridePath`, `projectConfigOverridesProvider`. + +Own imports: + +```ts +import type { OpencodeGeneratedConfig, OpencodeProviderBlocks, OpencodeLaunchEnv } from "../clients/config-export"; +import { OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, opencodeGlobalConfigPath } from "../clients/config-export"; +import { existsSync, readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { homedir } from "node:os"; +``` + +Leaf exports: `parseJsonc`, `OpencodeRuntimeConfigError`, `isOpencodeRuntimeConfigError`, `mergeOpencodeRuntimeConfig`, `serializeOpencodeRuntimeConfig`, `opencodeProviderOverridePath`, `projectConfigOverridesProvider`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/cli/opencode-catalog.ts` — expected 240 lines + +Symbols: `OpencodeRoutedModel`, `OpencodeProxyModelRow`, `opencodeModelKey`, `opencodeLaunchNativeSlugs`, `opencodeLaunchCatalog`, `buildOpencodeProviderBlock`, `buildOpencodeV2ProviderBlock`, `buildOpencodeProviderBlocksFromCatalog`, `opencodeBlocks`, `OPENCODE_PROXY_MODELS_TIMEOUT_MS`, `fetchOpencodeProxyModels`, `opencodeCatalogFromProxyRows`. + +Own imports: + +```ts +import type { OcxConfig } from "../types"; +import { providerCodexAccountMode } from "../providers/registry"; +import { visibleNativeSlugs } from "../codex/catalog"; +import type { OpencodeCatalogModel, OpencodeProviderBlock, OpencodeV2ProviderBlock, OpencodeProviderBlocks } from "../clients/config-export"; +import { OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, buildOpencodeProviderBlockFromCatalog, opencodeProviderBlocks, opencodeProxyBaseUrl } from "../clients/config-export"; +import type { LiveProxy } from "../server/proxy-liveness"; +import { probeHostname } from "../server/proxy-liveness"; +``` + +Leaf exports: `OpencodeRoutedModel`, `OpencodeProxyModelRow`, `opencodeModelKey`, `opencodeLaunchNativeSlugs`, `buildOpencodeProviderBlock`, `buildOpencodeV2ProviderBlock`, `buildOpencodeProviderBlocksFromCatalog`, `OPENCODE_PROXY_MODELS_TIMEOUT_MS`, `fetchOpencodeProxyModels`, `opencodeCatalogFromProxyRows`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +Residual `src/cli/opencode.ts`: expected **251 lines**. No follow-up split is required for this original. + +Retained declarations after this layer: `OPENCODE_CONFIG_CONTENT_ENV`, `buildOpencodeConfig`, `serviceTokenLookupEnv`, `opencodeProxyStartEnv`, `buildOpencodeEnv`, `opencodeApiKey`, `ensureProxyForOpencode`, `OPENCODE_INSTALL_HINT`, `opencodeNotFoundHint`, `cmdOpencode`. + +Arithmetic: 682 original − 441 cumulative moved original lines + 10 facade glue = 251. Glue comprises new imports, compatibility exports and separators. Retained original header imports can be pruned if unused, only decreasing the estimate. + +## Re-export block + +Exact forwards in the original path follow. Other public declarations remain exported in place. No wildcard, alias, wrapper, signature change or duplicate definition. + +```ts +export { + OPENCODE_API_KEY_ENV, + OPENCODE_API_KEY_ENV_REF, + OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, + OPENCODE_PROVIDER_ID, + SCHEMA_REQUIRED_OUTPUT_BUDGET, + buildOpencodeProviderBlockFromCatalog, + opencodeGlobalConfigPath, + opencodeProxyBaseUrl, + opencodeV2ProviderBlock, +} from "../clients/config-export"; +export type { + OpencodeCatalogModel, + OpencodeGeneratedConfig, + OpencodeLaunchEnv, + OpencodeModelEntry, + OpencodeProviderBlock, + OpencodeProviderBlocks, + OpencodeV2ProviderBlock, +} from "../clients/config-export"; +export { parseJsonc, isOpencodeRuntimeConfigError, mergeOpencodeRuntimeConfig, serializeOpencodeRuntimeConfig, opencodeProviderOverridePath, projectConfigOverridesProvider } from "./opencode-config"; +export type { OpencodeRuntimeConfigError } from "./opencode-config"; +export { opencodeModelKey, opencodeLaunchNativeSlugs, buildOpencodeProviderBlock, buildOpencodeV2ProviderBlock, buildOpencodeProviderBlocksFromCatalog, OPENCODE_PROXY_MODELS_TIMEOUT_MS, fetchOpencodeProxyModels, opencodeCatalogFromProxyRows } from "./opencode-catalog"; +export type { OpencodeRoutedModel, OpencodeProxyModelRow } from "./opencode-catalog"; +``` + +Explicit residual local imports (re-export binds nothing locally): + +```ts +import type { OpencodeRoutedModel, OpencodeProxyModelRow } from "./opencode-catalog"; +import { mergeOpencodeRuntimeConfig, isOpencodeRuntimeConfigError, serializeOpencodeRuntimeConfig, opencodeProviderOverridePath } from "./opencode-config"; +import { buildOpencodeProviderBlock, buildOpencodeV2ProviderBlock, fetchOpencodeProxyModels, opencodeCatalogFromProxyRows, buildOpencodeProviderBlocksFromCatalog } from "./opencode-catalog"; +import type { OpencodeRuntimeConfigError } from "./opencode-config"; +``` + +Retain original external imports still used by the residual; prune only proven-unused bindings. New leaves import one another directly. + +## Module-level state and cycles + +No top-level let, Map, Set, WeakMap, lock or timer. `PROJECT_CONFIG_FILENAMES` (`src/cli/opencode.ts:105`) moves only to opencode-config.ts. The fetch timeout (`:317`) and seen Set (`:377`) remain call-local. `ensureProxyForOpencode` (`:592–614`), its provenance-stamped spawn (`:597–602`), and `cmdOpencode` (`:631–682`) stay in the original. No eager launch or catalog fetch on leaf import. + +Lane 016's AST import BFS found no return path through the original. The partition avoids new return imports, including type-only ones. Both leaves import the stable client-export boundary; neither imports cli/opencode.ts. opencode-catalog.ts does not import opencode-config.ts. The residual composes the two in buildOpencodeConfig, so catalog/config never need an upward dependency. Types needed by each moved body are colocated or imported from the existing clients boundary. + +Coupling classification: existing config-schema coupling stays with format owners; sequential/functional coupling is explicit through parameters. No new common mutable state or temporal startup constraint. Existing auth/ownership checks are moved verbatim. Before execution rerun lane 016 method G against the actual layer base (relative static imports, re-exports, type-only edges and literal dynamic imports); any new return path is escalation, not permission for a lazy-import workaround. + +## Tests + +Discovery: `rg -l 'src/cli/opencode' tests --glob '*.ts'`, followed by import/source-read inspection. Every direct test/fixture importer is listed below, with disposition **unchanged** (old public path): + +- `tests/config/client-config-export.test.ts` — unchanged. +- `tests/providers/opencode-cli.test.ts` — unchanged. + +Text oracle: `tests/ci-workflows/bun-runtime.test.ts:241` lists src/cli/opencode.ts; **:246** calls `readFileSync(repoPath(relative), "utf8")`; :247–257 count process.execPath spawns and provenance stamps. **Unchanged**: the detached spawn remains in the residual. No retarget-to-leaf or add-leaf-to-scan-list needed, since neither leaf spawns. C-phase red proof: temporarily remove the residual's withProcessRuntimeProvenance stamp, run that named guard, observe failure, restore and pass. Never replace its list entry with a zero-spawn leaf or weaken the nonzero-spawn assertion. Keep all JSONC, provider-generation, inherited-content, duplicate-row, effort and timeout assertions in opencode-cli.test.ts. + +These are future implementation checks, not tests run by this docs author. No new test file is required. Facade/leaf identity assertions may be added in an existing focused test; if a new test file is required, parent must explicitly expand scope to include both test-layout registry files (`scripts/test-layout/layout.json`, `tests/fixtures/test-layout-expected.json`). Never commit red-proof mutations. + +## Verification + +Future implementation gate only, in the dedicated layer worktree at its actual tip. Domains: config, providers, ci-workflows, cli. Explicit source-reader and subprocess coverage is not replaced by test:changed. + +```sh +bun run typecheck +bun test tests/config/client-config-export.test.ts tests/providers/opencode-cli.test.ts tests/ci-workflows/bun-runtime.test.ts tests/cli/cli-export-command.test.ts tests/providers/minimax-clients.test.ts +bun run privacy:scan +wc -l src/cli/opencode-config.ts src/cli/opencode-catalog.ts src/cli/opencode.ts +# Compare resolved old-path consumer identities/counts with the list in this plan +rg -n 'cli/opencode' src gui/src scripts tests +# Full suite on lidge only; parent serializes access to this shared remote checkout +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-cli-opencode && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test' +``` + +The remote command intentionally keeps bun run test last, preserving its exit code instead of masking failure behind tail. Parent records remote HEAD and full output. Every command exits 0; focused/full tests report 0 failures. Delivery requires a green exact-head GitHub CI rollup, not an empty required-check list. + +Per 002, `bun test tests/lab/core-lab-boundary.test.ts` is conditional on source edits under `src/server|src/router|src/lib`: **not applicable** to this approved layer touch set. Do not edit its PROTECTED roots. If implementation expands into those directories, parent must approve scope and run that guard explicitly. Preserve the 5 original direct consumer files; new facade-to-leaf imports are not caller churn. The grep is a discovery list, not by itself a proof of consumer identity: resolve relative and dynamic paths as in the inventory method. Repeat lane 016 method G on the final imports to prove zero new cycles; typecheck alone is not a cycle detector. + +Drafting verification is document-only: required heading order, complete symbol ranges/ownership, projected line arithmetic, export coverage, referenced test paths, unique leaf paths and assigned-file scope. No test, typecheck, privacy scan or remote command above was executed in this drafting task. + +## Accept criteria + +1. Parent resolves the 500-line budget definition/exception or revises topology before implementation; no claim that literal added+deleted churn passes. +2. Every inventory declaration has exactly one implementation owner. Preserve all original export names/signatures and value/type importability; do not extract L1 declarations a second time. +3. Every new leaf is ≤400 lines. Residual target is 251, ≤400. Measure actual files and explain drift before proceeding. +4. Preserve function bodies, branch order, literals, serialized bytes/key order, class/object identity and state initialization. Only moves, explicit imports and named forwards change source structure. +5. Old-path consumers and assertions remain intact. Record the exact red/restored-green evidence named under Tests; no guard deletion, skipping, weakened assertions or empty-facade source scans. +6. Singleton state/allowlists each have one owner; no leaf imports the original even for types; resolved static/re-export/type/dynamic-literal graph has no new cycles. +7. Typecheck, focused checks, privacy, remote full suite and exact-head CI pass at this layer tip independently of later layers. No full local suite and no merge. +8. Diff stays within the original/new leaves and genuinely required existing focused tests. New tests, SoT edits, new topology or unrelated code require parent scope approval. + +## PR + +Title: `refactor(cli): separate OpenCode config and catalog from launch (split S13 L3/5)` + +Branch: `codex/split-cli-opencode`. Base: `codex/split-clients-config-export-b`. Closes: none. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), including the size-gate disposition and DEV-STACK-03 map below. This draft creates no PR; placeholder PR numbers are intentional. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S13-L1 | 400 | `codex/split-clients-config-export-a` | `dev` | extract low-fanout client formats and dependency foundations | +| 2 | #TBD-S13-L2 | 410 | `codex/split-clients-config-export-b` | `codex/split-clients-config-export-a` | finish client path and format partitions | +| 3 | #TBD-S13-L3 | 420 — this layer | `codex/split-cli-opencode` | `codex/split-clients-config-export-b` | separate OpenCode config and catalog from launch | +| 4 | #TBD-S13-L4 | 430 | `codex/split-cli-minimax` | `codex/split-cli-opencode` | isolate MMX protocol and termination owners | +| 5 | #TBD-S13-L5 | 440 | `codex/split-integrations-state` | `codex/split-clients-config-export-b` | separate classification from state reads | + +Depends on #TBD-S13-L2. Review this layer's diff only. Cascade this layer only from its real parent `codex/split-clients-config-export-b`, then re-verify its tip/base ref while preserving checkout ownership. Bottom-up merging remains a separate user-authorized action and is out of scope. diff --git a/devlog/_plan/260905_now_split_train/430_cli_minimax.md b/devlog/_plan/260905_now_split_train/430_cli_minimax.md new file mode 100644 index 0000000000..a9ff33a761 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/430_cli_minimax.md @@ -0,0 +1,184 @@ +# 430 — S13 L4/5: isolate MMX protocol and termination owners + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. Bounded delegated **docs-only C3** task; parent owns orchestration, loop and goal state. +- Goal: isolate MMX protocol and termination owners, preserving the original public import path and behavior. +- Non-goals: behavior fixes, exported renames, signature changes, new validation, changed credentials/admission policy, changed config paths, new framework, caller migration, merges or releases. Preserve function bodies verbatim, including >50-line functions; function redesign is not this pure-move train. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; every layer must pass independently at its actual tip. Full suite on `ssh lidge` only, never locally. +- Stop: exact-tip acceptance evidence recorded; do not merge. This drafting task stops after document checks and runs no tests, code entrypoints, or Git mutations. +- Escalation: parent must resolve the 002 size-budget contradiction before execution. This layer moves **278 original lines** including attached comments/whitespace: plain added+deleted churn is at least **556 lines** before glue. If 500 means moved-once lines, this layer fits; ordinary additions plus deletions do not. Request an explicit pure-move churn exception or a parent-approved topology expansion; do not silently waive the gate or edit 002. Stale source, a leaf >400, any new cycle, or any behavioral difference also stops implementation. + +Basis: task docs HEAD `4cc219549`; code `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. Read 000, 001, S13 rows/Per-layer gate of 002, and the relevant records in `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md`. Source was read with `git show origin/dev:`; `git diff origin/dev -- src/clients/config-export.ts src/cli/opencode.ts src/cli/minimax.ts src/integrations/state.ts` was empty. Older tips in 000/001 are historical, not this plan's code basis. + +Structural decision (cxc-dev §1/§5, architecture ARCH-MAP-01/ARCH-DECISION-01): 497 lines mix distinct concerns. Reject deleting/configuring the feature (does not preserve behavior), and generic helpers/index barrels (do not establish ownership). Reuse every existing algorithm and lower-level dependency; only relocate declarations. Inspected conventions: `src/config/paths.ts`, `src/config/process-state.ts`, `src/cli/launcher-context.ts`, `src/cli/account-extended.ts`, `src/integrations/ownership-policy.ts`. Use named siblings in the existing directory. The original remains an existing compatibility boundary, not an internal import shortcut. + +Structural map: 3 direct source/test/fixture consumer files. Production dependents: `src/cli/dispatch.ts`. Current direction is dependents → original → existing imported owners; intended direction is dependents → original → concern leaves → existing owners. Leaf imports are fully enumerated below; no leaf → original edge. Blast radius: client/CLI integration feature, with public consumers unchanged. `structure/09_client-integrations.md:11` identifies builders and classification as single authorities; no parallel implementation is introduced. + +## Symbol inventory + +Exact syntax spans at `origin/dev:src/cli/minimax.ts` (leading comments excluded). Reproduce: `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration,variable_declaration,class_declaration' --json=compact src/cli/minimax.ts`, filtering declarations enclosed by another declaration. Consumers = distinct direct importer/re-exporter files per symbol, resolved by literal module path then counted with `rg -l -w '' `. Dynamic dispatch destructuring counts too. Private declarations have 0 external consumers, not 0 local calls. Imported bindings are covered by the leaf imports; export-only declarations are noted below. L2 repeats the complete basis inventory and marks L1-owned rows already moved. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `MinimaxLaunchEnv` | interface | 25–27 | yes | 0 | `src/cli/minimax-mmx.ts` (L4) | +| `MmxTextBridge` | interface | 29–33 | yes | 0 | `src/cli/minimax-mmx.ts` (L4) | +| `MmxTextBridgeOptions` | interface | 35–38 | yes | 0 | `src/cli/minimax-mmx.ts` (L4) | +| `MmxTerminationTarget` | interface | 40–45 | yes | 0 | `src/cli/minimax-termination.ts` (L4) | +| `MmxTerminationDeps` | interface | 47–50 | yes | 0 | `src/cli/minimax-termination.ts` (L4) | +| `MmxSignalHost` | interface | 52–55 | yes | 0 | `src/cli/minimax-termination.ts` (L4) | +| `MmxTerminationHandlersOptions` | interface | 57–64 | yes | 0 | `src/cli/minimax-termination.ts` (L4) | +| `MMX_TERMINATION_DUPLICATE_WINDOW_MS` | const | 66–66 | no | 0 | `src/cli/minimax-termination.ts` (L4) | +| `MMX_CHILD_OWNED_ENV_KEYS` | const | 68–76 | no | 0 | `src/cli/minimax-mmx.ts` (L4) | +| `MMX_GLOBAL_BOOLEAN_FLAGS` | const | 78–91 | no | 0 | `src/cli/minimax-mmx.ts` (L4) | +| `mmxCommandPath` | function | 94–113 | yes | 1 | `src/cli/minimax-mmx.ts` (L4) | +| `mmxUnsafeOverride` | function | 116–123 | yes | 1 | `src/cli/minimax-mmx.ts` (L4) | +| `buildMmxEnv` | function | 125–145 | yes | 1 | `src/cli/minimax-mmx.ts` (L4) | +| `startMmxTextBridge` | function | 153–227 | yes | 2 | `src/cli/minimax-mmx.ts` (L4) | +| `normalizedMcodeBaseUrl` | function | 229–243 | no | 0 | `src/cli/minimax.ts` (residual) | +| `mcodeOpenCodexBaseUrl` | function | 246–256 | yes | 1 | `src/cli/minimax.ts` (residual) | +| `usableMinimaxLiveProxy` | function | 259–262 | yes | 1 | `src/cli/minimax.ts` (residual) | +| `ensureProxy` | function | 264–285 | no | 0 | `src/cli/minimax.ts` (residual) | +| `isStandaloneInformationalInvocation` | function | 288–298 | yes | 1 | `src/cli/minimax.ts` (residual) | +| `forwardMmxTerminationSignal` | function | 301–324 | yes | 1 | `src/cli/minimax-termination.ts` (L4) | +| `installMmxTerminationHandlers` | function | 327–360 | yes | 1 | `src/cli/minimax-termination.ts` (L4) | +| `finishMmxClientCleanup` | function | 363–372 | yes | 1 | `src/cli/minimax-termination.ts` (L4) | +| `spawnClient` | function | 374–394 | no | 0 | `src/cli/minimax.ts` (residual) | +| `MCODE_INSTALL_HINT` | const | 396–396 | no | 0 | `src/cli/minimax.ts` (residual) | +| `MMX_INSTALL_HINT` | const | 397–397 | no | 0 | `src/cli/minimax.ts` (residual) | +| `cmdMcode` | function | 399–433 | yes | 1 | `src/cli/minimax.ts` (residual) | +| `cmdMmx` | function | 435–497 | yes | 1 | `src/cli/minimax.ts` (residual) | + +No other export-only top-level declaration exists. + +## Leaf partition + +Keep launch/read orchestration in the original; no source-scanned spawn site or process singleton moves. This is a pure relocation, not a new adapter abstraction. + +Line-budget convention: each declaration carries immediately preceding comments/whitespace, from previous declaration end+1 (first declaration starts after the import/export header). Counts include those blocks, the exact one-line imports shown, one header line and one separator. These are conservative projected implementation counts, not measurements of files already written. Do not discard comments to meet limits. Adding an export keyword does not add a line. All new files are ≤400. + +### `src/cli/minimax-mmx.ts` — expected 182 lines + +Symbols: `MinimaxLaunchEnv`, `MmxTextBridge`, `MmxTextBridgeOptions`, `MMX_CHILD_OWNED_ENV_KEYS`, `MMX_GLOBAL_BOOLEAN_FLAGS`, `mmxCommandPath`, `mmxUnsafeOverride`, `buildMmxEnv`, `startMmxTextBridge`. + +Own imports: + +```ts +import type { LiveProxy } from "../server/proxy-liveness"; +import { probeHostname } from "../server/proxy-liveness"; +import { LOOPBACK_API_KEY_PLACEHOLDER } from "../clients/config-export"; +import { clearableDeadline } from "../lib/abort"; +``` + +Leaf exports: `MinimaxLaunchEnv`, `MmxTextBridge`, `MmxTextBridgeOptions`, `mmxCommandPath`, `mmxUnsafeOverride`, `buildMmxEnv`, `startMmxTextBridge`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +### `src/cli/minimax-termination.ts` — expected 105 lines + +Symbols: `MmxTerminationTarget`, `MmxTerminationDeps`, `MmxSignalHost`, `MmxTerminationHandlersOptions`, `MMX_TERMINATION_DUPLICATE_WINDOW_MS`, `forwardMmxTerminationSignal`, `installMmxTerminationHandlers`, `finishMmxClientCleanup`. + +Own imports: + +```ts +import { execFileSync } from "node:child_process"; +``` + +Leaf exports: `MmxTerminationTarget`, `MmxTerminationDeps`, `MmxSignalHost`, `MmxTerminationHandlersOptions`, `forwardMmxTerminationSignal`, `installMmxTerminationHandlers`, `finishMmxClientCleanup`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +Residual `src/cli/minimax.ts`: expected **228 lines**. No follow-up split is required for this original. + +Retained declarations after this layer: `normalizedMcodeBaseUrl`, `mcodeOpenCodexBaseUrl`, `usableMinimaxLiveProxy`, `ensureProxy`, `isStandaloneInformationalInvocation`, `spawnClient`, `MCODE_INSTALL_HINT`, `MMX_INSTALL_HINT`, `cmdMcode`, `cmdMmx`. + +Arithmetic: 497 original − 278 cumulative moved original lines + 9 facade glue = 228. Glue comprises new imports, compatibility exports and separators. Retained original header imports can be pruned if unused, only decreasing the estimate. + +## Re-export block + +Exact forwards in the original path follow. Other public declarations remain exported in place. No wildcard, alias, wrapper, signature change or duplicate definition. + +```ts +export { mmxCommandPath, mmxUnsafeOverride, buildMmxEnv, startMmxTextBridge } from "./minimax-mmx"; +export type { MinimaxLaunchEnv, MmxTextBridge, MmxTextBridgeOptions } from "./minimax-mmx"; +export { forwardMmxTerminationSignal, installMmxTerminationHandlers, finishMmxClientCleanup } from "./minimax-termination"; +export type { MmxTerminationTarget, MmxTerminationDeps, MmxSignalHost, MmxTerminationHandlersOptions } from "./minimax-termination"; +``` + +Explicit residual local imports (re-export binds nothing locally): + +```ts +import { mmxUnsafeOverride, mmxCommandPath, startMmxTextBridge, buildMmxEnv } from "./minimax-mmx"; +import type { MmxTextBridge } from "./minimax-mmx"; +import { installMmxTerminationHandlers, finishMmxClientCleanup } from "./minimax-termination"; +``` + +Retain original external imports still used by the residual; prune only proven-unused bindings. New leaves import one another directly. + +## Module-level state and cycles + +`MMX_CHILD_OWNED_ENV_KEYS` (`src/cli/minimax.ts:68–76`) and `MMX_GLOBAL_BOOLEAN_FLAGS` (`:78–91`) have one owner, minimax-mmx.ts; both remain module-private Sets. No top-level let/Map/WeakMap/lock exists. `MMX_TERMINATION_DUPLICATE_WINDOW_MS` (`:66`) belongs to minimax-termination.ts. Timestamps/listeners at `:332–360` remain per handler installation. bridge/child/cleanupPromise declarations in cmdMmx remain closure-local; never hoist them into a module singleton. Preserve cleanup, duplicate suppression and listener-removal order. + +Lane 016's AST import BFS found no return path through the original. The partition avoids new return imports, including type-only ones. The MMX protocol and termination leaves are independent. All their types move with the consuming operations. The existing opencodeProxyStartEnv dependency remains on the original minimax launcher, not a new leaf. The original coordinates shared child lifetime without exposing the closure's mutable state. + +Coupling classification: existing config-schema coupling stays with format owners; sequential/functional coupling is explicit through parameters. No new common mutable state or temporal startup constraint. Existing auth/ownership checks are moved verbatim. Before execution rerun lane 016 method G against the actual layer base (relative static imports, re-exports, type-only edges and literal dynamic imports); any new return path is escalation, not permission for a lazy-import workaround. + +## Tests + +Discovery: `rg -l 'src/cli/minimax' tests --glob '*.ts'`, followed by import/source-read inspection. Every direct test/fixture importer is listed below, with disposition **unchanged** (old public path): + +- `tests/fixtures/minimax-bridge-direct.ts` — unchanged. +- `tests/providers/minimax-clients.test.ts` — unchanged. + +No source-text reader of src/cli/minimax.ts was found. No retarget-to-leaf or add-leaf-to-scan-list action. `tests/providers/minimax-clients.test.ts:166` spawns the listed fixture; keep that path and run the parent test explicitly because test:changed may miss subprocess dependencies. C-phase red proofs: temporary bypass of mmxUnsafeOverride must fail :223; removing duplicate-signal suppression must fail :311. Restore each mutation and record green. The direct-hop fixture test at :154 also remains unchanged. + +These are future implementation checks, not tests run by this docs author. No new test file is required. Facade/leaf identity assertions may be added in an existing focused test; if a new test file is required, parent must explicitly expand scope to include both test-layout registry files (`scripts/test-layout/layout.json`, `tests/fixtures/test-layout-expected.json`). Never commit red-proof mutations. + +## Verification + +Future implementation gate only, in the dedicated layer worktree at its actual tip. Domains: providers. Explicit source-reader and subprocess coverage is not replaced by test:changed. + +```sh +bun run typecheck +bun test tests/providers/minimax-clients.test.ts +bun run privacy:scan +wc -l src/cli/minimax-mmx.ts src/cli/minimax-termination.ts src/cli/minimax.ts +# Compare resolved old-path consumer identities/counts with the list in this plan +rg -n 'cli/minimax' src gui/src scripts tests +# Full suite on lidge only; parent serializes access to this shared remote checkout +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-cli-minimax && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test' +``` + +The remote command intentionally keeps bun run test last, preserving its exit code instead of masking failure behind tail. Parent records remote HEAD and full output. Every command exits 0; focused/full tests report 0 failures. Delivery requires a green exact-head GitHub CI rollup, not an empty required-check list. + +Per 002, `bun test tests/lab/core-lab-boundary.test.ts` is conditional on source edits under `src/server|src/router|src/lib`: **not applicable** to this approved layer touch set. Do not edit its PROTECTED roots. If implementation expands into those directories, parent must approve scope and run that guard explicitly. Preserve the 3 original direct consumer files; new facade-to-leaf imports are not caller churn. The grep is a discovery list, not by itself a proof of consumer identity: resolve relative and dynamic paths as in the inventory method. Repeat lane 016 method G on the final imports to prove zero new cycles; typecheck alone is not a cycle detector. + +Drafting verification is document-only: required heading order, complete symbol ranges/ownership, projected line arithmetic, export coverage, referenced test paths, unique leaf paths and assigned-file scope. No test, typecheck, privacy scan or remote command above was executed in this drafting task. + +## Accept criteria + +1. Parent resolves the 500-line budget definition/exception or revises topology before implementation; no claim that literal added+deleted churn passes. +2. Every inventory declaration has exactly one implementation owner. Preserve all original export names/signatures and value/type importability; do not extract L1 declarations a second time. +3. Every new leaf is ≤400 lines. Residual target is 228, ≤400. Measure actual files and explain drift before proceeding. +4. Preserve function bodies, branch order, literals, serialized bytes/key order, class/object identity and state initialization. Only moves, explicit imports and named forwards change source structure. +5. Old-path consumers and assertions remain intact. Record the exact red/restored-green evidence named under Tests; no guard deletion, skipping, weakened assertions or empty-facade source scans. +6. Singleton state/allowlists each have one owner; no leaf imports the original even for types; resolved static/re-export/type/dynamic-literal graph has no new cycles. +7. Typecheck, focused checks, privacy, remote full suite and exact-head CI pass at this layer tip independently of later layers. No full local suite and no merge. +8. Diff stays within the original/new leaves and genuinely required existing focused tests. New tests, SoT edits, new topology or unrelated code require parent scope approval. + +## PR + +Title: `refactor(cli): isolate MMX protocol and termination owners (split S13 L4/5)` + +Branch: `codex/split-cli-minimax`. Base: `codex/split-cli-opencode`. Closes: none. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), including the size-gate disposition and DEV-STACK-03 map below. This draft creates no PR; placeholder PR numbers are intentional. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S13-L1 | 400 | `codex/split-clients-config-export-a` | `dev` | extract low-fanout client formats and dependency foundations | +| 2 | #TBD-S13-L2 | 410 | `codex/split-clients-config-export-b` | `codex/split-clients-config-export-a` | finish client path and format partitions | +| 3 | #TBD-S13-L3 | 420 | `codex/split-cli-opencode` | `codex/split-clients-config-export-b` | separate OpenCode config and catalog from launch | +| 4 | #TBD-S13-L4 | 430 — this layer | `codex/split-cli-minimax` | `codex/split-cli-opencode` | isolate MMX protocol and termination owners | +| 5 | #TBD-S13-L5 | 440 | `codex/split-integrations-state` | `codex/split-clients-config-export-b` | separate classification from state reads | + +Depends on #TBD-S13-L3. Review this layer's diff only. Cascade this layer only from its real parent `codex/split-cli-opencode`, then re-verify its tip/base ref while preserving checkout ownership. Bottom-up merging remains a separate user-authorized action and is out of scope. diff --git a/devlog/_plan/260905_now_split_train/440_integrations_state.md b/devlog/_plan/260905_now_split_train/440_integrations_state.md new file mode 100644 index 0000000000..164a6644df --- /dev/null +++ b/devlog/_plan/260905_now_split_train/440_integrations_state.md @@ -0,0 +1,166 @@ +# 440 — S13 L5/5: separate classification from state reads + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`. Bounded delegated **docs-only C3** task; parent owns orchestration, loop and goal state. +- Goal: separate classification from state reads, preserving the original public import path and behavior. +- Non-goals: behavior fixes, exported renames, signature changes, new validation, changed credentials/admission policy, changed config paths, new framework, caller migration, merges or releases. Preserve function bodies verbatim, including >50-line functions; function redesign is not this pure-move train. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; every layer must pass independently at its actual tip. Full suite on `ssh lidge` only, never locally. +- Stop: exact-tip acceptance evidence recorded; do not merge. This drafting task stops after document checks and runs no tests, code entrypoints, or Git mutations. +- Escalation: parent must resolve the 002 size-budget contradiction before execution. This layer moves **315 original lines** including attached comments/whitespace: plain added+deleted churn is at least **630 lines** before glue. If 500 means moved-once lines, this layer fits; ordinary additions plus deletions do not. Request an explicit pure-move churn exception or a parent-approved topology expansion; do not silently waive the gate or edit 002. Stale source, a leaf >400, any new cycle, or any behavioral difference also stops implementation. + +Basis: task docs HEAD `4cc219549`; code `origin/dev=1362b1a3841b4de20177e5d65865a513dd7936c4`. Read 000, 001, S13 rows/Per-layer gate of 002, and the relevant records in `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md`. Source was read with `git show origin/dev:`; `git diff origin/dev -- src/clients/config-export.ts src/cli/opencode.ts src/cli/minimax.ts src/integrations/state.ts` was empty. Older tips in 000/001 are historical, not this plan's code basis. + +Structural decision (cxc-dev §1/§5, architecture ARCH-MAP-01/ARCH-DECISION-01): 495 lines mix distinct concerns. Reject deleting/configuring the feature (does not preserve behavior), and generic helpers/index barrels (do not establish ownership). Reuse every existing algorithm and lower-level dependency; only relocate declarations. Inspected conventions: `src/config/paths.ts`, `src/config/process-state.ts`, `src/cli/launcher-context.ts`, `src/cli/account-extended.ts`, `src/integrations/ownership-policy.ts`. Use named siblings in the existing directory. The original remains an existing compatibility boundary, not an internal import shortcut. + +Structural map: 7 direct source/test/fixture consumer files. Production dependents: `src/integrations/writer.ts`, `src/server/management/integration-routes.ts`. Current direction is dependents → original → existing imported owners; intended direction is dependents → original → concern leaves → existing owners. Leaf imports are fully enumerated below; no leaf → original edge. Blast radius: client/CLI integration feature, with public consumers unchanged. `structure/09_client-integrations.md:11` identifies builders and classification as single authorities; no parallel implementation is introduced. + +## Symbol inventory + +Exact syntax spans at `origin/dev:src/integrations/state.ts` (leading comments excluded). Reproduce: `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration,variable_declaration,class_declaration' --json=compact src/integrations/state.ts`, filtering declarations enclosed by another declaration. Consumers = distinct direct importer/re-exporter files per symbol, resolved by literal module path then counted with `rg -l -w '' `. Dynamic dispatch destructuring counts too. Private declarations have 0 external consumers, not 0 local calls. Imported bindings are covered by the leaf imports; export-only declarations are noted below. L2 repeats the complete basis inventory and marks L1-owned rows already moved. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `IntegrationState` | type | 30–30 | yes | 1 | `src/integrations/state-classification.ts` (L5) | +| `StateReason` | type | 31–39 | yes | 0 | `src/integrations/state-classification.ts` (L5) | +| `IntegrationStatus` | interface | 41–53 | yes | 0 | `src/integrations/state-classification.ts` (L5) | +| `readPath` | function | 55–63 | yes | 0 | `src/integrations/state-classification.ts` (L5) | +| `hasOurFragments` | function | 66–68 | yes | 0 | `src/integrations/state-classification.ts` (L5) | +| `blockedContainerPath` | function | 81–111 | yes | 0 | `src/integrations/state-classification.ts` (L5) | +| `recordedContribution` | function | 120–143 | no | 0 | `src/integrations/state-classification.ts` (L5) | +| `recordedBlockIsOwned` | function | 153–205 | no | 0 | `src/integrations/state-classification.ts` (L5) | +| `classifyIntegration` | function | 220–343 | yes | 2 | `src/integrations/state-classification.ts` (L5) | +| `IntegrationStateInput` | interface | 345–355 | yes | 0 | `src/integrations/state.ts` (residual) | +| `exportContextOf` | function | 357–376 | yes | 2 | `src/integrations/state.ts` (residual) | +| `retriedThisProcess` | let | 378–378 | no | 0 | `src/integrations/state.ts` (residual) | +| `retryPendingPrunesOnce` | function | 385–395 | yes | 0 | `src/integrations/state.ts` (residual) | +| `retentionOf` | function | 402–413 | no | 0 | `src/integrations/state.ts` (residual) | +| `readIntegrationState` | function | 416–495 | yes | 6 | `src/integrations/state.ts` (residual) | + +No other export-only top-level declaration exists. + +## Leaf partition + +Keep launch/read orchestration in the original; no source-scanned spawn site or process singleton moves. This is a pure relocation, not a new adapter abstraction. + +Line-budget convention: each declaration carries immediately preceding comments/whitespace, from previous declaration end+1 (first declaration starts after the import/export header). Counts include those blocks, the exact one-line imports shown, one header line and one separator. These are conservative projected implementation counts, not measurements of files already written. Do not discard comments to meet limits. Adding an export keyword does not add a line. All new files are ≤400. + +### `src/integrations/state-classification.ts` — expected 325 lines + +Symbols: `IntegrationState`, `StateReason`, `IntegrationStatus`, `readPath`, `hasOurFragments`, `blockedContainerPath`, `recordedContribution`, `recordedBlockIsOwned`, `classifyIntegration`. + +Own imports: + +```ts +import type { IntegrationClientId } from "./registry"; +import type { ManagedContribution } from "../clients/config-export"; +import type { OwnershipRecord } from "./ownership"; +import { fingerprint, canonicalContribution, semanticContribution } from "./ownership"; +import { validRefreshablePaths, protectedContributionFingerprint, semanticProtectedContributionFingerprint, refreshablePathsOf } from "./ownership-policy"; +import { PARSE_FAILED } from "./config-io"; +import { INTEGRATION_CLIENTS } from "./registry"; +import { EXPORT_CLIENTS } from "../clients/config-export"; +``` + +Leaf exports: `IntegrationState`, `StateReason`, `IntegrationStatus`, `readPath`, `hasOurFragments`, `blockedContainerPath`, `classifyIntegration`. Other listed declarations remain private. Only previously public symbols are forwarded from the original path; newly exposed internal symbols serve production registry/sibling calls, not tests. + +Residual `src/integrations/state.ts`: expected **186 lines**. No follow-up split is required for this original. + +Retained declarations after this layer: `IntegrationStateInput`, `exportContextOf`, `retriedThisProcess`, `retryPendingPrunesOnce`, `retentionOf`, `readIntegrationState`. + +Arithmetic: 495 original − 315 cumulative moved original lines + 6 facade glue = 186. Glue comprises new imports, compatibility exports and separators. Retained original header imports can be pruned if unused, only decreasing the estimate. + +## Re-export block + +Exact forwards in the original path follow. Other public declarations remain exported in place. No wildcard, alias, wrapper, signature change or duplicate definition. + +```ts +export { readPath, hasOurFragments, blockedContainerPath, classifyIntegration } from "./state-classification"; +export type { IntegrationState, StateReason, IntegrationStatus } from "./state-classification"; +``` + +Explicit residual local imports (re-export binds nothing locally): + +```ts +import type { IntegrationStatus } from "./state-classification"; +import { classifyIntegration } from "./state-classification"; +``` + +Retain original external imports still used by the residual; prune only proven-unused bindings. New leaves import one another directly. + +## Module-level state and cycles + +`retriedThisProcess` (`src/integrations/state.ts:378`) stays only in state.ts, adjacent to `retryPendingPrunesOnce` (`:385–395`) and `readIntegrationState` (`:416–495`). Never copy it into the classifier or reset on import. Default-store reads retry once per process; explicit-store behavior is unchanged. No top-level Map/Set/WeakMap/lock. The recordedPaths Set at `:261` is per classification call. The classifier owns no maintenance scheduling or mutable singleton. + +Lane 016's AST import BFS found no return path through the original. The partition avoids new return imports, including type-only ones. state-classification.ts depends on registry/ownership/config-io, none of whose import graphs returned through state.ts in the lane evidence. IntegrationState/StateReason/IntegrationStatus move with classification, avoiding a type-only back-import. Registry remains independent of state; writer → state → classifier retains one classification authority. + +Coupling classification: existing config-schema coupling stays with format owners; sequential/functional coupling is explicit through parameters. No new common mutable state or temporal startup constraint. Existing auth/ownership checks are moved verbatim. Before execution rerun lane 016 method G against the actual layer base (relative static imports, re-exports, type-only edges and literal dynamic imports); any new return path is escalation, not permission for a lazy-import workaround. + +## Tests + +Discovery: `rg -l 'src/integrations/state' tests --glob '*.ts'`, followed by import/source-read inspection. Every direct test/fixture importer is listed below, with disposition **unchanged** (old public path): + +- `tests/clients/integrations-state.test.ts` — unchanged. +- `tests/clients/integrations-writer.test.ts` — unchanged. +- `tests/gui/integrations-invariants.test.ts` — unchanged. +- `tests/providers/aside-client.test.ts` — unchanged. +- `tests/server/management-integration-routes.test.ts` — unchanged. + +No source-text reader of src/integrations/state.ts was found. 001's basename heuristic is not four confirmed text oracles: `tests/clients/integrations-state.test.ts:139` reads a temporary client config; `tests/gui/integrations-invariants.test.ts:142` reads a temporary journal and :145 records; `tests/providers/aside-client.test.ts:271` reads a generated account catalog. Keep these unchanged; do not retarget them to source. No retarget-to-leaf or add-leaf-to-scan-list action. + +C-phase red proofs: temporarily bypass blockedContainerPath in the moved classifier and observe `tests/clients/integrations-state.test.ts:346` fail; restore. Temporarily bypass recordedBlockIsOwned and observe :148 fail; restore. Existing writer tests must still show these classifications prevent mutation; fingerprint-only replacements are insufficient. + +These are future implementation checks, not tests run by this docs author. No new test file is required. Facade/leaf identity assertions may be added in an existing focused test; if a new test file is required, parent must explicitly expand scope to include both test-layout registry files (`scripts/test-layout/layout.json`, `tests/fixtures/test-layout-expected.json`). Never commit red-proof mutations. + +## Verification + +Future implementation gate only, in the dedicated layer worktree at its actual tip. Domains: clients, gui, providers, server. Explicit source-reader and subprocess coverage is not replaced by test:changed. + +```sh +bun run typecheck +bun test tests/clients/integrations-state.test.ts tests/clients/integrations-writer.test.ts tests/gui/integrations-invariants.test.ts tests/providers/aside-client.test.ts tests/server/management-integration-routes.test.ts +bun run privacy:scan +wc -l src/integrations/state-classification.ts src/integrations/state.ts +# Compare resolved old-path consumer identities/counts with the list in this plan +rg -n 'integrations/state' src gui/src scripts tests +# Full suite on lidge only; parent serializes access to this shared remote checkout +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-integrations-state && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test' +``` + +The remote command intentionally keeps bun run test last, preserving its exit code instead of masking failure behind tail. Parent records remote HEAD and full output. Every command exits 0; focused/full tests report 0 failures. Delivery requires a green exact-head GitHub CI rollup, not an empty required-check list. + +Per 002, `bun test tests/lab/core-lab-boundary.test.ts` is conditional on source edits under `src/server|src/router|src/lib`: **not applicable** to this approved layer touch set. Do not edit its PROTECTED roots. If implementation expands into those directories, parent must approve scope and run that guard explicitly. Preserve the 7 original direct consumer files; new facade-to-leaf imports are not caller churn. The grep is a discovery list, not by itself a proof of consumer identity: resolve relative and dynamic paths as in the inventory method. Repeat lane 016 method G on the final imports to prove zero new cycles; typecheck alone is not a cycle detector. + +Drafting verification is document-only: required heading order, complete symbol ranges/ownership, projected line arithmetic, export coverage, referenced test paths, unique leaf paths and assigned-file scope. No test, typecheck, privacy scan or remote command above was executed in this drafting task. + +## Accept criteria + +1. Parent resolves the 500-line budget definition/exception or revises topology before implementation; no claim that literal added+deleted churn passes. +2. Every inventory declaration has exactly one implementation owner. Preserve all original export names/signatures and value/type importability; do not extract L1 declarations a second time. +3. Every new leaf is ≤400 lines. Residual target is 186, ≤400. Measure actual files and explain drift before proceeding. +4. Preserve function bodies, branch order, literals, serialized bytes/key order, class/object identity and state initialization. Only moves, explicit imports and named forwards change source structure. +5. Old-path consumers and assertions remain intact. Record the exact red/restored-green evidence named under Tests; no guard deletion, skipping, weakened assertions or empty-facade source scans. +6. Singleton state/allowlists each have one owner; no leaf imports the original even for types; resolved static/re-export/type/dynamic-literal graph has no new cycles. +7. Typecheck, focused checks, privacy, remote full suite and exact-head CI pass at this layer tip independently of later layers. No full local suite and no merge. +8. Diff stays within the original/new leaves and genuinely required existing focused tests. New tests, SoT edits, new topology or unrelated code require parent scope approval. + +## PR + +Title: `refactor(integrations): separate classification from state reads (split S13 L5/5)` + +Branch: `codex/split-integrations-state`. Base: `codex/split-clients-config-export-b`. Closes: none. + +Use all sections of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist), including the size-gate disposition and DEV-STACK-03 map below. This draft creates no PR; placeholder PR numbers are intentional. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S13-L1 | 400 | `codex/split-clients-config-export-a` | `dev` | extract low-fanout client formats and dependency foundations | +| 2 | #TBD-S13-L2 | 410 | `codex/split-clients-config-export-b` | `codex/split-clients-config-export-a` | finish client path and format partitions | +| 3 | #TBD-S13-L3 | 420 | `codex/split-cli-opencode` | `codex/split-clients-config-export-b` | separate OpenCode config and catalog from launch | +| 4 | #TBD-S13-L4 | 430 | `codex/split-cli-minimax` | `codex/split-cli-opencode` | isolate MMX protocol and termination owners | +| 5 | #TBD-S13-L5 | 440 — this layer | `codex/split-integrations-state` | `codex/split-clients-config-export-b` | separate classification from state reads | + +Depends on #TBD-S13-L2. Review this layer's diff only. Cascade this layer only from its real parent `codex/split-clients-config-export-b`, then re-verify its tip/base ref while preserving checkout ownership. Bottom-up merging remains a separate user-authorized action and is out of scope. diff --git a/devlog/_plan/260905_now_split_train/445_server_port_probe_disposal.md b/devlog/_plan/260905_now_split_train/445_server_port_probe_disposal.md index b1b02ddd38..0723ac77bb 100644 --- a/devlog/_plan/260905_now_split_train/445_server_port_probe_disposal.md +++ b/devlog/_plan/260905_now_split_train/445_server_port_probe_disposal.md @@ -1,5 +1,10 @@ # 445 — Runtime verification prerequisite +> Preserved published history from pinned devbf58ef182. Continuation and +> coordination below are retired;800/810/820 govern the cutoff. This record +> authorizes no new execution and does not change the scratch-only disclosure +> boundary. Later delivery evidence remains in450 and the private receipts. + ## Scope and workflow C3 independent runtime-maintenance prerequisite for the modularization train. @@ -36,17 +41,15 @@ Independent review, exact-head remote gates and hosted CI must pass. A prior head's results do not establish a later head. Detailed verification records are kept with private receipt evidence; no completion is inferred from a plan. -## Continuation and coordination +## Historical continuation and coordination — do not execute This work does not close a modularization ledger row. D resumes suspended WP450 for its own P/A, restack and fresh verification; do not count it done. PR #3633 remains independent until that controlled restack is performed. -The user requires conversational one-at-a-time non-Windows CI coordination. -Windows-owner work remains excluded. Changes that start CI, including pushes, -retargeting and landing, require the scheduled slot. Code/static review may -continue while waiting. Scope authority is already granted; a queue wait is -not a request for more user permission. +The user then required conversational one-at-a-time non-Windows CI scheduling, +excluding Windows-owner work. That policy is now retired. No scheduled slot, +peer communication or resumed WP445/WP450 work is authorized by this history. ## Review disposition diff --git a/devlog/_plan/260905_now_split_train/447_port_probe_verification_progress.md b/devlog/_plan/260905_now_split_train/447_port_probe_verification_progress.md new file mode 100644 index 0000000000..e1b3991a19 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/447_port_probe_verification_progress.md @@ -0,0 +1,35 @@ +# 447 — Maintenance verification status + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Historical investigation or process record; not current execution authority. Pending post-merge status below is timestamp-specific, not an active watch; no later outcome is inferred. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +WP445/PR #3640 is an independent prerequisite, not a completed modularization +ledger row. Its implementation and review stay in the bound a2c0 checkout. +Detailed investigation and regression records belong to ignored scratch. + +At head `f47a8e39885a6c79ffdb7b50fb4594aae199a2da`, remote typecheck/build/ +privacy passed,70focused tests passed, and the full suite reported +18,775pass/16skip/0fail. Source-bound receipt exit0 records a clean matching +head. Hosted CI33953131438 also succeeded. These are historical head-specific +results, not evidence for subsequent documentation revisions. + +A review correction remains: minimize public working notes and preserve +investigation only in scratch. Final-head gates must be refreshed after that +change. No completion or landing is claimed. Preserve the original runtime +test assertions and verification requirements. + +## Final delivery + +Final head d2b4a81c61294c3c9ae7a2d58a01397167b120d0 passed hosted +CI33954745415 and a fresh remote full-suite receipt (18,775pass/16skip/0fail; +70focusedpass; typecheck/privacy/build0). Both review findings were resolved. +WP445 closed through D and resumed WP450. It does not count as another +modularization row. + +The user's later instruction authorized admin landing after CI. PR #3640 +was merged with an expected-head match as +ebb0e5e174e0cc035d4e7ffa668c25652bd1caca. Its tree +a3142ef0bef9a5b9747037c41b3aa803d13b69b2 matches the actual tested merge +d880bffc83e4a8329b540f4771efaf2e47e6efa6. Fetched dev ancestry was confirmed. +No open PR targeted the deleted parent branch. Post-merge dev CI33956565008 +is a separate sequential gate and remains pending at this record's timestamp. diff --git a/devlog/_plan/260905_now_split_train/450_cli_status.md b/devlog/_plan/260905_now_split_train/450_cli_status.md index 7ccd1cd57c..81eb21035f 100644 --- a/devlog/_plan/260905_now_split_train/450_cli_status.md +++ b/devlog/_plan/260905_now_split_train/450_cli_status.md @@ -1,5 +1,8 @@ # 450 — S14 L1 — CLI status probe extraction +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Delivered history; later final delivery and successful post-merge follow-up below govern. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + ## Loop spec - Archetype: pure-move, C3 CLI/module refactor; main owns the goal and persisted PABCD. @@ -423,3 +426,43 @@ The status source/test blobs and reviewed source boundaries remain unchanged. Re-review the repaired documents and verify the new resulting HEAD. The preceding8bc CI belongs to that prior HEAD, even if it passes; it cannot be presented as the new commit's exact-head proof. + +## Verified delivery record + +Final layer HEAD: df92323d3406535c7eacd0bfa2d5bae6adb610e1. Final hosted +CI33963307005 passed18jobs with two configured dispatch-only skips. Fresh +isolated remote verification passed build preparation, typecheck,50focused +tests, privacy, and the full suite:18842pass/16skip/0fail. This includes the +main18681pass batch and six disjoint serial lanes totaling161pass. Both +named refusal/snapshot negative controls failed with exit1, then restored +to27pass/0fail each; final remote HEAD remained clean. The bound receipt and +full/mutation logs are retained in the session evidence directory. + +Independent C review validated source identity, all15owners/11exports, +51bindings,366/346module closures, test activation and execution accounting. +The three retained shared-document findings were repaired and resolved. +An older duplicate label cancellation had a newer same-head successful +replacement; no cancelled test was counted as passing. + +PR #3633 was admin-merged with expected-head matching as +09335d7d451335a74ad1c02e88ee37ef89f5a007. Its actual tree +7ffe001817a487a47f5836eedfe1645574111393 equals the tested PR merge tree. +Freshly fetched dev contains both the layer and its merge; no open direct +child PR required preservation. The approval bypass is recorded in PR +comment5551535773; optional CodeRabbit was pending at that snapshot, not +counted as PASS. Independent review and executable gates had passed. + +Delivery is verified, while post-merge dev CI33964069626 is monitored +separately and is not yet claimed successful in this record. Any failure +remains work under the active goal; the next unit must check its base before +implementation. No local suite, release or live-service change occurred. + +Residual: status.ts is384lines and the new leaf168, but collectStatus at +status.ts:170 remains an unchanged215-line function. This is file-boundary +completion, not elimination of that function debt or completion of all68 +rows. Changed public bindings, a new return cycle, or a negative control +that does not fail would invalidate this direction; none was observed. + +Post-merge follow-up: CI33964069626 completed SUCCESS on +09335d7d451335a74ad1c02e88ee37ef89f5a007. The separately monitored base check +is now closed; no additional source change or rerun was needed. diff --git a/devlog/_plan/260905_now_split_train/460_cli_provider.md b/devlog/_plan/260905_now_split_train/460_cli_provider.md new file mode 100644 index 0000000000..93d214d319 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/460_cli_provider.md @@ -0,0 +1,138 @@ +# S14 L2 — CLI provider read handlers and argument parsing + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only delegation. Parent owns orchestration, loop and goal state. +- Goal: reduce `src/cli/provider.ts` below 400 by moving its read-only list/show family and shared argument parsing, leaving mutation validation/save and dispatch intact. Basis: docs `4cc219549`; code `origin/dev = 1362b1a38`, 485 lines. All source ranges below refer to that basis. +- Non-goals: no provider/auth/preset changes, no credential-mask changes, no new argument semantics, no change to save ordering, output, exit status, sync behavior or runtime fallback; no cross-command parser consolidation. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated below; this drafting task runs document checks only. +- Stop: executor records standalone gate evidence and green exact-head CI on an open L2 PR, without merging; this delegation stops at checked docs. +- Escalation: stale source, new oracle coupling, cycle, >400 output or >500 changed source lines requires parent direction. An actual behavior/auth change is outside pure-move scope and needs separate security review. No extra docs or code may be written by this delegated task. + +Structural decision: lane 016:584–597 identifies handler-family separation behind the command/validation boundary. Current map: `src/cli/dispatch.ts:657` dynamically imports the one exported `handleProviderCommand`; it dispatches private read and mutation handlers (`provider.ts:447–485`) using config/registry dependencies at 11–20. Intended map: dispatch → retained `provider.ts` → `provider-read.ts` → `provider-args.ts`, with a direct residual → args edge; mutation handlers and `validateAndSave` stay together. Blast radius is one CLI feature. Do nothing/delete/configure cannot preserve required behavior while shrinking 485 lines. Moving all handlers would cost more churn; using the models/account parsers as an owner would introduce cross-command coupling. Concern siblings match `src/cli/provider-runtime.ts`, `models-runtime-subcommands.ts` and `status-oauth.ts`; no new directory or generic helpers module. + +## Symbol inventory + +Inventory: numbered `git show origin/dev:src/cli/provider.ts` plus ast-grep top-level declaration ranges. Ranges exclude leading comments. Imports are dependencies, not owned declarations. Counts are distinct external importers from `rg -l -w '' src gui/src scripts tests`, filtered by resolved module identity. All private declarations have zero external consumers; similarly named functions in `models.ts`, `account.ts` or a UI component are unrelated declarations. The `handleProviderCommand` text in a test fixture is not an import. File fan-in: **1 production dynamic importer, 0 test importers**. + +Aliases: `A` = `src/cli/provider-args.ts` (new); `D` = `src/cli/provider-read.ts` (new); `R` = `src/cli/provider.ts` (residual). + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| consumeFlag | function | 26–31 | no | 0 | A | +| consumeFlagValue | function | 33–39 | no | 0 | A | +| rejectUnknownArgs | function | 42–52 | no | 0 | A | +| maskSecret | function | 54–57 | no | 0 | D | +| validateAndSave | function | 63–73 | no | 0 | R | +| handleList | function | 79–124 | no | 0 | D | +| ADD_USAGE | const string | 130–130 | no | 0 | R | +| handleAdd | async function | 132–272 | no | 0 | R | +| handleRemove | function | 278–335 | no | 0 | R | +| handleShow | function | 341–377 | no | 0 | D | +| handleSetDefault | function | 383–418 | no | 0 | R | +| PROVIDER_USAGE | const string | 424–445 | no | 0 | R | +| handleProviderCommand | async function | 447–485 | yes | 1 | R | + +No #a/#b ordering applies: one layer resolves this file. Both leaves start with zero external consumers, avoiding migration churn; the sole public export remains in place. + +## Leaf partition + +1. **`src/cli/provider-args.ts` — expected 32 lines, ceiling 400.** Move **22–53** verbatim with comments/separators; export `consumeFlag`, `consumeFlagValue`, `rejectUnknownArgs` for their production callers. Imports: **none**. `process` and `console` remain the existing runtime globals. Preserve mutation of the passed array, missing-value behavior, diagnostics and exit(1). +2. **`src/cli/provider-read.ts` — expected 102 lines, ceiling 400.** Move **54–58, 75–125, 337–378**, i.e. 5 + 51 + 42 = **98 lines**, containing `maskSecret`, `handleList`, `handleShow`. Export only the handlers; `maskSecret` stays private. Add these three import lines and one separator: + + ```ts + import { hasOwnProvider, loadConfig, sanitizeModelCostsForDisplay } from "../config"; + import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry"; + import { consumeFlag, rejectUnknownArgs } from "./provider-args"; + ``` + +3. **Residual `src/cli/provider.ts` — expected 357 lines, ceiling 400.** Keep all R declarations and original header. Remove `sanitizeModelCostsForDisplay` from import line 11 and `PROVIDER_REGISTRY` from line 14, but retain `getProviderRegistryEntry` for `handleAdd`. All other existing imports remain used. Add the two imports below. Accounting: **485 − (32 + 98) + 2 = 357**; leaves 32 and 98 + 4 = 102. Aggregate 491 = original 485 + 6 wiring lines. No #b needed. Expected source numstat churn is about 270 lines; measure actual diff against L1. + +Pre-write owner search: `rg -n 'consumeFlag|consumeFlagValue|rejectUnknownArgs|maskSecret|handleList|handleShow' src/cli` finds private argument functions at `src/cli/models.ts:145,152` and `src/cli/account.ts:68`, not a shared public owner. Keep their behavior untouched; this layer moves only the provider implementation rather than deduplicating commands. The existing config sanitizer remains the owner of display cost normalization (`provider.ts:360`). + +## Re-export block + +**No re-export statements are required:** the sole current export, `handleProviderCommand`, remains an exported declaration in `src/cli/provider.ts` (old 447–485). There are no current exported types and no moved current exports. Do not invent `export { handleList, handleShow }` on the original path; that would enlarge its public contract. + +Exact new local imports in the residual: + +```ts +import { consumeFlag, consumeFlagValue, rejectUnknownArgs } from "./provider-args"; +import { handleList, handleShow } from "./provider-read"; +``` + +The new leaves expose symbols only to real production callers, not just tests. Keeping the dispatch declaration at the old path preserves the dynamic import at `src/cli/dispatch.ts:657`. No facade wrapper, new index, wildcard, rename, or convenience barrel. + +## Module-level state and cycles + +No top-level `let`, Map/Set/WeakMap/WeakSet, timer or lock (lane 016:592, checked against declarations). `ADD_USAGE` at 130 and `PROVIDER_USAGE` at 424 are immutable strings; each remains owned once by R. `provConfig` at 170 and `codexSyncSkipped` at 241 are per-call locals, not singletons. A continues to mutate the caller-owned argument array exactly as before. + +Potential direct cycle: D importing argument functions from R while R imports D. Avoid it with the lower, import-free A owner; neither leaf imports R. A has no runtime dependency edge; D depends only on A and the existing config/registry owners. Functional coupling replaces lexical locality. Existing mutation sequencing (`validateAndSave` at 225, 315, 409) is retained in R, with no new callback/control flag or shared mutable config owner. Lane 016:593 found no original return cycle; executor repeats method G (relative static/type import/export plus literal dynamic-import resolution) for all changed modules and requires zero return paths. The runtime-command dynamic import at 474 stays untouched; it is existing command dispatch, not a new cycle workaround. + +## Tests + +`rg -l 'cli/provider["\x27]' tests` returns **no files**. No behavioral test imports this module directly. Its command-level coverage is essential because import-graph selection cannot see CLI subprocess dispatch: + +| test file | anchor | disposition | +|---|---|---| +| tests/cli/cli-provider.test.ts | executable path 11; spawn 19; list/show cases 64, 77, 153, 356, 375; strict args 437, 448, 459 | unchanged; execute the real CLI | +| tests/cli/cli-transport-honesty.test.ts | source read 20; provider fixture 98; exit-code checks 106, 111 | unchanged; source owner is dispatch.ts, not provider.ts | + +**Oracle discrepancy resolved:** 001's broad `textoracle=1` candidate is a false positive for this file. `tests/cli/cli-transport-honesty.test.ts:20` reads `repoPath("src", "cli", "dispatch.ts")`; line 98 contains `import("./provider")` inside an artificial pre-fix source string. It does not read `provider.ts`. Its additional readers at 129, 291 and 296 read account-family/index files. Basename/qualified-path/split-segment searches find **zero direct provider.ts source readers**, agreeing with lane 016:594. No `retarget-to-leaf` or `add-leaf-to-scan-list` is needed. Do not broaden that dispatch-specific scan to the new leaves. + +Guards to drive red once in the later implementation worktree: temporarily make moved `rejectUnknownArgs` accept unknown arguments and observe the existing line-437 and line-459 tests fail; restore. Temporarily remove the moved display sanitizer at old line 360 and observe line-153's secret-shaped-model-cost guard fail; restore. These mutations test already-public behavior, must never be committed, and are not executed during drafting. Existing exit-code oracle and its built-in red-first fixture remain unchanged. + +## Verification + +Later executor only, in the L2 worktree: + +```sh +bun run typecheck +bun test tests/cli/cli-provider.test.ts tests/cli/cli-transport-honesty.test.ts +bun run privacy:scan +wc -l src/cli/provider-args.ts src/cli/provider-read.ts src/cli/provider.ts +rg -n 'import\("\./provider"\)' src/cli/dispatch.ts +rg -l 'cli/provider["\x27]' tests +rg -n '^import|^export .* from ' src/cli/provider-args.ts src/cli/provider-read.ts src/cli/provider.ts +git diff --numstat origin/dev...HEAD -- src/cli/provider.ts src/cli/provider-args.ts src/cli/provider-read.ts +``` + +The test-import `rg` deliberately exits 1 for zero matches; that is the expected result, not a failing verification. Domain: `tests/cli`. Parent-path importer set remains the one dynamic import; 002's static `from`-only count is insufficient for this file, so retain the dynamic check above. Require the original single-export set and method-G acyclic closure. No server/router/lib source is touched, so 002 does not require `core-lab-boundary`; its protected roots are never edited. If scope changes, escalate. + +Full suite only on lidge at the published layer SHA, with remote HEAD compared to that SHA and pipeline status preserved: + +```sh +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-cli-provider && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Record exits, negative-control restoration, remote full-suite result and exact-head CI rollup. No local full suite; no tests or live provider operations run in this docs delegation. Review credential-display preservation explicitly per `MAINTAINERS.md:60–61`; extraction is not permission to change masking policy. + +## Accept criteria + +1. All 13 top-level owned declarations are assigned once; only the named ranges move, retaining comments and function bodies. +2. New/residual sizes ≤400 (expected 32, 102, 357); parent-relative source churn ≤500 or stop for parent re-plan; no #b debt remains. +3. `handleProviderCommand` is the exact original single export, the dispatch import remains unchanged, and no public helper is added to the original path. +4. No A/D → R edge or method-G cycle; mutation validation/save remains in one original owner; no copied state or cross-command parser edits. +5. List/show output, costs/secret display, args, mutation output and exit-code assertions remain unchanged; negative controls fail once and restored focused/typecheck/privacy checks pass. +6. Exact L2 SHA passes remote full suite and required CI independently of L3; PR base is L1 and contains no unrelated source changes. No merge. + +## PR + +Title: `refactor(cli): isolate provider read handlers and argument parsing (split S14 L2/3)` + +Branch: `codex/split-cli-provider`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification and Checklist with actual evidence. DEV-STACK-03 map, replacing placeholders when PRs exist: + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 3 | # | hub transport / codex/split-client-hub-client | dev | transport and error identity | +| 2 | # | provider readers / codex/split-cli-provider — this PR | dev | read handlers and argument parsing | +| 1 | # | status probes / codex/split-cli-status | dev | diagnostic probes and old exports | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Review only this layer's diff; no reliance on other layers' checks and no merge authorization. diff --git a/devlog/_plan/260905_now_split_train/470_client_hub_client.md b/devlog/_plan/260905_now_split_train/470_client_hub_client.md new file mode 100644 index 0000000000..efb09cd5fd --- /dev/null +++ b/devlog/_plan/260905_now_split_train/470_client_hub_client.md @@ -0,0 +1,161 @@ +# S14 L3 — Bounded hub transport and decoding + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 docs-only architecture planning with security-sensitive boundary review required for execution. Parent owns orchestration, loop and goal state. +- Goal: move existing bounded request/decoding primitives and their error identity from `src/client/hub-client.ts` to a sibling, keeping protocol adapters and the old public import path. Basis: docs `4cc219549`; source `origin/dev = 1362b1a38`, 481 lines. All source anchors below use that source basis. +- Non-goals: no transport, credentials, URL policy, validation, deadline, catalog, key-rotation or wire-contract changes; no live hub calls, generic HTTP client, new dependencies or consolidation with the management relay. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated below. Drafting uses only read-only source inspection and doc checks; no tests or production imports execute. +- Stop: executor finishes with standalone L3 evidence, exact-head CI green and its open PR; never merge. This delegation stops after its assigned docs are checked. +- Escalation: upstream drift, new source oracle, cycle, >400 result or >500 changed source lines requires parent re-plan. Any required auth/credential/policy behavior change is a separate C4 scope, never bundled into the move. Unreleased security findings go to permitted scratch via the parent, not this public devlog. + +Structural decision: lane 016:599–611 names transport/decoding versus protocol adapters. Current map: `src/client/connect.ts:38–53` and three test importers → `hub-client.ts` → catalog limit, bounded-body, abort and remote protocol (`hub-client.ts:1–3,24–28`). Intended map: same consumers → retained `hub-client.ts` → `hub-client-transport.ts` → existing bounded-body/abort owners; catalog/remote protocol stay in the residual. Feature blast radius includes connection and its server rotation round-trip test, not server implementation. Do nothing/delete/configure cannot preserve the protocol surface while removing excess lines. Extracting pairing/rotation first would require sharing transport/error declarations anyway and cost more churn. Reusing `hub-relay.ts` is rejected: that module has relay-specific headers/policy (`src/client/hub-relay.ts:1–29`) rather than these client contracts. Concern-named siblings match `hub-relay.ts`, `machine-auth.ts` and `machine-api.ts`. Preserve the remote lifecycle source-of-truth at `structure/09_client-integrations.md:118–120`. + +## Symbol inventory + +Ranges: numbered `git show origin/dev:src/client/hub-client.ts` and ast-grep top-level declaration ranges; comments excluded from syntax ranges. All owned declarations listed; imports are dependencies. Consumer counts are distinct external import files from `rg -l -w '' src gui/src scripts tests`, filtered by imports resolving to this module, excluding self. Same-name private functions/constants in unrelated modules are not consumers. File fan-in: **4** (1 production, 3 tests). + +Aliases: `T` = `src/client/hub-client-transport.ts` (new); `R` = `src/client/hub-client.ts` (residual). + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| isPairingTransportPermitted | function | 12–23 | no | 0 | R | +| READY_BODY_LIMIT | const number | 30–30 | no | 0 | R | +| MANAGEMENT_BODY_LIMIT | const number | 31–31 | no | 0 | R | +| DEFAULT_TIMEOUT_MS | const number | 32–32 | no | 0 | T | +| OneTimeConnectCredential | type | 34–36 | yes | 1 | R | +| ConnectGuiSession | interface | 38–43 | yes | 1 | R | +| IssuedClientKey | interface | 45–50 | yes | 1 | R | +| StartedClientKeyRotation | interface | 52–55 | yes | 0 | R | +| HubClientError | class | 57–67 | yes | 2 | T | +| credentialString | function | 69–75 | no | 0 | T | +| safeTimeout | function | 77–81 | no | 0 | T | +| fetchBounded | async function | 83–109 | no | 0 | T | +| boundedText | async function | 111–132 | no | 0 | T | +| jsonCompatibleContentType | function | 134–137 | no | 0 | R | +| validateRemoteCatalog | function | 139–158 | no | 0 | R | +| parseJson | function | 160–166 | no | 0 | T | +| normalizeHubOrigin | function | 168–189 | yes | 2 | T | +| fetchHubReady | async function | 191–222 | yes | 2 | R | +| htmlMeta | function | 224–232 | no | 0 | R | +| exchangeConnectPairingGrant | async function | 234–267 | yes | 2 | R | +| parseIssuedClientKey | function | 269–279 | no | 0 | R | +| issueClientKey | async function | 281–319 | yes | 2 | R | +| revokeClientKey | async function | 321–346 | yes | 1 | R | +| rotationManagementHeaders | function | 348–360 | no | 0 | R | +| assertRotationAuthorityOrigin | function | 362–366 | no | 0 | R | +| startClientKeyRotation | async function | 368–390 | yes | 2 | R | +| commitClientKeyRotation | async function | 392–407 | yes | 2 | R | +| abortClientKeyRotation | async function | 409–424 | yes | 1 | R | +| downloadClientCatalog | async function | 426–466 | yes | 3 | R | +| probeClientKeyId | async function | 468–481 | yes | 1 | R | + +All single-consumer exports resolve to `src/client/connect.ts:38–53`. `HubClientError` additionally has `tests/clients/remote-catalog.test.ts:2`; origin/ready/pairing/issue additionally have `tests/clients/client-connect.test.ts:8–14`; rotation start/commit additionally have `tests/server/api-keys-routes.test.ts:9`; catalog has both client test consumers. The class constructor at 58–66 is a class member, not a separate top-level declaration, and moves intact with its class. + +## Leaf partition + +1. **`src/client/hub-client-transport.ts` — expected 113 lines, ceiling 400.** Own `DEFAULT_TIMEOUT_MS`, `HubClientError`, `credentialString`, `safeTimeout`, `fetchBounded`, `boundedText`, `parseJson`, `normalizeHubOrigin`. Move exact ranges **32–33, 57–133, 160–190**, including separators: 2 + 77 + 31 = **110 moved lines**. Export the helpers needed by R, while leaving the default timeout private. Only two imports plus one separator: + + ```ts + import { readBoundedResponseBytes } from "../lib/bounded-body"; + import { clearableDeadline } from "../lib/abort"; + ``` + +2. **Residual `src/client/hub-client.ts` — expected 379 lines, ceiling 400.** Retain all R declarations, including four public types, protocol-specific origin checks, readiness, pairing, issuance/revocation/rotation, catalog validation, catalog download and key-id probe. Remove the two moved imports at old lines 2–3; retain `MAX_REMOTE_CATALOG_BYTES` and all remote protocol imports. Add the ten physical wiring lines below (one export plus nine-line import). Accounting: **481 − 110 − 2 + 10 = 379**; leaf 110 + 3 = 113; total 492 = original 481 + 11 wiring lines. No #b needed; the residual remains below 400 without changing protocol bodies. Expected parent-relative source numstat churn about 235 lines, to be measured before PR readiness. + +Owner search: `rg -n 'fetchBounded|boundedText|credentialString|safeTimeout|parseJson|normalizeHubOrigin' src/client` finds this owner, not an equivalent client leaf. Reuse the already-imported bounded-body and abort modules; do not copy them or bring catalog/server dependencies into T. Helpers become leaf exports because the retained production protocol functions call them, not to expose internals solely for tests. + +## Re-export block + +Exact residual wiring: + +```ts +export { HubClientError, normalizeHubOrigin } from "./hub-client-transport"; +import { + HubClientError, + boundedText, + credentialString, + fetchBounded, + normalizeHubOrigin, + parseJson, + safeTimeout, +} from "./hub-client-transport"; +``` + +There are **no `export type ... from` lines** because all four public type/interface declarations remain in R: `OneTimeConnectCredential`, `ConnectGuiSession`, `IssuedClientKey`, `StartedClientKeyRotation`. Also retain the exported declarations of `fetchHubReady`, `exchangeConnectPairingGrant`, `issueClientKey`, `revokeClientKey`, `startClientKeyRotation`, `commitClientKeyRotation`, `abortClientKeyRotation`, `downloadClientCatalog`, `probeClientKeyId`. This preserves all **15** original exports without renames/wrappers. Re-export binds neither the class nor the normalizer locally; both require the explicit import. New helper exports do not become exports of the original path. The residual's named compatibility exports are required by this train, not a new internal barrel. + +## Module-level state and cycles + +No top-level mutable collection, `let`, lock, timer or singleton (lane 016:606, verified declarations). Immutable scalar ownership: `READY_BODY_LIMIT` at 30 and `MANAGEMENT_BODY_LIMIT` at 31 stay solely in R; `DEFAULT_TIMEOUT_MS` at 32 moves solely to T. The `slugs` Set at 147 is invocation-local to R's catalog validator; it is not shared state. `headerDeadline` at 91 stays per invocation in T's `fetchBounded`, with both clears (98 and 107) unchanged. + +The same `HubClientError` class must be owned **only by T** and re-exported, never recreated/subclassed in R. Otherwise `instanceof` checks at old lines 104 and 478 and existing consumers would diverge. Putting that class in R and importing it back into T would create R → T → R; moving the class with transport removes that cycle. T imports neither R nor R's protocol types. Existing helpers use only their current Web/Bun types, so no type-only reverse edge is necessary. + +New R → T coupling is functional; request/header deadline sequencing is existing temporal behavior and stays wholly inside the transport implementation. R's validation remains at the existing HTTP/credential boundary—do not add or remove checks merely because the internal file boundary changes. Lane 016:607 found no original return cycle; executor repeats its method G over relative static/type imports/exports and literal dynamic imports and requires no return path through R or T. A leaf import inventory must remain exactly the two imports above. No server/router/lib source or protected Lab root is edited. + +## Tests + +Exact direct-test `rg -l 'client/hub-client["\x27]' tests` list: + +| test file | import/read anchor | disposition | +|---|---|---| +| tests/clients/remote-catalog.test.ts | import at 2 | unchanged; catalog behavior and old-path class identity | +| tests/clients/client-connect.test.ts | import block 8–14 | unchanged; origin, ready, pairing, issuance and connect lifecycle | +| tests/server/api-keys-routes.test.ts | import at 9 | unchanged; server-to-client rotation round trip at 98 | + +No test reads **`src/client/hub-client.ts`** as source after basename, qualified-path and path-segment searches. `tests/server/api-keys-routes.test.ts:262` is a real source oracle, but it reads **`src/server/management/oauth-account-routes.ts`**, not the hub client; unchanged. Other `readFileSync` calls in client-connect inspect generated config/catalog/token artifacts (e.g. 272, 287, 612), not source; unchanged. No `retarget-to-leaf` or `add-leaf-to-scan-list` action is needed. Keep test imports at the old public path to verify the re-exported error class. + +Guards to drive red once later: mutate the moved oversize-result check corresponding to old line 124 and require `tests/clients/remote-catalog.test.ts:93` (forged Content-Length/oversized chunks) to fail; restore. Give catalog requests a whole-request deadline instead of the existing headers-only handling and require the streaming-progress test at line 11 to fail; restore. Existing tests at 39 (inactivity), 73–90 (malformed JSON and class identity), 112 (exact cap), 121 (unconditional requests), 136 (304/content type), plus client-connect 59, 88 and 131 (origin and authority boundaries) must remain intact. Only disposable test fixtures may be used; no live credential exchange or real hub writes. This is future verification, not a claimed red/green run. + +## Verification + +Later executor only, in the L3 worktree: + +```sh +bun run typecheck +bun test tests/clients/remote-catalog.test.ts tests/clients/client-connect.test.ts tests/server/api-keys-routes.test.ts +bun run privacy:scan +wc -l src/client/hub-client-transport.ts src/client/hub-client.ts +rg -n 'from "[^"]*/hub-client"' src gui/src scripts tests +rg -n '^import|^export .* from ' src/client/hub-client-transport.ts src/client/hub-client.ts +git diff --numstat origin/dev...HEAD -- src/client/hub-client.ts src/client/hub-client-transport.ts +``` + +Domains: `tests/clients` and `tests/server` (specific file, not a server-wide source change). Original fan-in stays exactly 4, with consumer identities above. Check export/type equality against the inventory, including constructor identity across leaf/public path, and repeat method G. 002's core-Lab conditional is not triggered by the source touch set; do not change `PROTECTED` roots. Expand scope only after parent approval and include the guard if server/router/lib source is added. + +Full suite only on lidge at the published exact L3 SHA, verify remote HEAD matches that tip and preserve pipeline failure: + +```sh +ssh lidge 'set -o pipefail; cd ~/ocx-ci/opencodex && git fetch origin codex/split-client-hub-client && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Record actual focused/typecheck/privacy/remote exits, restored negative controls and full exact-head CI rollup. Explicit security review under `MAINTAINERS.md:60–61` covers unchanged credential/origin/deadline boundaries and class identity. No local full suite and no test runs, code imports, git mutations or hub traffic during drafting. + +## Accept criteria + +1. All 30 owned declarations are assigned once, with all 15 original public exports preserved; moved bodies/signatures/comments remain identical except required leaf `export` keywords. +2. Both files ≤400 (expected T=113, R=379); actual parent-relative source churn ≤500 or parent re-plan; no #b debt remains. +3. `HubClientError` has one class owner and identical constructor identity from the old and new paths; old imports resolve without consumer rewrites and internal helper exports do not leak through R. +4. No R/T cycle or type-only back edge; no new singleton or duplicated timeout/body-limit owner; imports in T are exactly bounded-body and abort. +5. Origin, credential, redirect, size, UTF-8, timeout, content-type, unconditional-catalog and rotation checks stay at their current boundaries; specified negative controls fail once, are restored, and focused tests/typecheck/privacy pass. +6. Explicit security review, remote full suite and exact-head CI are recorded for L3 independently; base is L2, no unrelated implementation or merge is included. + +## PR + +Title: `refactor(client): isolate bounded hub transport and decoding (split S14 L3/3)` + +Branch: `codex/split-client-hub-client`. Base: `dev`. Closes: none. + +Fill every `.github/PULL_REQUEST_TEMPLATE.md` section (Summary, Verification, Checklist), including actual security review evidence. DEV-STACK-03 map; replace PR placeholders after creation: + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 3 | # | hub transport / codex/split-client-hub-client — this PR | dev | transport and error identity | +| 2 | # | provider readers / codex/split-cli-provider | dev | read handlers and argument parsing | +| 1 | # | status probes / codex/split-cli-status | dev | diagnostic probes and old exports | + +Base: dev — no dependency on the layers below; no cascade obligation. + +Review only this layer's diff. Verify its own base, ancestry and exact-head evidence; this train does not authorize any merge. diff --git a/devlog/_plan/260905_now_split_train/480_lab_events_validate.md b/devlog/_plan/260905_now_split_train/480_lab_events_validate.md new file mode 100644 index 0000000000..ea921bca16 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/480_lab_events_validate.md @@ -0,0 +1,450 @@ +# 480 — S15 L1/5: src/lab/events/validate.ts + +> Historical record imported from `ddb7013ac0c58e513c651d54a96e07f52ac0efbe`. Deferred before implementation; archival proposal only. Continue-goal, before-B admission and worker-implementation instructions below are disabled by the cutoff. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: satisfy-spec through a `pure-move` partition; C3 module refactor with explicit security review of preserved validation/privacy/purge boundaries. cxc-dev §1/§5 and cxc-dev-architecture apply. Main alone owns orchestration, loop and goal state. +- Trigger: the user's68-file modular-debt completion goal and this781-line validator exceeding the400-line file limit. +- Goal: separate field subject and claim validators, with every original public export and behavior preserved. +- Non-goals: no behavior fixes, new validation, renamed symbols, signature changes, new dependencies, expansion of the original public surface, core activation changes, releases or live-service changes. No ledger/storage/fabric implementation is included. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; full tests only on `ssh lidge`, never locally. +- Stop: independent layer-tip verification, fresh exact-head CI and admin landing with expected-head/tree and fetched-dev ancestry proof. Close D, then continue the remaining goal. Do not stop on an ordinary wait timeout. +- Escalation: source drift, unexpected oracle coupling, new cycle, public export loss, changed state lifetime, any scope expansion, or the size-budget conflict below goes to the parent. Do not add a sixth stack layer or edit 002 here. +- Basis: selected published dev `0aae940d63be96481b469363a248e7c92bcac659`, following the verified WP450 delivery09335d7d4. Lab source/tests and build inputs remain byte-identical to093; validate.ts and the ledger test still match the original1362b1a38 inventory. Original source ranges remain valid. The selected-input section below defines admission and the B-stage merge; no unreviewed newer source may be substituted. +- Execution: same app-managed a2c0 worktree, branch `codex/split-lab-events-validate`; preserve completed branches and all checkpoint refs. All tests/typecheck/builds run in an isolated remote checkout on lidge. No peer-task communication or CI-slot arbitration. +- Scope and resources: five planned source files, bounded existing-test changes, one Lab SOT ownership section, and carried000/003/480 documents. Existing GitHub/SSH credentials only, never printed. User authorized unlimited time/tokens and gpt-6-astra high internal delegation; main reclaims a worker packet after two distinct failed workers. No host goal/FSM ownership is delegated. +- Memory artifact: this480 document, the bound goalplan/ledger, and session-local baseline/receipt/mutation artifacts under `.codexclaw/evidence/01a06e97-b9d8-7250-8204-bb788338c288/`. +- Expected outcomes: DONE means this layer's verified admin delivery; NOOP requires evidence the boundary is already fully resolved upstream; external verification failure leaves it unverified, and an unsafe semantic/scope change requires a new plan rather than a waiver. Other goal units remain open. +- Delegation boundary: an internal worker owns only the five named source files and bounded ledger-test changes after B entry; main owns SOT/docs/Git and execution. Any new downward scope is a P amendment, not a mid-B improvisation. No peer-task communication. +- Prior audited seam: `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md:206`. Read together with 000, 001, 002; actual consumer/oracle evidence below supersedes the approximate basename-based counts in 001. + +Structural decision before implementation: Current: artifacts/store.ts:12, ledger/store.ts:19, fabric/observe.ts:14 and lab/index.ts:5 consume this boundary; it imports limits/errors/constants/digest/conformance types/event types (1–48). The 781-line boundary mixes observation construction, common assertions, subjects and claims. Chosen: extract four existing cohesive groups; retain observation validation and dispatch. Rejected: a single 445-line leaf violates the leaf limit; copying common assertions would create two owners. No new runtime abstraction or validation rule is introduced. + +## Symbol inventory + +Measured by `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration' --json=compact src/lab/events/validate.ts`, matched to column-zero declarations in the pinned source. Nested declarations are excluded. Ranges include declaration syntax through its closing line, not preceding comments. + +Consumers are distinct direct import/re-export files across `src gui/src scripts tests`, found with `rg -l` path/symbol searches and verified against the actual import binding. A wildcard re-export counts once for every public symbol; a dynamic namespace import counts for runtime exports, not erased types. Private declarations have zero external consumers, even if unrelated same-named declarations occur elsewhere. Transitive barrel clients are covered by the Lab domain gate, not double-counted. Total direct module consumers: **10**. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| isPlainObject | function | 50–52 | no | 0 | src/lab/events/validate-fields.ts | +| assertString | function | 54–60 | no | 0 | src/lab/events/validate-fields.ts | +| assertIntMs | function | 62–67 | no | 0 | src/lab/events/validate-fields.ts | +| assertClosed | function | 69–74 | no | 0 | src/lab/events/validate-fields.ts | +| utf8LexLess | function | 76–85 | no | 0 | src/lab/events/validate-fields.ts | +| validateSortedUniqueHexIds | function | 88–116 | yes | 1 | src/lab/events/validate-fields.ts | +| validateArtifactRef | function | 118–136 | no | 0 | src/lab/events/validate.ts | +| validateProtocolSubject | function | 138–152 | no | 0 | src/lab/events/validate-subject.ts | +| validateRouteSubject | function | 154–191 | no | 0 | src/lab/events/validate-subject.ts | +| validateTaskSubject | function | 193–211 | no | 0 | src/lab/events/validate-subject.ts | +| validateSubject | function | 213–230 | yes | 2 | src/lab/events/validate-subject.ts | +| stripEventId | function | 232–235 | no | 0 | src/lab/events/validate.ts | +| enforceEventId | function | 237–242 | no | 0 | src/lab/events/validate.ts | +| enforceSerializedSize | function | 244–249 | no | 0 | src/lab/events/validate.ts | +| validateAssertionRecord | function | 251–273 | no | 0 | src/lab/events/validate.ts | +| validateObservationLimits | function | 275–293 | no | 0 | src/lab/events/validate.ts | +| validateObservationEnvironment | function | 295–320 | no | 0 | src/lab/events/validate.ts | +| validateExpectedFailure | function | 322–351 | no | 0 | src/lab/events/validate.ts | +| validateSourceRefs | function | 353–356 | no | 0 | src/lab/events/validate.ts | +| validateObservation | function | 358–433 | no | 0 | src/lab/events/validate.ts | +| validateClaimSnapshot | function | 435–471 | no | 0 | src/lab/events/validate-control-events.ts | +| validateInvalidation | function | 473–487 | no | 0 | src/lab/events/validate-control-events.ts | +| validatePurge | function | 489–544 | no | 0 | src/lab/events/validate-control-events.ts | +| validateLabEvent | function | 547–576 | yes | 4 | src/lab/events/validate.ts | +| FORBIDDEN_FACT_KEYS | const | 578–591 | no | 0 | src/lab/events/validate-claim-source.ts | +| ALLOWED_FACT_KEYS | const | 593–603 | no | 0 | src/lab/events/validate-claim-source.ts | +| validateFacts | function | 605–668 | no | 0 | src/lab/events/validate-claim-source.ts | +| validateResolvedEvidence | function | 670–704 | no | 0 | src/lab/events/validate-claim-source.ts | +| validateClaimSourceManifest | function | 707–752 | yes | 2 | src/lab/events/validate-claim-source.ts | +| assignEventId | function | 754–760 | yes | 5 | src/lab/events/validate.ts | +| artifactClassMediaType | function | 762–781 | yes | 2 | src/lab/events/validate.ts | +| LabValidationError | existing named re-export | 3–3 | yes | 4 | src/lab/events/errors.ts (unchanged owner) | + +Direct edge evidence (including public re-exports): + +- `src/lab/index.ts:5` — *. +- `src/lab/observe/from-conformance.ts:19` — assignEventId. +- `src/lab/observe/from-live.ts:7` — assignEventId. +- `src/lab/ledger/store.ts:19` — LabValidationError, validateLabEvent. +- `src/lab/ledger/invalidation.ts:9` — LabValidationError. +- `src/lab/ledger/purge.ts:16` — assignEventId, validateLabEvent. +- `src/lab/fabric/observe.ts:14` — assignEventId, validateSubject. +- `src/lab/artifacts/store.ts:12` — artifactClassMediaType, validateClaimSourceManifest. +- `src/lab/query/dto-map.ts:12` — validateLabEvent. +- `tests/lab/lab-evidence-ledger.test.ts:42` — LabValidationError. + +Import declarations are not new owners: their exact leaf/residual binding allocations are given below. No default export exists. + +## Leaf partition + +Reuse the existing same-directory sibling convention: `events/limits.ts`, `events/errors.ts`, `ledger/artifact-refs.ts`, `artifacts/secure-fs.ts`, `fabric/producer-protocol.ts`. The five source directories and proposed names were inspected with `rg --files`; none of the new paths exists at the pinned source. No new index/barrel, generic utils module, package or directory is needed. The original paths are compatibility boundaries explicitly retained by the split-train contract, not new internal convenience barrels. + +Move complete source slices with their inline/leading comments as listed; only add the listed imports, named re-exports and leaf-local export modifiers needed by other leaves/the residual. Never re-export formerly private implementation helpers from the original public path. + +### src/lab/events/validate-fields.ts + +- Original slices: `src/lab/events/validate.ts:50–116`. +- Symbols: `isPlainObject`, `assertString`, `assertIntMs`, `assertClosed`, `utf8LexLess`, `validateSortedUniqueHexIds`. +- Expected lines: **71** = 67 moved lines + 4 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: `isPlainObject`, `assertString`, `assertIntMs`, `assertClosed`. +- Own imports: + +```ts +import { LabValidationError } from "./errors"; +import { MAX_SANITIZED_STRING_FIELD } from "../constants"; +import { isSha256Hex } from "../digest"; +``` + +### src/lab/events/validate-subject.ts + +- Original slices: `src/lab/events/validate.ts:138–230`. +- Symbols: `validateProtocolSubject`, `validateRouteSubject`, `validateTaskSubject`, `validateSubject`. +- Expected lines: **98** = 93 moved lines + 5 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: none; preserve existing exported declaration modifiers. +- Own imports: + +```ts +import { isPlainObject, assertString } from "./validate-fields"; +import { LabValidationError } from "./errors"; +import type { EvidenceLayer } from "../constants"; +import type { EvidenceSubjectV1, ProtocolSubjectV1, RouteSubjectV1, TaskSubjectV1 } from "./types"; +``` + +### src/lab/events/validate-claim-source.ts + +- Original slices: `src/lab/events/validate.ts:578–752`. +- Symbols: `FORBIDDEN_FACT_KEYS`, `ALLOWED_FACT_KEYS`, `validateFacts`, `validateResolvedEvidence`, `validateClaimSourceManifest`. +- Expected lines: **181** = 175 moved lines + 6 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: none; preserve existing exported declaration modifiers. +- Own imports: + +```ts +import { isPlainObject, assertString, assertClosed } from "./validate-fields"; +import { LabValidationError } from "./errors"; +import { CLAIM_SOURCE_KINDS, type ClaimSourceKind } from "../constants"; +import { claimSourceManifestDigest, isSha256Hex } from "../digest"; +import type { ClaimCapabilityFactsV1, ClaimSourceManifestV1, ClaimSourceV1, RouteCapabilityEvidenceV1 } from "./types"; +``` + +### src/lab/events/validate-control-events.ts + +- Original slices: `src/lab/events/validate.ts:435–544`. +- Symbols: `validateClaimSnapshot`, `validateInvalidation`, `validatePurge`. +- Expected lines: **117** = 110 moved lines + 7 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: `validateClaimSnapshot`, `validateInvalidation`, `validatePurge`. +- Own imports: + +```ts +import { assertString, assertIntMs, assertClosed, validateSortedUniqueHexIds } from "./validate-fields"; +import { validateSubject } from "./validate-subject"; +import { LabValidationError } from "./errors"; +import { LAB_EVENT_SCHEMA_VERSION, MAX_INVALIDATION_TARGETS, CLAIM_POLARITIES, INVALIDATION_REASONS, PURGE_ACTIONS } from "../constants"; +import { subjectIdForSubject, isSha256Hex } from "../digest"; +import type { ClaimSnapshotEvent, RouteSubjectV1, InvalidationEvent, PurgeTombstoneEvent } from "./types"; +``` + +Residual `src/lab/events/validate.ts`: **301 expected lines**. Retained declarations: `validateArtifactRef`, `stripEventId`, `enforceEventId`, `enforceSerializedSize`, `validateAssertionRecord`, `validateObservationLimits`, `validateObservationEnvironment`, `validateExpectedFailure`, `validateSourceRefs`, `validateObservation`, `validateLabEvent`, `assignEventId`, `artifactClassMediaType`. + +Line accounting: 781 logical source lines − 445 moved lines − 48 original import/header lines + 13 explicit import/re-export lines = 301. Keep formatting compact as shown; extra formatting lines must still fit the 400-line gate. No residual exceeds 400; no #b layer is required for file size. + +Changeset accounting:445 original lines move, giving890 raw move lines before import glue. This exceeds the default500 raw-line threshold; it uses the already approved003 PURE-MOVE-SIZE-01 exception for a single cohesive validator boundary. Measure and report raw churn separately; non-move wiring/test changes must remain at most150. Moved body/comment identity and all31owners require explicit review. This is not a claim that the raw diff is below500 and does not relax the leaf/residual400-line limit. + +## Re-export block + +Exact named re-exports to add/retain at the original path: + +```ts +export { LabValidationError } from "./errors"; +export { validateSortedUniqueHexIds } from "./validate-fields"; +export { validateSubject } from "./validate-subject"; +export { validateClaimSourceManifest } from "./validate-claim-source"; +``` + +validateLabEvent, assignEventId and artifactClassMediaType remain exported declarations. + +Explicit local imports for the residual (replace the original import block); re-export statements bind nothing locally: + +```ts +import { enforceEventStructureLimits } from "./limits"; +import { LabValidationError } from "./errors"; +import { ARTIFACT_CLASSES, ARTIFACT_FILENAME_EXT, EVIDENCE_LAYERS, EVENT_KINDS, EXECUTION_MODES, LAB_EVENT_SCHEMA_VERSION, MAX_SERIALIZED_EVENT_BYTES, OBSERVATION_LIMIT_NAMES, OUTCOMES, type ArtifactClass, type LabEventKind } from "../constants"; +import { eventIdForPayload, isSha256Hex, jcsStringify, subjectIdForSubject } from "../digest"; +import { FAILURE_CLASSIFICATIONS } from "../conformance/types"; +import type { ArtifactRefV1, LabEvent, ObservationEvent } from "./types"; +import { isPlainObject, assertString, assertIntMs, assertClosed } from "./validate-fields"; +import { validateSubject } from "./validate-subject"; +import { validateClaimSnapshot, validateInvalidation, validatePurge } from "./validate-control-events"; +``` + +The residual does not call validateClaimSourceManifest or validateSortedUniqueHexIds after control-event extraction; do not add unused local imports for them. + +## Module-level state and cycles + +The only top-level collections are `FORBIDDEN_FACT_KEYS` at 578–591 and `ALLOWED_FACT_KEYS` at 593–603: both move once to `validate-claim-source.ts`, remain private, and are read-only by convention. No top-level let, Map, WeakMap or lock exists. Sets inside validation functions (253, 304, 324, 714) stay per-call and are not hoisted. +Dependency direction: original → control-events → subject → fields → errors/constants/digest/types; claim-source → fields. In particular, moving control events without moving sorted-ID validation would create control-events → original → control-events; the shared field leaf removes that edge. No leaf imports `./validate` or `../index`. Preserve the existing single LabValidationError class in errors.ts rather than creating another class identity. + +Existing lane evidence found no cycle through this file. Recheck the concrete resolved graph at implementation tip, including type-only edges; typecheck alone does not prove acyclicity. This plan introduces only the directed edges above. Do not change protected core roots, turn startServer async, or add activation imports into them. + +## Tests + +Original direct import/dynamic-import test `rg -l` list; retain its existing public import paths: + +- `tests/lab/lab-evidence-ledger.test.ts` — existing class import at42 and Lab barrel bindings stay; add the narrowly scoped identity/assertion checks below. + +Discovery commands (run across all tests, not just tests/lab): + +```sh +rg -l 'src/lab/events/validate' tests --glob '*.ts' +rg -n 'src/lab/events/validate|validate\.ts' tests --glob '*.ts' +rg -n 'readFileSync|Bun\.file|readFile\(|source\(' tests --glob '*.ts' +``` + +Dedicated source-text readers of this file: **none found**. No retarget-to-leaf or add-leaf-to-scan-list is required for a dedicated source oracle. +The generic `tests/lab/core-lab-boundary.test.ts` reads traversed source at **69**, protected roots at **278/336**, and the server composition source at **355**. It reports the first edge into Lab before traversing that target, so these Lab leaves are not dedicated source-text inputs on a successful run. Disposition: **unchanged**, no scan-list addition, never edit `PROTECTED` (20–28). Include its existing negative-fixture cases in the implementation gate. + +Additional transitive-barrel/behavioral coverage: `tests/lab/lab-fabric-outcome-validation.test.ts` — unchanged; `tests/lab/lab-post-merge-hardening.test.ts` — unchanged. Run `tests/lab` for all indirect callers. + +### Concrete regression changes + +Reuse the existing CL-02 invalidation-validation group at345. Do not add a +new fixture helper, test file, registry entry or broad test split. Searches +found its existing sorted-ID assertions and a private invalidation fixture in +another test file; importing that test would execute an unrelated suite. +The1220-line ledger test is existing test debt: this layer adds only bounded +binding/guard assertions beside its current oracle, not a test-architecture +rewrite. + +Add five imports next to the existing validator import: the facade namespace +as `eventValidation`, the canonical error class from `events/errors` as +`CanonicalLabValidationError`, and the three moved public functions from +their leaves as `fieldSortedIds`, `subjectValidator`, and +`claimManifestValidator`. Add one test named +`event validator facade preserves public bindings and hides private helpers`: + +```ts +expect(Object.keys(eventValidation).sort()).toEqual([ + "LabValidationError", "artifactClassMediaType", "assignEventId", + "validateClaimSourceManifest", "validateLabEvent", "validateSortedUniqueHexIds", "validateSubject", +]); +expect(LabValidationError).toBe(CanonicalLabValidationError); +expect(eventValidation.validateSortedUniqueHexIds).toBe(fieldSortedIds); +expect(eventValidation.validateSubject).toBe(subjectValidator); +expect(eventValidation.validateClaimSourceManifest).toBe(claimManifestValidator); +expect(validateSortedUniqueHexIds).toBe(fieldSortedIds); +expect(validateClaimSourceManifest).toBe(claimManifestValidator); +``` + +In the existing `rejects unsorted, duplicate, empty, and oversize target lists` +test, retain the empty and valid-list assertions, make the unsorted assertion +require `UTF-8 lexicographically sorted`, and the duplicate assertion require +`contains duplicates`. Add the missing oversize assertion: +`expect(() => validateSortedUniqueHexIds([lo, hi], "t", { max: 1 })).toThrow("exceeds 1")`. +Messages distinguish the duplicate guard from the later sortedness guard, +which would otherwise also throw and hide removal of duplicate validation. + +### Named negative controls + +Only in the fresh remote C checkout after its normal suite finishes: remove +the moved duplicate-id guard once, require the named rejection test to fail +on the duplicate-message assertion, restore, and require green. Separately +remove the moved UTF-8 ordering guard, require the same named test to fail on +the unsorted assertion, restore, and require green. Each temporary patch is +reversed on exit; final expected HEAD and clean tree are mandatory. Existing +event-ID validation stays in the residual and is not a moved-guard control. +Keep the post-dispatch event-ID → structure-limit → serialized-size chain +unchanged; existing Lab domain tests cover it and the task-subject callers. + +### Source-of-truth update + +Add an Event validation ownership section to `structure/09_compatibility-lab.md` +with the original facade and the four leaf paths/responsibilities above. State +that `events/errors.ts` remains the one error-class owner and the final guard +order and per-call state stay unchanged. Do not alter the live-route approval, +sanitization, optional-core activation, or compatibility-contract policies. +Read09and11 confirmed those boundaries. No docs-site behavior change is needed. + +## Verification + +Run from the same a2c0 checkout at C after publishing its clean layer HEAD. +All Bun execution is remote. This adapts the verified WP450 recipe, preserving +receipt-internal local/remote SHA checks, frozen installs, repository Bun1.4.0, +build preparation, full logs, failure propagation and final clean identity. +The three named files directly/transitively exercise this boundary; the Lab +domain command covers indirect consumers and core-lab-boundary.test.ts. + +```bash +#!/usr/bin/env bash +set -euo pipefail +wp480_root=$(git rev-parse --show-toplevel) +wp480_expected=$(git rev-parse HEAD) +wp480_status=$(git status --porcelain) +test -z "$wp480_status" +wp480_log="$wp480_root/.codexclaw/evidence/01a06e97-b9d8-7250-8204-bb788338c288/wp480-remote-check-$wp480_expected.log" +mkdir -p "$(dirname "$wp480_log")" +cxc receipt test --cwd "$wp480_root" --session 01a06e97-b9d8-7250-8204-bb788338c288 -- bash -c ' +set -euo pipefail +test "$(git rev-parse HEAD)" = "$1" +local_status=$(git status --porcelain) +test -z "$local_status" +ssh lidge bash -s -- "$1" 2>&1 | tee "$2" +test "$(git rev-parse HEAD)" = "$1" +local_status=$(git status --porcelain) +test -z "$local_status" +' -- "$wp480_expected" "$wp480_log" <<'REMOTE' +set -euo pipefail +expected=${1:?expected SHA required} +[[ "$expected" =~ ^[0-9a-f]{40}$ ]] +run_dir=$(mktemp -d /tmp/ocx-wp480.XXXXXX) +printf 'RETAINED_RUN_DIR=%s\n' "$run_dir" +git clone --no-checkout https://github.com/lidge-jun/opencodex.git "$run_dir/repo" +cd "$run_dir/repo" +git fetch origin refs/heads/codex/split-lab-events-validate +test "$(git rev-parse FETCH_HEAD)" = "$expected" +git checkout --detach "$expected" +bun install --frozen-lockfile +export PATH="$PWD/node_modules/.bin:$PATH" +test "$(bun --version)" = 1.4.0 +(cd gui && bun install --frozen-lockfile && bun run build) +tree_status=$(git status --porcelain) +test -z "$tree_status" +printf 'CHECKOUT=%s\nHEAD=%s\n' "$PWD" "$(git rev-parse HEAD)" +unset OCX_TEST_NO_QUEUE +bun run typecheck +bun test tests/lab/lab-evidence-ledger.test.ts tests/lab/lab-fabric-outcome-validation.test.ts tests/lab/lab-post-merge-hardening.test.ts +bun test tests/lab +bun run privacy:scan +if bun run test; then + test_rc=0 +else + test_rc=$? +fi +printf 'SUITE_EXIT=%s\n' "$test_rc" +if [ "$test_rc" -ne 0 ]; then exit "$test_rc"; fi +test "$(git rev-parse HEAD)" = "$expected" +tree_status=$(git status --porcelain) +test -z "$tree_status" +printf 'VERIFIED_HEAD=%s\n' "$expected" +REMOTE +``` + +Local checks are static only: `git diff --check`, five source line counts, +AST body/symbol/export comparisons and resolved import-graph review. No local +tests/typecheck/build/install, shared remote checkout switch, tail-only proof, +or substitution of baseline/previous-WP success for this resulting HEAD. +Run the two named field-guard negative controls in that same fresh remote +checkout only after its normal suite ends, restore each and record green. +The source/test non-move budget and raw move churn are recorded separately; +planning/SOT prose is disclosed as documentation, not runtime wiring. + +Preserve the original10consumer files and seven public exports. Recheck all +new leaves and the facade for return cycles, including type/literal dynamic +edges. The unchanged error class, module-private Sets, per-call Sets and +final dispatch guard order are explicit review targets. No protected core +source or live-runtime activation is edited. + +## Accept criteria + +1. Exactly this layer's original source plus the listed 4 new leaves and necessary existing-test adjustments are changed at implementation time; no other S15 file is implemented in this PR. +2. The complete inventory above has exactly one implementation/type owner per declaration; all original public names resolve from `src/lab/events/validate.ts`, with no newly public private helper. +3. Every moved body, constant initializer, comment-backed order and signature matches the pinned source; only import/export plumbing changes. +4. Leaf line counts are 71 for `src/lab/events/validate-fields.ts`, 98 for `src/lab/events/validate-subject.ts`, 181 for `src/lab/events/validate-claim-source.ts`, 117 for `src/lab/events/validate-control-events.ts` (or verified formatted equivalents ≤400); residual is approximately 301, always ≤400. No deferred >400 residual. +5. State owners and operation lifetimes match the state section; resolved import graph has no cycle involving the partition. +6. Direct test imports and all source-oracle dispositions are applied exactly as listed; named guards have recorded red→restored-green evidence, without weakening assertions or editing protected roots. +7. Every instantiated remote gate and exact-tip full suite succeeds; source/consumer inventory, privacy and clean bound receipt are recorded. No local suite. +8. Raw move churn and source/test non-move churn are reported separately under the documented003 exception; the latter stays at most150. Leaf/residual limits are not waived. +9. PR base is `dev`, stack map contains all five layers, and fresh exact-head CI and independent review pass. Admin landing uses expected-head matching; preserve open children and prove actual tree/fetched-dev ancestry. + +## PR + +Title: `refactor(lab-events): separate field subject and claim validators (split S15 L1/5)` + +Branch: `codex/split-lab-events-validate`. Base: `dev`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Include this full DEV-STACK-03 map; placeholder PR numbers are intentional until the parent creates the PRs. Review only this layer's diff against its base; L1 is the current layer. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| L1/5 | #TBD-S15-L1 | codex/split-lab-events-validate | dev | separate field subject and claim validators | +| L2/5 | #TBD-S15-L2 | codex/split-lab-ledger-store | codex/split-lab-events-validate | isolate ledger lock ownership | +| L3/5 | #TBD-S15-L3 | codex/split-lab-artifacts-sanitize | dev | separate lexical redaction and UTF-8 truncation | +| L4/5 | #TBD-S15-L4 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | isolate producer outcome validation | +| L5/5 | #TBD-S15-L5 | codex/split-lab-fabric-scratch | dev | separate scratch access from fixture lifetime | + +Base: dev — no dependency on lower layers. If490 is opened while this PR is open, its declared parent is this branch; cascade affected child changes with explicit lease protection while preserving unrelated/checkpoint refs. If this parent has already landed,490 targets dev and must contain the verified parent output. Do not target a deleted parent branch. + +Admin landing is authorized by the user's later instruction and003. No peer-task communication or shared-slot approval is required; do not expand into another task's scope. + +## Current P continuity + +WP450 closed through D after finaldf verification and admin merge09335d7d4; +its post-merge dev CI33964069626 is still monitored. Current branch480 is +based on that09335d7d4 in the same a2c0 worktree, with prior branches/receipts +preserved. Before B, require this base's post-merge result and a fresh base +check; amend/re-audit if relevant input changes. + +Main read the complete781-line validator, applicable source instructions, +Lab/compatibility SOT, and the existing invalidation oracle. cxc map confirmed +the declaration anchors. Copernicus independently verified all31owners, +18moved/13retained declarations,69named imports, seven exports and ten legacy +consumer files. Virtual sizes are71/98/181/117/301; closures6/9/9/10/13 have +no root-return cycle or unresolved relative edge. This is static evidence, +not an A approval or runtime result. Source and ledger-test blobs match the +original1362b1a38 baseline. + +The existing76-line observation validator,64-line facts validator and56-line +purge validator remain function-level debt. Their bodies are preserved in +this file-boundary move; no function-extraction success is claimed. + +P verifier proof: an isolated remote09335d7d4 checkout used frozen dependencies +and repository Bun1.4.0. Build preparation passed; `bun test tests/lab` +executed449tests across53files with0fail, including the named direct/transitive +oracles and core boundary tests. Final baseline HEAD remained clean. Full +output is retained as `wp480-baseline.log` in the session evidence directory. +This validates the newly instantiated domain command, not the future split. +Typecheck/privacy/full-suite commands were already exercised on the identical +WP450-delivered tree and must run again on the changed480 HEAD at C. Both +the baseline and final recipes passed Bash syntax checking; no local test ran. + +WP450 post-merge CI33964069626 also completed successfully on09335d7d4. +That previously pending base check is closed. Before B, still refresh the +base and confirm source identity; any changed input receives a P amendment. + +## Selected published input after independent upstream progress + +Dev advanced while this unit was being planned. Pin +0aae940d63be96481b469363a248e7c92bcac659, tree +8861ad05a9c5fa844edd3df9abf0fc1e68564bd3. Compared with09335d7d4, no Lab +source/test, Lab SOT, package/lockfile, Bun/TypeScript configuration, or +test-runner change exists. The incoming changes are separately published +transport/image work, not this layer's implementation. Do not alter them. + +Run the scoped baseline on this exact input, including the core-boundary +tests that observe changed upstream roots. Audit the unchanged validation +closure and require that baseline to pass before B. This does not claim a +pending or cancelled upstream post-merge run passed: this unit independently +validates its input and its final changed HEAD still needs full remote and +hosted gates. Do not wait for or communicate with peer tasks. + +At B entry, Main normal-merges this pinned commit into the existing docs-only +branch, checks the reviewed source identity, then the worker applies only +the five validator files and bounded ledger-test changes. Main owns the SOT +section and Git. This real source delta occurs during B. Any later upstream +change is inspected for relevant source/dependency drift; the final tested +integration tree must be freshly checked again before admin landing. + +Selected-input proof: the exact0aae940d6 remote baseline passed449Lab tests +across53files,0fail, with clean final HEAD and repository Bun1.4.0. Log: +`wp480-baseline-0aae940d63be96481b469363a248e7c92bcac659.log`. Copernicus checked +the nine existing closure files against093; all are unchanged. The four-leaf +virtual partition therefore retains13total closure modules,69bindings and +the prior no-return-cycle/state-ownership proof. The previous093 baseline +remains historical evidence, not a substitute for this selected-input run. + +No new production field, enum value or enforcement rule is introduced, so +new-field creation/serialization/consumer chains and new-enforcement bypass +fields are not applicable. Existing closed guards retain their semantics +and receive the concrete negative controls above. The namespace/identity +test observes export wiring; it is not a substitute for runtime validation. diff --git a/devlog/_plan/260905_now_split_train/490_lab_ledger_store.md b/devlog/_plan/260905_now_split_train/490_lab_ledger_store.md new file mode 100644 index 0000000000..3dd2f347b7 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/490_lab_ledger_store.md @@ -0,0 +1,202 @@ +# 490 — S15 L2/5: src/lab/ledger/store.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture apply. Parent alone owns orchestration, loop and goal state. +- Goal: isolate ledger lock ownership, with every original public export and behavior preserved. +- Non-goals: no behavior fixes, new validation, renamed symbols, signature changes, new dependencies, public API expansion, core activation changes, releases or merges. This document plans implementation; this drafting task changes no source and runs no tests. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; full tests only on `ssh lidge`, never locally. +- Stop: independent layer-tip verification and green exact-head CI evidence recorded, with the layer PR open; do not merge. Stop before implementation if a stated escalation is unresolved. +- Escalation: source drift, unexpected oracle coupling, new cycle, public export loss, changed state lifetime, any scope expansion, or the size-budget conflict below goes to the parent. Do not add a sixth stack layer or edit 002 here. +- Basis: docs HEAD `4cc219549`; verified source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source anchors in this document refer to that revision. `git show origin/dev:src/lab/ledger/store.ts` matches the working file byte-for-byte. +- Prior audited seam: `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md:403`. Read together with 000, 001, 002; actual consumer/oracle evidence below supersedes the approximate basename-based counts in 001. + +Structural decision before implementation: Current: projection/rebuild.ts:15, observe/from-conformance.ts and fabric/observe.ts:15 consume store; store imports events/validate, digest, paths and filesystem built-ins (1–20). Chosen: move the complete private lock subsystem (34–220) to a kebab/single-concern sibling, retaining append, replay and both public interfaces. Rejected: extracting lock acquisition alone leaves release/recovery split across owners; moving replay too is unnecessary for the size target. Functional callback coupling retains the existing lock lifetime. + +## Symbol inventory + +Measured by `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration' --json=compact src/lab/ledger/store.ts`, matched to column-zero declarations in the pinned source. Nested declarations are excluded. Ranges include declaration syntax through its closing line, not preceding comments. + +Consumers are distinct direct import/re-export files across `src gui/src scripts tests`, found with `rg -l` path/symbol searches and verified against the actual import binding. A wildcard re-export counts once for every public symbol; a dynamic namespace import counts for runtime exports, not erased types. Private declarations have zero external consumers, even if unrelated same-named declarations occur elsewhere. Transitive barrel clients are covered by the Lab domain gate, not double-counted. Total direct module consumers: **10**. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| LedgerStore | interface | 22–26 | yes | 1 | src/lab/ledger/store.ts | +| LedgerMutationContext | interface | 28–32 | yes | 1 | src/lab/ledger/store.ts | +| LEDGER_LOCK_STALE_MS | const | 34–34 | no | 0 | src/lab/ledger/lock.ts | +| LEDGER_LOCK_WAIT_MS | const | 35–35 | no | 0 | src/lab/ledger/lock.ts | +| LedgerLockMeta | interface | 37–41 | no | 0 | src/lab/ledger/lock.ts | +| sleepSyncMs | function | 44–46 | no | 0 | src/lab/ledger/lock.ts | +| readLedgerLockMeta | function | 49–64 | no | 0 | src/lab/ledger/lock.ts | +| isLockHolderAlive | function | 67–77 | no | 0 | src/lab/ledger/lock.ts | +| isLedgerLockStale | function | 80–90 | no | 0 | src/lab/ledger/lock.ts | +| writeLedgerLockMeta | function | 93–107 | no | 0 | src/lab/ledger/lock.ts | +| discardUninitialisedLedgerLock | function | 110–121 | no | 0 | src/lab/ledger/lock.ts | +| releaseLedgerLock | function | 124–136 | no | 0 | src/lab/ledger/lock.ts | +| recoverStaleLedgerLock | function | 146–177 | no | 0 | src/lab/ledger/lock.ts | +| tryAcquireLedgerLock | function | 180–207 | no | 0 | src/lab/ledger/lock.ts | +| withLedgerLock | function | 210–220 | no | 0 | src/lab/ledger/lock.ts | +| appendValidatedLabEvent | function | 223–241 | no | 0 | src/lab/ledger/store.ts | +| isThenable | function | 243–247 | no | 0 | src/lab/ledger/store.ts | +| withLedgerMutation | function | 255–298 | yes | 5 | src/lab/ledger/store.ts | +| appendLabEvent | function | 301–305 | yes | 1 | src/lab/ledger/store.ts | +| appendLabEventIfAbsent | function | 311–313 | yes | 2 | src/lab/ledger/store.ts | +| processLine | function | 315–367 | no | 0 | src/lab/ledger/store.ts | +| processBufferedLines | function | 369–421 | no | 0 | src/lab/ledger/store.ts | +| replayLabLedger | function | 427–515 | yes | 5 | src/lab/ledger/store.ts | +| openLedgerStore | function | 517–528 | yes | 1 | src/lab/ledger/store.ts | +| defaultLedgerPath | function | 530–532 | yes | 1 | src/lab/ledger/store.ts | + +Direct edge evidence (including public re-exports): + +- `src/lab/index.ts:9` — *. +- `src/lab/observe/from-conformance.ts:20` — withLedgerMutation. +- `src/lab/observe/from-live.ts:8` — withLedgerMutation. +- `src/lab/ledger/purge.ts:22` — withLedgerMutation. +- `src/lab/projection/rebuild.ts:15` — replayLabLedger. +- `src/lab/fabric/observe.ts:15` — withLedgerMutation. +- `src/lab/public/operator.ts:1` — replayLabLedger. +- `tests/lab/lab-evidence-ledger.test.ts:34` — appendLabEventIfAbsent. +- `tests/lab/lab-public-review-fixes.test.ts:12` — replayLabLedger. +- `tests/lab/lab-live-probe.test.ts:13` — replayLabLedger. + +Import declarations are not new owners: their exact leaf/residual binding allocations are given below. No default export exists. + +## Leaf partition + +Reuse the existing same-directory sibling convention: `events/limits.ts`, `events/errors.ts`, `ledger/artifact-refs.ts`, `artifacts/secure-fs.ts`, `fabric/producer-protocol.ts`. The five source directories and proposed names were inspected with `rg --files`; none of the new paths exists at the pinned source. No new index/barrel, generic utils module, package or directory is needed. The original paths are compatibility boundaries explicitly retained by the split-train contract, not new internal convenience barrels. + +Move complete source slices with their inline/leading comments as listed; only add the listed imports, named re-exports and leaf-local export modifiers needed by other leaves/the residual. Never re-export formerly private implementation helpers from the original public path. + +### src/lab/ledger/lock.ts + +- Original slices: `src/lab/ledger/store.ts:34–220`. +- Symbols: `LEDGER_LOCK_STALE_MS`, `LEDGER_LOCK_WAIT_MS`, `LedgerLockMeta`, `sleepSyncMs`, `readLedgerLockMeta`, `isLockHolderAlive`, `isLedgerLockStale`, `writeLedgerLockMeta`, `discardUninitialisedLedgerLock`, `releaseLedgerLock`, `recoverStaleLedgerLock`, `tryAcquireLedgerLock`, `withLedgerLock`. +- Expected lines: **192** = 187 moved lines + 5 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: `withLedgerLock`. +- Own imports: + +```ts +import { closeSync, constants as fsConstants, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs"; +import { randomBytes } from "node:crypto"; +import { dirname } from "node:path"; +import { LabValidationError } from "../events/validate"; +``` + +Residual `src/lab/ledger/store.ts`: **333 expected lines**. Retained declarations: `LedgerStore`, `LedgerMutationContext`, `appendValidatedLabEvent`, `isThenable`, `withLedgerMutation`, `appendLabEvent`, `appendLabEventIfAbsent`, `processLine`, `processBufferedLines`, `replayLabLedger`, `openLedgerStore`, `defaultLedgerPath`. + +Line accounting: 532 logical source lines − 187 moved lines − 20 original import/header lines + 8 explicit import/re-export lines = 333. The inventory's 531 is `wc -l`: the original lacks a trailing newline and has 532 logical lines. Keep formatting compact as shown; extra formatting lines must still fit the 400-line gate. No residual exceeds 400; no #b layer is required for file size. + +Changeset accounting: 187 original lines move; raw additions+deletions for the move alone are 374, before import glue. This move is below 500 raw changed lines before glue; check final per-layer numstat, including tests, before PR readiness. Escalate if it exceeds 500. + +## Re-export block + +No public declaration is moved, so the exact re-export addition is the empty block. Do not add `export { withLedgerLock } from "./lock"`: that would widen the public surface. + +LedgerStore, LedgerMutationContext, withLedgerMutation, appendLabEvent, appendLabEventIfAbsent, replayLabLedger, openLedgerStore and defaultLedgerPath remain exported declarations. + +Explicit local imports for the residual (replace the original import block); re-export statements bind nothing locally: + +```ts +import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readSync, statSync, writeSync } from "node:fs"; +import { dirname } from "node:path"; +import { jcsStringify } from "../digest"; +import { MAX_SERIALIZED_EVENT_BYTES } from "../constants"; +import type { LabEvent, LedgerCorruption, ReplayResult } from "../events/types"; +import { LabValidationError, validateLabEvent } from "../events/validate"; +import { ensureLabDirs, labLedgerPath } from "../paths"; +import { withLedgerLock } from "./lock"; +``` + + + +## Module-level state and cycles + +There is no module-level let, Map, Set, WeakMap, wait buffer or live lock handle. `LEDGER_LOCK_STALE_MS` (34) and `LEDGER_LOCK_WAIT_MS` (35) move to lock.ts as private constants; `LedgerLockMeta` (37–41) is owned there. Disk lock and recovery mutex acquisition/release (146–220) move together. File descriptors, random ownership tokens and deadlines remain per-call. The transaction's `active` closure (260–296) remains in store.ts; replay's seenIds Set (449) remains per replay. +Current coupling is temporal lock → callback → finally release, expressed by the existing synchronous callback. New direction is store → lock → event validation boundary; lock never imports store, calls replay, or owns a second mutation gate. Splitting replay is unnecessary to meet 400 lines and would raise churn; it remains with the append/mutation façade. Do not substitute independent lock instances or alter timeout, stale-owner, recovery-mutex, token-match or cleanup behavior. + +Existing lane evidence found no cycle through this file. Recheck the concrete resolved graph at implementation tip, including type-only edges; typecheck alone does not prove acyclicity. This plan introduces only the directed edges above. Do not change protected core roots, turn startServer async, or add activation imports into them. + +## Tests + +Direct import/dynamic-import test `rg -l` list, all **unchanged** at their original import path: + +- `tests/lab/lab-live-probe.test.ts` — unchanged (import at 13). +- `tests/lab/lab-public-review-fixes.test.ts` — unchanged (import at 12). +- `tests/lab/lab-evidence-ledger.test.ts` — unchanged (import at 34). + +Discovery commands (run across all tests, not just tests/lab): + +```sh +rg -l 'src/lab/ledger/store' tests --glob '*.ts' +rg -n 'src/lab/ledger/store|store\.ts' tests --glob '*.ts' +rg -n 'readFileSync|Bun\.file|readFile\(|source\(' tests --glob '*.ts' +``` + +Dedicated source-text readers of this file: **none found**. No retarget-to-leaf or add-leaf-to-scan-list is required for a dedicated source oracle. The three direct test files above import runtime exports. The `store.ts` basename hits in other domains read different stores; 001's “3 text oracles” is not three reads of this ledger source. Do not retarget those unrelated tests. +The generic `tests/lab/core-lab-boundary.test.ts` reads traversed source at **69**, protected roots at **278/336**, and the server composition source at **355**. It reports the first edge into Lab before traversing that target, so these Lab leaves are not dedicated source-text inputs on a successful run. Disposition: **unchanged**, no scan-list addition, never edit `PROTECTED` (20–28). Include its existing negative-fixture cases in the implementation gate. + +Additional transitive-barrel/behavioral coverage: `tests/lab/lab-ledger-mutation-lock.test.ts` — unchanged; `tests/lab/lab-private-file-durability.test.ts` — unchanged. Run `tests/lab` for all indirect callers. + +Guards to drive red once during implementation (temporary mutations must be restored before committing): + +Drive the existing lock-wait case red once with a temporary bypass of the moved lock boundary (`tests/lab/lab-ledger-mutation-lock.test.ts:134`), then restore. Keep dead-owner recovery (152), async callback rejection/context invalidation (185), artifact publication under the same lock (208), and purge serialization (238). Run the unchanged bounded-line and UTF-8 replay cases at `tests/lab/lab-evidence-ledger.test.ts:978,991` even though replay stays in place. + +No tests or red mutations were run while drafting this plan; these are executor obligations. + +## Verification + +Instantiate `002_layer_map.md` Per-layer gate in the dedicated layer worktree, not this docs worktree: + +```sh +bun run typecheck +bun test tests/lab/lab-ledger-mutation-lock.test.ts tests/lab/lab-evidence-ledger.test.ts tests/lab/lab-live-probe.test.ts tests/lab/lab-public-review-fixes.test.ts tests/lab/lab-private-file-durability.test.ts +bun test tests/lab +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/lab/ledger/lock.ts src/lab/ledger/store.ts +rg -n 'lab/ledger/store|from "./store"' src gui/src scripts tests +git diff --check +git diff --numstat codex/split-lab-events-validate...HEAD +# Full repository suite: remote only, exact branch tip; pipefail preserves failures. +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-ledger-store && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test 2>&1 | tail -15"' +``` + +Required outcome: all local gates exit 0; focused/domain tests have zero failures; every leaf and residual ≤400. The boundary test is included explicitly even though no protected source is edited. Confirm the remote printed SHA equals the layer tip and save the full exit status plus test totals; the tail alone is not proof. Full suite remains remote-only. + +Compare resolved direct consumer bindings against the 10-file baseline above (raw basename grep is only a candidate search and can include unrelated modules). Leaf names matching the search are not new original-path consumers. Existing public callers must not need migration. Use the already available parser/import-graph mechanism, or a read-only resolver, to report no cycles containing this residual or any new leaf, including type edges; do not install a new analyzer just for this split. Verify moved declaration bodies are identical to origin/dev after stripping only the newly required export modifiers, and inspect `git diff --color-moved` for accidental behavior edits. + +For PR readiness, record exact-head CI (Linux, macOS, Windows) and review status separately from local checks. No tests, typecheck, privacy scan or remote suite have been executed in this docs-only delegation. + +## Accept criteria + +1. Exactly this layer's original source plus the listed 1 new leaves and necessary existing-test adjustments are changed at implementation time; no other S15 file is implemented in this PR. +2. The complete inventory above has exactly one implementation/type owner per declaration; all original public names resolve from `src/lab/ledger/store.ts`, with no newly public private helper. +3. Every moved body, constant initializer, comment-backed order and signature matches the pinned source; only import/export plumbing changes. +4. Leaf line counts are 192 for `src/lab/ledger/lock.ts` (or verified formatted equivalents ≤400); residual is approximately 333, always ≤400. No deferred >400 residual. +5. State owners and operation lifetimes match the state section; resolved import graph has no cycle involving the partition. +6. Direct test imports and all source-oracle dispositions are applied exactly as listed; named guards have recorded red→restored-green evidence, without weakening assertions or editing protected roots. +7. Every instantiated local gate and exact-tip remote suite succeeds; source/consumer inventory and privacy scan are recorded. No repository-wide local suite. +8. Final raw changed-source-line count stays ≤500 or the parent explicitly resolves the size escalation. +9. PR base is `codex/split-lab-events-validate`, stack map contains all five layers, and exact-head CI is green. No merge is performed. + +## PR + +Title: `refactor(lab-ledger): isolate ledger lock ownership (split S15 L2/5)` + +Branch: `codex/split-lab-ledger-store`. Base: `codex/split-lab-events-validate`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Include this full DEV-STACK-03 map; placeholder PR numbers are intentional until the parent creates the PRs. Review only this layer's diff against its base; L2 is the current layer. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| L1/5 | #TBD-S15-L1 | codex/split-lab-events-validate | dev | separate field subject and claim validators | +| L2/5 | #TBD-S15-L2 | codex/split-lab-ledger-store | codex/split-lab-events-validate | isolate ledger lock ownership | +| L3/5 | #TBD-S15-L3 | codex/split-lab-artifacts-sanitize | dev | separate lexical redaction and UTF-8 truncation | +| L4/5 | #TBD-S15-L4 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | isolate producer outcome validation | +| L5/5 | #TBD-S15-L5 | codex/split-lab-fabric-scratch | dev | separate scratch access from fixture lifetime | + +Depends on #TBD-S15-L1. A change to the real parent `codex/split-lab-events-validate` requires parent-managed cascade of this layer and fresh exact-head verification. Bottom-up integration applies only to this dependency chain; no merge authorization is conveyed by the plan. The current delegated task performs no Git mutation or PR action. diff --git a/devlog/_plan/260905_now_split_train/500_lab_artifacts_sanitize.md b/devlog/_plan/260905_now_split_train/500_lab_artifacts_sanitize.md new file mode 100644 index 0000000000..1704ffedb9 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/500_lab_artifacts_sanitize.md @@ -0,0 +1,224 @@ +# 500 — S15 L3/5: src/lab/artifacts/sanitize.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture apply. Parent alone owns orchestration, loop and goal state. +- Goal: separate lexical redaction and UTF-8 truncation, with every original public export and behavior preserved. +- Non-goals: no behavior fixes, new validation, renamed symbols, signature changes, new dependencies, public API expansion, core activation changes, releases or merges. This document plans implementation; this drafting task changes no source and runs no tests. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; full tests only on `ssh lidge`, never locally. +- Stop: independent layer-tip verification and green exact-head CI evidence recorded, with the layer PR open; do not merge. Stop before implementation if a stated escalation is unresolved. +- Escalation: source drift, unexpected oracle coupling, new cycle, public export loss, changed state lifetime, any scope expansion, or the size-budget conflict below goes to the parent. Do not add a sixth stack layer or edit 002 here. +- Basis: docs HEAD `4cc219549`; verified source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source anchors in this document refer to that revision. `git show origin/dev:src/lab/artifacts/sanitize.ts` matches the working file byte-for-byte. +- Prior audited seam: `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md:335`. Read together with 000, 001, 002; actual consumer/oracle evidence below supersedes the approximate basename-based counts in 001. + +Structural decision before implementation: Current: artifacts/store.ts:23, projection/rebuild.ts:5 and fabric/observe.ts:2 consume sanitize; the only imports are ArtifactClass, MAX_SANITIZED_STRING_FIELD, jcsStringify and redactSecretString (5–8). Chosen: move existing address/scanned-span operations, account/URL-path operations and UTF-8 truncation into three dependency-free siblings. Keep contract checks, recursive normalization and scrubString's ordered pipeline. Rejected: moving the complete lexical section into one file would exceed 400 lines; changing regex behavior or merging it with src/lib/redact would not be a pure move. structure/09_compatibility-lab.md's evidence-text contract remains authoritative and unchanged. + +## Symbol inventory + +Measured by `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration' --json=compact src/lab/artifacts/sanitize.ts`, matched to column-zero declarations in the pinned source. Nested declarations are excluded. Ranges include declaration syntax through its closing line, not preceding comments. + +Consumers are distinct direct import/re-export files across `src gui/src scripts tests`, found with `rg -l` path/symbol searches and verified against the actual import binding. A wildcard re-export counts once for every public symbol; a dynamic namespace import counts for runtime exports, not erased types. Private declarations have zero external consumers, even if unrelated same-named declarations occur elsewhere. Transitive barrel clients are covered by the Lab domain gate, not double-counted. Total direct module consumers: **8**. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| FORBIDDEN_KEY | const | 10–10 | no | 0 | src/lab/artifacts/sanitize.ts | +| SECRETISH | const | 11–11 | no | 0 | src/lab/artifacts/sanitize.ts | +| SECRETISH_GLOBAL | const | 12–12 | no | 0 | src/lab/artifacts/sanitize.ts | +| redactForArtifact | function | 14–27 | yes | 2 | src/lab/artifacts/sanitize.ts | +| FORBIDDEN_CONTRACT_KEYS | const | 29–29 | no | 0 | src/lab/artifacts/sanitize.ts | +| assertNoSecretMaterial | function | 31–53 | no | 0 | src/lab/artifacts/sanitize.ts | +| scrubValue | function | 55–80 | no | 0 | src/lab/artifacts/sanitize.ts | +| JWT_RE | const | 99–99 | no | 0 | src/lab/artifacts/sanitize.ts | +| EMAIL_RE | const | 108–109 | no | 0 | src/lab/artifacts/sanitize.ts | +| PREFIXED_ACCOUNT_RE | const | 113–113 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| UUID_RE | const | 114–114 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| MAC_RE | const | 121–121 | no | 0 | src/lab/artifacts/sanitize.ts | +| IPV4_RE | const | 122–122 | no | 0 | src/lab/artifacts/sanitize.ts | +| HOSTNAME_RE | const | 142–142 | no | 0 | src/lab/artifacts/sanitize.ts | +| STRONG_HOST_CONTEXT_RE | const | 163–164 | no | 0 | src/lab/artifacts/sanitize.ts | +| WEAK_HOST_CONTEXT_RE | const | 165–166 | no | 0 | src/lab/artifacts/sanitize.ts | +| DOTTED_NAMESPACE_RE | const | 173–173 | no | 0 | src/lab/artifacts/sanitize.ts | +| RESERVED_HOST_NAMES | const | 190–190 | no | 0 | src/lab/artifacts/sanitize.ts | +| PROSE_AFTER_MARKER | const | 191–194 | no | 0 | src/lab/artifacts/sanitize.ts | +| isHostCandidate | function | 195–204 | no | 0 | src/lab/artifacts/sanitize.ts | +| AMBIGUOUS_HOST_RE | const | 212–212 | no | 0 | src/lab/artifacts/sanitize.ts | +| CONTEXTUAL_HOST_TOKEN_RE | const | 221–221 | no | 0 | src/lab/artifacts/sanitize.ts | +| ACCOUNT_LABEL_RE | const | 229–229 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| IDENTIFIER_ONLY_RE | const | 230–230 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| UNQUOTED_TERMINATOR | const | 231–231 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| isPrefixedAccount | function | 233–238 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| scrubUrlPath | function | 246–266 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| isIdentifierShape | function | 268–270 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| UUID_ANYWHERE_RE | const | 272–272 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| redactIdentifiersInText | function | 281–283 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| decodeToFixedPoint | function | 286–299 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| isIpv4 | function | 302–306 | no | 0 | src/lab/artifacts/sanitize-addresses.ts | +| isIpv6 | function | 309–338 | no | 0 | src/lab/artifacts/sanitize-addresses.ts | +| redactIpv6 | function | 345–389 | no | 0 | src/lab/artifacts/sanitize-addresses.ts | +| redactScannedSpans | function | 392–410 | no | 0 | src/lab/artifacts/sanitize-addresses.ts | +| redactContextualAccounts | function | 422–459 | no | 0 | src/lab/artifacts/sanitize-accounts.ts | +| scrubString | function | 461–530 | no | 0 | src/lab/artifacts/sanitize.ts | +| TRUNCATION_MARKERS | const | 532–544 | no | 0 | src/lab/artifacts/sanitize-truncate.ts | +| truncateUtf8 | function | 554–577 | yes | 5 | src/lab/artifacts/sanitize-truncate.ts | +| sanitizeDiagnostic | function | 580–582 | yes | 8 | src/lab/artifacts/sanitize.ts | +| sanitizedJsonBytes | function | 584–586 | yes | 1 | src/lab/artifacts/sanitize.ts | + +Direct edge evidence (including public re-exports): + +- `src/lab/index.ts:8` — *. +- `src/lab/observe/from-conformance.ts:6` — sanitizeDiagnostic, truncateUtf8. +- `src/lab/observe/from-live.ts:3` — sanitizeDiagnostic, truncateUtf8. +- `src/lab/projection/rebuild.ts:5` — sanitizeDiagnostic. +- `src/lab/fabric/observe.ts:2` — sanitizeDiagnostic, truncateUtf8. +- `src/lab/artifacts/store.ts:23` — redactForArtifact, sanitizeDiagnostic. +- `src/lab/query/dto-map.ts:1` — sanitizeDiagnostic. +- `tests/lab/lab-evidence-sanitization.test.ts:22` — sanitizeDiagnostic, truncateUtf8. + +Import declarations are not new owners: their exact leaf/residual binding allocations are given below. No default export exists. + +## Leaf partition + +Reuse the existing same-directory sibling convention: `events/limits.ts`, `events/errors.ts`, `ledger/artifact-refs.ts`, `artifacts/secure-fs.ts`, `fabric/producer-protocol.ts`. The five source directories and proposed names were inspected with `rg --files`; none of the new paths exists at the pinned source. No new index/barrel, generic utils module, package or directory is needed. The original paths are compatibility boundaries explicitly retained by the split-train contract, not new internal convenience barrels. + +Move complete source slices with their inline/leading comments as listed; only add the listed imports, named re-exports and leaf-local export modifiers needed by other leaves/the residual. Never re-export formerly private implementation helpers from the original public path. + +### src/lab/artifacts/sanitize-accounts.ts + +- Original slices: `src/lab/artifacts/sanitize.ts:110–114`, `src/lab/artifacts/sanitize.ts:222–299`, `src/lab/artifacts/sanitize.ts:412–459`. +- Symbols: `PREFIXED_ACCOUNT_RE`, `UUID_RE`, `ACCOUNT_LABEL_RE`, `IDENTIFIER_ONLY_RE`, `UNQUOTED_TERMINATOR`, `isPrefixedAccount`, `scrubUrlPath`, `isIdentifierShape`, `UUID_ANYWHERE_RE`, `redactIdentifiersInText`, `decodeToFixedPoint`, `redactContextualAccounts`. +- Expected lines: **133** = 131 moved lines + 0 import/header-separator lines + 2 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: `PREFIXED_ACCOUNT_RE`, `scrubUrlPath`, `redactContextualAccounts`. +- Own imports: none (dependency-free). + +### src/lab/artifacts/sanitize-addresses.ts + +- Original slices: `src/lab/artifacts/sanitize.ts:301–410`. +- Symbols: `isIpv4`, `isIpv6`, `redactIpv6`, `redactScannedSpans`. +- Expected lines: **110** = 110 moved lines + 0 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: `isIpv4`, `redactIpv6`, `redactScannedSpans`. +- Own imports: none (dependency-free). + +### src/lab/artifacts/sanitize-truncate.ts + +- Original slices: `src/lab/artifacts/sanitize.ts:532–577`. +- Symbols: `TRUNCATION_MARKERS`, `truncateUtf8`. +- Expected lines: **46** = 46 moved lines + 0 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: none; preserve existing exported declaration modifiers. +- Own imports: none (dependency-free). + +Residual `src/lab/artifacts/sanitize.ts`: **302 expected lines**. Retained declarations: `FORBIDDEN_KEY`, `SECRETISH`, `SECRETISH_GLOBAL`, `redactForArtifact`, `FORBIDDEN_CONTRACT_KEYS`, `assertNoSecretMaterial`, `scrubValue`, `JWT_RE`, `EMAIL_RE`, `MAC_RE`, `IPV4_RE`, `HOSTNAME_RE`, `STRONG_HOST_CONTEXT_RE`, `WEAK_HOST_CONTEXT_RE`, `DOTTED_NAMESPACE_RE`, `RESERVED_HOST_NAMES`, `PROSE_AFTER_MARKER`, `isHostCandidate`, `AMBIGUOUS_HOST_RE`, `CONTEXTUAL_HOST_TOKEN_RE`, `scrubString`, `sanitizeDiagnostic`, `sanitizedJsonBytes`. + +Line accounting: 586 logical source lines − 287 moved lines + 3 explicit import/re-export lines = 302. The inventory's 585 is `wc -l`: the original lacks a trailing newline and has 586 logical lines. Keep formatting compact as shown; extra formatting lines must still fit the 400-line gate. No residual exceeds 400; no #b layer is required for file size. + +Changeset accounting: 287 original lines move; raw additions+deletions for the move alone are 574, before import glue. **Parent decision required:** this exceeds the ≤500 changed-source-line/default PR limit if measured as raw Git additions+deletions. The fixed five-layer S15 map does not allocate a #b for this file. Do not claim this layer satisfies that limit. Parent must explicitly accept a pure-move size exception (with moved-line review evidence) or revise the train topology before code execution. This document does not authorize either change. + +## Re-export block + +Exact named re-exports to add/retain at the original path: + +```ts +export { truncateUtf8 } from "./sanitize-truncate"; +``` + +redactForArtifact, sanitizeDiagnostic and sanitizedJsonBytes remain exported declarations. + +Explicit local imports for the residual (add alongside unchanged original imports); re-export statements bind nothing locally: + +```ts +import { PREFIXED_ACCOUNT_RE, scrubUrlPath, redactContextualAccounts } from "./sanitize-accounts"; +import { isIpv4, redactIpv6, redactScannedSpans } from "./sanitize-addresses"; +``` + +The residual does not call truncateUtf8, so no local truncateUtf8 import is needed. + +## Module-level state and cycles + +`RESERVED_HOST_NAMES` (190) and `PROSE_AFTER_MARKER` (191–194) stay private in sanitize.ts, with isHostCandidate and its ordered host replacements. No top-level let/Map/WeakMap/lock exists. +Stateful RegExp objects are not duplicated: `PREFIXED_ACCOUNT_RE` (113), `ACCOUNT_LABEL_RE` (229), `UUID_ANYWHERE_RE` (272) move once to sanitize-accounts.ts. PREFIXED_ACCOUNT_RE is a leaf export only because the existing ordered scrubString pipeline also uses that exact object; preserve its global flags and isPrefixedAccount's lastIndex resets (234,236), plus contextual account cursor updates (425,455). Do not clone it in the façade or expose it through lab/index.ts. UUID_RE (114), IDENTIFIER_ONLY_RE (230), UNQUOTED_TERMINATOR (231) are private leaf patterns. SECRETISH_GLOBAL (12), JWT_RE (99), EMAIL_RE (108), MAC_RE (121), IPV4_RE (122), HOSTNAME_RE (142), STRONG_HOST_CONTEXT_RE (163), WEAK_HOST_CONTEXT_RE (165) stay in the façade; the source inventory records the other non-global patterns too. TRUNCATION_MARKERS (532–544) has one private owner in sanitize-truncate.ts. +New edges are sanitize → accounts/addresses/truncate; all three leaves have no imports. In particular accounts does not import SECRETISH or scrubString from sanitize. The global-regex reuse is existing stateful lexical coupling, not permission to add new mutations or resets. Keeping scrubString in place preserves the total replacement order and avoids a façade/leaf back-edge. + +Existing lane evidence found no cycle through this file. Recheck the concrete resolved graph at implementation tip, including type-only edges; typecheck alone does not prove acyclicity. This plan introduces only the directed edges above. Do not change protected core roots, turn startServer async, or add activation imports into them. + +## Tests + +Direct import/dynamic-import test `rg -l` list, all **unchanged** at their original import path: + +- `tests/lab/lab-evidence-sanitization.test.ts` — unchanged (import at 22). + +Discovery commands (run across all tests, not just tests/lab): + +```sh +rg -l 'src/lab/artifacts/sanitize' tests --glob '*.ts' +rg -n 'src/lab/artifacts/sanitize|sanitize\.ts' tests --glob '*.ts' +rg -n 'readFileSync|Bun\.file|readFile\(|source\(' tests --glob '*.ts' +``` + +Dedicated source-text readers of this file: **none found**. No retarget-to-leaf or add-leaf-to-scan-list is required for a dedicated source oracle. +The generic `tests/lab/core-lab-boundary.test.ts` reads traversed source at **69**, protected roots at **278/336**, and the server composition source at **355**. It reports the first edge into Lab before traversing that target, so these Lab leaves are not dedicated source-text inputs on a successful run. Disposition: **unchanged**, no scan-list addition, never edit `PROTECTED` (20–28). Include its existing negative-fixture cases in the implementation gate. + +Additional transitive-barrel/behavioral coverage: `tests/lab/lab-evidence-ledger.test.ts` — unchanged; `tests/lab/lab-fabric-task.test.ts` — unchanged. Run `tests/lab` for all indirect callers. + +Guards to drive red once during implementation (temporary mutations must be restored before committing): + +Drive the account/path punctuation corpus (`tests/lab/lab-evidence-sanitization.test.ts:128,160,184,355`) red once with a temporary account-redactor bypass, then restore. Drive the marker/code-point truncation guard at 373 red once by temporarily replacing the moved truncator with a naïve slice, then restore. Keep address/compressed-form cases (146), false-positive preservation (115,338), and the integration sinks (385,437) unchanged. These are behavioral guards, not source oracles. + +No tests or red mutations were run while drafting this plan; these are executor obligations. + +## Verification + +Instantiate `002_layer_map.md` Per-layer gate in the dedicated layer worktree, not this docs worktree: + +```sh +bun run typecheck +bun test tests/lab/lab-evidence-sanitization.test.ts tests/lab/lab-evidence-ledger.test.ts tests/lab/lab-fabric-task.test.ts +bun test tests/lab +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/lab/artifacts/sanitize-accounts.ts src/lab/artifacts/sanitize-addresses.ts src/lab/artifacts/sanitize-truncate.ts src/lab/artifacts/sanitize.ts +rg -n 'lab/artifacts/sanitize|from "./sanitize"' src gui/src scripts tests +git diff --check +git diff --numstat origin/dev...HEAD +# Full repository suite: remote only, exact branch tip; pipefail preserves failures. +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-artifacts-sanitize && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test 2>&1 | tail -15"' +``` + +Required outcome: all local gates exit 0; focused/domain tests have zero failures; every leaf and residual ≤400. The boundary test is included explicitly even though no protected source is edited. Confirm the remote printed SHA equals the layer tip and save the full exit status plus test totals; the tail alone is not proof. Full suite remains remote-only. + +Compare resolved direct consumer bindings against the 8-file baseline above (raw basename grep is only a candidate search and can include unrelated modules). Leaf names matching the search are not new original-path consumers. Existing public callers must not need migration. Include wildcard re-export consumers in this comparison. Use the already available parser/import-graph mechanism, or a read-only resolver, to report no cycles containing this residual or any new leaf, including type edges; do not install a new analyzer just for this split. Verify moved declaration bodies are identical to origin/dev after stripping only the newly required export modifiers, and inspect `git diff --color-moved` for accidental behavior edits. + +For PR readiness, record exact-head CI (Linux, macOS, Windows) and review status separately from local checks. No tests, typecheck, privacy scan or remote suite have been executed in this docs-only delegation. + +## Accept criteria + +1. Exactly this layer's original source plus the listed 3 new leaves and necessary existing-test adjustments are changed at implementation time; no other S15 file is implemented in this PR. +2. The complete inventory above has exactly one implementation/type owner per declaration; all original public names resolve from `src/lab/artifacts/sanitize.ts`, with no newly public private helper. +3. Every moved body, constant initializer, comment-backed order and signature matches the pinned source; only import/export plumbing changes. +4. Leaf line counts are 133 for `src/lab/artifacts/sanitize-accounts.ts`, 110 for `src/lab/artifacts/sanitize-addresses.ts`, 46 for `src/lab/artifacts/sanitize-truncate.ts` (or verified formatted equivalents ≤400); residual is approximately 302, always ≤400. No deferred >400 residual. +5. State owners and operation lifetimes match the state section; resolved import graph has no cycle involving the partition. +6. Direct test imports and all source-oracle dispositions are applied exactly as listed; named guards have recorded red→restored-green evidence, without weakening assertions or editing protected roots. +7. Every instantiated local gate and exact-tip remote suite succeeds; source/consumer inventory and privacy scan are recorded. No repository-wide local suite. +8. The parent has explicitly resolved the raw-diff size exception/topology escalation before source implementation. +9. PR base is `codex/split-lab-ledger-store`, stack map contains all five layers, and exact-head CI is green. No merge is performed. + +## PR + +Title: `refactor(lab-artifacts): separate lexical redaction and UTF-8 truncation (split S15 L3/5)` + +Branch: `codex/split-lab-artifacts-sanitize`. Base: `dev`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Include this full DEV-STACK-03 map; placeholder PR numbers are intentional until the parent creates the PRs. Review only this layer's diff against its base; L3 is the current layer. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| L1/5 | #TBD-S15-L1 | codex/split-lab-events-validate | dev | separate field subject and claim validators | +| L2/5 | #TBD-S15-L2 | codex/split-lab-ledger-store | codex/split-lab-events-validate | isolate ledger lock ownership | +| L3/5 | #TBD-S15-L3 | codex/split-lab-artifacts-sanitize | dev | separate lexical redaction and UTF-8 truncation | +| L4/5 | #TBD-S15-L4 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | isolate producer outcome validation | +| L5/5 | #TBD-S15-L5 | codex/split-lab-fabric-scratch | dev | separate scratch access from fixture lifetime | + +Base: dev — no dependency on lower layers; this layer is the parent of 510 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). + +No merge authorization is conveyed by the plan. The current delegated task performs no Git mutation or PR action. diff --git a/devlog/_plan/260905_now_split_train/510_lab_fabric_observe.md b/devlog/_plan/260905_now_split_train/510_lab_fabric_observe.md new file mode 100644 index 0000000000..1c0502dd8a --- /dev/null +++ b/devlog/_plan/260905_now_split_train/510_lab_fabric_observe.md @@ -0,0 +1,207 @@ +# 510 — S15 L4/5: src/lab/fabric/observe.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture apply. Parent alone owns orchestration, loop and goal state. +- Goal: isolate producer outcome validation, with every original public export and behavior preserved. +- Non-goals: no behavior fixes, new validation, renamed symbols, signature changes, new dependencies, public API expansion, core activation changes, releases or merges. This document plans implementation; this drafting task changes no source and runs no tests. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; full tests only on `ssh lidge`, never locally. +- Stop: independent layer-tip verification and green exact-head CI evidence recorded, with the layer PR open; do not merge. Stop before implementation if a stated escalation is unresolved. +- Escalation: source drift, unexpected oracle coupling, new cycle, public export loss, changed state lifetime, any scope expansion, or the size-budget conflict below goes to the parent. Do not add a sixth stack layer or edit 002 here. +- Basis: docs HEAD `4cc219549`; verified source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source anchors in this document refer to that revision. `git show origin/dev:src/lab/fabric/observe.ts` matches the working file byte-for-byte. +- Prior audited seam: `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md:558`. Read together with 000, 001, 002; actual consumer/oracle evidence below supersedes the approximate basename-based counts in 001. + +Structural decision before implementation: Current: fabric/index.ts:79 is the production public re-export; the direct test performs dynamic import at lab-fabric-persistence-boundary.test.ts:5. observe currently depends on artifacts, events, ledger, paths, manifest, constants and types (1–34). Chosen: move only closed outcome parsing and its data tables. Rejected: moving persistence along with validation would expose the authority-free helper or couple validation back to storage. Existing public types and persistence functions stay in the residual boundary. Blast radius is the Lab fabric feature; no new public API. + +## Symbol inventory + +Measured by `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration' --json=compact src/lab/fabric/observe.ts`, matched to column-zero declarations in the pinned source. Nested declarations are excluded. Ranges include declaration syntax through its closing line, not preceding comments. + +Consumers are distinct direct import/re-export files across `src gui/src scripts tests`, found with `rg -l` path/symbol searches and verified against the actual import binding. A wildcard re-export counts once for every public symbol; a dynamic namespace import counts for runtime exports, not erased types. Private declarations have zero external consumers, even if unrelated same-named declarations occur elsewhere. Transitive barrel clients are covered by the Lab domain gate, not double-counted. Total direct module consumers: **2**. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| PersistFabricOptions | interface | 39–45 | yes | 1 | src/lab/fabric/observe.ts | +| PersistedFabricObservation | interface | 48–51 | yes | 1 | src/lab/fabric/observe.ts | +| OUTCOME_KEYS | const | 53–73 | no | 0 | src/lab/fabric/outcome-validation.ts | +| VERIFIER_KEYS | const | 75–75 | no | 0 | src/lab/fabric/outcome-validation.ts | +| PATH_SUMMARY_KEYS | const | 76–76 | no | 0 | src/lab/fabric/outcome-validation.ts | +| PATH_SUMMARY_KINDS | const | 77–77 | no | 0 | src/lab/fabric/outcome-validation.ts | +| USAGE_KEYS | const | 78–78 | no | 0 | src/lab/fabric/outcome-validation.ts | +| LIMIT_KEYS | const | 79–79 | no | 0 | src/lab/fabric/outcome-validation.ts | +| FAILURE_KEYS | const | 80–80 | no | 0 | src/lab/fabric/outcome-validation.ts | +| FAILURE_ATTRIBUTIONS | const | 81–81 | no | 0 | src/lab/fabric/outcome-validation.ts | +| assertPlainObject | function | 84–89 | no | 0 | src/lab/fabric/outcome-validation.ts | +| assertStringField | function | 92–98 | no | 0 | src/lab/fabric/outcome-validation.ts | +| assertIntegerField | function | 101–107 | no | 0 | src/lab/fabric/outcome-validation.ts | +| assertNonNegativeIntegerField | function | 110–116 | no | 0 | src/lab/fabric/outcome-validation.ts | +| wrapValidationError | function | 119–124 | no | 0 | src/lab/fabric/outcome-validation.ts | +| validateFabricVerifier | function | 127–171 | no | 0 | src/lab/fabric/outcome-validation.ts | +| validateFabricUsage | function | 174–183 | no | 0 | src/lab/fabric/outcome-validation.ts | +| validateFabricLimits | function | 186–195 | no | 0 | src/lab/fabric/outcome-validation.ts | +| validateFailureRecord | function | 198–216 | no | 0 | src/lab/fabric/outcome-validation.ts | +| routeSubjectsMatch | function | 219–221 | no | 0 | src/lab/fabric/outcome-validation.ts | +| sanitizedVerifierSummary | function | 224–247 | no | 0 | src/lab/fabric/observe.ts | +| assertFabricOutcomeV1 | function | 250–347 | yes | 2 | src/lab/fabric/outcome-validation.ts | +| observationFromFabricOutcome | function | 350–455 | yes | 2 | src/lab/fabric/observe.ts | +| persistFabricOutcome | function | 458–474 | no | 0 | src/lab/fabric/observe.ts | +| persistFabricRunResult | function | 477–489 | yes | 2 | src/lab/fabric/observe.ts | + +Direct edge evidence (including public re-exports): + +- `src/lab/fabric/index.ts:75` — assertFabricOutcomeV1, observationFromFabricOutcome, persistFabricRunResult. +- `src/lab/fabric/index.ts:80` — PersistFabricOptions, PersistedFabricObservation. +- `tests/lab/lab-fabric-persistence-boundary.test.ts:5` — *dynamic*. + +Import declarations are not new owners: their exact leaf/residual binding allocations are given below. No default export exists. + +## Leaf partition + +Reuse the existing same-directory sibling convention: `events/limits.ts`, `events/errors.ts`, `ledger/artifact-refs.ts`, `artifacts/secure-fs.ts`, `fabric/producer-protocol.ts`. The five source directories and proposed names were inspected with `rg --files`; none of the new paths exists at the pinned source. No new index/barrel, generic utils module, package or directory is needed. The original paths are compatibility boundaries explicitly retained by the split-train contract, not new internal convenience barrels. + +Move complete source slices with their inline/leading comments as listed; only add the listed imports, named re-exports and leaf-local export modifiers needed by other leaves/the residual. Never re-export formerly private implementation helpers from the original public path. + +### src/lab/fabric/outcome-validation.ts + +- Original slices: `src/lab/fabric/observe.ts:53–221`, `src/lab/fabric/observe.ts:249–347`. +- Symbols: `OUTCOME_KEYS`, `VERIFIER_KEYS`, `PATH_SUMMARY_KEYS`, `PATH_SUMMARY_KINDS`, `USAGE_KEYS`, `LIMIT_KEYS`, `FAILURE_KEYS`, `FAILURE_ATTRIBUTIONS`, `assertPlainObject`, `assertStringField`, `assertIntegerField`, `assertNonNegativeIntegerField`, `wrapValidationError`, `validateFabricVerifier`, `validateFabricUsage`, `validateFabricLimits`, `validateFailureRecord`, `routeSubjectsMatch`, `assertFabricOutcomeV1`. +- Expected lines: **279** = 268 moved lines + 10 import/header-separator lines + 1 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: none; preserve existing exported declaration modifiers. +- Own imports: + +```ts +import { OUTCOMES } from "../constants"; +import { FAILURE_CLASSIFICATIONS } from "../conformance/types"; +import { isSha256Hex, jcsStringify, subjectIdForSubject } from "../digest"; +import type { RouteSubjectV1, TaskSubjectV1 } from "../events/types"; +import { LabValidationError } from "../events/errors"; +import { validateSubject } from "../events/validate"; +import { FABRIC_LIMITS, FABRIC_VERIFIER_ID } from "./constants"; +import type { FabricLimitsV1, FabricTaskOutcomeV1 } from "./types"; +import { FabricTaskError } from "./types"; +``` + +Residual `src/lab/fabric/observe.ts`: **201 expected lines**. Retained declarations: `PersistFabricOptions`, `PersistedFabricObservation`, `sanitizedVerifierSummary`, `observationFromFabricOutcome`, `persistFabricOutcome`, `persistFabricRunResult`. + +Line accounting: 489 logical source lines − 268 moved lines − 34 original import/header lines + 14 explicit import/re-export lines = 201. Keep formatting compact as shown; extra formatting lines must still fit the 400-line gate. No residual exceeds 400; no #b layer is required for file size. + +Changeset accounting: 268 original lines move; raw additions+deletions for the move alone are 536, before import glue. **Parent decision required:** this exceeds the ≤500 changed-source-line/default PR limit if measured as raw Git additions+deletions. The fixed five-layer S15 map does not allocate a #b for this file. Do not claim this layer satisfies that limit. Parent must explicitly accept a pure-move size exception (with moved-line review evidence) or revise the train topology before code execution. This document does not authorize either change. + +## Re-export block + +Exact named re-exports to add/retain at the original path: + +```ts +export { assertFabricOutcomeV1 } from "./outcome-validation"; +``` + +PersistFabricOptions, PersistedFabricObservation, observationFromFabricOutcome and persistFabricRunResult remain exported declarations. No public type is moved. + +Explicit local imports for the residual (replace the original import block); re-export statements bind nothing locally: + +```ts +import { createArtifactStore, type ArtifactStore } from "../artifacts/store"; +import { sanitizeDiagnostic, truncateUtf8 } from "../artifacts/sanitize"; +import { LAB_EVENT_SCHEMA_VERSION, LAB_PRODUCER, LAB_PRODUCER_VERSION, OBSERVATION_LIMIT_NAMES } from "../constants"; +import { fixtureDigest } from "../digest"; +import type { ObservationEvent, RouteSubjectV1, TaskSubjectV1 } from "../events/types"; +import { assignEventId } from "../events/validate"; +import { withLedgerMutation } from "../ledger/store"; +import { ensureLabDirs } from "../paths"; +import { FABRIC_EVIDENCE_LAYER, FABRIC_SCENARIO_ID, FABRIC_SCENARIO_VERSION, FABRIC_SUITE_ID, FABRIC_SUITE_VERSION } from "./constants"; +import { expandFabricScenario, expandFabricSuiteManifest, fabricScenarioManifestDigest, fabricSuiteManifestDigest, loadFabricCaseAuthority } from "./manifest"; +import type { FabricTaskOutcomeV1, FabricTaskRunResult } from "./types"; +import { FabricTaskError } from "./types"; +import { assertFabricOutcomeV1 } from "./outcome-validation"; +``` + + + +## Module-level state and cycles + +Move all six top-level Sets together to outcome-validation.ts: OUTCOME_KEYS (53–73), VERIFIER_KEYS (75), PATH_SUMMARY_KEYS (76), PATH_SUMMARY_KINDS (77), FAILURE_KEYS (80), FAILURE_ATTRIBUTIONS (81). USAGE_KEYS (78) and derived LIMIT_KEYS (79) follow their validators; evaluate LIMIT_KEYS once exactly as before. There is no module let, Map, WeakMap or live lock. +New direction: observe → outcome-validation → events/validate/constants/digest/types. The leaf never imports observe, the fabric index, artifact storage or ledger storage. PersistFabricOptions/PersistedFabricObservation remain in observe, and the leaf does not need either type, avoiding even a type-only cycle. sanitizedVerifierSummary (224–247), observationFromFabricOutcome (350–455), private persistFabricOutcome (458–474), and persistFabricRunResult (477–489) stay together. The authority-free persistence helper MUST remain module-private; this layer must not expose it through any leaf or public barrel. Store ownership and finally-close ordering remain unchanged. + +Existing lane evidence found no cycle through this file. Recheck the concrete resolved graph at implementation tip, including type-only edges; typecheck alone does not prove acyclicity. This plan introduces only the directed edges above. Do not change protected core roots, turn startServer async, or add activation imports into them. + +## Tests + +Direct import/dynamic-import test `rg -l` list, all **unchanged** at their original import path: + +- `tests/lab/lab-fabric-persistence-boundary.test.ts` — unchanged (import at 5). + +Discovery commands (run across all tests, not just tests/lab): + +```sh +rg -l 'src/lab/fabric/observe' tests --glob '*.ts' +rg -n 'src/lab/fabric/observe|observe\.ts' tests --glob '*.ts' +rg -n 'readFileSync|Bun\.file|readFile\(|source\(' tests --glob '*.ts' +``` + +Dedicated source-text readers of this file: **none found**. No retarget-to-leaf or add-leaf-to-scan-list is required for a dedicated source oracle. +The generic `tests/lab/core-lab-boundary.test.ts` reads traversed source at **69**, protected roots at **278/336**, and the server composition source at **355**. It reports the first edge into Lab before traversing that target, so these Lab leaves are not dedicated source-text inputs on a successful run. Disposition: **unchanged**, no scan-list addition, never edit `PROTECTED` (20–28). Include its existing negative-fixture cases in the implementation gate. + +Additional transitive-barrel/behavioral coverage: `tests/lab/lab-fabric-outcome-validation.test.ts` — unchanged; `tests/lab/lab-fabric-task.test.ts` — unchanged; `tests/lab/lab-ledger-mutation-lock.test.ts` — unchanged. Run `tests/lab` for all indirect callers. + +Guards to drive red once during implementation (temporary mutations must be restored before committing): + +Drive `tests/lab/lab-fabric-persistence-boundary.test.ts:4` red once by a temporary export of persistFabricOutcome, restore it, then confirm the public boundary has no such export. Drive nested-field rejection in `tests/lab/lab-fabric-outcome-validation.test.ts:78` red once by a temporary moved-validator bypass; restore. Retain canonical acceptance (73), timestamps (99), identity contradictions (107), and trusted-versus-harness persistence tests at `tests/lab/lab-fabric-task.test.ts:483,500`. + +No tests or red mutations were run while drafting this plan; these are executor obligations. + +## Verification + +Instantiate `002_layer_map.md` Per-layer gate in the dedicated layer worktree, not this docs worktree: + +```sh +bun run typecheck +bun test tests/lab/lab-fabric-persistence-boundary.test.ts tests/lab/lab-fabric-outcome-validation.test.ts tests/lab/lab-fabric-task.test.ts tests/lab/lab-ledger-mutation-lock.test.ts +bun test tests/lab +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/lab/fabric/outcome-validation.ts src/lab/fabric/observe.ts +rg -n 'lab/fabric/observe|from "./observe"' src gui/src scripts tests +git diff --check +git diff --numstat codex/split-lab-artifacts-sanitize...HEAD +# Full repository suite: remote only, exact branch tip; pipefail preserves failures. +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-fabric-observe && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test 2>&1 | tail -15"' +``` + +Required outcome: all local gates exit 0; focused/domain tests have zero failures; every leaf and residual ≤400. The boundary test is included explicitly even though no protected source is edited. Confirm the remote printed SHA equals the layer tip and save the full exit status plus test totals; the tail alone is not proof. Full suite remains remote-only. + +Compare resolved direct consumer bindings against the 2-file baseline above (raw basename grep is only a candidate search and can include unrelated modules). Leaf names matching the search are not new original-path consumers. Existing public callers must not need migration. Use the already available parser/import-graph mechanism, or a read-only resolver, to report no cycles containing this residual or any new leaf, including type edges; do not install a new analyzer just for this split. Verify moved declaration bodies are identical to origin/dev after stripping only the newly required export modifiers, and inspect `git diff --color-moved` for accidental behavior edits. + +For PR readiness, record exact-head CI (Linux, macOS, Windows) and review status separately from local checks. No tests, typecheck, privacy scan or remote suite have been executed in this docs-only delegation. + +## Accept criteria + +1. Exactly this layer's original source plus the listed 1 new leaves and necessary existing-test adjustments are changed at implementation time; no other S15 file is implemented in this PR. +2. The complete inventory above has exactly one implementation/type owner per declaration; all original public names resolve from `src/lab/fabric/observe.ts`, with no newly public private helper. +3. Every moved body, constant initializer, comment-backed order and signature matches the pinned source; only import/export plumbing changes. +4. Leaf line counts are 279 for `src/lab/fabric/outcome-validation.ts` (or verified formatted equivalents ≤400); residual is approximately 201, always ≤400. No deferred >400 residual. +5. State owners and operation lifetimes match the state section; resolved import graph has no cycle involving the partition. +6. Direct test imports and all source-oracle dispositions are applied exactly as listed; named guards have recorded red→restored-green evidence, without weakening assertions or editing protected roots. +7. Every instantiated local gate and exact-tip remote suite succeeds; source/consumer inventory and privacy scan are recorded. No repository-wide local suite. +8. The parent has explicitly resolved the raw-diff size exception/topology escalation before source implementation. +9. PR base is `codex/split-lab-artifacts-sanitize`, stack map contains all five layers, and exact-head CI is green. No merge is performed. + +## PR + +Title: `refactor(lab-fabric): isolate producer outcome validation (split S15 L4/5)` + +Branch: `codex/split-lab-fabric-observe`. Base: `codex/split-lab-artifacts-sanitize`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Include this full DEV-STACK-03 map; placeholder PR numbers are intentional until the parent creates the PRs. Review only this layer's diff against its base; L4 is the current layer. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| L1/5 | #TBD-S15-L1 | codex/split-lab-events-validate | dev | separate field subject and claim validators | +| L2/5 | #TBD-S15-L2 | codex/split-lab-ledger-store | codex/split-lab-events-validate | isolate ledger lock ownership | +| L3/5 | #TBD-S15-L3 | codex/split-lab-artifacts-sanitize | dev | separate lexical redaction and UTF-8 truncation | +| L4/5 | #TBD-S15-L4 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | isolate producer outcome validation | +| L5/5 | #TBD-S15-L5 | codex/split-lab-fabric-scratch | dev | separate scratch access from fixture lifetime | + +Depends on #TBD-S15-L3. A change to the real parent `codex/split-lab-artifacts-sanitize` requires parent-managed cascade of this layer and fresh exact-head verification. Bottom-up integration applies only to this dependency chain; no merge authorization is conveyed by the plan. The current delegated task performs no Git mutation or PR action. diff --git a/devlog/_plan/260905_now_split_train/520_lab_fabric_scratch.md b/devlog/_plan/260905_now_split_train/520_lab_fabric_scratch.md new file mode 100644 index 0000000000..69f33194ae --- /dev/null +++ b/devlog/_plan/260905_now_split_train/520_lab_fabric_scratch.md @@ -0,0 +1,221 @@ +# 520 — S15 L5/5: src/lab/fabric/scratch.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only bounded delegation. cxc-dev §1/§5 and cxc-dev-architecture apply. Parent alone owns orchestration, loop and goal state. +- Goal: separate scratch access from fixture lifetime, with every original public export and behavior preserved. +- Non-goals: no behavior fixes, new validation, renamed symbols, signature changes, new dependencies, public API expansion, core activation changes, releases or merges. This document plans implementation; this drafting task changes no source and runs no tests. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; full tests only on `ssh lidge`, never locally. +- Stop: independent layer-tip verification and green exact-head CI evidence recorded, with the layer PR open; do not merge. Stop before implementation if a stated escalation is unresolved. +- Escalation: source drift, unexpected oracle coupling, new cycle, public export loss, changed state lifetime, any scope expansion, or the size-budget conflict below goes to the parent. Do not add a sixth stack layer or edit 002 here. +- Basis: docs HEAD `4cc219549`; verified source `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source anchors in this document refer to that revision. `git show origin/dev:src/lab/fabric/scratch.ts` matches the working file byte-for-byte. +- Prior audited seam: `devlog/_plan/260905_modular_debt_ledger/016_lane_cli_storage_usage_update_lab_scripts.md:739`. Read together with 000, 001, 002; actual consumer/oracle evidence below supersedes the approximate basename-based counts in 001. + +Structural decision before implementation: Current: patch.ts:2, verifier.ts, executor.ts and fabric/index.ts:50 consume scratch; dependencies are node fs/path/crypto, paths, fabric constants and FabricTaskError (1–19). Chosen: extract the complete trusted path/descriptor-access subsystem and fixture lifetime into two siblings, retain walk/read/write APIs and user-repo exclusion. Rejected: fixture-only extraction back-imports private access helpers and creates a cycle; replacing the scratch capability design is outside a pure-move layer. The exported interface remains available at the same path with no signature change. + +## Symbol inventory + +Measured by `sg run --lang ts --kind 'function_declaration,interface_declaration,type_alias_declaration,lexical_declaration' --json=compact src/lab/fabric/scratch.ts`, matched to column-zero declarations in the pinned source. Nested declarations are excluded. Ranges include declaration syntax through its closing line, not preceding comments. + +Consumers are distinct direct import/re-export files across `src gui/src scripts tests`, found with `rg -l` path/symbol searches and verified against the actual import binding. A wildcard re-export counts once for every public symbol; a dynamic namespace import counts for runtime exports, not erased types. Private declarations have zero external consumers, even if unrelated same-named declarations occur elsewhere. Transitive barrel clients are covered by the Lab domain gate, not double-counted. Total direct module consumers: **5**. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| O_DIRECTORY | const | 23–23 | no | 0 | src/lab/fabric/scratch-access.ts | +| O_NOFOLLOW | const | 24–24 | no | 0 | src/lab/fabric/scratch-access.ts | +| FILE_MODE | const | 25–25 | no | 0 | src/lab/fabric/scratch-access.ts | +| TrustedScratchDir | interface | 27–31 | no | 0 | src/lab/fabric/scratch-access.ts | +| assertRegularFile | function | 34–38 | no | 0 | src/lab/fabric/scratch-access.ts | +| assertRealDirectory | function | 41–45 | no | 0 | src/lab/fabric/scratch-access.ts | +| identityOf | function | 48–50 | no | 0 | src/lab/fabric/scratch-access.ts | +| platformSupportsNoFollow | function | 53–55 | no | 0 | src/lab/fabric/scratch-access.ts | +| openFlags | function | 58–61 | no | 0 | src/lab/fabric/scratch-access.ts | +| assertScratchName | function | 64–68 | no | 0 | src/lab/fabric/scratch-access.ts | +| revalidateScratchDir | function | 71–77 | no | 0 | src/lab/fabric/scratch-access.ts | +| childScratchPath | function | 80–84 | no | 0 | src/lab/fabric/scratch-access.ts | +| openAtScratch | function | 87–103 | no | 0 | src/lab/fabric/scratch-access.ts | +| openTrustedScratchRoot | function | 106–117 | no | 0 | src/lab/fabric/scratch-access.ts | +| closeTrustedScratchRoot | function | 120–126 | no | 0 | src/lab/fabric/scratch-access.ts | +| openScratchRelativePath | function | 129–165 | no | 0 | src/lab/fabric/scratch-access.ts | +| readAllFromFd | function | 168–180 | no | 0 | src/lab/fabric/scratch-access.ts | +| assertSafeRelativePosixPath | function | 183–201 | yes | 2 | src/lab/fabric/scratch-access.ts | +| assertUnderScratchRoot | function | 204–208 | no | 0 | src/lab/fabric/scratch-access.ts | +| resolveInsideScratch | function | 214–240 | yes | 1 | src/lab/fabric/scratch-access.ts | +| ensureScratchRelativeDir | function | 243–267 | no | 0 | src/lab/fabric/scratch-access.ts | +| ScratchTree | interface | 270–273 | yes | 2 | src/lab/fabric/scratch-fixture.ts | +| createSyntheticScratch | function | 276–323 | yes | 2 | src/lab/fabric/scratch-fixture.ts | +| WalkedFile | interface | 326–330 | yes | 1 | src/lab/fabric/scratch.ts | +| walkScratchFiles | function | 333–376 | yes | 1 | src/lab/fabric/scratch.ts | +| readScratchFileUtf8 | function | 379–397 | yes | 2 | src/lab/fabric/scratch.ts | +| writeScratchFileUtf8 | function | 400–430 | yes | 2 | src/lab/fabric/scratch.ts | +| assertNotUnderUserRepo | function | 433–439 | yes | 2 | src/lab/fabric/scratch.ts | + +Direct edge evidence (including public re-exports): + +- `src/lab/fabric/patch.ts:2` — assertSafeRelativePosixPath, writeScratchFileUtf8. +- `src/lab/fabric/index.ts:45` — assertSafeRelativePosixPath, resolveInsideScratch, createSyntheticScratch, assertNotUnderUserRepo. +- `src/lab/fabric/index.ts:51` — ScratchTree, WalkedFile. +- `src/lab/fabric/verifier.ts:9` — readScratchFileUtf8, walkScratchFiles. +- `src/lab/fabric/executor.ts:19` — assertNotUnderUserRepo, createSyntheticScratch, ScratchTree. +- `tests/lab/lab-fabric-task.test.ts:49` — writeScratchFileUtf8, readScratchFileUtf8. + +Import declarations are not new owners: their exact leaf/residual binding allocations are given below. No default export exists. + +## Leaf partition + +Reuse the existing same-directory sibling convention: `events/limits.ts`, `events/errors.ts`, `ledger/artifact-refs.ts`, `artifacts/secure-fs.ts`, `fabric/producer-protocol.ts`. The five source directories and proposed names were inspected with `rg --files`; none of the new paths exists at the pinned source. No new index/barrel, generic utils module, package or directory is needed. The original paths are compatibility boundaries explicitly retained by the split-train contract, not new internal convenience barrels. + +Move complete source slices with their inline/leading comments as listed; only add the listed imports, named re-exports and leaf-local export modifiers needed by other leaves/the residual. Never re-export formerly private implementation helpers from the original public path. + +### src/lab/fabric/scratch-access.ts + +- Original slices: `src/lab/fabric/scratch.ts:23–267`. +- Symbols: `O_DIRECTORY`, `O_NOFOLLOW`, `FILE_MODE`, `TrustedScratchDir`, `assertRegularFile`, `assertRealDirectory`, `identityOf`, `platformSupportsNoFollow`, `openFlags`, `assertScratchName`, `revalidateScratchDir`, `childScratchPath`, `openAtScratch`, `openTrustedScratchRoot`, `closeTrustedScratchRoot`, `openScratchRelativePath`, `readAllFromFd`, `assertSafeRelativePosixPath`, `assertUnderScratchRoot`, `resolveInsideScratch`, `ensureScratchRelativeDir`. +- Expected lines: **249** = 245 moved lines + 4 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: `FILE_MODE`, `TrustedScratchDir`, `assertRegularFile`, `assertRealDirectory`, `openFlags`, `openTrustedScratchRoot`, `closeTrustedScratchRoot`, `openScratchRelativePath`, `readAllFromFd`, `ensureScratchRelativeDir`. +- Own imports: + +```ts +import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, type Stats } from "node:fs"; +import { join, posix, resolve, sep } from "node:path"; +import { FabricTaskError } from "./types"; +``` + +### src/lab/fabric/scratch-fixture.ts + +- Original slices: `src/lab/fabric/scratch.ts:269–323`. +- Symbols: `ScratchTree`, `createSyntheticScratch`. +- Expected lines: **63** = 55 moved lines + 8 import/header-separator lines + 0 inter-slice separators; ≤400. +- Additional leaf-only exports for existing cross-partition calls: none; preserve existing exported declaration modifiers. +- Own imports: + +```ts +import { closeSync, constants as fsConstants, rmSync, writeSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { randomBytes } from "node:crypto"; +import { ensureLabDirs, ensureRestrictedDir, labRoot, labScratchDir } from "../paths"; +import { FABRIC_LIMITS, SYNTHETIC_BEFORE_UTF8, SYNTHETIC_VALUE_PATH } from "./constants"; +import { FabricTaskError } from "./types"; +import { FILE_MODE, type TrustedScratchDir, openFlags, openTrustedScratchRoot, closeTrustedScratchRoot, openScratchRelativePath } from "./scratch-access"; +``` + +Residual `src/lab/fabric/scratch.ts`: **128 expected lines**. Retained declarations: `WalkedFile`, `walkScratchFiles`, `readScratchFileUtf8`, `writeScratchFileUtf8`, `assertNotUnderUserRepo`. + +Line accounting: 439 logical source lines − 300 moved lines − 19 original import/header lines + 8 explicit import/re-export lines = 128. Keep formatting compact as shown; extra formatting lines must still fit the 400-line gate. No residual exceeds 400; no #b layer is required for file size. + +Changeset accounting: 300 original lines move; raw additions+deletions for the move alone are 600, before import glue. **Parent decision required:** this exceeds the ≤500 changed-source-line/default PR limit if measured as raw Git additions+deletions. The fixed five-layer S15 map does not allocate a #b for this file. Do not claim this layer satisfies that limit. Parent must explicitly accept a pure-move size exception (with moved-line review evidence) or revise the train topology before code execution. This document does not authorize either change. + +## Re-export block + +Exact named re-exports to add/retain at the original path: + +```ts +export { assertSafeRelativePosixPath, resolveInsideScratch } from "./scratch-access"; +export { createSyntheticScratch } from "./scratch-fixture"; +export type { ScratchTree } from "./scratch-fixture"; +``` + +WalkedFile, walkScratchFiles, readScratchFileUtf8, writeScratchFileUtf8 and assertNotUnderUserRepo remain exported declarations. + +Explicit local imports for the residual (replace the original import block); re-export statements bind nothing locally: + +```ts +import { closeSync, constants as fsConstants, fstatSync, lstatSync, readdirSync, writeSync } from "node:fs"; +import { join, relative, resolve, sep } from "node:path"; +import { FABRIC_LIMITS } from "./constants"; +import { FabricTaskError } from "./types"; +import { FILE_MODE, assertRegularFile, assertRealDirectory, openFlags, openTrustedScratchRoot, closeTrustedScratchRoot, openScratchRelativePath, readAllFromFd, assertSafeRelativePosixPath, resolveInsideScratch, ensureScratchRelativeDir } from "./scratch-access"; +``` + +The residual does not call createSyntheticScratch or name ScratchTree; its re-exports need no matching local import. + +## Module-level state and cycles + +No top-level let, Map, Set, WeakMap or active lock exists. O_DIRECTORY (23), O_NOFOLLOW (24), FILE_MODE (25) move once to scratch-access.ts; the feature-detected filesystem flag values remain eagerly captured at module evaluation. TrustedScratchDir (27–31) has a single type owner there. File descriptors and intermediateFds (137) remain operation-local, with unchanged close paths. createSyntheticScratch's trusted handle and cleanup capture (282,302–320) stay together in scratch-fixture.ts. No singleton root or new registry is introduced. +New direction: scratch façade → fixture → access → FabricTaskError; scratch façade → access. Access never imports scratch or scratch-fixture. Extracting fixture alone would produce fixture → scratch → fixture through openTrustedScratchRoot/openScratchRelativePath; moving the complete access group removes that cycle. Keep assertSafeRelativePosixPath with openScratchRelativePath so their mutual file-placement dependency cannot point back to the façade. Existing no-follow checks, inode checks, path resolution, synchronous writes and cleanup are moved verbatim, not redesigned. + +Existing lane evidence found no cycle through this file. Recheck the concrete resolved graph at implementation tip, including type-only edges; typecheck alone does not prove acyclicity. This plan introduces only the directed edges above. Do not change protected core roots, turn startServer async, or add activation imports into them. + +## Tests + +Direct import/dynamic-import test `rg -l` list, all **unchanged** at their original import path: + +- `tests/lab/lab-fabric-task.test.ts` — unchanged (import at 49). + +Discovery commands (run across all tests, not just tests/lab): + +```sh +rg -l 'src/lab/fabric/scratch' tests --glob '*.ts' +rg -n 'src/lab/fabric/scratch|scratch\.ts' tests --glob '*.ts' +rg -n 'readFileSync|Bun\.file|readFile\(|source\(' tests --glob '*.ts' +``` + +Dedicated source-text readers of this file: **none found**. No retarget-to-leaf or add-leaf-to-scan-list is required for a dedicated source oracle. +The generic `tests/lab/core-lab-boundary.test.ts` reads traversed source at **69**, protected roots at **278/336**, and the server composition source at **355**. It reports the first edge into Lab before traversing that target, so these Lab leaves are not dedicated source-text inputs on a successful run. Disposition: **unchanged**, no scan-list addition, never edit `PROTECTED` (20–28). Include its existing negative-fixture cases in the implementation gate. + +Additional transitive-barrel/behavioral coverage: `tests/lab/lab-fabric-outcome-validation.test.ts` — unchanged; `tests/lab/lab-fabric-persistence-boundary.test.ts` — unchanged. Run `tests/lab` for all indirect callers. + +Guards to drive red once during implementation (temporary mutations must be restored before committing): + +Drive the existing traversal rejection at `tests/lab/lab-fabric-task.test.ts:551` red once by a temporary bypass of the moved path validator, restore and rerun. Keep special-file (572), intermediate-symlink IO (586), patch-path boundary (812), and user-repository exclusion (856) cases unchanged. For the split specifically, verify the public ScratchTree return shape, fixture contents and cleanup behavior through the existing synthetic-patch tests (379,388). + +No tests or red mutations were run while drafting this plan; these are executor obligations. + +## Verification + +Instantiate `002_layer_map.md` Per-layer gate in the dedicated layer worktree, not this docs worktree: + +```sh +bun run typecheck +bun test tests/lab/lab-fabric-task.test.ts tests/lab/lab-fabric-outcome-validation.test.ts tests/lab/lab-fabric-persistence-boundary.test.ts +bun test tests/lab +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/lab/fabric/scratch-access.ts src/lab/fabric/scratch-fixture.ts src/lab/fabric/scratch.ts +rg -n 'lab/fabric/scratch|from "./scratch"' src gui/src scripts tests +git diff --check +git diff --numstat origin/dev...HEAD +# Full repository suite: remote only, exact branch tip; pipefail preserves failures. +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-fabric-scratch && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test 2>&1 | tail -15"' +``` + +Required outcome: all local gates exit 0; focused/domain tests have zero failures; every leaf and residual ≤400. The boundary test is included explicitly even though no protected source is edited. Confirm the remote printed SHA equals the layer tip and save the full exit status plus test totals; the tail alone is not proof. Full suite remains remote-only. + +Compare resolved direct consumer bindings against the 5-file baseline above (raw basename grep is only a candidate search and can include unrelated modules). Leaf names matching the search are not new original-path consumers. Existing public callers must not need migration. Use the already available parser/import-graph mechanism, or a read-only resolver, to report no cycles containing this residual or any new leaf, including type edges; do not install a new analyzer just for this split. Verify moved declaration bodies are identical to origin/dev after stripping only the newly required export modifiers, and inspect `git diff --color-moved` for accidental behavior edits. + +For PR readiness, record exact-head CI (Linux, macOS, Windows) and review status separately from local checks. No tests, typecheck, privacy scan or remote suite have been executed in this docs-only delegation. + +## Accept criteria + +1. Exactly this layer's original source plus the listed 2 new leaves and necessary existing-test adjustments are changed at implementation time; no other S15 file is implemented in this PR. +2. The complete inventory above has exactly one implementation/type owner per declaration; all original public names resolve from `src/lab/fabric/scratch.ts`, with no newly public private helper. +3. Every moved body, constant initializer, comment-backed order and signature matches the pinned source; only import/export plumbing changes. +4. Leaf line counts are 249 for `src/lab/fabric/scratch-access.ts`, 63 for `src/lab/fabric/scratch-fixture.ts` (or verified formatted equivalents ≤400); residual is approximately 128, always ≤400. No deferred >400 residual. +5. State owners and operation lifetimes match the state section; resolved import graph has no cycle involving the partition. +6. Direct test imports and all source-oracle dispositions are applied exactly as listed; named guards have recorded red→restored-green evidence, without weakening assertions or editing protected roots. +7. Every instantiated local gate and exact-tip remote suite succeeds; source/consumer inventory and privacy scan are recorded. No repository-wide local suite. +8. The parent has explicitly resolved the raw-diff size exception/topology escalation before source implementation. +9. PR base is `codex/split-lab-fabric-observe`, stack map contains all five layers, and exact-head CI is green. No merge is performed. + +## PR + +Title: `refactor(lab-fabric): separate scratch access from fixture lifetime (split S15 L5/5)` + +Branch: `codex/split-lab-fabric-scratch`. Base: `dev`. Closes: **none**. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Include this full DEV-STACK-03 map; placeholder PR numbers are intentional until the parent creates the PRs. Review only this layer's diff against its base; L5 is the current layer. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| L1/5 | #TBD-S15-L1 | codex/split-lab-events-validate | dev | separate field subject and claim validators | +| L2/5 | #TBD-S15-L2 | codex/split-lab-ledger-store | codex/split-lab-events-validate | isolate ledger lock ownership | +| L3/5 | #TBD-S15-L3 | codex/split-lab-artifacts-sanitize | dev | separate lexical redaction and UTF-8 truncation | +| L4/5 | #TBD-S15-L4 | codex/split-lab-fabric-observe | codex/split-lab-artifacts-sanitize | isolate producer outcome validation | +| L5/5 | #TBD-S15-L5 | codex/split-lab-fabric-scratch | dev | separate scratch access from fixture lifetime | + +Base: dev — no dependency on the layers below; no cascade obligation. + +No merge authorization is conveyed by the plan. The current delegated task performs no Git mutation or PR action. diff --git a/devlog/_plan/260905_now_split_train/530_lab_conformance_executor.md b/devlog/_plan/260905_now_split_train/530_lab_conformance_executor.md new file mode 100644 index 0000000000..fa64691635 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/530_lab_conformance_executor.md @@ -0,0 +1,252 @@ +# 530 — S16 L1/5: src/lab/conformance/executor.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Work class: C3 architecture planning, docs-only delegated scope. Parent owns orchestration, loop and goal state; this document executes none of them. +- Goal: split `src/lab/conformance/executor.ts` (741 lines) into the named leaves while preserving all current exports, signatures, object identities and behavior. +- Non-goals: no behavior fixes, public identifier renames, schema changes, new dependencies, import-consumer churn, function-body rewrites, core-root edits, merge, release or deployment. No code/test/git-state mutation in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current planning basis is docs HEAD `4cc219549`, code `origin/dev = 1362b1a38`; `git diff origin/dev -- src/lab/conformance/executor.ts` is empty. All source line anchors below refer to that code basis, not future leaf line numbers. +- Stop: drafting ends after this plan's declaration/export/state/test inventory is checked. Implementation ends only when its independent per-layer gates and exact-head CI evidence are recorded; no merge is authorized by this document. +- Escalation: stop implementation and return to the parent if source drift invalidates the partition, an export/identity changes, an oracle cannot move without weakening, a new cycle appears, any residual/leaf exceeds 400, or the fixed layer scope needs expansion. Do not create an unplanned #b or edit 002 from this task. + +L1 SIZE CONFLICT: the partition below relocates 376 declaration-body lines, so it contributes at least 752 raw additions+deletions before import, comment and whitespace edits. It cannot satisfy a literal ≤500 raw changed-source-lines gate in the fixed five-layer map. Proposed documented DEFAULT exception: review pure moves with --color-moved and judge new logic (zero), while recording raw numstat honestly. Parent must approve that exception before implementation, or authorize another stack/layer and update 002; this delegate does not change topology. All >50-line functions remain unchanged under the pure-move non-goal. + +## Symbol inventory + +Origin/dev declaration spans were enumerated with `sg run --lang ts --kind 'function_declaration,lexical_declaration,interface_declaration,type_alias_declaration,export_statement' --json=compact src/lab/conformance/executor.ts`, keeping column-zero declarations; exported declarations are counted once. Imports are not redeclarations of their source owners: original import block is src/lab/conformance/executor.ts:1–26, and the exact post-split imports appear below. + +Consumer counts mean **direct importing/re-exporting modules**, not occurrences or transitive barrel consumers. Resolved relative import clauses were checked with `rg -q -w `; namespace imports and wildcard re-exports count once for every exported symbol. Non-exported declarations have zero external consumers. `rg --files src gui/src scripts tests` supplied the search universe. Module fan-in is 14; the mechanically requested basename-only gate returns 35 because it also matches non-conformance executors. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `resolveProtocolExecutionContext` | function | 28–38 | yes | 9 | `executor.ts (residual)` | +| `collectAdapterEvents` | function | 40–44 | no | 0 | `executor-transport.ts` | +| `nonstreamObservationJson` | function | 46–54 | yes | 2 | `executor-transport.ts` | +| `collectBridgeSse` | function | 56–82 | no | 0 | `executor-transport.ts` | +| `parseUpstreamSse` | function | 84–92 | no | 0 | `executor-transport.ts` | +| `parsedFromContext` | function | 94–114 | no | 0 | `executor.ts (residual)` | +| `normalizeTools` | function | 116–123 | no | 0 | `executor-transport.ts` | +| `createHarnessAdapter` | function | 125–131 | no | 0 | `executor-transport.ts` | +| `runBuildRequest` | function | 133–146 | no | 0 | `executor-transport.ts` | +| `executeAdapterVector` | function | 148–216 | no | 0 | `executor.ts (residual)` | +| `runToolRoundTrip` | function | 218–255 | no | 0 | `executor-tools.ts` | +| `runCustomToolRoundTrip` | function | 257–293 | no | 0 | `executor-tools.ts` | +| `runToolResultContent` | function | 295–314 | no | 0 | `executor-tools.ts` | +| `normalizeImageToolResultUpstream` | function | 316–336 | no | 0 | `executor-tools.ts` | +| `runApplyPatchTurn` | function | 338–367 | no | 0 | `executor-tools.ts` | +| `runCodexToolContinuation` | function | 369–386 | no | 0 | `executor-tools.ts` | +| `runPreviousResponseReplay` | function | 388–421 | no | 0 | `executor-reasoning.ts` | +| `runReasoningEffortMapping` | function | 423–442 | no | 0 | `executor-reasoning.ts` | +| `runReasoningReplay` | function | 444–494 | no | 0 | `executor-reasoning.ts` | +| `runReasoningPrivateIsolation` | function | 496–523 | no | 0 | `executor-reasoning.ts` | +| `executeClientRequest` | function | 525–536 | no | 0 | `executor.ts (residual)` | +| `recordInitiatingRequest` | function | 538–547 | no | 0 | `executor.ts (residual)` | +| `executeStreamScenario` | function | 549–651 | no | 0 | `executor.ts (residual)` | +| `executeScenario` | function | 653–674 | yes | 1 | `executor.ts (residual)` | +| `runScenario` | function | 676–741 | yes | 8 | `executor.ts (residual)` | + +Direct production consumers / public boundaries, all preserved: + +- `src/lab/conformance/runner.ts:2`. +- `src/lab/conformance/index.ts:4`. +- `src/lab/automation/dispatch.ts:3`. +- `src/lab/automation/planner.ts:5`. +- `src/lab/observe/from-conformance.ts:31`. + +## Leaf partition + +Structural decision: Keep role dispatch, client/stream execution and result assembly in the original entry. Move the shared transport primitives before the two vector families: executor → tools/reasoning → transport; executor also imports transport directly. Existing observation.ts and harness-budget.ts remain canonical. Reject a single vector-family leaf that imports runBuildRequest or collectBridgeSse back from executor: that creates a facade cycle. Reject a generic helpers.ts and adapting the production adapters, neither is needed for this pure move. + +Sibling convention evidence: `src/lab/conformance/fixture-provider.ts`, `harness-budget.ts`, `sse-normalize.ts` and `observation.ts` already use concern-named siblings; no new index barrel. + +The existing lane-016 inventory replaces an extra map command. Search evidence: `rg --files src/lab/conformance`, exact symbol searches and the direct-consumer inventory above; existing owners are reused, not copied. Doing nothing leaves the approved file-size debt; deletion/configuration would change behavior. Blast radius: local Lab feature plus unchanged entry-path consumers. + +Expected counts below are an in-memory plan calculation: original complete declaration bodies and attached comments, the imports shown here, named re-exports, and one blank line between declarations. They are not a claim of executed source changes. Formatting may change the exact number; implementation must run wc and still stay ≤400. Private declarations listed in each leaf's “leaf exports” gain only the internal import seam; they are **not** added to the original public export surface. + +### `src/lab/conformance/executor-transport.ts` — expected 95 lines + +Symbols: `collectAdapterEvents`, `nonstreamObservationJson`, `collectBridgeSse`, `parseUpstreamSse`, `normalizeTools`, `createHarnessAdapter`, `runBuildRequest`. + +Leaf exports: `nonstreamObservationJson`, `collectBridgeSse`, `parseUpstreamSse`, `normalizeTools`, `runBuildRequest`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { createOpenAIChatAdapter } from "../../adapters/openai-chat"; +import { createResponsesPassthroughAdapter } from "../../adapters/openai-responses"; +import { bridgeToResponsesSSE, buildResponseJSON } from "../../bridge"; +import { createTranslatorBudget } from "../../lib/translator-budget"; +import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types"; +import { withHarnessTranslatorBudget } from "./harness-budget"; +import { recordUpstreamRequest } from "./observation"; +import { normalizeSseBytes } from "./sse-normalize"; +import type { NormalizedObservation } from "./types"; +``` + +### `src/lab/conformance/executor-tools.ts` — expected 179 lines + +Symbols: `runToolRoundTrip`, `runCustomToolRoundTrip`, `runToolResultContent`, `normalizeImageToolResultUpstream`, `runApplyPatchTurn`, `runCodexToolContinuation`. + +Leaf exports: `runToolRoundTrip`, `runCustomToolRoundTrip`, `runToolResultContent`, `runApplyPatchTurn`, `runCodexToolContinuation`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { createOpenAIChatAdapter } from "../../adapters/openai-chat"; +import { createResponsesPassthroughAdapter } from "../../adapters/openai-responses"; +import { parseRequest } from "../../responses/parser"; +import type { OcxProviderConfig } from "../../types"; +import { fixtureProviderConfig } from "./fixture-provider"; +import { withHarnessTranslatorBudget } from "./harness-budget"; +import { finalizeObservation, recordUpstreamRequest } from "./observation"; +import type { NormalizedObservation } from "./types"; +import { collectBridgeSse, normalizeTools, parseUpstreamSse } from "./executor-transport"; +``` + +### `src/lab/conformance/executor-reasoning.ts` — expected 146 lines + +Symbols: `runPreviousResponseReplay`, `runReasoningEffortMapping`, `runReasoningReplay`, `runReasoningPrivateIsolation`. + +Leaf exports: `runPreviousResponseReplay`, `runReasoningEffortMapping`, `runReasoningReplay`, `runReasoningPrivateIsolation`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { createResponsesPassthroughAdapter } from "../../adapters/openai-responses"; +import { parseRequest } from "../../responses/parser"; +import { clearResponseStateForTests, expandPreviousResponseInput, rememberResponseState } from "../../responses/state"; +import type { OcxProviderConfig } from "../../types"; +import { fixtureProviderConfig } from "./fixture-provider"; +import { withHarnessTranslatorBudget } from "./harness-budget"; +import { recordUpstreamRequest } from "./observation"; +import type { NormalizedObservation } from "./types"; +import { runBuildRequest } from "./executor-transport"; +``` + +### Residual `src/lab/conformance/executor.ts` — expected 342 lines + +Retains: `resolveProtocolExecutionContext`, `parsedFromContext`, `executeAdapterVector`, `executeClientRequest`, `recordInitiatingRequest`, `executeStreamScenario`, `executeScenario`, `runScenario`. + +No #a/#b/#c subdivision: the whole file's assigned work is this layer, and no residual exceeds 400. There is no unnamed later remainder. Upstream imports retained by the residual, in addition to the local imports in the next section: + +```ts +import { createOpenAIChatAdapter } from "../../adapters/openai-chat"; +import { createResponsesPassthroughAdapter } from "../../adapters/openai-responses"; +import { bridgeToResponsesSSE } from "../../bridge"; +import { anthropicToResponsesTranslation } from "../../claude/inbound"; +import { responsesSseToAnthropicSse } from "../../claude/outbound"; +import { createTranslatorBudget } from "../../lib/translator-budget"; +import { parseRequest } from "../../responses/parser"; +import { evaluateAssertions } from "./assertion"; +import { fixtureProviderConfig, upstreamAdapterForProtocol } from "./fixture-provider"; +import { withHarnessTranslatorBudget } from "./harness-budget"; +import { attachMcpVerifiers, executeMcpSyntheticAction } from "./mcp-stub"; +import { attachVerifiers, emptyObservation, finalizeObservation, filterAnthropicEvents } from "./observation"; +import { normalizeSseBytes } from "./sse-normalize"; +import type { CaseRecord, NormalizedObservation, ScenarioRunResult, ProtocolExecutionContextV1 } from "./types"; +``` + +## Re-export block + +Add exactly these compatibility re-exports to `src/lab/conformance/executor.ts`: + +```ts +export { nonstreamObservationJson } from "./executor-transport"; +``` + +Retained exports in the original file: `resolveProtocolExecutionContext`, `executeScenario`, `runScenario`. No wildcard or renamed re-export is introduced. This is preservation of an existing boundary, not a new internal convenience barrel. + +Explicit local imports required by residual call sites (re-exporting binds nothing): + +```ts +import { nonstreamObservationJson, collectBridgeSse, parseUpstreamSse, normalizeTools, runBuildRequest } from "./executor-transport"; +import { runToolRoundTrip, runCustomToolRoundTrip, runToolResultContent, runApplyPatchTurn, runCodexToolContinuation } from "./executor-tools"; +import { runPreviousResponseReplay, runReasoningEffortMapping, runReasoningReplay, runReasoningPrivateIsolation } from "./executor-reasoning"; +``` + +## Module-level state and cycles + +No top-level let/Map/Set/WeakMap/lock exists. The Set at executor.ts:62 is per collectBridgeSse invocation, not a singleton. The global response store is still owned by ../../responses/state; the clear/remember/clear sequences at executor.ts:392–420 and :500–522 move intact to executor-reasoning.ts, including both finally blocks. Adapter and translator-budget disposal stays per-call. Do not initialize adapters, budgets, timers or response-state caches at leaf import time. Cross-leaf calls are functional/sequential coupling; response-store setup/cleanup is existing temporal coupling, not a newly shared owner. + +Lane 016 reported no return path through this file. The proposed edges above preserve that direction; this is a design argument, not a completed implementation cycle scan. During implementation, repeat lane 016 method G (resolved static imports/exports, type-only edges and literal dynamic imports) for each new leaf and the residual, and require no new cycle. Do not “fix” a cycle with lazy imports or duplicate a type/constant. No protected core root, activation timing or optional-Lab registration seam is changed. + +## Tests + +Direct test import inventory, from `rg -l 'src/lab/conformance/executor"' tests` with relative specifiers resolved and hits inspected: + +| test file / import anchor | action | +|---|---| +| `tests/routing/cl01-review-regressions.test.ts:2` | unchanged — keep original import path | +| `tests/lab/lab-evidence-sanitization.test.ts:21` | unchanged — keep original import path | +| `tests/lab/lab-public-surfaces.test.ts:14` | unchanged — keep original import path | +| `tests/lab/lab-read-surfaces.test.ts:21` | unchanged — keep original import path | +| `tests/lab/lab-conformance-harness.test.ts:6` | unchanged — keep original import path | +| `tests/lab/lab-conformance-runner-failures.test.ts:2` | unchanged — keep original import path | +| `tests/lab/lab-evidence-ledger.test.ts:41` | unchanged — keep original import path | +| `tests/lab/lab-public-export-transaction.test.ts:12` | unchanged — keep original import path | +| `tests/lab/lab-ledger-mutation-lock.test.ts:20` | unchanged — keep original import path | + +Additional indirect/guard coverage (all unchanged unless a narrowly described case is added below): + +- `tests/lab/core-lab-boundary.test.ts`. + +Text-oracle inventory: **zero tests read this specific file as source**. Checked `rg -n '(executor\\.ts|persistence\\.ts|community\\.ts|verification\\.ts|verdicts\\.ts)' tests`, qualified source paths and candidate reader bodies. Therefore retarget-to-leaf = none; add-leaf-to-scan-list = none. Behavioral imports stay unchanged; source-reading tests are not weakened into export-existence checks. + +The 001 executor textoracle=1 is a basename false positive: `tests/lib/credential-redirect-guard.test.ts:65` lists **src/web-search/executor.ts**, read with `Bun.file(repoPath(file)).text()` at :71. Leave that test and its scan list unchanged; it does not govern any of these conformance leaves. This confirms lane 016's qualified-path result. + +The generic boundary guard reads graph nodes at `tests/lab/core-lab-boundary.test.ts:69` and its composition root at :355; its PROTECTED list (:20–28) and reader paths are unchanged. It discovers relative graph edges without a new leaf scan list. Never retarget or edit the protected production roots to accommodate this split. + +No text guard needs retargeting. During implementation, drive tests/routing/cl01-review-regressions.test.ts:76 red once by temporarily making nonstreamObservationJson always return fixture JSON in executor-transport.ts; restore immediately. Also preserve the malformed-negative-control assertion at that file:62 and the harness-failure accounting at tests/lab/lab-conformance-runner-failures.test.ts:24. Never replace production work with fixture-only stubs. + +## Verification + +This is the `002_layer_map.md` Per-layer gate instantiated for S16 L1. These are **future implementation commands**, not tests run by this docs-only delegate. Run at this layer's own tip, not the top of the stack. Focused domains: tests/lab and tests/routing/cl01-review-regressions.test.ts. + +```sh +bun run typecheck +bun test tests/routing/cl01-review-regressions.test.ts tests/lab/lab-evidence-sanitization.test.ts tests/lab/lab-public-surfaces.test.ts tests/lab/lab-read-surfaces.test.ts tests/lab/lab-conformance-harness.test.ts tests/lab/lab-conformance-runner-failures.test.ts tests/lab/lab-evidence-ledger.test.ts tests/lab/lab-public-export-transaction.test.ts tests/lab/lab-ledger-mutation-lock.test.ts tests/lab/core-lab-boundary.test.ts +bun test tests/lab +bun run privacy:scan +bun test tests/lab/core-lab-boundary.test.ts +wc -l src/lab/conformance/executor-transport.ts src/lab/conformance/executor-tools.ts src/lab/conformance/executor-reasoning.ts src/lab/conformance/executor.ts +rg -n 'from "[^"]*/executor"' src gui/src scripts tests | wc -l +# Full suite only on the designated remote, never in this local worktree: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-conformance-executor && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused commands overlapping the full lab domain need not be repeated on unchanged code: capture the focused red/green during the move, then domain coverage once at the final tip. Typecheck/privacy must exit 0; tests must report zero failures. The basename-only rg baseline is 35; the resolved exact-module fan-in must remain 14. Leaf names deliberately do not end in /executor, so they do not inflate that gate. Recount against the actual parent if upstream changes. + +The inherited remote pipeline's tail status alone is not proof of a passing Bun process: capture its complete test result and actual test exit status (enable pipefail or retain the status separately) and record the checked-out SHA. Do not treat fetch/checkout as authorization granted to this docs delegate. Parent/executor verifies remote checkout ownership before use. Record a green **complete exact-head CI rollup**, not an empty required-check list. New or modified source-oracle guards, if discovered, must be driven red and restored before claiming green. No test runner is installed for this plan. + +Use `git diff --check`, `git diff --numstat ...HEAD` and move-aware diff inspection to prove only declaration moves/import rewiring. Compare all original exports (including erased types) to the explicit inventory. Re-run the lane-G import graph check, including type edges; a clean typecheck alone does not prove acyclicity. + +## Accept criteria + +1. Every declaration in the inventory has exactly one owner after the split; no duplicated mutable state or constants, and no omitted declaration. +2. All 4 original exported names remain importable from `src/lab/conformance/executor` with the same signatures/identity; the named re-export and local-import blocks above are present exactly where needed. +3. The 3 new leaves have expected counts 95, 179, 146; residual expected 342. Actual `wc -l` is ≤400 for every one. No hidden #b or sixth stack layer is assumed. +4. Existing function bodies, comparison ordering, errors, cleanup/finally behavior, and allocation timing are unchanged apart from export visibility needed by the private leaf seam. No new upward or facade-back import; static/type/dynamic graph has no newly introduced cycle. +5. All direct tests keep original imports; all identified text-oracle dispositions are implemented without weakening. The named deliberate red mutation fails for the intended reason and is fully removed before the final green run. +6. The instantiated local focused/domain, typecheck and privacy gates plus the remote-only full suite pass on the recorded layer SHA, and its complete exact-head CI is green. No local full suite. +7. The PR contains only this layer's pure move and necessary existing-test additions, retains the parent branch base, and includes the full five-layer stack map. The raw-diff-size exception is explicitly approved by the parent before code execution; otherwise this layer is not implementation-ready. + +## PR + +Title: `refactor(lab-conformance): separate scenario transport and vector families (split S16 L1/5)` + +Branch: `codex/split-lab-conformance-executor`. Base: `dev`. Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); include the pure-move thesis, planned/actual counts, gate evidence and this DEV-STACK-03 map. The placeholders below are intentional pre-creation PR numbers, not existing PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S16-L1 | 530 — this PR | `codex/split-lab-conformance-executor` | `dev` | separate scenario transport and vector families | +| 2 | #TBD-S16-L2 | 540 | `codex/split-lab-automation-persistence` | `dev` | isolate the state-file lock owner | +| 3 | #TBD-S16-L3 | 550 | `codex/split-lab-public-community` | `dev` | extract bounded community input validation | +| 4 | #TBD-S16-L4 | 560 | `codex/split-lab-projection-verification` | `dev` | isolate suite artifact parsing | +| 5 | #TBD-S16-L5 | 570 | `codex/split-lab-projection-verdicts` | `codex/split-lab-projection-verification` | separate projection keys and claim reduction | + +Base: dev — no dependency on the layers below; no cascade obligation. Every layer passes independently. Merge remains separately user-authorized; never merge or enable auto-merge as part of this plan. diff --git a/devlog/_plan/260905_now_split_train/540_lab_automation_persistence.md b/devlog/_plan/260905_now_split_train/540_lab_automation_persistence.md new file mode 100644 index 0000000000..044af1d757 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/540_lab_automation_persistence.md @@ -0,0 +1,204 @@ +# 540 — S16 L2/5: src/lab/automation/persistence.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Work class: C3 architecture planning, docs-only delegated scope. Parent owns orchestration, loop and goal state; this document executes none of them. +- Goal: split `src/lab/automation/persistence.ts` (512 lines) into the named leaves while preserving all current exports, signatures, object identities and behavior. +- Non-goals: no behavior fixes, public identifier renames, schema changes, new dependencies, import-consumer churn, function-body rewrites, core-root edits, merge, release or deployment. No code/test/git-state mutation in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current planning basis is docs HEAD `4cc219549`, code `origin/dev = 1362b1a38`; `git diff origin/dev -- src/lab/automation/persistence.ts` is empty. All source line anchors below refer to that code basis, not future leaf line numbers. +- Stop: drafting ends after this plan's declaration/export/state/test inventory is checked. Implementation ends only when its independent per-layer gates and exact-head CI evidence are recorded; no merge is authorized by this document. +- Escalation: stop implementation and return to the parent if source drift invalidates the partition, an export/identity changes, an oracle cannot move without weakening, a new cycle appears, any residual/leaf exceeds 400, or the fixed layer scope needs expansion. Do not create an unplanned #b or edit 002 from this task. + +The lock algorithm, file publication and reclamation are a sensitive boundary: pure move only, with explicit maintainer security review when the implementation PR is prepared. This is not authorization to redesign locking, change timeouts, or consolidate the independent config lock. + +## Symbol inventory + +Origin/dev declaration spans were enumerated with `sg run --lang ts --kind 'function_declaration,lexical_declaration,interface_declaration,type_alias_declaration,export_statement' --json=compact src/lab/automation/persistence.ts`, keeping column-zero declarations; exported declarations are counted once. Imports are not redeclarations of their source owners: original import block is src/lab/automation/persistence.ts:1–28, and the exact post-split imports appear below. + +Consumer counts mean **direct importing/re-exporting modules**, not occurrences or transitive barrel consumers. Resolved relative import clauses were checked with `rg -q -w `; namespace imports and wildcard re-exports count once for every exported symbol. Non-exported declarations have zero external consumers. `rg --files src gui/src scripts tests` supplied the search universe. Module fan-in is 12; the mechanically requested basename-only gate returns 12. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ROUTES_KEYS` | const | 30–30 | no | 0 | `persistence.ts (residual)` | +| `ROUTE_KEYS` | const | 31–31 | no | 0 | `persistence.ts (residual)` | +| `STATE_KEYS` | const | 32–39 | no | 0 | `persistence.ts (residual)` | +| `RUN_KEYS` | const | 40–63 | no | 0 | `persistence.ts (residual)` | +| `RUN_STATES` | const | 64–64 | no | 0 | `persistence.ts (residual)` | +| `TERMINAL_RUN_STATES` | const | 65–65 | no | 0 | `persistence.ts (residual)` | +| `RUN_REASONS` | const | 66–79 | no | 0 | `persistence.ts (residual)` | +| `STATE_LOCK_WAIT_MS` | const | 80–80 | no | 0 | `state-lock.ts` | +| `LOCK_SLEEP` | const | 81–81 | no | 0 | `state-lock.ts` | +| `StateLockMeta` | interface | 83–86 | no | 0 | `state-lock.ts` | +| `assertClosedKeys` | function | 88–97 | no | 0 | `persistence.ts (residual)` | +| `assertBoundedString` | function | 99–109 | no | 0 | `persistence.ts (residual)` | +| `assertNonNegativeInt` | function | 111–121 | no | 0 | `persistence.ts (residual)` | +| `atomicWriteJson` | function | 123–129 | no | 0 | `persistence.ts (residual)` | +| `basename` | function | 131–134 | no | 0 | `persistence.ts (residual)` | +| `readJsonFile` | function | 136–145 | no | 0 | `persistence.ts (residual)` | +| `sleepLockRetry` | function | 147–149 | no | 0 | `state-lock.ts` | +| `stateLockPath` | function | 151–153 | no | 0 | `state-lock.ts` | +| `readStateLockMeta` | function | 155–166 | no | 0 | `state-lock.ts` | +| `pidDefinitelyDead` | function | 168–178 | no | 0 | `state-lock.ts` | +| `releaseStateLock` | function | 180–186 | no | 0 | `state-lock.ts` | +| `reclaimDeadStateLock` | function | 188–201 | no | 0 | `state-lock.ts` | +| `cleanupPrivateLockFile` | function | 203–209 | no | 0 | `state-lock.ts` | +| `acquireStateLock` | function | 211–250 | no | 0 | `state-lock.ts` | +| `loadLabAutomationPolicy` | function | 252–258 | yes | 5 | `persistence.ts (residual)` | +| `saveLabAutomationPolicy` | function | 260–264 | yes | 6 | `persistence.ts (residual)` | +| `normalizeLabAutomationRoutesV1` | function | 266–292 | yes | 4 | `persistence.ts (residual)` | +| `defaultLabAutomationRoutesV1` | function | 294–296 | yes | 2 | `persistence.ts (residual)` | +| `loadLabAutomationRoutes` | function | 298–303 | yes | 3 | `persistence.ts (residual)` | +| `saveLabAutomationRoutes` | function | 305–308 | yes | 5 | `persistence.ts (residual)` | +| `optionalTimestamp` | function | 310–313 | no | 0 | `persistence.ts (residual)` | +| `normalizeRunRecord` | function | 315–404 | no | 0 | `persistence.ts (residual)` | +| `assertStateRunInvariants` | function | 406–416 | no | 0 | `persistence.ts (residual)` | +| `normalizeState` | function | 418–461 | no | 0 | `persistence.ts (residual)` | +| `defaultLabAutomationStateV1` | function | 463–472 | yes | 7 | `persistence.ts (residual)` | +| `loadLabAutomationStateUnlocked` | function | 474–479 | no | 0 | `persistence.ts (residual)` | +| `saveLabAutomationStateUnlocked` | function | 481–484 | no | 0 | `persistence.ts (residual)` | +| `loadLabAutomationState` | function | 486–488 | yes | 9 | `persistence.ts (residual)` | +| `saveLabAutomationState` | function | 490–497 | yes | 7 | `persistence.ts (residual)` | +| `mutateLabAutomationState` | function | 499–512 | yes | 3 | `persistence.ts (residual)` | + +Direct production consumers / public boundaries, all preserved: + +- `src/lab/automation/orchestrator.ts:7`. +- `src/lab/automation/config-persistence.ts:19`. +- `src/lab/automation/index.ts:5`. +- `src/cli/lab.ts:58`. +- `src/server/management/lab-automation-routes.ts:23`. + +## Leaf partition + +Structural decision: Keep schema validation, policy/routes/state persistence and mutation ordering together; extract the state lock as the smallest cohesive leaf that brings persistence below 400. Existing config-persistence.ts:29–35 has a different config-file lock and constants; do not merge these independent lock identities or import config-persistence (which already imports persistence at :19–23). Reject extracting every schema now: that moves more code without being needed for this layer's file limit. No delete/configure alternative removes this structural debt without behavior change. + +Sibling convention evidence: `src/lab/automation/config-persistence.ts`, `run-key.ts`, `route-context.ts` and `runs-query.ts` are concern-named siblings; state-lock.ts names the specific owner rather than generic locking utilities. + +The existing lane-016 inventory replaces an extra map command. Search evidence: `rg --files src/lab/automation`, exact symbol searches and the direct-consumer inventory above; existing owners are reused, not copied. Doing nothing leaves the approved file-size debt; deletion/configuration would change behavior. Blast radius: local Lab feature plus unchanged entry-path consumers. + +Expected counts below are an in-memory plan calculation: original complete declaration bodies and attached comments, the imports shown here, named re-exports, and one blank line between declarations. They are not a claim of executed source changes. Formatting may change the exact number; implementation must run wc and still stay ≤400. Private declarations listed in each leaf's “leaf exports” gain only the internal import seam; they are **not** added to the original public export surface. + +### `src/lab/automation/state-lock.ts` — expected 118 lines + +Symbols: `STATE_LOCK_WAIT_MS`, `LOCK_SLEEP`, `StateLockMeta`, `sleepLockRetry`, `stateLockPath`, `readStateLockMeta`, `pidDefinitelyDead`, `releaseStateLock`, `reclaimDeadStateLock`, `cleanupPrivateLockFile`, `acquireStateLock`. + +Leaf exports: `acquireStateLock`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { randomUUID } from "node:crypto"; +import { closeSync, fsyncSync, linkSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { ensureLabDirs, labAutomationStatePath } from "../paths"; +import { LabAutomationError } from "./types"; +``` + +### Residual `src/lab/automation/persistence.ts` — expected 388 lines + +Retains: `ROUTES_KEYS`, `ROUTE_KEYS`, `STATE_KEYS`, `RUN_KEYS`, `RUN_STATES`, `TERMINAL_RUN_STATES`, `RUN_REASONS`, `assertClosedKeys`, `assertBoundedString`, `assertNonNegativeInt`, `atomicWriteJson`, `basename`, `readJsonFile`, `loadLabAutomationPolicy`, `saveLabAutomationPolicy`, `normalizeLabAutomationRoutesV1`, `defaultLabAutomationRoutesV1`, `loadLabAutomationRoutes`, `saveLabAutomationRoutes`, `optionalTimestamp`, `normalizeRunRecord`, `assertStateRunInvariants`, `normalizeState`, `defaultLabAutomationStateV1`, `loadLabAutomationStateUnlocked`, `saveLabAutomationStateUnlocked`, `loadLabAutomationState`, `saveLabAutomationState`, `mutateLabAutomationState`. + +No #a/#b/#c subdivision: the whole file's assigned work is this layer, and no residual exceeds 400. There is no unnamed later remainder. Upstream imports retained by the residual, in addition to the local imports in the next section: + +```ts +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { renameAtomicFile } from "../../lib/windows-atomic-replace"; +import { ensureLabDirs, labAutomationPolicyPath, labAutomationRoutesPath, labAutomationStatePath } from "../paths"; +import { LAB_AUTOMATION_HARD_MAX } from "./constants"; +import { defaultLabAutomationPolicyV1, normalizeLabAutomationPolicyV1 } from "./policy"; +import type { LabAutomationPolicyV1, LabAutomationRoutesV1, LabAutomationRunRecordV1, LabAutomationStateV1 } from "./types"; +import { LabAutomationError } from "./types"; +``` + +## Re-export block + +The compatibility re-export block is **empty**: this partition moves no currently exported declaration. Keep the existing exported function definitions in the original file. Do not fabricate an `export { acquireStateLock }` or expose any other formerly private leaf helper from the facade. + +Retained exports in the original file: `loadLabAutomationPolicy`, `saveLabAutomationPolicy`, `normalizeLabAutomationRoutesV1`, `defaultLabAutomationRoutesV1`, `loadLabAutomationRoutes`, `saveLabAutomationRoutes`, `defaultLabAutomationStateV1`, `loadLabAutomationState`, `saveLabAutomationState`, `mutateLabAutomationState`. No wildcard or renamed re-export is introduced. This is preservation of an existing boundary, not a new internal convenience barrel. + +Explicit local imports required by residual call sites (re-exporting binds nothing): + +```ts +import { acquireStateLock } from "./state-lock"; +``` + +## Module-level state and cycles + +ROUTES_KEYS (:30), ROUTE_KEYS (:31), STATE_KEYS (:32–39), RUN_KEYS (:40–63), RUN_STATES (:64), TERMINAL_RUN_STATES (:65), RUN_REASONS (:66–79) remain single-owner allowlist Sets in persistence.ts; they are not caches. STATE_LOCK_WAIT_MS (:80), LOCK_SLEEP (:81) and StateLockMeta (:83–86) move only to state-lock.ts. The Int32Array/SharedArrayBuffer and all retry/acquire/reclaim/release logic have one owner. No second lock buffer is retained in the facade. state-lock → paths/types only; it cannot import persistence or config-persistence. File lock ownership is existing temporal coupling; persistence retains acquire → read → mutate → save → finally release at :499–512 and acquire → save → finally release at :490–497. Leave unlocked reads at :486–488 unchanged. + +Lane 016 reported no return path through this file. The proposed edges above preserve that direction; this is a design argument, not a completed implementation cycle scan. During implementation, repeat lane 016 method G (resolved static imports/exports, type-only edges and literal dynamic imports) for each new leaf and the residual, and require no new cycle. Do not “fix” a cycle with lazy imports or duplicate a type/constant. No protected core root, activation timing or optional-Lab registration seam is changed. + +## Tests + +Direct test import inventory, from `rg -l 'src/lab/automation/persistence"' tests` with relative specifiers resolved and hits inspected: + +| test file / import anchor | action | +|---|---| +| `tests/lab/lab-automation.test.ts:9` | unchanged — keep original import path | +| `tests/lab/lab-automation-ingwannu-regressions.test.ts:7` | unchanged — keep original import path | +| `tests/lab/lab-automation-final-coderabbit-regressions.test.ts:12` | unchanged — keep original import path | +| `tests/lab/lab-automation-persisted-cap-regression.test.ts:3` | unchanged — keep original import path | +| `tests/lab/lab-automation-coderabbit-regressions.test.ts:7` | unchanged — keep original import path | +| `tests/lab/lab-automation-management-http.test.ts:8` | unchanged — keep original import path | +| `tests/lab/lab-automation-review-regressions.test.ts:7` | unchanged — keep original import path | + +Text-oracle inventory: **zero tests read this specific file as source**. Checked `rg -n '(executor\\.ts|persistence\\.ts|community\\.ts|verification\\.ts|verdicts\\.ts)' tests`, qualified source paths and candidate reader bodies. Therefore retarget-to-leaf = none; add-leaf-to-scan-list = none. Behavioral imports stay unchanged; source-reading tests are not weakened into export-existence checks. + +The reader at `tests/lab/lab-automation-ingwannu-regressions.test.ts:121` checks a CL-08 plan document's trailing whitespace, not persistence.ts; leave it unchanged. + +The generic boundary guard reads graph nodes at `tests/lab/core-lab-boundary.test.ts:69` and its composition root at :355; its PROTECTED list (:20–28) and reader paths are unchanged. It discovers relative graph edges without a new leaf scan list. Never retarget or edit the protected production roots to accommodate this split. + +No source-text guard is retargeted. Add lock-ownership behavioral cases inside existing tests/lab/lab-automation-review-regressions.test.ts (no new test file): saving state must not reclaim a canonical lock owned by the current live PID, dead-owner lock is reclaimable, and finally release leaves no canonical lock after a failing mutation. Drive the live-owner case red once by temporarily allowing live-PID reclaim in state-lock.ts, then restore. Keep the unknown-field contract at :334 and tests/lab/lab-automation-coderabbit-regressions.test.ts:157 unchanged. New tests use the original persistence API, not private lock helpers. + +## Verification + +This is the `002_layer_map.md` Per-layer gate instantiated for S16 L2. These are **future implementation commands**, not tests run by this docs-only delegate. Run at this layer's own tip, not the top of the stack. Focused domains: tests/lab. + +```sh +bun run typecheck +bun test tests/lab/lab-automation.test.ts tests/lab/lab-automation-ingwannu-regressions.test.ts tests/lab/lab-automation-final-coderabbit-regressions.test.ts tests/lab/lab-automation-persisted-cap-regression.test.ts tests/lab/lab-automation-coderabbit-regressions.test.ts tests/lab/lab-automation-management-http.test.ts tests/lab/lab-automation-review-regressions.test.ts +bun test tests/lab +bun run privacy:scan +# No src/server, src/router or src/lib edit: 002's extra core-boundary command is not triggered. +wc -l src/lab/automation/state-lock.ts src/lab/automation/persistence.ts +rg -n 'from "[^"]*/persistence"' src gui/src scripts tests | wc -l +# Full suite only on the designated remote, never in this local worktree: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-automation-persistence && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused commands overlapping the full lab domain need not be repeated on unchanged code: capture the focused red/green during the move, then domain coverage once at the final tip. Typecheck/privacy must exit 0; tests must report zero failures. The basename-only rg baseline is 12; the resolved exact-module fan-in must remain 12. Leaf names deliberately do not end in /persistence, so they do not inflate that gate. Recount against the actual parent if upstream changes. + +The inherited remote pipeline's tail status alone is not proof of a passing Bun process: capture its complete test result and actual test exit status (enable pipefail or retain the status separately) and record the checked-out SHA. Do not treat fetch/checkout as authorization granted to this docs delegate. Parent/executor verifies remote checkout ownership before use. Record a green **complete exact-head CI rollup**, not an empty required-check list. New or modified source-oracle guards, if discovered, must be driven red and restored before claiming green. No test runner is installed for this plan. + +Use `git diff --check`, `git diff --numstat ...HEAD` and move-aware diff inspection to prove only declaration moves/import rewiring. Compare all original exports (including erased types) to the explicit inventory. Re-run the lane-G import graph check, including type edges; a clean typecheck alone does not prove acyclicity. + +## Accept criteria + +1. Every declaration in the inventory has exactly one owner after the split; no duplicated mutable state or constants, and no omitted declaration. +2. All 10 original exported names remain importable from `src/lab/automation/persistence` with the same signatures/identity; the named re-export and local-import blocks above are present exactly where needed. +3. The 1 new leaves have expected counts 118; residual expected 388. Actual `wc -l` is ≤400 for every one. No hidden #b or sixth stack layer is assumed. +4. Existing function bodies, comparison ordering, errors, cleanup/finally behavior, and allocation timing are unchanged apart from export visibility needed by the private leaf seam. No new upward or facade-back import; static/type/dynamic graph has no newly introduced cycle. +5. All direct tests keep original imports; all identified text-oracle dispositions are implemented without weakening. The named deliberate red mutation fails for the intended reason and is fully removed before the final green run. +6. The instantiated local focused/domain, typecheck and privacy gates plus the remote-only full suite pass on the recorded layer SHA, and its complete exact-head CI is green. No local full suite. +7. The PR contains only this layer's pure move and necessary existing-test additions, retains the parent branch base, and includes the full five-layer stack map. Any raw changeset above 500 lines is returned for explicit parent review; do not expand the authorized topology silently. + +## PR + +Title: `refactor(lab-automation): isolate the state-file lock owner (split S16 L2/5)` + +Branch: `codex/split-lab-automation-persistence`. Base: `dev`. Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); include the pure-move thesis, planned/actual counts, gate evidence and this DEV-STACK-03 map. The placeholders below are intentional pre-creation PR numbers, not existing PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S16-L1 | 530 | `codex/split-lab-conformance-executor` | `dev` | separate scenario transport and vector families | +| 2 | #TBD-S16-L2 | 540 — this PR | `codex/split-lab-automation-persistence` | `dev` | isolate the state-file lock owner | +| 3 | #TBD-S16-L3 | 550 | `codex/split-lab-public-community` | `dev` | extract bounded community input validation | +| 4 | #TBD-S16-L4 | 560 | `codex/split-lab-projection-verification` | `dev` | isolate suite artifact parsing | +| 5 | #TBD-S16-L5 | 570 | `codex/split-lab-projection-verdicts` | `codex/split-lab-projection-verification` | separate projection keys and claim reduction | + +Base: dev — no dependency on the layers below; no cascade obligation. Every layer passes independently. Merge remains separately user-authorized; never merge or enable auto-merge as part of this plan. diff --git a/devlog/_plan/260905_now_split_train/550_lab_public_community.md b/devlog/_plan/260905_now_split_train/550_lab_public_community.md new file mode 100644 index 0000000000..31244735ef --- /dev/null +++ b/devlog/_plan/260905_now_split_train/550_lab_public_community.md @@ -0,0 +1,219 @@ +# 550 — S16 L3/5: src/lab/public/community.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Work class: C3 architecture planning, docs-only delegated scope. Parent owns orchestration, loop and goal state; this document executes none of them. +- Goal: split `src/lab/public/community.ts` (479 lines) into the named leaves while preserving all current exports, signatures, object identities and behavior. +- Non-goals: no behavior fixes, public identifier renames, schema changes, new dependencies, import-consumer churn, function-body rewrites, core-root edits, merge, release or deployment. No code/test/git-state mutation in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current planning basis is docs HEAD `4cc219549`, code `origin/dev = 1362b1a38`; `git diff origin/dev -- src/lab/public/community.ts` is empty. All source line anchors below refer to that code basis, not future leaf line numbers. +- Stop: drafting ends after this plan's declaration/export/state/test inventory is checked. Implementation ends only when its independent per-layer gates and exact-head CI evidence are recorded; no merge is authorized by this document. +- Escalation: stop implementation and return to the parent if source drift invalidates the partition, an export/identity changes, an oracle cannot move without weakening, a new cycle appears, any residual/leaf exceeds 400, or the fixed layer scope needs expansion. Do not create an unplanned #b or edit 002 from this task. + +Public evidence validation is an existing security boundary. Require explicit security review of the move, but make no authority, privacy, signature, cache-quota, locking, filesystem safety or validation-policy changes. Any newly discovered security finding is recorded only in ignored scratch, not this public devlog. + +## Symbol inventory + +Origin/dev declaration spans were enumerated with `sg run --lang ts --kind 'function_declaration,lexical_declaration,interface_declaration,type_alias_declaration,export_statement' --json=compact src/lab/public/community.ts`, keeping column-zero declarations; exported declarations are counted once. Imports are not redeclarations of their source owners: original import block is src/lab/public/community.ts:1–25, and the exact post-split imports appear below. + +Consumer counts mean **direct importing/re-exporting modules**, not occurrences or transitive barrel consumers. Resolved relative import clauses were checked with `rg -q -w `; namespace imports and wildcard re-exports count once for every exported symbol. Non-exported declarations have zero external consumers. `rg --files src gui/src scripts tests` supplied the search universe. Module fan-in is 3; the mechanically requested basename-only gate returns 3. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `MAX_IMPORT_BYTES` | const | 27–27 | no | 0 | `community-input.ts` | +| `MAX_CACHE_FILES` | const | 28–28 | no | 0 | `community.ts (residual)` | +| `MAX_CACHE_BYTES` | const | 29–29 | no | 0 | `community.ts (residual)` | +| `MAX_DEPTH` | const | 30–30 | no | 0 | `community-input.ts` | +| `MAX_OBJECT_KEYS` | const | 31–31 | no | 0 | `community-input.ts` | +| `MAX_ARRAY_ELEMENTS` | const | 32–32 | no | 0 | `community-input.ts` | +| `MAX_GENERIC_STRING_BYTES` | const | 33–33 | no | 0 | `community-input.ts` | +| `COMMUNITY_MUTATION_LOCK_NAME` | const | 34–34 | no | 0 | `community.ts (residual)` | +| `COMMUNITY_BUNDLE_FILE_RE` | const | 35–35 | no | 0 | `community.ts (residual)` | +| `COMMUNITY_REVOCATION_FILE_RE` | const | 36–36 | no | 0 | `community.ts (residual)` | +| `COMMUNITY_FILE_OPTIONS` | const | 38–44 | no | 0 | `community.ts (residual)` | +| `CommunitySummaryCache` | type | 46–50 | no | 0 | `community.ts (residual)` | +| `communitySummaryCache` | let | 52–52 | no | 0 | `community.ts (residual)` | +| `assertId` | function | 54–59 | no | 0 | `community-input.ts` | +| `scanStructure` | function | 61–90 | no | 0 | `community-input.ts` | +| `boundedInput` | function | 92–108 | no | 0 | `community-input.ts` | +| `assertCommunityArtifactAuthority` | function | 110–117 | no | 0 | `community-input.ts` | +| `verifiedBundle` | function | 119–128 | no | 0 | `community-input.ts` | +| `bundleObjectPath` | function | 130–135 | no | 0 | `community.ts (residual)` | +| `revocationObjectPath` | function | 137–139 | no | 0 | `community.ts (residual)` | +| `readBounded` | function | 141–144 | no | 0 | `community.ts (residual)` | +| `cacheUsage` | function | 146–164 | no | 0 | `community.ts (residual)` | +| `assertCacheCanAdd` | function | 166–171 | no | 0 | `community.ts (residual)` | +| `persistAtLocked` | function | 173–218 | no | 0 | `community.ts (residual)` | +| `persistAt` | function | 220–231 | no | 0 | `community.ts (residual)` | +| `readJson` | function | 233–237 | no | 0 | `community.ts (residual)` | +| `files` | function | 239–241 | no | 0 | `community.ts (residual)` | +| `readVerifiedBundleAt` | function | 243–245 | no | 0 | `community.ts (residual)` | +| `bundleFromName` | function | 247–257 | no | 0 | `community.ts (residual)` | +| `bundlesFromNames` | function | 259–266 | no | 0 | `community.ts (residual)` | +| `restoreOwnPublisherOrigin` | function | 268–277 | no | 0 | `community.ts (residual)` | +| `importCommunityEvidenceBundle` | function | 279–293 | yes | 2 | `community.ts (residual)` | +| `readCommunityEvidenceBundleForPublisherLocked` | function | 295–305 | no | 0 | `community.ts (residual)` | +| `readCommunityEvidenceBundleForPublisher` | function | 307–316 | yes | 1 | `community.ts (residual)` | +| `RevocationMetadata` | type | 318–321 | no | 0 | `community-input.ts` | +| `resolveTargetBundle` | function | 323–360 | no | 0 | `community-input.ts` | +| `findTargetBundleLocked` | function | 362–387 | no | 0 | `community.ts (residual)` | +| `importCommunityEvidenceRevocation` | function | 389–409 | yes | 1 | `community.ts (residual)` | +| `communityFingerprint` | function | 411–417 | no | 0 | `community.ts (residual)` | +| `copySummaries` | function | 419–421 | no | 0 | `community.ts (residual)` | +| `listCommunityEvidenceLocked` | function | 423–475 | no | 0 | `community.ts (residual)` | +| `listCommunityEvidence` | function | 477–479 | yes | 3 | `community.ts (residual)` | + +Direct production consumers / public boundaries, all preserved: + +- `src/lab/public/index.ts:12`. +- `src/lab/public/operator.ts:5`. + +## Leaf partition + +Structural decision: Move bounded parsing, bundle validation and in-memory revocation-target resolution to community-input.ts; keep storage, locks, listing and cache invalidation together. Existing community-authority.ts, strict-json.ts, privacy.ts and signature.ts remain canonical and are reused; community-files.ts naming remains untouched. Reject lifting persistAtLocked into a separate storage leaf: its writes to communitySummaryCache at :209/:214 would require a new invalidation API or a back-import, so it is not the lowest-churn pure move. The chosen seam narrows the lane's broader import/storage recommendation to its stateless input portion. + +Sibling convention evidence: `src/lab/public/community-files.ts`, `community-authority.ts`, `file-safety.ts` and `strict-json.ts` are concern-named siblings; no second community registry or generic helpers module. + +The existing lane-016 inventory replaces an extra map command. Search evidence: `rg --files src/lab/public`, exact symbol searches and the direct-consumer inventory above; existing owners are reused, not copied. Doing nothing leaves the approved file-size debt; deletion/configuration would change behavior. Blast radius: local Lab feature plus unchanged entry-path consumers. + +Expected counts below are an in-memory plan calculation: original complete declaration bodies and attached comments, the imports shown here, named re-exports, and one blank line between declarations. They are not a claim of executed source changes. Formatting may change the exact number; implementation must run wc and still stay ≤400. Private declarations listed in each leaf's “leaf exports” gain only the internal import seam; they are **not** added to the original public export surface. + +### `src/lab/public/community-input.ts` — expected 137 lines + +Symbols: `MAX_IMPORT_BYTES`, `MAX_DEPTH`, `MAX_OBJECT_KEYS`, `MAX_ARRAY_ELEMENTS`, `MAX_GENERIC_STRING_BYTES`, `assertId`, `scanStructure`, `boundedInput`, `assertCommunityArtifactAuthority`, `verifiedBundle`, `RevocationMetadata`, `resolveTargetBundle`. + +Leaf exports: `MAX_IMPORT_BYTES`, `assertId`, `scanStructure`, `boundedInput`, `verifiedBundle`, `RevocationMetadata`, `resolveTargetBundle`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { jcsStringify } from "../digest"; +import { validateCommunityEvidenceAuthorities } from "./community-authority"; +import { validatePublicEvidencePrivacy } from "./privacy"; +import { verifyPublicEvidenceBundle } from "./signature"; +import { parseStrictPublicJson } from "./strict-json"; +import type { PublicEvidenceBundleV1 } from "./types"; +import { PublicEvidenceValidationError } from "./validate"; +``` + +### Residual `src/lab/public/community.ts` — expected 350 lines + +Retains: `MAX_CACHE_FILES`, `MAX_CACHE_BYTES`, `COMMUNITY_MUTATION_LOCK_NAME`, `COMMUNITY_BUNDLE_FILE_RE`, `COMMUNITY_REVOCATION_FILE_RE`, `COMMUNITY_FILE_OPTIONS`, `CommunitySummaryCache`, `communitySummaryCache`, `bundleObjectPath`, `revocationObjectPath`, `readBounded`, `cacheUsage`, `assertCacheCanAdd`, `persistAtLocked`, `persistAt`, `readJson`, `files`, `readVerifiedBundleAt`, `bundleFromName`, `bundlesFromNames`, `restoreOwnPublisherOrigin`, `importCommunityEvidenceBundle`, `readCommunityEvidenceBundleForPublisherLocked`, `readCommunityEvidenceBundleForPublisher`, `findTargetBundleLocked`, `importCommunityEvidenceRevocation`, `communityFingerprint`, `copySummaries`, `listCommunityEvidenceLocked`, `listCommunityEvidence`. + +No #a/#b/#c subdivision: the whole file's assigned work is this layer, and no residual exceeds 400. There is no unnamed later remainder. Upstream imports retained by the residual, in addition to the local imports in the next section: + +```ts +import { lstatSync, readdirSync, unlinkSync } from "node:fs"; +import { join } from "node:path"; +import { jcsStringify } from "../digest"; +import { ensureLabDirs, labCommunityDir } from "../paths"; +import { communityBundleFileName } from "./community-files"; +import { privateRegularFileSize, readPrivateRegularFile } from "./file-safety"; +import { withPublicEvidenceMutationLock } from "./mutation-lock"; +import { recordLocalPublicOrigin } from "./origin"; +import { cleanupStalePrivateFileStages, cleanupStalePrivateFileStagesInDir, isPrivateFileStageName, publishPrivateFileExclusive } from "./private-file"; +import { verifyPublicEvidenceRevocation } from "./revocation"; +import { loadExistingPublicPublisher } from "./signature"; +import { parseStrictPublicJson } from "./strict-json"; +import type { CommunityEvidenceSummaryV1, PublicEvidenceBundleV1, PublicEvidenceRevocationV1 } from "./types"; +import { PublicEvidenceValidationError } from "./validate"; +``` + +## Re-export block + +The compatibility re-export block is **empty**: this partition moves no currently exported declaration. Keep the existing exported function definitions in the original file. Do not fabricate an `export { boundedInput }` or expose any other formerly private leaf helper from the facade. + +Retained exports in the original file: `importCommunityEvidenceBundle`, `readCommunityEvidenceBundleForPublisher`, `importCommunityEvidenceRevocation`, `listCommunityEvidence`. No wildcard or renamed re-export is introduced. This is preservation of an existing boundary, not a new internal convenience barrel. + +Explicit local imports required by residual call sites (re-exporting binds nothing): + +```ts +import { MAX_IMPORT_BYTES, assertId, scanStructure, boundedInput, verifiedBundle, resolveTargetBundle } from "./community-input"; +import type { RevocationMetadata } from "./community-input"; +``` + +## Module-level state and cycles + +communitySummaryCache is the only top-level mutable singleton (community.ts:52). Keep its type at :46–50, all reads/writes (:209, :214, :427–428, :473), fingerprint (:411–417) and copy-on-read helper (:419–421) in community.ts, one owner. COMMUNITY_MUTATION_LOCK_NAME (:34) is a string, not a new lock object; the real lock remains ./mutation-lock. MAX_IMPORT_BYTES (:27) moves to community-input.ts and is explicitly imported for COMMUNITY_FILE_OPTIONS (:38–44) and persistAtLocked (:181), never duplicated. All other moved MAX_* values are immutable scalars. Sets at :340, :367, :449–450 remain per-call allocations. The dependency direction is community → community-input → existing validation authorities, never community-input → community. Existing cache/list/persist temporal coupling stays local; pure input functions are functional coupling. + +Lane 016 reported no return path through this file. The proposed edges above preserve that direction; this is a design argument, not a completed implementation cycle scan. During implementation, repeat lane 016 method G (resolved static imports/exports, type-only edges and literal dynamic imports) for each new leaf and the residual, and require no new cycle. Do not “fix” a cycle with lazy imports or duplicate a type/constant. No protected core root, activation timing or optional-Lab registration seam is changed. + +## Tests + +Direct test import inventory, from `rg -l 'src/lab/public/community"' tests` with relative specifiers resolved and hits inspected: + +| test file / import anchor | action | +|---|---| +| `tests/lab/lab-community-mutation-lock.test.ts:6` | unchanged — keep original import path | + +Additional indirect/guard coverage (all unchanged unless a narrowly described case is added below): + +- `tests/lab/lab-community-evidence.test.ts`. +- `tests/lab/lab-community-publisher-continuity.test.ts`. +- `tests/lab/lab-community-filename-contract.test.ts`. +- `tests/lab/lab-public-core-contract.test.ts`. +- `tests/lab/lab-public-review-fixes.test.ts`. +- `tests/lab/lab-public-deep-review-regressions.test.ts`. +- `tests/lab/lab-public-coderabbit-regressions.test.ts`. +- `tests/lab/lab-public-final-review-regressions.test.ts`. +- `tests/lab/lab-public-lifecycle-hardening.test.ts`. +- `tests/lab/lab-public-wire-contract.test.ts`. +- `tests/lab/lab-public-provenance-recovery.test.ts`. + +Text-oracle inventory: **zero tests read this specific file as source**. Checked `rg -n '(executor\\.ts|persistence\\.ts|community\\.ts|verification\\.ts|verdicts\\.ts)' tests`, qualified source paths and candidate reader bodies. Therefore retarget-to-leaf = none; add-leaf-to-scan-list = none. Behavioral imports stay unchanged; source-reading tests are not weakened into export-existence checks. + +The generic boundary guard reads graph nodes at `tests/lab/core-lab-boundary.test.ts:69` and its composition root at :355; its PROTECTED list (:20–28) and reader paths are unchanged. It discovers relative graph edges without a new leaf scan list. Never retarget or edit the protected production roots to accommodate this split. + +No source-text guard is retargeted. Drive tests/lab/lab-community-evidence.test.ts:151 red once by temporarily bypassing validateCommunityEvidenceAuthorities inside community-input.ts:verifiedBundle, then restore; retain same-key revocation/idempotence (:159), cross-key rejection (:178), and the original-path mutation-lock test (:72). These are planned controlled mutations in an isolated implementation checkout, not changes made by this docs task. + +## Verification + +This is the `002_layer_map.md` Per-layer gate instantiated for S16 L3. These are **future implementation commands**, not tests run by this docs-only delegate. Run at this layer's own tip, not the top of the stack. Focused domains: tests/lab. + +```sh +bun run typecheck +bun test tests/lab/lab-community-mutation-lock.test.ts tests/lab/lab-community-evidence.test.ts tests/lab/lab-community-publisher-continuity.test.ts tests/lab/lab-community-filename-contract.test.ts tests/lab/lab-public-core-contract.test.ts tests/lab/lab-public-review-fixes.test.ts tests/lab/lab-public-deep-review-regressions.test.ts tests/lab/lab-public-coderabbit-regressions.test.ts tests/lab/lab-public-final-review-regressions.test.ts tests/lab/lab-public-lifecycle-hardening.test.ts tests/lab/lab-public-wire-contract.test.ts tests/lab/lab-public-provenance-recovery.test.ts +bun test tests/lab +bun run privacy:scan +# No src/server, src/router or src/lib edit: 002's extra core-boundary command is not triggered. +wc -l src/lab/public/community-input.ts src/lab/public/community.ts +rg -n 'from "[^"]*/community"' src gui/src scripts tests | wc -l +# Full suite only on the designated remote, never in this local worktree: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-public-community && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused commands overlapping the full lab domain need not be repeated on unchanged code: capture the focused red/green during the move, then domain coverage once at the final tip. Typecheck/privacy must exit 0; tests must report zero failures. The basename-only rg baseline is 3; the resolved exact-module fan-in must remain 3. Leaf names deliberately do not end in /community, so they do not inflate that gate. Recount against the actual parent if upstream changes. + +The inherited remote pipeline's tail status alone is not proof of a passing Bun process: capture its complete test result and actual test exit status (enable pipefail or retain the status separately) and record the checked-out SHA. Do not treat fetch/checkout as authorization granted to this docs delegate. Parent/executor verifies remote checkout ownership before use. Record a green **complete exact-head CI rollup**, not an empty required-check list. New or modified source-oracle guards, if discovered, must be driven red and restored before claiming green. No test runner is installed for this plan. + +Use `git diff --check`, `git diff --numstat ...HEAD` and move-aware diff inspection to prove only declaration moves/import rewiring. Compare all original exports (including erased types) to the explicit inventory. Re-run the lane-G import graph check, including type edges; a clean typecheck alone does not prove acyclicity. + +## Accept criteria + +1. Every declaration in the inventory has exactly one owner after the split; no duplicated mutable state or constants, and no omitted declaration. +2. All 4 original exported names remain importable from `src/lab/public/community` with the same signatures/identity; the named re-export and local-import blocks above are present exactly where needed. +3. The 1 new leaves have expected counts 137; residual expected 350. Actual `wc -l` is ≤400 for every one. No hidden #b or sixth stack layer is assumed. +4. Existing function bodies, comparison ordering, errors, cleanup/finally behavior, and allocation timing are unchanged apart from export visibility needed by the private leaf seam. No new upward or facade-back import; static/type/dynamic graph has no newly introduced cycle. +5. All direct tests keep original imports; all identified text-oracle dispositions are implemented without weakening. The named deliberate red mutation fails for the intended reason and is fully removed before the final green run. +6. The instantiated local focused/domain, typecheck and privacy gates plus the remote-only full suite pass on the recorded layer SHA, and its complete exact-head CI is green. No local full suite. +7. The PR contains only this layer's pure move and necessary existing-test additions, retains the parent branch base, and includes the full five-layer stack map. Any raw changeset above 500 lines is returned for explicit parent review; do not expand the authorized topology silently. + +## PR + +Title: `refactor(lab-public): extract bounded community input validation (split S16 L3/5)` + +Branch: `codex/split-lab-public-community`. Base: `dev`. Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); include the pure-move thesis, planned/actual counts, gate evidence and this DEV-STACK-03 map. The placeholders below are intentional pre-creation PR numbers, not existing PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S16-L1 | 530 | `codex/split-lab-conformance-executor` | `dev` | separate scenario transport and vector families | +| 2 | #TBD-S16-L2 | 540 | `codex/split-lab-automation-persistence` | `dev` | isolate the state-file lock owner | +| 3 | #TBD-S16-L3 | 550 — this PR | `codex/split-lab-public-community` | `dev` | extract bounded community input validation | +| 4 | #TBD-S16-L4 | 560 | `codex/split-lab-projection-verification` | `dev` | isolate suite artifact parsing | +| 5 | #TBD-S16-L5 | 570 | `codex/split-lab-projection-verdicts` | `codex/split-lab-projection-verification` | separate projection keys and claim reduction | + +Base: dev — no dependency on the layers below; no cascade obligation. Every layer passes independently. Merge remains separately user-authorized; never merge or enable auto-merge as part of this plan. diff --git a/devlog/_plan/260905_now_split_train/560_lab_projection_verification.md b/devlog/_plan/260905_now_split_train/560_lab_projection_verification.md new file mode 100644 index 0000000000..5212420a8c --- /dev/null +++ b/devlog/_plan/260905_now_split_train/560_lab_projection_verification.md @@ -0,0 +1,173 @@ +# 560 — S16 L4/5: src/lab/projection/verification.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Work class: C3 architecture planning, docs-only delegated scope. Parent owns orchestration, loop and goal state; this document executes none of them. +- Goal: split `src/lab/projection/verification.ts` (412 lines) into the named leaves while preserving all current exports, signatures, object identities and behavior. +- Non-goals: no behavior fixes, public identifier renames, schema changes, new dependencies, import-consumer churn, function-body rewrites, core-root edits, merge, release or deployment. No code/test/git-state mutation in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current planning basis is docs HEAD `4cc219549`, code `origin/dev = 1362b1a38`; `git diff origin/dev -- src/lab/projection/verification.ts` is empty. All source line anchors below refer to that code basis, not future leaf line numbers. +- Stop: drafting ends after this plan's declaration/export/state/test inventory is checked. Implementation ends only when its independent per-layer gates and exact-head CI evidence are recorded; no merge is authorized by this document. +- Escalation: stop implementation and return to the parent if source drift invalidates the partition, an export/identity changes, an oracle cannot move without weakening, a new cycle appears, any residual/leaf exceeds 400, or the fixed layer scope needs expansion. Do not create an unplanned #b or edit 002 from this task. + +L5 still imports evaluateAllApplicableRequiredPassV1, newestObservationByScenario and ScenarioRequirements from verification.ts; this layer must pass independently before L5. No #b is needed. Long evaluator bodies are retained unchanged because this train only moves declarations. + +## Symbol inventory + +Origin/dev declaration spans were enumerated with `sg run --lang ts --kind 'function_declaration,lexical_declaration,interface_declaration,type_alias_declaration,export_statement' --json=compact src/lab/projection/verification.ts`, keeping column-zero declarations; exported declarations are counted once. Imports are not redeclarations of their source owners: original import block is src/lab/projection/verification.ts:1–5, and the exact post-split imports appear below. + +Consumer counts mean **direct importing/re-exporting modules**, not occurrences or transitive barrel consumers. Resolved relative import clauses were checked with `rg -q -w `; namespace imports and wildcard re-exports count once for every exported symbol. Non-exported declarations have zero external consumers. `rg --files src gui/src scripts tests` supplied the search universe. Module fan-in is 7; the mechanically requested basename-only gate returns 7. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `VerificationEvaluation` | interface | 7–13 | yes | 1 | `verification.ts (residual)` | +| `LoadScenarioManifest` | type | 15–15 | yes | 1 | `verification.ts (residual)` | +| `ScenarioRequirements` | interface | 17–26 | yes | 3 | `verification.ts (residual)` | +| `LoadScenarioRequirements` | type | 28–28 | yes | 1 | `verification.ts (residual)` | +| `isScenarioApplicable` | function | 31–42 | yes | 2 | `verification.ts (residual)` | +| `scenarioApplicableToRequirements` | function | 45–57 | no | 0 | `verification.ts (residual)` | +| `routeSubjectApplicableToRequirements` | function | 60–76 | yes | 1 | `verification.ts (residual)` | +| `taskSubjectApplicableToRequirements` | function | 79–96 | yes | 2 | `verification.ts (residual)` | +| `isNonNegativeInteger` | function | 99–101 | no | 0 | `verification-manifest.ts` | +| `parseFreshness` | function | 104–110 | no | 0 | `verification-manifest.ts` | +| `parseStringArray` | function | 113–116 | no | 0 | `verification.ts (residual)` | +| `scenarioContractFromManifest` | function | 118–153 | no | 0 | `verification.ts (residual)` | +| `effectiveMaxAgeMs` | function | 155–162 | no | 0 | `verification.ts (residual)` | +| `newestObservationByScenario` | function | 164–176 | yes | 2 | `verification.ts (residual)` | +| `evaluateAllApplicableRequiredPassV1` | function | 183–346 | yes | 4 | `verification.ts (residual)` | +| `requireNonEmptyString` | function | 348–350 | no | 0 | `verification-manifest.ts` | +| `parseSuiteManifestFromArtifact` | function | 352–412 | yes | 2 | `verification-manifest.ts` | + +Direct production consumers / public boundaries, all preserved: + +- `src/lab/automation/planner.ts:12`. +- `src/lab/index.ts:18`. +- `src/lab/projection/rebuild.ts:9`. +- `src/lab/projection/verdicts.ts:18`. + +## Leaf partition + +Structural decision: Extract the suite-artifact parser plus its freshness primitives; keep applicability, scenario-contract parsing and all-required-pass evaluation in verification.ts. Existing conformance/suite-manifest.ts owns SuiteManifestV1, and digest.ts owns isSha256Hex: reuse both. Reject moving only parseSuiteManifestFromArtifact while importing parseFreshness from verification; that creates a direct cycle. Reject additional applicability/types leaves because the parser extraction alone clears the 400-line gate. Scope is one Lab projection boundary, with no API/schema changes. + +Sibling convention evidence: `src/lab/projection/schema.ts`, `rebuild.ts`, `verification.ts` and `verdicts.ts` are sibling modules; the verification-manifest name distinguishes artifact parsing from conformance/suite-manifest.ts expansion. + +The existing lane-016 inventory replaces an extra map command. Search evidence: `rg --files src/lab/projection`, exact symbol searches and the direct-consumer inventory above; existing owners are reused, not copied. Doing nothing leaves the approved file-size debt; deletion/configuration would change behavior. Blast radius: local Lab feature plus unchanged entry-path consumers. + +Expected counts below are an in-memory plan calculation: original complete declaration bodies and attached comments, the imports shown here, named re-exports, and one blank line between declarations. They are not a claim of executed source changes. Formatting may change the exact number; implementation must run wc and still stay ≤400. Private declarations listed in each leaf's “leaf exports” gain only the internal import seam; they are **not** added to the original public export surface. + +### `src/lab/projection/verification-manifest.ts` — expected 84 lines + +Symbols: `isNonNegativeInteger`, `parseFreshness`, `requireNonEmptyString`, `parseSuiteManifestFromArtifact`. + +Leaf exports: `parseFreshness`, `parseSuiteManifestFromArtifact`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { EVIDENCE_LAYERS } from "../constants"; +import type { SuiteManifestV1 } from "../conformance/suite-manifest"; +import type { VerificationRole } from "../conformance/types"; +import { isSha256Hex } from "../digest"; +``` + +### Residual `src/lab/projection/verification.ts` — expected 334 lines + +Retains: `VerificationEvaluation`, `LoadScenarioManifest`, `ScenarioRequirements`, `LoadScenarioRequirements`, `isScenarioApplicable`, `scenarioApplicableToRequirements`, `routeSubjectApplicableToRequirements`, `taskSubjectApplicableToRequirements`, `parseStringArray`, `scenarioContractFromManifest`, `effectiveMaxAgeMs`, `newestObservationByScenario`, `evaluateAllApplicableRequiredPassV1`. + +No #a/#b/#c subdivision: the whole file's assigned work is this layer, and no residual exceeds 400. There is no unnamed later remainder. Upstream imports retained by the residual, in addition to the local imports in the next section: + +```ts +import type { ObservationEvent, ProtocolSubjectV1, RouteSubjectV1, TaskSubjectV1 } from "../events/types"; +import type { ExecutionMode } from "../constants"; +import type { SuiteManifestV1 } from "../conformance/suite-manifest"; +``` + +## Re-export block + +Add exactly these compatibility re-exports to `src/lab/projection/verification.ts`: + +```ts +export { parseSuiteManifestFromArtifact } from "./verification-manifest"; +``` + +Retained exports in the original file: `VerificationEvaluation`, `LoadScenarioManifest`, `ScenarioRequirements`, `LoadScenarioRequirements`, `isScenarioApplicable`, `routeSubjectApplicableToRequirements`, `taskSubjectApplicableToRequirements`, `newestObservationByScenario`, `evaluateAllApplicableRequiredPassV1`. No wildcard or renamed re-export is introduced. This is preservation of an existing boundary, not a new internal convenience barrel. + +Explicit local imports required by residual call sites (re-exporting binds nothing): + +```ts +import { parseFreshness } from "./verification-manifest"; +``` + +## Module-level state and cycles + +No module-level let/Map/Set/WeakMap/lock exists. byScenario (:167), scenarioMaxAgeById (:246), the Set at :290, and roles/seenScenarioIds (:376–377) are invocation-local. Keep their allocation timing intact. verification → verification-manifest → constants/conformance types/digest is acyclic; the parser imports no verification type. ScenarioRequirements, LoadScenarioManifest, LoadScenarioRequirements and VerificationEvaluation remain in verification.ts, so verdicts/rebuild keep the original types. parseFreshness has one owner in the new leaf; scenarioContractFromManifest uses the explicit import. Coupling is functional/sequential. + +Lane 016 reported no return path through this file. The proposed edges above preserve that direction; this is a design argument, not a completed implementation cycle scan. During implementation, repeat lane 016 method G (resolved static imports/exports, type-only edges and literal dynamic imports) for each new leaf and the residual, and require no new cycle. Do not “fix” a cycle with lazy imports or duplicate a type/constant. No protected core root, activation timing or optional-Lab registration seam is changed. + +## Tests + +Direct test import inventory, from `rg -l 'src/lab/projection/verification"' tests` with relative specifiers resolved and hits inspected: + +| test file / import anchor | action | +|---|---| +| `tests/lab/lab-fabric-task.test.ts:58` | unchanged — keep original import path | +| `tests/lab/lab-post-merge-projection.test.ts:11` | unchanged — keep original import path | +| `tests/lab/lab-evidence-ledger.test.ts:37` | unchanged — keep original import path | + +Text-oracle inventory: **zero tests read this specific file as source**. Checked `rg -n '(executor\\.ts|persistence\\.ts|community\\.ts|verification\\.ts|verdicts\\.ts)' tests`, qualified source paths and candidate reader bodies. Therefore retarget-to-leaf = none; add-leaf-to-scan-list = none. Behavioral imports stay unchanged; source-reading tests are not weakened into export-existence checks. + +The generic boundary guard reads graph nodes at `tests/lab/core-lab-boundary.test.ts:69` and its composition root at :355; its PROTECTED list (:20–28) and reader paths are unchanged. It discovers relative graph edges without a new leaf scan list. Never retarget or edit the protected production roots to accommodate this split. + +No source-text guard is retargeted. Add parser rejection/acceptance cases in existing tests/lab/lab-post-merge-projection.test.ts through the retained verification.ts path: valid suite accepted; duplicate scenario ID, invalid digest/role and invalid freshness rejected. Drive the duplicate-ID case red once by temporarily removing seenScenarioIds.has from verification-manifest.ts, then restore. Keep the stricter suite/scenario freshness contract at tests/lab/lab-post-merge-projection.test.ts:108 unchanged. + +## Verification + +This is the `002_layer_map.md` Per-layer gate instantiated for S16 L4. These are **future implementation commands**, not tests run by this docs-only delegate. Run at this layer's own tip, not the top of the stack. Focused domains: tests/lab. + +```sh +bun run typecheck +bun test tests/lab/lab-fabric-task.test.ts tests/lab/lab-post-merge-projection.test.ts tests/lab/lab-evidence-ledger.test.ts +bun test tests/lab +bun run privacy:scan +# No src/server, src/router or src/lib edit: 002's extra core-boundary command is not triggered. +wc -l src/lab/projection/verification-manifest.ts src/lab/projection/verification.ts +rg -n 'from "[^"]*/verification"' src gui/src scripts tests | wc -l +# Full suite only on the designated remote, never in this local worktree: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-projection-verification && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused commands overlapping the full lab domain need not be repeated on unchanged code: capture the focused red/green during the move, then domain coverage once at the final tip. Typecheck/privacy must exit 0; tests must report zero failures. The basename-only rg baseline is 7; the resolved exact-module fan-in must remain 7. Leaf names deliberately do not end in /verification, so they do not inflate that gate. Recount against the actual parent if upstream changes. + +The inherited remote pipeline's tail status alone is not proof of a passing Bun process: capture its complete test result and actual test exit status (enable pipefail or retain the status separately) and record the checked-out SHA. Do not treat fetch/checkout as authorization granted to this docs delegate. Parent/executor verifies remote checkout ownership before use. Record a green **complete exact-head CI rollup**, not an empty required-check list. New or modified source-oracle guards, if discovered, must be driven red and restored before claiming green. No test runner is installed for this plan. + +Use `git diff --check`, `git diff --numstat ...HEAD` and move-aware diff inspection to prove only declaration moves/import rewiring. Compare all original exports (including erased types) to the explicit inventory. Re-run the lane-G import graph check, including type edges; a clean typecheck alone does not prove acyclicity. + +## Accept criteria + +1. Every declaration in the inventory has exactly one owner after the split; no duplicated mutable state or constants, and no omitted declaration. +2. All 10 original exported names remain importable from `src/lab/projection/verification` with the same signatures/identity; the named re-export and local-import blocks above are present exactly where needed. +3. The 1 new leaves have expected counts 84; residual expected 334. Actual `wc -l` is ≤400 for every one. No hidden #b or sixth stack layer is assumed. +4. Existing function bodies, comparison ordering, errors, cleanup/finally behavior, and allocation timing are unchanged apart from export visibility needed by the private leaf seam. No new upward or facade-back import; static/type/dynamic graph has no newly introduced cycle. +5. All direct tests keep original imports; all identified text-oracle dispositions are implemented without weakening. The named deliberate red mutation fails for the intended reason and is fully removed before the final green run. +6. The instantiated local focused/domain, typecheck and privacy gates plus the remote-only full suite pass on the recorded layer SHA, and its complete exact-head CI is green. No local full suite. +7. The PR contains only this layer's pure move and necessary existing-test additions, retains the parent branch base, and includes the full five-layer stack map. Any raw changeset above 500 lines is returned for explicit parent review; do not expand the authorized topology silently. + +## PR + +Title: `refactor(lab-projection): isolate suite artifact parsing (split S16 L4/5)` + +Branch: `codex/split-lab-projection-verification`. Base: `dev`. Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); include the pure-move thesis, planned/actual counts, gate evidence and this DEV-STACK-03 map. The placeholders below are intentional pre-creation PR numbers, not existing PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S16-L1 | 530 | `codex/split-lab-conformance-executor` | `dev` | separate scenario transport and vector families | +| 2 | #TBD-S16-L2 | 540 | `codex/split-lab-automation-persistence` | `dev` | isolate the state-file lock owner | +| 3 | #TBD-S16-L3 | 550 | `codex/split-lab-public-community` | `dev` | extract bounded community input validation | +| 4 | #TBD-S16-L4 | 560 — this PR | `codex/split-lab-projection-verification` | `dev` | isolate suite artifact parsing | +| 5 | #TBD-S16-L5 | 570 | `codex/split-lab-projection-verdicts` | `codex/split-lab-projection-verification` | separate projection keys and claim reduction | + +Base: dev — no dependency on lower layers; this layer is the parent of 570 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). Every layer passes independently. Merge remains separately user-authorized; never merge or enable auto-merge as part of this plan. diff --git a/devlog/_plan/260905_now_split_train/570_lab_projection_verdicts.md b/devlog/_plan/260905_now_split_train/570_lab_projection_verdicts.md new file mode 100644 index 0000000000..1e158e2a6d --- /dev/null +++ b/devlog/_plan/260905_now_split_train/570_lab_projection_verdicts.md @@ -0,0 +1,188 @@ +# 570 — S16 L5/5: src/lab/projection/verdicts.ts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. Work class: C3 architecture planning, docs-only delegated scope. Parent owns orchestration, loop and goal state; this document executes none of them. +- Goal: split `src/lab/projection/verdicts.ts` (474 lines) into the named leaves while preserving all current exports, signatures, object identities and behavior. +- Non-goals: no behavior fixes, public identifier renames, schema changes, new dependencies, import-consumer churn, function-body rewrites, core-root edits, merge, release or deployment. No code/test/git-state mutation in this drafting task. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. Current planning basis is docs HEAD `4cc219549`, code `origin/dev = 1362b1a38`; `git diff origin/dev -- src/lab/projection/verdicts.ts` is empty. All source line anchors below refer to that code basis, not future leaf line numbers. +- Stop: drafting ends after this plan's declaration/export/state/test inventory is checked. Implementation ends only when its independent per-layer gates and exact-head CI evidence are recorded; no merge is authorized by this document. +- Escalation: stop implementation and return to the parent if source drift invalidates the partition, an export/identity changes, an oracle cannot move without weakening, a new cycle appears, any residual/leaf exceeds 400, or the fixed layer scope needs expansion. Do not create an unplanned #b or edit 002 from this task. + +Layer 5 uses the original verification.ts interface preserved by L4; it must not opportunistically retarget callers to L4's parser leaf. No #b or sixth layer is needed for file size. + +## Symbol inventory + +Origin/dev declaration spans were enumerated with `sg run --lang ts --kind 'function_declaration,lexical_declaration,interface_declaration,type_alias_declaration,export_statement' --json=compact src/lab/projection/verdicts.ts`, keeping column-zero declarations; exported declarations are counted once. Imports are not redeclarations of their source owners: original import block is src/lab/projection/verdicts.ts:1–22, and the exact post-split imports appear below. + +Consumer counts mean **direct importing/re-exporting modules**, not occurrences or transitive barrel consumers. Resolved relative import clauses were checked with `rg -q -w `; namespace imports and wildcard re-exports count once for every exported symbol. Non-exported declarations have zero external consumers. `rg --files src gui/src scripts tests` supplied the search universe. Module fan-in is 2; the mechanically requested basename-only gate returns 2. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ProjectionKey` | interface | 24–31 | yes | 1 | `verdict-keys.ts` | +| `componentKey` | function | 33–35 | no | 0 | `verdict-keys.ts` | +| `projectionKeyString` | function | 37–46 | yes | 2 | `verdict-keys.ts` | +| `claimKeyString` | function | 48–50 | yes | 2 | `verdict-keys.ts` | +| `DerivedVerdict` | interface | 52–61 | yes | 1 | `verdicts.ts (residual)` | +| `ClaimState` | interface | 63–68 | yes | 1 | `verdict-claims.ts` | +| `ProjectVerdictsOptions` | interface | 70–78 | yes | 1 | `verdicts.ts (residual)` | +| `resolveClaimStates` | function | 84–167 | yes | 2 | `verdict-claims.ts` | +| `supportedClaimsForSubject` | function | 169–177 | no | 0 | `verdict-claims.ts` | +| `projectVerdicts` | function | 182–292 | yes | 2 | `verdicts.ts (residual)` | +| `isMatchedCapabilityAbsenceControl` | function | 294–302 | no | 0 | `verdicts.ts (residual)` | +| `evaluateRequiredPassVerdict` | function | 304–346 | no | 0 | `verdicts.ts (residual)` | +| `projectObservationGroup` | function | 348–466 | no | 0 | `verdicts.ts (residual)` | +| `excludeEventIds` | function | 468–472 | yes | 2 | `verdicts.ts (residual)` | +| `isEventExcluded` | re-export | 474–474 | yes | 1 | `verdicts.ts (residual)` | + +Direct production consumers / public boundaries, all preserved: + +- `src/lab/index.ts:13`. +- `src/lab/projection/rebuild.ts:18`. + +## Leaf partition + +Structural decision: Separate canonical JCS component keys and claim supersession reduction; retain observation verdict precedence and orchestration in verdicts.ts. verdicts → verdict-claims → verdict-keys → digest, with verdicts → verdict-keys too. Reject extracting resolveClaimStates alone with claimKeyString imported from verdicts: that would create a direct cycle. Reject moving observation projection as well because the claim/key partition alone meets the file limit. Keep the original src/lab/index.ts export boundary and all rebuild imports. + +Sibling convention evidence: `src/lab/projection/schema.ts`, `rebuild.ts` and `verification.ts` are concern-named siblings; verdict-keys/verdict-claims retain projection ownership instead of moving generic key utilities into src/lib. + +The existing lane-016 inventory replaces an extra map command. Search evidence: `rg --files src/lab/projection`, exact symbol searches and the direct-consumer inventory above; existing owners are reused, not copied. Doing nothing leaves the approved file-size debt; deletion/configuration would change behavior. Blast radius: local Lab feature plus unchanged entry-path consumers. + +Expected counts below are an in-memory plan calculation: original complete declaration bodies and attached comments, the imports shown here, named re-exports, and one blank line between declarations. They are not a claim of executed source changes. Formatting may change the exact number; implementation must run wc and still stay ≤400. Private declarations listed in each leaf's “leaf exports” gain only the internal import seam; they are **not** added to the original public export surface. + +### `src/lab/projection/verdict-keys.ts` — expected 29 lines + +Symbols: `ProjectionKey`, `componentKey`, `projectionKeyString`, `claimKeyString`. + +Leaf exports: `ProjectionKey`, `projectionKeyString`, `claimKeyString`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import { jcsStringify } from "../digest"; +``` + +### `src/lab/projection/verdict-claims.ts` — expected 108 lines + +Symbols: `ClaimState`, `resolveClaimStates`, `supportedClaimsForSubject`. + +Leaf exports: `ClaimState`, `resolveClaimStates`, `supportedClaimsForSubject`. Everything else in this leaf stays private. + +Own imports (exact): + +```ts +import type { ClaimSnapshotEvent, LedgerCorruption } from "../events/types"; +import { claimKeyString } from "./verdict-keys"; +``` + +### Residual `src/lab/projection/verdicts.ts` — expected 333 lines + +Retains: `DerivedVerdict`, `ProjectVerdictsOptions`, `projectVerdicts`, `isMatchedCapabilityAbsenceControl`, `evaluateRequiredPassVerdict`, `projectObservationGroup`, `excludeEventIds`, `isEventExcluded`. + +No #a/#b/#c subdivision: the whole file's assigned work is this layer, and no residual exceeds 400. There is no unnamed later remainder. Upstream imports retained by the residual, in addition to the local imports in the next section: + +```ts +import type { CompatibilityVerdict } from "../constants"; +import { LAB_PROJECTION_SPEC_VERSION } from "../constants"; +import type { SuiteManifestV1 } from "../conformance/suite-manifest"; +import type { LabEvent, LedgerCorruption, ObservationEvent } from "../events/types"; +import { buildInvalidationIndex, isEventExcluded, usableClaims, usableObservations, type InvalidationIndex } from "../ledger/invalidation"; +import { evaluateAllApplicableRequiredPassV1, newestObservationByScenario, type ScenarioRequirements } from "./verification"; +``` + +## Re-export block + +Add exactly these compatibility re-exports to `src/lab/projection/verdicts.ts`: + +```ts +export { projectionKeyString, claimKeyString } from "./verdict-keys"; +export type { ProjectionKey } from "./verdict-keys"; +export { resolveClaimStates } from "./verdict-claims"; +export type { ClaimState } from "./verdict-claims"; +``` + +Retained exports in the original file: `DerivedVerdict`, `ProjectVerdictsOptions`, `projectVerdicts`, `excludeEventIds`, `isEventExcluded`. In particular, retain the exact existing `export { isEventExcluded };` at origin/dev:474, with its local import from `../ledger/invalidation`. No wildcard or renamed re-export is introduced. This is preservation of an existing boundary, not a new internal convenience barrel. + +Explicit local imports required by residual call sites (re-exporting binds nothing): + +```ts +import { projectionKeyString } from "./verdict-keys"; +import type { ProjectionKey } from "./verdict-keys"; +import { resolveClaimStates, supportedClaimsForSubject } from "./verdict-claims"; +``` + +## Module-level state and cycles + +No module-level let/Map/Set/WeakMap/lock exists. The Maps/Sets in resolveClaimStates (:94–115), supportedClaimsForSubject (:170), projectVerdicts (:188–208), projectObservationGroup (:366/:382) and excludeEventIds (:469) are per invocation; none becomes a shared cache. ClaimState belongs only to verdict-claims.ts, ProjectionKey only to verdict-keys.ts. Neither leaf imports verdicts.ts, verification.ts or rebuild.ts; verdicts keeps its existing verification dependency. This is functional/sequential coupling with no new common state. isEventExcluded retains its existing ../ledger/invalidation owner and re-export identity. + +Lane 016 reported no return path through this file. The proposed edges above preserve that direction; this is a design argument, not a completed implementation cycle scan. During implementation, repeat lane 016 method G (resolved static imports/exports, type-only edges and literal dynamic imports) for each new leaf and the residual, and require no new cycle. Do not “fix” a cycle with lazy imports or duplicate a type/constant. No protected core root, activation timing or optional-Lab registration seam is changed. + +## Tests + +Direct test import inventory, from `rg -l 'src/lab/projection/verdicts"' tests` with relative specifiers resolved and hits inspected: + +None (zero direct test importers). Do not interpret this as zero coverage: the barrel-mediated tests below exercise the public API. + +Additional indirect/guard coverage (all unchanged unless a narrowly described case is added below): + +- `tests/lab/lab-evidence-ledger.test.ts`. +- `tests/lab/lab-post-merge-projection.test.ts`. + +Text-oracle inventory: **zero tests read this specific file as source**. Checked `rg -n '(executor\\.ts|persistence\\.ts|community\\.ts|verification\\.ts|verdicts\\.ts)' tests`, qualified source paths and candidate reader bodies. Therefore retarget-to-leaf = none; add-leaf-to-scan-list = none. Behavioral imports stay unchanged; source-reading tests are not weakened into export-existence checks. + +The generic boundary guard reads graph nodes at `tests/lab/core-lab-boundary.test.ts:69` and its composition root at :355; its PROTECTED list (:20–28) and reader paths are unchanged. It discovers relative graph edges without a new leaf scan list. Never retarget or edit the protected production roots to accommodate this split. + +No direct source-text test or retarget exists. Drive tests/lab/lab-evidence-ledger.test.ts:475's conflicting-current-claims assertion red once by temporarily suppressing the multiple-unsuperseded-claims corruption in verdict-claims.ts; restore immediately. Also retain supersession (:465), projectVerdicts empty/replay behavior (:1031/:1038) and the capability-absence precedence regression at tests/lab/lab-post-merge-projection.test.ts:155. + +## Verification + +This is the `002_layer_map.md` Per-layer gate instantiated for S16 L5. These are **future implementation commands**, not tests run by this docs-only delegate. Run at this layer's own tip, not the top of the stack. Focused domains: tests/lab. + +```sh +bun run typecheck +bun test tests/lab/lab-evidence-ledger.test.ts tests/lab/lab-post-merge-projection.test.ts +bun test tests/lab +bun run privacy:scan +# No src/server, src/router or src/lib edit: 002's extra core-boundary command is not triggered. +wc -l src/lab/projection/verdict-keys.ts src/lab/projection/verdict-claims.ts src/lab/projection/verdicts.ts +rg -n 'from "[^"]*/verdicts"' src gui/src scripts tests | wc -l +# Full suite only on the designated remote, never in this local worktree: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-lab-projection-verdicts && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +Focused commands overlapping the full lab domain need not be repeated on unchanged code: capture the focused red/green during the move, then domain coverage once at the final tip. Typecheck/privacy must exit 0; tests must report zero failures. The basename-only rg baseline is 2; the resolved exact-module fan-in must remain 2. Leaf names deliberately do not end in /verdicts, so they do not inflate that gate. Recount against the actual parent if upstream changes. + +The inherited remote pipeline's tail status alone is not proof of a passing Bun process: capture its complete test result and actual test exit status (enable pipefail or retain the status separately) and record the checked-out SHA. Do not treat fetch/checkout as authorization granted to this docs delegate. Parent/executor verifies remote checkout ownership before use. Record a green **complete exact-head CI rollup**, not an empty required-check list. New or modified source-oracle guards, if discovered, must be driven red and restored before claiming green. No test runner is installed for this plan. + +Use `git diff --check`, `git diff --numstat ...HEAD` and move-aware diff inspection to prove only declaration moves/import rewiring. Compare all original exports (including erased types) to the explicit inventory. Re-run the lane-G import graph check, including type edges; a clean typecheck alone does not prove acyclicity. + +## Accept criteria + +1. Every declaration in the inventory has exactly one owner after the split; no duplicated mutable state or constants, and no omitted declaration. +2. All 10 original exported names remain importable from `src/lab/projection/verdicts` with the same signatures/identity; the named re-export and local-import blocks above are present exactly where needed. +3. The 2 new leaves have expected counts 29, 108; residual expected 333. Actual `wc -l` is ≤400 for every one. No hidden #b or sixth stack layer is assumed. +4. Existing function bodies, comparison ordering, errors, cleanup/finally behavior, and allocation timing are unchanged apart from export visibility needed by the private leaf seam. No new upward or facade-back import; static/type/dynamic graph has no newly introduced cycle. +5. All direct tests keep original imports; all identified text-oracle dispositions are implemented without weakening. The named deliberate red mutation fails for the intended reason and is fully removed before the final green run. +6. The instantiated local focused/domain, typecheck and privacy gates plus the remote-only full suite pass on the recorded layer SHA, and its complete exact-head CI is green. No local full suite. +7. The PR contains only this layer's pure move and necessary existing-test additions, retains the parent branch base, and includes the full five-layer stack map. Any raw changeset above 500 lines is returned for explicit parent review; do not expand the authorized topology silently. + +## PR + +Title: `refactor(lab-projection): separate projection keys and claim reduction (split S16 L5/5)` + +Branch: `codex/split-lab-projection-verdicts`. Base: `codex/split-lab-projection-verification`. Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist); include the pure-move thesis, planned/actual counts, gate evidence and this DEV-STACK-03 map. The placeholders below are intentional pre-creation PR numbers, not existing PRs. + +| # | PR | Layer | Branch | Base | Review focus | +|---|---|---|---|---|---| +| 1 | #TBD-S16-L1 | 530 | `codex/split-lab-conformance-executor` | `dev` | separate scenario transport and vector families | +| 2 | #TBD-S16-L2 | 540 | `codex/split-lab-automation-persistence` | `dev` | isolate the state-file lock owner | +| 3 | #TBD-S16-L3 | 550 | `codex/split-lab-public-community` | `dev` | extract bounded community input validation | +| 4 | #TBD-S16-L4 | 560 | `codex/split-lab-projection-verification` | `dev` | isolate suite artifact parsing | +| 5 | #TBD-S16-L5 | 570 — this PR | `codex/split-lab-projection-verdicts` | `codex/split-lab-projection-verification` | separate projection keys and claim reduction | + +Depends on #TBD-S16-L4 (`codex/split-lab-projection-verification`); review only this layer's diff against that parent. Every layer passes independently. Changes to the real parent, S16 L4 (`codex/split-lab-projection-verification`), require a parent-owned cascade to S16 L5 and fresh exact-head checks for L5 (DEV-STACK-02). No cascade dependency on S16 L1–L3. Bottom-up merge of L4 then L5 remains separately user-authorized; never merge or enable auto-merge as part of this plan. diff --git a/devlog/_plan/260905_now_split_train/580_components_storage_workspace_StorageWorkspace.md b/devlog/_plan/260905_now_split_train/580_components_storage_workspace_StorageWorkspace.md new file mode 100644 index 0000000000..f554365624 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/580_components_storage_workspace_StorageWorkspace.md @@ -0,0 +1,219 @@ +# 580 — S17 L1/3: storage workspace diagnostic boundary + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**. Class C3 boundary plan, docs-only delegated mode. +Non-goals: no new API client, data deletion behavior, label change, hook lifetime +change, export removal, dependency installation, or runtime fix. +Goal: keep workspace dispatch/reconciliation in its current owner while moving +diagnostic rendering and DTO definitions into two feature-local leaves. +Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. +Stop: all accept criteria and parent-approved diff budget satisfied; an open +exact-head-green PR is the eventual train outcome, never a merge. +Escalation: stop implementation for stale source, changed public signatures, +unexpected oracle/cycle, or a >500-line diff. L1 moves 309 source lines; ordinary +additions+deletions exceed 500, so the parent must explicitly settle move-aware +accounting or approve/reslice it. Do not silently call a 600+ line diff ≤500. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; +docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. All source ranges below +are inclusive origin/dev ranges, not post-move positions. Read with `git show +origin/dev:`; `git diff origin/dev -- gui/src/pages/Storage.tsx +gui/src/components/storage-workspace/StorageWorkspace.tsx` was empty. +Declaration endpoints were checked with `sg run --kind --json=compact +`, and top-level starts with anchored rg. No code or test execution occurred. + +Evidence: `015_lane_gui.md:262–271`. Current map: +`Storage.tsx + three GUI tests → StorageWorkspace → React/i18n/format-bytes`. +Intended map: those callers retain the original path; original → DTO leaf and +diagnostic leaf; diagnostic → DTO leaf and existing i18n/formatting modules. +Blast radius: feature-local GUI, no backend/public protocol changes. +Decision: reject deleting/configuring away diagnostic UI or importing backend +scanner DTOs (the scanner contract at `src/storage/scanner.ts:33–59` is not the +GUI log-guard response contract). Choose colocation, not a generic utils module. +Sibling convention: `gui/src/pages/startup-sections.tsx:1–13` and +`gui/src/pages/claude-code-types.ts:1–7`; kebab-case named feature leaves. + +## Symbol inventory + +All ranges below refer to +`gui/src/components/storage-workspace/StorageWorkspace.tsx` at origin/dev. +E / R = distinct external importer files / local rg identifier-reference count, +excluding the declaration (and the file-header mention of StorageWorkspace). +Path search `rg -l 'from .*storage-workspace/StorageWorkspace["\x27]' +src gui/src gui/tests scripts tests` returned four files: Storage.tsx and the +three GUI test files listed below. Only StorageReport and the default are used +externally; similarly named server scanner symbols are not consumers. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| useMemo, useState | import bindings | 8–8 | no | dependency bindings | original; panel also imports useState | +| IconChevron, IconHardDrive | import bindings | 9–9 | no | dependency bindings | original | +| useT, TFn, TKey, Locale | import bindings | 10–10 | no | dependency bindings | original; leaf types as listed below | +| logGuardLabel | import | 11–11 | no | dependency binding | original + diagnostic | +| logGuardOperationLabel | import | 12–12 | no | dependency binding | original + diagnostic | +| logGuardProtectionModeLabel, logGuardProtectionStateLabel, logGuardSchemaStateLabel | imports | 13–17 | no | dependency bindings | diagnostic | +| formatBytes | import | 18–18 | no | dependency binding | original + diagnostic | +| StorageLargestEntry | interface | 20–23 | yes | 0 / 2 | storage-workspace-types.ts | +| StorageBucket | interface | 25–34 | yes | 0 / 3 | storage-workspace-types.ts | +| LogGuardReason | type | 36–36 | no | 0 / 1 | storage-workspace-types.ts | +| LogGuardCapability | type | 37–37 | no | 0 / 3 | storage-workspace-types.ts | +| LogGuardSchema | type | 38–42 | no | 0 / 1 | storage-workspace-types.ts | +| CodexLogGuardProtection | interface | 44–48 | yes | 0 / 1 | storage-workspace-types.ts | +| CodexLogGuardReport | interface | 50–78 | yes | 0 / 5 | storage-workspace-types.ts | +| StorageReport | interface | 80–88 | yes | 4 / 1 | storage-workspace-types.ts | +| CodexLogGuardAction | type | 90–94 | yes | 0 / 3 | storage-workspace-types.ts | +| BUCKET_TKEYS | const record | 97–105 | no | 0 / 1 | original | +| bucketLabel | function | 107–110 | yes | 0 / 4 | original | +| formatDate | function | 112–114 | no | 0 / 2 | original | +| rowsDisplay | function | 116–120 | no | 0 / 1 | original | +| mutationErrorLabel | function | 122–141 | no | 0 / 1 | original | +| CodexLogGuardPanel | component | 143–357 | no | 0 / 1 | codex-log-guard-panel.tsx | +| CodexLogGuardUnavailablePanel | component | 359–366 | no | 0 / 1 | codex-log-guard-panel.tsx | +| StorageWorkspaceProps | interface | 368–374 | yes | 0 / 1 | storage-workspace-types.ts | +| GenerationScopedLogGuardReport | type | 376–379 | no | 0 / 1 | original | +| GenerationScopedError | type | 381–384 | no | 0 / 1 | original | +| GenerationScopedCompaction | type | 392–395 | no | 0 / 1 | original | +| StorageWorkspace | component | 397–668 | default | 4 / 0 | original | + +## Leaf partition + +1. **NEW `gui/src/components/storage-workspace/storage-workspace-types.ts`**: + definitions 20–94 and 368–374 (all DTOs and props in the inventory); + expected **85 lines** = 75 + 7 body lines + 3 import/separator lines. + Own import: `import type { Locale } from "../../i18n/shared";`. + Preserve private LogGuardReason/Capability/Schema as private dependencies of + the exported report; preserve the seven existing exported type names. +2. **NEW `gui/src/components/storage-workspace/codex-log-guard-panel.tsx`**: + CodexLogGuardPanel and CodexLogGuardUnavailablePanel, 143–366; + expected **232 lines** = 224 body + 8 import/separator lines. + Own imports: useState from react; Locale/TFn types from ../../i18n/shared; + logGuardLabel from ../../i18n/log-guard-labels; logGuardOperationLabel from + ../../i18n/log-guard-operation-labels; the three state-label functions from + ../../i18n/log-guard-state-labels; formatBytes from ../../format-bytes; + CodexLogGuardReport/CodexLogGuardAction types from ./storage-workspace-types. + Export both components by name only for the original's internal imports. + +Residual **`gui/src/components/storage-workspace/StorageWorkspace.tsx`: 358 +expected lines**, using single-line named import/re-export declarations: +668 − 76 (20–95) − 225 (143–367) − 8 (368–375) − 5 (old state-label +import) + 4 (two local imports, re-export, separator) = 358. +Formatting may vary; every file must be ≤400 at verification. No #b required. +Total planned physical lines: 358 + 85 + 232 = 675, seven lines of net glue. +Large pre-existing functions remain: this pure-move file-size layer does not +claim to resolve every >50-line function in the debt ledger. + +## Re-export block + +At the original path, preserve every current named type export exactly: + +```ts +export type { StorageLargestEntry, StorageBucket, CodexLogGuardProtection, CodexLogGuardReport, StorageReport, CodexLogGuardAction, StorageWorkspaceProps } from "./storage-workspace-types"; +import type { StorageLargestEntry, StorageBucket, CodexLogGuardReport, CodexLogGuardAction, StorageWorkspaceProps } from "./storage-workspace-types"; +import { CodexLogGuardPanel, CodexLogGuardUnavailablePanel } from "./codex-log-guard-panel"; +``` + +Keep existing `export function bucketLabel` (107) and `export default function +StorageWorkspace` (397) in place: no value re-export is necessary because neither +moves. Re-exporting the DTOs does not bind them locally; the explicit type +import above is required. Do not expose the formerly private diagnostic +components or private report aliases through the original public path. +Keep the existing react-refresh suppression with bucketLabel; no new index barrel. + +## Module-level state and cycles + +`BUCKET_TKEYS:97–105` is the sole top-level data object, read-only by usage; +its only owner remains the original. No top-level let, Map, Set, WeakMap or +lock exists. `new Map` at 436 is component-local useMemo state, not a singleton. +Generation-tagged state at 405–409 and the action dispatcher at 440–531 stay +together; do not move fetching into the renderer. Confirmation state at 169 +moves with CodexLogGuardPanel, preserving that component's identity/lifetime. +Do not nest a new component definition inside the residual component. + +Avoid original → panel → original by importing report/action types directly from +the DTO leaf. DTOs must not import the panel or original, including type-only +imports. Existing i18n and format modules are downstream dependencies, not +consumers of the workspace. Coupling: typed props/events (functional); generation +and compaction receipt order (temporal) remain within the original owner. +The lane's static-relative SCC scan found no cycle; implementation must freshly +walk static import/export edges including type-only edges for these three files. + +## Tests + +`rg -l 'from .*storage-workspace/StorageWorkspace["\x27]' gui/tests tests` list: +- `gui/tests/storage-log-guard.test.tsx:4` — unchanged. +- `gui/tests/storage-log-guard-protection.test.tsx:4` — unchanged. +- `gui/tests/storage-log-guard-compact.test.tsx:7` — unchanged. + +No source-text reader for StorageWorkspace found with either literal +`StorageWorkspace.tsx` or extensionless name searches in tests/gui/tests. +No retarget-to-leaf or add-leaf-to-scan-list is required for this layer. +Existing behavioral guards to drive red once during implementation: +disable the new panel's compact confirmation gate and require +`storage-log-guard-compact.test.tsx:110` to fail, then restore; suppress the +metrics-skipped notice and require `storage-log-guard.test.tsx:141` to fail. +These prove the unchanged public imports execute the moved panel. Preserve +unsupported-schema controls, compaction receipts after failed refresh, and +generation reconciliation; do not mutate actual user storage for verification. + +## Verification + +Future executor only; none of these commands ran in this docs task. +Instantiate 002's Per-layer gate at this layer's exact tip: + +```sh +bun run typecheck +bun test gui/tests/storage-log-guard.test.tsx gui/tests/storage-log-guard-protection.test.tsx gui/tests/storage-log-guard-compact.test.tsx +bun run privacy:scan +wc -l gui/src/components/storage-workspace/StorageWorkspace.tsx gui/src/components/storage-workspace/storage-workspace-types.ts gui/src/components/storage-workspace/codex-log-guard-panel.tsx +rg -l 'from .*storage-workspace/StorageWorkspace["\x27]' src gui/src gui/tests scripts tests +git diff --check +git diff --numstat dev...HEAD +(cd gui && bun run build && bun run lint) +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-components-storage-workspace-StorageWorkspace && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && cd gui && bun test tests && bun run lint && bun run build' +``` + +Domain: GUI storage/log-guard. Original-path importer list stays four files, +with the same imported names; TypeScript resolves re-exported types. +No src/server, src/router, or src/lib edits, so the conditional core-lab test +does not apply and PROTECTED roots remain untouched. Full suite runs only on +lidge, not locally; capture full output and remote HEAD equality, not just a +tail pipeline's status. Resolve remote checkout ownership through the parent +before its shared runner is used. Fresh exact-head CI and static cycle check +are also required. No copy/locale changes, so no new i18n strings are permitted. + +## Accept criteria + +1. Exactly two new leaves; physical line counts ≤400 and original ≤400. +2. Seven named type exports, bucketLabel, and default component remain available + from the original path; all four existing consumer files remain unchanged. +3. AST-normalized moved bodies are identical except export/import glue; no new + request, validation, mutation, label, or state lifetime. +4. No leaf imports the original; static import/export SCC containing these + files is empty, including type-only edges. +5. Focused guards show red-once/restored-green evidence; typecheck, build, lint, + privacy, remote suite, and exact-head CI are green. +6. Parent resolves the >500 ordinary-diff accounting before implementation/PR + readiness. Never claim the gate was satisfied using deleted-lines-only math. + +## PR + +Title: `refactor(gui-storage): isolate workspace diagnostics and contracts (split S17 L1/3)` +Base: `dev`. Branch: `codex/split-components-storage-workspace-StorageWorkspace`. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S17 L1/3 | # | codex/split-components-storage-workspace-StorageWorkspace | dev | Diagnostic panel and workspace DTOs | +| S17 L2/3 | # | codex/split-pages-Storage-a | codex/split-components-storage-workspace-StorageWorkspace | Manual cleanup and quarantine leaves | +| S17 L3/3 | # | codex/split-pages-Storage-b | codex/split-pages-Storage-a | Policy ownership and cleanup composition | + +Review only the diff against the named base. Merge bottom-up only with separate +authorization; no merge or auto-merge is authorized by this plan. A lower-layer +change requires a verified cascade and renewed exact-head gates for upper layers. +Fill Summary, Verification, and Checklist from the repository PR template; because +this is GUI scope, attach unchanged-layout screenshot evidence in the eventual PR. +Closes: none. diff --git a/devlog/_plan/260905_now_split_train/590_pages_Storage_a.md b/devlog/_plan/260905_now_split_train/590_pages_Storage_a.md new file mode 100644 index 0000000000..abf7e1fe20 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/590_pages_Storage_a.md @@ -0,0 +1,253 @@ +# 590 — S17 L2/3: Storage part a + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**. C3 module-boundary plan, docs-only delegated task. +Non-goals: no new cleanup/restore behavior, changed confirmations, public +signature changes, cache replacement, polling changes, or backend DTO reuse. +Goal: extract the low-fan-in manual/quarantine panels and their private data +dependencies while leaving automatic policy and page composition in place. +Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. +Stop: mechanical acceptance plus parent resolution of the size gate; eventual +open exact-head-green PR, never merge. Escalate any stale source, ownership +collision, weakened test, state lifetime change, or required out-of-scope file. +**Escalation S17-SIZE-01:** 002 fixes only two Storage layers at ≤500 changed +source lines each, but 1469 − 400 = 1069 original lines must leave the page even +before import/wrapper overhead. Thus no honest two-layer partition can satisfy +that cap, even under the generous count-once definition of a move. Ordinary +added+deleted diff accounting is larger still. This document supplies the +requested concrete partition, not an approved exception. Parent must authorize +a size exception or expand/remap the stack; do not invent a fourth document, +change 002, or claim this layer is implementation-ready. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; +docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. All source ranges below +are inclusive origin/dev ranges, not post-move positions. Read with `git show +origin/dev:`; `git diff origin/dev -- gui/src/pages/Storage.tsx +gui/src/components/storage-workspace/StorageWorkspace.tsx` was empty. +Declaration endpoints were checked with `sg run --kind --json=compact +`, and top-level starts with anchored rg. No code or test execution occurred. + +Structural evidence: `015_lane_gui.md:131–143,604`. +Current: App/tests → Storage → cleanup panels + workspace + resource/cache. +Chosen: original → two panel leaves; both → cleanup-error; original/quarantine +→ cleanup-contracts. Later #b cleanup-card consumes the same leaves. +Reject a shared storage service or generic helpers module: no new behavior is +needed, and backend types are not the existing GUI DTOs. Reuse kebab-case page +siblings (`startup-sections.tsx:1`, `claude-code-types.ts:1`); no index barrel. +Blast radius is the storage feature plus one existing source-oracle scan list. + +## Symbol inventory + +For every row, ranges refer to `gui/src/pages/Storage.tsx` at origin/dev. +Consumer notation `E / R`: E = distinct external importing files, R = local +identifier-reference occurrences from `git show origin/dev: | rg -o -w +`, excluding the declaration. Non-exported symbols have E=0; R is +lexical evidence, not a claim that every token is a runtime call. Path importer +search `rg -l 'from .*pages/Storage["\x27]' src gui/src gui/tests scripts tests` +found 3 files: `gui/src/App.tsx:9`, `gui/tests/storage-loading-race.test.tsx:7`, +`gui/tests/storage-policy-metadata-warning.test.tsx:7`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| useCallback, useEffect, useRef, useState, KeyboardEvent | import bindings | 1–1 | no | dependency bindings | redistribute by own-import lists | +| useI18n, TFn, Locale | import bindings | 2–2 | no | dependency bindings | redistribute by own-import lists | +| EmptyState | import binding | 3–3 | no | dependency binding | original | +| IconRefresh | import binding | 4–4 | no | dependency binding | original | +| formatBytes | import binding | 5–5 | no | dependency binding | panel leaves | +| NumberStepper | import binding | 6–6 | no | dependency binding | #b storage-policy-view.tsx | +| clampNumberDraft | import binding | 7–7 | no | dependency binding | #b storage-policy-view.tsx | +| StorageWorkspace, StorageReport | import bindings | 8–10 | no | dependency bindings | original, unchanged public path | +| readSessionListCache, writeSessionListCache | import bindings | 11–11 | no | dependency bindings | original + #b storage-policy-panel.tsx | +| useDataSurface | import binding | 12–12 | no | dependency binding | original + #a storage-quarantine-panel.tsx | +| DataSurfaceSkeleton, DataSurfaceStatus | import bindings | 13–13 | no | dependency bindings | original + #a storage-quarantine-panel.tsx | +| CleanupPreview | interface | 16–22 | no | 0 / 2 | #a storage-archived-panel.tsx | +| CleanupResult | interface | 24–32 | no | 0 / 2 | #a storage-archived-panel.tsx | +| TrashEntry | interface | 34–41 | no | 0 / 9 | #a storage-cleanup-contracts.ts | +| TrashList | interface | 43–45 | no | 0 / 1 | #a storage-quarantine-panel.tsx | +| RestoreResult | interface | 47–54 | no | 0 / 2 | #a storage-quarantine-panel.tsx | +| GB | const | 56–56 | no | 0 / 4 | #b storage-policy-model.ts | +| CleanupPolicy | interface | 58–83 | no | 0 / 22 | #b storage-policy-model.ts | +| PRESETS | const tuple | 85–85 | no | 0 / 1 | #a storage-archived-panel.tsx | +| localizedCatch | const arrow function | 87–100 | no | 0 / 3 | #a storage-cleanup-error.ts | +| ArchivedCleanupPanel | component | 102–344 | no | 0 / 1 | #a storage-archived-panel.tsx | +| QuarantineTrashPanel | component | 346–569 | no | 0 / 1 | #a storage-quarantine-panel.tsx | +| policyFieldsFromResponse | function | 571–575 | no | 0 / 3 | #b storage-policy-model.ts | +| CachedCleanupPolicy | type | 577–583 | no | 0 / 2 | #b storage-policy-model.ts | +| draftsFromPolicyResponse | function | 585–603 | no | 0 / 1 | #b storage-policy-model.ts | +| sleep | async function | 605–607 | no | 0 / 1 | #b storage-policy-model.ts | +| AutoCleanupPolicyPanel | component | 609–1227 | no | 0 / 1 | #b storage-policy-panel.tsx + extracted storage-policy-view.tsx | +| StorageCleanupTab | type | 1229–1229 | no | 0 / 3 | #b storage-cleanup-card.tsx | +| StorageCleanupCard | component | 1231–1343 | no | 0 / 1 | #b storage-cleanup-card.tsx | +| Storage | default component | 1345–1469 | default | 3 / 0 | original | + +Both #a and #b reproduce the complete origin inventory intentionally. Rows marked +#a are already moved at #b's base, not a second move. #a takes zero-external-consumer +manual/quarantine leaves first; the policy's 22-reference contract and page resource +remain for #b. Each panel itself has one local JSX caller; ties are resolved by +dependency direction and preserving a complete panel lifetime. + +## Leaf partition + +All new leaves are siblings under `gui/src/pages/`, matching existing page-leaf +conventions. Preserve declarations, comments, endpoint strings, and hook order. + +| NEW file | symbols / origin body ranges | expected lines | own imports | +|---|---|---:|---| +| gui/src/pages/storage-archived-panel.tsx | CleanupPreview, CleanupResult (16–32); PRESETS (85); ArchivedCleanupPanel (102–344) | 268 | useCallback/useEffect/useRef/useState from react; Locale/TFn types from ../i18n/shared; formatBytes from ../format-bytes; localizedCatch from ./storage-cleanup-error | +| gui/src/pages/storage-quarantine-panel.tsx | TrashList, RestoreResult (43–54); QuarantineTrashPanel (346–569) | 246 | useCallback/useEffect/useRef/useState from react; Locale/TFn types from ../i18n/shared; formatBytes from ../format-bytes; TrashEntry type from ./storage-cleanup-contracts; localizedCatch from ./storage-cleanup-error; useDataSurface from ../data-surface; DataSurfaceSkeleton/DataSurfaceStatus from ../components/data-surface | +| gui/src/pages/storage-cleanup-contracts.ts | TrashEntry (34–41), named type export for internal consumers | 8 | none | +| gui/src/pages/storage-cleanup-error.ts | localizedCatch (87–100), named export for both panels | 14 | none | + +Line budgeting uses original body lines plus imports and separators, not compacted +code: archived 17 + 1 + 243 + 7 = 268; quarantine 12 + 224 + 10 = 246. +Residual `gui/src/pages/Storage.tsx` **947 expected lines**: +1469 − 40 (16–55) − 486 (85–570) + 4 (three imports and separator) = 947. +All original imports still used by policy/page/card stay in this intermediate +file. Original 56–84 (GB/CleanupPolicy), 571–1344 (policy helpers/panel/card) +and 1345–1469 (page) remain. **#b doc 600 takes the rest**, ending at 140 +expected residual lines. L2 source total: 947 + 268 + 246 + 8 + 14 = 1483. +The >400 residual is temporary and explicit; none of the four new files exceeds +400. The 526 original lines removed also independently exceed the 500 cap: +S17-SIZE-01 must be resolved, not hidden by ignoring types/comments/glue. + +## Re-export block + +Storage.tsx currently exports **only its default component** at 1345. +It remains declared there. Exact required re-export block: **empty**; adding +exports for formerly private panels/types would broaden the public API. + +Required local imports in Storage.tsx (re-exports would not bind these): + +```ts +import { ArchivedCleanupPanel } from "./storage-archived-panel"; +import { QuarantineTrashPanel } from "./storage-quarantine-panel"; +import type { TrashEntry } from "./storage-cleanup-contracts"; +``` + +Leaf exports are `export function ArchivedCleanupPanel`, +`export function QuarantineTrashPanel`, `export interface TrashEntry`, +and `export const localizedCatch`. They are internal direct-import seams; +do not re-export them from the page. Existing workspace default/StorageReport +imports remain at the public L1 path. + +## Module-level state and cycles + +There is no module-level mutable state, let, Map, Set, WeakMap or lock in +Storage.tsx. `GB:56` stays in the page until #b; `PRESETS:85` moves once to +storage-archived-panel.tsx. The arrow constant `localizedCatch:87` is stateless +code, owned only by storage-cleanup-error.ts. +Archived state/refs at 113–122 and effects at 130–146 move as one component. +Quarantine state/refs at 361–367, focus effect at 375–387, and resource at +389–404 move as one component. Keep callbacks/dependency arrays and active +resource keys intact. The page still owns report/trash coordination at +1347–1404; no singleton is created from these component-local refs. + +Potential cycles: panel → Storage for TrashEntry or localizedCatch would close +original → panel → original. Both dependencies therefore live in downward-only +leaves; neither leaf imports Storage or a component. Do not type-import through +the original. Existing session-list-cache stays its own owner, untouched. +Coupling is functional props/events; focus restore and busyRef effect ordering +are temporal and preserved, not rewritten. Card stays mounted/inert exactly as +before (1300–1335), preserving hidden panel state. + +## Tests + +Importing test files, exact `rg -l 'from .*pages/Storage["\x27]' gui/tests tests` +result (both remain unchanged, importing the original default): +- `gui/tests/storage-loading-race.test.tsx:7` — aborted request/loading test at + 80 and cached-report failed-revalidation test at 155. +- `gui/tests/storage-policy-metadata-warning.test.tsx:7` — outcome warning at + 62. Must still exercise the page, not a replacement mock leaf. + +The only source-text reader found by literal-path and extensionless searches is +`gui/tests/page-loading-contract.test.tsx`: path list entry at 43, actual +`Bun.file(new URL(path, import.meta.url)).text()` at 22, invoked at +51, 60, 67, 80, 95, and 111. Disposition: **unchanged** for Storage; the report +resource and its cold/stale/error/status rendering remain at +`gui/src/pages/Storage.tsx:1374–1453`. Do not retarget this entry to the policy +view (it does not own a data surface). **Add-leaf-to-scan-list**: +`../src/pages/storage-quarantine-panel.tsx` in MIGRATED, name +`StorageQuarantine`, to retain coverage of the resource moved from 397–402 and +the skeleton/status/error rendering moved from 473–485. Do not add the policy +panel to MIGRATED: its existing custom loading lifetime is not this contract. + +Implementation-only red-once proof: temporarily remove `.showSkeleton` access +from the quarantine leaf; the added entry must fail the existing cold-skeleton +guard. Restore it and record green. Similarly remove the original page's +`useDataSurface` identifier to demonstrate the unchanged page guard still scans +the original file. Never weaken the six assertions or concatenate unrelated +sources just to satisfy them. There is no source reader to retarget-to-leaf. + +#b must keep the added quarantine scan entry. Existing tests do not directly +import private manual/quarantine components; do not claim source pinning alone +proves digest, focus restoration, or restore behavior. At implementation add +focused page-driven cases in existing `gui/tests/storage-loading-race.test.tsx` +for stale preview → re-preview, cancel/focus restoration, restore success → +onDone refresh, and hidden quarantine state preservation. Use fake fetch/DOM +fixtures only, never real cleanup endpoints. These are proposed coverage, not +already-existing tests or a new test-file requirement. + +## Verification + +Future executor only; no test/build/scanner was run in this docs task. + +```sh +bun run typecheck +bun test gui/tests/storage-loading-race.test.tsx gui/tests/storage-policy-metadata-warning.test.tsx gui/tests/page-loading-contract.test.tsx +bun run privacy:scan +wc -l gui/src/pages/Storage.tsx gui/src/pages/storage-archived-panel.tsx gui/src/pages/storage-quarantine-panel.tsx gui/src/pages/storage-cleanup-contracts.ts gui/src/pages/storage-cleanup-error.ts +rg -l 'from .*pages/Storage["\x27]' src gui/src gui/tests scripts tests +git diff --check +git diff --numstat codex/split-components-storage-workspace-StorageWorkspace...HEAD +(cd gui && bun run build && bun run lint) +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-Storage-a && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && cd gui && bun test tests && bun run lint && bun run build' +``` + +002's conditional core-lab test: not applicable (GUI only; protected roots +untouched). Domains: GUI storage/loading contracts; full repository/GUI suite +only on lidge. Parent checks remote worktree ownership before using that runner, +records remote HEAD equal to layer tip, retains full logs/exit status, and +requires exact-head CI. Original-path importers remain the same three files. +Walk resolved static import and export edges, including type-only edges, for the +page and four new leaves; no SCC may include them. Fresh browser smoke covers +manual/quarantine tabs, modal cancel/focus and rescan without real deletion. +No UI copy/locale changes; preserve all current keys. + +## Accept criteria + +1. Four new leaves ≤400 lines each; original expected 947 with doc 600 explicitly + responsible for the remaining >400 residual. +2. Every origin top-level definition has exactly one owner; private DTOs and + localizedCatch are moved, not duplicated; default export stays at original. +3. Three existing public importer files unchanged; no circular static/type-only + dependency and no leaf → Storage import. +4. Six loading source guards retain their assertions; quarantine added to scan + list, red once then green; page source entry unchanged. +5. Focused behavioral/negative cases, typecheck, GUI build/lint, privacy, + remote full suite and exact-head CI all recorded at this layer tip. +6. Parent resolves S17-SIZE-01 before this can be marked implementation-ready. + No external mutation, extra layer, or 002 edit is authorized by this draft. + +## PR + +Title: `refactor(gui-storage): isolate manual cleanup and quarantine panels (split S17 L2/3)` +Base: `codex/split-components-storage-workspace-StorageWorkspace`. +Branch: `codex/split-pages-Storage-a`. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S17 L1/3 | # | codex/split-components-storage-workspace-StorageWorkspace | dev | Diagnostic panel and workspace DTOs | +| S17 L2/3 | # | codex/split-pages-Storage-a | codex/split-components-storage-workspace-StorageWorkspace | Manual cleanup and quarantine leaves | +| S17 L3/3 | # | codex/split-pages-Storage-b | codex/split-pages-Storage-a | Policy ownership and cleanup composition | + +Review only the diff against the named base. Merge bottom-up only with separate +authorization; no merge or auto-merge is authorized by this plan. A lower-layer +change requires a verified cascade and renewed exact-head gates for upper layers. +Fill Summary, Verification, and Checklist from the repository PR template; because +this is GUI scope, attach unchanged-layout screenshot evidence in the eventual PR. +Closes: none. diff --git a/devlog/_plan/260905_now_split_train/600_pages_Storage_b.md b/devlog/_plan/260905_now_split_train/600_pages_Storage_b.md new file mode 100644 index 0000000000..cbf5db2fa6 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/600_pages_Storage_b.md @@ -0,0 +1,297 @@ +# 600 — S17 L3/3: Storage part b + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**; C3 module-boundary planning, docs-only delegated mode. +Non-goals: no cache/poll rewrite, changed validation, endpoint, deadline, cancellation, +outcome precedence, i18n key, page loading behavior, or new service abstraction. +Goal: finish Storage's decomposition by moving automatic-policy ownership and +cleanup composition, retaining the page report resource in the original. +Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. +Stop: final files ≤400, all mechanical checks green, and parent resolution of +size/pure-move interpretation; eventual open exact-head-green PR, never merge. +Escalate if the parent interprets pure-move as whole-declaration-only: the +619-line AutoCleanupPolicyPanel cannot be relocated whole into a ≤400-line leaf. +This plan names the minimal JSX render seam and explicit lexical dependencies; +it does not grant permission for arbitrary controller redesign. +**Escalation S17-SIZE-01:** 002 fixes only two Storage layers at ≤500 changed +source lines each, but 1469 − 400 = 1069 original lines must leave the page even +before import/wrapper overhead. Thus no honest two-layer partition can satisfy +that cap, even under the generous count-once definition of a move. Ordinary +added+deleted diff accounting is larger still. This document supplies the +requested concrete partition, not an approved exception. Parent must authorize +a size exception or expand/remap the stack; do not invent a fourth document, +change 002, or claim this layer is implementation-ready. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; +docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. All source ranges below +are inclusive origin/dev ranges, not post-move positions. Read with `git show +origin/dev:`; `git diff origin/dev -- gui/src/pages/Storage.tsx +gui/src/components/storage-workspace/StorageWorkspace.tsx` was empty. +Declaration endpoints were checked with `sg run --kind --json=compact +`, and top-level starts with anchored rg. No code or test execution occurred. + +Read predecessor 590 first. Evidence: `015_lane_gui.md:131–143,508,511,604`. +Current at #a tip: page contains policy component and cleanup-card; manual and +quarantine leaves already exist. Intended: page → cleanup-card → panels; +policy-panel → policy-model and stateless policy-view; view → policy-model type. +Existing resource/cache modules remain dependencies, not new state owners. +Rejected: move all 619 lines to an oversized leaf; extract a new global service; +move the report resource away from the source oracle; or split rendering into +nested component definitions that remount on every render. +Chosen: keep hooks together in the same named component, move its hook-free +render tail to a plain named render function with an explicit argument record. +Consequence: internal argument glue only, zero public contract change; behavior +verification must cover lexical captures, timing, and outcome ordering. + +## Symbol inventory + +For every row, ranges refer to `gui/src/pages/Storage.tsx` at origin/dev. +Consumer notation `E / R`: E = distinct external importing files, R = local +identifier-reference occurrences from `git show origin/dev: | rg -o -w +`, excluding the declaration. Non-exported symbols have E=0; R is +lexical evidence, not a claim that every token is a runtime call. Path importer +search `rg -l 'from .*pages/Storage["\x27]' src gui/src gui/tests scripts tests` +found 3 files: `gui/src/App.tsx:9`, `gui/tests/storage-loading-race.test.tsx:7`, +`gui/tests/storage-policy-metadata-warning.test.tsx:7`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| useCallback, useEffect, useRef, useState, KeyboardEvent | import bindings | 1–1 | no | dependency bindings | redistribute by own-import lists | +| useI18n, TFn, Locale | import bindings | 2–2 | no | dependency bindings | redistribute by own-import lists | +| EmptyState | import binding | 3–3 | no | dependency binding | original | +| IconRefresh | import binding | 4–4 | no | dependency binding | original | +| formatBytes | import binding | 5–5 | no | dependency binding | panel leaves | +| NumberStepper | import binding | 6–6 | no | dependency binding | #b storage-policy-view.tsx | +| clampNumberDraft | import binding | 7–7 | no | dependency binding | #b storage-policy-view.tsx | +| StorageWorkspace, StorageReport | import bindings | 8–10 | no | dependency bindings | original, unchanged public path | +| readSessionListCache, writeSessionListCache | import bindings | 11–11 | no | dependency bindings | original + #b storage-policy-panel.tsx | +| useDataSurface | import binding | 12–12 | no | dependency binding | original + #a storage-quarantine-panel.tsx | +| DataSurfaceSkeleton, DataSurfaceStatus | import bindings | 13–13 | no | dependency bindings | original + #a storage-quarantine-panel.tsx | +| CleanupPreview | interface | 16–22 | no | 0 / 2 | #a storage-archived-panel.tsx | +| CleanupResult | interface | 24–32 | no | 0 / 2 | #a storage-archived-panel.tsx | +| TrashEntry | interface | 34–41 | no | 0 / 9 | #a storage-cleanup-contracts.ts | +| TrashList | interface | 43–45 | no | 0 / 1 | #a storage-quarantine-panel.tsx | +| RestoreResult | interface | 47–54 | no | 0 / 2 | #a storage-quarantine-panel.tsx | +| GB | const | 56–56 | no | 0 / 4 | #b storage-policy-model.ts | +| CleanupPolicy | interface | 58–83 | no | 0 / 22 | #b storage-policy-model.ts | +| PRESETS | const tuple | 85–85 | no | 0 / 1 | #a storage-archived-panel.tsx | +| localizedCatch | const arrow function | 87–100 | no | 0 / 3 | #a storage-cleanup-error.ts | +| ArchivedCleanupPanel | component | 102–344 | no | 0 / 1 | #a storage-archived-panel.tsx | +| QuarantineTrashPanel | component | 346–569 | no | 0 / 1 | #a storage-quarantine-panel.tsx | +| policyFieldsFromResponse | function | 571–575 | no | 0 / 3 | #b storage-policy-model.ts | +| CachedCleanupPolicy | type | 577–583 | no | 0 / 2 | #b storage-policy-model.ts | +| draftsFromPolicyResponse | function | 585–603 | no | 0 / 1 | #b storage-policy-model.ts | +| sleep | async function | 605–607 | no | 0 / 1 | #b storage-policy-model.ts | +| AutoCleanupPolicyPanel | component | 609–1227 | no | 0 / 1 | #b storage-policy-panel.tsx + extracted storage-policy-view.tsx | +| StorageCleanupTab | type | 1229–1229 | no | 0 / 3 | #b storage-cleanup-card.tsx | +| StorageCleanupCard | component | 1231–1343 | no | 0 / 1 | #b storage-cleanup-card.tsx | +| Storage | default component | 1345–1469 | default | 3 / 0 | original | + +Both #a and #b reproduce the complete origin inventory intentionally. Rows marked +#a are already moved at #b's base, not a second move. #a takes zero-external-consumer +manual/quarantine leaves first; the policy's 22-reference contract and page resource +remain for #b. Each panel itself has one local JSX caller; ties are resolved by +dependency direction and preserving a complete panel lifetime. + +## Leaf partition + +New files are `gui/src/pages/` siblings; four #a leaves are reused, not recreated. + +| NEW file | symbols / exact origin ranges | expected lines | own imports | +|---|---|---:|---| +| gui/src/pages/storage-policy-model.ts | GB/CleanupPolicy (56–83), policyFieldsFromResponse/CachedCleanupPolicy/draftsFromPolicyResponse/sleep (571–607) | 66 | none; window in sleep remains a call-time global | +| gui/src/pages/storage-policy-panel.tsx | AutoCleanupPolicyPanel signature + hooks/handlers (609–925); new call to renderAutoCleanupPolicyView | 331 | useCallback/useEffect/useRef/useState from react; Locale/TFn types from ../i18n/shared; readSessionListCache/writeSessionListCache from ../session-list-cache; formatBytes from ../format-bytes; GB/policyFieldsFromResponse/draftsFromPolicyResponse/sleep and CleanupPolicy/CachedCleanupPolicy types from ./storage-policy-model; renderAutoCleanupPolicyView from ./storage-policy-view | +| gui/src/pages/storage-policy-view.tsx | NEW renderAutoCleanupPolicyView enclosing unchanged render tail 926–1227, including local formatWhen and early returns | 350 | Locale/TFn types from ../i18n/shared; formatBytes from ../format-bytes; NumberStepper from ../components/NumberStepper; clampNumberDraft from ../clamp-draft; CleanupPolicy type from ./storage-policy-model | +| gui/src/pages/storage-cleanup-card.tsx | StorageCleanupTab and StorageCleanupCard (1229–1343) | 122 | useRef/useState and KeyboardEvent type from react; Locale/TFn types from ../i18n/shared; TrashEntry type from ./storage-cleanup-contracts; ArchivedCleanupPanel from ./storage-archived-panel; QuarantineTrashPanel from ./storage-quarantine-panel; AutoCleanupPolicyPanel from ./storage-policy-panel | + +Model budget = 28 + 37 + one separator = 66. +Policy component budget = 317 preserved signature/body lines + 14 import/call +glue = 331. Render budget = 302 preserved tail lines + 48 signature/import glue += 350. Card budget = 115 + 7 = 122. These are expected physical counts, not +permission to compress existing code; final wc must verify ≤400 for each. + +The view argument record lists **all** lexical captures: +`locale, t, policy, loading, saving, running, status, error, targetMode, percent, +reduceGb, thresholdGb, setTargetMode, setPercent, setReduceGb, setThresholdGb, +markDirty, setEditing, savePolicy, runNow`. +Use the existing types: policy is `CleanupPolicy | null`, status/error +`string | null`, targetMode `"percent" | "reduce"`, draft strings remain +strings; setters take their respective value type, markDirty returns void, +setEditing takes boolean, savePolicy takes `Partial?` and +returns Promise, runNow returns Promise. Locale and TFn come from +the existing i18n owner. Define this argument type inline in the view signature, +not as a second state model. Call the function after all hooks, passing the +same render's bindings. It has no hooks or state and must not be defined inside +the policy component. Do not memoize, debounce, reorder, or rewrite event closures. + +Residual `gui/src/pages/Storage.tsx`: **140 expected lines**: +#a's 947 − 29 (56–84) − 774 (571–1344) − 5 obsolete import lines + 1 +cleanup-card import = 140. Replace line-1/2 imports in place to drop now-unused +useEffect/KeyboardEvent/TFn/Locale, remove formatBytes/NumberStepper/clampNumberDraft +and the two #a panel imports. Keep TrashEntry, useI18n, EmptyState, IconRefresh, +workspace public imports, session cache, useDataSurface, skeleton/status. +Original default page body 1345–1469 remains unchanged. + +Consistent stack accounting: Storage 1469 → #a 947 → #b 140; #a leaves +268 + 246 + 8 + 14 = 536; #b leaves 66 + 331 + 350 + 122 = 869. +Final Storage family estimate 140 + 536 + 869 = 1545 (76 net glue lines versus +1469); workspace family 358 + 85 + 232 = 675. **Ten new files total for S17**; +zero final residual files >400; one temporary >400 residual after #a. +#b removes 803 original lines before import cleanup, so cannot meet 002's 500 +cap even if a moved line is counted only once. No further #c is silently assumed. + +## Re-export block + +The exact required re-export block is **empty**: the only current export, +`export default function Storage`, remains at the original path. + +Residual local imports added/retained: + +```ts +import { StorageCleanupCard } from "./storage-cleanup-card"; +import type { TrashEntry } from "./storage-cleanup-contracts"; +``` + +The card imports its three panels directly as listed above. Model named exports: +GB, policyFieldsFromResponse, draftsFromPolicyResponse, sleep, and type exports +CleanupPolicy/CachedCleanupPolicy; the private StorageCleanupTab stays in its +card leaf. The view exports renderAutoCleanupPolicyView; the panel exports +AutoCleanupPolicyPanel. None is re-exported from Storage.tsx and none imports +Storage.tsx. Keep the existing workspace default/type import unchanged. +Re-exporting any internal name would neither provide the card's local binding nor +preserve the original public surface accurately. + +## Module-level state and cycles + +No top-level let/Map/Set/WeakMap/lock exists. GB at 56 moves once into +storage-policy-model.ts; PRESETS at 85 already belongs to the #a archived leaf. +localizedCatch at 87 already belongs to #a cleanup-error; it is stateless. +The policy cacheKey at 620 is per render; read/writeSessionListCache keeps its +existing external owner and invocation timing. No new module singleton or +module-initialization browser access is introduced. + +All policy hooks/refs 620–642 remain in AutoCleanupPolicyPanel: +hasCacheRef, policy/loading/saving/running/status/error/drafts, runAbortRef, +dirtyRef, editingRef, loadGenerationRef. Effects at 688–705 and handlers at +644–924 remain together. Keep delayed load cancellation, generation increments, +dirty/focused-draft protection, 250ms polling and 120000ms deadline. Preserve +runNow's save → start → observe order and metadata-warning precedence at +890–915 exactly. These are temporal dependencies, not a new shared-state contract. +Card refs/tab state at 1250–1255 move together; hidden panels stay mounted/inert. +Report/trash coordination remains in Storage (1347–1404). + +Avoid card → panel → card and panel → view → panel: share CleanupPolicy directly +through policy-model, pass callbacks and values to the renderer, and never +import the panel's type via the original/card. The view's argument object is +functional coupling with all fields actually used, not an entire page controller. +Recheck resolved static import/export SCCs including type-only edges for all +S17 leaves; lane evidence is a starting point, not a post-move proof. + +## Tests + +Importing test files, exact `rg -l 'from .*pages/Storage["\x27]' gui/tests tests` +result (both remain unchanged, importing the original default): +- `gui/tests/storage-loading-race.test.tsx:7` — aborted request/loading test at + 80 and cached-report failed-revalidation test at 155. +- `gui/tests/storage-policy-metadata-warning.test.tsx:7` — outcome warning at + 62. Must still exercise the page, not a replacement mock leaf. + +The only source-text reader found by literal-path and extensionless searches is +`gui/tests/page-loading-contract.test.tsx`: path list entry at 43, actual +`Bun.file(new URL(path, import.meta.url)).text()` at 22, invoked at +51, 60, 67, 80, 95, and 111. Disposition: **unchanged** for Storage; the report +resource and its cold/stale/error/status rendering remain at +`gui/src/pages/Storage.tsx:1374–1453`. Do not retarget this entry to the policy +view (it does not own a data surface). **Add-leaf-to-scan-list**: +`../src/pages/storage-quarantine-panel.tsx` in MIGRATED, name +`StorageQuarantine`, to retain coverage of the resource moved from 397–402 and +the skeleton/status/error rendering moved from 473–485. Do not add the policy +panel to MIGRATED: its existing custom loading lifetime is not this contract. + +Implementation-only red-once proof: temporarily remove `.showSkeleton` access +from the quarantine leaf; the added entry must fail the existing cold-skeleton +guard. Restore it and record green. Similarly remove the original page's +`useDataSurface` identifier to demonstrate the unchanged page guard still scans +the original file. Never weaken the six assertions or concatenate unrelated +sources just to satisfy them. There is no source reader to retarget-to-leaf. + +In this layer the #a quarantine scan-list addition is already present and stays +unchanged. Retarget-to-leaf: none. The policy view does not replace the original +page source in any existing test. + +Drive `gui/tests/storage-policy-metadata-warning.test.tsx:62` red once by +temporarily suppressing the metadataPersistenceError branch at original 892–894 +in the moved controller, restore, then record green. Extend the existing file's +fake-fetch cases for disabled policy remaining disabled, 409 already-running, +invalid draft preventing PUT/start, run polling cancellation on unmount, stale +GET not overwriting edits, and matched-job completion. These are proposed focused +coverage additions, not claims that current tests cover every policy branch. +The JSX-tail extraction must retain Enter/composition guards, blur-inside-wrapper +behavior, radio draft values, disabled controls and status/error live-region roles. +Retain #a page-driven manual/quarantine checks. No new test file is required. + +## Verification + +Future executor only. All command results remain pending in this docs task. + +```sh +bun run typecheck +bun test gui/tests/storage-loading-race.test.tsx gui/tests/storage-policy-metadata-warning.test.tsx gui/tests/page-loading-contract.test.tsx +bun run privacy:scan +wc -l gui/src/pages/Storage.tsx gui/src/pages/storage-policy-model.ts gui/src/pages/storage-policy-panel.tsx gui/src/pages/storage-policy-view.tsx gui/src/pages/storage-cleanup-card.tsx gui/src/pages/storage-archived-panel.tsx gui/src/pages/storage-quarantine-panel.tsx gui/src/pages/storage-cleanup-contracts.ts gui/src/pages/storage-cleanup-error.ts +rg -l 'from .*pages/Storage["\x27]' src gui/src gui/tests scripts tests +git diff --check +git diff --numstat codex/split-pages-Storage-a...HEAD +(cd gui && bun run build && bun run lint) +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-Storage-b && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && cd gui && bun test tests && bun run lint && bun run build' +``` + +Domains: GUI storage policy/loading. No core protected root is touched; 002's +conditional core-lab test is not applicable. No local full suite. Parent verifies +remote runner ownership and exact HEAD before the full suite and retains full +logs/exit code; an output tail alone is insufficient. Require fresh exact-head +CI, unchanged three public importer files, and no S17 SCC including type-only +imports. Browser smoke/unchanged screenshot covers policy drafts, save feedback, +tab keyboard navigation and run status using safe fixtures, not real cleanup. +UI-copy changes are forbidden; any necessary new copy escalates out of pure-move. + +## Accept criteria + +1. Four new #b leaves ≤400 each; four #a leaves unchanged ≤400; Storage ≤400 + (140 expected), workspace residual ≤400 (358 expected). No final >400 residual. +2. Origin inventory owners are unique and match both part documents; only the + default page export remains public and its three consumers do not change. +3. JSX tail and handlers preserve AST/ordering apart from wrapper/import glue; + the view has exactly the listed 20 captures and introduces no hooks/state. +4. No circular import/export including type-only edges, no leaf → page import, + no new module state/cache, and no changed component remount boundaries. +5. Source oracle remains at the original page plus #a quarantine scan entry; + warning and loading guards have fresh red/restored-green evidence. +6. Typecheck, focused tests, build/lint, privacy, remote full suite, and exact-head + CI are green at this layer tip; original route/loading behavior unchanged. +7. Parent explicitly resolves S17-SIZE-01 and accepts the pure JSX render + extraction before implementation. Otherwise this is a blocked plan, not + a claim that the three-layer map can satisfy all its own constraints. + +## PR + +Title: `refactor(gui-storage): isolate policy rendering and cleanup composition (split S17 L3/3)` +Base: `codex/split-pages-Storage-a`. Branch: `codex/split-pages-Storage-b`. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S17 L1/3 | # | codex/split-components-storage-workspace-StorageWorkspace | dev | Diagnostic panel and workspace DTOs | +| S17 L2/3 | # | codex/split-pages-Storage-a | codex/split-components-storage-workspace-StorageWorkspace | Manual cleanup and quarantine leaves | +| S17 L3/3 | # | codex/split-pages-Storage-b | codex/split-pages-Storage-a | Policy ownership and cleanup composition | + +Review only the diff against the named base. Merge bottom-up only with separate +authorization; no merge or auto-merge is authorized by this plan. A lower-layer +change requires a verified cascade and renewed exact-head gates for upper layers. +Fill Summary, Verification, and Checklist from the repository PR template; because +this is GUI scope, attach unchanged-layout screenshot evidence in the eventual PR. +Closes: none. diff --git a/devlog/_plan/260905_now_split_train/610_pages_integrations_overview_clients.md b/devlog/_plan/260905_now_split_train/610_pages_integrations_overview_clients.md new file mode 100644 index 0000000000..bc10362404 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/610_pages_integrations_overview_clients.md @@ -0,0 +1,182 @@ +# S18 L1 — Overview contracts and primary row adapters + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only delegated task. No implementation, tests, Git mutation, or orchestration was performed here. +- Goal: reduce `gui/src/pages/integrations/overview-clients.ts` from 555 to an expected 378 physical lines, retaining all 15 current exports at that path. Move existing contracts and the Codex/credential row adapters; no new behavior. +- Non-goals: native toggle policy, journal-map relocation, client ordering, translation changes, cache policy, API changes, merge/release, or decomposition of long functions left within the limit. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below; this document is a plan, not a green gate receipt. +- Stop: two leaves and the residual satisfy size/export/oracle checks and the layer's exact-head checks are recorded. Stop and re-inventory if the source changes before implementation. +- Escalation: changes beyond these moves, changed API semantics, cycles requiring another owner, or a measured source diff above 500 lines go to the parent before expanding the layer. S18 L2's remaining component debt is not resolved by L1. + +Basis: docs HEAD `4cc219549`; code `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`. All source line ranges below refer to that code revision. Read `000_plan.md`, `001_stale_check.md`, the S18 rows and gate in `002_layer_map.md`, and `015_lane_gui.md:359–370`. The lane names the adapter seam and specifically leaves ordered assembly and `JOURNAL_KIND_KEY` at the existing boundary. + +Structural decision: the file combines DTOs, source-specific row adapters, and ordered aggregation. Do nothing/configure cannot satisfy the size goal; deleting mappings changes behavior. Reusing `integration-api.ts`, `native-api.ts`, `cursor-api.ts`, and `IntegrationStateBadge.tsx` contracts is retained, not replaced. Moving every native adapter would require a larger diff; moving types alone leaves about 447 lines after compatibility exports. Chosen move is contracts plus the two primary-surface adapters, with no external importer migration. Consequence: a type-only contract leaf becomes the single owner of shared types, and the original module imports the adapter leaf without a back-edge. + +Current map: `IntegrationsOverview.tsx:15`, `RollbackHistory.tsx:23`, `components/integration-marks.ts:18`, and three GUI test files import the original boundary; the boundary depends on the five existing contract/API owners at lines 15–24. Intended map: those six importers → unchanged boundary → `overview-primary-rows.ts` → `overview-client-types.ts` → existing contracts. Blast radius: local integrations feature and its existing type consumers. No package export, route, or wire contract changes. Sibling naming follows `integration-api.ts`, `native-api.ts`, `cursor-api.ts`, and `refusal-copy.ts`; no new internal `index.ts`. + +## Symbol inventory + +Ranges were checked with `git show origin/dev: | nl -ba` and `sg run --lang typescript --kind function_declaration/interface_declaration --json=compact --stdin`; `rg` covers type aliases and constants. Inventory covers all 25 top-level owned declarations, not imported bindings. Consumer counts are distinct external files from `rg -l -w '' src gui/src scripts tests gui/tests`, excluding this source, then resolving imports versus comments/homonyms. `0` does not mean unused inside this module. + +Target abbreviations: `types` = `gui/src/pages/integrations/overview-client-types.ts`; `primary` = `gui/src/pages/integrations/overview-primary-rows.ts`; `residual` = original path. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| OverviewClientId | type alias | 26–32 | yes, type | 1 importer; 2 name-hit files | types | +| ApiKeyReadPhase | type alias | 35–35 | yes, type | 1 | types | +| ApiKeysOverviewRow | interface | 49–55 | yes, type | 1 | types | +| OverviewRows | interface | 57–60 | yes, type | 0 | types | +| OverviewRow | interface | 62–95 | yes, type | 1 | types | +| CodexRoutingPayload | interface | 98–102 | yes, type | 0 | types | +| ClaudeCodePayload | interface | 103–106 | yes, type | 0 | types | +| ClaudeDesktopPayload | interface | 107–117 | yes, type | 0 | types | +| GrokPayload | interface | 118–121 | yes, type | 0 | types | +| OverviewSources | interface | 123–140 | yes, type | 3 | types | +| FILE_LABEL_KEY | const record | 142–155 | no | 0 | residual | +| JOURNAL_KIND_KEY | const record | 165–176 | yes, value | 1 importer + 1 text oracle | residual | +| isAppliedState | function | 178–180 | yes, value | 0 | residual | +| codexRow | function | 191–224 | no | 0 | primary | +| keysRow | function | 227–250 | no | 0 importers; 2 property/local-name hits | primary | +| claudeDetailKey | function | 253–263 | no | 0 | residual | +| claudeRow | function | 265–302 | no | 0 | residual | +| claudeDesktopRow | function | 314–384 | no | 0 | residual | +| grokDetail | function | 394–401 | no | 0 | residual | +| grokRow | function | 403–437 | no | 0 | residual | +| cursorRow | function | 444–468 | no | 0 importers; 1 local-name hit | residual | +| fileRow | function | 470–488 | no | 0 | residual | +| buildOverviewRows | function | 495–539 | yes, value | 4 | residual | +| OverviewCounts | interface | 541–546 | yes, type | 0 | types | +| countOverviewRows | function | 548–555 | yes, value | 2 | residual | + +`integration-marks.test.ts:22` only mentions `OverviewClientId` in prose; it imports the marks owner, not this type. `keysRow` external hits refer to the result property/local variable, and the `cursorRow` hit is a test-local variable. The actual external imports are three production files plus three tests, not all seven basename-hit files (the seventh is the journal source reader). + +## Leaf partition + +1. **`gui/src/pages/integrations/overview-client-types.ts` — expected 128 lines.** Move source 26–140 (115 lines, including inter-declaration comments) and 541–546 (6), separated by one blank line, after the following five import lines and one blank line. Symbols: all 11 type exports in the inventory. Keep fields, optionality, comments, and unions verbatim. + + ```ts + import type { TKey } from "../../i18n/shared"; + import type { VisualIntegrationState } from "./IntegrationStateBadge"; + import type { FileIntegrationClientId, IntegrationStatus } from "./integration-api"; + import type { NativeIntegrationClientId, NativeStatus } from "./native-api"; + import type { CursorIntegrationStatus } from "./cursor-api"; + ``` + +2. **`gui/src/pages/integrations/overview-primary-rows.ts` — expected 72 lines.** Move source 182–250 (69 lines, including the Codex and key comments), adding `export` to the existing `codexRow` and `keysRow` declarations solely for the original owner to import. Two import lines plus one blank line: + + ```ts + import type { TKey } from "../../i18n/shared"; + import type { ApiKeyReadPhase, ApiKeysOverviewRow, CodexRoutingPayload, OverviewRow } from "./overview-client-types"; + ``` + +3. **Residual `gui/src/pages/integrations/overview-clients.ts` — expected 378 lines.** Keep header, `FILE_LABEL_KEY`, `JOURNAL_KIND_KEY`, `isAppliedState`, all Claude/Desktop/Grok/Cursor/file adapters, ordered `buildOverviewRows`, and `countOverviewRows`. Keep original imports at 15–24 except drop the unused `NativeIntegrationClientId` binding from line 23 (retain `NativeStatus`). Add exactly the 13 one-line imports/re-exports below, using existing surrounding blank lines. + +Arithmetic: `555 - 115 - 6 - 69 + 13 = 378`; leaves `128 + 72`; total planned source `578`, versus `555` before (23 lines of wiring/separation). Expected added+deleted source diff is about 405 lines, with a 450-line planning allowance; measure it at implementation, including tests. Both leaves and residual are ≤400, no `#b` for this file. This is not an `#a/#b` series; the chosen executable leaves have zero external consumers and leave the higher-fan-in aggregation/map stable. + +## Re-export block + +Exact additions to the original path (no `export *`, no exported identifier renames): + +```ts +export type { OverviewClientId } from "./overview-client-types"; +export type { ApiKeyReadPhase } from "./overview-client-types"; +export type { ApiKeysOverviewRow } from "./overview-client-types"; +export type { OverviewRows } from "./overview-client-types"; +export type { OverviewRow } from "./overview-client-types"; +export type { CodexRoutingPayload } from "./overview-client-types"; +export type { ClaudeCodePayload } from "./overview-client-types"; +export type { ClaudeDesktopPayload } from "./overview-client-types"; +export type { GrokPayload } from "./overview-client-types"; +export type { OverviewSources } from "./overview-client-types"; +export type { OverviewCounts } from "./overview-client-types"; +import type { ClaudeCodePayload, ClaudeDesktopPayload, GrokPayload, OverviewRow, OverviewRows, OverviewSources, OverviewCounts } from "./overview-client-types"; +import { codexRow, keysRow } from "./overview-primary-rows"; +``` + +The explicit local imports are necessary: the re-exports bind nothing locally. `JOURNAL_KIND_KEY`, `isAppliedState`, `buildOverviewRows`, and `countOverviewRows` remain their existing exported declarations; no forwarding lines for these are needed. Do not re-export the formerly private adapters from the original path. This compatibility façade is required by the train; it is not permission to create convenience barrels or route new leaf dependencies upward through it. + +## Module-level state and cycles + +- `FILE_LABEL_KEY` at 142–155: one record, owned by the residual, used by `fileRow` and unknown-file assembly. Do not duplicate it in a leaf. +- `JOURNAL_KIND_KEY` at 165–176: one record, owned by the residual; `RollbackHistory` keeps the same identity and path. No new eager execution. +- No top-level `let`, Map, Set, WeakMap, lock, timer, or mutable cache. `statusByClient = new Map(...)` at 499 is invocation-local inside `buildOverviewRows`, stays there, and must not become a singleton. `Date.now()` at 444 remains evaluated on each `cursorRow` call. +- The type leaf must not import `overview-clients`: doing so would make `residual → primary → types → residual` a cycle. Both residual and primary import shared types directly from their owner. Existing contract dependencies are one-way (`integration-api.ts:1`, `native-api.ts:1`, `cursor-api.ts:5`, `IntegrationStateBadge.tsx:1–2`); none imports this model. Include type-only and re-export edges in the implementation cycle audit. +- Coupling remains functional (row adapters) and existing external DTO coupling; no new shared mutable or temporal coupling. No new validation, retries, or error swallowing. + +## Tests + +Discovery: `rg -l 'overview-clients' tests gui/tests` returns the following four files. Three import the module; one reads it as source. Also checked direct source consumers and their adjacent tests. + +| Test file | Exact dependency at origin/dev | Disposition | +|---|---|---| +| `gui/tests/integrations-overview-rows.test.ts` | import block 2–6 | unchanged; maps/null phases/counts/order exercised through original exports | +| `gui/tests/overview-state-merge.test.ts` | import at 2 | unchanged; original `buildOverviewRows`/`OverviewSources` path | +| `gui/tests/cursor-integration-page.test.tsx` | import at 5 | unchanged; original model exports and Cursor recency semantics | +| `tests/clients/integrations-journal.test.ts` | `readFileSync(repoPath("gui/src/pages/integrations/overview-clients.ts"), "utf8")` at 386; match/assert at 387–390 | unchanged; map remains physically in original file | + +Additional affected behavioral coverage: `gui/tests/integration-marks.test.ts` (imports marks owner at 4, not this module), and `gui/tests/integrations-surfaces.test.tsx` (dynamic page import at **545** in origin/dev, not working-tree line 509). Both unchanged. The upstream surfaces file is 69 lines ahead of docs HEAD; use the layer's origin/dev test version, not a copied docs-worktree file. + +No retarget-to-leaf or add-leaf-to-scan-list is needed in L1: the only exact source reader still reaches its owning declaration. Never replace its enum-completeness assertion with a re-export-presence assertion. + +Guards to drive red once during implementation C phase, then restore: remove the `overwrite` map member and run the journal copy test by name (must fail at the existing assertion); change the moved Codex adapter to key off `status` instead of `routingInjected` and run the matching existing row test (must fail). No mutation-test or test execution occurred during planning. + +## Verification + +Planning-only validation: a fresh read-only Node check confirmed the nine required headings in order, all 25 declarations, and the source-range arithmetic (555 → 378; leaves 128/72). These are document checks, not implementation/test results. + +Execute in the dedicated L1 implementation worktree, not this docs checkout. Instantiate 002's gate: + +```sh +bun run typecheck +bun test tests/clients/integrations-journal.test.ts +bun test gui/tests/integrations-overview-rows.test.ts gui/tests/overview-state-merge.test.ts gui/tests/cursor-integration-page.test.tsx gui/tests/integration-marks.test.ts gui/tests/integrations-surfaces.test.tsx +bun run privacy:scan +wc -l gui/src/pages/integrations/overview-client-types.ts gui/src/pages/integrations/overview-primary-rows.ts gui/src/pages/integrations/overview-clients.ts +rg -n 'from "[^\"]*/overview-clients"' src gui/src scripts tests gui/tests +git diff --numstat dev -- gui/src/pages/integrations tests/clients gui/tests +``` + +The importer command has **6** matching import-end lines before/after (three GUI source + three GUI tests). Keep file identity as well as count; no current import is migrated. The 002 command omits `gui/tests`, which would count only 3 here; record both baselines rather than silently excluding the GUI tests. Audit added leaf import/export edges against the acyclic map above; typecheck alone is not a cycle proof. A read-only relative-edge SCC scan including type imports must report no SCC containing a touched module. No `src/server`, `src/router`, or `src/lib` file is touched, so 002's conditional core-Lab test is not applicable; never edit its roots. + +GUI-specific build gate: `(cd gui && bun run lint && bun run build)`; no copy changes, so no translation changes or i18n-key migration. Before review-ready, the full GUI test gate also runs remotely, not as a repository-wide local test. + +Full suite **only on lidge**, using the 002 branch and checkout gate, with pipe failure preserved: + +```sh +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-integrations-overview-clients && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15"' +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile >/dev/null && bun test tests 2>&1 | tail -15"' +``` + +Parent serializes access to that shared remote checkout and records the same exact SHA for both remote runs and CI; a plain successful `tail` is not proof that tests passed. No test runs, dependency installs, or remote checkout changes are authorized by this delegated planning task. + +## Accept criteria + +1. Pinned source rechecked; all 25 inventory declarations have exactly one owner, preserving function bodies, comments, field order, and signatures. +2. Exactly two new source leaves, each ≤400 lines; residual ≤400 (expected 378). No unrelated source changes; measured source added+deleted total ≤500. +3. All 11 type and 4 value exports remain importable from `overview-clients`; the six existing importer files retain their paths. Formerly private functions are not added to that compatibility API. +4. `JOURNAL_KIND_KEY` remains at the original path, its source oracle unchanged and demonstrated red/green; moved Codex adapter test also demonstrated red/green. +5. No cycles through runtime, type-only, or re-export edges; no new state/cache owner; file-client ordering and credential/client separation unchanged. +6. Focused tests, root typecheck, GUI lint/build, privacy scan, remote full suites, and exact-head CI all pass with SHA-linked evidence before implementation completion/review readiness. +7. PR targets `dev`, contains the complete two-layer map and repository template, and is not merged. Planning completion itself claims only this document, not code delivery. + +## PR + +Title: `refactor(gui-integrations): separate overview contracts and primary row adapters (split S18 L1/2)` + +Branch: `codex/split-pages-integrations-overview-clients` + +Base: `dev` + +Closes: none. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S18 L1/3 | #TBD-L1 | `codex/split-pages-integrations-overview-clients` | `dev` | contracts and primary adapters; this layer | +| S18 L2/3 | #TBD-L2 | `codex/split-pages-integrations-IntegrationsOverview-a` | `codex/split-pages-integrations-overview-clients` | existing card/key views (620) | +| S18 L3/3 | #TBD-L3 | `codex/split-pages-integrations-IntegrationsOverview-b` | `codex/split-pages-integrations-IntegrationsOverview-a` | passthrough section extraction (625) | + +Use `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, Checklist; include unchanged-UI screenshot evidence because the title contains `gui`. Review only this layer's diff. Cascade L2 if L1 changes, with refreshed exact-head checks. Merge remains parent/user-authorized, bottom-up; this task creates no PR. diff --git a/devlog/_plan/260905_now_split_train/620_pages_integrations_IntegrationsOverview.md b/devlog/_plan/260905_now_split_train/620_pages_integrations_IntegrationsOverview.md new file mode 100644 index 0000000000..6e4f529048 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/620_pages_integrations_IntegrationsOverview.md @@ -0,0 +1,161 @@ +# S18 L2/3 — Existing overview presentation leaves (#a) + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`; C3 architecture planning, docs-only delegated task. +- Goal: extract the existing `OverviewCard` and `ApiKeysRow` components without changing their bodies, props, UI, or the page's resource/action lifetimes. Keep the existing default page export. +- Non-goals: rewriting the page controller, introducing a context/store, changing cache/refresh policy, moving confirmation ownership, changing bulk sequencing or upstream delete handling, code/test execution, or editing the train map. +- Verifier: `002_layer_map.md` **Per-layer gate**, amended by `003_parent_decisions.md` PURE-MOVE-SIZE-01 and GUI-SEAM-01, instantiated below. This is an intermediate layer; 625 brings the residual under 400. +- Stop: views moved and checks green for this layer, with the 619-line intermediate residual recorded. S18 file-size completion belongs to L3/3, not this layer. +- Escalation: any change beyond the approved views and the 625 continuation goes to the parent. Parent has approved S18 L3/3 (#b); no further map changes are authorized by this delegated task. + +Basis: docs HEAD `4cc219549`; source `origin/dev` = `1362b1a3841b4de20177e5d65865a513dd7936c4`. Read source with `git show origin/dev:gui/src/pages/integrations/IntegrationsOverview.tsx`, never the working copy. The actual source is **757** physical lines, not 748: `001_stale_check.md` records the +9 upstream change but repeats the old count. The function now spans **171–729 (559 lines)**. `isMissingJournalEntry` is imported at 32 and handles a missing journal row at 679–683. All line references below are origin/dev. Lane basis: `015_lane_gui.md:208–219`; it explicitly proposes views first, then action slices while preserving confirmation ownership, not that moving two views alone finishes this file. + +Structural decision: consumers are `gui/src/pages/Integrations.tsx:10` and the dynamic import in `gui/tests/integrations-surfaces.test.tsx:545`; the source reader is `gui/tests/integrations-cache-freshness.test.ts:19`. Existing page dependencies are React, data-surface, shared UI/i18n/routing/marks, model L1, integration/native/cursor API clients, and dialog/history components (source 1–41). Intended map: parent page → original default export → two presentation leaves; both leaves use existing contracts/UI owners, never the page. The original page keeps all data resources, mutations, and dialogs. Blast radius: local integrations feature; no route or package boundary changes. + +Rejected alternatives: do nothing/delete/configure cannot remove executable size debt without losing UI. Moving the whole 559-line component merely relocates the violation. Extracting resources/actions would alter lexical boundaries unnecessarily. The chosen first move is the two existing zero-external-consumer components; the higher-fan-in default page and its cache oracle stay stable. The original raw-diff size objection is superseded by 003 PURE-MOVE-SIZE-01 (≤150 non-move lines); GUI-SEAM-01 permits 625 to move the remaining JSX tree with props passed through, without extracting the controller. + +## Symbol inventory + +`git show origin/dev: | sg run --lang tsx --kind function_declaration --json=compact --stdin` gives the function spans; `rg`/numbered source gives the two const records. These are all **6 top-level owned declarations** (imports are wiring, listed under partition). `rg -l -w '' src gui/src scripts tests gui/tests` counts external files, then import resolution excludes homonyms/comments. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| GROK_DISABLE_COPY | const record | 43–49 | no | 0 | residual `IntegrationsOverview.tsx` | +| DESKTOP_DISABLE_COPY | const record | 51–58 | no | 0 | residual `IntegrationsOverview.tsx` | +| isApplied | function | 60–62 | no | 0 importers; 4 unrelated backend name-hit files | residual `IntegrationsOverview.tsx` | +| OverviewCard | component function | 77–169 | no | 0 | `gui/src/pages/integrations/OverviewCard.tsx` | +| IntegrationsOverview | component function | 171–729 | yes, default | 2 importers; 5 external name-hit files | residual `IntegrationsOverview.tsx` | +| ApiKeysRow | component function | 743–757 | no | 0 | `gui/src/pages/integrations/ApiKeysRow.tsx` | + +The five `IntegrationsOverview` name-hit files are the two importers, cache source reader, `integrations-routing.test.ts` (reads its parent), and a comment in `FileIntegrationPage.tsx`; those last three are not module importers. `isApplied` backend names are unrelated declarations, not consumers of this private function. + +## Leaf partition + +Existing PascalCase component siblings (`ConsequenceDialog.tsx`, `RestoreDialog.tsx`, `IntegrationStateBadge.tsx`, `RollbackHistory.tsx`) establish the naming pattern; exact-name search found no existing `OverviewCard`/`ApiKeysRow` owner outside this file. + +1. **`gui/src/pages/integrations/OverviewCard.tsx` — expected 115 lines.** Move source 64–169 inclusive (106 lines with its accessibility comment), prefix the existing function with `export`, and add these eight imports plus a blank line. Keep its existing inline props, refusal construction, switch guard, markup, and handler wiring exactly. + + ```ts + import { useT } from "../../i18n/shared"; + import { Notice, Switch } from "../../ui"; + import ClientMark from "../../components/ClientMark"; + import { markFor } from "../../components/integration-marks"; + import IntegrationStateBadge from "./IntegrationStateBadge"; + import { describeRefusal } from "./refusal-copy"; + import { NativeApiError } from "./native-api"; + import type { OverviewRow } from "./overview-client-types"; + ``` + +2. **`gui/src/pages/integrations/ApiKeysRow.tsx` — expected 32 lines.** Move source 730–757 inclusive (28 lines including the credential semantics comment), prefix the existing function with `export`, and add three imports plus a blank line: + + ```ts + import { navigateHash } from "../../hash-routing"; + import { useT } from "../../i18n/shared"; + import type { ApiKeysOverviewRow } from "./overview-client-types"; + ``` + +3. **Residual `gui/src/pages/integrations/IntegrationsOverview.tsx` — expected 619 lines.** Remove 64–170 (component/comment plus its following blank, 107 lines) and 730–757 (28). Remove the three full imports at 7–9 and the `ApiKeysOverviewRow`/`NativeApiError` import lines at 19/38. Change `{ Notice, Switch }` to `{ Notice }` at 6. Add the two explicit view imports below. All other original imports remain, including `navigateHash`, `describeRefusal`, `isMissingJournalEntry`, all resource types/loaders, and the React hooks. + +Arithmetic: `757 - 107 - 28 - 5 + 2 = 619`; new leaves `115 + 32 = 147`, total `766` (= original +9 net wiring lines). Raw added+deleted source estimate is about 292 lines. Per 003 PURE-MOVE-SIZE-01, enforce ≤150 non-move wiring/test lines and record moved lines separately. The two new leaves are under 400; **one intermediate residual remains over 400**, permitted by INTERMEDIATE-RESIDUAL-01 because L3/3 takes it to 398. + +**Approved #b:** `625_pages_integrations_IntegrationsOverview_b.md`, S18 L3/3, branch `codex/split-pages-integrations-IntegrationsOverview-b`, base `codex/split-pages-integrations-IntegrationsOverview-a`. It takes the 619-line residual to **398** by moving the complete existing return tree (origin/dev 519–728) and its two private dialog-copy records (43–59) into `IntegrationsOverviewContent.tsx`. All hooks and named action closures remain in the page; this uses GUI-SEAM-01, not a new resource/controller hook. This layer still plans only its two leaves; 625 adds one more. + +## Re-export block + +**No compatibility re-export statements are required.** The only current export is `default function IntegrationsOverview`, and it stays as an actual declaration in the original file. Moving the private views does not justify widening its public API. Specifically, do not add `export { OverviewCard }` or `export { ApiKeysRow }` to the original module and do not replace the page export with a wrapper. + +Exact explicit local imports added to the residual: + +```ts +import { OverviewCard } from "./OverviewCard"; +import { ApiKeysRow } from "./ApiKeysRow"; +``` + +The leaves export their respective existing component names so the page can bind them. Re-export syntax alone would not bind a local name. L1 keeps the page's existing `overview-clients` model imports working; the new leaves import the L1 type owner directly. No `export *` or convenience barrel. + +## Module-level state and cycles + +- `GROK_DISABLE_COPY` (43–49) and `DESKTOP_DISABLE_COPY` (51–58) are the only top-level const records. Both remain owned by the original page; never copy them to a view leaf. +- No top-level `let`, Map, Set, WeakMap, cache, lock, or timer. Page-local hooks at 179–188 and 430 remain per mounted page. `restoreFocusRef` at 188 and its effect at 190–196 stay with the pending native toggle; no hoisting to a module singleton or new mount boundary. +- Resources and their `enabled: active`/session keys remain at 198–297; refresh membership/order at 348–357 and 432–436 stays unchanged. In particular, do not add Cursor to the current refresh closure as opportunistic cleanup. +- Sequential file-only bulk disable at 364–421 remains in the page, including confirmation, serial awaits, server re-read, and localized partial results. `requestToggle` 481–492 and the delete/native/overwrite dialogs 661–726 remain together. Upstream `isMissingJournalEntry` handling at 679–683 stays untouched. +- New edges: page → views → L1 contract leaf/existing UI owners. No view imports the page. `OverviewCard → integration-marks → overview-clients → overview-primary-rows → overview-client-types` is one-way; neither L1 leaf imports a view or the page. Type-only edges count in the cycle audit. No new state owner or internal validation. +- Coupling: functional props and existing external API refusal types. Moving presentation alone does not introduce control flow back into the page beyond the already-existing callback props. The forbidden alternative is reading controller state by importing the page. + +## Tests + +Discovery: `rg -l 'IntegrationsOverview' tests gui/tests` returns exactly these three files; only the first imports this module. All test line references below are from origin/dev, not stale docs HEAD. + +| Test file | Exact dependency | Disposition | +|---|---|---| +| `gui/tests/integrations-surfaces.test.tsx` | dynamic `import("../src/pages/integrations/IntegrationsOverview")` at 545 | unchanged; covers page rendering, credential/client distinction, callbacks, bulk result confirmation and upstream journal-delete reconciliation at 558–588 | +| `gui/tests/integrations-cache-freshness.test.ts` | path array entry at 13; `readFileSync(new URL(\`../${path}\`, import.meta.url), "utf8")` at 19; assertions 20–21 | unchanged; all resource/cache declarations remain in residual | +| `gui/tests/integrations-routing.test.ts` | `Bun.file(..."../src/pages/Integrations.tsx"...).text()` at 125, child-name/prop assertions 129–135 | unchanged; reads parent, not the split source | + +There are no test imports of the private components. `page-loading-contract.test.tsx` was checked: its explicit pages at 34–46 do not include this overview, so no entry should be invented. Run the existing L1 model tests and `integration-marks.test.ts` as focused dependency coverage; their import paths stay unchanged. + +No retarget-to-leaf or add-leaf-to-scan-list in this bounded L2: neither new view owns a resource. Adding them to the cache oracle's `MIRROR_SURFACES` would incorrectly require a `sessionCacheKey` in stateless presentation. The approved 625 also leaves all resources in the original page, so its cache oracle remains unchanged too. + +Guards to drive red once during implementation C phase, then restore: insert a `staleAfterMs` option into one page resource and run `integrations-cache-freshness.test.ts` (fails its negative assertion); corrupt the moved key row's `data-key-state` and run the corresponding `integrations-surfaces.test.tsx` credential-state case (must fail); disable the retained missing-journal reconciliation branch and run `--test-name-pattern 'the overview reconciles a journal row another tab already deleted'` (must fail). The last is verification-only and must be fully restored; it is not permission to redesign deletion behavior. + +## Verification + +Planning-only validation: a fresh read-only Node check confirmed the nine required headings in order, all six declarations, and source-range arithmetic (757 → 619; leaves 115/32). The pinned-source test read also confirmed the existing key-state assertion at `gui/tests/integrations-surfaces.test.tsx:962` and the delete reconciliation case at 558–589. These are document checks, not test-pass claims. + +In the future dedicated L2 worktree based on L1, instantiate 002's gate: + +```sh +bun run typecheck +bun test gui/tests/integrations-surfaces.test.tsx gui/tests/integrations-cache-freshness.test.ts gui/tests/integrations-routing.test.ts +bun test gui/tests/integrations-overview-rows.test.ts gui/tests/overview-state-merge.test.ts gui/tests/cursor-integration-page.test.tsx gui/tests/integration-marks.test.ts +bun run privacy:scan +wc -l gui/src/pages/integrations/OverviewCard.tsx gui/src/pages/integrations/ApiKeysRow.tsx gui/src/pages/integrations/IntegrationsOverview.tsx +rg -n 'from "[^\"]*/IntegrationsOverview"|import\("[^\"]*/IntegrationsOverview"\)' src gui/src scripts tests gui/tests +git diff --numstat codex/split-pages-integrations-overview-clients -- gui/src/pages/integrations gui/tests +``` + +Importer baseline: **2 files** (one static production import, one dynamic GUI-test import). 002's static-only command over `src gui/src scripts tests` returns **1**, so preserve that baseline too and explicitly supplement it with the dynamic/GUI-test search. Both identities must remain unchanged. No core/server/lib changes; the conditional `tests/lab/core-lab-boundary.test.ts` gate is not applicable and its protected roots must not be edited. + +Audit relative runtime/type/re-export edges with a read-only SCC scan: no new cycle (003 TYPE-CYCLE-01 allows unchanged pre-existing type-only cycles). Typecheck does not establish absence of cycles. Per GUI-SEAM-01, run `bun run lint:gui` and `bun run build:gui`, and attach before/after screenshots. Record `git diff -M --stat` and use `git diff --color-moved=dimmed-zebra` with a symbol-owner check for PURE-MOVE-SIZE-01. Full GUI tests run on the remote host below. Preserve copy, CSS, and accessibility. + +Full suite **only on lidge**; same 002 remote gate, preserving pipeline failure and recording the exact head: + +```sh +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-integrations-IntegrationsOverview-a && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15"' +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile >/dev/null && bun test tests 2>&1 | tail -15"' +``` + +Parent coordinates exclusive use of the remote checkout, verifies both runs used this layer SHA, and records exact-head CI. No commands in this section were executed in the delegated docs task. Size gate outcome must be recorded as **two passing leaves, one explicitly deferred residual (619 → required #b)**, not all files passing. + +## Accept criteria + +1. Source basis is the 757-line origin/dev version, with current delete-journal reconciliation and the matching upstream regression test retained. +2. Exactly two new source files, expected 115 and 32 lines and each ≤400; original default export and its two importer identities remain unchanged. No new exports added to the original boundary. +3. Existing component bodies/comments/props move intact; only imports and leaf export modifiers change. No controller, hook order, effect dependencies, callback sequencing, dialog ownership, CSS, or locale changes. Measured non-move diff ≤150 lines under PURE-MOVE-SIZE-01, with move and unique-owner evidence. +4. Residual count is recorded (expected **619**, not ≤400). The approved 625 / S18 L3/3 brings it to **398**; do not claim terminal size completion before that layer. No extra branch or map expansion beyond the approved three layers. +5. Cache oracle remains on the original resource owner, all named tests stay intact, required guards have red/green evidence, and the delete 404 test is from origin/dev. +6. No new runtime/type-only/re-export cycle (003 TYPE-CYCLE-01); no new module-level state owner; all existing state remains per page instance. +7. Root typecheck, focused GUI tests, privacy scan, GUI lint/build, remote full suites, and exact-head CI pass before this bounded move is review-ready. Record the residual exception separately from check outcomes. +8. PR base is L1, map includes all three allocated layers, template is complete, before/after screenshot evidence is attached, and no merge occurs under this task's authority. + +## PR + +Title: `refactor(gui-integrations): extract overview card and credential views (split S18 L2/3)` + +Branch: `codex/split-pages-integrations-IntegrationsOverview-a` + +Base: `codex/split-pages-integrations-overview-clients` + +Closes: none. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S18 L1/3 | #TBD-L1 | `codex/split-pages-integrations-overview-clients` | `dev` | contracts and primary adapters | +| S18 L2/3 | #TBD-L2 | `codex/split-pages-integrations-IntegrationsOverview-a` | `codex/split-pages-integrations-overview-clients` | existing card/key views; this layer | +| S18 L3/3 | #TBD-L3 | `codex/split-pages-integrations-IntegrationsOverview-b` | `codex/split-pages-integrations-IntegrationsOverview-a` | return-tree leaf; residual ≤400 | + +Depends on #TBD-L1. Review this layer's diff only. Fill Summary/Verification/Checklist in `.github/PULL_REQUEST_TEMPLATE.md`; disclose the intermediate 619-line residual and approved 625 continuation, link `git diff --color-moved=dimmed-zebra` review guidance, and attach before/after screenshots. Cascade through L2 and L3 after an L1 update and refresh exact-head evidence. Merge is bottom-up and separately authorized; this task opens no PR. diff --git a/devlog/_plan/260905_now_split_train/625_pages_integrations_IntegrationsOverview_b.md b/devlog/_plan/260905_now_split_train/625_pages_integrations_IntegrationsOverview_b.md new file mode 100644 index 0000000000..3d719cb722 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/625_pages_integrations_IntegrationsOverview_b.md @@ -0,0 +1,239 @@ +# S18 L3/3 — Overview return-tree leaf (#b) + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: `pure-move`, under `003_parent_decisions.md` **GUI-SEAM-01**; C3, docs-only delegated planning. The JSX return tree, embedded callbacks, and two private copy records move verbatim. Props carry the same values/functions without changing the rendered DOM. +- Goal: finish `IntegrationsOverview.tsx` from the **619-line #a residual to 398 lines**, adding one 283-line sibling component. Across S18: five new source files total, zero final residuals over 400. +- Non-goals: extracting hooks or named action closures, moving state/cache ownership, changing deletion/refusal behavior, introducing context/memoization, changing DOM/CSS/i18n, modifying L1/L2 source, implementing the plan, test execution, or Git/orchestration commands. +- Verifier: `002_layer_map.md` **Per-layer gate**, amended by 003 **PURE-MOVE-SIZE-01**, **GUI-SEAM-01**, and **TYPE-CYCLE-01**, instantiated below. Non-move wiring/test diff must be ≤150 lines; moved code is checked as a verbatim relocation. +- Stop: one leaf and residual each ≤400, all old exports/importers preserved, unchanged render/action behavior verified, and exact-head gate evidence recorded by the executor. This planning task stops after document consistency checks only. +- Escalation: any moved expression/body change, new hook/state owner, non-move diff >150, actual residual >400, or new dependency cycle returns to the parent. No #c is planned or needed by this partition. + +Read first: `003_parent_decisions.md`, then current 002 S18 rows, 620, and the actual source. Docs HEAD is `4cc219549`; source `origin/dev` remains `1362b1a3841b4de20177e5d65865a513dd7936c4`. Every source citation below is `gui/src/pages/integrations/IntegrationsOverview.tsx` at **origin/dev**, unless another path is given. The original source is 757 lines, and the hypothetical 619-line #a residual has not been implemented in this docs checkout. Do not read the stale working-tree page or invent exact post-#a physical positions: ranges use the stable original-source coordinates, with #a's transformations applied in memory. + +Structural decision: the page mixes a state/resource/action controller (171–518) and a complete return tree (519–728). Existing dependents are `gui/src/pages/Integrations.tsx:10` and the dynamic import in `gui/tests/integrations-surfaces.test.tsx:545`. Existing downstream owners are `data-surface`, i18n/UI/routing, model L1, the #a card/key leaves, and integration dialogs/API clients. Intended direction: parent route → original page/controller → `IntegrationsOverviewContent.tsx` → existing view/dialog/API/type owners. No upward dependency from the new leaf. Blast radius: the same GUI feature; no route, package, or management API contract changes. + +Rejected alternatives: deleting/configuring away UI loses behavior; moving the whole original component preserves its oversized function; a new resource/action hook moves lifecycle and cache ownership unnecessarily. Moving JSX alone removes 210 lines but leaves 409 before wiring, so the leaf also takes its two private dialog-copy constants and now-unused imports. The single return-tree leaf is explicitly authorized by GUI-SEAM-01, unlike a controller rewrite. The wide props surface is a literal capture of the existing render scope, not a new store abstraction; types narrow the resource views and every passed binding is actually used. The cost is 29 passthrough bindings, accepted here to keep the lifetime-sensitive controller intact. Source resource types already exist at `gui/src/data-surface.ts:24–39`; translation function type at `gui/src/i18n/shared.ts:59`. + +Search evidence: `rg -n 'OverviewContent|IntegrationsOverviewContent' gui/src gui/tests tests` found no existing owner; sibling conventions are `ConsequenceDialog.tsx`, `RestoreDialog.tsx`, `RollbackHistory.tsx`, and the two #a component leaves. Reuse those components and existing API/types; no convenience barrel or new common/helper module. + +## Symbol inventory + +Exact ranges: `git show origin/dev:gui/src/pages/integrations/IntegrationsOverview.tsx | nl -ba` and `sg run --lang tsx --kind function_declaration --json=compact --stdin`, with `rg` for top-level constants. `rg -l -w '' src gui/src scripts tests gui/tests` counts distinct files; resolve name hits against import paths and exclude the source itself. The **four declarations present in the #a residual** are below. No top-level declaration is omitted; the two previously extracted components are accounted for afterward. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---|---| +| GROK_DISABLE_COPY | const record | 43–49 | no | 0 external | `gui/src/pages/integrations/IntegrationsOverviewContent.tsx` (private) | +| DESKTOP_DISABLE_COPY | const record | 51–58 | no | 0 external | `gui/src/pages/integrations/IntegrationsOverviewContent.tsx` (private) | +| isApplied | function | 60–62 | no | 0 importers; 4 unrelated backend name-hit files | residual `IntegrationsOverview.tsx` | +| IntegrationsOverview | component function | 171–729 | yes, default | 2 importers; 5 external name-hit files | residual; only return tree 519–728 moves | + +#a provenance, not additional #b moves: `OverviewCard` (77–169) already belongs to `OverviewCard.tsx`; `ApiKeysRow` (743–757) already belongs to `ApiKeysRow.tsx`. Their sole direct importer after #a is the residual page; this layer relocates those two import edges to the new content leaf. No export or component body is edited in either #a leaf. Thus all six original declarations remain uniquely owned across the final tree. + +New wiring-only declarations: private `IntegrationsOverviewContentProps` interface (no origin/dev range) and exported `IntegrationsOverviewContent` component shell around the existing return tree (provenance 519–728). The shell is used by the original page only and is not re-exported from it. Neither new declaration substitutes for a named action helper. + +Nested boundary audit: all declarations at 178–518 stay in `IntegrationsOverview`, including all nine fetch callbacks/resources, derived rows/counts, `refresh` (348–357), `disableAll` (364–421), `lastChange` (423), `cardPending` (430), `refreshNativeDetails` (432–436), `setCardResult` (438–445), `toggleCard` (447–479), `requestToggle` (481–492), and `overwriteCard` (501–517). Inline JSX callbacks at 552, 607–611, 640, 657, 671–695, 701–705, and 720–724 move inside the intact return tree; they are not new controller abstractions. + +## Leaf partition + +**One new source file:** `gui/src/pages/integrations/IntegrationsOverviewContent.tsx`, expected **283 lines**, hard maximum 400. + +- Move source **43–59** (17 lines, both private copy constants and their separators) without adding exports or changing values. +- Move source **519–728** (210 lines, the complete `return (...)` including `
`, all comments, and all four conditional dialogs) verbatim into the component shell below. Keep its original indentation; it already matches a top-level component's return block. +- Do not insert a wrapper DOM node, change keys, conditionally mount the content component, wrap it in `memo`, or call it as a plain function. The original page always returns the same component type, so local dialog lifetimes and DOM ordering remain stable across updates. +- Imports: exactly the following 13 lines, using L1's direct type owner and #a's view owners. `DataSurfaceResource`, `TFn`, and the model types are type-only imports. + +```ts +import type { DataSurfaceResource } from "../../data-surface"; +import { DataSurfaceSkeleton } from "../../components/data-surface"; +import { navigateHash } from "../../hash-routing"; +import type { TFn } from "../../i18n/shared"; +import { Notice } from "../../ui"; +import ConsequenceDialog, { type ConsequenceCopy } from "./ConsequenceDialog"; +import RestoreDialog from "./RestoreDialog"; +import { RollbackHistory } from "./RollbackHistory"; +import { describeRefusal } from "./refusal-copy"; +import { deleteJournalEntry, isMissingJournalEntry, type IntegrationJournalRow, type IntegrationStatus } from "./integration-api"; +import type { ApiKeysOverviewRow, OverviewCounts, OverviewRow } from "./overview-client-types"; +import { ApiKeysRow } from "./ApiKeysRow"; +import { OverviewCard } from "./OverviewCard"; +``` + +Add the following **31-line private props interface**. These are typed passthrough bindings, not copies of resource state or new domain DTOs. Passing the resource object itself preserves method receiver semantics in `historyResource.refresh()`; the narrowed type prevents the view from relying on unused resource fields. Setters only need the existing direct-value call form here, while their real React dispatch functions remain in the parent. + +```ts +interface IntegrationsOverviewContentProps { + t: TFn; + counts: OverviewCounts; + lastChange: string | undefined; + disableAll: () => Promise; + bulkPending: boolean; + appliedClients: IntegrationStatus[]; + keysRow: ApiKeysOverviewRow; + statesResource: { state: Pick["state"], "kind"> }; + bulkResult: { tone: "ok" | "err"; text: string } | null; + rows: OverviewRow[]; + cardPending: OverviewRow["id"] | null; + cardResults: Partial>; + requestToggle: (row: OverviewRow, next: boolean) => void; + setPendingOverwrite: (row: OverviewRow | null) => void; + clientsSettled: boolean; + installedFileClients: IntegrationStatus[]; + historyResource: { state: Pick["state"], "kind" | "showSkeleton">; refresh: DataSurfaceResource["refresh"] }; + history: IntegrationJournalRow[]; + setRestoring: (row: IntegrationJournalRow | null) => void; + setDeleting: (row: IntegrationJournalRow | null) => void; + restoring: IntegrationJournalRow | null; + apiBase: string; + refresh: () => void; + deleting: IntegrationJournalRow | null; + pendingToggle: OverviewRow | null; + setPendingToggle: (row: OverviewRow | null) => void; + toggleCard: (row: OverviewRow, next: boolean) => Promise; + pendingOverwrite: OverviewRow | null; + overwriteCard: (row: OverviewRow) => Promise; +} +``` + +Component shell opening (8 lines); then one blank line, the verbatim 210-line return block, and one closing `}`. No new hook, computed value, handler body, error boundary, or branching is added: + +```ts +export function IntegrationsOverviewContent({ + t, counts, lastChange, disableAll, bulkPending, appliedClients, + keysRow, statesResource, bulkResult, rows, cardPending, cardResults, + requestToggle, setPendingOverwrite, clientsSettled, installedFileClients, + historyResource, history, setRestoring, setDeleting, restoring, + apiBase, refresh, deleting, pendingToggle, setPendingToggle, + toggleCard, pendingOverwrite, overwriteCard, +}: IntegrationsOverviewContentProps) { +``` + +Line accounting for the leaf: `13 imports + 1 blank + 17 copied constants/separators + 31 props + 1 blank + 8 signature + 1 blank + 210 copied return + 1 closing brace = 283`. The existing comments are retained, not shortened to manufacture headroom. Non-move shell/wiring is 56 new leaf lines. + +**Residual original path:** expected **398 lines**. From #a's 619 remove 43–59 (17 original-source lines), replace 519–728 (210) with the exact 15-line return shown below, remove ten obsolete import lines, and add one content import. The ten removed imports are original-source lines **3, 4, 6, 10, 11, 12, 31, 32** plus #a's two new imports of `OverviewCard` and `ApiKeysRow`. Other imports remain, including `describeRefusal`, all data hooks/loaders/types, `toggleIntegration`, and `toggleNativeIntegration`. `IntegrationJournalRow` remains needed for state and resources. + +Arithmetic: **`619 - 17 - 210 - 10 + 1 + 15 = 398`**. This leaves the page's function at `559 - 210 + 15 = 364` lines: existing function-length debt is not a claim of being ≤50, but the file-size goal is satisfied without a controller rewrite. The 003 RESIDUAL-FN-01 exception need not be used because the file fits. Formatting must preserve the stated bounded passthrough layout; if the actual file exceeds 400, stop and report rather than deleting comments or silently compressing executable code. + +Stack totals: L1 leaves 128/72, model residual 378; #a leaves 115/32, page intermediate 619; #b leaf 283, page final 398. Thus **five leaves** and both final original files ≤400. #a took zero-external-consumer existing views first; #b now takes the remaining page render scope. No symbol is copied into both parts. + +## Re-export block + +No compatibility re-export lines: the sole current public export remains `export default function IntegrationsOverview` in the original path. Neither copy constant was public, and the new props interface stays private. No additional view export is exposed through the page; there is no `export *` or compatibility wrapper. + +Exact local import addition to the residual (a re-export would bind nothing locally): + +```ts +import { IntegrationsOverviewContent } from "./IntegrationsOverviewContent"; +``` + +Exact **15-line** replacement of the old return block; every binding is passed unchanged, no spread of an opaque controller object or callback adaptation: + +```tsx + return ( + + ); +``` + +## Module-level state and cycles + +- `GROK_DISABLE_COPY` (43–49) and `DESKTOP_DISABLE_COPY` (51–58) have exactly one new owner, the content leaf. They remain module-level private records; no mutation, cloning, lazy initialization, or per-render recreation. Both are used only by the moved conditional native dialog at 700. +- No top-level `let`, Map, Set, WeakMap, lock, timer, or cache exists in the original page or planned leaf. The controller's React state at 179–187 and 430, focus ref at 188, and restoration effect 190–196 stay at their original hook positions. The leaf has zero hooks. +- Resource definitions 198–297, `enabled: active`, session keys, lack of polling, and no `staleAfterMs` remain physically in the original page. `refresh` membership/order 348–357 and `refreshNativeDetails` 432–436 are unchanged; no opportunistic addition of Cursor refresh. +- Bulk disable 364–421 stays file-only, sequential, confirmation-gated and server-reconciled. `requestToggle` retains its original DOM-focus capture. Pending/result state remains parent-owned and callbacks are passed by reference, not wrapped or memoized. +- The entire existing delete `onConfirm` body (672–695) moves inside the copied JSX, retaining `isMissingJournalEntry` at 679, clear-and-refresh/return 680–682, localized rethrow 691, and success clear/refresh 693–694. Moving it does not authorize any API/deletion-policy change. The stale journal regression remains essential. +- Dependency map: page → content → #a views/dialogs/API clients/L1 type leaf. Content never imports the page, and neither the type leaf nor existing dialog/API owners import content. `OverviewCard → integration-marks → overview-clients` remains the existing one-way path. Type-only edges count; under TYPE-CYCLE-01 unchanged pre-existing type-only cycles are not new defects, but no new type/runtime cycle is allowed. +- Coupling is functional callback/data flow. Narrow resource projections explicitly retain the existing external data contract; no singleton/common mutable state or new validation boundary. Named per-page closures do not move into the leaf. + +## Tests + +Exact discovery command: `rg -l 'IntegrationsOverview' tests gui/tests`. Result is the following three files, deduplicated. Only one is a module importer. All lines are pinned origin/dev, including upstream's 69-line addition to the surfaces test versus docs HEAD. + +| Test file | Import/source-read location | Disposition | +|---|---|---| +| `gui/tests/integrations-surfaces.test.tsx` | dynamic page import 545 | unchanged; keep import through original default export, render exercises the content transitively | +| `gui/tests/integrations-cache-freshness.test.ts` | literal path 13, `readFileSync(new URL(\`../${path}\`, import.meta.url), "utf8")` at 19; assertions 20–21 | unchanged; original page still owns all nine resources/session keys | +| `gui/tests/integrations-routing.test.ts` | `Bun.file(new URL("../src/pages/Integrations.tsx", import.meta.url)).text()` at 125; child assertions 129–135 | unchanged; text reader of parent route, not the split file | + +No `retarget-to-leaf` needed: the cache policy does not move. No `add-leaf-to-scan-list`: the content component has no cache or resource hook, so adding it to `MIRROR_SURFACES` would incorrectly demand a cache key in presentation. Do not weaken either cache assertion or substitute an import-presence assertion. `page-loading-contract.test.tsx`'s explicit list 34–46 omits this page. `tests/clients/integrations-journal.test.ts:386` reads the separate `overview-clients.ts` journal map and remains unchanged; that source is not touched in #b. + +Affected downstream tests kept unchanged: `gui/tests/integrations-overview-rows.test.ts`, `gui/tests/overview-state-merge.test.ts`, `gui/tests/cursor-integration-page.test.tsx`, and `gui/tests/integration-marks.test.ts`. No direct test imports of the newly extracted component are added just to expose its implementation. + +Drive guards red once in implementation C phase, then restore exactly: (1) insert `staleAfterMs` on an original-page resource; cache freshness assertion must fail; (2) disable the moved missing-journal-entry reconciliation conditional, then run surfaces case **"the overview reconciles a journal row another tab already deleted"** (558–589), which must fail; (3) drop the `keysRow` passthrough temporarily, then run the surfaces credential-state case **"a source that cannot be read is unknown, never 'not applied'"** (943, `data-key-state` assertion 962), which must fail. These are bounded test-sensitivity checks, not proposed production changes. Existing bulk outcome cases and dialog focus cases run as part of the focused file. No tests were executed while drafting. + +## Verification + +Planning-only evidence: a fresh read-only Node reducer loaded the pinned source and applied #a/#b's line selections in memory: origin **757**, #a **619**, #b **398**, content leaf **283**. It checked the exact code-fence lengths (imports 13, props 31, signature 8, local import 1, return call 15), all **29** props against destructuring and unchanged-name call arguments, and all nine required headings plus three-layer branch references in both 620/625. `git diff --no-index --check /dev/null ` found no whitespace errors in either document. No code, test, build, or Git-state mutation was performed. + +Future executor only, in the #b worktree based on #a. The 002 gate plus 003 GUI/move requirements: + +```sh +bun run typecheck +bun test gui/tests/integrations-surfaces.test.tsx gui/tests/integrations-cache-freshness.test.ts gui/tests/integrations-routing.test.ts +bun test gui/tests/integrations-overview-rows.test.ts gui/tests/overview-state-merge.test.ts gui/tests/cursor-integration-page.test.tsx gui/tests/integration-marks.test.ts +bun run privacy:scan +bun run lint:gui +bun run build:gui +wc -l gui/src/pages/integrations/IntegrationsOverviewContent.tsx gui/src/pages/integrations/IntegrationsOverview.tsx +rg -n 'from "[^\"]*/IntegrationsOverview"|import\("[^\"]*/IntegrationsOverview"\)' src gui/src scripts tests gui/tests +git diff -M --stat codex/split-pages-integrations-IntegrationsOverview-a +git diff --color-moved=dimmed-zebra codex/split-pages-integrations-IntegrationsOverview-a -- gui/src/pages/integrations +``` + +Counts: existing page importer identities stay **2** (one production static + one GUI-test dynamic); 002's static-only scan excluding `gui/tests` remains **1**. The #a view imports change owner from page to content exactly as listed above; counts each remain one and no external consumer is migrated. Add a read-only relative-edge SCC delta scan over the touched graph: no new runtime/type cycle. No server/router/lib changes, so 002's conditional core-Lab gate is not applicable and protected roots remain untouched. + +PURE-MOVE-SIZE-01 receipt: record the **227 verbatim moved lines** (17 constants/separators + 210 return), compare the copied strings/AST against the #a/origin source, and confirm unique declaration ownership for all six original symbols plus the two new wiring declarations. Expected non-move budget is about **82 added/deleted lines** (`56 leaf wiring + 15 replacement + 1 import + 10 removed imports`), ceiling **150** including any verification-test additions. Count non-move edits separately from raw moved-line noise, not by hiding changes with whitespace-ignore. Any changed moved expression/body falls back to the literal 500-line rule and triggers escalation here. This planned raw diff is approximately 536 lines, which is why the parent amendment matters. + +GUI-SEAM-01 additionally requires before/after screenshots of the same controlled fixture/page state attached to the PR. Compare the full content tree (summary, credential row, catalog, rollback and open consequence dialog) and exercise keyboard focus after confirm/cancel. Use isolated mocked/test data for mutation dialogs, not live user integrations. The only added React component boundary must introduce no DOM wrapper and no new state lifetime. Build/lint and rendered proof are required, not inferred from a move diff. + +Full suites **only on lidge**, never repository-wide locally, at exact #b SHA: + +```sh +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-integrations-IntegrationsOverview-b && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15"' +ssh lidge 'bash -o pipefail -c "cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile >/dev/null && bun test tests 2>&1 | tail -15"' +``` + +Parent serializes this shared remote checkout, proves both runs used the same #b SHA, and records exact-head CI. `pipefail` prevents a successful tail from concealing test failure. These commands are planned, not run by this docs-only delegation. Planning checks validate required headings, passthrough-name coverage, source-line arithmetic, and copied-slice provenance without running source code or tests. + +## Accept criteria + +1. #b starts at the documented 619-line #a residual on branch `codex/split-pages-integrations-IntegrationsOverview-a`; source provenance remains pinned origin/dev 757 lines, including the upstream delete reconciliation. +2. Exactly one new source leaf, expected 283 lines; original residual expected **398**, both ≤400. S18 totals are five new leaves and zero final oversized originals; no #c or undisclosed state/controller split. +3. All four #a-residual top-level declarations have one owner; #a's two view declarations remain untouched. All 29 captured JSX bindings are typed, passed under their original names, and used without adaptation. The only original export remains default `IntegrationsOverview` and both existing importers are unchanged. +4. Both copied ranges are verbatim and the JSX tree's DOM structure/order/keys are unchanged; hook order, data-resource policy, focus ownership, named actions, bulk sequencing, and callback outcomes are unchanged. The content component has no hooks, memoization, wrapper DOM, or conditional mounting. +5. Non-move diff ≤150 with move-aware review and exact unique-owner/copy evidence; no new runtime or type-only cycle. No new module-level cache/lock/timer or duplicated copy records. +6. Source-reading tests remain directed at their real owners without weakened assertions; the cache, credential, and stale-delete guards each have a recorded red/green sensitivity check during implementation. +7. Focused tests, typecheck, privacy scan, GUI lint/build, before/after rendered evidence, remote full suites, and exact-head CI pass before delivery. Planning arithmetic is not implementation or test-pass evidence. +8. PR targets #a, contains the complete three-layer map/template and screenshots, and is not merged without separate authorization. Only 625 and 620 are written by this planning follow-up; 610 metadata synchronization remains parent-owned/outside this write scope. + +## PR + +Title: `refactor(gui-integrations): separate overview rendering from its controller (split S18 L3/3)` + +Branch: `codex/split-pages-integrations-IntegrationsOverview-b` + +Base: `codex/split-pages-integrations-IntegrationsOverview-a` + +Closes: none. + +| Layer | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| S18 L1/3 | #TBD-L1 | `codex/split-pages-integrations-overview-clients` | `dev` | contracts and primary adapters | +| S18 L2/3 | #TBD-L2 | `codex/split-pages-integrations-IntegrationsOverview-a` | `codex/split-pages-integrations-overview-clients` | existing card/key leaves; intermediate 619 | +| S18 L3/3 | #TBD-L3 | `codex/split-pages-integrations-IntegrationsOverview-b` | `codex/split-pages-integrations-IntegrationsOverview-a` | return tree and private copy records; final 398; this layer | + +Depends on #TBD-L2. Review this layer's diff only. Use `.github/PULL_REQUEST_TEMPLATE.md` Summary/Verification/Checklist; include `git diff --color-moved=dimmed-zebra` guidance, `git diff -M --stat`, unique-owner/copied-range evidence, non-move line count, and before/after screenshots. Cascade L2/L3 after an L1 change, L3 after an L2 change, and refresh exact-head evidence. Merge bottom-up only when separately authorized; no PR/Git operation is performed by this planning task. diff --git a/devlog/_plan/260905_now_split_train/630_pages_compatibility_matrix_api.md b/devlog/_plan/260905_now_split_train/630_pages_compatibility_matrix_api.md new file mode 100644 index 0000000000..f6c4cd12ac --- /dev/null +++ b/devlog/_plan/260905_now_split_train/630_pages_compatibility_matrix_api.md @@ -0,0 +1,178 @@ +# S19 L1 — Compatibility API pagination and contract owner + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**, C3, docs-only delegated plan. No code, tests, Git mutation, or parent orchestration executed in this task. +- Goal: reduce `gui/src/pages/compatibility-matrix-api.ts` (432 physical lines) below 400 by extracting its pagination foundation while preserving every original export and request/cancellation contract. +- Non-goals: changing parser strictness, page limits, endpoints, error identity, detail concurrency, community trust policy, UI state, or fixing unrelated defects. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated in Verification below; this is a future implementation gate, not evidence that tests have run. +- Stop: this layer has a reviewed pure-move diff, size/export/cycle proof, passing focused checks and exact-head remote full-suite/CI evidence; no merge. Parent owns execution and loop state. +- Escalation: upstream source drift, any behavior or signature change, an unlisted source reader, a new cycle, >400-line output, or >500 raw changed source lines requires parent review before expanding the partition. +- Basis: docs HEAD `4cc219549`; code `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. All source ranges below refer to that code revision. All four S19 sources were byte-compared with `git show origin/dev:` and match the working tree. Read `000_plan.md`, `001_stale_check.md`, S19 rows and gate in `002_layer_map.md`, and the four relevant file sections of `260905_modular_debt_ledger/015_lane_gui.md`. + +## Symbol inventory + +Ranges were obtained from `sg run --kind --json=compact `, selecting the top-level declaration lines matched by `rg`. Imports are dependency edges, not declaration rows. Every non-import top-level declaration is listed. + +Consumers means distinct external importing files, not raw identifier hits: `rg -l 'from ["\x27][^"\x27]*/compatibility-matrix-api(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts`, then `rg -l -w '' `. Non-exported symbols have zero external consumers; they still move with their internal callers. Four importing files: the page plus the three tests listed below. Target P = `gui/src/pages/compatibility-matrix-pagination.ts`; R = residual original path. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| PAGE_LIMIT | const | 25–25 | no | 0 | P | +| MAX_PAGES | const | 26–26 | no | 0 | P | +| DETAIL_CONCURRENCY | const | 27–27 | no | 0 | R | +| MAX_DETAIL_REFERENCES | const | 28–28 | no | 0 | R | +| fetchLabJson | async function | 30–33 | no | 0 | P | +| buildQuery | function | 35–40 | no | 0 | P | +| LabDataContractError | class | 42–42 | yes | 1 | P | +| invalidResponse | function | 44–46 | no | 0 | P | +| assertPaginationContract | function | 48–52 | no | 0 | P | +| parseStrictVerdictPage | function | 54–60 | no | 0 | P | +| parseStrictSubjectPage | function | 62–68 | no | 0 | P | +| parseStrictObservationPage | function | 70–76 | no | 0 | P | +| fetchLabStatus | async function | 78–83 | yes | 1 | R | +| fetchVerdictPage | async function | 85–102 | yes | 0 | P | +| fetchSubjectPage | async function | 104–111 | yes | 0 | P | +| CollectedPages | type | 113–116 | no | 0 | P | +| collectPages | async function | 118–136 | no | 0 | P | +| fetchAllSubjects | async function | 138–143 | yes | 2 | P | +| fetchSubjectDetail | async function | 145–154 | yes | 0 | R | +| fetchObservationsPage | async function | 156–172 | yes | 0 | P | +| fetchAllObservations | async function | 174–183 | no | 0 | P | +| fetchEventById | async function | 185–190 | yes | 0 | R | +| fetchArtifactByDigest | async function | 192–201 | yes | 0 | R | +| PassiveProductionSummaryDto | type | 203–213 | yes | 0 | R | +| parsePassiveProductionSummary | function | 215–229 | no | 0 | R | +| fetchPassiveProductionSummary | async function | 231–242 | yes | 0 | R | +| CommunityEvidenceSummaryRowDto | type | 244–251 | yes | 0 | R | +| CommunityEvidenceContextDto | type | 253–257 | yes | 2 | R | +| hasOnlyKeys | function | 259–262 | no | 0 | R | +| isSha256Hex | function | 264–266 | no | 0 | R | +| isNonNegativeInteger | function | 268–270 | no | 0 | R | +| parseCommunityEvidenceContext | function | 272–306 | yes | 1 | R | +| fetchCommunityEvidenceContext | async function | 308–316 | yes | 0 | R | +| LabPageData | type | 318–326 | yes | 1 | R | +| fetchLabPageData | async function | 328–356 | yes | 2 | R | +| fetchMoreVerdicts | async function | 358–365 | yes | 1 | R | +| VerdictDetailData | type | 367–374 | yes | 1 | R | +| mapSettledBounded | async function | 376–400 | no | 0 | R | +| fetchVerdictDetail | async function | 402–432 | yes | 3 | R | + +## Leaf partition + +Structural decision: extract the foundation, not a wrapper around the existing facade. The source pressure is 432 lines spanning paginated reads and detail/community assembly. Keeping `collectPages` separate while importing `LabDataContractError` back from the facade would create a cycle. Instead move the error constructor, low-level fetch/query helpers and strict pagination parsers together. Preserve the existing application boundary rather than introducing a new generic HTTP client. + +Current direction: `CompatibilityMatrix.tsx:8` and tests → API → `fetch-json.ts` / `compatibility-matrix-shared.ts`. Intended: same consumers → API → pagination → existing JSON/shared modules. Blast radius: GUI compatibility feature, with zero consumer path edits. Existing sibling convention: `compatibility-matrix-api.ts`, `compatibility-matrix-shared.ts`; do not enlarge the existing shared DTO/parser module or add an `index.ts`. + +One NEW file: + +- `gui/src/pages/compatibility-matrix-pagination.ts`: all P rows above. Move contiguous original spans 25–26, 30–76, 85–143 and 156–183: **136 source lines** including intervening blanks. Expected **160 lines** including its own imports/separators (budget, not a measured output). Keep `CollectedPages` private; its inferred return shapes remain structurally identical. Export `fetchLabJson`, `buildQuery`, `invalidResponse`, `fetchAllObservations` only from this internal leaf because the residual needs them; do not add them to the old public facade. + +Its imports are exactly: + +```ts +import { readJsonOrThrow } from "../fetch-json"; +import { + isPlainObject, parseObservationsPage, parseSubjectPage, parseVerdictPage, + type ObservationDto, type PaginatedObservations, type PaginatedSubjects, + type PaginatedVerdicts, type SubjectListItemDto, type VerdictQueryFilters, +} from "./compatibility-matrix-shared"; +``` + +Residual `gui/src/pages/compatibility-matrix-api.ts`: **expected 310 lines** with a conservative import/re-export reserve: 432 − 136 = 296 retained source lines before import cleanup/plumbing. Keep every R declaration byte-equivalent. Drop its `readJsonOrThrow`, strict page parser and paginated-subject/observation imports now owned by P; retain the used shared DTOs/parsers. No #b layer is needed. Raw source diff budget is approximately 300 additions plus deletions, comfortably below 500; count actual `git diff --numstat` at implementation. + +## Re-export block + +Insert these exact compatibility re-exports in the original path; all other existing exports remain declarations there: + +```ts +export { LabDataContractError, fetchVerdictPage, fetchSubjectPage, fetchAllSubjects, fetchObservationsPage } from "./compatibility-matrix-pagination"; +``` + +No current exported type moves, so no `export type` line is required. Re-exporting does not bind the residual's names; it explicitly imports: + +```ts +import { buildQuery, fetchAllObservations, fetchAllSubjects, fetchLabJson, fetchVerdictPage, invalidResponse } from "./compatibility-matrix-pagination"; +``` + +Keep `fetchLabStatus`, `fetchSubjectDetail`, `fetchEventById`, `fetchArtifactByDigest`, `fetchPassiveProductionSummary`, `parseCommunityEvidenceContext`, `fetchCommunityEvidenceContext`, `fetchLabPageData`, `fetchMoreVerdicts`, `fetchVerdictDetail` and all five exported DTO/data types importable exactly as before. No `export *`, default export, or compatibility alias is added. + +## Module-level state and cycles + +- No top-level mutable collection, `let`, lock or cache. `PAGE_LIMIT` at `gui/src/pages/compatibility-matrix-api.ts:25` and `MAX_PAGES:26` have one owner in P. `DETAIL_CONCURRENCY:27` and `MAX_DETAIL_REFERENCES:28` remain R. +- `LabDataContractError:42` has one constructor in P; R re-exports that exact binding so `instanceof` at existing tests still works. Do not subclass or redeclare it in R. +- `seen` at original line 122 is per-call pagination state, not a module singleton. `allowedSet:260`, bounded-worker `index:384` and detail event-ID Set at 407 likewise retain their local lifetimes. +- Potential cycle: P → R for fetch/error/query helpers while R → P for pagination. Avoid it by moving all six helper dependencies into P; neither runtime nor type imports in P may reference R. +- New coupling is functional calls / type-only DTOs; existing boundary validation stays at the HTTP response parser. No validation removal or new policy checks. + +## Tests + +Direct import `rg -l` result (all unchanged, original import paths retained): + +```text +gui/tests/compatibility-lab.test.tsx +gui/tests/compatibility-community-evidence.test.ts +gui/tests/compatibility-pagination-cap.test.ts +``` + +The first imports at line 27, the second at line 7, the third at line 2. `rg -n 'compatibility-matrix-api' tests gui/tests` plus inspection of their `Bun.file` / `readFileSync` sites found **no text-oracle reader of this API file**. No retarget-to-leaf or add-leaf-to-scan-list action is required here. `compatibility-matrix-layout.test.ts:7` reads the page, not this API; that change belongs to L2. + +Guards to drive red once in the implementation worktree, then restore before green: change P's `MAX_PAGES` from 200 to 199 and observe the pagination-cap test fail; replace P's thrown constructor in the malformed-page path and observe the `LabDataContractError` assertion fail in `compatibility-lab.test.tsx`. The production/community response assertions must remain unchanged. These are planned mutation checks, not runs performed by this docs task. + +## Verification + +Run in the dedicated L1 implementation worktree, at its own tip (002 Per-layer gate, domains `gui/tests` compatibility + JSON API): + +```sh +bun run typecheck +bun test gui/tests/compatibility-lab.test.tsx gui/tests/compatibility-community-evidence.test.ts gui/tests/compatibility-pagination-cap.test.ts +bun run privacy:scan +(cd gui && bun run lint && bun run build) +wc -l gui/src/pages/compatibility-matrix-pagination.ts gui/src/pages/compatibility-matrix-api.ts +rg -l 'from ["\x27][^"\x27]*/compatibility-matrix-api(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts +sg run --kind import_statement --json=compact gui/src/pages/compatibility-matrix-pagination.ts +git diff --check +git diff --numstat +``` + +Require zero test failures and exit 0 for typecheck/privacy/lint/build. Importer set stays the four observed files until a later authorized S19 layer adds internal consumers; compare the set, not just its size. Inspect P's imports against the exact inward graph above, including type edges; no facade backlink. Core-Lab boundary test is not triggered: no `src/server`, `src/router`, or `src/lib` change; do not edit its protected roots. + +Full suites only on the parent's allocated `lidge` checkout at the exact pushed branch tip: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-compatibility-matrix-api && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && (cd gui && bun install --frozen-lockfile && bun test tests)' +``` + +Record remote `git rev-parse HEAD`, full log/exit status and exact-head CI rollup. Do not accept `tail`'s exit code as suite success or share a mutable remote checkout concurrently with another stack. No local full suite. No commands in this section were run during drafting. + +## Accept criteria + +1. All 39 declaration rows have exactly one owner; only the one named leaf is added. +2. P and R are each ≤400 physical lines; retained export names and parameter/return contracts are unchanged, including zero-consumer exports. +3. Pagination still uses limit 50, cap 200, repeated-cursor rejection and the same caller signal; returned truncation semantics are unchanged. +4. There is exactly one `LabDataContractError` definition and no P → R import or re-export edge. +5. Three direct-import tests remain on the original path; the two planned negative probes fail, restored focused tests pass, and remote full-suite plus exact-head CI are green. +6. Layer-only diff passes the 500-line budget check; no UI copy, CSS, endpoint, consumer path, unrelated code or protected-root edits. +7. PR base is `dev`, parent records evidence, and no merge occurs. + +## PR + +Title: `refactor(gui): isolate compatibility pagination contracts (split S19 L1/4)` + +Branch: `codex/split-pages-compatibility-matrix-api`. Base: `dev`. Closes: none. + +Use all Summary / Verification / Checklist sections of `.github/PULL_REQUEST_TEMPLATE.md`. Include unchanged-UI screenshot evidence because the title names gui; do not claim a UI redesign. DEV-STACK-03 map (placeholder PR numbers, replace only when PRs exist): + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S19-L1 | codex/split-pages-compatibility-matrix-api | dev | pagination/error owner; this layer | +| 2 | #TBD-S19-L2 | codex/split-pages-CompatibilityMatrix | codex/split-pages-compatibility-matrix-api | matrix presentation leaves | +| 3 | #TBD-S19-L3 | codex/split-combo-workspace-data | dev | quota evidence and combo contracts | +| 4 | #TBD-S19-L4 | codex/split-components-combo-workspace-detail-panel | codex/split-combo-workspace-data | controlled Config contents | + +Base: dev — no dependency on lower layers; this layer is the parent of 640 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). + +Review this layer's diff only. Merge only after separate user authorization; never enable auto-merge. diff --git a/devlog/_plan/260905_now_split_train/640_pages_CompatibilityMatrix.md b/devlog/_plan/260905_now_split_train/640_pages_CompatibilityMatrix.md new file mode 100644 index 0000000000..46c3becf11 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/640_pages_CompatibilityMatrix.md @@ -0,0 +1,165 @@ +# S19 L2 — Matrix presentation without moving request lifetimes + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**, C3, bounded docs-only delegation; implementation and loop state belong to the parent. +- Goal: reduce `gui/src/pages/CompatibilityMatrix.tsx` from 628 to ≤400 lines by moving existing view components and page-local supporting declarations into two siblings. Preserve the default page export and every existing prop. +- Non-goals: changing layout, labels, request ownership, hooks, filter behavior, polling, cancellation, selection, pagination semantics, or resource keys; no new controller hook or context. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below, plus the GUI build and text-oracle red proof. No tests run in this docs task. +- Stop: only this layer's pure-move diff, complete export/cycle/size evidence, preserved source and DOM oracles, exact-tip remote full suites and CI; no merge. +- Escalation: new source readers, source drift, changed JSX/props or hook lifetimes, size >400, or an unapproved >500-line raw source diff. The planned 238-line move produces about 476 add/delete lines before imports; expected raw diff around 520 can exceed 002's 500-line gate. **Parent must explicitly decide a move-only budget exception or revise 002 to add a part; this document does not silently change the four-layer stack.** +- Basis: docs `4cc219549`; `origin/dev` code `1362b1a3841b4de20177e5d65865a513dd7936c4`. Source ranges below are at that revision, verified equal to the working tree. Inputs: `000_plan.md`, `001_stale_check.md`, S19 in `002_layer_map.md`, and `260905_modular_debt_ledger/015_lane_gui.md` (DetailPane/summary seam at original lines 81–269). + +## Symbol inventory + +Exact inclusive spans: top-level `rg` declarations reconciled with `sg run --kind --json=compact gui/src/pages/CompatibilityMatrix.tsx`. Imports are enumerated as dependency edges below, not declarations. Consumers = distinct external importing files from `rg -l 'from ["\x27][^"\x27]*/CompatibilityMatrix(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts`, followed by `rg -l -w ''` within that set; private declarations have zero external consumers. + +V = `gui/src/pages/compatibility-matrix-views.tsx`; S = `gui/src/pages/compatibility-matrix-page-state.ts`; R = original path. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| LAYER_LABEL | const record | 30–34 | no | 0 | V | +| LAYER_COLUMN | const record | 36–40 | no | 0 | V | +| VERDICT_LABEL | const record | 42–50 | no | 0 | V | +| ARTIFACT_STATUS_LABEL | const record | 52–56 | no | 0 | V | +| ExtraVerdictPage | type | 58–64 | no | 0 | S | +| LoadMoreFailure | type | 66–70 | no | 0 | S | +| localizedFetchError | function | 72–79 | no | 0 | S | +| VerdictBadge | function component | 81–103 | no | 0 | V | +| VerdictCell | function component | 105–126 | no | 0 | V | +| StatusCards | function component | 128–151 | no | 0 | V | +| CommunityEvidencePanel | function component | 153–171 | no | 0 | V | +| DetailPane | function component | 173–269 | no | 0 | V | +| CompatibilityMatrix | function component | 271–628 | default | 3 | R | + +## Leaf partition + +Structural decision: retain the entire `CompatibilityMatrix` function at `gui/src/pages/CompatibilityMatrix.tsx:271`, extract the already-top-level renderers, and give its two pagination result types/error formatter a small page-state sibling. Rejected alternatives: moving the whole page leaves a new 628-line problem; moving only DetailPane leaves >500 lines; moving types into a renderer or importing them back from the page creates misleading ownership or a cycle. Do nothing/delete/configure cannot resolve this file's modular debt. Existing `compatibility-matrix-shared.ts` is the DTO/parser owner, not a home for page request state. + +Current map: `Models.tsx:22` and two mounted tests → page → React/i18n/data-surface/API/shared DTOs. New map: same entry → page → V and S; V and S → API/shared DTOs, never back to page. V also → i18n/UI/data-surface presentation. Local GUI-feature blast radius; no routing, backend or public prop changes. Existing sibling naming is demonstrated by `dashboard-overview-head.tsx`, `dashboard-overview-panels.tsx`, and `compatibility-matrix-shared.ts`. + +NEW files: + +1. `gui/src/pages/compatibility-matrix-views.tsx`: all V rows, original 30–56 plus 81–269 = **216 moved lines**. Expected **235 lines** including imports and separation. Export the three page-used label maps and five existing components from this leaf. `ARTIFACT_STATUS_LABEL` remains leaf-private. Components keep their original signatures and JSX byte-equivalent; `VerdictCell` calls the local `VerdictBadge`, not a facade re-export. +2. `gui/src/pages/compatibility-matrix-page-state.ts`: all S rows, original 58–79 = **22 moved lines**. Expected **27 lines** with its type-only imports and separators. Export both types and `localizedFetchError` for direct use by R. It owns no effects, stores or hooks; this is the value/error representation accompanying a page request, not a new controller abstraction. + +V imports: + +```ts +import type { TKey } from "../i18n/shared"; +import { labSupplement, type LabSupplementKey } from "../i18n/lab-translations"; +import { Notice } from "../ui"; +import { DataSurfaceStatus } from "../components/data-surface"; +import type { CommunityEvidenceContextDto, LabPageData, VerdictDetailData } from "./compatibility-matrix-api"; +import { + formatAsOf, shortSubjectId, + type ArtifactStatus, type CompatibilityVerdict, type EvidenceLayer, type VerdictDto, +} from "./compatibility-matrix-shared"; +``` + +S imports: + +```ts +import type { LabPageData } from "./compatibility-matrix-api"; +import type { VerdictDto } from "./compatibility-matrix-shared"; +``` + +Residual `gui/src/pages/CompatibilityMatrix.tsx`: **expected 395 lines**. Arithmetic: 628 − 216 − 22 = 390 retained original lines including the old imports; remove unused `TKey`, `LabSupplementKey`, `CommunityEvidenceContextDto`, `ArtifactStatus` imports and add the three explicit leaf imports below. Keep readable formatting; the implementation's `wc -l` is authoritative. All state and the 358-line page function remain intact. This intentionally does not claim to eliminate existing function-length debt; slicing that controller is a different behavior-risk task. No residual >400 or #b is planned; only the raw diff budget may require parent-authorized topology expansion. + +## Re-export block + +**No re-export statement is needed:** the only current export is `export default function CompatibilityMatrix` at original line 271, and it stays in R. Do not expose previously private renderers/types from the page just to create a barrel. The exact public export block is therefore unchanged (one default function, no named exports). + +The residual requires these local imports; re-exports would not bind these names: + +```ts +import { LAYER_LABEL, LAYER_COLUMN, VERDICT_LABEL, VerdictBadge, VerdictCell, StatusCards, CommunityEvidencePanel, DetailPane } from "./compatibility-matrix-views"; +import { localizedFetchError } from "./compatibility-matrix-page-state"; +import type { ExtraVerdictPage, LoadMoreFailure } from "./compatibility-matrix-page-state"; +``` + +Retain its React hooks, `IconRefresh`, `useI18n`, `labSupplement`, `EmptyState`/`Notice`/`Select`, `useDataSurface`, data-surface components, three API functions and page/detail DTO types. Retain shared constants/matrix/format/query helpers plus `CompatibilityVerdict`, `EvidenceLayer`, `VerdictDto`, `VerdictFilters`. No leaf exports added to the old public path; existing default imports in Models and tests remain untouched. + +## Module-level state and cycles + +- No top-level `let`, Map, Set, WeakMap, lock, subscription or effect. Four label records at `gui/src/pages/CompatibilityMatrix.tsx:30`, `:36`, `:42`, `:52` move to V once; their read-only usage is preserved without introducing freezes or changing types. +- Component-local `expectedEventCount` Set at original line 182 moves inside `DetailPane`; it must not become shared module state. +- All `useState` values at 277–284, request refs at 286–288, callback/effect lifetimes at 292–332, request identity checks at 334–343 and 362–424 remain in R. The `baseData` object identity is not replaced by a value comparison. +- Potential V → R cycle for labels or DTOs is avoided by owning labels in V and importing DTO types from the existing API/shared modules. S imports only existing DTO types, not R or V. API from L1 must never import this page or either leaf. New edges are functional props and type-only contracts, not shared mutable state. + +## Tests + +Every directly importing test from `rg -l` (unchanged default imports): + +```text +gui/tests/compatibility-lab.test.tsx +gui/tests/compatibility-lab-followup.test.tsx +``` + +Imports occur at lines 8 and 7 respectively. The third importing file is `gui/src/pages/Models.tsx:22`, unchanged. These mounted tests cover selection, refresh, abort/races, inactive-tab behavior and rendering through the original boundary. + +Every discovered source-text reader of the page: + +| test/read location at origin/dev | disposition | exact action | +|---|---|---| +| `gui/tests/compatibility-matrix-layout.test.ts:7`, `Bun.file(new URL("../src/pages/CompatibilityMatrix.tsx", import.meta.url)).text()` | add-leaf-to-scan-list | retain this page read and all Models/routing/tabs/CSS reads; additionally read `../src/pages/compatibility-matrix-views.tsx` and include it in `readSources` | + +Do not replace the page read with only V: the Models mount and page table markup must remain guarded. The combined source string also contains CSS, so merely appending V can leave a vacuous badge assertion. Add a separate assertion against V's own source for the `const className =` declaration whose template starts with `lab-verdict-badge`; preserve all existing assertions. No source read of S is necessary for those layout tokens. The CSS-only second test remains unchanged. + +Drive the migrated guard red once by changing the actual badge class prefix only in V while leaving page/CSS/test expectations intact. It must fail independently of CSS containing `.lab-verdict-badge`. Restore, then require green. Also exercise existing follow-up race tests unchanged; a pure-move claim cannot be based solely on text tokens. No tests or negative mutations are performed by this draft task. + +## Verification + +Future L2 worktree commands, 002 Per-layer gate with GUI compatibility domain: + +```sh +bun run typecheck +bun test gui/tests/compatibility-lab.test.tsx gui/tests/compatibility-lab-followup.test.tsx gui/tests/compatibility-matrix-layout.test.ts +bun run privacy:scan +(cd gui && bun run lint && bun run build) +wc -l gui/src/pages/CompatibilityMatrix.tsx gui/src/pages/compatibility-matrix-views.tsx gui/src/pages/compatibility-matrix-page-state.ts +rg -l 'from ["\x27][^"\x27]*/CompatibilityMatrix(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts +sg run --kind import_statement --json=compact gui/src/pages/compatibility-matrix-views.tsx gui/src/pages/compatibility-matrix-page-state.ts +git diff --check +git diff --numstat codex/split-pages-compatibility-matrix-api...HEAD +``` + +Importer set remains exactly three; leaf imports match the inward graph including type edges. All outputs ≤400; default export and JSX signatures unchanged; focused checks/privacy/lint/build exit 0. Compare extracted bodies with `git diff --color-moved` and original line ranges. No backend protected roots touched, hence no conditional core-Lab boundary run. Obtain parent budget decision before publishing if raw source additions + deletions exceed 500; no unilateral branch/map changes. + +Full suites on the parent's exclusively allocated lidge checkout only: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-CompatibilityMatrix && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && (cd gui && bun install --frozen-lockfile && bun test tests)' +``` + +Record remote SHA equal to the PR tip, full log and actual test exit status, exact-head CI rollup and unchanged-UI screenshot (matrix, selected detail, empty/error states). Never run full suites locally; no live service restart/deploy is implied. The parent executes and reports these checks, not this docs task. + +## Accept criteria + +1. All 13 top-level declarations retain one owner; the default page and its props remain importable from the original path with exactly three original importers. +2. V, S and R are ≤400 physical lines; all 358 lines of the original page function are unchanged apart from relocated references resolving through imports. +3. Labels/JSX and existing renderer signatures are byte-equivalent; no hook, state, callback, signal, request identity or timer is moved to a new lifetime. +4. No runtime or type cycle enters R from either leaf; no additional internal barrel is created. +5. The source oracle still reads R, adds V and fails on the specified leaf-only badge mutation; two mounted test files keep their original imports and pass. +6. Typecheck, focused tests, privacy, GUI lint/build, remote suites and exact-head CI have fresh successful evidence; raw diff >500 has an explicit parent disposition before execution/publishing. +7. PR base is L1's branch, all lower-layer changes are cascaded by the parent, and no merge occurs. + +## PR + +Title: `refactor(gui): separate matrix presentation from request state (split S19 L2/4)` + +Branch: `codex/split-pages-CompatibilityMatrix`. Base: `codex/split-pages-compatibility-matrix-api`. Closes: none. + +Fill Summary / Verification / Checklist in the repository PR template, include screenshot evidence for unchanged GUI, document the raw-diff budget disposition. DEV-STACK-03 map: + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S19-L1 | codex/split-pages-compatibility-matrix-api | dev | pagination/error owner | +| 2 | #TBD-S19-L2 | codex/split-pages-CompatibilityMatrix | codex/split-pages-compatibility-matrix-api | matrix presentation leaves; this layer | +| 3 | #TBD-S19-L3 | codex/split-combo-workspace-data | dev | quota evidence and combo contracts | +| 4 | #TBD-S19-L4 | codex/split-components-combo-workspace-detail-panel | codex/split-combo-workspace-data | controlled Config contents | + +Depends on #TBD-S19-L1; review this diff only. Parent cascades edits to `codex/split-pages-compatibility-matrix-api` into this layer and refreshes evidence (DEV-STACK-02). Merge after that parent only with separate user authorization; no auto-merge. diff --git a/devlog/_plan/260905_now_split_train/650_combo_workspace_data.md b/devlog/_plan/260905_now_split_train/650_combo_workspace_data.md new file mode 100644 index 0000000000..67ca564a64 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/650_combo_workspace_data.md @@ -0,0 +1,195 @@ +# S19 L3 — Combo quota evidence and neutral contracts + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**, C3, docs-only bounded delegation. The parent owns implementation, orchestration, loop and goal state. +- Goal: bring `gui/src/combo-workspace-data.ts` (650 lines) below 400 using quota/attention and neutral-type leaves; preserve all original named exports, native-catalog identity and the target-key sequence. +- Non-goals: quota policy changes, draft validation changes, new network requests, backend changes, normalizer fixes, alias-policy changes, new caching, or serialization rewrites. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below. No tests, code edits or Git mutations executed during this draft. +- Stop: pure-move diff, every export still available, single state ownership, size/cycle evidence, focused and remote exact-tip full-suite/CI proof; no merge. +- Escalation: source drift, unlisted readers/callers, >400-line outputs, changed policy/signatures or cycles. **The 284-line extraction alone is about 568 raw add/delete lines, before plumbing. This exceeds a literal 500-line raw diff cap. Parent must approve an explicit pure-move size exception or expand 002 with an additional part before execution; this delegated plan cannot change the four-layer map.** +- Basis: docs `4cc219549`; code `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`, identical to this source in the working tree. Read train `000_plan.md`, `001_stale_check.md`, S19/gate in `002_layer_map.md` and `260905_modular_debt_ledger/015_lane_gui.md` (quota seam at original lines 295–490, key ownership at 97). + +## Symbol inventory + +Top-level declarations from `rg` were reconciled against `sg run --kind --json=compact gui/src/combo-workspace-data.ts`. Inclusive line spans refer to origin/dev. Import statements are dependency edges; the extra native-catalog re-export row is included to preserve the complete public surface. + +Consumers = distinct external importer files: `rg -l 'from ["\x27][^"\x27]*/combo-workspace-data(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts`, then `rg -l -w ''` within that set. Private symbols have zero external consumers. Thirteen files import this path, enumerated below. T = `gui/src/combo-workspace-contracts.ts`; Q = `gui/src/combo-workspace-quota.ts`; R = residual original. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| SUPPORTED_NATIVE_OPENAI_SLUGS | imported binding re-export | 9–9 | yes | 0 | R; canonical owner remains native-models.ts | +| ComboStrategy | type | 11–11 | yes | 1 | T | +| ComboEffort | type | 12–12 | yes | 1 | T | +| COMBO_EFFORTS | const array | 14–14 | yes | 2 | R | +| COMBO_STRATEGIES | const array | 16–22 | yes | 1 | R | +| COMBO_STRATEGY_LABEL_KEYS | const record | 24–30 | yes | 1 | R | +| COMBO_STRATEGY_HINT_KEYS | const record | 32–38 | yes | 2 | R | +| COMBO_TARGETS_HINT_KEYS | const record | 40–46 | yes | 2 | R | +| COMBO_STRATEGY_SET | const Set | 48–48 | no | 0 | R | +| intersectComboEfforts | function | 54–81 | yes | 3 | R | +| ComboTarget | interface | 83–89 | yes | 2 | T | +| ComboQuotaState | type | 91–91 | yes | 0 | T | +| ProviderQuotaStates | type | 92–92 | yes | 5 | T | +| COMBO_QUOTA_MAX_AGE_MS | const | 95–95 | yes | 0 | Q | +| comboTargetKeySeq | let counter | 97–97 | no | 0 | R | +| newComboTarget | function | 99–106 | yes | 1 | R | +| normalizeImageInput | function | 109–111 | no | 0 | R | +| normalizeReasoningEffortMode | function | 113–115 | no | 0 | R | +| ComboItem | interface | 117–137 | yes | 9 | T | +| ComboSections | interface | 139–143 | yes | 0 | T | +| ComboAttentionItem | interface | 145–149 | yes | 0 | T | +| COMBO_ID_RE | const RegExp | 151–151 | yes | 0 | R | +| COMBO_ALIAS_RE | const RegExp | 153–153 | yes | 0 | R | +| NATIVE_OPENAI_FAMILY_RE | const RegExp | 154–154 | no | 0 | R | +| isValidComboId | function | 156–158 | yes | 1 | R | +| comboModelId | function | 160–162 | yes | 3 | R | +| comboPublicModelId | function | 165–168 | yes | 3 | R | +| updateComboAliasDraft | function | 171–181 | yes | 2 | R | +| normalizeAlias | function | 183–185 | no | 0 | R | +| normalizeStrategy | function | 187–191 | yes | 0 | R | +| normalizeStickyLimit | function | 193–197 | yes | 0 | R | +| normalizeDefaultEffort | function | 199–203 | yes | 0 | R | +| normalizeWeight | function | 205–209 | yes | 0 | R | +| parseComboList | function | 211–249 | yes | 3 | R | +| groupCombos | function | 251–261 | yes | 4 | R | +| filterCombos | function | 263–273 | yes | 2 | R | +| recordFromUnknown | function | 275–279 | no | 0 | Q | +| finiteNumber | function | 281–283 | no | 0 | Q | +| quotaTimestampIsFresh | function | 285–288 | no | 0 | Q | +| nonNegativeInteger | function | 290–293 | no | 0 | Q | +| aggregateWindowIsComplete | function | 295–306 | no | 0 | Q | +| aggregateEvidenceIsComplete | function | 308–359 | no | 0 | Q | +| quotaStateFromReport | function | 361–412 | no | 0 | Q | +| providerQuotaStatesFromReports | function | 415–431 | yes | 2 | Q | +| comboQuotaState | function | 437–457 | yes | 3 | Q | +| buildComboAttention | function | 459–490 | yes | 2 | Q | +| draftEquals | function | 492–509 | yes | 2 | R | +| toPutBody | function | 511–544 | yes | 3 | R | +| ComboDraftError | type | 546–565 | yes | 0 | T | +| validateComboDraft | function | 567–634 | yes | 3 | R | +| emptyDraft | function | 636–650 | yes | 4 | R | + +## Leaf partition + +Structural decision: extract quota parsing and attention together, sharing neutral contracts with the residual. Current callers → data facade → native model catalog + i18n types. Intended callers → same facade → Q → T, with facade → T; only the facade retains native-model runtime imports and target creation. Functional/type coupling only. Local combo-feature blast radius, no new backend dependency or endpoint. + +Rejected alternatives: move only the 216-line quota block and leave a >400-line residual; import `ComboItem` back from the facade inside Q (type cycle); put domain DTOs in `components/combo-workspace-types.ts` (presentation owner already imports the facade, creating an upward dependency); move draft normalizers/key creation as well (unnecessary state churn). Do nothing, deletion and configuration do not discharge this modular debt. Keep existing exported constants/aliases instead of duplicating them. + +Convention/search evidence: inspected `gui/src/combo-capabilities.ts:1`, `components/combo-workspace-types.ts:1`, `components/combo-workspace-controls.tsx:2`, the original public importers and existing hyphenated `combo-workspace-*` siblings. The existing component types are UI option/props types, not an alternative owner for the DTOs. Both new modules are siblings under `gui/src/`; no generic utils or barrel folder. + +NEW files: + +1. `gui/src/combo-workspace-contracts.ts`: every T row (nine types/interfaces). Move original spans 11–12, 83–93, 117–149 and 546–565 = **66 lines** including associated blanks. Expected **70 lines** with separators. Imports: **none**; `ComboItem`, `ComboSections` and quota/attention types resolve their dependencies locally. All definitions and optional-field comments remain verbatim. +2. `gui/src/combo-workspace-quota.ts`: every Q row. Move original 94–95 and 275–490 = **218 lines**. Expected **225 lines** including type import and separation. Its only import is: + +```ts +import type { ComboAttentionItem, ComboItem, ComboQuotaState, ComboTarget, ProviderQuotaStates } from "./combo-workspace-contracts"; +``` + +Residual `gui/src/combo-workspace-data.ts`: **expected 388 lines**, using a conservative 22-line plumbing reserve: 650 − 66 − 218 = 366 retained source lines. Keep all R declarations, original top comment, `SUPPORTED_NATIVE_OPENAI_SLUGS` import/re-export and `TKey` import. Exactly one target-key sequence owner remains here. No #b is required for residual size after the full proposed L3 move. Expected raw source diff about 600, not a claim of ≤500: require the parent's budget decision from Loop spec. If a new part is mandated, the parent must allocate its branch/doc and update successors before implementation; do not leave an unrecorded >400 residual or invent #b in this scope. + +## Re-export block + +Exact new named compatibility exports at the original path: + +```ts +export type { ComboStrategy, ComboEffort, ComboTarget, ComboQuotaState, ProviderQuotaStates, ComboItem, ComboSections, ComboAttentionItem, ComboDraftError } from "./combo-workspace-contracts"; +export { COMBO_QUOTA_MAX_AGE_MS, providerQuotaStatesFromReports, comboQuotaState, buildComboAttention } from "./combo-workspace-quota"; +``` + +The residual uses six moved types and must import them explicitly: + +```ts +import type { ComboStrategy, ComboEffort, ComboTarget, ComboItem, ComboSections, ComboDraftError } from "./combo-workspace-contracts"; +``` + +No residual runtime call requires a quota function: `buildComboAttention` and its local call to `comboQuotaState` move together. Therefore no unused quota import is added. All remaining original exports stay declarations, including the exact original `export { SUPPORTED_NATIVE_OPENAI_SLUGS };` backed by its original import from `../../src/codex/catalog/native-models`. Do not replace that exported Set with a copy. + +## Module-level state and cycles + +- `COMBO_STRATEGY_SET` at `gui/src/combo-workspace-data.ts:48`: one Set, owned by R and used by `normalizeStrategy:187`. Do not rebuild it per call or move/copy it to T/Q. +- `comboTargetKeySeq:97`: one mutable counter in R, incremented only by `newComboTarget:99`. `parseComboList:211` and `emptyDraft:636` still call that same function. No initialization in a leaf or extra sequence per import path. +- `COMBO_EFFORTS:14`, `COMBO_STRATEGIES:16`, three label/hint records at 24/32/40 and regexes at 151/153/154 retain R ownership and identity. No new freezes or changed mutability contracts. `COMBO_QUOTA_MAX_AGE_MS:95` moves to Q once. +- Imported `SUPPORTED_NATIVE_OPENAI_SLUGS` is owned by `src/codex/catalog/native-models.ts`, not a new S19 Set. Keep its identity across the facade. +- Other Sets/Maps in this source (`effortSet:61`, `memberSet:74`, `commonSet:79`, `targets:611`) are function-local and stay so. There are no top-level WeakMaps, locks, timer or cache owners. +- Critical hypothetical cycle R → Q → R is avoided even for type edges: Q imports types only from T, and T imports nothing. Neither new leaf imports `components/combo-workspace-types.ts`, `combo-capabilities.ts` or the facade. Existing component → facade edges remain inward, not a route back into presentation. +- Quota parsing remains at the untrusted report boundary, preserving fail-unknown behavior. No new validation is added between typed internal calls. Existing >50-line functions are copied intact; this layer claims file-size relief, not a validator rewrite. + +## Tests + +Complete direct test-import `rg -l` list, all **unchanged** with old facade paths: + +```text +tests/gui/combo-workspace-data.test.ts +gui/tests/combo-strategy-roundtrip.test.ts +gui/tests/combo-native-alias-editor.test.tsx +gui/tests/combo-workspace-dirty.test.tsx +gui/tests/combos-detail-tabs-dom.test.tsx +``` + +Import line anchors: 19, 9, 5, 5, 16 respectively. The other eight importer files are `gui/src/combo-capabilities.ts`, `gui/src/pages/Combos.tsx`, and `gui/src/components/{ComboWorkspace.tsx,combo-workspace-add-modal.tsx,combo-workspace-controls.tsx,combo-workspace-detail-panel.tsx,combo-workspace-overview-panel.tsx,combo-workspace-types.ts}`. Counts are by file, not the multiple import declarations in some components. + +Text-oracle search: `rg -n 'combo-workspace-data(\.ts)?' tests gui/tests` and inspection of source-read sites found **no text reader of this file**. No retarget-to-leaf or add-leaf-to-scan-list action. `combos-detail-tabs-dom.test.tsx:139` reads the CSS, not this data module; unchanged. + +Guards to drive red once during implementation: make the Q aggregate completeness predicate accept an incomplete aggregate and observe the existing fail-unknown quota cases in `tests/gui/combo-workspace-data.test.ts` fail; restore it. Add a public-API identity/uniqueness assertion in that existing test if its current coverage does not distinguish a split counter: calls to `newComboTarget`, `emptyDraft`, and `parseComboList` must allocate distinct keys through the original facade. Drive that guard red by resetting the one counter for each allocation, then restore. Do not export the private sequence for tests. No new test file or test-layout registration is needed. Existing strategy, native-alias and dirty/quota-save behavior tests keep their assertions. + +## Verification + +Future implementation gate in the L3 worktree; domains `tests/gui` combo view-model and `gui/tests` combo forms/strategy: + +```sh +bun run typecheck +bun test tests/gui/combo-workspace-data.test.ts +bun test gui/tests/combo-strategy-roundtrip.test.ts gui/tests/combo-native-alias-editor.test.tsx gui/tests/combo-workspace-dirty.test.tsx gui/tests/combos-detail-tabs-dom.test.tsx +bun run privacy:scan +(cd gui && bun run lint && bun run build) +wc -l gui/src/combo-workspace-data.ts gui/src/combo-workspace-contracts.ts gui/src/combo-workspace-quota.ts +rg -l 'from ["\x27][^"\x27]*/combo-workspace-data(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts +sg run --kind import_statement --json=compact gui/src/combo-workspace-contracts.ts gui/src/combo-workspace-quota.ts +rg -n 'comboTargetKeySeq|COMBO_STRATEGY_SET' gui/src +git diff --check +git diff --numstat origin/dev...HEAD +``` + +Zero failures/exit 0; each source output ≤400. Original importer set remains thirteen until L4 deliberately introduces the controlled-content consumer; adding a public-API assertion changes no importing file count. The single definition sites of counter/Set and no-backlink import graph must be shown, including type edges. Conditional core-Lab test does not apply: no protected backend source touched. Record raw-diff budget exception or expanded map approval **before** implementation proceeds beyond this bounded plan. + +Full suites remotely only, on a parent-reserved, non-concurrent checkout: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-combo-workspace-data && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && (cd gui && bun install --frozen-lockfile && bun test tests)' +``` + +Record remote SHA matching the pushed PR head, actual full-suite exit status/log, exact-head CI rollup, and unchanged combo GUI screenshot for the PR template. No local full suite, service restart or deployment. This section is not a claim that any test ran during drafting. + +## Accept criteria + +1. Every one of 50 declarations plus the native-catalog re-export is assigned once; only T/Q are new files. +2. The facade preserves all current value/type exports, including zero-consumer exports and imported Set identity; nine moved types and four moved values use the exact named re-export block. +3. R ≤400, T ≤400, Q ≤400; movement totals reconcile to 650 − 284 = 366 before plumbing. Parent-approved disposition exists for the >500 raw diff; no unauthorized part or branch is added. +4. Exactly one `comboTargetKeySeq` and `COMBO_STRATEGY_SET` definition remains in R; target-key lifetime and all normalizer/serializer bodies are unchanged. +5. Q has only its T type import; T has no imports. No runtime or type-only cycles are introduced. +6. Thirteen original consumer files keep their paths; all five direct-import tests, the negative probes, typecheck/privacy/GUI lint/build, remote suites and exact-head CI have successful fresh evidence. +7. No report validation, quota TTL/exhaustion precedence, native-alias policy, UI copy, CSS, backend or API behavior changes; PR base is L2 and no merge occurs. + +## PR + +Title: `refactor(gui): isolate combo quota evidence and contracts (split S19 L3/4)` + +Branch: `codex/split-combo-workspace-data`. Base: `dev`. Closes: none. + +Fill every repository PR-template Summary / Verification / Checklist section; include unchanged-GUI screenshot and explicit raw-diff budget disposition. DEV-STACK-03 map: + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S19-L1 | codex/split-pages-compatibility-matrix-api | dev | pagination/error owner | +| 2 | #TBD-S19-L2 | codex/split-pages-CompatibilityMatrix | codex/split-pages-compatibility-matrix-api | matrix presentation leaves | +| 3 | #TBD-S19-L3 | codex/split-combo-workspace-data | dev | quota evidence and combo contracts; this layer | +| 4 | #TBD-S19-L4 | codex/split-components-combo-workspace-detail-panel | codex/split-combo-workspace-data | controlled Config contents | + +Base: dev — no dependency on lower layers; this layer is the parent of 660 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). + +Review this layer only. Merge only with separate user authorization; no auto-merge. diff --git a/devlog/_plan/260905_now_split_train/660_components_combo_workspace_detail_panel.md b/devlog/_plan/260905_now_split_train/660_components_combo_workspace_detail_panel.md new file mode 100644 index 0000000000..73251aad78 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/660_components_combo_workspace_detail_panel.md @@ -0,0 +1,181 @@ +# S19 L4 — Controlled Config contents inside the stable detail shell + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: **pure-move**, C3, docs-only delegated task; parent owns implementation and orchestration/loop state. +- Goal: reduce `gui/src/components/combo-workspace-detail-panel.tsx` (401 lines) below 400 by moving its Config form contents into one controlled sibling component. Keep the exported detail component, both mounted panel shells and all state lifetimes unchanged. +- Non-goals: tab/ARIA changes, styling, copy, component-state redesign, a hook extraction, save/validation changes, cleanup of existing callback dependencies, or removing the About content. +- Verifier: `002_layer_map.md` **Per-layer gate**, instantiated below with combo DOM/dirty/native-alias tests and source guards. +- Stop: pure-move layer diff, preserved export/DOM and state ownership, ≤400-line outputs, focused/build/privacy proof and remote exact-head full-suite/CI evidence; no merge. +- Escalation: prop behavior changes, a source guard needing unplanned weakening, new cycle, source drift, >400-line output, >500 raw source lines, or a requirement to move state beyond the approved JSX seam. Do not add a #b layer without the parent. +- Basis: docs `4cc219549`; code `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`. These line ranges are at origin/dev and were byte-verified against the working tree. Read train 000/001/002 and `260905_modular_debt_ledger/015_lane_gui.md`: its prescribed seam keeps tablist/panel shells at the old boundary and moves contents through typed draft/events. + +## Symbol inventory + +All top-level declarations from `rg` reconciled with `sg run --kind --json=compact gui/src/components/combo-workspace-detail-panel.tsx`. Imports are covered below, not declaration rows. Consumers are distinct importing files from `rg -l 'from ["\x27][^"\x27]*/combo-workspace-detail-panel(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts`, then `rg -l -w ''` within those files. R is the original path; F is `gui/src/components/combo-workspace-detail-config.tsx`. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| DetailTab | type | 21–21 | no | 0 | R | +| DETAIL_TABS | const array | 23–23 | no | 0 | R | +| detailTabDomId | const arrow function | 30–30 | no | 0 | R | +| detailPanelDomId | const arrow function | 31–31 | no | 0 | R | +| DetailPanel | function component | 33–401 | yes | 3 | R; nested Config JSX 251–378 moves to F | + +There are no existing independent top-level form components to move wholesale. The new `ComboDetailConfigFields` declaration and leaf-private `ComboDetailConfigFieldsProps` describe the extracted nested JSX only; they do not rename an existing public symbol. + +## Leaf partition + +Structural decision: retain controller, header, About content and accessibility shell; extract the existing `cwi-form-grid` at `gui/src/components/combo-workspace-detail-panel.tsx:251`. Current direction: `ComboWorkspace.tsx:12` and two direct tests → DetailPanel → data/controls/i18n/UI. Intended: same consumers → DetailPanel → F → data/controls/types; no F → DetailPanel import. Blast radius: combo presentation feature. + +Rejected alternatives: deleting a blank line only satisfies the size number, not the seam; moving the entire 369-line function creates a ~401-line leaf and loses shell locality; moving the tablist makes the source and ARIA guards needlessly migrate; lifting draft state into a new hook changes a lifetime the task must preserve. Reuse the existing StrategySeg/EffortSelect/TargetEditor/ComboCapabilities rather than create replacements. Sibling naming/props convention is present in `combo-workspace-controls.tsx`, `combo-workspace-overview-panel.tsx`, `combo-workspace-add-modal.tsx` and `combo-workspace-types.ts`. + +One NEW file: `gui/src/components/combo-workspace-detail-config.tsx`. + +- Symbols: exported `ComboDetailConfigFields`, leaf-private `ComboDetailConfigFieldsProps`. +- Body: copy original 251–378 (**128 lines**) as the component's returned root `
`. Preserve every field, event updater, label, condition and child component prop. The surrounding Config panel at original 243–249/380 and About shell at 386–398 remain in R, with the original always-mounted/hidden semantics. +- Expected **165 lines**, including imports and one props signature, not an unmeasured moved 401-line component. No hooks or new local draft state in F. `t` is passed from the existing owner, not replaced with an extra subscription. +- Its own imports: + +```ts +import { + type ComboItem, type ComboEffort, type ProviderQuotaStates, + comboModelId, comboPublicModelId, updateComboAliasDraft, + COMBO_STRATEGY_HINT_KEYS, COMBO_TARGETS_HINT_KEYS, +} from "../combo-workspace-data"; +import type { TFn } from "../i18n/shared"; +import type { ModelOption, ProviderOption } from "./combo-workspace-types"; +import { ComboCapabilities, EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { clampedNumberInput } from "./combo-workspace-utils"; +``` + +Use existing `TFn` from `gui/src/i18n/shared.ts:59`, not a duplicated translator signature. The exact private prop contract is: + +```ts +type ComboDetailConfigFieldsProps = { + t: TFn; + draft: ComboItem; + busy: boolean; + isCreate: boolean; + allowedEfforts: ComboEffort[]; + updateDraft: (updater: (prev: ComboItem) => ComboItem) => void; + providers: ProviderOption[]; + models: ModelOption[]; + providerQuotaStates: ProviderQuotaStates; +}; +``` + +All nine values already exist in the original function (props at 33–63, state/memos at 64–107). Pass them explicitly at the original form location: + +```tsx + +``` + +Residual `gui/src/components/combo-workspace-detail-panel.tsx`: **expected 285 lines**. Arithmetic: 401 − 128 + 11 call-site lines = 284 before import cleanup/new leaf import; removing form-only imports creates additional margin. Keep the original parentheses and both panel wrappers, headerModel, About section, all hooks/callbacks and current return structure. No #b required. Expected raw source additions/deletions about 310, below 500; verify at implementation, never minify to meet a limit. + +## Re-export block + +**No re-export is required.** `export function DetailPanel` remains at the original import path with the full unchanged public prop signature. No named or default export is moved out of R; do not expose the new private form component through R merely to create a barrel. Exact original public export remains `DetailPanel` only. + +Explicit residual import: + +```ts +import { ComboDetailConfigFields } from "./combo-workspace-detail-config"; +``` + +Drop form-only imports from R: `comboModelId`, `updateComboAliasDraft`, the four controls, `COMBO_STRATEGY_HINT_KEYS`, `COMBO_TARGETS_HINT_KEYS` and `clampedNumberInput`. Keep `comboPublicModelId` for save/header derivation at original 151 and 171. Keep `ComboItem`, `ProviderQuotaStates`, `comboQuotaState`, `draftEquals`, `intersectComboEfforts`, `validateComboDraft`, both icons, `useT`, `Notice`, option types and React hooks; they are still used by R. The imported names from the data facade resolve through L3's preserved exports. + +## Module-level state and cycles + +- No top-level mutable `let`, Map, Set, WeakMap, lock, cache or timer in this source. `DETAIL_TABS` at `gui/src/components/combo-workspace-detail-panel.tsx:23` stays a single read-only-used array in R. DOM-ID arrows at 30/31 also stay R. +- Tab state 65, draft/busy/message/copied state 84–87, the `baselineSyncKey:90`, effortMap's per-memo Map at 92, and `allowedEfforts:98` all remain in R. Do not recreate them per F mount. +- `updateDraft:103`, delayed baseline reset at 109–119, clipboard reset timer at 121–129 and save logic at 131–168 remain unchanged. The callback closes over the same draft/baseline as before; this plan does not opportunistically rewrite it to a functional state setter. +- F is controlled and stateless. No `useState`, effect, memo, new provider or default model/target allocation. Existing updater expressions move inside the same rendered form context; no React `memo`, keys or conditional mounting are added. +- Avoid R → F → R (including props type imports): define the small props type in F from the pre-existing neutral option/data/i18n contracts, never `Parameters`. With L3, data facade → quota → contracts stays inward; controls/options do not import DetailPanel. No new type-only or runtime cycle. + +## Tests + +Complete direct test-import `rg -l` list (unchanged): + +```text +gui/tests/combos-detail-tabs-dom.test.tsx +gui/tests/combo-native-alias-editor.test.tsx +``` + +Import lines 14 and 6 respectively. Third importer: `gui/src/components/ComboWorkspace.tsx:12`, unchanged. Add focused integration coverage via the existing indirect `gui/tests/combo-workspace-dirty.test.tsx` (mounts ComboWorkspace); it protects editing/revert/navigation and exhausted-quota save gating. + +Every discovered source-text reader: + +| test/read location at origin/dev | disposition | reason/action | +|---|---|---| +| `gui/tests/combos-detail-segmented.test.ts:15` starts `Bun.file`, line 16 names `../src/components/combo-workspace-detail-panel.tsx` | unchanged | all asserted tablist/tab/tabpanel/segmented markup remains in R; no retarget and no scan-list expansion | + +The same test's CSS read at 18–20 remains unchanged. `gui/tests/combos-detail-tabs-dom.test.tsx:139` reads only `styles-combos-workspace.css`, not this TSX file. Searches for full basename and extensionless stem across `tests` and `gui/tests` found no other source reader. Do not retarget the shell guard to F: F owns no tab roles. + +Drive guards red once during implementation: remove/mistype `role="tablist"` in the retained shell and require the segmented test to fail; restore it. Temporarily miswire the moved alias input's update callback in F and require `combo-native-alias-editor.test.tsx`'s edit/metadata case to fail; restore it. Mounted tab tests must still prove both IDREF targets exist, exactly one panel is exposed, roving tabindex works and About is focusable. No test runs or mutations happen in the docs task. + +## Verification + +Future L4 worktree gate, domains GUI combo controls, tabs, native alias and dirty navigation: + +```sh +bun run typecheck +bun test gui/tests/combos-detail-segmented.test.ts gui/tests/combos-detail-tabs-dom.test.tsx gui/tests/combo-native-alias-editor.test.tsx gui/tests/combo-workspace-dirty.test.tsx +bun run privacy:scan +(cd gui && bun run lint && bun run build) +wc -l gui/src/components/combo-workspace-detail-panel.tsx gui/src/components/combo-workspace-detail-config.tsx +rg -l 'from ["\x27][^"\x27]*/combo-workspace-detail-panel(\.tsx?)?["\x27]' src gui/src gui/tests tests scripts +sg run --kind import_statement --json=compact gui/src/components/combo-workspace-detail-config.tsx +git diff --check +git diff --numstat codex/split-combo-workspace-data...HEAD +``` + +All checks exit 0 / tests zero failures; both files ≤400. DetailPanel importer set stays exactly three. The data facade's importer set intentionally grows from thirteen to fourteen because F now imports it; no existing consumer is redirected. Compare the new graph including type edges and ensure F never imports R. No protected backend path is touched, so conditional core-Lab test is not required. GUI-copy/i18n keys are unchanged; lint/build still apply. Check the layer-only moved JSX and wrapper diff against original spans and capture unchanged Config/About screenshots. + +Full suites remotely only, using a parent-allocated checkout without concurrent stack checkout changes: + +```sh +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-components-combo-workspace-detail-panel && git checkout -q FETCH_HEAD && bun install --frozen-lockfile && bun run test && (cd gui && bun install --frozen-lockfile && bun test tests)' +``` + +Record remote SHA equal to PR tip, actual exit status/full log and exact-head CI rollup. No local full suite, deployment or live service restart is authorized by this plan. None of the test/build commands were run while drafting. + +## Accept criteria + +1. All five original declarations remain in R; its sole export `DetailPanel` and public prop contract are unchanged, with three unchanged importing files. +2. Exactly one new F file, expected 165 lines, contains the original 128-line form and the explicit typed props only; both files are ≤400 and raw source diff ≤500. +3. F contains no state/effects; all state, timers, save/copy handlers, request callbacks and baseline synchronization stay in R. +4. Both tabpanel shells remain mounted with the same ids, hidden conditions and About focusability; no extra DOM wrapper or ARIA/CSS/i18n change. +5. There is no F → R runtime/type dependency; L3 data exports remain intact. Existing component public imports are not rewritten. +6. The unchanged shell text guard and alias behavior guard fail under their specified negative probes and pass when restored; all four focused test files, typecheck/privacy/GUI lint/build, remote full suites and exact-head CI have fresh success evidence. +7. PR targets L3's branch, upper/lower ancestry is parent-verified, all four stack links are present, and no merge occurs. + +## PR + +Title: `refactor(gui): extract controlled combo configuration fields (split S19 L4/4)` + +Branch: `codex/split-components-combo-workspace-detail-panel`. Base: `codex/split-combo-workspace-data`. Closes: none. + +Use the full repository PR-template Summary / Verification / Checklist; attach unchanged Config/About GUI screenshots. DEV-STACK-03 map: + +| # | PR | Branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S19-L1 | codex/split-pages-compatibility-matrix-api | dev | pagination/error owner | +| 2 | #TBD-S19-L2 | codex/split-pages-CompatibilityMatrix | codex/split-pages-compatibility-matrix-api | matrix presentation leaves | +| 3 | #TBD-S19-L3 | codex/split-combo-workspace-data | dev | quota evidence and combo contracts | +| 4 | #TBD-S19-L4 | codex/split-components-combo-workspace-detail-panel | codex/split-combo-workspace-data | controlled Config contents; this layer | + +Depends on #TBD-S19-L3; review this layer only. Parent cascades edits to `codex/split-combo-workspace-data` into this layer before refreshing review/CI (DEV-STACK-02). Merge after that parent only on separate user authorization; no auto-merge. diff --git a/devlog/_plan/260905_now_split_train/670_pages_ClaudeDesktop.md b/devlog/_plan/260905_now_split_train/670_pages_ClaudeDesktop.md new file mode 100644 index 0000000000..84298179eb --- /dev/null +++ b/devlog/_plan/260905_now_split_train/670_pages_ClaudeDesktop.md @@ -0,0 +1,195 @@ +# S20 L1/5 — ClaudeDesktop + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. C3 architecture, docs-only delegated preparation; parent owns all orchestration/loop/goal state. +- Goal: split `gui/src/pages/ClaudeDesktop.tsx` into 3 cohesive sibling leaves, each ≤400 lines, with a projected 374-line residual and every existing export still importable from the old path. +- Non-goals: no behavior, copy, CSS, locale, request payload, exported name/signature, effect lifetime, auth/consent, or dependency changes. No source edits, test runs, Git mutation, PR creation or orchestration in this planning task. Existing long functions are not silently rewritten to satisfy a second metric. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated in Verification below (the 000 reference to “003” is stale; 002 is authoritative). +- Stop: plan complete when inventory/partition/export/state/oracle/gate records are internally consistent. Implementation stops on any failed gate or non-pure-move delta; completion later requires exact-tip checks and exact-head green CI, never a cached green check. +- Escalation: BLOCKED FOR IMPLEMENTATION on changeset size: even the theoretical minimum 289-line extraction to reach 400 costs at least 578 added+deleted source lines; this concrete plan moves 370 lines before glue. 002's ≤500 changed-source-lines policy cannot coexist with its single L1 allocation. Parent must explicitly approve a pure-move size exception or revise the topology with a ClaudeDesktop #b in another stack (S20 is already at its five-layer cap). This document does not approve that exception or add a sixth layer. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. Read with `git show origin/dev:gui/src/pages/ClaudeDesktop.tsx`; the working-tree copy was byte-compared and identical. All source ranges below are inclusive at this origin/dev revision. Read 000_plan.md, 001_stale_check.md, S20 rows / Per-layer gate in 002_layer_map.md, and the matching section in `../260905_modular_debt_ledger/015_lane_gui.md`. + +Structural decision (ARCH-DECISION-01 / ARCH-MAP-01): Context: 689-line page mixes profile/cache DTOs with two presentation blocks. Rejected do-nothing/config/delete: none reduces executable file size without losing features. Rejected moving the entire 539-line default component: it only relocates the violation. Reuse claude-desktop-lane.ts and collapse-store.ts unchanged; their helpers already own filtering and persistence. Chosen move: sibling data + stateless lane/status leaves, while the original remains the resource/save owner. Blast radius is the Claude Desktop feature; Claude.tsx and its two direct test importers retain the default boundary. + +## Symbol inventory + +Inventory uses installed ast-grep: `sg run --kind --json=compact gui/src/pages/ClaudeDesktop.tsx`, filtered to top-level declarations and checked against `git show origin/dev:gui/src/pages/ClaudeDesktop.tsx | nl -ba`. Imports are included for completeness but are not newly owned declarations. + +Consumer count = distinct external source/test files importing that binding from the original module, not identifier occurrences or documentation mentions. Command candidate set: `rg -l 'ClaudeDesktop' src gui/src scripts tests gui/tests`; inspect matched import clauses for each symbol and deduplicate files. Private declarations/import bindings have zero external consumers by definition; local uses are preserved through the explicit imports below. Module fan-in is **3 files** (including type/test imports); added leaf imports do not replace existing consumer imports. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";` | import declaration | 1–1 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { LANE_PAGE, defaultCollapsedFamilies, laneView, rowStartsOpen } from "./claude-desktop-lane";` | import declaration | 2–2 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { makeCollapseStore, toggleInSet } from "./collapse-store";` | import declaration | 3–3 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { IconChevron } from "../icons";` | import declaration | 4–4 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { EmptyState, Notice } from "../ui";` | import declaration | 5–5 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { LOCALES, useI18n, type TFn, type TKey } from "../i18n/shared";` | import declaration | 6–6 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { readJsonIfOk, readJsonOrThrow } from "../fetch-json";` | import declaration | 7–7 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { readSessionListCacheEntry, writeSessionListCacheEntry } from "../session-list-cache";` | import declaration | 8–8 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { useDataSurface } from "../data-surface";` | import declaration | 9–9 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { DataSurfaceSkeleton } from "../components/data-surface";` | import declaration | 10–10 | no | 0 external | allocation in Leaf partition / residual imports | +| `FAMILIES` | lexical declaration | 12–12 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `Family` | type alias declaration | 13–13 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `FAMILY_COLLAPSE` | lexical declaration | 19–19 | no | 0 | `gui/src/pages/ClaudeDesktop.tsx` | +| `Assignment` | interface declaration | 21–24 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `DesktopProfile` | interface declaration | 26–33 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `DesktopModel` | interface declaration | 35–43 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `DesktopStatus` | interface declaration | 45–57 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `DesktopResponse` | interface declaration | 59–64 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `PendingAction` | type alias declaration | 66–66 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `FAMILY_KEYS` | lexical declaration | 68–73 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `cloneProfile` | function declaration | 75–88 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `normalizeProfile` | function declaration | 90–109 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `errorMessage` | function declaration | 111–114 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `formatContextWindow` | function declaration | 116–124 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `CachedDesktop` | type alias declaration | 126–126 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `readDesktopCache` | function declaration | 128–130 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `readDesktopCachedAt` | function declaration | 132–134 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `seedDesktop` | function declaration | 136–149 | no | 0 | `gui/src/pages/claude-desktop-data.ts` | +| `ClaudeDesktop` | function declaration | 151–689 | default | 3 | `gui/src/pages/ClaudeDesktop.tsx` (residual; JSX ranges below move) | + +Current direct importer files (same import paths after the move): + +- `gui/tests/claude-desktop-vertical.test.tsx` +- `gui/tests/claude-desktop-row-disclosure.test.tsx` +- `gui/src/pages/Claude.tsx` + +## Leaf partition + +Reuse decision: no parallel infrastructure, utility barrel, controller or cache is introduced. The source-owned definitions above move rather than being copied; existing helpers named in Loop spec remain canonical. Sibling convention is feature-qualified lowercase helper filenames and PascalCase component files (e.g. `gui/src/pages/claude-desktop-lane.ts`, `gui/src/pages/dashboard-core-poll.ts`, `gui/src/components/provider-workspace/ProviderRail.tsx`). No `index.ts`, `utils.ts` or `common.ts` is created. + +### gui/src/pages/claude-desktop-data.ts + +- Symbols: FAMILIES Family Assignment DesktopProfile DesktopModel DesktopStatus DesktopResponse PendingAction FAMILY_KEYS cloneProfile normalizeProfile errorMessage formatContextWindow CachedDesktop readDesktopCache readDesktopCachedAt seedDesktop. +- Expected physical lines: 145 (including imports and new prop signatures; maximum 400). +- Move origin/dev lines 12–13 and 21–149 (131 physical lines, including separators). Keep the collapse adapter at the original path. Export only the types/values actually consumed by the page or the two presentation leaves; readDesktopCache stays private. These are the existing DTOs, not aliases to server contracts: normalizing DesktopResponse must remain identical. +- Own imports: + +```ts +import type { TFn, TKey } from "../i18n/shared"; +import { readSessionListCacheEntry } from "../session-list-cache"; +``` + +### gui/src/pages/claude-desktop-lanes.tsx + +- Symbols: ClaudeDesktopLanes (new extraction of ClaudeDesktop lines 498–686). +- Expected physical lines: 250 (including imports and new prop signatures; maximum 400). +- Move the complete group-stack JSX and family/row map (189 lines). No hooks or state migrate. Inline typed props carry t, modelsByFamily, profile (assignments/defaults), effectiveDefaults, destinations, laneSearch, laneLimit, collapsedFamilies, openRows; use narrow callbacks onDrop(event, family), onToggleFamily(family), onSearch(family, query), onMore(family), onToggleRow(route, next), onDefault(family, route), onDestination(route, family), onMove(route, family). The page retains the exact functional updater bodies from lines 558–564, 593, 641, 653 and 675. Key by family/route exactly as before; filtering stays downstream of modelsByFamily/effectiveDefaults. No extra DOM wrapper. +- Own imports: + +```ts +import type { DragEvent } from "react"; +import type { TFn } from "../i18n/shared"; +import { IconChevron } from "../icons"; +import { LANE_PAGE, laneView, rowStartsOpen } from "./claude-desktop-lane"; +import { FAMILIES, FAMILY_KEYS, formatContextWindow } from "./claude-desktop-data"; +import type { Family, DesktopModel, DesktopProfile } from "./claude-desktop-data"; +``` + +### gui/src/pages/claude-desktop-status.tsx + +- Symbols: ClaudeDesktopStatus (new extraction of ClaudeDesktop lines 426–475). +- Expected physical lines: 70 (including imports and new prop signatures; maximum 400). +- Move the 50-line status-bar block with its leading comment. Props are status: DesktopStatus | null, statusFailed: boolean, localeTag: string | undefined, and t: TFn. Retain the pending strut, activeProfile precedence, aria-busy expression and health copy exactly. No polling, effects or new state. +- Own imports: + +```ts +import type { TFn } from "../i18n/shared"; +import type { DesktopStatus } from "./claude-desktop-data"; +``` + +Residual `gui/src/pages/ClaudeDesktop.tsx`: **374 expected lines**. 689 − 131 (data) − 189 (lanes) − 50 (status) + 55 (replacement calls, callback bindings and import budget) = 374 residual lines. Leaf budgets 145 + 250 + 70 = 465; aggregate 839 = original 689 + 150 net extraction overhead. No #b is currently allocated. These are explicit physical-line budgets, not measured implementation output: reject a formatted result above the budget/400 rather than minifying it. The exact moved source blocks are disjoint; every original declaration has exactly one target in the inventory. Preserve associated comments, including i18n/lint exceptions. + +## Re-export block + +The only existing export is the default ClaudeDesktop declaration (151–689); keep it declared/exported in the residual. Exact new re-export block: empty (no existing exported symbol moves). Do not add named exports for formerly private helpers. + +Explicit local bindings needed in the residual (a re-export binds nothing): + +```ts +import { FAMILIES, FAMILY_KEYS, cloneProfile, normalizeProfile, errorMessage, readDesktopCachedAt, seedDesktop } from "./claude-desktop-data"; +import type { Family, DesktopProfile, DesktopModel, DesktopStatus, DesktopResponse, PendingAction, CachedDesktop } from "./claude-desktop-data"; +import { ClaudeDesktopLanes } from "./claude-desktop-lanes"; +import { ClaudeDesktopStatus } from "./claude-desktop-status"; +``` + +Retain original external imports still used by residual declarations; remove only moved-only bindings after reference checks. The listed leaf imports use verified existing modules or the exact new owners defined in this plan. Internal leaves import each other directly, never through the preserved original-path compatibility boundary. No wildcard re-export. + +## Module-level state and cycles + +FAMILY_COLLAPSE at 19 remains the one module-level persistence handle in ClaudeDesktop.tsx. makeCollapseStore is external-storage-backed (collapse-store.ts:35), not a new cache. FAMILIES (12) and FAMILY_KEYS (68–73) have one read-only owner in claude-desktop-data.ts. All Sets at 180/207 and draft hooks remain component-local. Neither view nor data leaf imports ClaudeDesktop.tsx; views → data and existing claude-desktop-lane, page → views/data. No leaf acquires the resource or save/apply lifetime. New edges are functional props/imports; no shared mutable module state is introduced. + +Cycle proof for the implementation gate: resolve static import/export edges, including type-only edges, from this original and its new leaves; fail if any leaf reaches the original (directly or transitively), or the changed induced graph has an SCC. Run the lane-015 read-only sg/import-resolution + Tarjan method; preserve the allow-edge and forbidden-back-edge evidence. No new graph tool/dependency installation is authorized. The plan records an acyclic intended edge map, not a claim that future source has been scanned. + +## Tests + +Direct importing tests — `rg -l` candidate list narrowed to actual imports of this module; **2 files**, all **unchanged**: + +- `gui/tests/claude-desktop-vertical.test.tsx` — unchanged original-path import. +- `gui/tests/claude-desktop-row-disclosure.test.tsx` — unchanged original-path import. + +Text-oracle disposition: `gui/tests/page-loading-contract.test.tsx` — unchanged source target `gui/src/pages/ClaudeDesktop.tsx`: path entry at 42; actual reader at 22 and calls at 51, 60, 67, 80, 95, 111. All positive resource/skeleton/error predicates stay in the residual. Do not retarget them to stateless leaves or concatenate files to mask a missing resource owner. The `.loading` string in the retained skeleton at source 402 still satisfies the existing lexical field guard; do not claim that this regex proves loading behavior. No other literal/extensionless source reader was found. No add-leaf-to-scan-list needed for these stateless leaves. + +Guards to drive red once during implementation C verification: Drive page-loading-contract's cold-skeleton guard red once by replacing the residual DataSurfaceSkeleton use/import, then restore it; drive the mounted row-disclosure assertion red once by inverting rowOpen in claude-desktop-lanes.tsx, then restore. Preserve the tests' full-model default and collapse semantics. Record the named failing assertion and restored green result; do not commit mutations. Do not weaken assertions, replace source guards with export-existence checks, or retarget behavioral tests away from the compatibility boundary. No guard has been executed during this documentation task. + +## Verification + +Future executor commands only — not run by this delegated author. In a dedicated layer worktree at its tip, instantiate 002 Per-layer gate: + +```sh +bun run typecheck +bun test gui/tests/claude-desktop-row-disclosure.test.tsx gui/tests/claude-desktop-vertical.test.tsx gui/tests/page-loading-contract.test.tsx gui/tests/claude-desktop-lane.test.ts +bun run privacy:scan +wc -l gui/src/pages/claude-desktop-data.ts gui/src/pages/claude-desktop-lanes.tsx gui/src/pages/claude-desktop-status.tsx gui/src/pages/ClaudeDesktop.tsx +rg -l 'from "[^"]*/ClaudeDesktop(\.tsx?)?"' src gui/src scripts tests gui/tests +# GUI TypeScript/bundler proof and scoped lint, required by gui/AGENTS.md: +(cd gui && bun run build && bun run lint) +# Whole repository suite only on the approved remote host: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-ClaudeDesktop && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' +# Full GUI PR-ready suite also remote, never substitute it for the root suite: +ssh lidge 'cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile && bun test tests' +``` + +Focused domains: `gui/tests`; only the listed files run locally. The core-Lab boundary gate is N/A: no `src/server`, `src/router`, or `src/lib` source is touched; never edit its protected roots. Unchanged UI copy means no locale churn; if copy unexpectedly changes, stop the pure-move layer rather than manufacturing new translations. + +Compare the importer list with the 3-file baseline above (count files, not lines; compare existing callers, excluding newly added internal leaves). Compare exported name/kind/signature inventory and explicit local bindings, inspect `git diff --numstat dev...HEAD -- gui/src` against the 500 added+deleted source-line cap, and perform the changed-graph cycle check described above. The remote checkout SHA must equal this PR head; serialize the shared lidge checkout or arrange parent-owned isolation before running it. Do not accept a later remote GUI run on another layer's SHA. Require actual exit statuses and full-suite totals: the command deliberately avoids 002's unguarded `| tail -15`, which could hide failure. Record exact-head CI for the layer and do not merge. + +Docs-only verification for this author: inspect only these five requested output documents for nine exact ordered headings, complete declaration coverage, ≤400 projected leaf/residual budgets, correct branch/base/stack map, and whitespace with `git diff --no-index --check /dev/null `. No runtime, build, privacy or test-pass result is claimed here. + +## Accept criteria + +1. Every top-level declaration in the origin/dev inventory has one canonical owner; moved blocks match original behavior and no unlisted source file is changed. +2. Existing default/named/type exports and signatures remain importable from `gui/src/pages/ClaudeDesktop.tsx`; all 3 existing importer files retain their paths. Re-exported symbols used locally have explicit imports. +3. Exactly 3 new leaves appear at the paths above, each ≤400 physical lines; residual ≤400 (budget 374); actual formatted counts and source diff size are recorded. Parent size-policy/topology resolution is mandatory before implementation; this plan alone is not approval. +4. State lifetime/ownership and side-effect timing match Module-level state and cycles; changed graph has no new value or type cycle and no upward leaf → original path. +5. Every listed behavioral/text oracle keeps its specified target/disposition; the named guard mutation produces the expected failure and restoration yields green focused tests. +6. Typecheck, focused checks, GUI build/lint, privacy scan, remote whole-suite and remote GUI PR-ready suite pass at the exact layer head, with exit codes and CI SHA evidence; no repository-wide local suite. +7. PR contains all repository template sections and the five-layer stack map; correct base/head, no merge, no release, no unrelated cleanup. If title/body says GUI, attach a real unchanged-UI screenshot as required by the repository gate; never fabricate an image link. + +## PR + +Title: `refactor(gui): isolate Claude Desktop profile data and lane views (split S20 L1/5)` + +Head: `codex/split-pages-ClaudeDesktop`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, Checklist; pure move only. Review only this layer's diff; publish later under parent authorization. Placeholder PR numbers below are intentional until PR creation, not fabricated existing PRs. + +| Layer | PR | Head branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S20-L1 | `codex/split-pages-ClaudeDesktop` | `dev` | isolate Claude Desktop profile data and lane views ← this layer | +| 2 | #TBD-S20-L2 | `codex/split-components-MemoryObservabilityCard` | `dev` | separate memory metrics and stat views from restart polling | +| 3 | #TBD-S20-L3 | `codex/split-components-provider-workspace-ProviderSettings` | `dev` | extract provider draft helpers and stateless settings fields | +| 4 | #TBD-S20-L4 | `codex/split-pages-dashboard-shared` | `dev` | isolate dashboard sidecar option contracts and selection | +| 5 | #TBD-S20-L5 | `codex/split-components-QuotaBars` | `dev` | extract quota reset date and locale formatting | + +DEV-STACK-03: each of the five layers carries its own gates and this complete map. S20 groups execution order and PR navigation only; all five layers are independent under STACK-INDEPENDENCE-01. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge remains forbidden here (DEV-STACK-04). diff --git a/devlog/_plan/260905_now_split_train/680_components_MemoryObservabilityCard.md b/devlog/_plan/260905_now_split_train/680_components_MemoryObservabilityCard.md new file mode 100644 index 0000000000..c7d4d641db --- /dev/null +++ b/devlog/_plan/260905_now_split_train/680_components_MemoryObservabilityCard.md @@ -0,0 +1,174 @@ +# S20 L2/5 — MemoryObservabilityCard + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. C3 architecture, docs-only delegated preparation; parent owns all orchestration/loop/goal state. +- Goal: split `gui/src/components/MemoryObservabilityCard.tsx` into 2 cohesive sibling leaves, each ≤400 lines, with a projected 356-line residual and every existing export still importable from the old path. +- Non-goals: no behavior, copy, CSS, locale, request payload, exported name/signature, effect lifetime, auth/consent, or dependency changes. No source edits, test runs, Git mutation, PR creation or orchestration in this planning task. Existing long functions are not silently rewritten to satisfy a second metric. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated in Verification below (the 000 reference to “003” is stale; 002 is authoritative). +- Stop: plan complete when inventory/partition/export/state/oracle/gate records are internally consistent. Implementation stops on any failed gate or non-pure-move delta; completion later requires exact-tip checks and exact-head green CI, never a cached green check. +- Escalation: Stop for any proposed restart-controller extraction, changed poll interval/cancellation semantics, duplicated formatter cache, or actual source diff over 500 lines; report to parent instead of extending L2. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. Read with `git show origin/dev:gui/src/components/MemoryObservabilityCard.tsx`; the working-tree copy was byte-compared and identical. All source ranges below are inclusive at this origin/dev revision. Read 000_plan.md, 001_stale_check.md, S20 rows / Per-layer gate in 002_layer_map.md, and the matching section in `../260905_modular_debt_ledger/015_lane_gui.md`. + +Structural decision (ARCH-DECISION-01 / ARCH-MAP-01): Context: the 527-line card combines reusable scalar rendering with an effectful restart controller. Rejected delete/configure/no-op: does not address size. Rejected hook extraction: would disturb cancellation and drain/PID lifetime unnecessarily. Reuse formatUptime and existing bounded-fetch/visibility-poll owners. Chosen move is metrics/cache ownership plus stat views. Card's dashboard-overview-panels.tsx caller and public test import remain unchanged; blast radius is one component feature. + +## Symbol inventory + +Inventory uses installed ast-grep: `sg run --kind --json=compact gui/src/components/MemoryObservabilityCard.tsx`, filtered to top-level declarations and checked against `git show origin/dev:gui/src/components/MemoryObservabilityCard.tsx | nl -ba`. Imports are included for completeness but are not newly owned declarations. + +Consumer count = distinct external source/test files importing that binding from the original module, not identifier occurrences or documentation mentions. Command candidate set: `rg -l 'MemoryObservabilityCard' src gui/src scripts tests gui/tests`; inspect matched import clauses for each symbol and deduplicate files. Private declarations/import bindings have zero external consumers by definition; local uses are preserved through the explicit imports below. Module fan-in is **2 files** (including type/test imports); added leaf imports do not replace existing consumer imports. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `import { useEffect, useState } from "react";` | import declaration | 1–1 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { formatUptime } from "../formatUptime";` | import declaration | 2–2 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { IconActivity } from "../icons";` | import declaration | 3–3 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { useI18n, type Locale, type TFn } from "../i18n/shared";` | import declaration | 4–4 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch";` | import declaration | 5–5 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { startVisibilityPoll } from "../visibility-poll";` | import declaration | 6–6 | no | 0 external | allocation in Leaf partition / residual imports | +| `MemorySample` | interface declaration | 15–24 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `MemoryMetric` | type alias declaration | 26–26 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `ResponseState` | interface declaration | 28–40 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `SystemMemory` | interface declaration | 42–58 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `RestartPhase` | type alias declaration | 60–60 | no | 0 | `gui/src/components/MemoryObservabilityCard.tsx` | +| `byteNumberFormats` | lexical declaration | 69–69 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `byteNumberFormat` | function declaration | 70–81 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `plainNumberFormats` | lexical declaration | 82–82 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `plainNumberFormat` | function declaration | 83–90 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `formatBytes` | function declaration | 92–99 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `formatAge` | function declaration | 102–105 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `observedMemory` | function declaration | 107–110 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `observedMetric` | function declaration | 112–121 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `observedGrowthPerHour` | function declaration | 124–131 | no | 0 | `gui/src/components/memory-observability-metrics.ts` | +| `Stat` | function declaration | 134–142 | no | 0 | `gui/src/components/memory-observability-stats.tsx` | +| `MemoryPressure` | function declaration | 149–198 | no | 0 | `gui/src/components/memory-observability-stats.tsx` | +| `DRAIN_TIMEOUT_S` | lexical declaration | 200–200 | no | 0 | `gui/src/components/MemoryObservabilityCard.tsx` | +| `RECONNECT_POLL_MS` | lexical declaration | 201–201 | no | 0 | `gui/src/components/MemoryObservabilityCard.tsx` | +| `RECONNECT_GIVE_UP_MS` | lexical declaration | 202–202 | no | 0 | `gui/src/components/MemoryObservabilityCard.tsx` | +| `MemoryObservabilityCard` | function declaration | 204–527 | default | 2 | `gui/src/components/MemoryObservabilityCard.tsx` | + +Current direct importer files (same import paths after the move): + +- `gui/src/pages/dashboard-overview-panels.tsx` +- `gui/tests/memory-observability-card.test.tsx` + +## Leaf partition + +Reuse decision: no parallel infrastructure, utility barrel, controller or cache is introduced. The source-owned definitions above move rather than being copied; existing helpers named in Loop spec remain canonical. Sibling convention is feature-qualified lowercase helper filenames and PascalCase component files (e.g. `gui/src/pages/claude-desktop-lane.ts`, `gui/src/pages/dashboard-core-poll.ts`, `gui/src/components/provider-workspace/ProviderRail.tsx`). No `index.ts`, `utils.ts` or `common.ts` is created. + +### gui/src/components/memory-observability-metrics.ts + +- Symbols: MemorySample MemoryMetric ResponseState SystemMemory byteNumberFormats byteNumberFormat plainNumberFormats plainNumberFormat formatBytes formatAge observedMemory observedMetric observedGrowthPerHour. +- Expected physical lines: 120 (including imports and new prop signatures; maximum 400). +- Move lines 15–58 and 62–131 (114 physical lines). The two formatter Maps and their accessors move together. Export SystemMemory/MemoryMetric and consumed format/measurement functions; MemorySample/ResponseState and byteNumberFormat remain private unless a production import requires them. Keep Intl key semantics, binary units and the observed-memory precedence byte-for-byte. +- Own imports: + +```ts +import type { Locale } from "../i18n/shared"; +import { formatUptime } from "../formatUptime"; +``` + +### gui/src/components/memory-observability-stats.tsx + +- Symbols: Stat MemoryPressure. +- Expected physical lines: 70 (including imports and new prop signatures; maximum 400). +- Move lines 133–198 (66 physical lines) unchanged apart from export keywords/imports. Keep inline prop signatures, warn threshold, CSS custom property, and locale/translator passed by the card. +- Own imports: + +```ts +import type { Locale, TFn } from "../i18n/shared"; +import { formatBytes } from "./memory-observability-metrics"; +import type { MemoryMetric } from "./memory-observability-metrics"; +``` + +Residual `gui/src/components/MemoryObservabilityCard.tsx`: **356 expected lines**. 527 − 114 (DTO/metrics blocks) − 66 (stat views) + 9 (import/separator budget) = 356 residual lines. Leaves 120 + 70 = 190; aggregate 546 = 527 + 19 net overhead. No #b required. These are explicit physical-line budgets, not measured implementation output: reject a formatted result above the budget/400 rather than minifying it. The exact moved source blocks are disjoint; every original declaration has exactly one target in the inventory. Preserve associated comments, including i18n/lint exceptions. + +## Re-export block + +The sole public export is default MemoryObservabilityCard (204–527), retained in place. Exact new re-export block: empty. The moved metrics were private and must not be added to the original public surface. + +Explicit local bindings needed in the residual (a re-export binds nothing): + +```ts +import { plainNumberFormat, formatBytes, formatAge, observedMemory, observedMetric, observedGrowthPerHour } from "./memory-observability-metrics"; +import type { SystemMemory } from "./memory-observability-metrics"; +import { Stat, MemoryPressure } from "./memory-observability-stats"; +``` + +Retain original external imports still used by residual declarations; remove only moved-only bindings after reference checks. The listed leaf imports use verified existing modules or the exact new owners defined in this plan. Internal leaves import each other directly, never through the preserved original-path compatibility boundary. No wildcard re-export. + +## Module-level state and cycles + +byteNumberFormats (69) and plainNumberFormats (82) each have one owner: memory-observability-metrics.ts. Never duplicate them in stats or the residual. DRAIN_TIMEOUT_S (200), RECONNECT_POLL_MS (201), RECONNECT_GIVE_UP_MS (202) remain immutable constants in the card. RestartPhase (60) stays there too. cancelled/inFlight/active at 230–232 and 293–295, started at 296, and the timers are effect-local, not globals; do not move any. Card → stats → metrics; card → metrics; metrics → formatUptime/i18n types. No upward import. Existing locale caches are encapsulated common state with one unchanged owner; all new intermodule calls are functional. + +Cycle proof for the implementation gate: resolve static import/export edges, including type-only edges, from this original and its new leaves; fail if any leaf reaches the original (directly or transitively), or the changed induced graph has an SCC. Run the lane-015 read-only sg/import-resolution + Tarjan method; preserve the allow-edge and forbidden-back-edge evidence. No new graph tool/dependency installation is authorized. The plan records an acyclic intended edge map, not a claim that future source has been scanned. + +## Tests + +Direct importing tests — `rg -l` candidate list narrowed to actual imports of this module; **1 files**, all **unchanged**: + +- `gui/tests/memory-observability-card.test.tsx` — unchanged original-path import. + +Text-oracle disposition: No test reads MemoryObservabilityCard.tsx as source: literal basename and extensionless path searches return only the behavioral importer. No retarget-to-leaf or add-leaf-to-scan-list action. `gui/tests/memory-observability-card.test.tsx:6` remains an unchanged public-path import, not a text oracle. + +Guards to drive red once during implementation C verification: No retargeted text guard exists. Drive gui/tests/memory-observability-card.test.tsx:111 red once by perturbing the binary unit selection in the metrics leaf, then restore. Preserve existing unmount (128), unavailable (140), confirm/restart (155), reconnect-management-health (189) and old-payload (230) assertions. Do not trigger a real server restart. Record the named failing assertion and restored green result; do not commit mutations. Do not weaken assertions, replace source guards with export-existence checks, or retarget behavioral tests away from the compatibility boundary. No guard has been executed during this documentation task. + +## Verification + +Future executor commands only — not run by this delegated author. In a dedicated layer worktree at its tip, instantiate 002 Per-layer gate: + +```sh +bun run typecheck +bun test gui/tests/memory-observability-card.test.tsx +bun run privacy:scan +wc -l gui/src/components/memory-observability-metrics.ts gui/src/components/memory-observability-stats.tsx gui/src/components/MemoryObservabilityCard.tsx +rg -l 'from "[^"]*/MemoryObservabilityCard(\.tsx?)?"' src gui/src scripts tests gui/tests +# GUI TypeScript/bundler proof and scoped lint, required by gui/AGENTS.md: +(cd gui && bun run build && bun run lint) +# Whole repository suite only on the approved remote host: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-components-MemoryObservabilityCard && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' +# Full GUI PR-ready suite also remote, never substitute it for the root suite: +ssh lidge 'cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile && bun test tests' +``` + +Focused domains: `gui/tests`; only the listed files run locally. The core-Lab boundary gate is N/A: no `src/server`, `src/router`, or `src/lib` source is touched; never edit its protected roots. Unchanged UI copy means no locale churn; if copy unexpectedly changes, stop the pure-move layer rather than manufacturing new translations. + +Compare the importer list with the 2-file baseline above (count files, not lines; compare existing callers, excluding newly added internal leaves). Compare exported name/kind/signature inventory and explicit local bindings, inspect `git diff --numstat origin/dev...HEAD -- gui/src` against the 500 added+deleted source-line cap, and perform the changed-graph cycle check described above. The remote checkout SHA must equal this PR head; serialize the shared lidge checkout or arrange parent-owned isolation before running it. Do not accept a later remote GUI run on another layer's SHA. Require actual exit statuses and full-suite totals: the command deliberately avoids 002's unguarded `| tail -15`, which could hide failure. Record exact-head CI for the layer and do not merge. + +Docs-only verification for this author: inspect only these five requested output documents for nine exact ordered headings, complete declaration coverage, ≤400 projected leaf/residual budgets, correct branch/base/stack map, and whitespace with `git diff --no-index --check /dev/null `. No runtime, build, privacy or test-pass result is claimed here. + +## Accept criteria + +1. Every top-level declaration in the origin/dev inventory has one canonical owner; moved blocks match original behavior and no unlisted source file is changed. +2. Existing default/named/type exports and signatures remain importable from `gui/src/components/MemoryObservabilityCard.tsx`; all 2 existing importer files retain their paths. Re-exported symbols used locally have explicit imports. +3. Exactly 2 new leaves appear at the paths above, each ≤400 physical lines; residual ≤400 (budget 356); actual formatted counts and source diff size are recorded. Exceeding the 500-line source diff or residual budget escalates before publication. +4. State lifetime/ownership and side-effect timing match Module-level state and cycles; changed graph has no new value or type cycle and no upward leaf → original path. +5. Every listed behavioral/text oracle keeps its specified target/disposition; the named guard mutation produces the expected failure and restoration yields green focused tests. +6. Typecheck, focused checks, GUI build/lint, privacy scan, remote whole-suite and remote GUI PR-ready suite pass at the exact layer head, with exit codes and CI SHA evidence; no repository-wide local suite. +7. PR contains all repository template sections and the five-layer stack map; correct base/head, no merge, no release, no unrelated cleanup. If title/body says GUI, attach a real unchanged-UI screenshot as required by the repository gate; never fabricate an image link. + +## PR + +Title: `refactor(gui): separate memory metrics and stat views from restart polling (split S20 L2/5)` + +Head: `codex/split-components-MemoryObservabilityCard`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, Checklist; pure move only. Review only this layer's diff; publish later under parent authorization. Placeholder PR numbers below are intentional until PR creation, not fabricated existing PRs. + +| Layer | PR | Head branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S20-L1 | `codex/split-pages-ClaudeDesktop` | `dev` | isolate Claude Desktop profile data and lane views | +| 2 | #TBD-S20-L2 | `codex/split-components-MemoryObservabilityCard` | `dev` | separate memory metrics and stat views from restart polling ← this layer | +| 3 | #TBD-S20-L3 | `codex/split-components-provider-workspace-ProviderSettings` | `dev` | extract provider draft helpers and stateless settings fields | +| 4 | #TBD-S20-L4 | `codex/split-pages-dashboard-shared` | `dev` | isolate dashboard sidecar option contracts and selection | +| 5 | #TBD-S20-L5 | `codex/split-components-QuotaBars` | `dev` | extract quota reset date and locale formatting | + +DEV-STACK-03: each of the five layers carries its own gates and this complete map. S20 groups execution order and PR navigation only; all five layers are independent under STACK-INDEPENDENCE-01. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge remains forbidden here (DEV-STACK-04). diff --git a/devlog/_plan/260905_now_split_train/690_components_provider_workspace_ProviderSettings.md b/devlog/_plan/260905_now_split_train/690_components_provider_workspace_ProviderSettings.md new file mode 100644 index 0000000000..d05d9fb9f7 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/690_components_provider_workspace_ProviderSettings.md @@ -0,0 +1,181 @@ +# S20 L3/5 — ProviderSettings + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. C3 architecture, docs-only delegated preparation; parent owns all orchestration/loop/goal state. +- Goal: split `gui/src/components/provider-workspace/ProviderSettings.tsx` into 2 cohesive sibling leaves, each ≤400 lines, with a projected 392-line residual and every existing export still importable from the old path. +- Non-goals: no behavior, copy, CSS, locale, request payload, exported name/signature, effect lifetime, auth/consent, or dependency changes. No source edits, test runs, Git mutation, PR creation or orchestration in this planning task. Existing long functions are not silently rewritten to satisfy a second metric. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated in Verification below (the 000 reference to “003” is stale; 002 is authoritative). +- Stop: plan complete when inventory/partition/export/state/oracle/gate records are internally consistent. Implementation stops on any failed gate or non-pure-move delta; completion later requires exact-tip checks and exact-head green CI, never a cached green check. +- Escalation: Stop if 40-line replacement budget is exceeded enough to leave the original above 400, total added+deleted source lines exceed 500, or a field extraction would move auth confirmation/save validation. Parent must resolve a size exception or additional part rather than allowing opportunistic controller/auth changes. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. Read with `git show origin/dev:gui/src/components/provider-workspace/ProviderSettings.tsx`; the working-tree copy was byte-compared and identical. All source ranges below are inclusive at this origin/dev revision. Read 000_plan.md, 001_stale_check.md, S20 rows / Per-layer gate in 002_layer_map.md, and the matching section in `../260905_modular_debt_ledger/015_lane_gui.md`. + +Structural decision (ARCH-DECISION-01 / ARCH-MAP-01): Context: state/save logic and 132 lines of form sections share a 514-line file. Rejected moving the default 456-line component intact: relocates the violation. Rejected independent pacing controller: changes the single save transaction. Reuse base-url-choice.ts, ProviderRail.authModeLabel and provider-workspace/types.ts. Chosen move: pure draft helpers plus stateless field views with explicit typed values/events. ProviderDetails.tsx and four public-path tests remain the callers; blast radius is provider-workspace presentation, with auth behavior deliberately retained. + +## Symbol inventory + +Inventory uses installed ast-grep: `sg run --kind --json=compact gui/src/components/provider-workspace/ProviderSettings.tsx`, filtered to top-level declarations and checked against `git show origin/dev:gui/src/components/provider-workspace/ProviderSettings.tsx | nl -ba`. Imports are included for completeness but are not newly owned declarations. + +Consumer count = distinct external source/test files importing that binding from the original module, not identifier occurrences or documentation mentions. Command candidate set: `rg -l 'ProviderSettings' src gui/src scripts tests gui/tests`; inspect matched import clauses for each symbol and deduplicate files. Private declarations/import bindings have zero external consumers by definition; local uses are preserved through the explicit imports below. Module fan-in is **5 files** (including type/test imports); added leaf imports do not replace existing consumer imports. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `import { useEffect, useMemo, useRef, useState } from "react";` | import declaration | 10–10 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { baseUrlForChoice, matchChoiceId, resolvedBaseUrlForChoice } from "../../base-url-choice";` | import declaration | 11–11 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { readJsonIfOk } from "../../fetch-json";` | import declaration | 12–12 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { createBoundedFetch } from "../../bounded-fetch";` | import declaration | 13–13 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { startVisibilityPoll } from "../../visibility-poll";` | import declaration | 14–14 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { useT } from "../../i18n/shared";` | import declaration | 15–15 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { IconLock } from "../../icons";` | import declaration | 16–16 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { isCatalogProviderId } from "../../provider-icons";` | import declaration | 17–17 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { openAiAccountProviderState } from "../../provider-payload";` | import declaration | 18–18 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { providerSupportsLiveModelDiscovery } from "../../provider-workspace/catalog";` | import declaration | 19–19 | no | 0 external | allocation in Leaf partition / residual imports | +| `import type { CatalogPreset } from "../provider-catalog/provider-presets";` | import declaration | 20–20 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { authModeLabel } from "./ProviderRail";` | import declaration | 21–21 | no | 0 external | allocation in Leaf partition / residual imports | +| `import type { WorkspaceItem, ProviderUpdatePatch, ProviderUpdateResult } from "./types";` | import declaration | 22–22 | no | 0 external | allocation in Leaf partition / residual imports | +| `ADAPTERS` | lexical declaration | 24–24 | no | 0 | `gui/src/components/provider-workspace/ProviderSettings.tsx` | +| `EMPTY_MODELS` | lexical declaration | 25–25 | no | 0 | `gui/src/components/provider-workspace/ProviderSettings.tsx` | +| `ChoicesStatus` | type alias declaration | 27–27 | no | 0 | `gui/src/components/provider-workspace/ProviderSettings.tsx` | +| `PacingRule` | type alias declaration | 28–28 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `PacingStatus` | type alias declaration | 29–29 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `CursorHttpVersion` | type alias declaration | 30–30 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `effectiveCursorHttpVersion` | function declaration | 32–34 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `numberDraft` | function declaration | 36–36 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `positiveRpm` | function declaration | 37–41 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `positiveInteger` | function declaration | 42–46 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `pacingSignature` | function declaration | 47–57 | no | 0 | `gui/src/components/provider-workspace/provider-settings-draft.ts` | +| `ProviderSettings` | function declaration | 59–514 | default | 5 | `gui/src/components/provider-workspace/ProviderSettings.tsx` (residual; JSX ranges below move) | + +Current direct importer files (same import paths after the move): + +- `gui/tests/provider-settings-cursor-transport.test.tsx` +- `gui/tests/provider-settings-live-models-provenance.test.tsx` +- `gui/tests/provider-settings-request-pacing.test.tsx` +- `gui/tests/provider-settings-account-mode.test.tsx` +- `gui/src/components/provider-workspace/ProviderDetails.tsx` + +## Leaf partition + +Reuse decision: no parallel infrastructure, utility barrel, controller or cache is introduced. The source-owned definitions above move rather than being copied; existing helpers named in Loop spec remain canonical. Sibling convention is feature-qualified lowercase helper filenames and PascalCase component files (e.g. `gui/src/pages/claude-desktop-lane.ts`, `gui/src/pages/dashboard-core-poll.ts`, `gui/src/components/provider-workspace/ProviderRail.tsx`). No `index.ts`, `utils.ts` or `common.ts` is created. + +### gui/src/components/provider-workspace/provider-settings-draft.ts + +- Symbols: PacingRule PacingStatus CursorHttpVersion effectiveCursorHttpVersion numberDraft positiveRpm positiveInteger pacingSignature. +- Expected physical lines: 34 (including imports and new prop signatures; maximum 400). +- Move lines 28–57 (30 physical lines). Export the three types and five helpers unchanged. ChoicesStatus, ADAPTERS and EMPTY_MODELS stay with the stateful form. +- Own imports: + +```ts +import type { WorkspaceItem } from "./types"; +``` + +### gui/src/components/provider-workspace/ProviderSettingsFields.tsx + +- Symbols: ProviderConnectionFields (new extraction 329–407); ProviderAdvancedFields (new extraction 445–473); ProviderPacingFields (new extraction 474–497). +- Expected physical lines: 235 (including imports and new prop signatures; maximum 400). +- Move 79 + 29 + 24 = 132 original JSX lines into three named stateless components in one settings-presentation leaf. Use inline typed props, with fragments rather than extra DOM wrappers. Connection fields receive providerName, t, adapter/isPreset/adapterOptions, hasEndpointPicker/baseUrlChoices/endpointChoice/baseUrl/plainBaseUrlLocked, cursorHttpVersion, modelOptions/defaultModel, authMode/authModeDisplay, endpointLabel and the corresponding setter callbacks. Keep endpoint selection's setEndpointChoice then setBaseUrl(baseUrlForChoice(...)) order. Advanced fields receive t, supportsApiKeyTransport/apiKeyTransport, note, allowPrivateNetwork, liveModels/liveModelDiscoverySupported and setters. Pacing fields receive providerName, t, availableModels, pacingEnabled/pacingRpm/pacingDelay/pacingStatus, pacingModelId/pacingModelRpm/pacingModelDelay/pacingModels, corresponding setters and addPacingModel. Type setPacingModels as Dispatch>> to preserve its existing functional removal updater. Do not pass the whole WorkspaceItem or a controller bag. authModeDisplay is authModeLabel(item,t), computed in the residual. The save/discard bar (498–506), message (507–511), account-mode confirmation (408–444), every hook (71–180) and all mutations (225–307) stay in ProviderSettings. +- Own imports: + +```ts +import type { Dispatch, SetStateAction } from "react"; +import type { TFn } from "../../i18n/shared"; +import { IconLock } from "../../icons"; +import { baseUrlForChoice } from "../../base-url-choice"; +import type { CatalogPreset } from "../provider-catalog/provider-presets"; +import type { PacingRule, PacingStatus, CursorHttpVersion } from "./provider-settings-draft"; +``` + +Residual `gui/src/components/provider-workspace/ProviderSettings.tsx`: **392 expected lines**. 514 − 30 (draft types/helpers) − 132 (three JSX regions) + 40 (imports and replacement prop-call budget) = 392 residual lines. Leaves 34 + 235 = 269; aggregate 661 = 514 + 147 net extraction overhead. No #b allocated. The residual has only eight lines of budget headroom: count actual formatted output before declaring this split complete. These are explicit physical-line budgets, not measured implementation output: reject a formatted result above the budget/400 rather than minifying it. The exact moved source blocks are disjoint; every original declaration has exactly one target in the inventory. Preserve associated comments, including i18n/lint exceptions. + +## Re-export block + +The only public export is default ProviderSettings (59–514); keep it in the residual. Exact new re-export block: empty. All current private helpers stay private to this feature's direct-import leaves; no new original-path exports. + +Explicit local bindings needed in the residual (a re-export binds nothing): + +```ts +import { effectiveCursorHttpVersion, numberDraft, positiveRpm, positiveInteger, pacingSignature } from "./provider-settings-draft"; +import type { PacingRule, PacingStatus, CursorHttpVersion } from "./provider-settings-draft"; +import { ProviderConnectionFields, ProviderAdvancedFields, ProviderPacingFields } from "./ProviderSettingsFields"; +``` + +Retain original external imports still used by residual declarations; remove only moved-only bindings after reference checks. The listed leaf imports use verified existing modules or the exact new owners defined in this plan. Internal leaves import each other directly, never through the preserved original-path compatibility boundary. No wildcard re-export. + +## Module-level state and cycles + +No module-level mutable Map/Set/WeakMap, let, lock or timer exists. ADAPTERS at 24 remains one read-only array; EMPTY_MODELS at 25 remains one stable fallback array in ProviderSettings.tsx (never inline [] into default props). Pacing model copies at 96 and Set at 204 are component/useMemo-local. All draft setters, account-mode synchronization, saveRef (268) and pacing visibility polling remain in the residual. Fields → draft/types and UI primitives; residual → fields/draft; no leaf imports ProviderSettings. Pure functions and explicit field events are functional coupling, not shared mutable state. + +Cycle proof for the implementation gate: resolve static import/export edges, including type-only edges, from this original and its new leaves; fail if any leaf reaches the original (directly or transitively), or the changed induced graph has an SCC. Run the lane-015 read-only sg/import-resolution + Tarjan method; preserve the allow-edge and forbidden-back-edge evidence. No new graph tool/dependency installation is authorized. The plan records an acyclic intended edge map, not a claim that future source has been scanned. + +## Tests + +Direct importing tests — `rg -l` candidate list narrowed to actual imports of this module; **4 files**, all **unchanged**: + +- `gui/tests/provider-settings-cursor-transport.test.tsx` — unchanged original-path import. +- `gui/tests/provider-settings-live-models-provenance.test.tsx` — unchanged original-path import. +- `gui/tests/provider-settings-request-pacing.test.tsx` — unchanged original-path import. +- `gui/tests/provider-settings-account-mode.test.tsx` — unchanged original-path import. + +Text-oracle disposition: No literal or extensionless source reader targets ProviderSettings.tsx. All four direct test files import the component at line 5 and remain unchanged; no retarget-to-leaf/add-leaf-to-scan-list required. ProviderDetails source-reading tests inspect ProviderDetails, whose import and JSX remain unchanged. + +Guards to drive red once during implementation C verification: No retargeted text guard. Temporarily change positiveRpm to return parsed + 1 for valid inputs in the draft leaf; gui/tests/provider-settings-request-pacing.test.tsx:39 must fail its exact 38/10 RPM patch assertion (70–76), then restore. This existing test does not prove the 1/60 lower bound. Keep the liveModels provenance, transport dirty/save and confirm-gated account-mode cases unchanged; execute only mocked requests. Record the named failing assertion and restored green result; do not commit mutations. Do not weaken assertions, replace source guards with export-existence checks, or retarget behavioral tests away from the compatibility boundary. No guard has been executed during this documentation task. + +## Verification + +Future executor commands only — not run by this delegated author. In a dedicated layer worktree at its tip, instantiate 002 Per-layer gate: + +```sh +bun run typecheck +bun test gui/tests/provider-settings-request-pacing.test.tsx gui/tests/provider-settings-live-models-provenance.test.tsx gui/tests/provider-settings-cursor-transport.test.tsx gui/tests/provider-settings-account-mode.test.tsx +bun run privacy:scan +wc -l gui/src/components/provider-workspace/provider-settings-draft.ts gui/src/components/provider-workspace/ProviderSettingsFields.tsx gui/src/components/provider-workspace/ProviderSettings.tsx +rg -l 'from "[^"]*/ProviderSettings(\.tsx?)?"' src gui/src scripts tests gui/tests +# GUI TypeScript/bundler proof and scoped lint, required by gui/AGENTS.md: +(cd gui && bun run build && bun run lint) +# Whole repository suite only on the approved remote host: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-components-provider-workspace-ProviderSettings && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' +# Full GUI PR-ready suite also remote, never substitute it for the root suite: +ssh lidge 'cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile && bun test tests' +``` + +Focused domains: `gui/tests`; only the listed files run locally. The core-Lab boundary gate is N/A: no `src/server`, `src/router`, or `src/lib` source is touched; never edit its protected roots. Unchanged UI copy means no locale churn; if copy unexpectedly changes, stop the pure-move layer rather than manufacturing new translations. + +Compare the importer list with the 5-file baseline above (count files, not lines; compare existing callers, excluding newly added internal leaves). Compare exported name/kind/signature inventory and explicit local bindings, inspect `git diff --numstat origin/dev...HEAD -- gui/src` against the 500 added+deleted source-line cap, and perform the changed-graph cycle check described above. The remote checkout SHA must equal this PR head; serialize the shared lidge checkout or arrange parent-owned isolation before running it. Do not accept a later remote GUI run on another layer's SHA. Require actual exit statuses and full-suite totals: the command deliberately avoids 002's unguarded `| tail -15`, which could hide failure. Record exact-head CI for the layer and do not merge. + +Docs-only verification for this author: inspect only these five requested output documents for nine exact ordered headings, complete declaration coverage, ≤400 projected leaf/residual budgets, correct branch/base/stack map, and whitespace with `git diff --no-index --check /dev/null `. No runtime, build, privacy or test-pass result is claimed here. + +## Accept criteria + +1. Every top-level declaration in the origin/dev inventory has one canonical owner; moved blocks match original behavior and no unlisted source file is changed. +2. Existing default/named/type exports and signatures remain importable from `gui/src/components/provider-workspace/ProviderSettings.tsx`; all 5 existing importer files retain their paths. Re-exported symbols used locally have explicit imports. +3. Exactly 2 new leaves appear at the paths above, each ≤400 physical lines; residual ≤400 (budget 392); actual formatted counts and source diff size are recorded. Exceeding the 500-line source diff or residual budget escalates before publication. +4. State lifetime/ownership and side-effect timing match Module-level state and cycles; changed graph has no new value or type cycle and no upward leaf → original path. +5. Every listed behavioral/text oracle keeps its specified target/disposition; the named guard mutation produces the expected failure and restoration yields green focused tests. +6. Typecheck, focused checks, GUI build/lint, privacy scan, remote whole-suite and remote GUI PR-ready suite pass at the exact layer head, with exit codes and CI SHA evidence; no repository-wide local suite. +7. PR contains all repository template sections and the five-layer stack map; correct base/head, no merge, no release, no unrelated cleanup. If title/body says GUI, attach a real unchanged-UI screenshot as required by the repository gate; never fabricate an image link. + +## PR + +Title: `refactor(gui): extract provider draft helpers and stateless settings fields (split S20 L3/5)` + +Head: `codex/split-components-provider-workspace-ProviderSettings`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, Checklist; pure move only. Review only this layer's diff; publish later under parent authorization. Placeholder PR numbers below are intentional until PR creation, not fabricated existing PRs. + +| Layer | PR | Head branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S20-L1 | `codex/split-pages-ClaudeDesktop` | `dev` | isolate Claude Desktop profile data and lane views | +| 2 | #TBD-S20-L2 | `codex/split-components-MemoryObservabilityCard` | `dev` | separate memory metrics and stat views from restart polling | +| 3 | #TBD-S20-L3 | `codex/split-components-provider-workspace-ProviderSettings` | `dev` | extract provider draft helpers and stateless settings fields ← this layer | +| 4 | #TBD-S20-L4 | `codex/split-pages-dashboard-shared` | `dev` | isolate dashboard sidecar option contracts and selection | +| 5 | #TBD-S20-L5 | `codex/split-components-QuotaBars` | `dev` | extract quota reset date and locale formatting | + +DEV-STACK-03: each of the five layers carries its own gates and this complete map. S20 groups execution order and PR navigation only; all five layers are independent under STACK-INDEPENDENCE-01. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge remains forbidden here (DEV-STACK-04). diff --git a/devlog/_plan/260905_now_split_train/700_pages_dashboard_shared.md b/devlog/_plan/260905_now_split_train/700_pages_dashboard_shared.md new file mode 100644 index 0000000000..3495a6496d --- /dev/null +++ b/devlog/_plan/260905_now_split_train/700_pages_dashboard_shared.md @@ -0,0 +1,224 @@ +# S20 L4/5 — dashboard-shared + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. C3 architecture, docs-only delegated preparation; parent owns all orchestration/loop/goal state. +- Goal: split `gui/src/pages/dashboard-shared.ts` into 1 cohesive sibling leaves, each ≤400 lines, with a projected 332-line residual and every existing export still importable from the old path. +- Non-goals: no behavior, copy, CSS, locale, request payload, exported name/signature, effect lifetime, auth/consent, or dependency changes. No source edits, test runs, Git mutation, PR creation or orchestration in this planning task. Existing long functions are not silently rewritten to satisfy a second metric. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated in Verification below (the 000 reference to “003” is stale; 002 is authoritative). +- Stop: plan complete when inventory/partition/export/state/oracle/gate records are internally consistent. Implementation stops on any failed gate or non-pure-move delta; completion later requires exact-tip checks and exact-head green CI, never a cached green check. +- Escalation: Stop if a leaf imports dashboard-shared even type-only, any public type/function is dropped or renamed, focus listeners change evaluation timing, or actual source diff exceeds 500. Changes to server vision contracts are outside this layer. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. Read with `git show origin/dev:gui/src/pages/dashboard-shared.ts`; the working-tree copy was byte-compared and identical. All source ranges below are inclusive at this origin/dev revision. Read 000_plan.md, 001_stale_check.md, S20 rows / Per-layer gate in 002_layer_map.md, and the matching section in `../260905_modular_debt_ledger/015_lane_gui.md`. + +Structural decision (ARCH-DECISION-01 / ARCH-MAP-01): Context: option construction contributes 141 lines to a 488-line shared dashboard module. Rejected moving only update-label helpers: insufficient size reduction. Rejected moving focus hooks: unnecessary side-effect timing risk. Reuse shadow-call-source.ts unchanged. Chosen move: colocate selection functions and their eight DTO types in a sibling named dashboard-sidecar-options.ts, preserving every original export through explicit named re-exports. Blast radius is the dashboard helper boundary, with 15 current importer files remaining on the original path. Existing dashboard-core-poll.ts/dashboard-dialogs.tsx show the sibling domain naming convention. + +## Symbol inventory + +Inventory uses installed ast-grep: `sg run --kind --json=compact gui/src/pages/dashboard-shared.ts`, filtered to top-level declarations and checked against `git show origin/dev:gui/src/pages/dashboard-shared.ts | nl -ba`. Imports are included for completeness but are not newly owned declarations. + +Consumer count = distinct external source/test files importing that binding from the original module, not identifier occurrences or documentation mentions. Command candidate set: `rg -l 'dashboard-shared' src gui/src scripts tests gui/tests`; inspect matched import clauses for each symbol and deduplicate files. Private declarations/import bindings have zero external consumers by definition; local uses are preserved through the explicit imports below. Module fan-in is **15 files** (including type/test imports); added leaf imports do not replace existing consumer imports. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `import type { RefObject } from "react";` | import declaration | 1–1 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { useEffect, useRef } from "react";` | import declaration | 2–2 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS, } from "../../../src/vision/timeout-bounds";` | import declaration | 3–7 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { readJsonOrThrow } from "../fetch-json";` | import declaration | 8–8 | no | 0 external | allocation in Leaf partition / residual imports | +| `import type { TKey } from "../i18n/shared";` | import declaration | 9–9 | no | 0 external | allocation in Leaf partition / residual imports | +| `import type { StartupHealthStatus } from "../startup-health-ui";` | import declaration | 10–10 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { shadowSourceModelList } from "./shadow-call-source";` | import declaration | 11–11 | no | 0 external | allocation in Leaf partition / residual imports | +| `DashboardSection` | type alias declaration | 13–13 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `DASHBOARD_UPDATE_HASH` | lexical declaration | 20–20 | yes | 0 | `gui/src/pages/dashboard-shared.ts` | +| `readDashboardSectionFromHash` | function declaration | 22–27 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `hashRequestsUpdateDialog` | function declaration | 30–32 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `dashboardHashForSection` | function declaration | 35–37 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `requireJson` | function declaration | 40–44 | yes | 4 | `gui/src/pages/dashboard-shared.ts` | +| `HealthData` | interface declaration | 46–46 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `ProviderInfo` | interface declaration | 47–47 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `ModelInfo` | interface declaration | 48–48 | yes | 5 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `SettingsData` | interface declaration | 49–64 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `SidecarBackend` | type alias declaration | 65–65 | yes | 0 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `VisionBackend` | type alias declaration | 73–73 | yes | 0 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `VisionReasoning` | type alias declaration | 74–74 | yes | 0 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `SidecarSetting` | interface declaration | 75–84 | yes | 1 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `VisionModelOption` | interface declaration | 85–85 | yes | 1 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `WebSearchModelOption` | interface declaration | 86–92 | yes | 0 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `WebSearchPickerOption` | interface declaration | 93–98 | yes | 0 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `SidecarData` | interface declaration | 99–110 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `SidecarPatch` | interface declaration | 111–121 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `ShadowCallData` | interface declaration | 122–122 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `UsageSummary30d` | interface declaration | 123–123 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `UpdateChannel` | type alias declaration | 124–124 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `Installer` | type alias declaration | 125–125 | yes | 0 | `gui/src/pages/dashboard-shared.ts` | +| `UpdateJobStatus` | type alias declaration | 126–126 | yes | 0 | `gui/src/pages/dashboard-shared.ts` | +| `SyncResult` | interface declaration | 127–138 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `ProjectCodexConfigWarning` | interface declaration | 139–144 | yes | 0 | `gui/src/pages/dashboard-shared.ts` | +| `ProjectCodexConfigGroup` | interface declaration | 145–149 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `UpdateCheckData` | interface declaration | 150–160 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `UpdateJob` | interface declaration | 161–173 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `EFFORT_CAP_LEVELS` | lexical declaration | 175–175 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `UPDATE_CHECK_MAX_AUTO_RETRIES` | lexical declaration | 176–176 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `UPDATE_CHECK_RETRY_BASE_MS` | lexical declaration | 177–177 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `defaultUpdateChannel` | function declaration | 179–181 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `updateReasonLabel` | function declaration | 183–190 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `updateJobLabel` | function declaration | 192–199 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `mergeSidecarSetting` | function declaration | 201–223 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `visionReasoningPatch` | function declaration | 226–228 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `visionEnabledPatch` | function declaration | 230–232 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `visionMaxDescriptionsPatch` | function declaration | 234–236 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `visionTimeoutPatch` | function declaration | 238–240 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `VISION_TIMEOUT_MS_DEFAULT` | lexical declaration | 246–246 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `VISION_TIMEOUT_MS_MAX` | lexical declaration | 247–247 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `VISION_TIMEOUT_MS_MIN` | lexical declaration | 248–248 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `VISION_MAX_DESCRIPTIONS_DEFAULT` | lexical declaration | 250–250 | yes | 3 | `gui/src/pages/dashboard-shared.ts` | +| `parsePositiveInteger` | function declaration | 252–258 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `parseVisionTimeoutMs` | function declaration | 260–264 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `VISION_REASONING_LEVELS` | lexical declaration | 266–266 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `visionReasoningLadder` | function declaration | 268–274 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `visionReasoningOptionsFor` | function declaration | 277–280 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `clampVisionReasoningToLadder` | function declaration | 283–300 | yes | 2 | `gui/src/pages/dashboard-shared.ts` | +| `sidecarModelOptions` | function declaration | 302–310 | yes | 0 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `webSearchModelOptionsForPicker` | function declaration | 318–351 | yes | 2 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `visionModelOptions` | function declaration | 368–381 | yes | 2 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `shadowCallModelOptions` | function declaration | 384–407 | yes | 3 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `sidecarBackendForModel` | function declaration | 409–411 | yes | 1 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `webSearchSidecarSelectionForModel` | function declaration | 414–424 | yes | 2 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `visionSidecarBackendForModel` | function declaration | 433–442 | yes | 2 | `gui/src/pages/dashboard-sidecar-options.ts` | +| `lastInputWasKeyboard` | lexical declaration | 444–444 | no | 0 | `gui/src/pages/dashboard-shared.ts` | +| `focusTriggerQuietly` | function declaration | 450–461 | no | 0 | `gui/src/pages/dashboard-shared.ts` | +| `useModalDialog` | function declaration | 463–486 | yes | 1 | `gui/src/pages/dashboard-shared.ts` | +| `StartupHealthStatus` | existing type re-export | 488–488 | yes | 0 | `dashboard-shared.ts` → `../startup-health-ui` (unchanged) | +| guarded keydown/pointerdown listener registration | top-level side-effect statement | 445–448 | no | 0 external | `gui/src/pages/dashboard-shared.ts` (unchanged) | + +Current direct importer files (same import paths after the move): + +- `gui/src/pages/dashboard-core-poll.ts` +- `gui/src/pages/use-subagent-delegation.ts` +- `gui/src/pages/dashboard-providers-section.tsx` +- `gui/src/pages/dashboard-models-section.tsx` +- `gui/tests/vision-sidecar-controls.test.ts` +- `gui/tests/vision-reasoning-contract.test.ts` +- `gui/tests/vision-sidecar-dashboard.test.tsx` +- `gui/tests/vision-model-options.test.ts` +- `gui/tests/shadow-call-model-options.test.ts` +- `gui/src/pages/dashboard-overview-sections.tsx` +- `gui/src/pages/dashboard-dialogs.tsx` +- `gui/src/pages/Dashboard.tsx` +- `gui/src/pages/Models.tsx` +- `gui/src/pages/use-dashboard-data.ts` +- `tests/gui/vision-sidecar-timeout-bounds.test.ts` + +## Leaf partition + +Reuse decision: no parallel infrastructure, utility barrel, controller or cache is introduced. The source-owned definitions above move rather than being copied; existing helpers named in Loop spec remain canonical. Sibling convention is feature-qualified lowercase helper filenames and PascalCase component files (e.g. `gui/src/pages/claude-desktop-lane.ts`, `gui/src/pages/dashboard-core-poll.ts`, `gui/src/components/provider-workspace/ProviderRail.tsx`). No `index.ts`, `utils.ts` or `common.ts` is created. + +### gui/src/pages/dashboard-sidecar-options.ts + +- Symbols: ModelInfo SidecarBackend VisionBackend VisionReasoning SidecarSetting VisionModelOption WebSearchModelOption WebSearchPickerOption sidecarModelOptions webSearchModelOptionsForPicker visionModelOptions shadowCallModelOptions sidecarBackendForModel webSearchSidecarSelectionForModel visionSidecarBackendForModel. +- Expected physical lines: 184 (including imports and new prop signatures; maximum 400). +- Move ModelInfo at 48, sidecar types 65–98 and the complete option/selection block 302–442: 1 + 34 + 141 = 176 physical lines. Export all existing exported identifiers identically. Reasoning/timeout patches, merged settings, update presentation and modal focus ownership remain in dashboard-shared. Types travel with option selection to avoid a leaf → original type cycle; the original imports the moved types for SidecarData/SidecarPatch/mergeSidecarSetting and reasoning helpers. +- Own imports: + +```ts +import { shadowSourceModelList } from "./shadow-call-source"; +``` + +Residual `gui/src/pages/dashboard-shared.ts`: **332 expected lines**. 488 − 176 moved source lines + 20 (type imports/re-export/spacing budget) = 332 residual lines. Leaf 184; aggregate 516 = 488 + 28 net overhead. No #b required. These are explicit physical-line budgets, not measured implementation output: reject a formatted result above the budget/400 rather than minifying it. The exact moved source blocks are disjoint; every original declaration has exactly one target in the inventory. Preserve associated comments, including i18n/lint exceptions. + +## Re-export block + +```ts +export type { ModelInfo, SidecarBackend, VisionBackend, VisionReasoning, SidecarSetting, VisionModelOption, WebSearchModelOption, WebSearchPickerOption } from "./dashboard-sidecar-options"; +export { sidecarModelOptions, webSearchModelOptionsForPicker, visionModelOptions, shadowCallModelOptions, sidecarBackendForModel, webSearchSidecarSelectionForModel, visionSidecarBackendForModel } from "./dashboard-sidecar-options"; +``` + +All other current exported declarations remain in the residual unchanged. In particular keep `export type { StartupHealthStatus };` at the original boundary with its existing type import. + +Explicit local bindings needed in the residual (a re-export binds nothing): + +```ts +import type { ModelInfo, SidecarBackend, VisionBackend, VisionReasoning, SidecarSetting, VisionModelOption, WebSearchModelOption } from "./dashboard-sidecar-options"; +``` + +Retain original external imports still used by residual declarations; remove only moved-only bindings after reference checks. The listed leaf imports use verified existing modules or the exact new owners defined in this plan. Internal leaves import each other directly, never through the preserved original-path compatibility boundary. No wildcard re-export. + +## Module-level state and cycles + +lastInputWasKeyboard (444) stays solely in dashboard-shared.ts together with the guarded top-level listener-registration statement (445–448), focusTriggerQuietly (450–461) and useModalDialog (463–486). Do not move, duplicate or defer the keydown/pointerdown listeners. EFFORT_CAP_LEVELS (175), retry constants (176–177), VISION_TIMEOUT aliases/default (246–250), VISION_REASONING_LEVELS (266), and DASHBOARD_UPDATE_HASH (20) remain single original-path owners. invalidSelectors Set at 393 is function-local in shadowCallModelOptions and moves inside that function, not to module scope. Residual → option leaf → shadow-call-source.ts (which has no imports). Neither value nor type edges return to the original. Existing eager browser side effects remain attached to original-module evaluation. + +Cycle proof for the implementation gate: resolve static import/export edges, including type-only edges, from this original and its new leaves; fail if any leaf reaches the original (directly or transitively), or the changed induced graph has an SCC. Run the lane-015 read-only sg/import-resolution + Tarjan method; preserve the allow-edge and forbidden-back-edge evidence. No new graph tool/dependency installation is authorized. The plan records an acyclic intended edge map, not a claim that future source has been scanned. + +## Tests + +Direct importing tests — `rg -l` candidate list narrowed to actual imports of this module; **6 files**, all **unchanged**: + +- `gui/tests/vision-sidecar-controls.test.ts` — unchanged original-path import. +- `gui/tests/vision-reasoning-contract.test.ts` — unchanged original-path import. +- `gui/tests/vision-sidecar-dashboard.test.tsx` — unchanged original-path import. +- `gui/tests/vision-model-options.test.ts` — unchanged original-path import. +- `gui/tests/shadow-call-model-options.test.ts` — unchanged original-path import. +- `tests/gui/vision-sidecar-timeout-bounds.test.ts` — unchanged original-path import. + +Text-oracle disposition: No literal or extensionless source-text reader of dashboard-shared.ts was found. Six direct importing test files listed below remain unchanged. `gui/tests/dashboard-contracts.test.ts` reads dashboard-core-poll.ts/use-dashboard-data.ts (24–25 and subsequent calls), not this file; unchanged adjacent guard. Source tests of Dashboard.tsx/Models.tsx keep their original imports and need no retarget or scan-list change. + +Guards to drive red once during implementation C verification: No text guard is retargeted. Temporarily collapse an empty server option array to the legacy fallback in dashboard-sidecar-options.ts; vision-model-options.test.ts must fail, then restore. Also prove original-path export preservation by temporarily removing one moved re-export and observing its focused import fail, then restore. Never run a full suite locally for these checks. Record the named failing assertion and restored green result; do not commit mutations. Do not weaken assertions, replace source guards with export-existence checks, or retarget behavioral tests away from the compatibility boundary. No guard has been executed during this documentation task. + +## Verification + +Future executor commands only — not run by this delegated author. In a dedicated layer worktree at its tip, instantiate 002 Per-layer gate: + +```sh +bun run typecheck +bun test gui/tests/vision-sidecar-controls.test.ts gui/tests/vision-reasoning-contract.test.ts gui/tests/vision-sidecar-dashboard.test.tsx gui/tests/vision-model-options.test.ts gui/tests/shadow-call-model-options.test.ts tests/gui/vision-sidecar-timeout-bounds.test.ts +bun run privacy:scan +wc -l gui/src/pages/dashboard-sidecar-options.ts gui/src/pages/dashboard-shared.ts +rg -l 'from "[^"]*/dashboard-shared(\.tsx?)?"' src gui/src scripts tests gui/tests +# GUI TypeScript/bundler proof and scoped lint, required by gui/AGENTS.md: +(cd gui && bun run build && bun run lint) +# Whole repository suite only on the approved remote host: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-pages-dashboard-shared && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' +# Full GUI PR-ready suite also remote, never substitute it for the root suite: +ssh lidge 'cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile && bun test tests' +``` + +Focused domains: `tests/gui` and `gui/tests`; only the listed files run locally. The core-Lab boundary gate is N/A: no `src/server`, `src/router`, or `src/lib` source is touched; never edit its protected roots. Unchanged UI copy means no locale churn; if copy unexpectedly changes, stop the pure-move layer rather than manufacturing new translations. + +Compare the importer list with the 15-file baseline above (count files, not lines; compare existing callers, excluding newly added internal leaves). Compare exported name/kind/signature inventory and explicit local bindings, inspect `git diff --numstat origin/dev...HEAD -- gui/src` against the 500 added+deleted source-line cap, and perform the changed-graph cycle check described above. The remote checkout SHA must equal this PR head; serialize the shared lidge checkout or arrange parent-owned isolation before running it. Do not accept a later remote GUI run on another layer's SHA. Require actual exit statuses and full-suite totals: the command deliberately avoids 002's unguarded `| tail -15`, which could hide failure. Record exact-head CI for the layer and do not merge. + +Docs-only verification for this author: inspect only these five requested output documents for nine exact ordered headings, complete declaration coverage, ≤400 projected leaf/residual budgets, correct branch/base/stack map, and whitespace with `git diff --no-index --check /dev/null `. No runtime, build, privacy or test-pass result is claimed here. + +## Accept criteria + +1. Every top-level declaration in the origin/dev inventory has one canonical owner; moved blocks match original behavior and no unlisted source file is changed. +2. Existing default/named/type exports and signatures remain importable from `gui/src/pages/dashboard-shared.ts`; all 15 existing importer files retain their paths. Re-exported symbols used locally have explicit imports. +3. Exactly 1 new leaves appear at the paths above, each ≤400 physical lines; residual ≤400 (budget 332); actual formatted counts and source diff size are recorded. Exceeding the 500-line source diff or residual budget escalates before publication. +4. State lifetime/ownership and side-effect timing match Module-level state and cycles; changed graph has no new value or type cycle and no upward leaf → original path. +5. Every listed behavioral/text oracle keeps its specified target/disposition; the named guard mutation produces the expected failure and restoration yields green focused tests. +6. Typecheck, focused checks, GUI build/lint, privacy scan, remote whole-suite and remote GUI PR-ready suite pass at the exact layer head, with exit codes and CI SHA evidence; no repository-wide local suite. +7. PR contains all repository template sections and the five-layer stack map; correct base/head, no merge, no release, no unrelated cleanup. If title/body says GUI, attach a real unchanged-UI screenshot as required by the repository gate; never fabricate an image link. + +## PR + +Title: `refactor(gui): isolate dashboard sidecar option contracts and selection (split S20 L4/5)` + +Head: `codex/split-pages-dashboard-shared`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, Checklist; pure move only. Review only this layer's diff; publish later under parent authorization. Placeholder PR numbers below are intentional until PR creation, not fabricated existing PRs. + +| Layer | PR | Head branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S20-L1 | `codex/split-pages-ClaudeDesktop` | `dev` | isolate Claude Desktop profile data and lane views | +| 2 | #TBD-S20-L2 | `codex/split-components-MemoryObservabilityCard` | `dev` | separate memory metrics and stat views from restart polling | +| 3 | #TBD-S20-L3 | `codex/split-components-provider-workspace-ProviderSettings` | `dev` | extract provider draft helpers and stateless settings fields | +| 4 | #TBD-S20-L4 | `codex/split-pages-dashboard-shared` | `dev` | isolate dashboard sidecar option contracts and selection ← this layer | +| 5 | #TBD-S20-L5 | `codex/split-components-QuotaBars` | `dev` | extract quota reset date and locale formatting | + +DEV-STACK-03: each of the five layers carries its own gates and this complete map. S20 groups execution order and PR navigation only; all five layers are independent under STACK-INDEPENDENCE-01. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge remains forbidden here (DEV-STACK-04). diff --git a/devlog/_plan/260905_now_split_train/710_components_QuotaBars.md b/devlog/_plan/260905_now_split_train/710_components_QuotaBars.md new file mode 100644 index 0000000000..6ce54b83fb --- /dev/null +++ b/devlog/_plan/260905_now_split_train/710_components_QuotaBars.md @@ -0,0 +1,169 @@ +# S20 L5/5 — QuotaBars + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +- Archetype: pure-move. C3 architecture, docs-only delegated preparation; parent owns all orchestration/loop/goal state. +- Goal: split `gui/src/components/QuotaBars.tsx` into 1 cohesive sibling leaves, each ≤400 lines, with a projected 366-line residual and every existing export still importable from the old path. +- Non-goals: no behavior, copy, CSS, locale, request payload, exported name/signature, effect lifetime, auth/consent, or dependency changes. No source edits, test runs, Git mutation, PR creation or orchestration in this planning task. Existing long functions are not silently rewritten to satisfy a second metric. +- Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated in Verification below (the 000 reference to “003” is stale; 002 is authoritative). +- Stop: plan complete when inventory/partition/export/state/oracle/gate records are internally consistent. Implementation stops on any failed gate or non-pure-move delta; completion later requires exact-tip checks and exact-head green CI, never a cached green check. +- Escalation: Stop if reset formatting behavior changes, the leaf imports the original, existing public row/tone/age exports move or disappear unintentionally, or actual source diff exceeds 500. No quota polling/provider-probe/backend changes. + +Source basis: `origin/dev = 1362b1a3841b4de20177e5d65865a513dd7936c4`; docs HEAD `4cc219549eafbf9cd2efd651482fbfefd88944d5`. Read with `git show origin/dev:gui/src/components/QuotaBars.tsx`; the working-tree copy was byte-compared and identical. All source ranges below are inclusive at this origin/dev revision. Read 000_plan.md, 001_stale_check.md, S20 rows / Per-layer gate in 002_layer_map.md, and the matching section in `../260905_modular_debt_ledger/015_lane_gui.md`. + +Structural decision (ARCH-DECISION-01 / ARCH-MAP-01): Context: the 452-line quota component also owns a cohesive 90-line reset-date subsystem. Rejected extracting all row construction/rendering: more churn than needed for the 400-line target. Rejected reusing generic uptime formatting: different date and locale semantics. Keep normalizeQuotaForPlan in its existing codex-quota-utils owner. Chosen move: one quota-reset.ts sibling, consistent with existing codex-account-pool-* helper naming; preserve the component and all other helper declarations. Six production component callers plus three test importer files remain on the original boundary. + +## Symbol inventory + +Inventory uses installed ast-grep: `sg run --kind --json=compact gui/src/components/QuotaBars.tsx`, filtered to top-level declarations and checked against `git show origin/dev:gui/src/components/QuotaBars.tsx | nl -ba`. Imports are included for completeness but are not newly owned declarations. + +Consumer count = distinct external source/test files importing that binding from the original module, not identifier occurrences or documentation mentions. Command candidate set: `rg -l 'QuotaBars' src gui/src scripts tests gui/tests`; inspect matched import clauses for each symbol and deduplicate files. Private declarations/import bindings have zero external consumers by definition; local uses are preserved through the explicit imports below. Module fan-in is **9 files** (including type/test imports); added leaf imports do not replace existing consumer imports. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `import type { CSSProperties } from "react";` | import declaration | 1–1 | no | 0 external | allocation in Leaf partition / residual imports | +| `import type { Locale, TFn } from "../i18n/shared";` | import declaration | 2–2 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { useI18n } from "../i18n/shared";` | import declaration | 3–3 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { IconAlert } from "../icons";` | import declaration | 4–4 | no | 0 external | allocation in Leaf partition / residual imports | +| `import { type AccountQuota, normalizeQuotaForPlan } from "../codex-quota-utils";` | import declaration | 5–5 | no | 0 external | allocation in Leaf partition / residual imports | +| `QuotaWindowKey` | type alias declaration | 10–10 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `QuotaBarRow` | type alias declaration | 11–18 | yes | 0 | `gui/src/components/QuotaBars.tsx` | +| `rawCustomWindowRank` | function declaration | 25–30 | no | 0 | `gui/src/components/QuotaBars.tsx` | +| `localizeCustomQuotaLabel` | function declaration | 32–43 | no | 0 | `gui/src/components/QuotaBars.tsx` | +| `buildQuotaRows` | function declaration | 45–100 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `maxQuotaUtilisation` | function declaration | 103–111 | yes | 2 | `gui/src/components/QuotaBars.tsx` | +| `bcp47` | function declaration | 113–138 | no | 0 | `gui/src/components/quota-reset.ts` | +| `isQuotaExhausted` | function declaration | 141–143 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `isQuotaWarn` | function declaration | 145–147 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `quotaBarTone` | function declaration | 149–151 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `barWidth` | function declaration | 154–158 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `barFillStyle` | function declaration | 160–162 | no | 0 | `gui/src/components/QuotaBars.tsx` | +| `formatObservedAge` | function declaration | 172–179 | yes | 1 | `gui/src/components/QuotaBars.tsx` | +| `QuotaBars` | function declaration | 181–304 | default | 7 | `gui/src/components/QuotaBars.tsx` | +| `QuotaRow` | function declaration | 306–340 | no | 0 | `gui/src/components/QuotaBars.tsx` | +| `StackedQuotaRow` | function declaration | 342–387 | no | 0 | `gui/src/components/QuotaBars.tsx` | +| `resetDate` | function declaration | 390–396 | no | 0 | `gui/src/components/quota-reset.ts` | +| `formatResetAt` | function declaration | 398–411 | no | 0 | `gui/src/components/quota-reset.ts` | +| `formatResetFuture` | function declaration | 414–452 | yes | 2 | `gui/src/components/quota-reset.ts` | + +Current direct importer files (same import paths after the move): + +- `tests/gui/quota-bars-rows.test.ts` +- `gui/tests/fr-localization.test.ts` +- `gui/tests/quota-observed-age.test.tsx` +- `gui/src/components/codex-account-pool-cards.tsx` +- `gui/src/components/provider-workspace/ProviderAuthPanel.tsx` +- `gui/src/components/provider-workspace/ProviderUsage.tsx` +- `gui/src/components/provider-workspace/ProviderCapacityQuota.tsx` +- `gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx` +- `gui/src/components/codex-account-pool-main-card.tsx` + +## Leaf partition + +Reuse decision: no parallel infrastructure, utility barrel, controller or cache is introduced. The source-owned definitions above move rather than being copied; existing helpers named in Loop spec remain canonical. Sibling convention is feature-qualified lowercase helper filenames and PascalCase component files (e.g. `gui/src/pages/claude-desktop-lane.ts`, `gui/src/pages/dashboard-core-poll.ts`, `gui/src/components/provider-workspace/ProviderRail.tsx`). No `index.ts`, `utils.ts` or `common.ts` is created. + +### gui/src/components/quota-reset.ts + +- Symbols: bcp47 resetDate formatResetAt formatResetFuture. +- Expected physical lines: 96 (including imports and new prop signatures; maximum 400). +- Move lines 113–138 and 389–452: 26 + 64 = 90 physical lines. bcp47 and resetDate remain private. Export formatResetAt for the residual row and retain formatResetFuture's existing signature/defaults as an explicit re-export. Keep Date creation inside calls and preserve second/millisecond normalization, locale mapping, DST/calendar-day logic and relative/future formatting. +- Own imports: + +```ts +import type { Locale, TFn } from "../i18n/shared"; +``` + +Residual `gui/src/components/QuotaBars.tsx`: **366 expected lines**. 452 − 90 (locale/reset blocks) + 4 (import/re-export/spacing budget) = 366 residual lines. Leaf 96; aggregate 462 = 452 + 10 net overhead. No #b required. These are explicit physical-line budgets, not measured implementation output: reject a formatted result above the budget/400 rather than minifying it. The exact moved source blocks are disjoint; every original declaration has exactly one target in the inventory. Preserve associated comments, including i18n/lint exceptions. + +## Re-export block + +```ts +export { formatResetFuture } from "./quota-reset"; +``` + +All other current exported declarations remain in the residual unchanged. + +Explicit local bindings needed in the residual (a re-export binds nothing): + +```ts +import { formatResetAt, formatResetFuture } from "./quota-reset"; +``` + +Retain original external imports still used by residual declarations; remove only moved-only bindings after reference checks. The listed leaf imports use verified existing modules or the exact new owners defined in this plan. Internal leaves import each other directly, never through the preserved original-path compatibility boundary. No wildcard re-export. + +## Module-level state and cycles + +No top-level mutable Map/Set/WeakMap/let/lock/timer. bcp47 is a pure mapping; Date/Intl.DateTimeFormat instances are call-local at original 393/402/404/422–439 and remain call-local. Residual → quota-reset → i18n types; never import QuotaBars (including its QuotaBarRow type) from quota-reset. The reset leaf does not need row types. Existing normalizeQuotaForPlan stays in the residual. Functional coupling only, no new side effects. + +Cycle proof for the implementation gate: resolve static import/export edges, including type-only edges, from this original and its new leaves; fail if any leaf reaches the original (directly or transitively), or the changed induced graph has an SCC. Run the lane-015 read-only sg/import-resolution + Tarjan method; preserve the allow-edge and forbidden-back-edge evidence. No new graph tool/dependency installation is authorized. The plan records an acyclic intended edge map, not a claim that future source has been scanned. + +## Tests + +Direct importing tests — `rg -l` candidate list narrowed to actual imports of this module; **3 files**, all **unchanged**: + +- `tests/gui/quota-bars-rows.test.ts` — unchanged original-path import. +- `gui/tests/fr-localization.test.ts` — unchanged original-path import. +- `gui/tests/quota-observed-age.test.tsx` — unchanged original-path import. + +Text-oracle disposition: No source-text reader targets QuotaBars.tsx. The three direct importing tests remain unchanged; fr-localization.test.ts reads locale catalogs, not the component. No retarget-to-leaf or add-leaf-to-scan-list action is needed. + +Guards to drive red once during implementation C verification: No retargeted text guard. Temporarily alter resetDate's seconds-to-milliseconds multiplier in quota-reset.ts to drive the reset-format assertions in quota-bars-rows.test.ts red; restore. The observed-age and rendering tests must continue to import QuotaBars from its old path. Record the named failing assertion and restored green result; do not commit mutations. Do not weaken assertions, replace source guards with export-existence checks, or retarget behavioral tests away from the compatibility boundary. No guard has been executed during this documentation task. + +## Verification + +Future executor commands only — not run by this delegated author. In a dedicated layer worktree at its tip, instantiate 002 Per-layer gate: + +```sh +bun run typecheck +bun test tests/gui/quota-bars-rows.test.ts gui/tests/quota-observed-age.test.tsx gui/tests/fr-localization.test.ts +bun run privacy:scan +wc -l gui/src/components/quota-reset.ts gui/src/components/QuotaBars.tsx +rg -l 'from "[^"]*/QuotaBars(\.tsx?)?"' src gui/src scripts tests gui/tests +# GUI TypeScript/bundler proof and scoped lint, required by gui/AGENTS.md: +(cd gui && bun run build && bun run lint) +# Whole repository suite only on the approved remote host: +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-components-QuotaBars && git checkout -q FETCH_HEAD && git rev-parse HEAD && bun install --frozen-lockfile && bun run test' +# Full GUI PR-ready suite also remote, never substitute it for the root suite: +ssh lidge 'cd ~/ocx-ci/opencodex/gui && bun install --frozen-lockfile && bun test tests' +``` + +Focused domains: `tests/gui` and `gui/tests`; only the listed files run locally. The core-Lab boundary gate is N/A: no `src/server`, `src/router`, or `src/lib` source is touched; never edit its protected roots. Unchanged UI copy means no locale churn; if copy unexpectedly changes, stop the pure-move layer rather than manufacturing new translations. + +Compare the importer list with the 9-file baseline above (count files, not lines; compare existing callers, excluding newly added internal leaves). Compare exported name/kind/signature inventory and explicit local bindings, inspect `git diff --numstat origin/dev...HEAD -- gui/src` against the 500 added+deleted source-line cap, and perform the changed-graph cycle check described above. The remote checkout SHA must equal this PR head; serialize the shared lidge checkout or arrange parent-owned isolation before running it. Do not accept a later remote GUI run on another layer's SHA. Require actual exit statuses and full-suite totals: the command deliberately avoids 002's unguarded `| tail -15`, which could hide failure. Record exact-head CI for the layer and do not merge. + +Docs-only verification for this author: inspect only these five requested output documents for nine exact ordered headings, complete declaration coverage, ≤400 projected leaf/residual budgets, correct branch/base/stack map, and whitespace with `git diff --no-index --check /dev/null `. No runtime, build, privacy or test-pass result is claimed here. + +## Accept criteria + +1. Every top-level declaration in the origin/dev inventory has one canonical owner; moved blocks match original behavior and no unlisted source file is changed. +2. Existing default/named/type exports and signatures remain importable from `gui/src/components/QuotaBars.tsx`; all 9 existing importer files retain their paths. Re-exported symbols used locally have explicit imports. +3. Exactly 1 new leaves appear at the paths above, each ≤400 physical lines; residual ≤400 (budget 366); actual formatted counts and source diff size are recorded. Exceeding the 500-line source diff or residual budget escalates before publication. +4. State lifetime/ownership and side-effect timing match Module-level state and cycles; changed graph has no new value or type cycle and no upward leaf → original path. +5. Every listed behavioral/text oracle keeps its specified target/disposition; the named guard mutation produces the expected failure and restoration yields green focused tests. +6. Typecheck, focused checks, GUI build/lint, privacy scan, remote whole-suite and remote GUI PR-ready suite pass at the exact layer head, with exit codes and CI SHA evidence; no repository-wide local suite. +7. PR contains all repository template sections and the five-layer stack map; correct base/head, no merge, no release, no unrelated cleanup. If title/body says GUI, attach a real unchanged-UI screenshot as required by the repository gate; never fabricate an image link. + +## PR + +Title: `refactor(gui): extract quota reset date and locale formatting (split S20 L5/5)` + +Head: `codex/split-components-QuotaBars`. Base: `dev`. Closes: none. + +Fill `.github/PULL_REQUEST_TEMPLATE.md` Summary, Verification, Checklist; pure move only. Review only this layer's diff; publish later under parent authorization. Placeholder PR numbers below are intentional until PR creation, not fabricated existing PRs. + +| Layer | PR | Head branch | Base | Review focus | +|---|---|---|---|---| +| 1 | #TBD-S20-L1 | `codex/split-pages-ClaudeDesktop` | `dev` | isolate Claude Desktop profile data and lane views | +| 2 | #TBD-S20-L2 | `codex/split-components-MemoryObservabilityCard` | `dev` | separate memory metrics and stat views from restart polling | +| 3 | #TBD-S20-L3 | `codex/split-components-provider-workspace-ProviderSettings` | `dev` | extract provider draft helpers and stateless settings fields | +| 4 | #TBD-S20-L4 | `codex/split-pages-dashboard-shared` | `dev` | isolate dashboard sidecar option contracts and selection | +| 5 | #TBD-S20-L5 | `codex/split-components-QuotaBars` | `dev` | extract quota reset date and locale formatting ← this layer | + +DEV-STACK-03: each of the five layers carries its own gates and this complete map. S20 groups execution order and PR navigation only; all five layers are independent under STACK-INDEPENDENCE-01. + +Base: dev — no dependency on the layers below; no cascade obligation. + +Merge remains forbidden here (DEV-STACK-04). diff --git a/devlog/_plan/260905_now_split_train/720_release_notes_a.md b/devlog/_plan/260905_now_split_train/720_release_notes_a.md new file mode 100644 index 0000000000..5c9cd010a8 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/720_release_notes_a.md @@ -0,0 +1,184 @@ +# S21 L1/4 — Release notes part a + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**. Mode: bounded docs-only delegation; C3 structural planning with C4-level release-surface review care for eventual execution. Parent owns orchestration, goal, and loop state. No orchestration commands here. + +Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated below. Stop after the specified layer has independently met those gates and has exact-head PR evidence; stop this drafting task after its assigned document is complete. Escalate behavior/signature changes, extra file owners, failed baseline, cycles, any leaf above 400, or literal diff-budget overruns. Never merge. + +Structural decision: split the 1,233-line mixed concern while retaining the existing executable/public path. Reject deletion/configuration (cannot preserve the API and reduce this source), and reject moving callers to leaves (needless churn). Existing owners searched with `rg --files scripts`, exact symbol searches, and import scans: `scripts/test-layout/{schema,plan,move}.ts` demonstrates same-directory feature folders; `scripts/build-release-changelog.ts` consumes these helpers rather than owning an interchangeable implementation. Use `scripts/release-notes/*.ts`, no convenience `index.ts`. Boundary exception to generic barrel-only guidance is explicit: the user requires compatibility re-exports in the executable file. + +Current edges: builder/bump/tests → release-notes; release-notes has no imports. Intended edges: existing consumers → same facade → concern leaves → format constants (and render → generated/commits). Blast radius: scripts feature/public helper surface, no runtime proxy modules. + +Goal: extract low-consumer carry, commit, polish, and formatting-constant leaves; keep parser/tag/renderer code for B. +Non-goals: no release changes, no API additions on the original path, no CLI redesign, no output/category/credit/transport changes, no cleanup of existing long functions. + +Budget escalation: 002 says “≤500 changed source lines.” A moves 471 and B moves 439 distinct original lines, but Git addition+deletion numstat is at least 942 and 878 respectively before binding changes. Two literal ≤500-numstat layers cannot remove the ≥833 lines necessary to reach 400 (even zero overhead needs ≥1,666 changed lines). Parent must explicitly approve the pure-move size exception or expand/replan S21 before implementation. This document does not silently reinterpret that limit or authorize extra branches. + +## Symbol inventory + +Basis: docs HEAD `4cc219549`; code `origin/dev = 1362b1a38`. A fresh `git diff origin/dev -- scripts/release-notes.ts scripts/test.ts scripts/disposable-host/codex-service-composed-acceptance.ts` was empty, so working-tree line anchors below are origin/dev anchors. Lane 016's `scripts/release-notes.ts` record supplies the audited seam; source is independently read. + +Range method: `sg run --lang ts --kind 'function_declaration,lexical_declaration,type_alias_declaration,interface_declaration,class_declaration' --json=compact scripts/release-notes.ts`, filtered against column-zero declarations/`export` lines with `rg`; inclusive declaration spans, excluding preceding comments. Consumer count = distinct **external direct importer files** returned by `rg -l` for the public path, then `rg -w` for the identifier in their named import blocks; private symbols have 0 external consumers (not 0 internal calls). CLI references and same-named local declarations are excluded. There are 52 declarations, no import declarations. The top-level `if (import.meta.main)` statement at 1231–1233 stays original in both parts. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ParsedReleaseTag` | type | 19–25 | no | 0 | `release-notes/tags.ts` (L2, retain here now) | +| `parseReleaseTag` | function | 27–36 | no | 0 | `release-notes/tags.ts` (L2, retain here now) | +| `comparePrereleaseIds` | function | 39–60 | no | 0 | `release-notes/tags.ts` (L2, retain here now) | +| `compareReleaseTags` | function | 66–79 | yes | 3 | `release-notes/tags.ts` (L2, retain here now) | +| `sortVersionTagsAscending` | function | 81–83 | no | 0 | `release-notes/tags.ts` (L2, retain here now) | +| `matchingPreviewTag` | function | 86–89 | yes | 1 | `release-notes/tags.ts` (L2, retain here now) | +| `matchingPreviewTags` | function | 96–103 | yes | 1 | `release-notes/tags.ts` (L2, retain here now) | +| `previousReleaseNotesTag` | function | 122–133 | yes | 1 | `release-notes/tags.ts` (L2, retain here now) | +| `stripCarriedReleaseNotes` | function | 136–159 | yes | 1 | `release-notes/carried.ts` (this layer) | +| `isEmptyGeneratedNotes` | function | 162–170 | yes | 0 | `release-notes/carried.ts` (this layer) | +| `hasMeaningfulCarriedNotes` | function | 177–179 | yes | 1 | `release-notes/carried.ts` (this layer) | +| `ReleaseNoteCommit` | type | 185–189 | yes | 0 | `release-notes/commits.ts` (this layer) | +| `RENDER_CATEGORY_ORDER` | const | 192–192 | no | 0 | `release-notes/format-constants.ts` (this layer) | +| `COMMIT_TYPE_CATEGORY` | const | 195–206 | no | 0 | `release-notes/commits.ts` (this layer) | +| `isReleasePlumbingCommit` | function | 213–220 | yes | 1 | `release-notes/commits.ts` (this layer) | +| `sanitizeCommitText` | function | 228–239 | yes | 2 | `release-notes/commits.ts` (this layer) | +| `renderCommitFallbackNotes` | function | 258–293 | yes | 1 | `release-notes/commits.ts` (this layer) | +| `extractCommitBulletSections` | function | 305–332 | yes | 1 | `release-notes/commits.ts` (this layer) | +| `mergeCommitBulletSections` | function | 339–373 | yes | 1 | `release-notes/commits.ts` (this layer) | +| `parseCommitLog` | function | 383–396 | yes | 1 | `release-notes/commits.ts` (this layer) | +| `hasNonWhitespace` | function | 398–400 | yes | 0 | `release-notes/carried.ts` (this layer) | +| `joinCarriedPreviewNotes` | function | 403–409 | yes | 1 | `release-notes/carried.ts` (this layer) | +| `selectNewestCarriedPreviewTag` | function | 417–427 | yes | 1 | `release-notes/carried.ts` (this layer) | +| `parseTakeoverSourcePr` | function | 434–440 | yes | 1 | `release-notes/takeovers.ts` (L2, retain here now) | +| `GENERATE_NOTES_PR_LINE` | const | 442–443 | no | 0 | `release-notes/takeovers.ts` (L2, retain here now) | +| `TakeoverCreditLookup` | type | 445–449 | yes | 0 | `release-notes/takeovers.ts` (L2, retain here now) | +| `rewriteTakeoverCredits` | function | 461–506 | yes | 2 | `release-notes/takeovers.ts` (L2, retain here now) | +| `ReleaseNotePr` | type | 508–512 | yes | 0 | `release-notes/generated.ts` (L2, retain here now) | +| `ReleaseNoteCategory` | type | 514–517 | yes | 0 | `release-notes/generated.ts` (L2, retain here now) | +| `GENERATED_PR_LINE` | const | 529–530 | no | 0 | `release-notes/generated.ts` (L2, retain here now) | +| `GENERATED_BULLET_LINE` | const | 531–532 | no | 0 | `release-notes/generated.ts` (L2, retain here now) | +| `CHANGELOG_PR_LINE` | const | 533–534 | no | 0 | `release-notes/generated.ts` (L2, retain here now) | +| `SCAFFOLD_HEADINGS` | const | 535–535 | no | 0 | `release-notes/format-constants.ts` (this layer) | +| `parseGeneratedNotes` | function | 537–591 | yes | 2 | `release-notes/generated.ts` (L2, retain here now) | +| `CONVENTIONAL_COMMIT_PREFIX` | const | 598–599 | no | 0 | `release-notes/render.ts` (L2, retain here now) | +| `cleanPrTitle` | function | 601–621 | yes | 2 | `release-notes/render.ts` (L2, retain here now) | +| `scopeLabel` | function | 624–629 | yes | 0 | `release-notes/render.ts` (L2, retain here now) | +| `groupPrsByScope` | function | 632–644 | yes | 0 | `release-notes/render.ts` (L2, retain here now) | +| `renderReleaseNotes` | function | 654–751 | yes | 1 | `release-notes/render.ts` (L2, retain here now) | +| `extractPrNumbers` | function | 754–760 | yes | 1 | `release-notes/polish.ts` (this layer) | +| `extractChangelogPrNumbers` | function | 767–774 | yes | 1 | `release-notes/polish.ts` (this layer) | +| `countPrNumbers` | function | 777–784 | no | 0 | `release-notes/polish.ts` (this layer) | +| `parseSectionHeadings` | function | 787–793 | yes | 1 | `release-notes/polish.ts` (this layer) | +| `validatePolishedSections` | function | 802–827 | yes | 1 | `release-notes/polish.ts` (this layer) | +| `POLISH_SYSTEM_PROMPT` | const | 829–838 | no | 0 | `release-notes/polish.ts` (this layer) | +| `POLISH_REQUEST_TIMEOUT_MS` | const | 840–840 | no | 0 | `release-notes/polish.ts` (this layer) | +| `callChatCompletion` | function | 842–883 | no | 0 | `release-notes/polish.ts` (this layer) | +| `splitPolishInput` | function | 890–905 | yes | 1 | `release-notes/polish.ts` (this layer) | +| `isPolishBaseUrlAllowed` | function | 912–927 | yes | 1 | `release-notes/polish.ts` (this layer) | +| `readStdinOrFile` | function | 929–934 | no | 0 | original | +| `parseFlagArgs` | function | 936–958 | no | 0 | original | +| `main` | function | 960–1229 | no | 0 | original | + +## Leaf partition + +Move these origin/dev ranges including their comments/spacing: 135–180 and 398–428 → carried (77 lines); 181–397 except 192 → commits (216); 192 and 535 → format-constants (2); 753–928 → polish (176). Total original lines moved = **471**. Export keywords may be added to existing private cross-leaf bindings, but bodies/signatures remain unchanged. + +| New leaf | Symbols | Expected lines including imports | Own imports | +|---|---|---:|---| +| `scripts/release-notes/format-constants.ts` | `RENDER_CATEGORY_ORDER`, `SCAFFOLD_HEADINGS` | 4 | none | +| `scripts/release-notes/carried.ts` | `stripCarriedReleaseNotes`, `isEmptyGeneratedNotes`, `hasMeaningfulCarriedNotes`, `hasNonWhitespace`, `joinCarriedPreviewNotes`, `selectNewestCarriedPreviewTag` | 77 | none | +| `scripts/release-notes/commits.ts` | `ReleaseNoteCommit`, `COMMIT_TYPE_CATEGORY`, `isReleasePlumbingCommit`, `sanitizeCommitText`, `renderCommitFallbackNotes`, `extractCommitBulletSections`, `mergeCommitBulletSections`, `parseCommitLog` | 219 | `import { RENDER_CATEGORY_ORDER, SCAFFOLD_HEADINGS } from "./format-constants";` | +| `scripts/release-notes/polish.ts` | `extractPrNumbers`, `extractChangelogPrNumbers`, `countPrNumbers`, `parseSectionHeadings`, `validatePolishedSections`, `POLISH_SYSTEM_PROMPT`, `POLISH_REQUEST_TIMEOUT_MS`, `callChatCompletion`, `splitPolishInput`, `isPolishBaseUrlAllowed` | 176 | none; existing Bun/fetch/Response/URL globals | + +Residual expectation: **780** lines = 1,233 − 471 + 18 import/re-export/spacing budget. This is intentionally above 400: **730 / S21 L2 (#b)** moves the remaining 439 original lines, leaving **349** (= 780 − 439 + 8 net binding/spacing budget). Counts are explicit physical-line budgets, not a claim to have generated code; implementation must record actual `wc -l`. + +Ordering evidence: format constants have 0 external consumers, carried/polish each have 1 consumer-file union, commits has 2 (the builder uses `sanitizeCommitText`). Remaining takeovers/generated/render have 2; tags has 4 at the module-group level and `compareReleaseTags` has 3. Thus A takes the smallest closed dependency sets first; same-fan-in ties keep the parser/renderer together in B. Private zero-consumer helpers move with their callers, not as pointless individual files. + +## Re-export block + +Add the following compatibility exports; all not-yet-moved exported declarations remain exactly where they are. + +```ts +export { stripCarriedReleaseNotes, isEmptyGeneratedNotes, hasMeaningfulCarriedNotes, hasNonWhitespace, joinCarriedPreviewNotes, selectNewestCarriedPreviewTag } from "./release-notes/carried"; +export type { ReleaseNoteCommit } from "./release-notes/commits"; +export { isReleasePlumbingCommit, sanitizeCommitText, renderCommitFallbackNotes, extractCommitBulletSections, mergeCommitBulletSections, parseCommitLog } from "./release-notes/commits"; +export { extractPrNumbers, extractChangelogPrNumbers, parseSectionHeadings, validatePolishedSections, splitPolishInput, isPolishBaseUrlAllowed } from "./release-notes/polish"; +``` + +Explicit residual local imports (re-exports create no local bindings): + +```ts +import { RENDER_CATEGORY_ORDER, SCAFFOLD_HEADINGS } from "./release-notes/format-constants"; +import { stripCarriedReleaseNotes, hasMeaningfulCarriedNotes, joinCarriedPreviewNotes } from "./release-notes/carried"; +import { renderCommitFallbackNotes, parseCommitLog, extractCommitBulletSections, mergeCommitBulletSections } from "./release-notes/commits"; +import { extractPrNumbers, extractChangelogPrNumbers, parseSectionHeadings, validatePolishedSections, splitPolishInput, isPolishBaseUrlAllowed, callChatCompletion } from "./release-notes/polish"; +``` + +The remaining original parser/render/type declarations are local; do not import them from the facade itself. +`callChatCompletion` is a new internal leaf export used by the unchanged CLI, **not** a new compatibility export. + +## Module-level state and cycles + +`SCAFFOLD_HEADINGS` at origin `scripts/release-notes.ts:535` is the sole top-level Set; owner `scripts/release-notes/format-constants.ts` from L1 onward. `RENDER_CATEGORY_ORDER` (:192) is a read-only-by-convention array, same owner. `COMMIT_TYPE_CATEGORY` (:195–206) belongs only to commits. Regex constants belong to takeovers (:442), generated (:529–534), and render (:598); prompt and timeout constants (:829, :840) belong only to polish. No module-level let, Map, WeakMap, timer, or lock. Function-local Maps/Sets stay per-call, including renderer categories and polish counts; do not hoist them. + +The takeovers/generated/render owners above are the final L2 destinations; their declarations remain original during L1. Avoid commits → facade → commits through `SCAFFOLD_HEADINGS`, and (in L2) render → facade → render through `parseGeneratedNotes`: leaves import their dependencies directly, never the original file. Types `ReleaseNotePr`/`ReleaseNoteCategory` remain original in L1 and move to generated in L2; render then imports the type from that leaf, not from the facade. There is no runtime or type-only return edge. Coupling is functional/sequential; immutable-by-convention formatting data is not duplicated. Lazy dynamic imports are not introduced. The CLI `import.meta.main` guard remains on the executable path, with no top-level I/O added to leaves. + +## Tests + +Exact public-path importer search, `rg -l 'from ".*/release-notes"' src gui/src scripts tests`, returns four files: `scripts/build-release-changelog.ts:20`, `scripts/bump-dev-version.ts:57`, and these two test files: + +- `tests/ci-workflows/release-notes.test.ts:27` — unchanged public import and assertions. +- `tests/ci-workflows/release-version-line.test.ts:3` — unchanged public import and assertions. + +No test reads `scripts/release-notes.ts` as source. The broad basename-plus-reader intersection also finds `tests/ci-workflows/release-version-line.test.ts` and `tests/ci-workflows/ci-workflows.test.ts`, but those read package/release/workflow inputs, not this implementation. In particular `ci-workflows.test.ts:868` checks a workflow command string. Disposition: unchanged, no retarget-to-leaf and no add-leaf-to-scan-list for existing text oracles. Do not turn the stale-check estimate into a fictitious source reader. + +Indirect consumer regression: `tests/ci-workflows/build-release-changelog.test.ts` remains unchanged and must run explicitly because the release builder imports five helpers. Preserve CLI dispatch and `import.meta.main` at origin `scripts/release-notes.ts:1231`; never import `scripts/release.ts` in its place. + +Future implementation guards: add named-export equivalence and leaf-no-facade-import assertions to the existing `tests/ci-workflows/release-notes.test.ts` (no new test file/layout mapping). Add each new leaf path to that new scan's explicit list. Drive it red once by removing one compatibility re-export, restore it, then inject one leaf-to-facade import and restore it. Do this only in the future isolated implementation worktree; this docs task ran no guards. + +Behavioral guards to drive red: the existing polish missing/repeated-PR assertions at release-notes.test.ts:805 and :830 by temporarily bypassing the matching check in polish; restore unchanged logic. + +## Verification + +These are future implementation commands, not checks run by this docs-only delegation. Instantiate `002_layer_map.md` → **Per-layer gate** at this layer's exact tip: + +```sh +bun run typecheck +bun test tests/ci-workflows/release-notes.test.ts tests/ci-workflows/release-version-line.test.ts tests/ci-workflows/build-release-changelog.test.ts tests/ci-workflows/ci-workflows.test.ts +bun run privacy:scan +wc -l scripts/release-notes/format-constants.ts scripts/release-notes/carried.ts scripts/release-notes/commits.ts scripts/release-notes/polish.ts scripts/release-notes.ts +rg -l 'from ".*/release-notes"' src gui/src scripts tests +git diff --numstat dev...HEAD -- scripts +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-release-notes-a && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The importer result must remain the same four paths, not just a same-sized replacement set. Inspect named imports separately: the builder defines its own `renderReleaseNotes`; it does not import that identifier. Run explicit-import DFS (including type/re-export edges) over the new leaf paths and facade; zero return paths. No `src/server`, `src/router`, or `src/lib` changes, so the conditional core-Lab test is not activated and its protected roots stay untouched. + +Require full remote command exit status and complete retained log, not only the tail: the example pipeline in 002 can hide Bun's failure; use a pipefail-capable remote shell or capture the test status before printing its tail. Verify remote checkout SHA equals this layer's tip. `scripts/AGENTS.md` additionally requires `bun run prepush`; it includes a full suite (`package.json:55`) and therefore must also run on the authorized remote, never locally. No release/publish/network polish operation is a verifier. Obtain explicit release-tooling security review under MAINTAINERS.md:59–71 before review-ready; it is not a permission to publish. + +## Accept criteria + +1. Every one of the 52 origin declarations has exactly one owner in the inventory; moved bodies/comments match the origin ranges except imports/export markers. +2. Every original value/type export remains importable through `scripts/release-notes.ts`; no leaf imports that facade, even type-only. +3. Four new leaves meet ≤400; the measured residual is recorded against the 780 budget and explicitly assigned to 730 (#b). +4. All public importer paths stay unchanged; focused tests, remote full suite/prepush, typecheck, privacy scan, and negative guard receipts are recorded at the exact head. +5. No command dispatch, ordering, credit, PR-reference validation, exit status, network policy, or release behavior changes; no release is executed. +6. Parent resolves the literal diff-size contradiction before execution; stack bases and required reviews/CI match this layer, with no merge. + +## PR + +Title: `refactor(scripts): extract carry commit and polish leaves (split S21 L1/4)` +Branch: `codex/split-release-notes-a`. +Base: `dev`. +Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Review only this layer's diff; this is the bottom layer. Stack navigation (only L2 depends on L1; merges require separate authorization): + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S21-L4 | `codex/split-disposable-host-codex-service-composed-acceptance` | `dev` | Fixture owner; sentinel order | +| 3 | #TBD-S21-L3 | `codex/split-test` | `dev` | Environment and selection leaves | +| 2 | #TBD-S21-L2 | `codex/split-release-notes-b` | `codex/split-release-notes-a` | Tags, attribution, PR rendering | +| 1 | #TBD-S21-L1 | `codex/split-release-notes-a` | `dev` | Carry, commit fallback, polish | + +Base: dev — no dependency on lower layers; this layer is the parent of 730 (branch based on it), so any change here cascades into that layer with `git rebase --update-refs` + `--force-with-lease` before review (DEV-STACK-02). No such Git action is part of this docs-only delegation. diff --git a/devlog/_plan/260905_now_split_train/730_release_notes_b.md b/devlog/_plan/260905_now_split_train/730_release_notes_b.md new file mode 100644 index 0000000000..8eb53d8981 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/730_release_notes_b.md @@ -0,0 +1,190 @@ +# S21 L2/4 — Release notes part b + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**. Mode: bounded docs-only delegation; C3 structural planning with C4-level release-surface review care for eventual execution. Parent owns orchestration, goal, and loop state. No orchestration commands here. + +Verifier: `002_layer_map.md` → **Per-layer gate**, instantiated below. Stop after the specified layer has independently met those gates and has exact-head PR evidence; stop this drafting task after its assigned document is complete. Escalate behavior/signature changes, extra file owners, failed baseline, cycles, any leaf above 400, or literal diff-budget overruns. Never merge. + +Structural decision: split the 1,233-line mixed concern while retaining the existing executable/public path. Reject deletion/configuration (cannot preserve the API and reduce this source), and reject moving callers to leaves (needless churn). Existing owners searched with `rg --files scripts`, exact symbol searches, and import scans: `scripts/test-layout/{schema,plan,move}.ts` demonstrates same-directory feature folders; `scripts/build-release-changelog.ts` consumes these helpers rather than owning an interchangeable implementation. Use `scripts/release-notes/*.ts`, no convenience `index.ts`. Boundary exception to generic barrel-only guidance is explicit: the user requires compatibility re-exports in the executable file. + +Current edges: builder/bump/tests → release-notes; release-notes has no imports. Intended edges: existing consumers → same facade → concern leaves → format constants (and render → generated/commits). Blast radius: scripts feature/public helper surface, no runtime proxy modules. + +Goal: finish the split through tags, takeover attribution, generated-note parsing, and rendering; bring the facade below 400. +Non-goals: no release changes, no API additions on the original path, no CLI redesign, no output/category/credit/transport changes, no cleanup of existing long functions. + +Budget escalation: 002 says “≤500 changed source lines.” A moves 471 and B moves 439 distinct original lines, but Git addition+deletion numstat is at least 942 and 878 respectively before binding changes. Two literal ≤500-numstat layers cannot remove the ≥833 lines necessary to reach 400 (even zero overhead needs ≥1,666 changed lines). Parent must explicitly approve the pure-move size exception or expand/replan S21 before implementation. This document does not silently reinterpret that limit or authorize extra branches. + +## Symbol inventory + +Basis: docs HEAD `4cc219549`; code `origin/dev = 1362b1a38`. A fresh `git diff origin/dev -- scripts/release-notes.ts scripts/test.ts scripts/disposable-host/codex-service-composed-acceptance.ts` was empty, so working-tree line anchors below are origin/dev anchors. Lane 016's `scripts/release-notes.ts` record supplies the audited seam; source is independently read. + +Range method: `sg run --lang ts --kind 'function_declaration,lexical_declaration,type_alias_declaration,interface_declaration,class_declaration' --json=compact scripts/release-notes.ts`, filtered against column-zero declarations/`export` lines with `rg`; inclusive declaration spans, excluding preceding comments. Consumer count = distinct **external direct importer files** returned by `rg -l` for the public path, then `rg -w` for the identifier in their named import blocks; private symbols have 0 external consumers (not 0 internal calls). CLI references and same-named local declarations are excluded. There are 52 declarations, no import declarations. The top-level `if (import.meta.main)` statement at 1231–1233 stays original in both parts. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `ParsedReleaseTag` | type | 19–25 | no | 0 | `release-notes/tags.ts` (this layer) | +| `parseReleaseTag` | function | 27–36 | no | 0 | `release-notes/tags.ts` (this layer) | +| `comparePrereleaseIds` | function | 39–60 | no | 0 | `release-notes/tags.ts` (this layer) | +| `compareReleaseTags` | function | 66–79 | yes | 3 | `release-notes/tags.ts` (this layer) | +| `sortVersionTagsAscending` | function | 81–83 | no | 0 | `release-notes/tags.ts` (this layer) | +| `matchingPreviewTag` | function | 86–89 | yes | 1 | `release-notes/tags.ts` (this layer) | +| `matchingPreviewTags` | function | 96–103 | yes | 1 | `release-notes/tags.ts` (this layer) | +| `previousReleaseNotesTag` | function | 122–133 | yes | 1 | `release-notes/tags.ts` (this layer) | +| `stripCarriedReleaseNotes` | function | 136–159 | yes | 1 | `release-notes/carried.ts` (L1, unchanged) | +| `isEmptyGeneratedNotes` | function | 162–170 | yes | 0 | `release-notes/carried.ts` (L1, unchanged) | +| `hasMeaningfulCarriedNotes` | function | 177–179 | yes | 1 | `release-notes/carried.ts` (L1, unchanged) | +| `ReleaseNoteCommit` | type | 185–189 | yes | 0 | `release-notes/commits.ts` (L1, unchanged) | +| `RENDER_CATEGORY_ORDER` | const | 192–192 | no | 0 | `release-notes/format-constants.ts` (L1, unchanged) | +| `COMMIT_TYPE_CATEGORY` | const | 195–206 | no | 0 | `release-notes/commits.ts` (L1, unchanged) | +| `isReleasePlumbingCommit` | function | 213–220 | yes | 1 | `release-notes/commits.ts` (L1, unchanged) | +| `sanitizeCommitText` | function | 228–239 | yes | 2 | `release-notes/commits.ts` (L1, unchanged) | +| `renderCommitFallbackNotes` | function | 258–293 | yes | 1 | `release-notes/commits.ts` (L1, unchanged) | +| `extractCommitBulletSections` | function | 305–332 | yes | 1 | `release-notes/commits.ts` (L1, unchanged) | +| `mergeCommitBulletSections` | function | 339–373 | yes | 1 | `release-notes/commits.ts` (L1, unchanged) | +| `parseCommitLog` | function | 383–396 | yes | 1 | `release-notes/commits.ts` (L1, unchanged) | +| `hasNonWhitespace` | function | 398–400 | yes | 0 | `release-notes/carried.ts` (L1, unchanged) | +| `joinCarriedPreviewNotes` | function | 403–409 | yes | 1 | `release-notes/carried.ts` (L1, unchanged) | +| `selectNewestCarriedPreviewTag` | function | 417–427 | yes | 1 | `release-notes/carried.ts` (L1, unchanged) | +| `parseTakeoverSourcePr` | function | 434–440 | yes | 1 | `release-notes/takeovers.ts` (this layer) | +| `GENERATE_NOTES_PR_LINE` | const | 442–443 | no | 0 | `release-notes/takeovers.ts` (this layer) | +| `TakeoverCreditLookup` | type | 445–449 | yes | 0 | `release-notes/takeovers.ts` (this layer) | +| `rewriteTakeoverCredits` | function | 461–506 | yes | 2 | `release-notes/takeovers.ts` (this layer) | +| `ReleaseNotePr` | type | 508–512 | yes | 0 | `release-notes/generated.ts` (this layer) | +| `ReleaseNoteCategory` | type | 514–517 | yes | 0 | `release-notes/generated.ts` (this layer) | +| `GENERATED_PR_LINE` | const | 529–530 | no | 0 | `release-notes/generated.ts` (this layer) | +| `GENERATED_BULLET_LINE` | const | 531–532 | no | 0 | `release-notes/generated.ts` (this layer) | +| `CHANGELOG_PR_LINE` | const | 533–534 | no | 0 | `release-notes/generated.ts` (this layer) | +| `SCAFFOLD_HEADINGS` | const | 535–535 | no | 0 | `release-notes/format-constants.ts` (L1, unchanged) | +| `parseGeneratedNotes` | function | 537–591 | yes | 2 | `release-notes/generated.ts` (this layer) | +| `CONVENTIONAL_COMMIT_PREFIX` | const | 598–599 | no | 0 | `release-notes/render.ts` (this layer) | +| `cleanPrTitle` | function | 601–621 | yes | 2 | `release-notes/render.ts` (this layer) | +| `scopeLabel` | function | 624–629 | yes | 0 | `release-notes/render.ts` (this layer) | +| `groupPrsByScope` | function | 632–644 | yes | 0 | `release-notes/render.ts` (this layer) | +| `renderReleaseNotes` | function | 654–751 | yes | 1 | `release-notes/render.ts` (this layer) | +| `extractPrNumbers` | function | 754–760 | yes | 1 | `release-notes/polish.ts` (L1, unchanged) | +| `extractChangelogPrNumbers` | function | 767–774 | yes | 1 | `release-notes/polish.ts` (L1, unchanged) | +| `countPrNumbers` | function | 777–784 | no | 0 | `release-notes/polish.ts` (L1, unchanged) | +| `parseSectionHeadings` | function | 787–793 | yes | 1 | `release-notes/polish.ts` (L1, unchanged) | +| `validatePolishedSections` | function | 802–827 | yes | 1 | `release-notes/polish.ts` (L1, unchanged) | +| `POLISH_SYSTEM_PROMPT` | const | 829–838 | no | 0 | `release-notes/polish.ts` (L1, unchanged) | +| `POLISH_REQUEST_TIMEOUT_MS` | const | 840–840 | no | 0 | `release-notes/polish.ts` (L1, unchanged) | +| `callChatCompletion` | function | 842–883 | no | 0 | `release-notes/polish.ts` (L1, unchanged) | +| `splitPolishInput` | function | 890–905 | yes | 1 | `release-notes/polish.ts` (L1, unchanged) | +| `isPolishBaseUrlAllowed` | function | 912–927 | yes | 1 | `release-notes/polish.ts` (L1, unchanged) | +| `readStdinOrFile` | function | 929–934 | no | 0 | original | +| `parseFlagArgs` | function | 936–958 | no | 0 | original | +| `main` | function | 960–1229 | no | 0 | original | + +## Leaf partition + +L1's four leaves are already present and untouched. Move origin/dev 19–134 → tags (116); 429–507 → takeovers (79); 508–534 plus 536–592 → generated (84); 593–752 → render (160). Total original lines moved in B = **439**. These anchors remain origin coordinates, not post-A line numbers. + +| New leaf | Symbols | Expected lines including imports | Own imports | +|---|---|---:|---| +| `scripts/release-notes/tags.ts` | `ParsedReleaseTag`, `parseReleaseTag`, `comparePrereleaseIds`, `compareReleaseTags`, `sortVersionTagsAscending`, `matchingPreviewTag`, `matchingPreviewTags`, `previousReleaseNotesTag` | 116 | none | +| `scripts/release-notes/takeovers.ts` | `parseTakeoverSourcePr`, `GENERATE_NOTES_PR_LINE`, `TakeoverCreditLookup`, `rewriteTakeoverCredits` | 79 | none | +| `scripts/release-notes/generated.ts` | `ReleaseNotePr`, `ReleaseNoteCategory`, `GENERATED_PR_LINE`, `GENERATED_BULLET_LINE`, `CHANGELOG_PR_LINE`, `parseGeneratedNotes` | 86 | `import { SCAFFOLD_HEADINGS } from "./format-constants";` | +| `scripts/release-notes/render.ts` | `CONVENTIONAL_COMMIT_PREFIX`, `cleanPrTitle`, `scopeLabel`, `groupPrsByScope`, `renderReleaseNotes` | 165 | `import { RENDER_CATEGORY_ORDER } from "./format-constants";`; `import { parseGeneratedNotes } from "./generated";`; `import type { ReleaseNotePr } from "./generated";`; `import { extractCommitBulletSections, mergeCommitBulletSections } from "./commits";` | + +Residual expectation: **349** lines = A's 780 − 439 + 8 net import/re-export/spacing budget. Combined source accounting: 1,233 − 471 (A) − 439 (B) = 323 original lines retained (1–18 and 929–1233), plus 26 cumulative binding/spacing budget = 349. No #c is required; all eight release-note leaves are ≤400. This budget retains `main` intact at 270 lines: existing >50-function debt is not silently recast as solved by a pure file split. + +## Re-export block + +Final cumulative compatibility exports (retain A's lines and add B's). No export is removed, renamed, or widened by re-exporting private leaf bindings. + +```ts +export { stripCarriedReleaseNotes, isEmptyGeneratedNotes, hasMeaningfulCarriedNotes, hasNonWhitespace, joinCarriedPreviewNotes, selectNewestCarriedPreviewTag } from "./release-notes/carried"; +export type { ReleaseNoteCommit } from "./release-notes/commits"; +export { isReleasePlumbingCommit, sanitizeCommitText, renderCommitFallbackNotes, extractCommitBulletSections, mergeCommitBulletSections, parseCommitLog } from "./release-notes/commits"; +export { extractPrNumbers, extractChangelogPrNumbers, parseSectionHeadings, validatePolishedSections, splitPolishInput, isPolishBaseUrlAllowed } from "./release-notes/polish"; +export { compareReleaseTags, matchingPreviewTag, matchingPreviewTags, previousReleaseNotesTag } from "./release-notes/tags"; +export type { TakeoverCreditLookup } from "./release-notes/takeovers"; +export { parseTakeoverSourcePr, rewriteTakeoverCredits } from "./release-notes/takeovers"; +export type { ReleaseNotePr, ReleaseNoteCategory } from "./release-notes/generated"; +export { parseGeneratedNotes } from "./release-notes/generated"; +export { cleanPrTitle, scopeLabel, groupPrsByScope, renderReleaseNotes } from "./release-notes/render"; +``` + +Explicit residual local imports (re-exports create no local bindings): + +```ts +import { stripCarriedReleaseNotes, hasMeaningfulCarriedNotes, joinCarriedPreviewNotes } from "./release-notes/carried"; +import { renderCommitFallbackNotes, parseCommitLog } from "./release-notes/commits"; +import { extractPrNumbers, extractChangelogPrNumbers, parseSectionHeadings, validatePolishedSections, splitPolishInput, isPolishBaseUrlAllowed, callChatCompletion } from "./release-notes/polish"; +import { matchingPreviewTag, matchingPreviewTags, previousReleaseNotesTag } from "./release-notes/tags"; +import { rewriteTakeoverCredits } from "./release-notes/takeovers"; +import { renderReleaseNotes } from "./release-notes/render"; +``` + +Residual helpers `readStdinOrFile`, `parseFlagArgs`, and `main` remain local. No residual type import is needed. +`callChatCompletion` is a new internal leaf export used by the unchanged CLI, **not** a new compatibility export. + +## Module-level state and cycles + +`SCAFFOLD_HEADINGS` at origin `scripts/release-notes.ts:535` is the sole top-level Set; owner `scripts/release-notes/format-constants.ts` from L1 onward. `RENDER_CATEGORY_ORDER` (:192) is a read-only-by-convention array, same owner. `COMMIT_TYPE_CATEGORY` (:195–206) belongs only to commits. Regex constants belong to takeovers (:442), generated (:529–534), and render (:598); prompt and timeout constants (:829, :840) belong only to polish. No module-level let, Map, WeakMap, timer, or lock. Function-local Maps/Sets stay per-call, including renderer categories and polish counts; do not hoist them. + +Avoid commits → facade → commits through `SCAFFOLD_HEADINGS`, and render → facade → render through `parseGeneratedNotes`: leaves import constants/generated/commits directly as listed, never the original file. Types `ReleaseNotePr`/`ReleaseNoteCategory` belong to generated; render imports the type from that leaf, not from the facade. There is no runtime or type-only return edge. Coupling is functional/sequential; immutable-by-convention formatting data is not duplicated. Lazy dynamic imports are not introduced. The CLI `import.meta.main` guard remains on the executable path, with no top-level I/O added to leaves. + +## Tests + +Exact public-path importer search, `rg -l 'from ".*/release-notes"' src gui/src scripts tests`, returns four files: `scripts/build-release-changelog.ts:20`, `scripts/bump-dev-version.ts:57`, and these two test files: + +- `tests/ci-workflows/release-notes.test.ts:27` — unchanged public import and assertions. +- `tests/ci-workflows/release-version-line.test.ts:3` — unchanged public import and assertions. + +No test reads `scripts/release-notes.ts` as source. The broad basename-plus-reader intersection also finds `tests/ci-workflows/release-version-line.test.ts` and `tests/ci-workflows/ci-workflows.test.ts`, but those read package/release/workflow inputs, not this implementation. In particular `ci-workflows.test.ts:868` checks a workflow command string. Disposition: unchanged, no retarget-to-leaf and no add-leaf-to-scan-list for existing text oracles. Do not turn the stale-check estimate into a fictitious source reader. + +Indirect consumer regression: `tests/ci-workflows/build-release-changelog.test.ts` remains unchanged and must run explicitly because the release builder imports five helpers. Preserve CLI dispatch and `import.meta.main` at origin `scripts/release-notes.ts:1231`; never import `scripts/release.ts` in its place. + +Future implementation guards: add named-export equivalence and leaf-no-facade-import assertions to the existing `tests/ci-workflows/release-notes.test.ts` (no new test file/layout mapping). Add each new leaf path to that new scan's explicit list. Drive it red once by removing one compatibility re-export, restore it, then inject one leaf-to-facade import and restore it. Do this only in the future isolated implementation worktree; this docs task ran no guards. + +Behavioral guard to drive red: category rendering/order via release-notes.test.ts:881 by temporarily reversing the shared order, then restore; this validates the whole preserved public-path chain. + +## Verification + +These are future implementation commands, not checks run by this docs-only delegation. Instantiate `002_layer_map.md` → **Per-layer gate** at this layer's exact tip: + +```sh +bun run typecheck +bun test tests/ci-workflows/release-notes.test.ts tests/ci-workflows/release-version-line.test.ts tests/ci-workflows/build-release-changelog.test.ts tests/ci-workflows/ci-workflows.test.ts +bun run privacy:scan +wc -l scripts/release-notes/format-constants.ts scripts/release-notes/carried.ts scripts/release-notes/commits.ts scripts/release-notes/polish.ts scripts/release-notes/tags.ts scripts/release-notes/takeovers.ts scripts/release-notes/generated.ts scripts/release-notes/render.ts scripts/release-notes.ts +rg -l 'from ".*/release-notes"' src gui/src scripts tests +git diff --numstat codex/split-release-notes-a...HEAD -- scripts +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-release-notes-b && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The importer result must remain the same four paths, not just a same-sized replacement set. Inspect named imports separately: the builder defines its own `renderReleaseNotes`; it does not import that identifier. Run explicit-import DFS (including type/re-export edges) over the new leaf paths and facade; zero return paths. No `src/server`, `src/router`, or `src/lib` changes, so the conditional core-Lab test is not activated and its protected roots stay untouched. + +Require full remote command exit status and complete retained log, not only the tail: the example pipeline in 002 can hide Bun's failure; use a pipefail-capable remote shell or capture the test status before printing its tail. Verify remote checkout SHA equals this layer's tip. `scripts/AGENTS.md` additionally requires `bun run prepush`; it includes a full suite (`package.json:55`) and therefore must also run on the authorized remote, never locally. No release/publish/network polish operation is a verifier. Obtain explicit release-tooling security review under MAINTAINERS.md:59–71 before review-ready; it is not a permission to publish. + +## Accept criteria + +1. Every one of the 52 origin declarations has exactly one owner in the inventory; moved bodies/comments match the origin ranges except imports/export markers. +2. Every original value/type export remains importable through `scripts/release-notes.ts`; no leaf imports that facade, even type-only. +3. Four new leaves plus A's four meet ≤400; original facade meets ≤400 (349 expected); all A exports still resolve. +4. All public importer paths stay unchanged; focused tests, remote full suite/prepush, typecheck, privacy scan, and negative guard receipts are recorded at the exact head. +5. No command dispatch, ordering, credit, PR-reference validation, exit status, network policy, or release behavior changes; no release is executed. +6. Parent resolves the literal diff-size contradiction before execution; stack bases and required reviews/CI match this layer, with no merge. + +## PR + +Title: `refactor(scripts): separate release tag parsing and rendering (split S21 L2/4)` +Branch: `codex/split-release-notes-b`. +Base: `codex/split-release-notes-a`. +Closes: none. + +Use every section of `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Review only this layer's diff; depends on #TBD-S21-L1. Stack navigation (only L2 depends on L1; merges require separate authorization): + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S21-L4 | `codex/split-disposable-host-codex-service-composed-acceptance` | `dev` | Fixture owner; sentinel order | +| 3 | #TBD-S21-L3 | `codex/split-test` | `dev` | Environment and selection leaves | +| 2 | #TBD-S21-L2 | `codex/split-release-notes-b` | `codex/split-release-notes-a` | Tags, attribution, PR rendering | +| 1 | #TBD-S21-L1 | `codex/split-release-notes-a` | `dev` | Carry, commit fallback, polish | + +If the real parent `codex/split-release-notes-a` (#TBD-S21-L1) changes, cascade this layer onto that parent, verify ancestry/base refs, and refresh exact-head evidence (DEV-STACK-02). No such Git action is part of this docs-only delegation. diff --git a/devlog/_plan/260905_now_split_train/740_test.md b/devlog/_plan/260905_now_split_train/740_test.md new file mode 100644 index 0000000000..d6c36e5a93 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/740_test.md @@ -0,0 +1,174 @@ +# S21 L3/4 — Test runner selection leaves + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**. Goal: reduce `scripts/test.ts` from 572 to approximately 268 lines by extracting environment creation, argument interpretation, and changed-run preflight. Non-goals: no parallelism/timing/lock changes, no CLI flag changes, no serial-list migration, no test-layout writer changes, no dependency installation policy changes, no new runner framework. + +Mode: bounded docs-only architectural planning (C3; eventual environment/dependency-installation tooling receives C4 review care). Apply cxc-dev §1/§5 and cxc-dev-architecture; parent owns all orchestration and goal state. Verifier = `002_layer_map.md` → **Per-layer gate**, instantiated below. Stop this drafting task after this document; eventual layer stops only after its own exact-head verification and PR evidence. Escalate any behavioral change, hidden source reader, cycle, leaf >400, extra owner, or failure at the base. No merge. + +Structural decision: existing mixed runner/selection/environment module forces the split. Reject deletion/configuration because it cannot preserve the interface; reject moving the serial table because its owner-specific source writer is already an external boundary. Reuse naming convention `scripts/test-layout/{schema,plan,move}.ts` as `scripts/test/{environment,arguments,changed-selection}.ts`; no generic helpers/index. Search evidence: `rg --files scripts`, `rg -n 'SERIAL_LANE_SOURCE|readFileSync' scripts/test-layout/move.ts`, and public-path/symbol searches below. + +Current map: `tests/preload.ts:15` and `tests/ci-workflows/test-runner.test.ts:14` → test facade → `scripts/test-run-lock.ts`; `scripts/test-layout/move.ts:52` reads the facade text. Intended map: those same consumers → facade → three independent leaves, while facade → existing lock owner remains. Blast radius: test tooling and preload boundary; no product runtime changes. The original script remains executable and a compatibility boundary, not a new convenience barrel. + +Budget note: 312 original lines move; literal addition+deletion count is ≥624 before bindings, above 002's 500-changed-line statement. Parent must approve the mechanical-move exception or expand topology before execution; no unauthorized fifth layer is introduced here. + +## Symbol inventory + +Basis: docs `4cc219549`, code `origin/dev 1362b1a38`; fresh source diff for all three S21 files was empty. All ranges refer to `origin/dev:scripts/test.ts`, not a future rebased file. Lane 016's `scripts/test.ts` record is the audit input. + +Method: `sg run --lang ts --kind 'function_declaration,lexical_declaration,type_alias_declaration,interface_declaration,class_declaration' --json=compact scripts/test.ts`, retaining module declarations confirmed with column-zero `rg`. Consumer count is distinct direct public importer files with the symbol in their named import block (`rg -l` then `rg -w`); private bindings have zero external import consumers. Every one of 26 named non-import declarations follows; function locals/for-loop initializers are excluded. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `IsolatedTestEnvironment` | interface | 13–17 | yes | 0 | `test/environment.ts` | +| `createIsolatedTestEnvironment` | function | 19–68 | yes | 2 | `test/environment.ts` | +| `hasCliFlag` | function | 70–74 | no | 0 | `test/arguments.ts` | +| `DEFAULT_TEST_PARALLELISM` | const | 76–76 | no | 0 | `test/arguments.ts` | +| `BUN_TEST_OPTIONS_REQUIRING_VALUES` | const | 81–151 | no | 0 | `test/arguments.ts` | +| `ChangedRunPreflight` | interface | 153–157 | yes | 0 | `test/changed-selection.ts` | +| `changedComparisonRefs` | const | 159–159 | no | 0 | `test/changed-selection.ts` | +| `selectChangedComparisonRef` | function | 162–164 | yes | 1 | `test/changed-selection.ts` | +| `decodeOutput` | function | 166–168 | no | 0 | `test/changed-selection.ts` | +| `changedComparisonRef` | function | 170–181 | no | 0 | `test/changed-selection.ts` | +| `gitRefExists` | function | 183–195 | no | 0 | `test/changed-selection.ts` | +| `gitOutput` | function | 197–213 | no | 0 | `test/changed-selection.ts` | +| `inspectChangedRun` | function | 216–254 | yes | 1 | `test/changed-selection.ts` | +| `changedSelectionFailure` | function | 257–270 | yes | 1 | `test/changed-selection.ts` | +| `isFullSuiteRun` | function | 277–290 | no | 0 | `test/arguments.ts` | +| `resolveBunTestArgs` | function | 303–323 | yes | 1 | `test/arguments.ts` | +| `SERIAL_FULL_SUITE_FILES` | const | 327–334 | yes | 1 | original | +| `SerialLaneBasename` | type | 336–338 | no | 0 | original | +| `SERIAL_LANE_TIMEOUT_MS` | const | 339–343 | no | 0 | original | +| `BunTestLane` | interface | 345–349 | yes | 0 | original | +| `withoutParallelOverride` | function | 351–353 | no | 0 | original | +| `canUseSerialLanes` | function | 355–358 | no | 0 | original | +| `resolveBunTestPlan` | function | 361–379 | yes | 1 | original | +| `waitWithTimeout` | function | 381–395 | no | 0 | original | +| `runTestLane` | function | 397–459 | no | 0 | original | +| `ensureGuiDependencies` | function | 472–500 | yes | 1 | original | + +Import declarations (binding redistribution; not new public symbols): + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `randomUUID` | import | 1–1 | no | 0 | original | +| `existsSync, mkdirSync, mkdtempSync, rmSync` | import | 2–2 | no | 0 | existsSync original; others environment | +| `homedir, tmpdir` | import | 3–3 | no | 0 | environment | +| `basename, join` | import | 4–4 | no | 0 | original; join also environment | +| `acquireTestRunLock, resolveWrappedTestRunLockPath, TEST_RUN_ID_ENV, TEST_RUN_LOCK_PATH_ENV, TEST_RUN_LOCK_TOKEN_ENV` | import | 5–11 | no | 0 | original, unchanged lock module | + +Top-level execution statement `if (import.meta.main)` at 502–572 remains original; its local bindings are not module-level state. No leaf adds an entrypoint. + +## Leaf partition + +| New leaf | Symbols | Expected lines including imports | Own imports | +|---|---|---:|---| +| `scripts/test/environment.ts` | `IsolatedTestEnvironment`, `createIsolatedTestEnvironment` | 62 | `import { mkdirSync, mkdtempSync, rmSync } from "node:fs";`; `import { homedir, tmpdir } from "node:os";`; `import { join } from "node:path";` | +| `scripts/test/arguments.ts` | `hasCliFlag`, `DEFAULT_TEST_PARALLELISM`, `BUN_TEST_OPTIONS_REQUIRING_VALUES`, `isFullSuiteRun`, `resolveBunTestArgs` | 136 | none | +| `scripts/test/changed-selection.ts` | `ChangedRunPreflight`, `changedComparisonRefs`, `selectChangedComparisonRef`, `decodeOutput`, `changedComparisonRef`, `gitRefExists`, `gitOutput`, `inspectChangedRun`, `changedSelectionFailure` | 119 | none; existing Bun/TextDecoder globals | + +Physical range accounting, including comments/blanks: environment 13–69 = 57; arguments 70–152 plus 272–324 = 136; changed-selection 153–271 = 119. Total moved = **312**. Residual expectation **268** = 572 − 312 + 8 net import/re-export/spacing budget. All leaves and residual ≤400; no #b required. These are budgets for a future move, to replace with actual `wc -l` measurements. + +Keep all of 325–572 in the original: serial list/type/timeout map, lane type/planning, timeout/signal supervision, GUI dependency check, and entrypoint. `decodeOutput` moves with Git-output decoding and gets an internal leaf export because the retained `ensureGuiDependencies` also calls it (:494). This avoids a changed-selection → facade back-import; no duplicate decoder is created. No runtime dependency on the lock module is added to any leaf. + +## Re-export block + +Add these exact compatibility re-exports; existing original exports `SERIAL_FULL_SUITE_FILES`, `BunTestLane`, `resolveBunTestPlan`, and `ensureGuiDependencies` remain their current declarations. + +```ts +export type { IsolatedTestEnvironment } from "./test/environment"; +export { createIsolatedTestEnvironment } from "./test/environment"; +export type { ChangedRunPreflight } from "./test/changed-selection"; +export { selectChangedComparisonRef, inspectChangedRun, changedSelectionFailure } from "./test/changed-selection"; +export { resolveBunTestArgs } from "./test/arguments"; +``` + +Explicit residual local imports (independent of re-exports): + +```ts +import { createIsolatedTestEnvironment } from "./test/environment"; +import { inspectChangedRun, changedSelectionFailure, decodeOutput } from "./test/changed-selection"; +import { hasCliFlag, DEFAULT_TEST_PARALLELISM, isFullSuiteRun, resolveBunTestArgs } from "./test/arguments"; +``` + +Original built-in imports become `randomUUID` from `node:crypto`, `existsSync` from `node:fs`, and `basename, join` from `node:path`; retain all five existing lock imports from `"./test-run-lock"`. No residual `IsolatedTestEnvironment` or `ChangedRunPreflight` type import is needed; the entry uses `ReturnType`. +Internal cross-leaf exports (`decodeOutput`, `hasCliFlag`, `DEFAULT_TEST_PARALLELISM`, `isFullSuiteRun`) are not re-exported from the original public path. + +## Module-level state and cycles + +- `BUN_TEST_OPTIONS_REQUIRING_VALUES`, origin `scripts/test.ts:81–151`: one Set owner, arguments leaf; never copied into lane planning. +- `DEFAULT_TEST_PARALLELISM` (:76): arguments leaf, imported by residual warning/planning code. +- `changedComparisonRefs` (:159): one array owner in changed-selection. +- `SERIAL_FULL_SUITE_FILES` (:327–334) and `SERIAL_LANE_TIMEOUT_MS` (:339–343): remain original, with the exact assignment spelling read by the layout mover. +- No module-level let, Map, WeakMap, timer, or lock instance. `lock` (:530) and lane timers (:383) remain invocation-scoped; no lock acquired when preload imports the facade. + +Intended graph is facade → independent leaves; leaves have no local module imports. Existing facade → test-run-lock stays unchanged. In particular the decoder dependency is residual → changed-selection, never reversed. Coupling is functional; signal handling and lock release remain temporally coupled within the existing runner invocation. Preserve `process.once/off`, timeout ordering, inherited lock token values, cleanup timing, and home capture before environment overwrite. No circular type import or lazy-import workaround. + +## Tests + +Exact importer search `rg -l 'from ".*/scripts/test"' src gui/src scripts tests` returns: + +- `tests/ci-workflows/test-runner.test.ts:14` — unchanged; all eight imported bindings still resolve. +- `tests/preload.ts:15` — unchanged; support module, not an extra test suite. Its real-home isolation happens at the same point. + +Exact source-oracle/path inventory: + +| Test / exact read or pin | Classification | Disposition | +|---|---|---| +| `tests/test-layout-tooling.test.ts:391` — `readFileSync(join(root, "scripts", "test.ts"), "utf8")` | Fixture's runner source, seeded at :328 and changed at :356; indirectly exercises real mover's source contract | unchanged; keep `SERIAL_FULL_SUITE_FILES` in original, no retarget | +| `tests/ci-workflows/test-runner.test.ts:381` — `repoPath("scripts", "test.ts")` passed to `Bun.spawnSync` at :379 | Executable-path pin, **not** a text read | unchanged | + +No existing test directly reads the real `scripts/test.ts` implementation as text. The “40 text oracles” in 001 is a broad basename heuristic (`test.ts` also matches unrelated test filenames), not forty source readers of this file. Searches used `rg -n 'scripts/test|"scripts", "test.ts"' tests` plus basename/reader intersection and inspection. No existing test requires retarget-to-leaf or add-leaf-to-scan-list. + +Non-test text consumer must be preserved: `scripts/test-layout/move.ts:20` names `scripts/test.ts`; :50–52 reads/parses its serial assignment; :145 rewrites it. Moving that assignment would require expanding the layer to the writer and its fixtures. This partition avoids that expansion. + +Future guards in existing `tests/ci-workflows/test-runner.test.ts`: add each of the three leaves to a new no-facade-import/ownership scan and verify old-path value exports identify the same function as direct leaves. Drive export/cycle guards red once and restore. Drive the existing argument-required-value behavior guard red by temporarily removing `"--timeout"` from the moved Set, and restore. Retain tests' subprocess test at :369 and full-suite plan assertions at :173–188. Never run the full suite locally just to demonstrate the plan. + +## Verification + +Future commands at L3's exact tip; none were executed by this docs-only task: + +```sh +bun run typecheck +bun test tests/ci-workflows/test-runner.test.ts tests/test-layout-tooling.test.ts tests/test-layout.test.ts +bun run privacy:scan +wc -l scripts/test/environment.ts scripts/test/arguments.ts scripts/test/changed-selection.ts scripts/test.ts +rg -l 'from ".*/scripts/test"' src gui/src scripts tests +rg -n 'SERIAL_FULL_SUITE_FILES = \[' scripts/test.ts +git diff --numstat origin/dev...HEAD -- scripts +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-test && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +This is 002's per-layer gate with ci-workflows + test-layout focused paths; core-Lab conditional gate is not triggered because no protected source family changes. Preserve its roots. Require the same two public importer paths and a zero-return-edge import graph including type/re-export edges. New leaves have no local imports, so the cycle check must confirm that negative fact rather than only typechecking. + +Record remote exact SHA, full log, Bun exit status and test counts; the 002 sample's tail alone is insufficient, so use pipefail or capture status before tail. Additional `scripts/AGENTS.md` prepush gate runs **on lidge**, since `package.json:55` invokes the full suite; no local full-suite command. Explicit tooling/security review and Windows/macOS/Linux CI are required for review-ready. Tests and dependency installation are not authorized in this drafting turn. + +## Accept criteria + +1. All 26 named declarations and five original import declarations have one explicit disposition; moved spans total 312 original lines. +2. Three leaves and the residual each measure ≤400 (residual 268 expected); exported types/functions and serial table remain available at the original path. +3. `SERIAL_FULL_SUITE_FILES` remains a literal assignment in `scripts/test.ts`; layout mover and fixture read at test-layout-tooling.test.ts:391 need no retargeting. +4. No leaf imports the facade or acquires locks, starts subprocesses, changes environment, or installs dependencies merely on import. +5. Focused checks, negative guard receipts, typecheck, privacy, remote full suite/prepush and platform CI pass for the exact L3 head. CLI flags, default parallelism, selection rejection, signal exits, timeout and lock-cleanup behavior are unchanged. +6. Parent resolves the literal changed-line budget exception before execution; PR base is the latest L2 branch with no missing parent commits. No merge or release. + +## PR + +Title: `refactor(scripts): isolate test environment and selection (split S21 L3/4)` +Branch: `codex/split-test`. +Base: `dev`. +Closes: none. + +Fill Summary, Verification, Checklist in `.github/PULL_REQUEST_TEMPLATE.md`. Review only this layer's diff. Stack navigation (only L2 depends on L1; merges require separate authorization): + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S21-L4 | `codex/split-disposable-host-codex-service-composed-acceptance` | `dev` | Fixture owner; sentinel order | +| 3 | #TBD-S21-L3 | `codex/split-test` | `dev` | Environment and selection leaves | +| 2 | #TBD-S21-L2 | `codex/split-release-notes-b` | `codex/split-release-notes-a` | Tags, attribution, PR rendering | +| 1 | #TBD-S21-L1 | `codex/split-release-notes-a` | `dev` | Carry, commit fallback, polish | + +Base: dev — no dependency on the layers below; no cascade obligation. No Git mutations in this delegation. diff --git a/devlog/_plan/260905_now_split_train/750_disposable_host_codex_service_composed_acceptance.md b/devlog/_plan/260905_now_split_train/750_disposable_host_codex_service_composed_acceptance.md new file mode 100644 index 0000000000..9dd6a2db7e --- /dev/null +++ b/devlog/_plan/260905_now_split_train/750_disposable_host_codex_service_composed_acceptance.md @@ -0,0 +1,151 @@ +# S21 L4/4 — Disposable-host fixture owner + +> Historical record imported from `9c0952e482b1586c0dc62d5c536698fe5578cf28`. Deferred before implementation; archival proposal only. +> Operational instructions and verification recipes below are superseded by800_closeout.md,801_closeout_regression_matrix.md,810_first_rebase_regression.md and820_second_regression_delivery.md. Peer coordination is closed. Historical checks certify only their recorded heads; this document authorizes no new debt implementation or execution. + +## Loop spec + +Archetype: **pure-move**. Goal: separate the disposable-host fixture/preflight owner from row scenarios in the 402-line executable; leave both files ≤400. Non-goals: no systemd operation, no running the acceptance script, no sentinel creation, no changing credentials/account paths, no new scenario or changed teardown/provenance semantics, no moving the executable path. + +Mode: bounded docs-only planning; C3 module analysis with C4 review care for the eventual globally addressed service/deletion harness. cxc-dev §1/§5 and cxc-dev-architecture apply; parent owns orchestration/loop/goal. Verifier = `002_layer_map.md` → **Per-layer gate**, below. Drafting stop: this assigned document complete. Implementation stop: its own tip has gates and exact-head PR evidence; no merge. Escalate any new dependency, contract/body change, unsafe host requirement, cycle, leaf >400, or file scope expansion. + +Structural decision: split at the existing fixture/scenario seam (lane 016; source `scripts/disposable-host/codex-service-composed-acceptance.ts:129`, :328). Reject deleting comments to get under 400, and reject exporting state from the executable (importing it runs `main().catch`). One sibling leaf follows existing `scripts/test-run-lock.ts` and `scripts/*-child.ts` naming. Search `rg --files scripts` and exact declarations found no existing disposable fixture owner; the workstation-safe test fixture is deliberately a different environment and must not be imported into this production script. + +Current map: no source importer or local dependency; built-in fs/os/path/crypto and bun:sqlite; unguarded `main().catch` owns execution. Intended map: same executable → sibling fixture leaf → those same built-ins, with no return edge. Blast radius: this harness and a safe test-source guard, not the service runtime. + +Budget escalation: moving the 301 original fixture/preflight lines gives ≥602 literal changed lines before imports. Parent must approve a mechanical-move budget exception or adjust the assigned topology before implementation; this document does not create additional layers. + +## Symbol inventory + +Basis: docs `4cc219549`; source `origin/dev 1362b1a38`. Fresh `git diff origin/dev -- scripts/disposable-host/codex-service-composed-acceptance.ts` was empty. All inclusive ranges below are origin/dev source lines. Use `sg run --lang ts --kind 'function_declaration,lexical_declaration,type_alias_declaration,interface_declaration,class_declaration' --json=compact scripts/disposable-host/codex-service-composed-acceptance.ts` and column-zero declaration verification with `rg`. + +`rg -l 'codex-service-composed-acceptance' src gui/src scripts tests` returns no matches (no importing external file); every symbol therefore has zero external consumer files. Internal calls are not counted as import consumers. There are 22 named non-import declarations. + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `SENTINEL` | const | 26–26 | no | 0 | `codex-service-composed-fixture.ts` | +| `SENTINEL_BYTES` | const | 27–27 | no | 0 | `codex-service-composed-fixture.ts` | +| `UNIT` | const | 28–28 | no | 0 | `codex-service-composed-fixture.ts` | +| `repoRoot` | const | 29–29 | no | 0 | `codex-service-composed-fixture.ts` | +| `cliPath` | const | 30–30 | no | 0 | `codex-service-composed-fixture.ts` | +| `accountHome` | const | 31–31 | no | 0 | `codex-service-composed-fixture.ts` | +| `accountUnit` | const | 32–32 | no | 0 | `codex-service-composed-fixture.ts` | +| `eventLedger` | const | 33–33 | no | 0 | `codex-service-composed-fixture.ts` | +| `RowId` | type | 35–35 | no | 0 | `codex-service-composed-fixture.ts` | +| `ChildResult` | type | 36–36 | no | 0 | `codex-service-composed-fixture.ts` | +| `Transition` | type | 37–37 | no | 0 | `codex-service-composed-fixture.ts` | +| `fail` | function | 39–41 | no | 0 | `codex-service-composed-fixture.ts` | +| `assertDisposableSentinel` | function | 43–52 | no | 0 | `codex-service-composed-fixture.ts` | +| `spawnResult` | function | 54–67 | no | 0 | `codex-service-composed-fixture.ts` | +| `requireCommand` | function | 69–76 | no | 0 | `codex-service-composed-fixture.ts` | +| `emptyRegistrationGate` | function | 78–101 | no | 0 | `codex-service-composed-fixture.ts` | +| `byteManifest` | function | 103–117 | no | 0 | `codex-service-composed-fixture.ts` | +| `sameManifest` | function | 119–121 | no | 0 | `codex-service-composed-fixture.ts` | +| `coordinatorPath` | function | 123–127 | no | 0 | `codex-service-composed-fixture.ts` | +| `Fixture` | class | 129–326 | no | 0 | `codex-service-composed-fixture.ts` | +| `runRow` | function | 328–383 | no | 0 | original | +| `main` | function | 385–393 | no | 0 | original | + +All import declarations: + +| symbol | kind | lines start–end | exported? | consumers (count from rg) | target leaf | +|---|---|---|---|---:|---| +| `createHash` | import | 8–8 | no | 0 | fixture | +| `existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync` | import | 9–21 | no | 0 | fixture; existsSync/readFileSync also original | +| `homedir, tmpdir` | import | 22–22 | no | 0 | fixture | +| `join, relative, resolve` | import | 23–23 | no | 0 | fixture; join also original | +| `Database` | import | 24–24 | no | 0 | fixture | + +Top-level statement `main().catch(...)` at 395–402 stays original exactly; it is not guarded by `import.meta.main`, and adding that guard would be an out-of-scope behavior change. All Fixture members remain inside their class; none become singleton declarations. + +## Leaf partition + +| New leaf | Symbols | Expected lines including imports | Own imports | +|---|---|---:|---| +| `scripts/disposable-host/codex-service-composed-fixture.ts` | `SENTINEL`, `SENTINEL_BYTES`, `UNIT`, `repoRoot`, `cliPath`, `accountHome`, `accountUnit`, `eventLedger`, `RowId`, `ChildResult`, `Transition`, `fail`, `assertDisposableSentinel`, `spawnResult`, `requireCommand`, `emptyRegistrationGate`, `byteManifest`, `sameManifest`, `coordinatorPath`, `Fixture` | 320 | `createHash` from `node:crypto`; all eleven existing fs imports from `node:fs`; `homedir, tmpdir` from `node:os`; `join, relative, resolve` from `node:path`; `Database` from `bun:sqlite` | + +Move **26–326 = 301** original lines, retaining their order and comments. Leaf imports preserve the original 17-line import block (:8–24), plus two spacing lines: 320 expected. Keeping a **sibling** (not a deeper subfolder) preserves `resolve(import.meta.dir, "../..")` at origin :29 exactly, and therefore the CLI path. + +Residual expectation: **88** = 402 − 301 − 17 old import-block lines + 4 replacement import lines. Retain header :1–7, row runner :328–383, main :385–393, catch :395–402 and surrounding existing spacing. All files ≤400; no #b required. Actual physical counts must be recorded during execution. `Fixture` stays a 198-line cohesive lifetime owner; don't refactor methods or delete the unused `byteManifest` while moving. + +Only the existing symbols used across the new boundary gain leaf exports: `Fixture`, `fail`, `assertDisposableSentinel`, `requireCommand`, `emptyRegistrationGate`, `eventLedger`, and the three types `RowId`, `ChildResult`, `Transition`. All other leaf helpers/constants remain private. This is the existing harness implementation boundary, not a user-facing API. + +## Re-export block + +**No compatibility re-export lines:** the original file currently exports no value/type/default symbols; preserve that empty export set. Do not re-export the fixture's internal API from the executable and never import the executable to get helpers. + +The entire replacement residual import block is: + +```ts +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { Fixture, fail, assertDisposableSentinel, requireCommand, emptyRegistrationGate, eventLedger } from "./codex-service-composed-fixture"; +import type { RowId, ChildResult, Transition } from "./codex-service-composed-fixture"; +``` + +No local binding is expected from a re-export. Both `runRow` and `main` remain private local functions. + +## Module-level state and cycles + +- `eventLedger: string[] = []` at origin :33 is mutable module-level state, even though lane 016's narrower let/Map/Set scan reported none. Its **one owner** is the fixture leaf. All writes remain there (:50, :71, :85, :92); original main only reads `.join` (:392). Do not copy, reset, freeze, or reinitialize the array. +- `SENTINEL` (:26), `SENTINEL_BYTES` (:27), `UNIT` (:28), `repoRoot` (:29), `cliPath` (:30), `accountHome` (:31), `accountUnit` (:32) move together to that same owner and keep evaluation order. No new top-level I/O beyond the existing path/home computations. +- No top-level let, Map, Set, WeakMap or lock. `Fixture.lock` and `lockAllowlist` (:151–152, :187–189) remain instance-owned paths, not newly acquired locks. `baselineOutside`, seed and temp root stay constructor-scoped/instance-scoped. No Fixture instance is constructed at import time. + +Single directed edge original → fixture, only built-ins below; no type-only back edge. Temporal coupling remains explicit: main platform check → sentinel verification → require systemctl → each row's empty-registration gate → constructor/install → teardown → final empty gate. The ledger initialization stays before all of them. A leaf-to-entry import would eagerly rerun the harness and create a cycle: prohibit it with the source guard below. The ledger's sole-writer/read-only-consumer relationship is retained; no new shared writer or defensive logic is introduced. + +## Tests + +Exact `rg -l` public-path importer list in `tests`: **empty**. Exact basename/path and reader-intersection searches found **no existing source-text oracle** for this script. There are consequently no existing retarget-to-leaf or add-leaf-to-scan-list dispositions to invent. + +Related but not importing this script: `tests/codex-integration/codex-composed-acceptance.test.ts` covers workstation-safe composed rows through the real CLI/server (entry paths :49–50); keep all existing assertions unchanged. Its fixture is not reusable for these globally addressed systemd rows. + +Future test change: in that existing test file, add a separate **source-only** guard describing the disposable fixture boundary. Read exact paths `scripts/disposable-host/codex-service-composed-acceptance.ts` and `scripts/disposable-host/codex-service-composed-fixture.ts` via the existing repository-root helper; no new test filename or layout-map edits. Add-leaf-to-scan-list: the new guard includes the fixture path, checks that the sentinel/empty-registration implementation resides there, and the scenario entry imports only the declared boundary. Check no main call/spawn/Fixture construction at leaf module evaluation, one eventLedger declaration, no leaf import of the executable, and sentinel invocation before the first command in main. Use AST/syntax-aware checks where comments also contain those words. + +Drive the new guard red once by removing the sentinel invocation from the entry source in the isolated implementation worktree, then restore; separately introduce a forbidden leaf-to-entry import and restore. The red test must only read text, never import/execute the destructive script. A safe export inventory assertion may import the inert fixture leaf but must not instantiate Fixture. Existing guards are not weakened or retargeted. + +The real six-row census is **not** a local/ordinary CI test. It requires a separately authorized, root-sentinel-provisioned disposable Linux/systemd image. Do not create the sentinel or run service operations on this workstation or on the generic lidge checkout. Source-only verification must not be reported as real six-row acceptance. + +## Verification + +Future L4 commands instantiate 002's per-layer gate (no test or script executed in this drafting turn): + +```sh +bun run typecheck +bun test tests/codex-integration/codex-composed-acceptance.test.ts +bun run privacy:scan +wc -l scripts/disposable-host/codex-service-composed-fixture.ts scripts/disposable-host/codex-service-composed-acceptance.ts +rg -l 'from ".*codex-service-composed-acceptance"' src gui/src scripts tests +git diff --numstat origin/dev...HEAD -- scripts/disposable-host +ssh lidge 'cd ~/ocx-ci/opencodex && git fetch origin codex/split-disposable-host-codex-service-composed-acceptance && git checkout -q FETCH_HEAD && bun install --frozen-lockfile >/dev/null && bun run test 2>&1 | tail -15' +``` + +The importer check must have no matches (rg exit 1 is expected, not a failed contract). The new entry → fixture edge is measured separately; no leaf → entry edge and no type cycle. Full remote run must retain actual exit status with pipefail or explicit status capture plus complete logs; tail alone cannot prove green. Confirm the remote SHA equals L4. `scripts/AGENTS.md` prepush is also remote-only because it includes the full suite; obtain explicit security review for service/deletion tooling. + +No core-Lab conditional check is activated: none of `src/server`, `src/router`, `src/lib` is touched. Preserve protected roots. Platform CI checks static/import portability; only a separately authorized disposable Linux/systemd run can prove six-row runtime results. Missing disposable-host evidence is reported explicitly, never substituted with a workstation launch. + +## Accept criteria + +1. All 22 named declarations and five import declarations have an explicit owner; 301 original lines move without body/signature changes. +2. One new leaf measures ≤400 (320 expected), original ≤400 (88 expected), public export set remains empty. +3. Leaf is a sibling, preserving repoRoot/cliPath semantics; no main invocation, service query or temporary fixture creation occurs on leaf import. +4. One eventLedger owner remains; sentinel and before/after empty-registration checks, transaction counts, account paths, and teardown allowlist are unchanged. +5. Source guards have recorded red/restore evidence; focused safe tests, typecheck, privacy, remote full suite/prepush and exact-head CI pass. Real disposable-host results are reported separately if authorized; no false acceptance claim. +6. Parent resolves the diff-size exception and obtains tooling/security review before execution/review-ready. Base contains L3; no merge, release, account mutation or extra branch occurs in this drafting task. + +## PR + +Title: `refactor(scripts): separate disposable service fixture ownership (split S21 L4/4)` +Branch: `codex/split-disposable-host-codex-service-composed-acceptance`. +Base: `dev`. +Closes: none. + +Fill Summary, Verification, Checklist in `.github/PULL_REQUEST_TEMPLATE.md`. Review only this layer's diff. Stack navigation (only L2 depends on L1; merges require separate authorization): + +| # | PR | Layer / branch | Base | Review focus | +|---|---|---|---|---| +| 4 | #TBD-S21-L4 | `codex/split-disposable-host-codex-service-composed-acceptance` | `dev` | Fixture owner; sentinel order | +| 3 | #TBD-S21-L3 | `codex/split-test` | `dev` | Environment and selection leaves | +| 2 | #TBD-S21-L2 | `codex/split-release-notes-b` | `codex/split-release-notes-a` | Tags, attribution, PR rendering | +| 1 | #TBD-S21-L1 | `codex/split-release-notes-a` | `dev` | Carry, commit fallback, polish | + +Base: dev — no dependency on the layers below; no cascade obligation. This final layer does not authorize landing any part of the stack. diff --git a/devlog/_plan/260905_now_split_train/800_closeout.md b/devlog/_plan/260905_now_split_train/800_closeout.md new file mode 100644 index 0000000000..3688d7a633 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/800_closeout.md @@ -0,0 +1,102 @@ +# 800 — Existing split-train cutoff closeout + +> Latest user sequencing:830 now governs delivery. Publish and admin-merge +> the verified aggregate into dev FIRST, then run two full post-merge +> regression cycles840/850. Earlier prepublication ordering below is historical. + +## Loop spec + +- Archetype: satisfy-spec integration closeout; C3 refactor integration with explicit security review for affected trust boundaries. +- Trigger: the user stopped further debt-layer implementation and requested current-dev rebases, final-head-only CI, main-to-merged-dev regression checks, and delivery of recorded devlog. +- Goal: deliver the 14 already-implemented split PR contents and reviewed records without reverting newer dev behavior. +- Non-goals: implementing WP480 or other deferred layers; unrelated open PRs; main/preview promotion; releases, deployment, live-service changes, local suites, or peer-task communication. +- Verifier: per-stack range-diff/body/export/state/cycle review; pinned main and final candidate remote checks; actual final PR/merged-dev CI and ancestry. Detailed matrix:801. +- Stop: verified requested cutoff delivered and old PR dispositions accurately recorded; never claim all68 original rows were resolved. +- Memory: this800/801 pair, the new bound closeout goalplan and its ledger, and closeout-inventory.json in session evidence. Original goalplan remains unchanged except a supersession annotation. +- Expected outcomes: DONE for this cutoff only; unresolved verification or required user choices remain explicitly incomplete. No cancelled or missing check becomes PASS. +- Escalation: ambiguous consolidation authority, dirty ownership, lost source changes, semantic conflicts beyond necessary regression repair, or missing verification. Main reclaims a packet after two distinct failed workers; changing a worker scope requires a plan amendment. +- Resources: existing repository/GitHub/SSH credentials only; scoped local candidate refs/worktrees and isolated remote verification. User authorized unlimited time/tokens and gpt-6-astra high internal subagents. No numerical budget is invented. + +## Pinned input and cutoff + +Initial dev: `ba9a45570986aa7828508285e9a469549344dd70`. +Execution rebase pin: `bf58ef1824e7b827b2a6bc1a5effb5d36ce80180`. The +intervening Reserve and release-version changes remain part of the baseline. +In particular, preserve the new `planVisionSidecar` admission/policy options +and conditional Reserve compatibility in its moved planning leaf. The new +release-version behavior is not changed by this train. +Initial main: `48f8186647d9ffb108d226dcfa91a64225aae2a7`. +Preserve the WP480 docs-only head `ddb7013ac0c58e513c651d54a96e07f52ac0efbe` and central records head `9c0952e482b1586c0dc62d5c536698fe5578cf28`. +The original 68-file plan has17done/1in-progress/61pending work-phases; these counts are not file-resolution counts and are not rewritten as completed. +An existing native host goal cannot be replaced by the exposed create/update tools. This separate goalplan records the user-directed scope replacement; it is not a claim that a new native host goal was created or the old objective achieved. + +## Publication decision — confirmed by user + +The user explicitly confirmed preserving original PRs/branches, rebasing new local staging refs, and delivering the reviewed contents through one standalone aggregate PR. After verified landing, close the originals as superseded, not individually merged. Do not ask for that choice again. +The user additionally requires at least two complete main-to-dev regression PABCD cycles. Cycle1 follows810: local rebases, consolidation and first baseline/candidate regression proof, with no publication. Cycle2 follows820: an independent pinned-main export-contract guard, second regression pass and final-head-only publication/admin delivery. Two CHECK invocations or a docs-only cycle do not meet this requirement. +Local staging rebases are in progress; no external publication has occurred. +Original unfinished debt remains deferred, not completed. + +## Exact inventory + +| PR | Original head | Replay boundary | Staging ref | +|---|---|---|---| +| #3557 | `97df51515c22ccd610665989aa940f15bc3bca24` | `4457429662bc98279d8b321e6f75d752f77e78e8` | `codex/closeout-pr-3557` | +| #3559 | `5b253af7f3392c4af3c2177d6b66a06a8d674044` | `4dde2db97aaa7c16566ad192bf55fcbb609ab13a` | `codex/closeout-pr-3559` | +| #3566 | `58dba9e0b2209bd9f76c4d5fb4943df0d6ab710b` | `4dde2db97aaa7c16566ad192bf55fcbb609ab13a` | `codex/closeout-pr-3566` | +| #3567 | `c1d436738c5fb012b666cc15e87e777a66e7648d` | `4dde2db97aaa7c16566ad192bf55fcbb609ab13a` | `codex/closeout-pr-3567` | +| #3570 | `fdddbd3e1516997111b201a7c191fc08a6f8d4dd` | `97df51515c22ccd610665989aa940f15bc3bca24` | `codex/closeout-pr-3570` | +| #3574 | `8a404cb889abda5ab6d9cd384833e5d3c34dd873` | `24cc558d53262abde171c8228dc41d8613fa16c7` | `codex/closeout-pr-3574` | +| #3577 | `51f5a82d7c6ff3cc3a2df1a08716fa5eff1e67b1` | `24cc558d53262abde171c8228dc41d8613fa16c7` | `codex/closeout-pr-3577` | +| #3580 | `3793fb0326b8aea541918905461a8a4a0e5fcd79` | `a594a7f216f633afcedf0b44225f604b2f5f3f37` | `codex/closeout-pr-3580` | +| #3583 | `c0fab2d74b977092884ea817c274ef2f3f4021a7` | `a594a7f216f633afcedf0b44225f604b2f5f3f37` | `codex/closeout-pr-3583` | +| #3585 | `1cab08d405fc59bc5b386aa21a073f4301246ac2` | `760eddee1b0f60e3d9bf442bbc947f18c379ca5d` | `codex/closeout-pr-3585` | +| #3590 | `82e069c9fe59b9660bee7964cd58c0141687267b` | `3c920af5f7b18ecd98f87a589d21d299f5cbe172` | `codex/closeout-pr-3590` | +| #3594 | `0c914bf265ce38c57498c21ccf81f0202b9c133c` | `3c920af5f7b18ecd98f87a589d21d299f5cbe172` | `codex/closeout-pr-3594` | +| #3599 | `5c1a398da78975312c183c1c2b6e0ff8241ac02c` | `593978db019e03bcb03a862ee4e44f6356930c6a` | `codex/closeout-pr-3599` | +| #3611 | `bbf8d3cd25ccf70eb595bc7982f63528d060c1bd` | `be81013fab6d83ff630ca5f38e7881678a303871` | `codex/closeout-pr-3611` | + +All local original heads matched the GitHub inventory. The13 existing associated temporary worktrees were clean; #3611 has no checked-out worktree. Recheck ownership before any write. + +## Overall procedure and cycle boundary + +1. Preserve original refs with immutable checkpoint refs and a manifest. Keep original branches unchanged; use staging refs rather than rewriting originals checked out elsewhere. Stay in the existing a2c0 worktree for aggregate source, FSM and receipts. +2. For each root, create its staging ref at the recorded original head in a task-owned checkout. Rebase locally with `git -c core.hooksPath=/dev/null -c rebase.updateRefs=false rebase --onto `. No push. +3. Rebase #3557 first. Rebase #3570 onto the new #3557 staging tip, replaying only above original97df51515; do not replay the parent twice. All inventoried replay ranges contain no merge commits. +4. Review range-diff and source-level delta for each candidate. A dropped commit requires demonstrated prior inclusion; do not silently discard it. Preserve author metadata and any coauthor trailers. +5. Resolve conflicts by preserving current-dev behavior and the intended extraction, never blanket ours/theirs. Shared000/003 records use one reviewed final version; retain all layer-specific records and history. +6. Merge the staged results into `codex/closeout-split-train` in a2c0, parent before child. This is the actual B source delta. Record old PR/head → staged head → included aggregate ancestry/content. +7. Include reviewed public-safe accumulated devlog, including WP450 delivery/post-merge proof and the deferred WP480 plan. Do not copy .codexclaw, .tmp, secrets, or undisclosed security working material into tracked docs. +8. Freeze candidate source and documentation before publication. Transfer unpublished commits to isolated remote checkouts with a Git bundle and exact SHA verification; do not push intermediate heads merely to test them. +9. Close cycle1 only after real first-pass regression evidence. In cycle2, add the independently sourced guard specified in820, repair only demonstrated regressions, and run second-pass/final full gates with independent review. Re-check current dev before final publication and revalidate changed integration input. +10. Publish only the stabilized aggregate head, create the templated PR and observe its actual CI. Necessary corrective commits get fresh final-head checks; no workflow disabling, skip-ci camouflage, blind retries, or cancelled-check reuse. +11. After actual success and valid review closure, use admin merge with explicit expected-head matching. Verify actual merge tree equals the tested integration tree and fetch dev to prove ancestry. Observe normal final merged-dev CI; the final-head policy does not suppress this automatic run. +12. Only after delivery proof, reconcile original PRs using the confirmed disposition and record the cutoff result. Keep residual/unimplemented debt visible. + +## Mandatory conflict preservation + +- #3577: move current `syncRawBodyImageDescriptions` behavior into the rewrite leaf, including the already landed file-ID/empty-URL caption alignment change. +- #3580: keep current `outputToToolResultContent` reference handling in parser-content: URL precedence, file-ID fallback, malformed omission and detail normalization. +- #3583: keep current file-backed-image rejection in retained `imageBlockToInputImage`, using the one relocated AnthropicRequestError class. +- #3557/#3570: retain newer OcxProviderConfig fields and the type-contract cycle break; rebase parent before child. +- #3594: retain cooldown fields, bounds/defaults, Retry-After/reset precedence and cancellation/deletion behavior while extracting identifier helpers. +- All other stacks: preserve public exports, state/cache ownership, original assertions and each original layer's thesis. Tests do not replace static extraction review. + +## Scope of source writes + +- #3557: `src/adapters/cursor/desktop-executor-contract.ts`, `src/adapters/cursor/native-exec-desktop.ts`, `src/types/provider.ts`, `tests/providers/cursor/cursor-desktop-exec.test.ts`. +- #3559: `src/lib/redact-folding.ts`, `src/lib/redact.ts`, `tests/lib/redact.test.ts`. +- #3566: `src/providers/openai-tiers-destination.ts`, `src/providers/openai-tiers.ts`, `tests/adapters/openai/openai-provider-option.test.ts`. +- #3567: `src/adapters/anthropic-image-codec.ts`, `src/adapters/anthropic-image-normalize.ts`, `tests/adapters/anthropic/anthropic-image-normalize.test.ts`. +- #3570: `src/adapters/cursor/tool-definitions.ts`, `src/adapters/cursor/tool-guidance.ts`, `src/adapters/cursor/tool-naming.ts`, `src/adapters/cursor/tool-schemas.ts`, `tests/providers/cursor/cursor-tool-definitions.test.ts`. +- #3574: `src/adapters/xai-schema-analysis.ts`, `src/adapters/xai-tool-schema.ts`, `tests/providers/xai/xai-tool-schema.test.ts`. +- #3577: `src/vision/image-rewrite.ts`, `src/vision/index.ts`, `src/vision/plan.ts`, `tests/vision/vision-cache.test.ts`. +- #3580: `src/responses/parser-content.ts`, `src/responses/parser-text-format.ts`, `src/responses/parser-tools.ts`, `src/responses/parser.ts`, `tests/responses/responses-parser.test.ts`. +- #3583: `src/claude/inbound-content-options.ts`, `src/claude/inbound-model-options.ts`, `src/claude/inbound-records.ts`, `src/claude/inbound.ts`, `tests/claude-integration/claude-inbound.test.ts`. +- #3585: `src/server/system-env-shell.ts`, `src/server/system-env.ts`, `tests/server/system-env.test.ts`. +- #3590: `src/codex/prompt-layers.ts`, `src/codex/prompt-layers/encoding.ts`, `src/codex/prompt-layers/paths.ts`, `src/codex/prompt-layers/revision.ts`, `src/codex/prompt-layers/toml-edit.ts`, `src/codex/prompt-layers/toml-read.ts`, `tests/codex-integration/codex-prompt-layers.test.ts`. +- #3594: `src/combos/identifiers.ts`, `src/combos/types.ts`, `tests/codex-integration/combos.test.ts`. +- #3599: `src/codex/log-guard/inspect-schema.ts`, `src/codex/log-guard/inspect.ts`, `tests/codex-integration/codex-log-guard-inspect.test.ts`. +- #3611: `src/clients/config-export.ts`, `src/clients/config-export/constants.ts`, `src/clients/config-export/contracts.ts`, `src/clients/config-export/dsh.ts`, `src/clients/config-export/mcode.ts`, `src/clients/config-export/model-metadata.ts`, `src/clients/config-export/omp.ts`, `src/clients/config-export/zcode.ts`, `tests/config/client-config-export.test.ts`. + +Public regression evidence must name tested SHAs and distinguish intended main-to-dev changes from unintended regressions. A passing finite suite is not proof that every possible behavior is unchanged. diff --git a/devlog/_plan/260905_now_split_train/801_closeout_regression_matrix.md b/devlog/_plan/260905_now_split_train/801_closeout_regression_matrix.md new file mode 100644 index 0000000000..a0f7000386 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/801_closeout_regression_matrix.md @@ -0,0 +1,34 @@ +# 801 — Cutoff regression evidence matrix + +This is the shared verification matrix for the distinct810and820 work-phases. +Each has a full P/A/B/C/D history and its own fresh evidence. All rows are +currently pending; no prior split CI certifies a rebased candidate. + +| Comparison/surface | Required proof | Failure disposition | +|---|---|---| +| Each old stack → rebased stack | range-diff, moved declaration/body/export/state identity, original assertions retained, dependency direction and newly landed behavior carried | Resolve the actual conflicting logic; do not choose a side wholesale | +| Pinned dev → aggregate | Every one of14 manifest entries represented, no duplicate Cursor parent, no unrelated source reversal, reviewed docs precedence | Missing or duplicated delta blocks publication | +| Pinned main → final candidate | Review change categories including mechanical test relocations; map common contract cases across paths; distinguish intended feature changes from regressions | Add a concrete regression case or document intentional contract difference; never infer from pass counts alone | +| Runtime protocols | Responses/chat/Claude translation, image reference and tool-output preservation, streaming/terminal behavior, error contracts | Preserve landed fixtures and fix only observed regressions | +| Config/CLI/native clients | Config export bytes/order/auth representation, prompt encoding/TOML/EOL, shell ownership and status/install contracts | Same contracts at pinned baselines or explicit intended migration | +| Catalog/routing/state | Destination trust checks, combo cooldown/default/cancellation behavior, provider config fields and cache/singleton ownership | No permission/selection/state-loss regression | +| Privacy/optional boundaries | Project privacy scan, preserved redaction behavior, no new core→Lab reachability, explicit security review | Findings go to ignored scratch; no public working vulnerability notes | +| Dashboard/package | Pinned build, existing component tests/lint, isolated served smoke where the main→dev UI delta requires runtime proof | No global service changes or real user account mutation | +| Final exact head | Remote build/typecheck/full-suite and relevant focused gates, negative controls for changed guards, clean source-bound receipt; actual hosted CI | Failure/cancellation is not PASS; no intermediate publication to obtain evidence | +| Actual merged dev | Expected-head admin merge, tested-tree equality, fetched ancestry, normal post-merge CI and final review disposition | Any new discrepancy remains work; do not announce completed regression closure | +| Two-cycle requirement | Cycle1 baseline/current-dev/candidate comparison; cycle2 independent main-export guard plus adversarial second comparison and actual merged-dev proof | Repeating one CHECK or replaying old logs does not count | + +The main baseline uses its own package/lockfile and test runner, not a silently +substituted dev harness. At the pinned main, Bun is1.4.0, `bunfig.toml` roots +discovery in tests and preloads tests/preload.ts, and scripts/test.ts creates +isolated homes and honors the test-run lock. Read the matching guards before +execution, retain complete outputs and verify cleanup. No local test runs. + +The interval already contains substantial work beyond these14 splits. Full +baseline/final checks and this matrix are separate from split-focused checks. +No claim of overall regression safety is earned solely from clean rebases or +fourteen old CI statuses. Baseline failures need evidence-backed classification. + +Prepare documentation before final publication. Record source and verification +identities explicitly; final hosted/merge evidence may be attached to the PR +and durable ledger without changing tested source merely to embed its own SHA. diff --git a/devlog/_plan/260905_now_split_train/810_first_rebase_regression.md b/devlog/_plan/260905_now_split_train/810_first_rebase_regression.md new file mode 100644 index 0000000000..600120c394 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/810_first_rebase_regression.md @@ -0,0 +1,95 @@ +# 810 — Local rebase/consolidation and first main-to-dev regression cycle + +## Loop spec + +- Archetype/trigger: satisfy-spec first regression cycle under the user's confirmed cutoff/aggregation request. +- Goal: locally rebase all14 inventoried source candidates and independently compare the combined result with pinned dev and main before any publication. +- Non-goals: new debt implementation, unrelated PR changes, intermediate pushes/CI, release/deploy, live-user state or local suites. +- Verifier: recorded range-diffs and extraction/body/type/state/cycle checks, main/current-dev baselines, first801matrix pass, candidate remote full gates and independent review. +- Stop: first complete P/A/B/C/D regression cycle closes with a source-bound candidate receipt and an honest regression report; then enter820. No delivery is claimed here. +- Memory:800inventory/procedure,801matrix, this810, private per-head logs and closeout ledger. Preserve original68goal and all original refs. +- Outcomes: DONE means locally rebased/validated candidate only. Failed or unexplained verification remains incomplete. This is not the final merge cycle. +- Escalation/resources: preserve dirty owners and source identity; semantic conflicts beyond required preservation return to the plan. Existing credentials, task-owned checkouts, remote tests only; unlimited user budget. Main reclaims after two distinct failed workers and records new delegation scope before it is used. + +## Exact B work + +Execute800steps1–8 with staging refs and the recorded replay boundaries. Each +worker owns only its assigned staging checkout/branch and original layer's +source/test paths; main owns aggregate a2c0, docs, Git publication and receipts. +No worker may push, change an original ref, weaken tests, run a local suite, +change repository workflows, or contact another task. + +Only #3570 depends on another open split head: rebase #3557 first, then use +that staged tip as #3570's new base and original97df51515 as its replay +boundary. For other roots use the pinned dev from800. Review every dropped +or altered commit with range-diff and file/content evidence. + +The high-risk vision/parser/Claude preservation cases in800 must survive +in their correct moved or retained owner. Rebase is not permission to restore +an older implementation over a newer landed fix. Existing source/test +changes and any necessary preservation repairs become part of the manifest. + +Merge staged candidates into the main aggregate during B, preserving their +ancestry/author metadata. Include reviewed central devlog and the frozen +WP480 planning record, explicitly deferred. No new WP480 source is built. + +## First regression pass + +1. Freeze main48f818 and the selected dev SHA/tree. Classify their full diff, + including mechanical test relocation versus intentional behavior changes. +2. Run pinned main and current-dev baselines in separate remote temporary + clones. Inspect their own package/bunfig/test-runner guards first; use + their own frozen dependencies and repository runtime. Main's scripts/test + already provides isolated homes and a test-run lock. Never bypass them. +3. Run each baseline's typecheck, build preparation, privacy and full test + command. Preserve exit codes, complete logs and source identities. A + baseline failure requires classification; it is not silently waived. +4. Validate the000_3main export snapshot against the pinned source/runtime + independently. Its14modules/244value-export names were statically captured + from main, not generated from the candidate. Any temporary probe is kept + outside committed product source and removed from the disposable checkout + before its final clean-state claim. +5. For each stack, review old-base→old-head against dev→staged-result and + execute the relevant original/upstream regression cases remotely. Source + equivalence and preserved exports/state/cycles are separate from test pass. +6. Run the full801matrix against the aggregate, emphasizing main-compatible + requests, intentional feature differences and the landed fixes that old + split bodies could otherwise lose. No blanket equivalence claim from + aggregate pass counts. + +## Unpublished exact-head verification + +Intermediate staging refs remain local. For a clean aggregate candidate H, +create a task-owned Git bundle containing H above its pinned dev prerequisite: +`git bundle create /candidate.bundle HEAD ^`. +Transfer it to a fresh remote clone, verify bundle prerequisites, fetch its +HEAD and require FETCH_HEAD=H, then detach at H. The clone must contain the +recorded dev prerequisite. Never switch a shared remote seed checkout. + +Use the proven receipt wrapper pattern: main checks its clean expected HEAD +before/after SSH inside `cxc receipt test`; SSH and tee exits propagate with +pipefail; remote uses frozen root/dashboard installs, repository Bun, build, +typecheck, focused/full suites and privacy; final remote HEAD and clean status +must still match H. Git bundle transport replaces early publication, not +identity checks. Record the concrete paths/commands as B creates the harness; +do not claim an unexecuted recipe passed. + +## First-cycle acceptance + +Repair amendment813 adds one bounded existing-test fixture correction after +the first C watchdog failure was reproduced on pinned dev. The unsuccessful +cycle was reset toP for audit; it was not counted as completed. All source +rebases and prior valid evidence remain preserved. No extra debt layer is +implemented and no test budget or assertion is relaxed. + +- All14 original identities are preserved and staged heads are accounted for, + with correct Cursor dependency ancestry and no unexplained source loss. +- First main→dev/candidate regression report distinguishes intended changes, + baseline failures and regressions; required repairs are tested, not guessed. +- Main/current-dev/candidate verification uses isolated remote execution; + candidate has actual full-gate success and a clean source-bound receipt. +- The exact source/manifest and first report pass independent review. No + intermediate PR/head was published, no old PR closed, and no final delivery + or second-cycle completion is claimed. +- D closes this local result;820 begins a new P/A/B/C/D cycle with fresh + contract coverage and final delivery authority. diff --git a/devlog/_plan/260905_now_split_train/811_first_execution.md b/devlog/_plan/260905_now_split_train/811_first_execution.md new file mode 100644 index 0000000000..354895e554 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/811_first_execution.md @@ -0,0 +1,80 @@ +# 811 — First-cycle execution checkpoint + +## Rebase input and source identity + +All14 original branches and immutable checkpoint refs remain at the800 inventory +heads. Task-owned staging worktrees use `codex/closeout-pr-N`; aggregate stays +in the app-bound a2c0 checkout. Pinned dev is +`bf58ef1824e7b827b2a6bc1a5effb5d36ce80180`, main is +`48f8186647d9ffb108d226dcfa91a64225aae2a7`. + +| Original PR | Rebased staging head | +|---|---| +| #3557 | `426724e4904e8012f0d99241d3ca695d1aeaf2a9` | +| #3559 | `3914ccc33bd0142f7280bf7866ad532b1d58ac39` | +| #3566 | `a4f0118fc895cc1742c2204830ac4b23749c4b59` | +| #3567 | `812a741158e3390e602e34484de25e32ca720443` | +| #3570 | `024d1464607e0c1f6b53cb3ef81a65a95d778a04` | +| #3574 | `ac31bde36a23d1b4db9c620ab5fba8dffba7550f` | +| #3577 | `7f91bd7a2033c5586de350253add8bbce0c72ba1` | +| #3580 | `953985f121d58cd2aea6386773fa6283de88f328` | +| #3583 | `a14bff28e93b203c63c8f9d82369a251d8a00780` | +| #3585 | `96daae4d35f81a488868c03029fcda5fee1a5fe4` | +| #3590 | `2ef91f416d3b0da738f1fe5632c21c1cf3a8f831` | +| #3594 | `3f75e5dfc45293e014ff913feda3da308899087e` | +| #3599 | `990b1ba6d5ec13b3b0049da8c3e4bdb86d57b221` | +| #3611 | `2c3175ca07ab780edfce97b59069353014da5c2d` | + +The aggregate contains every staged tip by merge ancestry. Cursor3557 precedes +3570; the child's replay excludes the original parent. No original PR has +been pushed, retargeted, closed or merged by this closeout. + +## Replay accounting + +-3557/3559/3566/3567/3570/3574/3583/3585/3590/3599: every replayed patch is +range-diff equivalent. New dev fields and retained functions remain present. +-3594: the extraction retains the new cooldown default constant and all dev +normalization/validation fields; test and trailing-whitespace commits are +unchanged. +-3577: moved rewrite and planning functions include current-dev image caption +alignment and Reserve admission/policy options. A final whitespace-only +commit removes an inherited trailing blank line. +-3580: parser-content keeps current-dev URL/file-ID/detail handling. The old +trailing-blank-only commit is redundant because conflict resolution already +produced the original final parser blob; its content was not lost. +-3611: all source/test changes are retained. Shared000/003 historical changes +are already superseded by the reviewed versions on dev; use those newer +versions instead of restoring obsolete stack depth, class-method exception, +prerequisite or verifier wording. Layer400 history remains. + +## Completed remote baselines + +- Main48f818: frozen root/dashboard install, build, typecheck, privacy and +full suite succeeded;17717pass/16skip/0fail across parallel and six disjoint +serial lanes. Final HEAD matched and checkout was clean. +- Devbf58ef: same gates succeeded;19220pass/16skip/0fail across the same lane +partition. Final HEAD matched and checkout was clean. +- The pinned-main244value-export snapshot was independently checked against +the actual main runtime in an isolated test process:15pass/0fail. The +temporary probe was moved out of the checkout afterwards; final tree clean. + +These baseline results do not certify the aggregate. Full logs remain in +the session's ignored evidence directory (`closeout-main-baseline.log`, +`closeout-dev-baseline.log`, `closeout-main-exports.log`). + +## Candidate verifier + +Concrete ignored scripts: `closeout-check.sh` and +`closeout-remote-gates.sh`, with `closeout-stage-manifest.json`. +The local wrapper requires exact clean H before and after, creates a bundle +of H plus14 staging refs above pinned dev, transfers it to a fresh lidge +directory, and binds remote FETCH_HEAD to H. Every staging SHA gets focused +tests through the repository's isolated test runner. Shared dependencies +require byte-identical manifests and lockfiles. It then returns to H for +pinned-main export probe, typecheck, dashboard lint and isolated component +tests, privacy and full tests. +Bash syntax checks passed. Actual candidate execution remains pending. + +No local test, typecheck, install or build ran. No intermediate publication +or hosted-CI trigger occurred. This is a checkpoint, not cycle1 completion +and not final delivery. The second full regression cycle remains required. diff --git a/devlog/_plan/260905_now_split_train/812_first_cycle_outcome.md b/devlog/_plan/260905_now_split_train/812_first_cycle_outcome.md new file mode 100644 index 0000000000..dbaba43092 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/812_first_cycle_outcome.md @@ -0,0 +1,139 @@ +# 812 — Completed local verification cycle and evidence + + > Current sequencing is governed by830: deliver this verified aggregate into dev first, then execute two complete post-merge regression PABCD cycles. The local cycle recorded here does not replace those two cycles. + +## Scope and result boundary + +Pinned main48f8186647d9ffb108d226dcfa91a64225aae2a7, pinned +devbf58ef1824e7b827b2a6bc1a5effb5d36ce80180, candidate +e052a874085d9dde864086146330348c3cba150a. This cycle does not publish +or merge. It does not resolve all68 initial debt rows. A second full PABCD +cycle and final hosted/merged-dev gates remain required. + +The repaired verification candidate is +ef7914d4a51899f49baa141990f79750b4c75cf9. Its product/dashboard/package +content is identical to e052;813 adds only the test-fixture correction and +its records. The earlier failed attempts remain attributed to e052. + +## Per-stack preservation and remaining size debt + +| Original facade | Dev lines | Candidate lines | +|---|---:|---:| +| `src/lib/redact.ts` | 526 | 353 | +| `src/providers/openai-tiers.ts` | 416 | 319 | +| `src/adapters/anthropic-image-normalize.ts` | 518 | 228 | +| `src/adapters/cursor/native-exec-desktop.ts` | 207 | 194 | +| `src/adapters/cursor/tool-definitions.ts` | 777 | 112 | +| `src/adapters/xai-tool-schema.ts` | 436 | 351 | +| `src/vision/index.ts` | 673 | 380 | +| `src/responses/parser.ts` | 889 | 560 | +| `src/claude/inbound.ts` | 583 | 386 | +| `src/server/system-env.ts` | 537 | 310 | +| `src/codex/prompt-layers.ts` | 1652 | 1146 | +| `src/combos/types.ts` | 440 | 350 | +| `src/codex/log-guard/inspect.ts` | 524 | 392 | +| `src/clients/config-export.ts` | 1990 | 1298 | + +These are14 facade counts, not a fresh whole-repository debt census. The +desktop contract precursor was already under400. Parser, prompt layers and +config export remain above400; function-size debt is not eliminated by pure +moves. All31 new leaves are below400. No later debt layer was implemented. + +Main independently checked every staged tip's ancestry and scoped source/test +blob equality in the aggregate, plus original/checkpoint identities. Fresh +independent review inspected46source/14test deltas, preserved declarations and +value/type exports, and147runtime edges; no introduced cycle or state-owner +duplication found. Vision Reserve policy/admission and file-ID caption +alignment, parser URL/file-ID/detail behavior, Claude rejection/error identity, +Combo cooldown defaults and newer provider fields survived rebase. See811 +for every staged SHA and non-identical replay disposition. + +## Whole main-to-dev regression matrix + +The interval includes1837changed paths:1109test paths,185source paths, +96dashboard paths plus tooling/docs/assets.1026test renames mix relocation +with behavior-specific additions; rename similarity alone is not proof of +assertion equivalence. Root package version2.42→2.43 and native catalog5→8 +in the isolated fixture are intended changes, not invariance claims. + +| Surface | Evidence and interpretation | +|---|---| +| Responses/chat/Claude and images | Existing conformance, opaque recovery, native passthrough, inbound, tool-result image and vision cache cases retained; each original layer's focused checks passed at its own staged tip. File-backed translated Claude rejection is intentional; native preservation remains separately covered. | +| Streaming/cancellation/WS | Existing relay-eager, passthrough-abort and ws-upstream cases are in the full suite. WS metadata and task-recovery opt-in are intentional additions; finite cases do not prove every possible continuation chain. | +| Config/CLI/native | Client export bytes/fragments, TOML/EOL/ownership, shell env boundaries, CLI help/service and original assertion bodies retained. No live configuration or service changed. | +| Catalog/routing/state | Initial-selection fencing, combo wait/default/cancellation, reactive429 rotation and Reserve dispatch revocation remain in the full suite. Explicit opt-ins are distinct from default compatibility. Fast limitation below remains open. | +| Privacy/optional subsystem | Privacy scan passed at candidate; existing core-Lab boundary and destination/redaction coverage retained. Independent security review found no broadened authority in the extraction delta. | +| Dashboard/package | Main1310/0 and dev1443/0 component tests plus build/lint succeeded. Candidate1443/0 also passed. Isolated loopback rendered Dashboard/Providers/Models/Logs/Integrations; both showed one configured fixture, logs-empty and client-state surfaces; consoleerrors[]. Fixture upstream at127.0.0.1:1 intentionally refuses discovery, and configured-model fallback remained visible. Browser snapshots retained in ignored evidence. | +| Public value exports | Pinned-main14modules244names independently verified in actual main runtime15/0, then candidate15/0. Names-only check does not establish signature or semantic compatibility; declaration review and behavioral cases are separate evidence. | + +## Execution record — do not flatten failures into green + +The rendered baseline comparison above was pinned main versus pinned dev, +not initial-candidate UI proof. The repaired candidateef7914 was then served +separately on loopback18173 in its own fake home: Dashboard online, +Providers fixture ready, Models9/9 (8native plus1fixture), empty Logs and +unchanged Integrations cards were observed, with no console errors. Candidate +screenshots are retained; its exact test-owned server was stopped afterwards. + +Main baseline:17717pass/16skip/0fail; dev baseline:19220pass/16skip/0fail. +Both exact final SHAs clean; frozen installs/build/type/privacy succeeded. + +First candidate ordinary full run was interrupted after466s in +claude-models-discovery.test.ts. One worker was CPU-bound; suite143 and no +receipt. All14stage checks, export15/0, dashboard1443/0, type/lint/privacy0 +before that interruption remain valid same-head partial evidence. + +Unchanged discovery file alone passed12/12 in1.3s; diagnostic five repeats +passed60/60 in5.1s. A diagnostic full run withCPU-prof flags passed +19235/16/0, but produced no profiler artifact and its workerargv lacked those +flags. Profiling activation was not proved and no hotspot was obtained. +No test, timeout, runner partition or product source was weakened or changed. + +Third whole-suite attempt (second ordinary attempt) completed with one +server-auth WebSocket terminal watchdog failure at1026ms; no receipt was +written. Matched traces reproduced that same failure on pinned dev1/20; +candidate20/20 passed. Those measurements did not themselves fix anything. + +The fixture redirected fetch but missed the native WebSocket constructor. +A no-egress sentinel found2 unmocked canonical connection attempts while +the original old/new credential assertion passed.813 replanned the failed +cycle and repaired only that fixture, restoring WebSocket after every test +and covering exact path/scheme/host/argument delegation. The same sentinel +then passed with0 unmocked attempts at ef7914d4a, including unchanged +credential and status-log assertions. No timeout or product behavior changed. +Fresh repaired ordinary full suite passed19236/16skip/0fail (19059parallel +plus177serial), focused server-auth/native-WS/provider fixture161/1skip/0fail, +typecheck/privacy0. Source-bound receipt for ef7914d4a is clean, exit0, +owner01a06e97-b9d8-7250-8204-bb788338c288, epochc-20260905140213-d1684e. +Archived receipt: closeout-first-receipt-ef7914.json. Earlier failures remain +recorded; the separate discovery stall did not recur in this run, but is not +claimed fixed. Main accepts carrying that observation into820 as a +nonterminal risk, not as publication clearance or a defect-closure claim. +The first stall remains unexplained, not fixed or proven environmental. +A later passing run cannot erase it. It must carry into cycle2 and final +hosted evidence; recurrence triggers renewed RCA rather than blind retries. + +## Fast opt-in limitation + +A focused characterization on pinned dev confirmed a live-only literal +`fixture/foo--fast` remains literal while in the discovery cache, but after +explicit eviction can be read as Fast for`fixture/foo` when`fastRows=true`. +Default-off and explicitly configured literal controls keep the original +identity.1case/6assertions passed. The relevant source is unchanged in this +aggregate. Main has no Fast flag/parser, so the reviewer retracted an +existing-main-default-regression classification. + +This is an unresolved new-feature contract ambiguity/defect candidate against +an overstrong source comment, not an implemented fix or a blanket reserved-name +policy. Retaining literal history or narrowing synthetic routing needs its own +design decision; neither is silently introduced by a pure-move closeout. +No universal “zero regressions anywhere” claim follows from this report. + +## Next cycle + +The first local work-phase closed through an evidence-backed D at ef7914. +The user's latest correction makes830 delivery the next phase: publish this +aggregate, pass its exact-head hosted checks and admin-land into dev. Then +840/850 perform two complete post-merge regression cycles, consuming820's +independent contract-guard design and the limits recorded above. No claim of +post-merge regression completion is made here. diff --git a/devlog/_plan/260905_now_split_train/813_http_fixture_isolation.md b/devlog/_plan/260905_now_split_train/813_http_fixture_isolation.md new file mode 100644 index 0000000000..3871b1e0ed --- /dev/null +++ b/devlog/_plan/260905_now_split_train/813_http_fixture_isolation.md @@ -0,0 +1,106 @@ +# 813 — Deterministic transport isolation for the HTTP auth fixture + +## Loop spec and repair scope + +This is a bounded verifier repair inside810's first closeout work-phase, +not another modularization layer. The first C did not complete: one ordinary +run stalled in discovery; another completed with a WebSocket terminal +watchdog failure. No passing receipt was fabricated. Return toP/A before +changing the test. This amended cycle still counts only once when its real +C/D succeeds;820 remains a separate full second regression cycle. + +ClassC3 test isolation; explicit trust-boundary review. Goal: preserve the +downstream WebSocket per-turn auth assertions while making the existing +HTTP/SSE fixture independent of native upstream transport availability. +No product transport, auth policy, timeout, test skip or pipeline partition +changes. Main owns the one-file edit; reviewers remain read-only. + +## Evidence and rejected alternatives + +Matched traces reproduced the same1s watchdog failure on pinned dev. The +HTTP fixture helper only replaced fetch. A temporary no-egress sentinel +observed two canonical native WebSocket construction attempts; blocking those +attempts still allowed the original old/new HTTP credential assertion to +pass, while the new zero-unhandled-dials oracle failed2!=0. Complete traces +and the controlled RED remain in ignored session evidence. + +Do not increase the watchdog or weaken completion matching. Do not route an +upgrade through the HTTP fixture handler: it would add handshake requests to +the fixture's auth observation array. Reuse the Proxy constructor-isolation +pattern already present in +tests/adapters/openai/openai-provider-option-e2e.test.ts180. Native successful +upstream transport continues to be covered by tests/responses/ws-upstream.test.ts. + +This explains the HTTP-fixture dependency exposed by the watchdog; it does +not claim to explain or fix the separate CPU-bound discovery stall. + +## Exact file change + +MODIFY only tests/server/server-auth.test.ts: + +1. Capture originalGlobalWebSocket beside originalGlobalFetch. +2. In redirectCanonicalCodexTo, move the existing canonical path prefix from + inside the fetch callback to function scope before installing either wrapper. + Install a Proxy around the current constructor. For wss, exact chatgpt.com + host and that exact path or a slash-delimited child path, throw a fixed + HTTP-only-fixture refusal before + any real native dial. For every other URL, Reflect.construct the original + target with unchanged arguments and newTarget. Existing fetch redirection + stays unchanged. Downstream loopback WebSocket remains real. +3. Restore originalGlobalWebSocket in the existing afterEach. +4. Add one local constructor-boundary regression test in the existing + server-local-auth describe block. A capturing constructor avoids all real + network calls; assert canonical upstream refusal, unchanged loopback URL/ + protocol arguments, delegated near-prefix paths and other hostnames, and + preserved static OPEN. Hooks restore on failure. + Keep every existing auth/header/log assertion unchanged. + +No new test file or layout mapping. No generic helper module. The existing +large test file is not opportunistically restructured in this closeout. + +## Audited patch shape + + const prefix = "/backend-api/codex"; + const currentWebSocket = globalThis.WebSocket; + globalThis.WebSocket = new Proxy(currentWebSocket, { + construct(target, args, newTarget) { + const url = new URL(String(args[0])); + if (url.protocol === "wss:" && url.hostname === "chatgpt.com" + && (url.pathname === prefix || url.pathname.startsWith(\x60\x24{prefix}/\x60))) { + throw new Error("HTTP-only Codex fixture rejects native upstream WebSocket"); + } + return Reflect.construct(target, args, newTarget); + }, + }); + +The refusal is test-only. The production transport's existing constructor- +failure fallback runs; no new runtime bypass or altered credential policy is +introduced. + +## A synthesis + +Both reviewer findings are accepted. The shared prefix must be explicitly +hoisted, and the new WebSocket predicate must not swallow near-prefix paths. +The existing HTTP matcher is intentionally unchanged. Noncanonical delegation +cases are added to the new constructor test; no blocker was rebutted. Main +judges the amended plan near-pass with both concrete fixes folded in, subject +to independent code and runtime verification. The review did not certify a +fix for the separate discovery stall. + +## Verification and acceptance + +- Existing no-egress sentinel on unmodified e052 is RED2!=0 after the old/new + auth assertion passes (already observed). +- The same sentinel with the helper fix must be GREEN0 attempts, with both + original credentials observed. Reverse the temporary sentinel and confirm + clean exact-head source before acceptance. +- Run the new helper-boundary case and the complete existing server-auth + file, plus ws-upstream and the adjacent provider-option fixture remotely + under unchanged assertion/deadline policy. Repeated focused runs verify + restoration and measure recurrence; they do not substitute for full gates. +- Fresh exact-head typecheck, privacy and ordinary full suite through the + source-bound receipt, with no profiling flags. Existing same-content + dashboard build/component and14stage proofs stay accurately attributed. +- Independent C review; any discovery stall recurrence returns toRCA. + Do not close that earlier unexplained observation merely because the helper + repair and a later run pass. No publication before820 final gates. diff --git a/devlog/_plan/260905_now_split_train/820_second_regression_delivery.md b/devlog/_plan/260905_now_split_train/820_second_regression_delivery.md new file mode 100644 index 0000000000..bed87d9232 --- /dev/null +++ b/devlog/_plan/260905_now_split_train/820_second_regression_delivery.md @@ -0,0 +1,93 @@ +# 820 — Second main-to-dev regression cycle and final delivery + +## Loop spec + +- Archetype/trigger: satisfy-spec second independent regression cycle, explicitly required by the user after810. +- Goal: verify the composed result again using a pinned-main module-export contract and the full801matrix, then deliver one final aggregate PR and records. +- Non-goals: new product features, new debt splits, silent baseline refresh, weakened assertions, local suites, release/deploy or peer-task communication. +- Verifier: new contract test plus existing focused and full remote gates, independent adversarial review, final hosted CI, actual merged-dev tree/ancestry and post-merge result. +- Stop: all801rows have evidence, final aggregate is admin-merged, source tree/ancestry confirmed, old PRs accurately superseded, and records delivered. +- Memory:800/801/810/820, immutable000_3main baseline data, new goalplan/ledger and per-head remote artifacts. +- Outcomes: DONE only for this cutoff; unresolved regression/authority/data-integrity issues remain incomplete. Old68rows are not marked resolved. +- Escalation/resources: existing credentials and isolated task-owned staging only; unlimited user-authorized time/tokens; gpt-6-astra high internal workers, no recursion. Main reclaims after two failed workers; added write scope requires a P amendment. + +## Exact additions in B + +NEW `tests/fixtures/split-train-main-exports.json`: copy000_3_main_export_baseline.json byte-for-byte after810 validates it. Its14module/244value-export names were extracted from pinned main48f818 with Bun.Transpiler.scan; a synthetic check confirmed type declarations are omitted and no source modules executed. All14source files had no export-star declaration. This is an independent historical baseline, not current-DUT output. + +NEW `tests/ci-workflows/split-train-main-export-contract.test.ts`: + +```ts +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { fixturePath } from "../helpers/repo-root"; +import * as surface0 from "../../src/lib/redact"; +import * as surface1 from "../../src/providers/openai-tiers"; +import * as surface2 from "../../src/adapters/anthropic-image-normalize"; +import * as surface3 from "../../src/adapters/cursor/native-exec-desktop"; +import * as surface4 from "../../src/adapters/cursor/tool-definitions"; +import * as surface5 from "../../src/adapters/xai-tool-schema"; +import * as surface6 from "../../src/vision/index"; +import * as surface7 from "../../src/responses/parser"; +import * as surface8 from "../../src/claude/inbound"; +import * as surface9 from "../../src/server/system-env"; +import * as surface10 from "../../src/codex/prompt-layers"; +import * as surface11 from "../../src/combos/types"; +import * as surface12 from "../../src/codex/log-guard/inspect"; +import * as surface13 from "../../src/clients/config-export"; + +const baseline = JSON.parse(readFileSync(fixturePath("split-train-main-exports.json"), "utf8")) as { + baselineCommit: string; + modules: Record; +}; +const surfaces: Record = { + "src/lib/redact.ts": surface0, + "src/providers/openai-tiers.ts": surface1, + "src/adapters/anthropic-image-normalize.ts": surface2, + "src/adapters/cursor/native-exec-desktop.ts": surface3, + "src/adapters/cursor/tool-definitions.ts": surface4, + "src/adapters/xai-tool-schema.ts": surface5, + "src/vision/index.ts": surface6, + "src/responses/parser.ts": surface7, + "src/claude/inbound.ts": surface8, + "src/server/system-env.ts": surface9, + "src/codex/prompt-layers.ts": surface10, + "src/combos/types.ts": surface11, + "src/codex/log-guard/inspect.ts": surface12, + "src/clients/config-export.ts": surface13, +}; + +test("split-train baseline retains its pinned provenance and coverage", () => { + expect(baseline.baselineCommit).toBe("48f8186647d9ffb108d226dcfa91a64225aae2a7"); + expect(Object.keys(baseline.modules).sort()).toEqual(Object.keys(surfaces).sort()); + expect(Object.values(baseline.modules).reduce((count, names) => count + names.length, 0)).toBe(244); +}); + +for (const [path, names] of Object.entries(baseline.modules)) { + test(`${path} preserves pinned main exports`, () => { + expect(Object.keys(surfaces[path]!)).toEqual(expect.arrayContaining(names)); + }); +}; +``` + +MODIFY `scripts/test-layout/layout.json` explicit map and `tests/fixtures/test-layout-expected.json`: add the basename `split-train-main-export-contract.test.ts` with value `ci-workflows` in sorted order, following existing repo-hygiene registrations. Do not alter other entries. + +The guard protects these module export names, not arbitrary semantic equivalence, signatures or every program input. It permits additional exports. Existing focused behavioral tests, per-stack body/type/state review and the full matrix provide the other proof; this is not a new semver promise for unrelated internal helpers. + +## Independent second-pass checks + +1. Re-open810's actual findings and classifications. Use a fresh reviewer to challenge intentional-change explanations, missed caller paths, error/permission boundaries and retained source fixes. +2. Run the new guard and layout tooling remotely. Demonstrate oracle activation once by temporarily substituting an empty observed surface for redact in the remote test table; require its assertion failure, restore and require green. Label this a test-oracle control, not a reproduced product bug. +3. Repeat the main-to-final-candidate comparison with the801matrix, targeted high-risk cases, full runtime suite, typecheck, privacy and build/component/UI evidence appropriate to the actual diff. Do not count cycle1logs as cycle2execution. +4. Finalize reviewed source and public-safe documentation locally. Transfer unpublished final commits with a Git bundle for exact-head remote proof; the receipt must match the final clean candidate. +5. Publish only this stabilized head, create one templated aggregate PR, and run/observe its actual CI. A real corrective change requires fresh evidence for the new final head, never a skip or blind rerun. +6. Admin merge with expected-head protection after successful checks/review. Confirm the actual merge tree equals the tested integration tree, fetch dev and prove ancestry. Verify the actual merged dev against the pinned-main matrix and normal final dev CI before closing this second cycle. +7. Close old14PRs as superseded only after content delivery and fresh head checks prove no later edits would be lost. Preserve originals/checkpoint refs and link final delivery; do not call old PRs individually merged. + +## Acceptance + +- Two distinct completed PABCD regression cycles exist, with separate source/harness deltas, review and receipt evidence. +- The14-module baseline fixture is immutable/provenanced; its guard executes and fails under the named oracle control. +- Per-stack and whole main-to-final-dev results distinguish intended changes from regressions, with no unresolved failures hidden. +- All source, data registrations and reviewed devlog are in the final delivered head; no intermediate heads were published merely to start CI. +- Actual CI, admin merge/tree/ancestry, post-merge verification and old-PR disposition are recorded. No claim that remaining61debt implementations were performed. diff --git a/devlog/_plan/260905_now_split_train/830_delivery_first.md b/devlog/_plan/260905_now_split_train/830_delivery_first.md new file mode 100644 index 0000000000..db2b86296b --- /dev/null +++ b/devlog/_plan/260905_now_split_train/830_delivery_first.md @@ -0,0 +1,47 @@ +# 830 — Deliver the verified aggregate before post-merge regression + +## Current user authority +The user's latest correction requires publishing this aggregate now, merging +to dev after exact-head CI and admin checks, then running at least two complete +main-to-merged-dev regression PABCD cycles. This supersedes800/810/820's earlier +prepublication ordering. Do not delay this PR for the post-merge cycles. +No release, main promotion, peer coordination or local suite is authorized. + +## P/A/B/C/D for this delivery +- P/A: verify clean sourceef7914, completed first-cycle receipt/review and + existing14head inventory; review this changed sequencing. +- B: add812_first_cycle_outcome.md from the reviewed evidence report, with + the current sequencing notice; publish codex/closeout-split-train and open + one PR targetingdev using every repository template section. No new source + changes. Preserve original14PRs and refs until verified landing. +- C: watch actual PR checks on exact publishedH. Verify current PR head, + required/logical check results, review findings and the tested merge tree. + Do not rerun local suites. Capture a source-bound receipt around the actual + hosted-CI verifier. Failed/cancelled CI is not PASS. +- D: admin merge only with explicit expectedH; verify actual tree equals + tested integration tree and fetcheddev ancestry, observe post-merge CI. + Close originals as superseded by this aggregate, not individually merged. + Record delivered facts, then immediately enter840. + +## Integration input +Source last verified at ef7914d4a51899f49baa141990f79750b4c75cf9. +Pinned first-cycle dev wasbf58ef182. Latest fetcheddev is +c4701938c102b534983ea2912b92d524edbb2c4c (#3662). Hosted merge-ref CI must +validate the actual integration with currentdev; old local proof is not +claimed to test this later base. No blanket claim that earlier unexplained +discovery stall or Fast opt-in ambiguity is fixed. + +During final landing checks, dev advanced again to +ef9c538f36f94f0e95c7f4833642e5b03bd29e2e (#3664). The first PR head0d071d +passed CI33971079937 on tree739edf9d based on c470, but that result does not +certify the newer integration. Preserve0d071d as a checkpoint, merge the +new dev into this branch without conflict, and validate the updated final +head through the same PR before admin landing. This is required base-drift +handling, not the deferred post-merge regression cycles. + +## Post-merge phase map +-840: consume820's exact new test/fixture/layout plan on the delivereddev; + execute first full post-merge main comparison and deliver resulting evidence. +-850: second independent complete PABCD against finaldev, with fresh + transport/config/rendered scenarios and full gates. Previous loop receipts + are inputs, never substituted for this cycle's work. diff --git a/src/adapters/anthropic-image-codec.ts b/src/adapters/anthropic-image-codec.ts new file mode 100644 index 0000000000..68111eabfa --- /dev/null +++ b/src/adapters/anthropic-image-codec.ts @@ -0,0 +1,304 @@ +import { sniffImageDimensions } from "./anthropic-image-guard"; +import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory"; + +/** One ladder position: dimension cap, JPEG quality attempts, per-image base64 cap. */ +export interface TierSpec { + maxEdge: number; + qualities: number[]; + /** Hard per-image base64-length cap at this position; Infinity = terminal (measured size accepted). */ + hardCap: number; +} + +const KiB = 1024; +const MiB = 1024 * 1024; + +/** + * Ladder positions 0-5. 0-2 are the age-assigned tiers; 3-5 are demotion floor steps. + * Terminal (last) accepts its measured output so the aggregate loop always terminates + * (audit round 2, blocker 1). + */ +export const TIER_SPECS: TierSpec[] = [ + { maxEdge: 2000, qualities: [80, 60, 40, 30], hardCap: 2 * MiB }, + { maxEdge: 1024, qualities: [70, 50], hardCap: 512 * KiB }, + { maxEdge: 700, qualities: [60, 40], hardCap: 192 * KiB }, + { maxEdge: 500, qualities: [40], hardCap: 100 * KiB }, + { maxEdge: 400, qualities: [30], hardCap: 100 * KiB }, + { maxEdge: 320, qualities: [25], hardCap: Infinity }, +]; +export const TERMINAL_POS = TIER_SPECS.length - 1; + +/** Newest 6 images ride tier 0, the next 14 tier 1, the rest tier 2 (020 tier table). */ +export const TIER0_COUNT = 6; +export const TIER1_COUNT = 14; + +/** Decode-bomb guards: refuse to decode absurd inputs (020 guards; "extreme values excluded"). */ +export const MAX_INPUT_BASE64_LENGTH = 64 * MiB; + +/** + * First-pass worker-pool width. Memory-bound, not CPU-bound: each in-flight item can + * hold a decoded bitmap, so this bounds peak memory to ~4 decoded images while still + * overlapping I/O and native-encode threadpool work. Fixed on purpose — a config knob + * would widen the adapter contract with no demonstrated need. + */ +export const IMAGE_NORMALIZE_CONCURRENCY = 4; +export const MAX_INPUT_PIXELS = 100_000_000; + + +/** Formats Anthropic accepts as-is; anything else must be transcoded or dropped. */ +const PASSTHROUGH_MEDIA = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]); + +export interface NormalizeOptions { + /** Shift every image's starting ladder position down (413 retry tightening; 030). */ + tierBias?: number; + /** Test seam: replaces the Bun.Image encode path (audit round 1, blocker 6). */ + encode?: EncodeFn; + /** Test seam: replaces the pass-through decode validation (C-gate round 1, blocker 1). */ + validate?: ValidateFn; +} + +export type EncodeFn = ( + input: Uint8Array, + spec: TierSpec, + quality: number, +) => Promise<{ data: string; mediaType: string }>; + +/** Proves the payload fully decodes; must throw for corrupt/truncated data. */ +export type ValidateFn = (input: Uint8Array) => Promise; + +type ProcessResult = + | { kind: "pass"; b64Length: number } + | { kind: "encoded"; data: string; mediaType: string } + | { kind: "failed" }; + +/** + * Byte-weighted LRU over normalized outputs (audit round 1, blocker 2): aggregate cap, + * not entry count. Entries are immutable snapshots — demotions write NEW tier-suffixed + * keys, never mutate stored values. + */ +export const IMAGE_NORMALIZE_CACHE_MAX_BYTES = 64 * MiB; +const CACHE_MAX_ENTRIES = 4_096; +const CACHE_MAX_ENTRY_BYTES = 20 * MiB; +// "pass" = validated pass-through; "miss" = this position's ladder cannot meet its hard +// cap for these bytes (skip straight to the next position — C-gate round 2, blocker 1). +type CacheValue = { data: string; mediaType: string } | "pass" | "miss"; +interface CacheEntry { + value: CacheValue; + sizeBytes: number; + metadataBytes: number; + storedAt: number; +} +interface NormalizeCacheLimits { + maxBytes: number; + maxEntries: number; + maxEntryBytes: number; +} +const DEFAULT_CACHE_LIMITS: NormalizeCacheLimits = { + maxBytes: IMAGE_NORMALIZE_CACHE_MAX_BYTES, + maxEntries: CACHE_MAX_ENTRIES, + maxEntryBytes: CACHE_MAX_ENTRY_BYTES, +}; +const cacheEncoder = new TextEncoder(); +const cache = new Map(); +let cacheLimits = { ...DEFAULT_CACHE_LIMITS }; +let cacheBytes = 0; +let cacheMetadataBytes = 0; +let cacheSentinelEntries = 0; +let encodeCalls = 0; + +function cacheEntry(key: string, value: CacheValue): CacheEntry { + const keyBytes = cacheEncoder.encode(key).byteLength; + const valueBytes = typeof value === "string" + ? cacheEncoder.encode(value).byteLength + : cacheEncoder.encode(value.mediaType).byteLength + cacheEncoder.encode(value.data).byteLength; + const metadataBytes = keyBytes + (typeof value === "string" + ? cacheEncoder.encode(value).byteLength + : cacheEncoder.encode(value.mediaType).byteLength); + return { value, sizeBytes: keyBytes + valueBytes, metadataBytes, storedAt: Date.now() }; +} + +function deleteCacheEntry(key: string): number { + const entry = cache.get(key); + if (!entry) return 0; + cache.delete(key); + cacheBytes -= entry.sizeBytes; + cacheMetadataBytes -= entry.metadataBytes; + if (typeof entry.value === "string") cacheSentinelEntries--; + return entry.sizeBytes; +} + +function cachePut(key: string, value: CacheValue): boolean { + const next = cacheEntry(key, value); + if ( + next.sizeBytes > cacheLimits.maxEntryBytes + || next.sizeBytes > cacheLimits.maxBytes + || cacheLimits.maxEntries <= 0 + ) return false; + const existing = cache.get(key); + if (existing !== undefined) { + deleteCacheEntry(key); // re-insert refreshes recency and prevents double-count on concurrent misses + } + while (cache.size + 1 > cacheLimits.maxEntries || cacheBytes + next.sizeBytes > cacheLimits.maxBytes) { + const oldest = cache.keys().next().value; + if (oldest === undefined || deleteCacheEntry(oldest) === 0) return false; + } + cache.set(key, next); + cacheBytes += next.sizeBytes; + cacheMetadataBytes += next.metadataBytes; + if (typeof value === "string") cacheSentinelEntries++; + enforceAppOwnedMemoryBudget(); + return true; +} + +/** Read a cache entry, refreshing its recency (true LRU, C-gate round 1 blocker 5). */ +function cacheGet(key: string): CacheValue | undefined { + const entry = cache.get(key); + if (entry !== undefined) { + cache.delete(key); + entry.storedAt = Date.now(); + cache.set(key, entry); + } + return entry?.value; +} + +/** Test hooks: encoder-invocation counter + cache reset (no production caller). */ +export function getNormalizeStatsForTests(): { + encodeCalls: number; + cacheEntries: number; + cacheBytes: number; + sentinelEntries: number; + metadataBytes: number; + oldestAt: number | null; +} { + return { + encodeCalls, + cacheEntries: cache.size, + cacheBytes, + sentinelEntries: cacheSentinelEntries, + metadataBytes: cacheMetadataBytes, + oldestAt: cache.values().next().value?.storedAt ?? null, + }; +} +export function resetNormalizeStateForTests(): void { + cache.clear(); + cacheBytes = 0; + cacheMetadataBytes = 0; + cacheSentinelEntries = 0; + encodeCalls = 0; +} + +export function setNormalizeCacheLimitsForTests(limits?: Partial): void { + resetNormalizeStateForTests(); + cacheLimits = limits ? { ...DEFAULT_CACHE_LIMITS, ...limits } : { ...DEFAULT_CACHE_LIMITS }; +} + +export function anthropicImageNormalizeRetainedStoreSnapshot(): { + count: number; + bytes: number; + evictableBytes: number; + pinnedBytes: number; + oldestAt: number | null; +} { + return { + count: cache.size, + bytes: cacheBytes, + evictableBytes: cacheBytes, + pinnedBytes: 0, + oldestAt: cache.values().next().value?.storedAt ?? null, + }; +} + +export function evictOldestAnthropicImageNormalizeForBudget(): number { + const oldest = cache.keys().next().value; + return oldest === undefined ? 0 : deleteCacheEntry(oldest); +} + +/** Default encoder: Bun.Image resize-to-fit + JPEG at the given quality. */ +export const bunImageEncode: EncodeFn = async (input, spec, quality) => { + const image = new Bun.Image(input); + const meta = await image.metadata(); + const w = typeof meta.width === "number" ? meta.width : 0; + const h = typeof meta.height === "number" ? meta.height : 0; + let pipeline = new Bun.Image(input); + if (w > spec.maxEdge || h > spec.maxEdge) { + const scale = spec.maxEdge / Math.max(w, h); + pipeline = pipeline.resize(Math.max(1, Math.round(w * scale)), Math.max(1, Math.round(h * scale))); + } + const out = await pipeline.jpeg({ quality }).toBuffer(); + return { data: Buffer.from(out).toString("base64"), mediaType: "image/jpeg" }; +}; + +/** + * Default pass-through validation: force a full decode (resize forces pixel decoding, a + * header-only metadata read does not). A sniffable-but-truncated payload must throw here + * instead of riding pass-through to an Anthropic 400 (C-gate round 1, blocker 1). + */ +export const bunImageValidate: ValidateFn = async input => { + await new Bun.Image(input).resize(1, 1).jpeg({ quality: 1 }).toBuffer(); +}; + +/** + * Process one image at a ladder position: pass through when it already fits the + * position's caps (Anthropic-native format, dims within maxEdge, size within hardCap — + * this also exempts possibly-animated GIF/WebP from a lossy re-encode; pass-through is + * additionally VALIDATED with a full decode once, cached), otherwise walk positions + * downward encoding until a hard cap is met; terminal accepts measured size. + * `mediaType` must be the ORIGINAL source media type (cache keys include it — C-gate + * round 1, blocker 4 — and pass-through eligibility depends on it). + */ +export async function processAt( + b64: string, + startPos: number, + mediaType: string, + encode: EncodeFn, + validate: ValidateFn, +): Promise { + const dims = sniffImageDimensions(b64); + const hash = Bun.hash(b64).toString(36); + let input: Uint8Array; + try { + input = Uint8Array.from(Buffer.from(b64, "base64")); + } catch { + return { kind: "failed", pos: startPos }; + } + for (let pos = startPos; pos <= TERMINAL_POS; pos++) { + const spec = TIER_SPECS[pos]; + const key = `${hash}:${mediaType}:${pos}`; + const cached = cacheGet(key); + if (cached === "pass") return { kind: "pass", b64Length: b64.length, pos }; + if (cached === "miss") continue; // known cap miss: skip to the next position + if (cached) return { kind: "encoded", data: cached.data, mediaType: cached.mediaType, pos }; + + const fitsDims = dims !== null && dims.width <= spec.maxEdge && dims.height <= spec.maxEdge; + if (PASSTHROUGH_MEDIA.has(mediaType) && fitsDims && b64.length <= spec.hardCap) { + try { + await validate(input); // sniffable-but-truncated data must not ride pass-through + } catch { + return { kind: "failed", pos }; + } + cachePut(key, "pass"); + return { kind: "pass", b64Length: b64.length, pos }; + } + + let last: { data: string; mediaType: string } | null = null; + try { + for (const quality of spec.qualities) { + encodeCalls++; + last = await encode(input, spec, quality); + if (last.data.length <= spec.hardCap) { + cachePut(key, last); + return { kind: "encoded", data: last.data, mediaType: last.mediaType, pos }; + } + } + } catch { + // Decode/encode failure: corrupt or unsupported payload (audit round 2, blocker 2). + return { kind: "failed", pos }; + } + if (pos === TERMINAL_POS && last) { + cachePut(key, last); + return { kind: "encoded", data: last.data, mediaType: last.mediaType, pos }; + } + // Hard cap missed at this position — remember the miss, continue down the ladder. + cachePut(key, "miss"); + } + return { kind: "failed", pos: TERMINAL_POS }; +} diff --git a/src/adapters/anthropic-image-normalize.ts b/src/adapters/anthropic-image-normalize.ts index fed9cf10d2..cd5f52b16d 100644 --- a/src/adapters/anthropic-image-normalize.ts +++ b/src/adapters/anthropic-image-normalize.ts @@ -20,245 +20,21 @@ import { TOTAL_IMAGE_BASE64_BUDGET, type ImageBlockRef, } from "./anthropic-image-guard"; -import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory"; -/** One ladder position: dimension cap, JPEG quality attempts, per-image base64 cap. */ -export interface TierSpec { - maxEdge: number; - qualities: number[]; - /** Hard per-image base64-length cap at this position; Infinity = terminal (measured size accepted). */ - hardCap: number; -} - -const KiB = 1024; -const MiB = 1024 * 1024; - -/** - * Ladder positions 0-5. 0-2 are the age-assigned tiers; 3-5 are demotion floor steps. - * Terminal (last) accepts its measured output so the aggregate loop always terminates - * (audit round 2, blocker 1). - */ -export const TIER_SPECS: TierSpec[] = [ - { maxEdge: 2000, qualities: [80, 60, 40, 30], hardCap: 2 * MiB }, - { maxEdge: 1024, qualities: [70, 50], hardCap: 512 * KiB }, - { maxEdge: 700, qualities: [60, 40], hardCap: 192 * KiB }, - { maxEdge: 500, qualities: [40], hardCap: 100 * KiB }, - { maxEdge: 400, qualities: [30], hardCap: 100 * KiB }, - { maxEdge: 320, qualities: [25], hardCap: Infinity }, -]; -const TERMINAL_POS = TIER_SPECS.length - 1; - -/** Newest 6 images ride tier 0, the next 14 tier 1, the rest tier 2 (020 tier table). */ -const TIER0_COUNT = 6; -const TIER1_COUNT = 14; +export type { TierSpec, NormalizeOptions, EncodeFn, ValidateFn } from "./anthropic-image-codec"; +export { TIER_SPECS, MAX_INPUT_BASE64_LENGTH, IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_PIXELS } from "./anthropic-image-codec"; +export { IMAGE_NORMALIZE_CACHE_MAX_BYTES } from "./anthropic-image-codec"; +export { getNormalizeStatsForTests, resetNormalizeStateForTests, setNormalizeCacheLimitsForTests } from "./anthropic-image-codec"; +export { anthropicImageNormalizeRetainedStoreSnapshot, evictOldestAnthropicImageNormalizeForBudget } from "./anthropic-image-codec"; -/** Decode-bomb guards: refuse to decode absurd inputs (020 guards; "extreme values excluded"). */ -export const MAX_INPUT_BASE64_LENGTH = 64 * MiB; - -/** - * First-pass worker-pool width. Memory-bound, not CPU-bound: each in-flight item can - * hold a decoded bitmap, so this bounds peak memory to ~4 decoded images while still - * overlapping I/O and native-encode threadpool work. Fixed on purpose — a config knob - * would widen the adapter contract with no demonstrated need. - */ -export const IMAGE_NORMALIZE_CONCURRENCY = 4; -export const MAX_INPUT_PIXELS = 100_000_000; +import { bunImageEncode, bunImageValidate, processAt, TERMINAL_POS, TIER0_COUNT, TIER1_COUNT } from "./anthropic-image-codec"; +import { IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_BASE64_LENGTH, MAX_INPUT_PIXELS } from "./anthropic-image-codec"; +import type { NormalizeOptions } from "./anthropic-image-codec"; const UNDECODABLE_TEXT = "[image omitted: undecodable or corrupt image data]"; const BOMB_TEXT = "[image omitted: image too large to process safely]"; const OVERFLOW_DROP_TEXT = "[image omitted: total image payload exceeded the provider request budget; older images were dropped]"; -/** Formats Anthropic accepts as-is; anything else must be transcoded or dropped. */ -const PASSTHROUGH_MEDIA = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]); - -export interface NormalizeOptions { - /** Shift every image's starting ladder position down (413 retry tightening; 030). */ - tierBias?: number; - /** Test seam: replaces the Bun.Image encode path (audit round 1, blocker 6). */ - encode?: EncodeFn; - /** Test seam: replaces the pass-through decode validation (C-gate round 1, blocker 1). */ - validate?: ValidateFn; -} - -export type EncodeFn = ( - input: Uint8Array, - spec: TierSpec, - quality: number, -) => Promise<{ data: string; mediaType: string }>; - -/** Proves the payload fully decodes; must throw for corrupt/truncated data. */ -export type ValidateFn = (input: Uint8Array) => Promise; - -type ProcessResult = - | { kind: "pass"; b64Length: number } - | { kind: "encoded"; data: string; mediaType: string } - | { kind: "failed" }; - -/** - * Byte-weighted LRU over normalized outputs (audit round 1, blocker 2): aggregate cap, - * not entry count. Entries are immutable snapshots — demotions write NEW tier-suffixed - * keys, never mutate stored values. - */ -export const IMAGE_NORMALIZE_CACHE_MAX_BYTES = 64 * MiB; -const CACHE_MAX_ENTRIES = 4_096; -const CACHE_MAX_ENTRY_BYTES = 20 * MiB; -// "pass" = validated pass-through; "miss" = this position's ladder cannot meet its hard -// cap for these bytes (skip straight to the next position — C-gate round 2, blocker 1). -type CacheValue = { data: string; mediaType: string } | "pass" | "miss"; -interface CacheEntry { - value: CacheValue; - sizeBytes: number; - metadataBytes: number; - storedAt: number; -} -interface NormalizeCacheLimits { - maxBytes: number; - maxEntries: number; - maxEntryBytes: number; -} -const DEFAULT_CACHE_LIMITS: NormalizeCacheLimits = { - maxBytes: IMAGE_NORMALIZE_CACHE_MAX_BYTES, - maxEntries: CACHE_MAX_ENTRIES, - maxEntryBytes: CACHE_MAX_ENTRY_BYTES, -}; -const cacheEncoder = new TextEncoder(); -const cache = new Map(); -let cacheLimits = { ...DEFAULT_CACHE_LIMITS }; -let cacheBytes = 0; -let cacheMetadataBytes = 0; -let cacheSentinelEntries = 0; -let encodeCalls = 0; - -function cacheEntry(key: string, value: CacheValue): CacheEntry { - const keyBytes = cacheEncoder.encode(key).byteLength; - const valueBytes = typeof value === "string" - ? cacheEncoder.encode(value).byteLength - : cacheEncoder.encode(value.mediaType).byteLength + cacheEncoder.encode(value.data).byteLength; - const metadataBytes = keyBytes + (typeof value === "string" - ? cacheEncoder.encode(value).byteLength - : cacheEncoder.encode(value.mediaType).byteLength); - return { value, sizeBytes: keyBytes + valueBytes, metadataBytes, storedAt: Date.now() }; -} - -function deleteCacheEntry(key: string): number { - const entry = cache.get(key); - if (!entry) return 0; - cache.delete(key); - cacheBytes -= entry.sizeBytes; - cacheMetadataBytes -= entry.metadataBytes; - if (typeof entry.value === "string") cacheSentinelEntries--; - return entry.sizeBytes; -} - -function cachePut(key: string, value: CacheValue): boolean { - const next = cacheEntry(key, value); - if ( - next.sizeBytes > cacheLimits.maxEntryBytes - || next.sizeBytes > cacheLimits.maxBytes - || cacheLimits.maxEntries <= 0 - ) return false; - const existing = cache.get(key); - if (existing !== undefined) { - deleteCacheEntry(key); // re-insert refreshes recency and prevents double-count on concurrent misses - } - while (cache.size + 1 > cacheLimits.maxEntries || cacheBytes + next.sizeBytes > cacheLimits.maxBytes) { - const oldest = cache.keys().next().value; - if (oldest === undefined || deleteCacheEntry(oldest) === 0) return false; - } - cache.set(key, next); - cacheBytes += next.sizeBytes; - cacheMetadataBytes += next.metadataBytes; - if (typeof value === "string") cacheSentinelEntries++; - enforceAppOwnedMemoryBudget(); - return true; -} - -/** Read a cache entry, refreshing its recency (true LRU, C-gate round 1 blocker 5). */ -function cacheGet(key: string): CacheValue | undefined { - const entry = cache.get(key); - if (entry !== undefined) { - cache.delete(key); - entry.storedAt = Date.now(); - cache.set(key, entry); - } - return entry?.value; -} - -/** Test hooks: encoder-invocation counter + cache reset (no production caller). */ -export function getNormalizeStatsForTests(): { - encodeCalls: number; - cacheEntries: number; - cacheBytes: number; - sentinelEntries: number; - metadataBytes: number; - oldestAt: number | null; -} { - return { - encodeCalls, - cacheEntries: cache.size, - cacheBytes, - sentinelEntries: cacheSentinelEntries, - metadataBytes: cacheMetadataBytes, - oldestAt: cache.values().next().value?.storedAt ?? null, - }; -} -export function resetNormalizeStateForTests(): void { - cache.clear(); - cacheBytes = 0; - cacheMetadataBytes = 0; - cacheSentinelEntries = 0; - encodeCalls = 0; -} - -export function setNormalizeCacheLimitsForTests(limits?: Partial): void { - resetNormalizeStateForTests(); - cacheLimits = limits ? { ...DEFAULT_CACHE_LIMITS, ...limits } : { ...DEFAULT_CACHE_LIMITS }; -} - -export function anthropicImageNormalizeRetainedStoreSnapshot(): { - count: number; - bytes: number; - evictableBytes: number; - pinnedBytes: number; - oldestAt: number | null; -} { - return { - count: cache.size, - bytes: cacheBytes, - evictableBytes: cacheBytes, - pinnedBytes: 0, - oldestAt: cache.values().next().value?.storedAt ?? null, - }; -} - -export function evictOldestAnthropicImageNormalizeForBudget(): number { - const oldest = cache.keys().next().value; - return oldest === undefined ? 0 : deleteCacheEntry(oldest); -} - -/** Default encoder: Bun.Image resize-to-fit + JPEG at the given quality. */ -const bunImageEncode: EncodeFn = async (input, spec, quality) => { - const image = new Bun.Image(input); - const meta = await image.metadata(); - const w = typeof meta.width === "number" ? meta.width : 0; - const h = typeof meta.height === "number" ? meta.height : 0; - let pipeline = new Bun.Image(input); - if (w > spec.maxEdge || h > spec.maxEdge) { - const scale = spec.maxEdge / Math.max(w, h); - pipeline = pipeline.resize(Math.max(1, Math.round(w * scale)), Math.max(1, Math.round(h * scale))); - } - const out = await pipeline.jpeg({ quality }).toBuffer(); - return { data: Buffer.from(out).toString("base64"), mediaType: "image/jpeg" }; -}; - -/** - * Default pass-through validation: force a full decode (resize forces pixel decoding, a - * header-only metadata read does not). A sniffable-but-truncated payload must throw here - * instead of riding pass-through to an Anthropic 400 (C-gate round 1, blocker 1). - */ -const bunImageValidate: ValidateFn = async input => { - await new Bun.Image(input).resize(1, 1).jpeg({ quality: 1 }).toBuffer(); -}; function mediaTypeOf(ref: ImageBlockRef): string { const block = ref.container[ref.index] as { source?: { media_type?: unknown } } | undefined; @@ -279,72 +55,6 @@ function initialPosition(newestFirstIndex: number, bias: number): number { return Math.min(base + Math.max(0, bias), TERMINAL_POS); } -/** - * Process one image at a ladder position: pass through when it already fits the - * position's caps (Anthropic-native format, dims within maxEdge, size within hardCap — - * this also exempts possibly-animated GIF/WebP from a lossy re-encode; pass-through is - * additionally VALIDATED with a full decode once, cached), otherwise walk positions - * downward encoding until a hard cap is met; terminal accepts measured size. - * `mediaType` must be the ORIGINAL source media type (cache keys include it — C-gate - * round 1, blocker 4 — and pass-through eligibility depends on it). - */ -async function processAt( - b64: string, - startPos: number, - mediaType: string, - encode: EncodeFn, - validate: ValidateFn, -): Promise { - const dims = sniffImageDimensions(b64); - const hash = Bun.hash(b64).toString(36); - let input: Uint8Array; - try { - input = Uint8Array.from(Buffer.from(b64, "base64")); - } catch { - return { kind: "failed", pos: startPos }; - } - for (let pos = startPos; pos <= TERMINAL_POS; pos++) { - const spec = TIER_SPECS[pos]; - const key = `${hash}:${mediaType}:${pos}`; - const cached = cacheGet(key); - if (cached === "pass") return { kind: "pass", b64Length: b64.length, pos }; - if (cached === "miss") continue; // known cap miss: skip to the next position - if (cached) return { kind: "encoded", data: cached.data, mediaType: cached.mediaType, pos }; - - const fitsDims = dims !== null && dims.width <= spec.maxEdge && dims.height <= spec.maxEdge; - if (PASSTHROUGH_MEDIA.has(mediaType) && fitsDims && b64.length <= spec.hardCap) { - try { - await validate(input); // sniffable-but-truncated data must not ride pass-through - } catch { - return { kind: "failed", pos }; - } - cachePut(key, "pass"); - return { kind: "pass", b64Length: b64.length, pos }; - } - - let last: { data: string; mediaType: string } | null = null; - try { - for (const quality of spec.qualities) { - encodeCalls++; - last = await encode(input, spec, quality); - if (last.data.length <= spec.hardCap) { - cachePut(key, last); - return { kind: "encoded", data: last.data, mediaType: last.mediaType, pos }; - } - } - } catch { - // Decode/encode failure: corrupt or unsupported payload (audit round 2, blocker 2). - return { kind: "failed", pos }; - } - if (pos === TERMINAL_POS && last) { - cachePut(key, last); - return { kind: "encoded", data: last.data, mediaType: last.mediaType, pos }; - } - // Hard cap missed at this position — remember the miss, continue down the ladder. - cachePut(key, "miss"); - } - return { kind: "failed", pos: TERMINAL_POS }; -} /** * Wire-neutral image handle (devlog/260714_image_normalization_pipeline/050): the core diff --git a/src/adapters/cursor/desktop-executor-contract.ts b/src/adapters/cursor/desktop-executor-contract.ts new file mode 100644 index 0000000000..2d89a7fa46 --- /dev/null +++ b/src/adapters/cursor/desktop-executor-contract.ts @@ -0,0 +1,15 @@ +/** + * Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and + * cannot drive a screen itself; set these commands only when running on a host that can. Each + * command receives the request as JSON on stdin and must print a JSON result on stdout. + */ +export interface DesktopExecutorConfig { + /** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */ + computerUseCommand?: string; + /** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */ + recordScreenCommand?: string; + cwd?: string; + env?: Record; + /** Max time to wait for the external process. Default 30s. */ + timeoutMs?: number; +} diff --git a/src/adapters/cursor/native-exec-desktop.ts b/src/adapters/cursor/native-exec-desktop.ts index 15c6ae31b6..3a7b207ee8 100644 --- a/src/adapters/cursor/native-exec-desktop.ts +++ b/src/adapters/cursor/native-exec-desktop.ts @@ -17,24 +17,11 @@ import { } from "./gen/agent_pb"; import { errorText } from "./native-exec-common"; import type { CursorNativeToolDeps } from "./native-exec-tools"; +import type { DesktopExecutorConfig } from "./desktop-executor-contract"; const DEFAULT_DESKTOP_TIMEOUT_MS = 30_000; -/** - * Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and - * cannot drive a screen itself; set these commands only when running on a host that can. Each - * command receives the request as JSON on stdin and must print a JSON result on stdout. - */ -export interface DesktopExecutorConfig { - /** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */ - computerUseCommand?: string; - /** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */ - recordScreenCommand?: string; - cwd?: string; - env?: Record; - /** Max time to wait for the external process. Default 30s. */ - timeoutMs?: number; -} +export type { DesktopExecutorConfig } from "./desktop-executor-contract"; /** * Build `computerUse` / `recordScreen` deps from external executor commands. Returns `{}` when no diff --git a/src/adapters/cursor/tool-definitions.ts b/src/adapters/cursor/tool-definitions.ts index 31f583572a..164b05433d 100644 --- a/src/adapters/cursor/tool-definitions.ts +++ b/src/adapters/cursor/tool-definitions.ts @@ -1,271 +1,12 @@ import { create, fromJson, toBinary, type JsonValue } from "@bufbuild/protobuf"; import { ValueSchema } from "@bufbuild/protobuf/wkt"; import type { OcxRequestOptions, OcxTool } from "../../types"; -import { namespacedToolName, toolChoiceAliases } from "../../types"; import { McpToolDefinitionSchema, McpToolsSchema, type McpToolDefinition } from "./gen/agent_pb"; -import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize"; - -export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses"; -export const CODEX_EXEC_COMMAND_TOOL = "exec_command"; -export const CODEX_SHELL_COMMAND_TOOL = "shell_command"; -/** Codex Desktop unified-exec client tool. Companion of `wait`; not an `exec_command` schema alias. */ -export const CODEX_UNIFIED_EXEC_TOOL = "exec"; -export const CODEX_WAIT_TOOL = "wait"; -export const CODEX_APPLY_PATCH_TOOL = "apply_patch"; -export const CODEX_TOOL_SEARCH_TOOL = "tool_search"; -export const CURSOR_EDIT_FILE_TOOL = "edit_file"; -export const CURSOR_MULTI_EDIT_TOOL = "multi_edit"; -export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL] as const; -export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL; -export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const; -export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE = - 'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.'; -const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const; -const NEIGHBOR_AGENT_TOOL_ALIASES: Record<(typeof NEIGHBOR_AGENT_TOOL_NAMES)[number], readonly string[]> = { - Read: ["read", "read_file"], - Grep: ["grep"], - Glob: ["glob", "find"], - Bash: ["bash", "shell"], - LS: ["ls"], -}; - -export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [ - "For generic tool-use/count demos, satisfy the request with repeated Codex shell bridge calls (`shell_command` or `exec_command`) for harmless commands.", - "`shell_command` / `exec_command` are the Codex Responses shell bridge exposed through Cursor's tool protocol; do not describe them as an external MCP server tool.", - "Do not use `run_shell` unless this turn's tool catalog lists it.", - "A request for N tools means N separate shell-bridge invocations/results; never satisfy it with one chained shell command such as `cmd1 && cmd2`.", - "For independent read-only or output-only commands, emit all requested shell-bridge calls in the same response before waiting when the runtime supports parallel tool calls.", - "The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.", - "If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.", - "Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.", - "Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names or an equivalent listed client tool.", -].join(" "); - -export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = { - type: "object", - properties: { - cmd: { type: "string", description: "Shell command to execute." }, - workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." }, - shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." }, - tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." }, - yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." }, - max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." }, - }, - required: ["cmd"], - additionalProperties: false, -} as const; - -/** - * Structured single-replacement schema advertised to Cursor models in addition to the freeform - * `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native - * Edit shape) but cannot produce Codex's freeform patch grammar, so every file edit attempt on the - * Cursor route produced malformed `apply_patch` payloads that the Codex client rejected locally - * (#1017). Calls to this tool are converted server-side into a valid apply_patch payload. - */ -export const CURSOR_EDIT_FILE_INPUT_SCHEMA = { - type: "object", - properties: { - file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." }, - old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." }, - new_string: { type: "string", description: "Replacement text. Empty removes the matched text." }, - }, - required: ["file_path", "old_string", "new_string"], - additionalProperties: false, -} as const; - -/** Structured multi-replacement schema; mirrors Cursor's native MultiEdit shape. */ -export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = { - type: "object", - properties: { - file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." }, - edits: { - type: "array", - items: { - type: "object", - properties: { - old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." }, - new_string: { type: "string", description: "Replacement text. Empty removes the matched text." }, - }, - required: ["old_string", "new_string"], - additionalProperties: false, - }, - description: "Ordered replacement edits for this file. Each old_string must match the current file content.", - }, - }, - required: ["file_path", "edits"], - additionalProperties: false, -} as const; - -/** - * Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call. - * Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate - * `command`. Keeping `cmd` out of this schema lets `normalizeArgKeys` rewrite `cmd` → `command`. - */ -export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = { - type: "object", - properties: { - command: { type: "string", description: "Shell command to execute." }, - workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." }, - shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." }, - tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." }, - yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." }, - max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." }, - max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." }, - }, - required: ["command"], -} as const; - -export function isCodexShellBridgeToolName(name: string): boolean { - return (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name); -} - -/** - * Direct key lookup, then shell_command/exec_command sibling aliases when the key is a bridge name. - * Used for catalog admission, schema normalize maps, and Responses name maps (#399). - */ -export function resolveShellBridgeAliasKey( - key: string, - lookup: (name: string) => T | undefined, -): T | undefined { - const direct = lookup(key); - if (direct !== undefined) return direct; - if (!isCodexShellBridgeToolName(key)) return undefined; - for (const alias of CODEX_SHELL_BRIDGE_TOOL_NAMES) { - if (alias === key) continue; - const hit = lookup(alias); - if (hit !== undefined) return hit; - } - return undefined; -} - -export function cursorToolChoiceAliases(tool: Pick): string[] { - const aliases = new Set(toolChoiceAliases(tool)); - if (isBareCodexShellBridgeTool(tool)) { - for (const alias of CODEX_SHELL_BRIDGE_TOOL_NAMES) aliases.add(alias); - } - return [...aliases]; -} - -function catalogHasBareCodexShellBridge( - catalog: readonly Pick[], -): boolean { - return catalog.some(isBareCodexShellBridgeTool); -} - -/** - * Catalog-aware tool_choice matching for Cursor. - * When a bare Codex shell bridge is in the catalog, raw `shell_command` / `exec_command` - * choices select only that bridge (never a namespaced remote with the same raw name). - * When no bare bridge exists, raw bridge names may select a namespaced tool by raw name. - * Explicit wire names (`mcp__remote__exec_command`) always match the namespaced tool. - */ -function cursorToolChoiceMatches( - tool: Pick, - choiceName: string, - catalog: readonly Pick[], -): boolean { - if (isCodexShellBridgeToolName(choiceName)) { - if (catalogHasBareCodexShellBridge(catalog)) { - return isBareCodexShellBridgeTool(tool); - } - return tool.name === choiceName || cursorToolWireName(tool) === choiceName; - } - if (tool.name === choiceName) return true; - if (cursorToolChoiceAliases(tool).includes(choiceName)) return true; - return cursorToolWireName(tool) === choiceName - && !catalog.some(candidate => candidate.name === choiceName); -} - -export function isBareCodexShellBridgeTool(tool: Pick): boolean { - return !tool.namespace && isCodexShellBridgeToolName(tool.name); -} - -function isCursorResponsesProvider(namespace: string | undefined): boolean { - return !namespace || namespace === OCX_RESPONSES_TOOL_PROVIDER; -} - -const CURSOR_EXECUTION_PATH_TOOL_NAMES = [ - CODEX_UNIFIED_EXEC_TOOL, - CODEX_EXEC_COMMAND_TOOL, - CODEX_SHELL_COMMAND_TOOL, -] as const; - -/** True for the Codex execution path that must survive Cursor transport truncation. */ -export function isCursorExecutionPathTool(tool: Pick): boolean { - return isCursorResponsesProvider(tool.namespace) - && (CURSOR_EXECUTION_PATH_TOOL_NAMES as readonly string[]).includes(tool.name); -} - -/** `wait` only resumes a yielded exec cell; it is unusable without an execution-path tool. */ -export function isCursorWaitTool(tool: Pick): boolean { - return isCursorResponsesProvider(tool.namespace) && tool.name === CODEX_WAIT_TOOL; -} - -/** - * True for Codex's unified-exec "code mode" tool: a freeform `exec` whose body is JavaScript - * evaluated in a V8 isolate, not a shell command string. - */ -export function isCursorCodeModeExecTool( - tool: Pick, -): boolean { - return isCursorResponsesProvider(tool.namespace) - && tool.name === CODEX_UNIFIED_EXEC_TOOL - && tool.freeform === true; -} - -/** - * Codex code mode advertises ONE freeform `exec` tool and no bare shell bridge. Shell, file - * edits, and MCP calls are reachable only as nested `tools.(...)` helpers described inside - * that tool's own description, so a flat catalog scan cannot see them. - * - * This matters because the shell-bridge guidance below is written for a flat catalog. Emitting - * "call \`exec_command\`" into a code-mode turn names a top-level tool that does not exist: the - * model calls it, gets nothing back, and burns turns rediscovering the real contract from error - * messages (empty output until \`text()\` is called, \`require is not defined\` because the isolate - * is not Node, \`apply_patch\` rejected because it too is only a nested helper here). - */ -export function cursorRequestUsesCodeMode( - tools: readonly Pick[] | undefined, - toolChoice?: OcxRequestOptions["toolChoice"], -): boolean { - const catalog = tools ?? []; - const visible = catalog.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog)); - return visible.some(isCursorCodeModeExecTool) && !visible.some(isBareCodexShellBridgeTool); -} - -/** @deprecated Prefer isBareCodexShellBridgeTool; kept for older call sites/tests. */ -function isBareCodexExecCommandTool(tool: Pick): boolean { - return isBareCodexShellBridgeTool(tool); -} - -export function cursorRequestHasShellAlias(tools: readonly Pick[] | undefined): boolean { - return tools?.some(isBareCodexExecCommandTool) ?? false; -} - -function cursorRequestHasExecutionPath( - tools: readonly Pick[] | undefined, -): boolean { - return tools?.some(isCursorExecutionPathTool) ?? false; -} - -export function cursorRequestAdvertisesApplyPatch( - tools: readonly Pick[] | undefined, - toolChoice?: OcxRequestOptions["toolChoice"], -): boolean { - const catalog = tools ?? []; - return catalog.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && cursorToolAllowedByChoice(tool, toolChoice, catalog)); -} - -export function isCursorStructuredEditToolName(name: string): boolean { - return (CURSOR_STRUCTURED_EDIT_TOOLS as readonly string[]).includes(name); -} - -/** Internal provenance gate for synthetic edits after prompt filtering and catalog budgeting. */ -export function isCursorSyntheticStructuredEditTool( - tool: Pick, -): boolean { - return !tool.namespace && tool.cursorStructuredEdit === true && isCursorStructuredEditToolName(tool.name); -} +import { CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, cursorRequestAdvertisesApplyPatch, cursorToolAllowedByChoice, cursorToolWireName, OCX_RESPONSES_TOOL_PROVIDER } from "./tool-naming"; +import { CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, cursorToolInputSchema } from "./tool-schemas"; +export { OCX_RESPONSES_TOOL_PROVIDER, CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL, CODEX_UNIFIED_EXEC_TOOL, CODEX_WAIT_TOOL, CODEX_APPLY_PATCH_TOOL, CODEX_TOOL_SEARCH_TOOL, CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL, CURSOR_STRUCTURED_EDIT_TOOLS, CURSOR_EXEC_COMMAND_TOOL, CODEX_SHELL_BRIDGE_TOOL_NAMES, isCodexShellBridgeToolName, resolveShellBridgeAliasKey, cursorToolChoiceAliases, isBareCodexShellBridgeTool, isCursorExecutionPathTool, isCursorWaitTool, isCursorCodeModeExecTool, cursorRequestUsesCodeMode, cursorRequestHasShellAlias, cursorRequestAdvertisesApplyPatch, isCursorStructuredEditToolName, isCursorSyntheticStructuredEditTool, cursorToolWireName, normalizeCursorWireName, normalizeCursorTextToolMarkers, responsesToolNameFromCursorWire, cursorToolAllowedByChoice } from "./tool-naming"; +export { CURSOR_EXEC_COMMAND_INPUT_SCHEMA, CURSOR_EDIT_FILE_INPUT_SCHEMA, CURSOR_MULTI_EDIT_INPUT_SCHEMA, CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA, cursorToolInputSchema, cursorToolArgNormalizeSchema, shellBridgeRequiredCommandKeys, defaultShellBridgeArgNormalizeSchema, cursorShellBridgeDropError, nonEmptyShellBridgeCommandFromArgs, cursorShellBridgeArgsValid } from "./tool-schemas"; +export { CURSOR_SHELL_ALIAS_SYSTEM_NOTE, CURSOR_GENERIC_TOOL_USE_USER_HINT, isGenericToolUseCountDemoPrompt, requestedCursorToolUseCount, shouldAppendCursorGenericToolUseHint, appendCursorGenericToolUseHint, shouldUseNativeExecOnlyForGenericToolUse, cursorToolsForActivePrompt, buildCursorToolGuidanceSystemNote } from "./tool-guidance"; /** * Synthetic structured edit tools for the Cursor route (#1017). @@ -323,417 +64,11 @@ export function cursorRequestAdvertisesStructuredEdits( return cursorStructuredEditTools(tools, toolChoice).length > 0; } -const CURSOR_CLIENT_TOOL_WIRE_PREFIX = "ocx_client_"; -const CURSOR_PROXY_OWNED_BARE_TOOL_NAMES = new Set([ - CODEX_UNIFIED_EXEC_TOOL, - CODEX_WAIT_TOOL, - CODEX_EXEC_COMMAND_TOOL, - CODEX_SHELL_COMMAND_TOOL, - CODEX_APPLY_PATCH_TOOL, - CURSOR_EDIT_FILE_TOOL, - CURSOR_MULTI_EDIT_TOOL, - CODEX_TOOL_SEARCH_TOOL, -]); -/** Avoid collisions with Cursor's private bare-tool namespace. */ -function isCursorBareClientToolWireAliased( - tool: Pick, -): boolean { - return !tool.namespace - && !CURSOR_PROXY_OWNED_BARE_TOOL_NAMES.has(tool.name); -} -export function cursorToolWireName(tool: Pick): string { - if (isCursorBareClientToolWireAliased(tool)) { - return `${CURSOR_CLIENT_TOOL_WIRE_PREFIX}${tool.name}`; - } - return namespacedToolName(tool.namespace, tool.name); -} -function clientSemanticToolNameFromCursorWire(name: string): string { - return name.startsWith(CURSOR_CLIENT_TOOL_WIRE_PREFIX) - ? name.slice(CURSOR_CLIENT_TOOL_WIRE_PREFIX.length) - : name; -} -/** - * Cursor's harness shows MCP tools to the model as `mcp__`; models - * sometimes call that display name verbatim instead of the advertised short name (live 20:41/21:00 - * sessions: `mcp_opencodex-responses_exec_command` / `mcp_opencodex-responses_shell_command`). - * Fold the display prefix back to the advertised wire name, and treat `shell_command` / - * `exec_command` as the same Codex shell bridge, so alias thrash does not become "tool not found". - */ -const CURSOR_MCP_DISPLAY_PREFIX = `mcp_${OCX_RESPONSES_TOOL_PROVIDER}_`; - -export function normalizeCursorWireName(name: string): string { - return name.startsWith(CURSOR_MCP_DISPLAY_PREFIX) ? name.slice(CURSOR_MCP_DISPLAY_PREFIX.length) : name; -} - -/** - * #2305: some models emit a TEXTUAL pseudo tool call ("[TOOL_CALL]name[ARGS]{...}") - * instead of a real frame, using Cursor's display alias as the name. Text-mode clients - * (Pi) parse that text and then cannot dispatch the undeclared display name. Rewrite the - * display alias to the advertised wire name ONLY inside the marker pair — prose that - * merely mentions the alias stays untouched, and the scope guard is the exact - * `mcp_${OCX_RESPONSES_TOOL_PROVIDER}_` prefix, never generic `mcp_`. - * Known limit (recorded in devlog 230): a marker split across two streaming deltas is - * not rewritten; tail-buffering is deferred until a live trace shows split markers. - */ -const CURSOR_TEXT_TOOL_MARKER = new RegExp( - String.raw`\[TOOL_CALL\](${CURSOR_MCP_DISPLAY_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\[\]]+)\[ARGS\]`, - "g", -); - -export function normalizeCursorTextToolMarkers(text: string): string { - if (!text.includes(CURSOR_MCP_DISPLAY_PREFIX)) return text; - return text.replace(CURSOR_TEXT_TOOL_MARKER, (_match, name: string) => `[TOOL_CALL]${normalizeCursorWireName(name)}[ARGS]`); -} - -export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?: ReadonlyMap): string { - const normalized = normalizeCursorWireName(name); - if (!cursorToolNameMap) return normalized; - return resolveShellBridgeAliasKey(normalized, alias => cursorToolNameMap.get(alias)) ?? normalized; -} - -/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */ -export function cursorToolInputSchema(tool: OcxTool): unknown { - return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {}); -} - -/** - * Schema used to normalize completed Cursor tool args back to Responses/Codex field names. - * Must NOT reuse `cursorToolInputSchema` for the shell bridge: advertising `cmd` while also - * treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399). - */ -export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown { - if (isBareCodexShellBridgeTool(tool)) { - return shellBridgeArgNormalizeSchema(tool); - } - return tool.parameters ?? {}; -} - -function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown { - const parameters = tool.parameters; - if (!parameters || typeof parameters !== "object") return CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA; - const base = parameters as Record; - const rawProps = base.properties && typeof base.properties === "object" - ? { ...(base.properties as Record) } - : {}; - const required = Array.isArray(base.required) ? [...base.required as unknown[]] : []; - const requiresCommand = required.includes("command") || "command" in rawProps; - const requiresCmd = required.includes("cmd") || "cmd" in rawProps; - const shouldRewriteCmdToCommand = tool.name === CODEX_SHELL_COMMAND_TOOL || requiresCommand; - - if (!shouldRewriteCmdToCommand && requiresCmd) { - return parameters; - } - - // Drop Cursor-preferred aliases so normalizeArgKeys can rewrite them to Responses keys. - delete rawProps.cmd; - const properties = { - ...CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties, - ...rawProps, - command: rawProps.command ?? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties.command, - }; - return { - ...base, - type: "object", - properties, - required: requiresCommand ? required : ["command"], - }; -} - -export function isGenericToolUseCountDemoPrompt(text: string): boolean { - const trimmed = text.trim(); - if (trimmed.length === 0) return false; - return [ - /\b(?:use|call|invoke|try|exercise)\s+(?:any\s+)?\d+\s+tools?\b/i, - /\buse\s+any\s+tools?\b/i, - /\bactually\s+(?:call|use|invoke)\s+(?:the\s+)?tools?\b/i, - /\b\d+\s+tools?\b/i, - /\btools?\s+\d+\b/i, - /\btool\s+use\b/i, - /아무\s*(?:tool|tools?|도구|툴)/i, - /(?:tool|tools?|도구|툴)\s*\d+\s*(?:개|번)?/i, - /\d+\s*(?:개|번)?\s*(?:tool|tools?|도구|툴)/i, - /(?:도구|툴).{0,12}(?:써|사용|호출).{0,12}\d+\s*(?:개|번)?/i, - ].some(pattern => pattern.test(trimmed)); -} - -export function requestedCursorToolUseCount(text: string): number | undefined { - const patterns = [ - /\b(?:use|call|invoke|try|exercise)\s+(?:any\s+)?(\d+)\s+tools?\b/i, - /\b(\d+)\s+tools?\b/i, - /\btools?\s+(\d+)\b/i, - /(?:tool|tools?|도구|툴)\s*(\d+)\s*(?:개|번)?/i, - /(\d+)\s*(?:개|번)?\s*(?:tool|tools?|도구|툴)/i, - /(?:도구|툴).{0,12}(?:써|사용|호출).{0,12}(\d+)\s*(?:개|번)?/i, - ]; - for (const pattern of patterns) { - const match = pattern.exec(text); - const count = Number(match?.[1]); - if (Number.isInteger(count) && count > 0 && count <= 50) return count; - } - return undefined; -} - -function cursorGenericToolUseHint(text: string): string { - const count = requestedCursorToolUseCount(text); - if (!count) return CURSOR_GENERIC_TOOL_USE_USER_HINT; - return [ - `This turn requests ${count} tool uses: emit exactly ${count} separate Codex shell bridge function calls/results (\`shell_command\` or \`exec_command\`).`, - `One shell-bridge call containing chained commands counts as 1 tool call, not ${count}.`, - `Prefer one parallel tool-call batch containing all ${count} independent shell-bridge calls before waiting for results.`, - CURSOR_GENERIC_TOOL_USE_USER_HINT, - ].join(" "); -} - -function activeTextMentionsGenericToolUseHint(text: string): boolean { - return text.includes("Codex native exec tool") - || text.includes("Codex Responses bridge exec tool") - || text.includes("generic tool-use/count demos"); -} - -export function shouldAppendCursorGenericToolUseHint( - tools: readonly Pick[] | undefined, - text: string, -): boolean { - const trimmed = text.trim(); - return trimmed.length > 0 - && cursorRequestHasShellAlias(tools) - && isGenericToolUseCountDemoPrompt(trimmed) - && !activeTextMentionsGenericToolUseHint(trimmed); -} - -export function appendCursorGenericToolUseHint( - tools: readonly Pick[] | undefined, - text: string, -): string { - if (!shouldAppendCursorGenericToolUseHint(tools, text)) return text; - return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${cursorGenericToolUseHint(text)}`; -} - -export function shouldUseNativeExecOnlyForGenericToolUse( - tools: readonly Pick[] | undefined, - text: string, -): boolean { - const trimmed = text.trim(); - if (trimmed.length === 0 || !cursorRequestHasExecutionPath(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false; - return !/\b(?:mcp|resource|resources|tool_search|plugin|plugins|app connector|github)\b/i.test(trimmed) - && !/(?:리소스|플러그인|깃허브|github)/i.test(trimmed); -} - -export function cursorToolsForActivePrompt>( - tools: readonly T[] | undefined, - activeText: string, - toolChoice?: OcxRequestOptions["toolChoice"], -): readonly T[] | undefined { - if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools; - const execTools = tools?.filter(isCursorExecutionPathTool); - const catalog = tools ?? []; - if (execTools?.length && !execTools.some(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog))) return tools; - return execTools && execTools.length > 0 ? execTools : tools; -} - -/** - * Required command payload keys for a shell bridge tool, derived from the advertised schema when present. - */ -export function shellBridgeRequiredCommandKeys( - toolName: string, - schema?: unknown, -): readonly ("cmd" | "command")[] { - if (schema && typeof schema === "object") { - const required = (schema as Record).required; - if (Array.isArray(required)) { - const keys = required.filter((key): key is "cmd" | "command" => key === "cmd" || key === "command"); - if (keys.length > 0) return keys; - } - } - return toolName === CODEX_SHELL_COMMAND_TOOL ? ["command"] : ["cmd"]; -} -/** Normalize-schema defaults used when validating stateless synthetic shell-bridge calls. */ -export function defaultShellBridgeArgNormalizeSchema(toolName: string): unknown { - return toolName === CODEX_SHELL_COMMAND_TOOL - ? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA - : { - type: "object", - properties: CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties, - required: ["cmd"], - }; -} - -export function cursorShellBridgeDropError(toolName: string): string { - return `Cursor emitted ${toolName} without a non-empty command; the tool call was dropped.`; -} - -/** - * Extract a non-empty shell command from completed Cursor bridge args using the schema's required - * command key (`cmd` for bare exec_command, `command` for shell_command). - */ -export function nonEmptyShellBridgeCommandFromArgs( - finalArgs: string, - toolName: string, - schema?: unknown, -): string | undefined { - let parsed: unknown; - try { - parsed = finalArgs.length > 0 ? JSON.parse(finalArgs) : {}; - } catch { - return undefined; - } - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined; - const record = parsed as Record; - const requiredKeys = shellBridgeRequiredCommandKeys(toolName, schema); - const candidateKeys = new Set<"cmd" | "command">([ - ...requiredKeys, - requiredKeys.includes("cmd") ? "command" : "cmd", - ]); - for (const key of candidateKeys) { - const value = record[key]; - if (typeof value === "string" && value.trim().length > 0) return value.trim(); - } - return undefined; -} - -export function cursorShellBridgeArgsValid( - finalArgs: string, - toolName: string, - schema?: unknown, -): boolean { - return !isCodexShellBridgeToolName(toolName) - || nonEmptyShellBridgeCommandFromArgs(finalArgs, toolName, schema) !== undefined; -} - -export function cursorToolAllowedByChoice( - tool: Pick, - toolChoice: OcxRequestOptions["toolChoice"] | undefined, - catalog: readonly Pick[] = [tool], -): boolean { - if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true; - if (toolChoice === "none") return false; - if ("allowedTools" in toolChoice) { - return toolChoice.allowedTools.some(choiceName => cursorToolChoiceMatches(tool, choiceName, catalog)); - } - return cursorToolChoiceMatches(tool, toolChoice.name, catalog); -} - -function quotedNames(names: readonly string[]): string { - return names.map(name => `\`${name}\``).join(", "); -} - -function advertisedCoversNeighbor(wireNames: readonly string[], neighbor: (typeof NEIGHBOR_AGENT_TOOL_NAMES)[number]): boolean { - const advertised = new Set(wireNames.map(name => clientSemanticToolNameFromCursorWire(name).toLowerCase())); - if (advertised.has(neighbor.toLowerCase())) return true; - return NEIGHBOR_AGENT_TOOL_ALIASES[neighbor].some(alias => advertised.has(alias.toLowerCase())); -} - -function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string[] { - return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertisedCoversNeighbor(wireNames, name)); -} - -function discoveryToolLabel(wireNames: readonly string[]): string | undefined { - const labels: string[] = []; - if (wireNames.includes(CODEX_TOOL_SEARCH_TOOL)) labels.push(`\`${CODEX_TOOL_SEARCH_TOOL}\``); - if (wireNames.some(name => name.startsWith("mcp__"))) labels.push("MCP"); - if (wireNames.some(name => /resource/i.test(name))) labels.push("resource discovery"); - return labels.length > 0 ? labels.join(", ") : undefined; -} - -export function buildCursorToolGuidanceSystemNote( - tools: readonly Pick[] | undefined, - toolChoice?: OcxRequestOptions["toolChoice"], -): string | undefined { - if (!tools?.length) return undefined; - const wireNames = [...new Set( - tools - .filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools)) - .map(tool => cursorToolWireName(tool)), - )]; - if (wireNames.length === 0) return undefined; - - const listedNames = quotedNames(wireNames); - const shellBridgeNames = wireNames.filter(isCodexShellBridgeToolName); - const hasBareExec = shellBridgeNames.length > 0; - const codeMode = cursorRequestUsesCodeMode(tools, toolChoice); - // Code mode describes how the freeform exec tool works; it does not suppress the rest of the - // catalog. A turn can advertise freeform `exec` AND ordinary top-level tools at once, and - // telling the model those are "not separate top-level tools" would make it refuse tools that - // are right there in its catalog. Name the ones that stay callable instead. - const codeModeOtherTopLevelNames = codeMode - ? wireNames.filter(name => name !== CODEX_UNIFIED_EXEC_TOOL && !isCodexShellBridgeToolName(name)) - : []; - const shellBridgeLabel = quotedNames(shellBridgeNames.length > 0 ? shellBridgeNames : [...CODEX_SHELL_BRIDGE_TOOL_NAMES]); - const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice); - const structuredEditNames = tools - ?.filter(tool => !tool.namespace && isCursorStructuredEditToolName(tool.name)) - .map(tool => tool.name) ?? []; - const discoveryTools = discoveryToolLabel(wireNames); - const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames); - // Host-shell-neutral: the Codex client executes bridge commands, and may differ from - // the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls. - const hostShellNote = hasBareExec - ? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`< 0 - ? `This turn does not expose neighboring-agent tool names ${quotedNames(unavailableNeighborNames)}; do not call or suggest them unless the catalog lists them.` - : undefined, - // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the - // model probes for a top-level shell tool that is not there. - codeMode - ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched.` - : undefined, - codeMode - ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers." - : undefined, - codeMode - ? "NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces." - : undefined, - hasBareExec - ? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.` - : undefined, - hasBareExec - ? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user." - : undefined, - hasBareExec - ? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.` - : undefined, - hasBareExec - ? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons." - : undefined, - hostShellNote, - "Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.", - hasBareExec - ? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.` - : undefined, - hasApplyPatch - ? structuredEditNames.length > 0 - ? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.` - : "For file edits, use the `apply_patch` tool, not built-in file write/delete tools." - : undefined, - hasApplyPatch - ? "Creating or modifying file CONTENT via shell redirection (`>`, `>>`, `printf`/`echo` into a file, `cat < 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.` - : undefined, - ].filter((note): note is string => typeof note === "string"); - return notes.join(" "); -} export function encodeCursorInputSchema(schema: unknown): Uint8Array { const value: JsonValue = schema && typeof schema === "object" diff --git a/src/adapters/cursor/tool-guidance.ts b/src/adapters/cursor/tool-guidance.ts new file mode 100644 index 0000000000..54ebcc86d1 --- /dev/null +++ b/src/adapters/cursor/tool-guidance.ts @@ -0,0 +1,236 @@ +import type { OcxRequestOptions, OcxTool } from "../../types"; +import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize"; +import { CODEX_SHELL_BRIDGE_TOOL_NAMES, CODEX_TOOL_SEARCH_TOOL, CODEX_UNIFIED_EXEC_TOOL, clientSemanticToolNameFromCursorWire, cursorRequestAdvertisesApplyPatch, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolAllowedByChoice, cursorToolWireName, isCodexShellBridgeToolName, isCursorExecutionPathTool, isCursorStructuredEditToolName } from "./tool-naming"; + +export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE = + 'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.'; +const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const; +const NEIGHBOR_AGENT_TOOL_ALIASES: Record<(typeof NEIGHBOR_AGENT_TOOL_NAMES)[number], readonly string[]> = { + Read: ["read", "read_file"], + Grep: ["grep"], + Glob: ["glob", "find"], + Bash: ["bash", "shell"], + LS: ["ls"], +}; + +export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [ + "For generic tool-use/count demos, satisfy the request with repeated Codex shell bridge calls (`shell_command` or `exec_command`) for harmless commands.", + "`shell_command` / `exec_command` are the Codex Responses shell bridge exposed through Cursor's tool protocol; do not describe them as an external MCP server tool.", + "Do not use `run_shell` unless this turn's tool catalog lists it.", + "A request for N tools means N separate shell-bridge invocations/results; never satisfy it with one chained shell command such as `cmd1 && cmd2`.", + "For independent read-only or output-only commands, emit all requested shell-bridge calls in the same response before waiting when the runtime supports parallel tool calls.", + "The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.", + "If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.", + "Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.", + "Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names or an equivalent listed client tool.", +].join(" "); + + +export function isGenericToolUseCountDemoPrompt(text: string): boolean { + const trimmed = text.trim(); + if (trimmed.length === 0) return false; + return [ + /\b(?:use|call|invoke|try|exercise)\s+(?:any\s+)?\d+\s+tools?\b/i, + /\buse\s+any\s+tools?\b/i, + /\bactually\s+(?:call|use|invoke)\s+(?:the\s+)?tools?\b/i, + /\b\d+\s+tools?\b/i, + /\btools?\s+\d+\b/i, + /\btool\s+use\b/i, + /아무\s*(?:tool|tools?|도구|툴)/i, + /(?:tool|tools?|도구|툴)\s*\d+\s*(?:개|번)?/i, + /\d+\s*(?:개|번)?\s*(?:tool|tools?|도구|툴)/i, + /(?:도구|툴).{0,12}(?:써|사용|호출).{0,12}\d+\s*(?:개|번)?/i, + ].some(pattern => pattern.test(trimmed)); +} + +export function requestedCursorToolUseCount(text: string): number | undefined { + const patterns = [ + /\b(?:use|call|invoke|try|exercise)\s+(?:any\s+)?(\d+)\s+tools?\b/i, + /\b(\d+)\s+tools?\b/i, + /\btools?\s+(\d+)\b/i, + /(?:tool|tools?|도구|툴)\s*(\d+)\s*(?:개|번)?/i, + /(\d+)\s*(?:개|번)?\s*(?:tool|tools?|도구|툴)/i, + /(?:도구|툴).{0,12}(?:써|사용|호출).{0,12}(\d+)\s*(?:개|번)?/i, + ]; + for (const pattern of patterns) { + const match = pattern.exec(text); + const count = Number(match?.[1]); + if (Number.isInteger(count) && count > 0 && count <= 50) return count; + } + return undefined; +} + +function cursorGenericToolUseHint(text: string): string { + const count = requestedCursorToolUseCount(text); + if (!count) return CURSOR_GENERIC_TOOL_USE_USER_HINT; + return [ + `This turn requests ${count} tool uses: emit exactly ${count} separate Codex shell bridge function calls/results (\`shell_command\` or \`exec_command\`).`, + `One shell-bridge call containing chained commands counts as 1 tool call, not ${count}.`, + `Prefer one parallel tool-call batch containing all ${count} independent shell-bridge calls before waiting for results.`, + CURSOR_GENERIC_TOOL_USE_USER_HINT, + ].join(" "); +} + +function activeTextMentionsGenericToolUseHint(text: string): boolean { + return text.includes("Codex native exec tool") + || text.includes("Codex Responses bridge exec tool") + || text.includes("generic tool-use/count demos"); +} + +export function shouldAppendCursorGenericToolUseHint( + tools: readonly Pick[] | undefined, + text: string, +): boolean { + const trimmed = text.trim(); + return trimmed.length > 0 + && cursorRequestHasShellAlias(tools) + && isGenericToolUseCountDemoPrompt(trimmed) + && !activeTextMentionsGenericToolUseHint(trimmed); +} + +export function appendCursorGenericToolUseHint( + tools: readonly Pick[] | undefined, + text: string, +): string { + if (!shouldAppendCursorGenericToolUseHint(tools, text)) return text; + return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${cursorGenericToolUseHint(text)}`; +} + +export function shouldUseNativeExecOnlyForGenericToolUse( + tools: readonly Pick[] | undefined, + text: string, +): boolean { + const trimmed = text.trim(); + if (trimmed.length === 0 || !cursorRequestHasExecutionPath(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false; + return !/\b(?:mcp|resource|resources|tool_search|plugin|plugins|app connector|github)\b/i.test(trimmed) + && !/(?:리소스|플러그인|깃허브|github)/i.test(trimmed); +} + +export function cursorToolsForActivePrompt>( + tools: readonly T[] | undefined, + activeText: string, + toolChoice?: OcxRequestOptions["toolChoice"], +): readonly T[] | undefined { + if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools; + const execTools = tools?.filter(isCursorExecutionPathTool); + const catalog = tools ?? []; + if (execTools?.length && !execTools.some(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog))) return tools; + return execTools && execTools.length > 0 ? execTools : tools; +} + +function quotedNames(names: readonly string[]): string { + return names.map(name => `\`${name}\``).join(", "); +} + +function advertisedCoversNeighbor(wireNames: readonly string[], neighbor: (typeof NEIGHBOR_AGENT_TOOL_NAMES)[number]): boolean { + const advertised = new Set(wireNames.map(name => clientSemanticToolNameFromCursorWire(name).toLowerCase())); + if (advertised.has(neighbor.toLowerCase())) return true; + return NEIGHBOR_AGENT_TOOL_ALIASES[neighbor].some(alias => advertised.has(alias.toLowerCase())); +} + +function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string[] { + return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertisedCoversNeighbor(wireNames, name)); +} + +function discoveryToolLabel(wireNames: readonly string[]): string | undefined { + const labels: string[] = []; + if (wireNames.includes(CODEX_TOOL_SEARCH_TOOL)) labels.push(`\`${CODEX_TOOL_SEARCH_TOOL}\``); + if (wireNames.some(name => name.startsWith("mcp__"))) labels.push("MCP"); + if (wireNames.some(name => /resource/i.test(name))) labels.push("resource discovery"); + return labels.length > 0 ? labels.join(", ") : undefined; +} + +export function buildCursorToolGuidanceSystemNote( + tools: readonly Pick[] | undefined, + toolChoice?: OcxRequestOptions["toolChoice"], +): string | undefined { + if (!tools?.length) return undefined; + const wireNames = [...new Set( + tools + .filter(tool => cursorToolAllowedByChoice(tool, toolChoice, tools)) + .map(tool => cursorToolWireName(tool)), + )]; + if (wireNames.length === 0) return undefined; + + const listedNames = quotedNames(wireNames); + const shellBridgeNames = wireNames.filter(isCodexShellBridgeToolName); + const hasBareExec = shellBridgeNames.length > 0; + const codeMode = cursorRequestUsesCodeMode(tools, toolChoice); + // Code mode describes how the freeform exec tool works; it does not suppress the rest of the + // catalog. A turn can advertise freeform `exec` AND ordinary top-level tools at once, and + // telling the model those are "not separate top-level tools" would make it refuse tools that + // are right there in its catalog. Name the ones that stay callable instead. + const codeModeOtherTopLevelNames = codeMode + ? wireNames.filter(name => name !== CODEX_UNIFIED_EXEC_TOOL && !isCodexShellBridgeToolName(name)) + : []; + const shellBridgeLabel = quotedNames(shellBridgeNames.length > 0 ? shellBridgeNames : [...CODEX_SHELL_BRIDGE_TOOL_NAMES]); + const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice); + const structuredEditNames = tools + ?.filter(tool => !tool.namespace && isCursorStructuredEditToolName(tool.name)) + .map(tool => tool.name) ?? []; + const discoveryTools = discoveryToolLabel(wireNames); + const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames); + // Host-shell-neutral: the Codex client executes bridge commands, and may differ from + // the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls. + const hostShellNote = hasBareExec + ? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`< 0 + ? `This turn does not expose neighboring-agent tool names ${quotedNames(unavailableNeighborNames)}; do not call or suggest them unless the catalog lists them.` + : undefined, + // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the + // model probes for a top-level shell tool that is not there. + codeMode + ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched.` + : undefined, + codeMode + ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers." + : undefined, + codeMode + ? "NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces." + : undefined, + hasBareExec + ? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.` + : undefined, + hasBareExec + ? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user." + : undefined, + hasBareExec + ? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.` + : undefined, + hasBareExec + ? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons." + : undefined, + hostShellNote, + "Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.", + hasBareExec + ? `For file read/search/listing, use ${shellBridgeLabel} when no more specific listed tool is available.` + : undefined, + hasApplyPatch + ? structuredEditNames.length > 0 + ? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.` + : "For file edits, use the `apply_patch` tool, not built-in file write/delete tools." + : undefined, + hasApplyPatch + ? "Creating or modifying file CONTENT via shell redirection (`>`, `>>`, `printf`/`echo` into a file, `cat < 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.` + : undefined, + ].filter((note): note is string => typeof note === "string"); + return notes.join(" "); +} diff --git a/src/adapters/cursor/tool-naming.ts b/src/adapters/cursor/tool-naming.ts new file mode 100644 index 0000000000..a36fb3a4f2 --- /dev/null +++ b/src/adapters/cursor/tool-naming.ts @@ -0,0 +1,252 @@ +import { namespacedToolName, toolChoiceAliases, type OcxRequestOptions, type OcxTool } from "../../types"; + +export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses"; +export const CODEX_EXEC_COMMAND_TOOL = "exec_command"; +export const CODEX_SHELL_COMMAND_TOOL = "shell_command"; +/** Codex Desktop unified-exec client tool. Companion of `wait`; not an `exec_command` schema alias. */ +export const CODEX_UNIFIED_EXEC_TOOL = "exec"; +export const CODEX_WAIT_TOOL = "wait"; +export const CODEX_APPLY_PATCH_TOOL = "apply_patch"; +export const CODEX_TOOL_SEARCH_TOOL = "tool_search"; +export const CURSOR_EDIT_FILE_TOOL = "edit_file"; +export const CURSOR_MULTI_EDIT_TOOL = "multi_edit"; +export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL] as const; +export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL; +export const CODEX_SHELL_BRIDGE_TOOL_NAMES = [CODEX_EXEC_COMMAND_TOOL, CODEX_SHELL_COMMAND_TOOL] as const; + +export function isCodexShellBridgeToolName(name: string): boolean { + return (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name); +} + +/** + * Direct key lookup, then shell_command/exec_command sibling aliases when the key is a bridge name. + * Used for catalog admission, schema normalize maps, and Responses name maps (#399). + */ +export function resolveShellBridgeAliasKey( + key: string, + lookup: (name: string) => T | undefined, +): T | undefined { + const direct = lookup(key); + if (direct !== undefined) return direct; + if (!isCodexShellBridgeToolName(key)) return undefined; + for (const alias of CODEX_SHELL_BRIDGE_TOOL_NAMES) { + if (alias === key) continue; + const hit = lookup(alias); + if (hit !== undefined) return hit; + } + return undefined; +} + +export function cursorToolChoiceAliases(tool: Pick): string[] { + const aliases = new Set(toolChoiceAliases(tool)); + if (isBareCodexShellBridgeTool(tool)) { + for (const alias of CODEX_SHELL_BRIDGE_TOOL_NAMES) aliases.add(alias); + } + return [...aliases]; +} + +function catalogHasBareCodexShellBridge( + catalog: readonly Pick[], +): boolean { + return catalog.some(isBareCodexShellBridgeTool); +} + +/** + * Catalog-aware tool_choice matching for Cursor. + * When a bare Codex shell bridge is in the catalog, raw `shell_command` / `exec_command` + * choices select only that bridge (never a namespaced remote with the same raw name). + * When no bare bridge exists, raw bridge names may select a namespaced tool by raw name. + * Explicit wire names (`mcp__remote__exec_command`) always match the namespaced tool. + */ +function cursorToolChoiceMatches( + tool: Pick, + choiceName: string, + catalog: readonly Pick[], +): boolean { + if (isCodexShellBridgeToolName(choiceName)) { + if (catalogHasBareCodexShellBridge(catalog)) { + return isBareCodexShellBridgeTool(tool); + } + return tool.name === choiceName || cursorToolWireName(tool) === choiceName; + } + if (tool.name === choiceName) return true; + if (cursorToolChoiceAliases(tool).includes(choiceName)) return true; + return cursorToolWireName(tool) === choiceName + && !catalog.some(candidate => candidate.name === choiceName); +} + +export function isBareCodexShellBridgeTool(tool: Pick): boolean { + return !tool.namespace && isCodexShellBridgeToolName(tool.name); +} + +function isCursorResponsesProvider(namespace: string | undefined): boolean { + return !namespace || namespace === OCX_RESPONSES_TOOL_PROVIDER; +} + +const CURSOR_EXECUTION_PATH_TOOL_NAMES = [ + CODEX_UNIFIED_EXEC_TOOL, + CODEX_EXEC_COMMAND_TOOL, + CODEX_SHELL_COMMAND_TOOL, +] as const; + +/** True for the Codex execution path that must survive Cursor transport truncation. */ +export function isCursorExecutionPathTool(tool: Pick): boolean { + return isCursorResponsesProvider(tool.namespace) + && (CURSOR_EXECUTION_PATH_TOOL_NAMES as readonly string[]).includes(tool.name); +} + +/** `wait` only resumes a yielded exec cell; it is unusable without an execution-path tool. */ +export function isCursorWaitTool(tool: Pick): boolean { + return isCursorResponsesProvider(tool.namespace) && tool.name === CODEX_WAIT_TOOL; +} + +/** + * True for Codex's unified-exec "code mode" tool: a freeform `exec` whose body is JavaScript + * evaluated in a V8 isolate, not a shell command string. + */ +export function isCursorCodeModeExecTool( + tool: Pick, +): boolean { + return isCursorResponsesProvider(tool.namespace) + && tool.name === CODEX_UNIFIED_EXEC_TOOL + && tool.freeform === true; +} + +/** + * Codex code mode advertises ONE freeform `exec` tool and no bare shell bridge. Shell, file + * edits, and MCP calls are reachable only as nested `tools.(...)` helpers described inside + * that tool's own description, so a flat catalog scan cannot see them. + * + * This matters because the shell-bridge guidance below is written for a flat catalog. Emitting + * "call \`exec_command\`" into a code-mode turn names a top-level tool that does not exist: the + * model calls it, gets nothing back, and burns turns rediscovering the real contract from error + * messages (empty output until \`text()\` is called, \`require is not defined\` because the isolate + * is not Node, \`apply_patch\` rejected because it too is only a nested helper here). + */ +export function cursorRequestUsesCodeMode( + tools: readonly Pick[] | undefined, + toolChoice?: OcxRequestOptions["toolChoice"], +): boolean { + const catalog = tools ?? []; + const visible = catalog.filter(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog)); + return visible.some(isCursorCodeModeExecTool) && !visible.some(isBareCodexShellBridgeTool); +} + +/** @deprecated Prefer isBareCodexShellBridgeTool; kept for older call sites/tests. */ +export function isBareCodexExecCommandTool(tool: Pick): boolean { + return isBareCodexShellBridgeTool(tool); +} + +export function cursorRequestHasShellAlias(tools: readonly Pick[] | undefined): boolean { + return tools?.some(isBareCodexExecCommandTool) ?? false; +} + +export function cursorRequestHasExecutionPath( + tools: readonly Pick[] | undefined, +): boolean { + return tools?.some(isCursorExecutionPathTool) ?? false; +} + +export function cursorRequestAdvertisesApplyPatch( + tools: readonly Pick[] | undefined, + toolChoice?: OcxRequestOptions["toolChoice"], +): boolean { + const catalog = tools ?? []; + return catalog.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && cursorToolAllowedByChoice(tool, toolChoice, catalog)); +} + +export function isCursorStructuredEditToolName(name: string): boolean { + return (CURSOR_STRUCTURED_EDIT_TOOLS as readonly string[]).includes(name); +} + +/** Internal provenance gate for synthetic edits after prompt filtering and catalog budgeting. */ +export function isCursorSyntheticStructuredEditTool( + tool: Pick, +): boolean { + return !tool.namespace && tool.cursorStructuredEdit === true && isCursorStructuredEditToolName(tool.name); +} + +const CURSOR_CLIENT_TOOL_WIRE_PREFIX = "ocx_client_"; +const CURSOR_PROXY_OWNED_BARE_TOOL_NAMES = new Set([ + CODEX_UNIFIED_EXEC_TOOL, + CODEX_WAIT_TOOL, + CODEX_EXEC_COMMAND_TOOL, + CODEX_SHELL_COMMAND_TOOL, + CODEX_APPLY_PATCH_TOOL, + CURSOR_EDIT_FILE_TOOL, + CURSOR_MULTI_EDIT_TOOL, + CODEX_TOOL_SEARCH_TOOL, +]); + +/** Avoid collisions with Cursor's private bare-tool namespace. */ +function isCursorBareClientToolWireAliased( + tool: Pick, +): boolean { + return !tool.namespace + && !CURSOR_PROXY_OWNED_BARE_TOOL_NAMES.has(tool.name); +} + +export function cursorToolWireName(tool: Pick): string { + if (isCursorBareClientToolWireAliased(tool)) { + return `${CURSOR_CLIENT_TOOL_WIRE_PREFIX}${tool.name}`; + } + return namespacedToolName(tool.namespace, tool.name); +} + +export function clientSemanticToolNameFromCursorWire(name: string): string { + return name.startsWith(CURSOR_CLIENT_TOOL_WIRE_PREFIX) + ? name.slice(CURSOR_CLIENT_TOOL_WIRE_PREFIX.length) + : name; +} + +/** + * Cursor's harness shows MCP tools to the model as `mcp__`; models + * sometimes call that display name verbatim instead of the advertised short name (live 20:41/21:00 + * sessions: `mcp_opencodex-responses_exec_command` / `mcp_opencodex-responses_shell_command`). + * Fold the display prefix back to the advertised wire name, and treat `shell_command` / + * `exec_command` as the same Codex shell bridge, so alias thrash does not become "tool not found". + */ +const CURSOR_MCP_DISPLAY_PREFIX = `mcp_${OCX_RESPONSES_TOOL_PROVIDER}_`; + +export function normalizeCursorWireName(name: string): string { + return name.startsWith(CURSOR_MCP_DISPLAY_PREFIX) ? name.slice(CURSOR_MCP_DISPLAY_PREFIX.length) : name; +} + +/** + * #2305: some models emit a TEXTUAL pseudo tool call ("[TOOL_CALL]name[ARGS]{...}") + * instead of a real frame, using Cursor's display alias as the name. Text-mode clients + * (Pi) parse that text and then cannot dispatch the undeclared display name. Rewrite the + * display alias to the advertised wire name ONLY inside the marker pair — prose that + * merely mentions the alias stays untouched, and the scope guard is the exact + * `mcp_${OCX_RESPONSES_TOOL_PROVIDER}_` prefix, never generic `mcp_`. + * Known limit (recorded in devlog 230): a marker split across two streaming deltas is + * not rewritten; tail-buffering is deferred until a live trace shows split markers. + */ +const CURSOR_TEXT_TOOL_MARKER = new RegExp( + String.raw`\[TOOL_CALL\](${CURSOR_MCP_DISPLAY_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\[\]]+)\[ARGS\]`, + "g", +); + +export function normalizeCursorTextToolMarkers(text: string): string { + if (!text.includes(CURSOR_MCP_DISPLAY_PREFIX)) return text; + return text.replace(CURSOR_TEXT_TOOL_MARKER, (_match, name: string) => `[TOOL_CALL]${normalizeCursorWireName(name)}[ARGS]`); +} + +export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?: ReadonlyMap): string { + const normalized = normalizeCursorWireName(name); + if (!cursorToolNameMap) return normalized; + return resolveShellBridgeAliasKey(normalized, alias => cursorToolNameMap.get(alias)) ?? normalized; +} + +export function cursorToolAllowedByChoice( + tool: Pick, + toolChoice: OcxRequestOptions["toolChoice"] | undefined, + catalog: readonly Pick[] = [tool], +): boolean { + if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true; + if (toolChoice === "none") return false; + if ("allowedTools" in toolChoice) { + return toolChoice.allowedTools.some(choiceName => cursorToolChoiceMatches(tool, choiceName, catalog)); + } + return cursorToolChoiceMatches(tool, toolChoice.name, catalog); +} diff --git a/src/adapters/cursor/tool-schemas.ts b/src/adapters/cursor/tool-schemas.ts new file mode 100644 index 0000000000..96ad3dfa63 --- /dev/null +++ b/src/adapters/cursor/tool-schemas.ts @@ -0,0 +1,195 @@ +import type { OcxTool } from "../../types"; +import { CODEX_SHELL_COMMAND_TOOL, isBareCodexExecCommandTool, isBareCodexShellBridgeTool, isCodexShellBridgeToolName } from "./tool-naming"; + +export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = { + type: "object", + properties: { + cmd: { type: "string", description: "Shell command to execute." }, + workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." }, + shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." }, + tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." }, + yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." }, + max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." }, + }, + required: ["cmd"], + additionalProperties: false, +} as const; + +/** + * Structured single-replacement schema advertised to Cursor models in addition to the freeform + * `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native + * Edit shape) but cannot produce Codex's freeform patch grammar, so every file edit attempt on the + * Cursor route produced malformed `apply_patch` payloads that the Codex client rejected locally + * (#1017). Calls to this tool are converted server-side into a valid apply_patch payload. + */ +export const CURSOR_EDIT_FILE_INPUT_SCHEMA = { + type: "object", + properties: { + file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." }, + old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." }, + new_string: { type: "string", description: "Replacement text. Empty removes the matched text." }, + }, + required: ["file_path", "old_string", "new_string"], + additionalProperties: false, +} as const; + +/** Structured multi-replacement schema; mirrors Cursor's native MultiEdit shape. */ +export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = { + type: "object", + properties: { + file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." }, + edits: { + type: "array", + items: { + type: "object", + properties: { + old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." }, + new_string: { type: "string", description: "Replacement text. Empty removes the matched text." }, + }, + required: ["old_string", "new_string"], + additionalProperties: false, + }, + description: "Ordered replacement edits for this file. Each old_string must match the current file content.", + }, + }, + required: ["file_path", "edits"], + additionalProperties: false, +} as const; + +/** + * Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call. + * Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate + * `command`. Keeping `cmd` out of this schema lets `normalizeArgKeys` rewrite `cmd` → `command`. + */ +export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = { + type: "object", + properties: { + command: { type: "string", description: "Shell command to execute." }, + workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." }, + shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." }, + tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." }, + yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." }, + max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." }, + max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." }, + }, + required: ["command"], +} as const; + + +/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */ +export function cursorToolInputSchema(tool: OcxTool): unknown { + return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {}); +} + +/** + * Schema used to normalize completed Cursor tool args back to Responses/Codex field names. + * Must NOT reuse `cursorToolInputSchema` for the shell bridge: advertising `cmd` while also + * treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399). + */ +export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown { + if (isBareCodexShellBridgeTool(tool)) { + return shellBridgeArgNormalizeSchema(tool); + } + return tool.parameters ?? {}; +} + +function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown { + const parameters = tool.parameters; + if (!parameters || typeof parameters !== "object") return CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA; + const base = parameters as Record; + const rawProps = base.properties && typeof base.properties === "object" + ? { ...(base.properties as Record) } + : {}; + const required = Array.isArray(base.required) ? [...base.required as unknown[]] : []; + const requiresCommand = required.includes("command") || "command" in rawProps; + const requiresCmd = required.includes("cmd") || "cmd" in rawProps; + const shouldRewriteCmdToCommand = tool.name === CODEX_SHELL_COMMAND_TOOL || requiresCommand; + + if (!shouldRewriteCmdToCommand && requiresCmd) { + return parameters; + } + + // Drop Cursor-preferred aliases so normalizeArgKeys can rewrite them to Responses keys. + delete rawProps.cmd; + const properties = { + ...CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties, + ...rawProps, + command: rawProps.command ?? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties.command, + }; + return { + ...base, + type: "object", + properties, + required: requiresCommand ? required : ["command"], + }; +} + +/** + * Required command payload keys for a shell bridge tool, derived from the advertised schema when present. + */ +export function shellBridgeRequiredCommandKeys( + toolName: string, + schema?: unknown, +): readonly ("cmd" | "command")[] { + if (schema && typeof schema === "object") { + const required = (schema as Record).required; + if (Array.isArray(required)) { + const keys = required.filter((key): key is "cmd" | "command" => key === "cmd" || key === "command"); + if (keys.length > 0) return keys; + } + } + return toolName === CODEX_SHELL_COMMAND_TOOL ? ["command"] : ["cmd"]; +} + +/** Normalize-schema defaults used when validating stateless synthetic shell-bridge calls. */ +export function defaultShellBridgeArgNormalizeSchema(toolName: string): unknown { + return toolName === CODEX_SHELL_COMMAND_TOOL + ? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA + : { + type: "object", + properties: CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties, + required: ["cmd"], + }; +} + +export function cursorShellBridgeDropError(toolName: string): string { + return `Cursor emitted ${toolName} without a non-empty command; the tool call was dropped.`; +} + +/** + * Extract a non-empty shell command from completed Cursor bridge args using the schema's required + * command key (`cmd` for bare exec_command, `command` for shell_command). + */ +export function nonEmptyShellBridgeCommandFromArgs( + finalArgs: string, + toolName: string, + schema?: unknown, +): string | undefined { + let parsed: unknown; + try { + parsed = finalArgs.length > 0 ? JSON.parse(finalArgs) : {}; + } catch { + return undefined; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined; + const record = parsed as Record; + const requiredKeys = shellBridgeRequiredCommandKeys(toolName, schema); + const candidateKeys = new Set<"cmd" | "command">([ + ...requiredKeys, + requiredKeys.includes("cmd") ? "command" : "cmd", + ]); + for (const key of candidateKeys) { + const value = record[key]; + if (typeof value === "string" && value.trim().length > 0) return value.trim(); + } + return undefined; +} + +export function cursorShellBridgeArgsValid( + finalArgs: string, + toolName: string, + schema?: unknown, +): boolean { + return !isCodexShellBridgeToolName(toolName) + || nonEmptyShellBridgeCommandFromArgs(finalArgs, toolName, schema) !== undefined; +} diff --git a/src/adapters/xai-schema-analysis.ts b/src/adapters/xai-schema-analysis.ts new file mode 100644 index 0000000000..5e19c4d87d --- /dev/null +++ b/src/adapters/xai-schema-analysis.ts @@ -0,0 +1,86 @@ +export function isSchemaObject(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function decodeJsonPointerToken(token: string): string { + return token.replace(/~1/g, "/").replace(/~0/g, "~"); +} + +/** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */ +export function lookupLocalJsonPointer(root: unknown, ref: string): unknown { + if (ref === "#" || ref === "#/") return root; + if (!ref.startsWith("#/")) return undefined; + let current: unknown = root; + for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) { + if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined; + current = current[token]; + } + return current; +} + +/** Values a schema pins through `const`/`enum`, or undefined when it pins none. */ +function xaiLiteralValues(schema: unknown): unknown[] | undefined { + if (!isSchemaObject(schema)) return undefined; + if (Object.hasOwn(schema, "const")) return [schema.const]; + if (Array.isArray(schema.enum)) return schema.enum; + return undefined; +} + +/** JSON type name for a literal, so it can be compared against a `type` keyword. */ +function xaiJsonTypeOf(value: unknown): string { + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + if (typeof value === "string") return "string"; + if (typeof value === "boolean") return "boolean"; + if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number"; + return "object"; +} + +/** Types a schema declares, or undefined when it constrains none. */ +function xaiDeclaredTypes(schema: unknown): Set | undefined { + if (!isSchemaObject(schema)) return undefined; + const type = schema.type; + if (typeof type === "string") return new Set([type]); + if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]); + return undefined; +} + +/** `integer` is a subset of `number`, so those two names overlap rather than exclude. */ +function xaiTypesOverlap(left: string, right: string): boolean { + if (left === right) return true; + return (left === "integer" && right === "number") || (left === "number" && right === "integer"); +} + +/** + * Conservative mutual-exclusion test: true only when no instance can satisfy both schemas. + * Proof comes from disjoint literal sets or disjoint declared types; anything it cannot prove + * is reported as overlapping so the caller refuses the merge instead of widening the schema. + */ +function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean { + const leftValues = xaiLiteralValues(left); + const rightValues = xaiLiteralValues(right); + if (leftValues && rightValues) { + const seen = new Set(rightValues.map(value => JSON.stringify(value))); + return leftValues.every(value => !seen.has(JSON.stringify(value))); + } + const leftTypes = xaiDeclaredTypes(left); + const rightTypes = xaiDeclaredTypes(right); + const literalsExcludedByTypes = (values: unknown[], types: Set): boolean => + values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type))); + if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes); + if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes); + if (leftTypes && rightTypes) { + return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType))); + } + return false; +} + +/** Every pair provably disjoint, so a union over them accepts each instance exactly once. */ +export function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean { + for (let i = 0; i < schemas.length; i += 1) { + for (let j = i + 1; j < schemas.length; j += 1) { + if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false; + } + } + return true; +} diff --git a/src/adapters/xai-tool-schema.ts b/src/adapters/xai-tool-schema.ts index b805d767ea..e853296526 100644 --- a/src/adapters/xai-tool-schema.ts +++ b/src/adapters/xai-tool-schema.ts @@ -1,8 +1,6 @@ import type { OcxProviderConfig } from "../types"; - -function isSchemaObject(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} +export { lookupLocalJsonPointer } from "./xai-schema-analysis"; +import { isSchemaObject, lookupLocalJsonPointer, xaiSchemasArePairwiseDisjoint } from "./xai-schema-analysis"; export function isXaiSchemaTarget(provider: Pick): boolean { try { @@ -58,22 +56,6 @@ function createXaiSchemaBudget(): XaiSchemaBudget { return { remainingNodes: XAI_MAX_SCHEMA_NODES, remainingVariants: XAI_MAX_ROOT_VARIANTS }; } -function decodeJsonPointerToken(token: string): string { - return token.replace(/~1/g, "/").replace(/~0/g, "~"); -} - -/** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */ -export function lookupLocalJsonPointer(root: unknown, ref: string): unknown { - if (ref === "#" || ref === "#/") return root; - if (!ref.startsWith("#/")) return undefined; - let current: unknown = root; - for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) { - if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined; - current = current[token]; - } - return current; -} - /** Resolve local `#/` `$ref`s. Unresolvable, cyclic, or over-budget refs return undefined. */ function resolveXaiSchemaRefs( schema: unknown, @@ -168,73 +150,6 @@ function xaiRequiredSetsMatch(variants: Record[]): boolean { return serialized.every(value => value === serialized[0]); } -/** Values a schema pins through `const`/`enum`, or undefined when it pins none. */ -function xaiLiteralValues(schema: unknown): unknown[] | undefined { - if (!isSchemaObject(schema)) return undefined; - if (Object.hasOwn(schema, "const")) return [schema.const]; - if (Array.isArray(schema.enum)) return schema.enum; - return undefined; -} - -/** JSON type name for a literal, so it can be compared against a `type` keyword. */ -function xaiJsonTypeOf(value: unknown): string { - if (value === null) return "null"; - if (Array.isArray(value)) return "array"; - if (typeof value === "string") return "string"; - if (typeof value === "boolean") return "boolean"; - if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number"; - return "object"; -} - -/** Types a schema declares, or undefined when it constrains none. */ -function xaiDeclaredTypes(schema: unknown): Set | undefined { - if (!isSchemaObject(schema)) return undefined; - const type = schema.type; - if (typeof type === "string") return new Set([type]); - if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]); - return undefined; -} - -/** `integer` is a subset of `number`, so those two names overlap rather than exclude. */ -function xaiTypesOverlap(left: string, right: string): boolean { - if (left === right) return true; - return (left === "integer" && right === "number") || (left === "number" && right === "integer"); -} - -/** - * Conservative mutual-exclusion test: true only when no instance can satisfy both schemas. - * Proof comes from disjoint literal sets or disjoint declared types; anything it cannot prove - * is reported as overlapping so the caller refuses the merge instead of widening the schema. - */ -function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean { - const leftValues = xaiLiteralValues(left); - const rightValues = xaiLiteralValues(right); - if (leftValues && rightValues) { - const seen = new Set(rightValues.map(value => JSON.stringify(value))); - return leftValues.every(value => !seen.has(JSON.stringify(value))); - } - const leftTypes = xaiDeclaredTypes(left); - const rightTypes = xaiDeclaredTypes(right); - const literalsExcludedByTypes = (values: unknown[], types: Set): boolean => - values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type))); - if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes); - if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes); - if (leftTypes && rightTypes) { - return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType))); - } - return false; -} - -/** Every pair provably disjoint, so a union over them accepts each instance exactly once. */ -function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean { - for (let i = 0; i < schemas.length; i += 1) { - for (let j = i + 1; j < schemas.length; j += 1) { - if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false; - } - } - return true; -} - /** Deduplicate schemas by serialized shape, preserving first-seen order. */ function uniqueXaiSchemas(values: unknown[]): unknown[] { const unique: unknown[] = []; diff --git a/src/claude/inbound-content-options.ts b/src/claude/inbound-content-options.ts new file mode 100644 index 0000000000..f6b762aa83 --- /dev/null +++ b/src/claude/inbound-content-options.ts @@ -0,0 +1,60 @@ +import { isClaudeWebSearchToolName } from "./outbound"; +import { AnthropicRequestError, isRec, type Rec } from "./inbound-records"; + +export function systemToInstructions(system: unknown): string | undefined { + if (typeof system === "string") return system.length > 0 ? system : undefined; + if (Array.isArray(system)) { + const parts: string[] = []; + for (const block of system) { + if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text); + } + return parts.length > 0 ? parts.join("\n\n") : undefined; + } + return undefined; +} + +export function toolsToResponses(tools: unknown): Rec[] | undefined { + if (!Array.isArray(tools) || tools.length === 0) return undefined; + const out: Rec[] = []; + for (const raw of tools) { + if (!isRec(raw)) continue; + const type = typeof raw.type === "string" ? raw.type : ""; + if (type.startsWith("web_search")) { + out.push({ type: "web_search" }); // hosted sidecar path + continue; + } + if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) { + out.push({ + type: "function", + name: raw.name, + ...(typeof raw.description === "string" ? { description: raw.description } : {}), + parameters: raw.input_schema as Record, + }); + continue; + } + // Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop. + } + return out.length > 0 ? out : undefined; +} + +export function toolChoiceToResponses(choice: unknown, body: Rec): void { + if (!isRec(choice)) return; + if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false; + switch (choice.type) { + case "auto": body.tool_choice = "auto"; break; + case "none": body.tool_choice = "none"; break; + case "any": body.tool_choice = "required"; break; + case "tool": + if (typeof choice.name !== "string" || choice.name.length === 0) { + throw new AnthropicRequestError("tool_choice.tool requires a name"); + } + // Anthropic represents hosted WebSearch as a named tool choice, while + // Responses requires the choice type to match the hosted declaration. + // Preserve forced-tool intent rather than weakening it to `auto`. + body.tool_choice = isClaudeWebSearchToolName(choice.name) + ? { type: "web_search" } + : { type: "function", name: choice.name }; + break; + default: break; + } +} diff --git a/src/claude/inbound-model-options.ts b/src/claude/inbound-model-options.ts new file mode 100644 index 0000000000..e6ff9dbce7 --- /dev/null +++ b/src/claude/inbound-model-options.ts @@ -0,0 +1,142 @@ +import type { OcxClaudeCodeConfig } from "../types"; +import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema"; +import { resolveAlias } from "./alias"; +import { stripOneMillionMarker } from "./context-windows"; +import { resolveDesktop3pAlias } from "./desktop-3p"; +import { isRec, type Rec } from "./inbound-records"; + +function isClaudeClassifierModel(model: string): boolean { + const stripped = model.replace(/-\d{8}$/, ""); + return /^claude-opus-[45]/.test(stripped); +} + +/** + * Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697). + * + * Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered + * `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so + * routing them crosses no boundary the operator did not ask for. + * + * Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the + * injected/default config slot, not the provider the live session actually selected, so it goes + * stale the moment the user changes the model picker -- and acting on it would silently move a + * classifier turn onto a provider with its own privacy and billing consequences. Live session + * affinity needs the request/session state this function does not have; it is tracked as + * follow-up work rather than approximated from static config. + */ +function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined { + const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : ""; + if (explicit.length > 0) return explicit; + if (Array.isArray(cc?.classifierFallbacks)) { + for (const candidate of cc.classifierFallbacks) { + if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim(); + } + } + return undefined; +} + +/** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */ +export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string { + // Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a + // leaking build must not break alias decode (devlog 138 — the 1M signal is the + // anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i. + model = stripOneMillionMarker(model); + const aliased = resolveAlias(model); + if (aliased) return aliased; + // Desktop 3P aliases: claude-opus-4-{code} → provider/model route key + const desktop3p = resolveDesktop3pAlias(model); + if (desktop3p) { + // Native pseudo-provider returns bare slug; routed returns provider/model + const sep = desktop3p.indexOf("/"); + if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1); + return desktop3p; + } + const map = cc?.modelMap ?? {}; + const exact = map[model]; + if (typeof exact === "string" && exact.length > 0) return exact; + const stripped = model.replace(/-\d{8}$/, ""); + const dateless = map[stripped]; + if (typeof dateless === "string" && dateless.length > 0) return dateless; + + // Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as + // `claude-opus-5` carry no provider, so without this they fall through to defaultProvider -- + // which may not speak Anthropic at all. Only an operator-declared target is used. + if (isClaudeClassifierModel(model)) { + const configured = configuredClassifierRoute(cc); + if (configured) return configured; + } + return model; +} + +/** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */ +export function effortForThinkingBudget(budget: number): string { + if (budget <= 4096) return "low"; + if (budget <= 16384) return "medium"; + return "high"; +} + +/** + * Adaptive-thinking wire (devlog 080): Claude Code /effort sends + * `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local + * capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it + * is a known Responses effort; unknown strings are dropped so downstream defaults win. + */ +const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]); +export function effortFromOutputConfig(outputConfig: unknown): string | undefined { + if (!isRec(outputConfig)) return undefined; + const effort = outputConfig.effort; + return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined; +} + +export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined { + if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined; + const format = outputConfig.format; + if ( + format.type !== "json_schema" + || !isRec(format.schema) + || !isAnthropicOutputSchema(format.schema) + ) return undefined; + return { type: "json_schema", name: "response", schema: format.schema }; +} + +/** + * ocx-route directive (devlog 072): injected agent-definition bodies carry + * `` because Claude Code 2.1.207 ignores custom + * gateway ids in agent frontmatter (live-proven fallback to sonnet). The body + * rides the subagent's system prompt, so the proxy re-routes here. Only the + * FIRST directive wins; the scan is bounded to the system field. + */ +const OCX_ROUTE_RE = //; +const OCX_EFFORT_RE = //; + +function systemText(body: unknown): string | null { + if (!isRec(body)) return null; + const system = body.system; + if (typeof system === "string") return system || null; + if (!Array.isArray(system)) return null; + const text = system + .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string") + .map(b => b.text as string) + .join("\n"); + return text || null; +} + +export function extractOcxRouteDirective(body: unknown): string | null { + const text = systemText(body); + if (!text) return null; + const match = OCX_ROUTE_RE.exec(text); + return match ? match[1]! : null; +} + +/** + * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and + * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the + * exact generated-agent setting through the same trusted system-body channel as + * ocx-route so the inbound translator can restore `output_config.effort`. + */ +export function extractOcxEffortDirective(body: unknown): NonNullable | null { + const text = systemText(body); + if (!text) return null; + const match = OCX_EFFORT_RE.exec(text); + return match ? match[1] as NonNullable : null; +} diff --git a/src/claude/inbound-records.ts b/src/claude/inbound-records.ts new file mode 100644 index 0000000000..a39dd88c41 --- /dev/null +++ b/src/claude/inbound-records.ts @@ -0,0 +1,7 @@ +export class AnthropicRequestError extends Error {} + +export type Rec = Record; + +export function isRec(v: unknown): v is Rec { + return !!v && typeof v === "object" && !Array.isArray(v); +} diff --git a/src/claude/inbound.ts b/src/claude/inbound.ts index de8f474341..924f2b23ec 100644 --- a/src/claude/inbound.ts +++ b/src/claude/inbound.ts @@ -10,126 +10,15 @@ * - top_k is accepted and silently dropped (no Responses equivalent, CCR parity). */ import type { OcxClaudeCodeConfig } from "../types"; -import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema"; -import { resolveAlias } from "./alias"; -import { stripOneMillionMarker } from "./context-windows"; -import { resolveDesktop3pAlias } from "./desktop-3p"; -import { isClaudeWebSearchToolName } from "./outbound"; import { createHash } from "node:crypto"; -export class AnthropicRequestError extends Error {} +export { AnthropicRequestError } from "./inbound-records"; +export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, extractOcxRouteDirective, extractOcxEffortDirective } from "./inbound-model-options"; +import { AnthropicRequestError, isRec, type Rec } from "./inbound-records"; +import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options"; +import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options"; -type Rec = Record; -function isRec(v: unknown): v is Rec { - return !!v && typeof v === "object" && !Array.isArray(v); -} - -function isClaudeClassifierModel(model: string): boolean { - const stripped = model.replace(/-\d{8}$/, ""); - return /^claude-opus-[45]/.test(stripped); -} - -/** - * Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697). - * - * Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered - * `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so - * routing them crosses no boundary the operator did not ask for. - * - * Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the - * injected/default config slot, not the provider the live session actually selected, so it goes - * stale the moment the user changes the model picker -- and acting on it would silently move a - * classifier turn onto a provider with its own privacy and billing consequences. Live session - * affinity needs the request/session state this function does not have; it is tracked as - * follow-up work rather than approximated from static config. - */ -function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined { - const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : ""; - if (explicit.length > 0) return explicit; - if (Array.isArray(cc?.classifierFallbacks)) { - for (const candidate of cc.classifierFallbacks) { - if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim(); - } - } - return undefined; -} - -/** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */ -export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string { - // Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a - // leaking build must not break alias decode (devlog 138 — the 1M signal is the - // anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i. - model = stripOneMillionMarker(model); - const aliased = resolveAlias(model); - if (aliased) return aliased; - // Desktop 3P aliases: claude-opus-4-{code} → provider/model route key - const desktop3p = resolveDesktop3pAlias(model); - if (desktop3p) { - // Native pseudo-provider returns bare slug; routed returns provider/model - const sep = desktop3p.indexOf("/"); - if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1); - return desktop3p; - } - const map = cc?.modelMap ?? {}; - const exact = map[model]; - if (typeof exact === "string" && exact.length > 0) return exact; - const stripped = model.replace(/-\d{8}$/, ""); - const dateless = map[stripped]; - if (typeof dateless === "string" && dateless.length > 0) return dateless; - - // Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as - // `claude-opus-5` carry no provider, so without this they fall through to defaultProvider -- - // which may not speak Anthropic at all. Only an operator-declared target is used. - if (isClaudeClassifierModel(model)) { - const configured = configuredClassifierRoute(cc); - if (configured) return configured; - } - return model; -} - -/** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */ -export function effortForThinkingBudget(budget: number): string { - if (budget <= 4096) return "low"; - if (budget <= 16384) return "medium"; - return "high"; -} - -/** - * Adaptive-thinking wire (devlog 080): Claude Code /effort sends - * `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local - * capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it - * is a known Responses effort; unknown strings are dropped so downstream defaults win. - */ -const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]); -export function effortFromOutputConfig(outputConfig: unknown): string | undefined { - if (!isRec(outputConfig)) return undefined; - const effort = outputConfig.effort; - return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined; -} - -function formatFromOutputConfig(outputConfig: unknown): Rec | undefined { - if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined; - const format = outputConfig.format; - if ( - format.type !== "json_schema" - || !isRec(format.schema) - || !isAnthropicOutputSchema(format.schema) - ) return undefined; - return { type: "json_schema", name: "response", schema: format.schema }; -} - -function systemToInstructions(system: unknown): string | undefined { - if (typeof system === "string") return system.length > 0 ? system : undefined; - if (Array.isArray(system)) { - const parts: string[] = []; - for (const block of system) { - if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text); - } - return parts.length > 0 ? parts.join("\n\n") : undefined; - } - return undefined; -} function imageBlockToInputImage(block: Rec): Rec | null { const source = block.source; @@ -199,47 +88,6 @@ export function effectiveBlockedSkillNames(cc?: Pick name.length > 0))]; } -/** - * ocx-route directive (devlog 072): injected agent-definition bodies carry - * `` because Claude Code 2.1.207 ignores custom - * gateway ids in agent frontmatter (live-proven fallback to sonnet). The body - * rides the subagent's system prompt, so the proxy re-routes here. Only the - * FIRST directive wins; the scan is bounded to the system field. - */ -const OCX_ROUTE_RE = //; -const OCX_EFFORT_RE = //; - -function systemText(body: unknown): string | null { - if (!isRec(body)) return null; - const system = body.system; - if (typeof system === "string") return system || null; - if (!Array.isArray(system)) return null; - const text = system - .filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string") - .map(b => b.text as string) - .join("\n"); - return text || null; -} - -export function extractOcxRouteDirective(body: unknown): string | null { - const text = systemText(body); - if (!text) return null; - const match = OCX_ROUTE_RE.exec(text); - return match ? match[1]! : null; -} - -/** - * Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and - * `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the - * exact generated-agent setting through the same trusted system-body channel as - * ocx-route so the inbound translator can restore `output_config.effort`. - */ -export function extractOcxEffortDirective(body: unknown): NonNullable | null { - const text = systemText(body); - if (!text) return null; - const match = OCX_EFFORT_RE.exec(text); - return match ? match[1] as NonNullable : null; -} /** Injected-skill payloads below this size are never stubbed (not worth it). */ const SKILL_ELISION_MIN_CHARS = 10_000; @@ -396,51 +244,6 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void { flush(); } -function toolsToResponses(tools: unknown): Rec[] | undefined { - if (!Array.isArray(tools) || tools.length === 0) return undefined; - const out: Rec[] = []; - for (const raw of tools) { - if (!isRec(raw)) continue; - const type = typeof raw.type === "string" ? raw.type : ""; - if (type.startsWith("web_search")) { - out.push({ type: "web_search" }); // hosted sidecar path - continue; - } - if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) { - out.push({ - type: "function", - name: raw.name, - ...(typeof raw.description === "string" ? { description: raw.description } : {}), - parameters: raw.input_schema as Record, - }); - continue; - } - // Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop. - } - return out.length > 0 ? out : undefined; -} - -function toolChoiceToResponses(choice: unknown, body: Rec): void { - if (!isRec(choice)) return; - if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false; - switch (choice.type) { - case "auto": body.tool_choice = "auto"; break; - case "none": body.tool_choice = "none"; break; - case "any": body.tool_choice = "required"; break; - case "tool": - if (typeof choice.name !== "string" || choice.name.length === 0) { - throw new AnthropicRequestError("tool_choice.tool requires a name"); - } - // Anthropic represents hosted WebSearch as a named tool choice, while - // Responses requires the choice type to match the hosted declaration. - // Preserve forced-tool intent rather than weakening it to `auto`. - body.tool_choice = isClaudeWebSearchToolName(choice.name) - ? { type: "web_search" } - : { type: "function", name: choice.name }; - break; - default: break; - } -} /** Recursive canonical JSON (keys sorted at every depth) — stable cache-cohort input. */ function canonicalJson(value: unknown): string { diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index ea1b623f31..7aaf556f2b 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -24,56 +24,28 @@ import { existsSync, readFileSync } from "node:fs"; import { isAbsolute, join, resolve } from "node:path"; import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject"; import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize"; -import { providerCodexAccountMode } from "../providers/registry"; -import { canonicalizeReasoningEfforts, sanitizeCodexReasoningEfforts } from "../reasoning-effort"; +import { canonicalizeReasoningEfforts } from "../reasoning-effort"; import { probeHostname } from "../server/proxy-liveness"; import type { OcxConfig } from "../types"; -export type { ConfigFormat }; +export type { ConfigFormat } from "../integrations/serialize"; +export type { ManagedFragment, ManagedContribution, BuildContribution, OpencodeLaunchEnv, OpencodeCatalogModel, ExportModel, ExportContext, ExportClientId, ExportClientSpec, PiModelEntry } from "./config-export/contracts"; +export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, HERMES_API_KEY_ENV, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV, OPENCLAW_API_KEY_ENV_REF, LOOPBACK_API_KEY_PLACEHOLDER, GAJAE_API_KEY_ENV, SCHEMA_REQUIRED_OUTPUT_BUDGET, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG } from "./config-export/constants"; +export { normalizeExportModels } from "./config-export/model-metadata"; +export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp"; +export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode"; +export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh"; +export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode"; -/** - * One entry opencodex owns inside a client's config: the JSON path to it and - * the value we put there. - * - * A path list rather than a single provider key because ownership is not - * always one entry — Kimi owns its provider block AND one model entry per - * model, and a writer that only knew about the provider would strand the rest - * (devlog 260802 006 §2). - */ -export interface ManagedFragment { - path: readonly string[]; - value: unknown; -} +import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts"; +import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, GAJAE_API_KEY_ENV, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants"; +import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata"; +import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp"; +import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh"; +import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode"; +import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode"; -/** Everything opencodex contributes to one client's config, as one unit. */ -export interface ManagedContribution { - clientId: ExportClientId; - fragments: readonly ManagedFragment[]; -} - -export type BuildContribution = (ctx: ExportContext) => ManagedContribution; - -export interface OpencodeLaunchEnv { - [key: string]: string | undefined; -} -/** Visible catalog entry keyed by the proxy's canonical namespaced selector. */ -export interface OpencodeCatalogModel { - namespaced: string; - native?: boolean; - provider?: string; - id?: string; - contextWindow?: number; - displayName?: string; - /** Declared effort ladder. Exported as opencode model variants where the client reads them. */ - reasoningEfforts?: readonly string[]; - /** - * Declared default effort. Carried so every client export reads one deduped, visibility- - * filtered ladder per model. The opencode serializer deliberately does NOT turn it into a - * model-level setting — see {@link opencodeEffortVariants} for why. - */ - defaultReasoningEffort?: string; -} export interface OpencodeModelEntry { name: string; @@ -137,11 +109,6 @@ export interface OpencodeGeneratedConfig { providers: Record; } -/** Provider key owned by this project; the only key any exporter ever emits. */ -export const OPENCODE_PROVIDER_ID = "opencodex"; - -export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json"; - /** * The proxy speaks the OpenAI-compatible shape at /v1, which opencode reaches through * the AI SDK's openai-compatible package (the same wiring users hand-write today). @@ -163,67 +130,6 @@ const OPENCODE_V2_PROVIDER_PACKAGE = "@opencode-ai/ai/providers/openai-compatibl /** Display name for the provider block, identical in both generations. */ const OPENCODE_PROVIDER_NAME = "OpenCodex"; -/** - * Env var carrying the proxy admission key to opencode. The config only ever holds the - * `{env:...}` reference, so the secret never lands on disk. opencode substitutes it at - * load time. - */ -export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY"; - -/** Env reference shared by apiKey and the dedicated proxy admission header. */ -export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`; - -/** - * Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays - * in the environment exactly as it does for OpenCode. - */ -export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY"; -export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`; - -/** OpenClaw interpolates `${UPPERCASE_VAR}` and fails closed when it is unset. */ -export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY"; -export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`; - -/** - * Placeholder credential for loopback-only clients (Kimi, Pi). A loopback - * bind needs no real admission key, so we emit the same placeholder the Grok - * managed block uses rather than a user secret. Pi resolves `apiKey` before - * building its model list and hides the provider when an env reference is unset. - */ -export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback"; - -/** - * Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey` - * falls back to treating the literal text as the token when the variable is - * unset, which would silently ship a bogus credential — so we never emit it. - */ -export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY"; - -/** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */ -const PI_API_DIALECT = "openai-completions"; - -/** - * opencode's config schema rejects a `limit` block that carries `context` without - * `output`, but CatalogModel has no authoritative per-model output field. Dropping - * `limit` entirely would also throw away the authoritative context window we DO have, - * so the block is emitted with this budget standing in for the missing half. - * - * The value matches REASONING_MAX_TOKENS_CEILING in src/adapters/anthropic.ts — the - * project's existing "safe ceiling across current models" figure. It is a ceiling for - * schema validity, NOT a claim about any specific model's true maximum, and it is - * clamped to the context window so a small-context model can never be emitted with - * output > context. Pi's `maxTokens` uses the same stand-in and the same clamp. - */ -export const SCHEMA_REQUIRED_OUTPUT_BUDGET = 32_000; - -/** Deterministic loopback default for exported provider-block helpers in tests. */ -export const OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG: OcxConfig = { - port: 10100, - hostname: "127.0.0.1", - defaultProvider: "mock", - providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1" } }, -} as OcxConfig; - /** * Resolve the user's global opencode config path. opencode uses the XDG layout on every * platform (including Windows, where it is %USERPROFILE%\.config\opencode). @@ -626,184 +532,6 @@ export function asideConfigPath(env: OpencodeLaunchEnv = process.env, home: stri return join(asideAccountDir(env, home), "models.json"); } -/** - * One proxy-routed model destined for a client config. Deliberately narrower than - * `CatalogModel` so a serializer cannot reach for a field that does not survive the - * `/api/models` boundary. - */ -export interface ExportModel { - /** Canonical proxy selector: `provider/id`, or bare slug for native. */ - namespaced: string; - provider: string; - id: string; - /** Native OpenAI entry. Read by the shared label rule. */ - native?: boolean; - displayName?: string; - contextWindow?: number; - inputModalities?: string[]; - /** Optional effort ladder exported only to clients that support it. */ - reasoningEfforts?: string[]; - defaultReasoningEffort?: string; -} - -export interface ExportContext { - /** `http://host:port/v1` — the OpenAI-compatible surface the client dials. */ - baseUrl: string; - models: readonly ExportModel[]; - /** - * Live proxy config. Only the OpenCode path reads it: a non-loopback bind moves - * admission from `apiKey` to the `x-opencodex-api-key` header. - */ - config?: OcxConfig; -} - -export type ExportClientId = - | "opencode" - | "pi" - | "omp" - | "hermes" - | "openclaw" - | "kimi" - | "gajae" - | "dsh" - | "mcode" - | "zcode" - | "prime" - | "aside"; - -export interface ExportClientSpec { - id: ExportClientId; - /** Download filename; matches the destination file's own name (003 §5). */ - filename: string; - /** Canonical destination for humans. Never written to. */ - destination: (env: NodeJS.ProcessEnv) => string; - /** Env var the config references; the value is never serialized. */ - apiKeyEnv: string; - /** Shell line the user runs before launching the client. */ - exportHint: string; - build: (ctx: ExportContext) => unknown; - /** - * Text format of the client's config file. `filename` already carries the - * extension; this drives serialization and the download media type so no - * consumer has to infer either from the name. - */ - format: ConfigFormat; - /** - * Count models in THIS client's document shape. Required so a new client - * cannot be added without teaching the summarizer about it — the old - * "anything that is not OpenCode must be Pi" branch was a latent bug. - */ - summarize: (document: unknown) => { modelCount: number; modelsWithoutLimits: number }; - /** - * The fragments opencodex owns inside this client's config. Only the builder - * knows where a client keeps our entries, so ownership paths originate here - * rather than being re-derived by the writer. - */ - buildContribution: BuildContribution; - /** - * True when the generated integration deliberately supports loopback only. - * - * `/v1/chat/completions` rejects bearer credentials and requires the - * dedicated `x-opencodex-api-key` header (AUTH_MATRIX in - * src/server/auth-cors.ts). If this exporter cannot safely emit that header, - * it refuses a remote bind rather than generating a config that 401s. Same - * reasoning as the Grok managed block's non-loopback refusal. - */ - loopbackOnly: boolean; -} - -/** - * Authoritative context window, or undefined. Never guesses: a missing, non-finite, or - * non-positive value means the serializer omits every context-derived field. - */ -function authoritativeContextWindow(contextWindow: number | undefined): number | undefined { - if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) { - const integer = Math.floor(contextWindow); - return integer > 0 ? integer : undefined; - } - return undefined; -} - -/** Schema-required output budget for a known context window. */ -function outputBudgetFor(context: number): number { - return Math.min(SCHEMA_REQUIRED_OUTPUT_BUDGET, context); -} - -/** - * Modalities a given client's schema will actually accept. - * - * Our internal vocabulary is `text | image | audio` (ALLOWED_INPUT_MODALITIES in - * src/server/management/model-routes.ts). Pi and Gajae accept only - * `text | image`, and both reject the WHOLE config file over one out-of-enum - * value — Gajae reports `/providers/opencodex/models/N/input/2: Invalid option` - * and falls back to its built-in list, Pi returns an empty model config. So a - * single `audio` model takes every routed model down with it. That is not - * hypothetical: zenmux/meta-muse-spark-1.1 advertises audio and did exactly - * this. It is also the same defect the Codex catalog had with `video`, where - * the app showed zero apps (tests/codex-integration/catalog-input-modality-enum.test.ts). - * - * UNKNOWN and INCOMPATIBLE are different inputs, and the Codex fix could - * conflate them safely only because its enum is wider. A model with nothing - * declared is unknown, and `text` is the honest floor — every routed model takes - * prompts. A model declaring `["audio"]` and nothing else is incompatible with a - * text|image client, and rewriting it to `["text"]` would advertise a capability - * it does not have. That input is reachable three ways: `ocx models add - * --modalities audio`, `/api/custom-models`, and provider discovery. - * - * So unknown falls back to text and incompatible returns null, which drops the - * row. Omitting a model costs the user a line in a picker; fabricating `text` - * costs them a model that fails at call time with no explanation. - * - * Deliberately NOT applied in `ExportModel` construction: the management and CLI - * boundaries carry catalog modalities verbatim on purpose, and stripping `audio` - * globally would destroy valid metadata before the destination is known. - */ -const CLIENT_INPUT_MODALITIES: Record<"pi" | "gajae", ReadonlySet> = { - pi: new Set(["text", "image"]), - gajae: new Set(["text", "image"]), -}; - -/** `null` means the model cannot be represented for this client — drop the row. */ -function inputModalitiesForClient( - client: "pi" | "gajae", - modalities: readonly string[] | undefined, -): string[] | null { - const declared = modalities ?? []; - if (declared.length === 0) return ["text"]; - const accepted = CLIENT_INPUT_MODALITIES[client]; - const kept: string[] = []; - for (const value of declared) { - if (accepted.has(value) && !kept.includes(value)) kept.push(value); - } - return kept.length > 0 ? kept : null; -} - -/** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */ -function dshInputModalities(modalities: readonly string[] | undefined): string[] | null { - const declared = modalities ?? []; - if (declared.length === 0) return ["text"]; - const kept: string[] = []; - for (const value of declared) { - if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value); - } - if (kept.length > 0) return kept; - return declared.every(value => value === "audio") ? null : ["text"]; -} - -/** - * Label shared by every client: `" ()"`. The - * provider suffix is what makes two same-named models from different upstreams - * distinguishable in a client's model picker. - */ -function exportModelLabel(model: OpencodeCatalogModel): string { - const providerLabel = model.native ? "native" : (model.provider ?? "routed"); - const id = model.id ?? model.namespaced; - if (model.displayName && model.displayName.length > 0) { - return `${model.displayName} (${providerLabel})`; - } - return `${id} (${providerLabel})`; -} - /** Endpoint plus admission, identical for the V1 `options` and V2 `settings` field. */ function opencodeProviderConnection(baseURL: string, config: OcxConfig): OpencodeProviderConnection { const options: OpencodeProviderConnection = { baseURL }; @@ -925,23 +653,6 @@ export function buildOpencodeProviderBlockFromCatalog( return opencodeProviderBlock(opencodeProxyBaseUrl(port, hostname), catalogModels, config); } -/** - * Shared precondition for every serializer: drop duplicate `namespaced` (first wins, - * native rows lead `/api/models`) and sort by `namespaced` so two calls with the same - * models produce identical bytes. Stability matters because the GUI shows a diffable - * preview and agents may checksum the payload. - */ -export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] { - const seen = new Set(); - const unique: ExportModel[] = []; - for (const model of models) { - if (seen.has(model.namespaced)) continue; - seen.add(model.namespaced); - unique.push(model); - } - return unique.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0)); -} - /** * OpenCode document: both provider generations plus `$schema`, and nothing else. * @@ -961,23 +672,6 @@ function buildOpencodeClientConfig(ctx: ExportContext): OpencodeGeneratedConfig }; } -export interface PiModelEntry { - id: string; - name: string; - input: string[]; - contextWindow?: number; - maxTokens?: number; - /** Advertised when the catalog row carries a non-empty effort ladder. */ - reasoning?: true; - /** - * Constrains pi's own level scale (minimal..max) to the declared ladder: members map to - * themselves, everything else is hidden (`null`). Without it pi would offer levels the - * ladder does not contain — harmless for provider-config ladders (the proxy clamps those - * at the wire) but a real 400 risk for custom-row ladders, which are advertisement-only. - */ - thinkingLevelMap?: Record; -} - export interface PiProviderBlock { baseUrl: string; api: string; @@ -989,50 +683,6 @@ export interface PiGeneratedConfig { providers: Record; } -/** - * omp accepts a model-level API override. Keep the provider on Chat - * Completions so routed providers retain their established wire format, while - * native OpenAI models can use the lossless Responses surface. - */ -export interface OmpModelEntry extends PiModelEntry { - api?: "openai-responses"; - /** omp requires this flag before it honors a thinking block. */ - reasoning?: true; - thinking?: { - mode: "effort"; - efforts: string[]; - defaultLevel?: string; - }; -} - -export interface OmpProviderBlock { - baseUrl: string; - api: typeof PI_API_DIALECT; - apiKey: string; - models: OmpModelEntry[]; -} - -export interface OmpGeneratedConfig { - providers: Record; -} - -/** - * omp validates model entries strictly. These are its documented effort - * values; omit an unknown value rather than invalidating the whole provider. - */ -const OMP_EFFORT_VOCABULARY = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]); - -function ompEfforts(model: ExportModel): string[] { - const efforts: string[] = []; - for (const effort of model.reasoningEfforts ?? []) { - const normalized = effort.trim().toLowerCase(); - if (OMP_EFFORT_VOCABULARY.has(normalized) && !efforts.includes(normalized)) { - efforts.push(normalized); - } - } - return efforts; -} - /** * Hermes `~/.hermes/config.yaml`. We emit ONLY the provider entry — never * `model.default` — because hijacking the user's main model is not what a @@ -1124,85 +774,6 @@ export interface GajaeGeneratedConfig { providers: Record; } -export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max"; -export type DshWireReasoningEffort = DshReasoningEffort | "ultra"; - -export interface DshModelEntry { - id: string; - name: string; - input: string[]; - contextWindow?: number; - reasoningEfforts?: Partial>; -} - -export interface DshProviderBlock { - displayName: "OpenCodex"; - api: "openai-responses"; - baseURL: string; - headers: { Authorization: "Bearer ocx_data_dsh" }; - models: DshModelEntry[]; -} - -export interface DshGeneratedConfig { - "llm-pi-ai": { - providers: Record; - }; -} - -export interface McodeProviderBlock { - name: "OpenCodex"; - kind: "custom"; - enabled: true; - api: "anthropic-messages"; - options: { - apiKey: string; - baseURL: string; - authMode: "api-key"; - }; - models: Record; -} - -export interface McodeModelEntry { - /** MCode uses this value for context accounting and compaction. */ - limit?: { context: number }; - /** MCode exposes these exact levels in `/model` and sends the selected effort. */ - thinking?: { effortOptions: string[] }; -} - -export interface McodeGeneratedConfig { - custom_provider: Record; -} - -/** - * ZCode's `~/.zcode/v2/config.json` provider entry (observed schema, validated - * live against ZCode 3.7.7 / 3.8.1). `kind: "openai-compatible"` selects the - * OpenAI Chat Completions protocol, which the proxy serves at `/v1/chat/completions`. - * `apiKeyRequired` keeps ZCode's UI from prompting for a key it does not need on - * loopback; the serialized key is always the non-secret loopback placeholder. - */ -export interface ZcodeModelEntry { - name?: string; - limit?: { context: number; output?: number }; - modalities: { input: string[]; output: string[] }; -} - -export interface ZcodeProviderBlock { - name: "OpenCodex"; - kind: "openai-compatible"; - enabled: true; - source: "custom"; - options: { - apiKey: string; - baseURL: string; - apiKeyRequired: true; - }; - models: Record; -} - -export interface ZcodeGeneratedConfig { - provider: Record; -} - /** * Pi's `~/.pi/agent/models.json` shape. `models` is an ARRAY (identity lives in `id`), * unlike OpenCode's keyed object. @@ -1275,56 +846,6 @@ function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig { }; } -/** - * omp's models.yml is Pi-like, but it supports effort metadata and a per-model - * API dialect. Native OpenAI models use Responses; all routed models inherit - * the provider's existing Chat Completions dialect. - */ -function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig { - const models: OmpModelEntry[] = []; - for (const model of normalizeExportModels(ctx.models)) { - const input = inputModalitiesForClient("pi", model.inputModalities); - if (input === null) continue; - const entry: OmpModelEntry = { - id: model.namespaced, - name: exportModelLabel(model), - input, - ...(model.native && model.provider === "openai" ? { api: "openai-responses" } : {}), - }; - const context = authoritativeContextWindow(model.contextWindow); - if (context !== undefined) { - entry.contextWindow = context; - entry.maxTokens = outputBudgetFor(context); - } - const efforts = ompEfforts(model); - if (efforts.length > 0) { - const defaultLevel = model.defaultReasoningEffort?.trim().toLowerCase(); - entry.reasoning = true; - entry.thinking = { - mode: "effort", - efforts, - ...(defaultLevel && efforts.includes(defaultLevel) ? { defaultLevel } : {}), - }; - } - models.push(entry); - } - return { - providers: { - [OPENCODE_PROVIDER_ID]: { - baseUrl: ctx.baseUrl, - api: PI_API_DIALECT, - apiKey: LOOPBACK_API_KEY_PLACEHOLDER, - models, - }, - }, - }; -} - -/** Extra headers a non-loopback bind needs, or nothing on loopback. */ -function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record | undefined { - return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined; -} - function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig { const models: Record = {}; for (const model of normalizeExportModels(ctx.models)) { @@ -1437,174 +958,6 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig { }; } -const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"]; - -function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] { - const offered = new Set(); - for (const raw of model.reasoningEfforts ?? []) { - const effort = raw.trim().toLowerCase(); - if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort); - } - if (offered.size === 0) return undefined; - const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = []; - for (const effort of DSH_EFFORT_ORDER) { - if (effort !== "max") { - if (offered.has(effort)) entries.push([effort, effort]); - continue; - } - // DSH's key is the selectable level; the value is what it sends on the - // wire. Preserve OpenCodex's `ultra` spelling when that is the only - // highest effort, exactly like the rc.6 `max: ultra` contract. - if (offered.has("max")) entries.push(["max", "max"]); - else if (offered.has("ultra")) entries.push(["max", "ultra"]); - } - return Object.fromEntries(entries); -} - -function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean { - const combos = (config as { combos?: unknown }).combos; - if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false; - const combo = (combos as Record)[model.id]; - if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false; - const targets = (combo as { targets?: unknown }).targets; - if (!Array.isArray(targets) || targets.length === 0) return false; - return targets.every(target => { - if (typeof target !== "object" || target === null || Array.isArray(target)) return false; - const provider = (target as { provider?: unknown }).provider; - const modelId = (target as { model?: unknown }).model; - return typeof provider === "string" - && provider.length > 0 - && provider === provider.trim() - && provider !== "openai" - && typeof modelId === "string" - && modelId.length > 0 - && modelId === modelId.trim(); - }); -} - -function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig { - const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct"; - const models: DshModelEntry[] = []; - for (const model of normalizeExportModels(ctx.models)) { - if (direct && (model.native === true || model.provider === "openai")) continue; - if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue; - const input = dshInputModalities(model.inputModalities); - if (input === null) continue; - const contextWindow = authoritativeContextWindow(model.contextWindow); - const reasoningEfforts = dshReasoningEfforts(model); - models.push({ - id: model.namespaced, - name: exportModelLabel(model), - input, - ...(contextWindow !== undefined ? { contextWindow } : {}), - ...(reasoningEfforts ? { reasoningEfforts } : {}), - }); - } - return { - "llm-pi-ai": { - providers: { - [OPENCODE_PROVIDER_ID]: { - displayName: "OpenCodex", - api: "openai-responses", - baseURL: ctx.baseUrl, - headers: { Authorization: "Bearer ocx_data_dsh" }, - models, - }, - }, - }, - }; -} - -/** - * MiniMax Code's `provider add` command persists custom providers under - * `custom_provider.`. Its current model schema reads `limit.context` for - * context accounting and `thinking.effortOptions` for the `/model` effort - * control. Do not emit the removed `thinking.effort` / `defaultEffort` fields: - * MCode 0.1.6 migrates those into options and keeps the selected effort in the - * session. Do not emit `defaultModel` either: connecting a client must not - * silently replace the user's current model selection. - */ -function buildMcodeClientConfig(ctx: ExportContext): McodeGeneratedConfig { - const models: Record = {}; - for (const model of normalizeExportModels(ctx.models)) { - const entry: McodeModelEntry = {}; - const context = authoritativeContextWindow(model.contextWindow); - if (context !== undefined) entry.limit = { context }; - // `none` is an internal Codex catalog sentinel, not an MCode effort. MCode - // forwards every option as `output_config.effort` while keeping adaptive - // thinking enabled, and the Anthropic ingress deliberately accepts only - // minimal..ultra. Advertising `none` would therefore create a selectable - // value that cannot disable reasoning and is not forwarded as an effort. - const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts) - ?.filter(effort => effort !== "none"); - if (efforts && efforts.length > 0) entry.thinking = { effortOptions: efforts }; - models[model.namespaced] = entry; - } - return { - custom_provider: { - [OPENCODE_PROVIDER_ID]: { - name: "OpenCodex", - kind: "custom", - enabled: true, - api: "anthropic-messages", - options: { - apiKey: LOOPBACK_API_KEY_PLACEHOLDER, - baseURL: ctx.baseUrl.replace(/\/v1\/?$/, ""), - authMode: "api-key", - }, - models, - }, - }, - }; -} - -/** - * ZCode dials the OpenAI Chat Completions surface (`openai-compatible`), which - * appends `/chat/completions` to `baseURL`. We supply `baseURL` with the `/v1` - * suffix so requests land on `/v1/chat/completions`. Model ids are the proxy's canonical - * `provider/id` selectors, which `/v1/chat/completions` resolves directly. Context - * limits follow the authoritative-window rule: a model without one ships - * without `limit` rather than guessing. Modalities are ZCode's observed - * `text`-floor vocabulary; image-capable rows advertise image input. - */ -function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig { - const models: Record = {}; - for (const model of normalizeExportModels(ctx.models)) { - const input = inputModalitiesForClient("pi", model.inputModalities); - if (input === null) continue; - const entry: ZcodeModelEntry = { - name: exportModelLabel(model), - modalities: { input, output: ["text"] }, - }; - // `limit.context` follows the authoritative-window rule. `output` is - // deliberately absent: ZCode's schema makes it optional and we have no - // authoritative output budget to assert (reviewer finding: an emitted - // stand-in would be a guessed capability, exactly what "no metadata is - // guessed" forbids). - const context = authoritativeContextWindow(model.contextWindow); - if (context !== undefined) { - entry.limit = { context }; - } - models[model.namespaced] = entry; - } - return { - provider: { - [OPENCODE_PROVIDER_ID]: { - name: "OpenCodex", - kind: "openai-compatible", - enabled: true, - source: "custom", - options: { - apiKey: LOOPBACK_API_KEY_PLACEHOLDER, - baseURL: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1", - apiKeyRequired: true, - }, - models, - }, - }, - }; -} - /** * Per-client model counts, read back off the SERIALIZED document rather than * recomputed from the input rows: `modelsWithoutLimits` drives a GUI line about @@ -1621,11 +974,6 @@ function summarizePi(document: unknown): { modelCount: number; modelsWithoutLimi return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; } -function summarizeOmp(document: unknown): { modelCount: number; modelsWithoutLimits: number } { - const models = (document as OmpGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? []; - return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; -} - function summarizeHermes(document: unknown): { modelCount: number; modelsWithoutLimits: number } { const models = (document as HermesGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? {}; // Hermes carries capability metadata but no per-model limit to be missing. @@ -1649,26 +997,6 @@ function summarizeGajae(document: unknown): { modelCount: number; modelsWithoutL return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; } -function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } { - const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? []; - return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; -} - -function summarizeMcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } { - const models = Object.values((document as McodeGeneratedConfig | undefined)?.custom_provider?.[OPENCODE_PROVIDER_ID]?.models ?? {}); - return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length }; -} - -function summarizeZcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } { - const models = Object.values((document as ZcodeGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {}); - return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length }; -} - -/** One fragment at `path`, built from this client's own document. */ -function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution { - return { clientId, fragments: [{ path, value }] }; -} - function buildOpencodeContribution(ctx: ExportContext): ManagedContribution { const doc = buildOpencodeClientConfig(ctx); return { @@ -1688,11 +1016,6 @@ function buildPiContribution(ctx: ExportContext): ManagedContribution { return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); } -function buildOmpContribution(ctx: ExportContext): ManagedContribution { - const doc = buildOmpClientConfig(ctx); - return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); -} - function buildHermesContribution(ctx: ExportContext): ManagedContribution { const doc = buildHermesClientConfig(ctx); return singleFragment("hermes", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); @@ -1724,21 +1047,6 @@ function buildGajaeContribution(ctx: ExportContext): ManagedContribution { return singleFragment("gajae", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); } -function buildDshContribution(ctx: ExportContext): ManagedContribution { - const doc = buildDshClientConfig(ctx); - return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]); -} - -function buildMcodeContribution(ctx: ExportContext): ManagedContribution { - const doc = buildMcodeClientConfig(ctx); - return singleFragment("mcode", ["custom_provider", OPENCODE_PROVIDER_ID], doc.custom_provider[OPENCODE_PROVIDER_ID]); -} - -function buildZcodeContribution(ctx: ExportContext): ManagedContribution { - const doc = buildZcodeClientConfig(ctx); - return singleFragment("zcode", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]); -} - /** * Prime Agent (PrimeIntellect) is the pi coding agent shipped under a different * brand rather than a lookalike: its package declares a `piConfig` block, and diff --git a/src/clients/config-export/constants.ts b/src/clients/config-export/constants.ts new file mode 100644 index 0000000000..a37f87f0d5 --- /dev/null +++ b/src/clients/config-export/constants.ts @@ -0,0 +1,69 @@ +// Shared client export constants. +import type { OcxConfig } from "../../types"; + + +/** Provider key owned by this project; the only key any exporter ever emits. */ +export const OPENCODE_PROVIDER_ID = "opencodex"; + +export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json"; + +/** + * Env var carrying the proxy admission key to opencode. The config only ever holds the + * `{env:...}` reference, so the secret never lands on disk. opencode substitutes it at + * load time. + */ +export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY"; + +/** Env reference shared by apiKey and the dedicated proxy admission header. */ +export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`; + +/** + * Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays + * in the environment exactly as it does for OpenCode. + */ +export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY"; +export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`; + +/** OpenClaw interpolates `${UPPERCASE_VAR}` and fails closed when it is unset. */ +export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY"; +export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`; + +/** + * Placeholder credential for loopback-only clients (Kimi, Pi). A loopback + * bind needs no real admission key, so we emit the same placeholder the Grok + * managed block uses rather than a user secret. Pi resolves `apiKey` before + * building its model list and hides the provider when an env reference is unset. + */ +export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback"; + +/** + * Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey` + * falls back to treating the literal text as the token when the variable is + * unset, which would silently ship a bogus credential — so we never emit it. + */ +export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY"; + +/** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */ +export const PI_API_DIALECT = "openai-completions"; + +/** + * opencode's config schema rejects a `limit` block that carries `context` without + * `output`, but CatalogModel has no authoritative per-model output field. Dropping + * `limit` entirely would also throw away the authoritative context window we DO have, + * so the block is emitted with this budget standing in for the missing half. + * + * The value matches REASONING_MAX_TOKENS_CEILING in src/adapters/anthropic.ts — the + * project's existing "safe ceiling across current models" figure. It is a ceiling for + * schema validity, NOT a claim about any specific model's true maximum, and it is + * clamped to the context window so a small-context model can never be emitted with + * output > context. Pi's `maxTokens` uses the same stand-in and the same clamp. + */ +export const SCHEMA_REQUIRED_OUTPUT_BUDGET = 32_000; + +/** Deterministic loopback default for exported provider-block helpers in tests. */ +export const OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG: OcxConfig = { + port: 10100, + hostname: "127.0.0.1", + defaultProvider: "mock", + providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1" } }, +} as OcxConfig; diff --git a/src/clients/config-export/contracts.ts b/src/clients/config-export/contracts.ts new file mode 100644 index 0000000000..3edd91eb5e --- /dev/null +++ b/src/clients/config-export/contracts.ts @@ -0,0 +1,150 @@ +// Shared client export contracts. +import type { OcxConfig } from "../../types"; +import type { ConfigFormat } from "../../integrations/serialize"; + +/** + * One entry opencodex owns inside a client's config: the JSON path to it and + * the value we put there. + * + * A path list rather than a single provider key because ownership is not + * always one entry — Kimi owns its provider block AND one model entry per + * model, and a writer that only knew about the provider would strand the rest + * (devlog 260802 006 §2). + */ +export interface ManagedFragment { + path: readonly string[]; + value: unknown; +} + +/** Everything opencodex contributes to one client's config, as one unit. */ +export interface ManagedContribution { + clientId: ExportClientId; + fragments: readonly ManagedFragment[]; +} + +export type BuildContribution = (ctx: ExportContext) => ManagedContribution; + +export interface OpencodeLaunchEnv { + [key: string]: string | undefined; +} + +/** Visible catalog entry keyed by the proxy's canonical namespaced selector. */ +export interface OpencodeCatalogModel { + namespaced: string; + native?: boolean; + provider?: string; + id?: string; + contextWindow?: number; + displayName?: string; + /** Declared effort ladder. Exported as opencode model variants where the client reads them. */ + reasoningEfforts?: readonly string[]; + /** + * Declared default effort. Carried so every client export reads one deduped, visibility- + * filtered ladder per model. The opencode serializer deliberately does NOT turn it into a + * model-level setting — see {@link opencodeEffortVariants} for why. + */ + defaultReasoningEffort?: string; +} + +/** + * One proxy-routed model destined for a client config. Deliberately narrower than + * `CatalogModel` so a serializer cannot reach for a field that does not survive the + * `/api/models` boundary. + */ +export interface ExportModel { + /** Canonical proxy selector: `provider/id`, or bare slug for native. */ + namespaced: string; + provider: string; + id: string; + /** Native OpenAI entry. Read by the shared label rule. */ + native?: boolean; + displayName?: string; + contextWindow?: number; + inputModalities?: string[]; + /** Optional effort ladder exported only to clients that support it. */ + reasoningEfforts?: string[]; + defaultReasoningEffort?: string; +} + +export interface ExportContext { + /** `http://host:port/v1` — the OpenAI-compatible surface the client dials. */ + baseUrl: string; + models: readonly ExportModel[]; + /** + * Live proxy config. Only the OpenCode path reads it: a non-loopback bind moves + * admission from `apiKey` to the `x-opencodex-api-key` header. + */ + config?: OcxConfig; +} + +export type ExportClientId = + | "opencode" + | "pi" + | "omp" + | "hermes" + | "openclaw" + | "kimi" + | "gajae" + | "dsh" + | "mcode" + | "zcode" + | "prime" + | "aside"; + +export interface ExportClientSpec { + id: ExportClientId; + /** Download filename; matches the destination file's own name (003 §5). */ + filename: string; + /** Canonical destination for humans. Never written to. */ + destination: (env: NodeJS.ProcessEnv) => string; + /** Env var the config references; the value is never serialized. */ + apiKeyEnv: string; + /** Shell line the user runs before launching the client. */ + exportHint: string; + build: (ctx: ExportContext) => unknown; + /** + * Text format of the client's config file. `filename` already carries the + * extension; this drives serialization and the download media type so no + * consumer has to infer either from the name. + */ + format: ConfigFormat; + /** + * Count models in THIS client's document shape. Required so a new client + * cannot be added without teaching the summarizer about it — the old + * "anything that is not OpenCode must be Pi" branch was a latent bug. + */ + summarize: (document: unknown) => { modelCount: number; modelsWithoutLimits: number }; + /** + * The fragments opencodex owns inside this client's config. Only the builder + * knows where a client keeps our entries, so ownership paths originate here + * rather than being re-derived by the writer. + */ + buildContribution: BuildContribution; + /** + * True when the generated integration deliberately supports loopback only. + * + * `/v1/chat/completions` rejects bearer credentials and requires the + * dedicated `x-opencodex-api-key` header (AUTH_MATRIX in + * src/server/auth-cors.ts). If this exporter cannot safely emit that header, + * it refuses a remote bind rather than generating a config that 401s. Same + * reasoning as the Grok managed block's non-loopback refusal. + */ + loopbackOnly: boolean; +} + +export interface PiModelEntry { + id: string; + name: string; + input: string[]; + contextWindow?: number; + maxTokens?: number; + /** Advertised when the catalog row carries a non-empty effort ladder. */ + reasoning?: true; + /** + * Constrains pi's own level scale (minimal..max) to the declared ladder: members map to + * themselves, everything else is hidden (`null`). Without it pi would offer levels the + * ladder does not contain — harmless for provider-config ladders (the proxy clamps those + * at the wire) but a real 400 risk for custom-row ladders, which are advertisement-only. + */ + thinkingLevelMap?: Record; +} diff --git a/src/clients/config-export/dsh.ts b/src/clients/config-export/dsh.ts new file mode 100644 index 0000000000..e140d543ef --- /dev/null +++ b/src/clients/config-export/dsh.ts @@ -0,0 +1,132 @@ +// DSH config export. +import type { ExportModel, ExportContext, ManagedContribution } from "./contracts"; +import type { OcxConfig } from "../../types"; +import { providerCodexAccountMode } from "../../providers/registry"; +import { normalizeExportModels, authoritativeContextWindow, exportModelLabel, singleFragment } from "./model-metadata"; +import { OPENCODE_PROVIDER_ID } from "./constants"; + + +/** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */ +function dshInputModalities(modalities: readonly string[] | undefined): string[] | null { + const declared = modalities ?? []; + if (declared.length === 0) return ["text"]; + const kept: string[] = []; + for (const value of declared) { + if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value); + } + if (kept.length > 0) return kept; + return declared.every(value => value === "audio") ? null : ["text"]; +} + +export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max"; +export type DshWireReasoningEffort = DshReasoningEffort | "ultra"; + +export interface DshModelEntry { + id: string; + name: string; + input: string[]; + contextWindow?: number; + reasoningEfforts?: Partial>; +} + +export interface DshProviderBlock { + displayName: "OpenCodex"; + api: "openai-responses"; + baseURL: string; + headers: { Authorization: "Bearer ocx_data_dsh" }; + models: DshModelEntry[]; +} + +export interface DshGeneratedConfig { + "llm-pi-ai": { + providers: Record; + }; +} + +const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"]; + +function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] { + const offered = new Set(); + for (const raw of model.reasoningEfforts ?? []) { + const effort = raw.trim().toLowerCase(); + if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort); + } + if (offered.size === 0) return undefined; + const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = []; + for (const effort of DSH_EFFORT_ORDER) { + if (effort !== "max") { + if (offered.has(effort)) entries.push([effort, effort]); + continue; + } + // DSH's key is the selectable level; the value is what it sends on the + // wire. Preserve OpenCodex's `ultra` spelling when that is the only + // highest effort, exactly like the rc.6 `max: ultra` contract. + if (offered.has("max")) entries.push(["max", "max"]); + else if (offered.has("ultra")) entries.push(["max", "ultra"]); + } + return Object.fromEntries(entries); +} + +function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean { + const combos = (config as { combos?: unknown }).combos; + if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false; + const combo = (combos as Record)[model.id]; + if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false; + const targets = (combo as { targets?: unknown }).targets; + if (!Array.isArray(targets) || targets.length === 0) return false; + return targets.every(target => { + if (typeof target !== "object" || target === null || Array.isArray(target)) return false; + const provider = (target as { provider?: unknown }).provider; + const modelId = (target as { model?: unknown }).model; + return typeof provider === "string" + && provider.length > 0 + && provider === provider.trim() + && provider !== "openai" + && typeof modelId === "string" + && modelId.length > 0 + && modelId === modelId.trim(); + }); +} + +export function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig { + const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct"; + const models: DshModelEntry[] = []; + for (const model of normalizeExportModels(ctx.models)) { + if (direct && (model.native === true || model.provider === "openai")) continue; + if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue; + const input = dshInputModalities(model.inputModalities); + if (input === null) continue; + const contextWindow = authoritativeContextWindow(model.contextWindow); + const reasoningEfforts = dshReasoningEfforts(model); + models.push({ + id: model.namespaced, + name: exportModelLabel(model), + input, + ...(contextWindow !== undefined ? { contextWindow } : {}), + ...(reasoningEfforts ? { reasoningEfforts } : {}), + }); + } + return { + "llm-pi-ai": { + providers: { + [OPENCODE_PROVIDER_ID]: { + displayName: "OpenCodex", + api: "openai-responses", + baseURL: ctx.baseUrl, + headers: { Authorization: "Bearer ocx_data_dsh" }, + models, + }, + }, + }, + }; +} + +export function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } { + const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? []; + return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; +} + +export function buildDshContribution(ctx: ExportContext): ManagedContribution { + const doc = buildDshClientConfig(ctx); + return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]); +} diff --git a/src/clients/config-export/mcode.ts b/src/clients/config-export/mcode.ts new file mode 100644 index 0000000000..7cd004982d --- /dev/null +++ b/src/clients/config-export/mcode.ts @@ -0,0 +1,83 @@ +// MiniMax Code config export. +import type { ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata"; +import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort"; +import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; + + +export interface McodeProviderBlock { + name: "OpenCodex"; + kind: "custom"; + enabled: true; + api: "anthropic-messages"; + options: { + apiKey: string; + baseURL: string; + authMode: "api-key"; + }; + models: Record; +} + +export interface McodeModelEntry { + /** MCode uses this value for context accounting and compaction. */ + limit?: { context: number }; + /** MCode exposes these exact levels in `/model` and sends the selected effort. */ + thinking?: { effortOptions: string[] }; +} + +export interface McodeGeneratedConfig { + custom_provider: Record; +} + +/** + * MiniMax Code's `provider add` command persists custom providers under + * `custom_provider.`. Its current model schema reads `limit.context` for + * context accounting and `thinking.effortOptions` for the `/model` effort + * control. Do not emit the removed `thinking.effort` / `defaultEffort` fields: + * MCode 0.1.6 migrates those into options and keeps the selected effort in the + * session. Do not emit `defaultModel` either: connecting a client must not + * silently replace the user's current model selection. + */ +export function buildMcodeClientConfig(ctx: ExportContext): McodeGeneratedConfig { + const models: Record = {}; + for (const model of normalizeExportModels(ctx.models)) { + const entry: McodeModelEntry = {}; + const context = authoritativeContextWindow(model.contextWindow); + if (context !== undefined) entry.limit = { context }; + // `none` is an internal Codex catalog sentinel, not an MCode effort. MCode + // forwards every option as `output_config.effort` while keeping adaptive + // thinking enabled, and the Anthropic ingress deliberately accepts only + // minimal..ultra. Advertising `none` would therefore create a selectable + // value that cannot disable reasoning and is not forwarded as an effort. + const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts) + ?.filter(effort => effort !== "none"); + if (efforts && efforts.length > 0) entry.thinking = { effortOptions: efforts }; + models[model.namespaced] = entry; + } + return { + custom_provider: { + [OPENCODE_PROVIDER_ID]: { + name: "OpenCodex", + kind: "custom", + enabled: true, + api: "anthropic-messages", + options: { + apiKey: LOOPBACK_API_KEY_PLACEHOLDER, + baseURL: ctx.baseUrl.replace(/\/v1\/?$/, ""), + authMode: "api-key", + }, + models, + }, + }, + }; +} + +export function summarizeMcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } { + const models = Object.values((document as McodeGeneratedConfig | undefined)?.custom_provider?.[OPENCODE_PROVIDER_ID]?.models ?? {}); + return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length }; +} + +export function buildMcodeContribution(ctx: ExportContext): ManagedContribution { + const doc = buildMcodeClientConfig(ctx); + return singleFragment("mcode", ["custom_provider", OPENCODE_PROVIDER_ID], doc.custom_provider[OPENCODE_PROVIDER_ID]); +} diff --git a/src/clients/config-export/model-metadata.ts b/src/clients/config-export/model-metadata.ts new file mode 100644 index 0000000000..4f3038efac --- /dev/null +++ b/src/clients/config-export/model-metadata.ts @@ -0,0 +1,113 @@ +// Shared client export model metadata. +import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants"; +import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts"; +import type { OcxConfig } from "../../types"; +import { shouldInjectApiAuthHeader } from "../../codex/inject"; + + +/** + * Authoritative context window, or undefined. Never guesses: a missing, non-finite, or + * non-positive value means the serializer omits every context-derived field. + */ +export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined { + if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) { + const integer = Math.floor(contextWindow); + return integer > 0 ? integer : undefined; + } + return undefined; +} + +/** Schema-required output budget for a known context window. */ +export function outputBudgetFor(context: number): number { + return Math.min(SCHEMA_REQUIRED_OUTPUT_BUDGET, context); +} + +/** + * Modalities a given client's schema will actually accept. + * + * Our internal vocabulary is `text | image | audio` (ALLOWED_INPUT_MODALITIES in + * src/server/management/model-routes.ts). Pi and Gajae accept only + * `text | image`, and both reject the WHOLE config file over one out-of-enum + * value — Gajae reports `/providers/opencodex/models/N/input/2: Invalid option` + * and falls back to its built-in list, Pi returns an empty model config. So a + * single `audio` model takes every routed model down with it. That is not + * hypothetical: zenmux/meta-muse-spark-1.1 advertises audio and did exactly + * this. It is also the same defect the Codex catalog had with `video`, where + * the app showed zero apps (tests/codex-integration/catalog-input-modality-enum.test.ts). + * + * UNKNOWN and INCOMPATIBLE are different inputs, and the Codex fix could + * conflate them safely only because its enum is wider. A model with nothing + * declared is unknown, and `text` is the honest floor — every routed model takes + * prompts. A model declaring `["audio"]` and nothing else is incompatible with a + * text|image client, and rewriting it to `["text"]` would advertise a capability + * it does not have. That input is reachable three ways: `ocx models add + * --modalities audio`, `/api/custom-models`, and provider discovery. + * + * So unknown falls back to text and incompatible returns null, which drops the + * row. Omitting a model costs the user a line in a picker; fabricating `text` + * costs them a model that fails at call time with no explanation. + * + * Deliberately NOT applied in `ExportModel` construction: the management and CLI + * boundaries carry catalog modalities verbatim on purpose, and stripping `audio` + * globally would destroy valid metadata before the destination is known. + */ +const CLIENT_INPUT_MODALITIES: Record<"pi" | "gajae", ReadonlySet> = { + pi: new Set(["text", "image"]), + gajae: new Set(["text", "image"]), +}; + +/** `null` means the model cannot be represented for this client — drop the row. */ +export function inputModalitiesForClient( + client: "pi" | "gajae", + modalities: readonly string[] | undefined, +): string[] | null { + const declared = modalities ?? []; + if (declared.length === 0) return ["text"]; + const accepted = CLIENT_INPUT_MODALITIES[client]; + const kept: string[] = []; + for (const value of declared) { + if (accepted.has(value) && !kept.includes(value)) kept.push(value); + } + return kept.length > 0 ? kept : null; +} + +/** + * Label shared by every client: `" ()"`. The + * provider suffix is what makes two same-named models from different upstreams + * distinguishable in a client's model picker. + */ +export function exportModelLabel(model: OpencodeCatalogModel): string { + const providerLabel = model.native ? "native" : (model.provider ?? "routed"); + const id = model.id ?? model.namespaced; + if (model.displayName && model.displayName.length > 0) { + return `${model.displayName} (${providerLabel})`; + } + return `${id} (${providerLabel})`; +} + +/** + * Shared precondition for every serializer: drop duplicate `namespaced` (first wins, + * native rows lead `/api/models`) and sort by `namespaced` so two calls with the same + * models produce identical bytes. Stability matters because the GUI shows a diffable + * preview and agents may checksum the payload. + */ +export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] { + const seen = new Set(); + const unique: ExportModel[] = []; + for (const model of models) { + if (seen.has(model.namespaced)) continue; + seen.add(model.namespaced); + unique.push(model); + } + return unique.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0)); +} + +/** Extra headers a non-loopback bind needs, or nothing on loopback. */ +export function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record | undefined { + return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined; +} + +/** One fragment at `path`, built from this client's own document. */ +export function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution { + return { clientId, fragments: [{ path, value }] }; +} diff --git a/src/clients/config-export/omp.ts b/src/clients/config-export/omp.ts new file mode 100644 index 0000000000..e31d9bc59f --- /dev/null +++ b/src/clients/config-export/omp.ts @@ -0,0 +1,104 @@ +// Oh My Pi config export. +import type { PiModelEntry, ExportModel, ExportContext, ManagedContribution } from "./contracts"; +import { PI_API_DIALECT, OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; +import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, outputBudgetFor, singleFragment } from "./model-metadata"; + + +/** + * omp accepts a model-level API override. Keep the provider on Chat + * Completions so routed providers retain their established wire format, while + * native OpenAI models can use the lossless Responses surface. + */ +export interface OmpModelEntry extends PiModelEntry { + api?: "openai-responses"; + /** omp requires this flag before it honors a thinking block. */ + reasoning?: true; + thinking?: { + mode: "effort"; + efforts: string[]; + defaultLevel?: string; + }; +} + +export interface OmpProviderBlock { + baseUrl: string; + api: typeof PI_API_DIALECT; + apiKey: string; + models: OmpModelEntry[]; +} + +export interface OmpGeneratedConfig { + providers: Record; +} + +/** + * omp validates model entries strictly. These are its documented effort + * values; omit an unknown value rather than invalidating the whole provider. + */ +const OMP_EFFORT_VOCABULARY = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]); + +function ompEfforts(model: ExportModel): string[] { + const efforts: string[] = []; + for (const effort of model.reasoningEfforts ?? []) { + const normalized = effort.trim().toLowerCase(); + if (OMP_EFFORT_VOCABULARY.has(normalized) && !efforts.includes(normalized)) { + efforts.push(normalized); + } + } + return efforts; +} + +/** + * omp's models.yml is Pi-like, but it supports effort metadata and a per-model + * API dialect. Native OpenAI models use Responses; all routed models inherit + * the provider's existing Chat Completions dialect. + */ +export function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig { + const models: OmpModelEntry[] = []; + for (const model of normalizeExportModels(ctx.models)) { + const input = inputModalitiesForClient("pi", model.inputModalities); + if (input === null) continue; + const entry: OmpModelEntry = { + id: model.namespaced, + name: exportModelLabel(model), + input, + ...(model.native && model.provider === "openai" ? { api: "openai-responses" } : {}), + }; + const context = authoritativeContextWindow(model.contextWindow); + if (context !== undefined) { + entry.contextWindow = context; + entry.maxTokens = outputBudgetFor(context); + } + const efforts = ompEfforts(model); + if (efforts.length > 0) { + const defaultLevel = model.defaultReasoningEffort?.trim().toLowerCase(); + entry.reasoning = true; + entry.thinking = { + mode: "effort", + efforts, + ...(defaultLevel && efforts.includes(defaultLevel) ? { defaultLevel } : {}), + }; + } + models.push(entry); + } + return { + providers: { + [OPENCODE_PROVIDER_ID]: { + baseUrl: ctx.baseUrl, + api: PI_API_DIALECT, + apiKey: LOOPBACK_API_KEY_PLACEHOLDER, + models, + }, + }, + }; +} + +export function summarizeOmp(document: unknown): { modelCount: number; modelsWithoutLimits: number } { + const models = (document as OmpGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? []; + return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length }; +} + +export function buildOmpContribution(ctx: ExportContext): ManagedContribution { + const doc = buildOmpClientConfig(ctx); + return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); +} diff --git a/src/clients/config-export/zcode.ts b/src/clients/config-export/zcode.ts new file mode 100644 index 0000000000..bcd522a389 --- /dev/null +++ b/src/clients/config-export/zcode.ts @@ -0,0 +1,92 @@ +// ZCode config export. +import type { ExportContext, ManagedContribution } from "./contracts"; +import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment } from "./model-metadata"; +import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants"; + + +/** + * ZCode's `~/.zcode/v2/config.json` provider entry (observed schema, validated + * live against ZCode 3.7.7 / 3.8.1). `kind: "openai-compatible"` selects the + * OpenAI Chat Completions protocol, which the proxy serves at `/v1/chat/completions`. + * `apiKeyRequired` keeps ZCode's UI from prompting for a key it does not need on + * loopback; the serialized key is always the non-secret loopback placeholder. + */ +export interface ZcodeModelEntry { + name?: string; + limit?: { context: number; output?: number }; + modalities: { input: string[]; output: string[] }; +} + +export interface ZcodeProviderBlock { + name: "OpenCodex"; + kind: "openai-compatible"; + enabled: true; + source: "custom"; + options: { + apiKey: string; + baseURL: string; + apiKeyRequired: true; + }; + models: Record; +} + +export interface ZcodeGeneratedConfig { + provider: Record; +} + +/** + * ZCode dials the OpenAI Chat Completions surface (`openai-compatible`), which + * appends `/chat/completions` to `baseURL`. We supply `baseURL` with the `/v1` + * suffix so requests land on `/v1/chat/completions`. Model ids are the proxy's canonical + * `provider/id` selectors, which `/v1/chat/completions` resolves directly. Context + * limits follow the authoritative-window rule: a model without one ships + * without `limit` rather than guessing. Modalities are ZCode's observed + * `text`-floor vocabulary; image-capable rows advertise image input. + */ +export function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig { + const models: Record = {}; + for (const model of normalizeExportModels(ctx.models)) { + const input = inputModalitiesForClient("pi", model.inputModalities); + if (input === null) continue; + const entry: ZcodeModelEntry = { + name: exportModelLabel(model), + modalities: { input, output: ["text"] }, + }; + // `limit.context` follows the authoritative-window rule. `output` is + // deliberately absent: ZCode's schema makes it optional and we have no + // authoritative output budget to assert (reviewer finding: an emitted + // stand-in would be a guessed capability, exactly what "no metadata is + // guessed" forbids). + const context = authoritativeContextWindow(model.contextWindow); + if (context !== undefined) { + entry.limit = { context }; + } + models[model.namespaced] = entry; + } + return { + provider: { + [OPENCODE_PROVIDER_ID]: { + name: "OpenCodex", + kind: "openai-compatible", + enabled: true, + source: "custom", + options: { + apiKey: LOOPBACK_API_KEY_PLACEHOLDER, + baseURL: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1", + apiKeyRequired: true, + }, + models, + }, + }, + }; +} + +export function summarizeZcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } { + const models = Object.values((document as ZcodeGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {}); + return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length }; +} + +export function buildZcodeContribution(ctx: ExportContext): ManagedContribution { + const doc = buildZcodeClientConfig(ctx); + return singleFragment("zcode", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]); +} diff --git a/src/codex/log-guard/inspect-schema.ts b/src/codex/log-guard/inspect-schema.ts new file mode 100644 index 0000000000..e4c6576a04 --- /dev/null +++ b/src/codex/log-guard/inspect-schema.ts @@ -0,0 +1,137 @@ +import type { Database } from "bun:sqlite"; + +interface CurrentLogColumn { + name: string; + type: string; + notnull: number; + defaultValue: string | null; + pk: number; +} + +// Pinned to Codex logs migration 0002. Keep this schema private: inspection reports +// compatibility, not column names, so sensitive payload-bearing fields never leak through +// the management API. Any additive/rebuilt future schema is monitor-only until reviewed. +const CURRENT_LOG_SCHEMA: readonly CurrentLogColumn[] = [ + { name: "id", type: "INTEGER", notnull: 0, defaultValue: null, pk: 1 }, + { name: "ts", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 }, + { name: "ts_nanos", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 }, + { name: "level", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 }, + { name: "target", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 }, + { name: "feedback_log_body", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, + { name: "module_path", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, + { name: "file", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, + { name: "line", type: "INTEGER", notnull: 0, defaultValue: null, pk: 0 }, + { name: "thread_id", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, + { name: "process_uuid", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, + { name: "estimated_bytes", type: "INTEGER", notnull: 1, defaultValue: "0", pk: 0 }, +] as const; + +const CURRENT_LOG_TABLE_SQL = `CREATE TABLE logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + ts_nanos INTEGER NOT NULL, + level TEXT NOT NULL, + target TEXT NOT NULL, + feedback_log_body TEXT, + module_path TEXT, + file TEXT, + line INTEGER, + thread_id TEXT, + process_uuid TEXT, + estimated_bytes INTEGER NOT NULL DEFAULT 0 +)`; + +const CURRENT_LOG_INDEX_SQL = { + idx_logs_ts: "CREATE INDEX idx_logs_ts ON logs(ts DESC, ts_nanos DESC, id DESC)", + idx_logs_thread_id: "CREATE INDEX idx_logs_thread_id ON logs(thread_id)", + idx_logs_thread_id_ts: "CREATE INDEX idx_logs_thread_id_ts ON logs(thread_id, ts DESC, ts_nanos DESC, id DESC)", + idx_logs_process_uuid_threadless_ts: `CREATE INDEX idx_logs_process_uuid_threadless_ts + ON logs(process_uuid, ts DESC, ts_nanos DESC, id DESC) + WHERE thread_id IS NULL`, +} as const; + + +export interface ColumnRow { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +} +interface SchemaObjectRow { name: string; type: string; sql: string | null } + +function normalizeDeclaredType(type: string): string { + return String(type ?? "").trim().toUpperCase(); +} + +function normalizeDefault(value: string | null): string | null { + return value === null ? null : String(value).trim(); +} + +function normalizeSchemaSql(sql: string | null | undefined): string { + return (sql ?? "").trim().replace(/;\s*$/, "").replace(/\s+/g, " "); +} + +function sameColumns(columns: ColumnRow[]): boolean { + if (columns.length !== CURRENT_LOG_SCHEMA.length) return false; + return columns.every((column, index) => { + const expected = CURRENT_LOG_SCHEMA[index]; + return column.cid === index + && column.name === expected.name + && normalizeDeclaredType(column.type) === expected.type + && Number(column.notnull) === expected.notnull + && normalizeDefault(column.dflt_value) === expected.defaultValue + && Number(column.pk) === expected.pk; + }); +} + +/** + * The authoritative compatibility predicate: exact table SQL, exact column + * metadata, and every canonical index. + * + * Exported because the mutation paths must apply the SAME test inside their + * write transaction. They used to check column NAMES only, which is strictly + * weaker than what the inspector reports, so a schema change landing between + * the outer inspection and the locked write let Protect install a row-dropping + * trigger and let Reclaim vacuum pages on a database the inspector classifies + * as monitor-only. The lock serializes OpenCodex against itself; it does not + * stop Codex or another SQLite writer, so that TOCTOU window is real. + */ +export function hasCurrentLogsSchema(db: Database): boolean { + const columns = db.query("PRAGMA table_info(logs)").all(); + return hasCurrentLogsTable(db, columns); +} + +export function hasCurrentLogsTable(db: Database, columns: ColumnRow[]): boolean { + const table = db.query( + "SELECT name, type, sql FROM sqlite_schema WHERE name = 'logs' LIMIT 1", + ).get(); + if (table?.type !== "table" + || !sameColumns(columns) + || normalizeSchemaSql(table.sql) !== normalizeSchemaSql(CURRENT_LOG_TABLE_SQL)) { + return false; + } + + const indexes = db.query(` + SELECT name, type, sql FROM sqlite_schema + WHERE name IN ( + 'idx_logs_ts', + 'idx_logs_thread_id', + 'idx_logs_thread_id_ts', + 'idx_logs_process_uuid_threadless_ts' + ) + `).all(); + const byName = new Map(indexes.map(row => [row.name, row])); + for (const [name, expectedSql] of Object.entries(CURRENT_LOG_INDEX_SQL)) { + const row = byName.get(name); + if (row?.type !== "index" || normalizeSchemaSql(row.sql) !== normalizeSchemaSql(expectedSql)) { + return false; + } + } + + // Extra indexes and triggers do not redefine the table contract. In particular, + // Protect intentionally installs OpenCodex-owned triggers and unrelated user triggers + // are supported, so compatibility is based on the canonical table plus required indexes. + return true; +} diff --git a/src/codex/log-guard/inspect.ts b/src/codex/log-guard/inspect.ts index eca4402e25..d3d7d23dbe 100644 --- a/src/codex/log-guard/inspect.ts +++ b/src/codex/log-guard/inspect.ts @@ -8,6 +8,8 @@ import { resolveCodexSqliteHome, type CodexSqliteHomeDeps, } from "../paths"; +export { hasCurrentLogsSchema } from "./inspect-schema"; +import { hasCurrentLogsTable, type ColumnRow } from "./inspect-schema"; const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI; const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]); @@ -15,56 +17,6 @@ const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]); // alone; skipping all row aggregates above 64 MiB reduced /api/storage to 628ms. const MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES = 64 * 1024 * 1024; -interface CurrentLogColumn { - name: string; - type: string; - notnull: number; - defaultValue: string | null; - pk: number; -} - -// Pinned to Codex logs migration 0002. Keep this schema private: inspection reports -// compatibility, not column names, so sensitive payload-bearing fields never leak through -// the management API. Any additive/rebuilt future schema is monitor-only until reviewed. -const CURRENT_LOG_SCHEMA: readonly CurrentLogColumn[] = [ - { name: "id", type: "INTEGER", notnull: 0, defaultValue: null, pk: 1 }, - { name: "ts", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 }, - { name: "ts_nanos", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 }, - { name: "level", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 }, - { name: "target", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 }, - { name: "feedback_log_body", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, - { name: "module_path", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, - { name: "file", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, - { name: "line", type: "INTEGER", notnull: 0, defaultValue: null, pk: 0 }, - { name: "thread_id", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, - { name: "process_uuid", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 }, - { name: "estimated_bytes", type: "INTEGER", notnull: 1, defaultValue: "0", pk: 0 }, -] as const; - -const CURRENT_LOG_TABLE_SQL = `CREATE TABLE logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ts INTEGER NOT NULL, - ts_nanos INTEGER NOT NULL, - level TEXT NOT NULL, - target TEXT NOT NULL, - feedback_log_body TEXT, - module_path TEXT, - file TEXT, - line INTEGER, - thread_id TEXT, - process_uuid TEXT, - estimated_bytes INTEGER NOT NULL DEFAULT 0 -)`; - -const CURRENT_LOG_INDEX_SQL = { - idx_logs_ts: "CREATE INDEX idx_logs_ts ON logs(ts DESC, ts_nanos DESC, id DESC)", - idx_logs_thread_id: "CREATE INDEX idx_logs_thread_id ON logs(thread_id)", - idx_logs_thread_id_ts: "CREATE INDEX idx_logs_thread_id_ts ON logs(thread_id, ts DESC, ts_nanos DESC, id DESC)", - idx_logs_process_uuid_threadless_ts: `CREATE INDEX idx_logs_process_uuid_threadless_ts - ON logs(process_uuid, ts DESC, ts_nanos DESC, id DESC) - WHERE thread_id IS NULL`, -} as const; - export type CodexLogGuardCapabilityReason = | "database_missing" | "database_unreadable" @@ -124,15 +76,6 @@ export interface CodexLogGuardInspection { }; } -interface ColumnRow { - cid: number; - name: string; - type: string; - notnull: number; - dflt_value: string | null; - pk: number; -} -interface SchemaObjectRow { name: string; type: string; sql: string | null } interface CountRow { n: number } interface LevelRow { level: string; rows: number } interface TargetCountRow { rows: number } @@ -243,81 +186,6 @@ function unavailableInspection(): CodexLogGuardInspection { }; } -function normalizeDeclaredType(type: string): string { - return String(type ?? "").trim().toUpperCase(); -} - -function normalizeDefault(value: string | null): string | null { - return value === null ? null : String(value).trim(); -} - -function normalizeSchemaSql(sql: string | null | undefined): string { - return (sql ?? "").trim().replace(/;\s*$/, "").replace(/\s+/g, " "); -} - -function sameColumns(columns: ColumnRow[]): boolean { - if (columns.length !== CURRENT_LOG_SCHEMA.length) return false; - return columns.every((column, index) => { - const expected = CURRENT_LOG_SCHEMA[index]; - return column.cid === index - && column.name === expected.name - && normalizeDeclaredType(column.type) === expected.type - && Number(column.notnull) === expected.notnull - && normalizeDefault(column.dflt_value) === expected.defaultValue - && Number(column.pk) === expected.pk; - }); -} - -/** - * The authoritative compatibility predicate: exact table SQL, exact column - * metadata, and every canonical index. - * - * Exported because the mutation paths must apply the SAME test inside their - * write transaction. They used to check column NAMES only, which is strictly - * weaker than what the inspector reports, so a schema change landing between - * the outer inspection and the locked write let Protect install a row-dropping - * trigger and let Reclaim vacuum pages on a database the inspector classifies - * as monitor-only. The lock serializes OpenCodex against itself; it does not - * stop Codex or another SQLite writer, so that TOCTOU window is real. - */ -export function hasCurrentLogsSchema(db: Database): boolean { - const columns = db.query("PRAGMA table_info(logs)").all(); - return hasCurrentLogsTable(db, columns); -} - -function hasCurrentLogsTable(db: Database, columns: ColumnRow[]): boolean { - const table = db.query( - "SELECT name, type, sql FROM sqlite_schema WHERE name = 'logs' LIMIT 1", - ).get(); - if (table?.type !== "table" - || !sameColumns(columns) - || normalizeSchemaSql(table.sql) !== normalizeSchemaSql(CURRENT_LOG_TABLE_SQL)) { - return false; - } - - const indexes = db.query(` - SELECT name, type, sql FROM sqlite_schema - WHERE name IN ( - 'idx_logs_ts', - 'idx_logs_thread_id', - 'idx_logs_thread_id_ts', - 'idx_logs_process_uuid_threadless_ts' - ) - `).all(); - const byName = new Map(indexes.map(row => [row.name, row])); - for (const [name, expectedSql] of Object.entries(CURRENT_LOG_INDEX_SQL)) { - const row = byName.get(name); - if (row?.type !== "index" || normalizeSchemaSql(row.sql) !== normalizeSchemaSql(expectedSql)) { - return false; - } - } - - // Extra indexes and triggers do not redefine the table contract. In particular, - // Protect intentionally installs OpenCodex-owned triggers and unrelated user triggers - // are supported, so compatibility is based on the canonical table plus required indexes. - return true; -} - function pragmaNumber(db: Database, pragma: "page_size" | "page_count" | "freelist_count"): number { const row = db.query, []>(`PRAGMA ${pragma}`).get(); return Number(row?.[pragma] ?? 0); diff --git a/src/codex/prompt-layers.ts b/src/codex/prompt-layers.ts index 459c13fdfc..1fe09f0e08 100644 --- a/src/codex/prompt-layers.ts +++ b/src/codex/prompt-layers.ts @@ -26,13 +26,11 @@ * CODEX_HOME is resolved at CALL time (the `features.ts:58-67` pattern) so tests * can point fixtures via env or an explicit path. */ -import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; +import { existsSync, readdirSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; -import { createHash, randomBytes, type Hash } from "node:crypto"; +import { createHash, randomBytes } from "node:crypto"; import { expandUserPath } from "../config"; -import { CODEX_CONFIG_PATH } from "./paths"; import { resolveCodexHomeDir } from "./home"; -import { OCX_SECTION_MARKER } from "./injected-marker"; import { durableWrite, durableWriteExclusive, @@ -141,46 +139,19 @@ export function isToggleId(value: string): value is ToggleId { return Object.prototype.hasOwnProperty.call(TOGGLE_KEYS, value); } -// --------------------------------------------------------------------------- -// Paths -// --------------------------------------------------------------------------- - -export interface Paths { - configPath?: string; - storePath?: string; - baseVariantDir?: string; -} - -function activeCodexHome(): string { - const raw = process.env.CODEX_HOME?.trim(); - if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length); - const path = resolve(expandUserPath(raw)); - try { - return realpathSync.native(path); - } catch { - return path; - } -} - -export function activeConfigPath(opts?: Paths): string { - return opts?.configPath ?? join(activeCodexHome(), "config.toml"); -} - -export function activeStorePath(opts?: Paths): string { - return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json"); -} +export { activeConfigPath, activeStorePath, activeBaseVariantDir } from "./prompt-layers/paths"; +export type { Paths } from "./prompt-layers/paths"; +export { computeRevision, readFileBytes } from "./prompt-layers/revision"; +export { normalizeBody, findInvalidCharacter, encodeBasicString, decodeBasicString } from "./prompt-layers/encoding"; +export type { CharacterFinding } from "./prompt-layers/encoding"; +export { inspectOwnership } from "./prompt-layers/toml-read"; +export type { Ownership } from "./prompt-layers/toml-read"; -/** - * Where authored base-prompt variants live, one markdown file per variant. - * - * A directory of real files rather than another JSON store, because - * `model_instructions_file` points Codex at a path it reads directly. Embedding the - * bodies in `opencodex-prompt.json` would mean materialising a temp file at selection - * time, which is a second write path for no gain. - */ -export function activeBaseVariantDir(opts?: Paths): string { - return opts?.baseVariantDir ?? join(activeCodexHome(), "opencodex-prompt-base"); -} +import { activeConfigPath, activeStorePath, activeBaseVariantDir, journalPathFor, lockPathFor, type Paths } from "./prompt-layers/paths"; +import { readFileOrNull, computeRevision, updateFingerprintField } from "./prompt-layers/revision"; +import { normalizeBody, findInvalidCharacter, decodeBasicString } from "./prompt-layers/encoding"; +import { rootArrayEntries, hasRootKey, rootLines, tableLines, boolInLines, inspectOwnership } from "./prompt-layers/toml-read"; +import { setRootBool, setRootString, setTableBool, setProjection, removeUnownedProjection } from "./prompt-layers/toml-edit"; /** * Instruction documents the prompt probe renders out of CODEX_HOME, in the @@ -212,88 +183,6 @@ function probeInstructionFilenames(configBytes: string | null): string[] { return names; } -/** - * Decoded string entries of a root-scope TOML array. - * - * Parsed, not pattern-matched. Three successive review rounds each found another - * valid spelling a hand-rolled reader missed — multi-line arrays, a comment after the - * opening bracket, a quoted key — and every miss was a rendered document whose edits - * moved no admission key. The pattern was the defect: TOML is not a line format, so - * no regex over lines can enumerate what a parser accepts. - * - * The module header's warning about JS TOML parsers does apply here, and a review - * round proved it against an earlier version of this comment that claimed otherwise. - * Bun rejects an entire document containing an integer outside JavaScript's safe - * range, such as `model_context_window = 9223372036854775807`, which Rust accepts as - * an ordinary `i64`. A whole-document parse turned that into BOTH arrays disappearing - * — a worse failure than any single missed spelling, and one the old regex did not - * have. - * - * So the parse is the preferred reader, not the only one. When it fails, the scan - * below runs, and it is deliberately loose: it accepts any spelling it recognises and - * over-reports rather than under-reports, because an extra hashed filename costs one - * redundant probe while a missing one costs stale text. - */ -function rootArrayEntries(configBytes: string | null, key: string): string[] { - const value = rootValue(configBytes, key); - if (value === PARSE_FAILED) return scanRootArrayEntries(configBytes, key); - if (!Array.isArray(value)) return []; - return value.filter((entry): entry is string => typeof entry === "string"); -} - -/** - * Distinguishes "the parser could not read this file" from "the key is absent". - * Collapsing the two is what made an unrelated large integer silently empty the - * project-document set. - */ -const PARSE_FAILED = Symbol("toml-parse-failed"); - -/** A root-scope value, `undefined` when the key is absent, `PARSE_FAILED` when the file will not parse. */ -function rootValue(configBytes: string | null, key: string): unknown { - if (configBytes === null) return undefined; - let parsed: unknown; - try { - parsed = Bun.TOML.parse(configBytes); - } catch { - return PARSE_FAILED; - } - if (typeof parsed !== "object" || parsed === null) return PARSE_FAILED; - return (parsed as Record)[key]; -} - -/** - * Fallback reader for a config this parser will not accept but Codex will. - * - * Not a second attempt at being a TOML parser — that approach failed three review - * rounds. It is a deliberately over-eager scan: it takes the first bracketed group for - * the key under either spelling, spans lines, strips comments, and keeps anything that - * decodes. Over-reporting is the safe direction here. - */ -function scanRootArrayEntries(configBytes: string | null, key: string): string[] { - const lines = rootLines(configBytes ?? ""); - const opener = new RegExp(`^\\s*"?${key}"?\\s*=\\s*\\[(.*)$`); - for (let i = 0; i < lines.length; i += 1) { - const m = opener.exec(lines[i]!); - if (!m) continue; - let body = m[1]!.replace(/#.*$/, ""); - for (let j = i; !body.includes("]"); ) { - j += 1; - if (j >= lines.length) return []; - body += lines[j]!.replace(/#.*$/, ""); - } - const out: string[] = []; - for (const raw of body.slice(0, body.indexOf("]")).split(",")) { - const trimmed = raw.trim(); - if (trimmed === "") continue; - const decoded = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2 - ? trimmed.slice(1, -1) - : decodeBasicString(trimmed); - if (decoded !== null) out.push(decoded); - } - return out; - } - return []; -} /** * The directories Codex would look in for a project document, given the home the @@ -344,242 +233,6 @@ function projectRootMarkers(configBytes: string | null): string[] { return rootArrayEntries(configBytes, "project_root_markers").filter(m => m !== ""); } -/** - * Whether a root-scope key is present at all, regardless of what it holds. - * - * A parse failure is not an answer, so it falls through to the scan rather than - * counting as present: reading `PARSE_FAILED` as "present" would report an empty - * marker list and disable root detection on a config Codex reads fine. - */ -function hasRootKey(configBytes: string | null, key: string): boolean { - const value = rootValue(configBytes, key); - if (value === PARSE_FAILED) return scanHasRootKey(configBytes, key); - return value !== undefined; -} - -/** Textual presence check, used only when the parser cannot read the file. */ -function scanHasRootKey(configBytes: string | null, key: string): boolean { - const probe = new RegExp(`^\\s*"?${key}"?\\s*=`); - return rootLines(configBytes ?? "").some(line => probe.test(line)); -} - -/** - * Feed one named field into a fingerprint, framed so that no two distinct states - * can produce the same digest. - * - * Framing is the whole point. Concatenating `name + ":" + contents` is ambiguous: - * an adversarial review of the first version of this function showed that - * `{override: "left", agents: "right\nAGENTS.md:tail"}` and - * `{override: "left\nAGENTS.md:right", agents: "tail"}` hashed identically, because - * a file's own bytes can imitate the separator that follows it. That is exactly a - * missed invalidation: the fingerprint is the probe's admission key, so two - * different prompt states sharing a digest means one caller is served the other's - * stale text. - * - * A byte length cannot be forged by content, so each field carries one. Absence is - * a length of -1 rather than a sentinel string, because a sentinel is just more - * content: the same review found that `null` collided with a file whose bytes were - * literally NUL + "absent". - */ -function updateFingerprintField(hash: Hash, name: string, contents: string | null): void { - const bytes = contents === null ? -1 : Buffer.byteLength(contents, "utf8"); - hash.update(`\n${name}:${bytes}:`); - if (contents !== null) hash.update(contents); -} - -function journalPathFor(storePath: string): string { - return `${storePath.replace(/\.json$/, "")}.journal`; -} - -function lockPathFor(storePath: string): string { - return `${storePath.replace(/\.json$/, "")}.lock`; -} - -// --------------------------------------------------------------------------- -// Character policy — see the header. Defined over Unicode SCALAR VALUES, not -// UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8 -// encoding would silently substitute U+FFFD. -// --------------------------------------------------------------------------- - -export interface CharacterFinding { - /** code-point index, consistent across module, route and editor */ - position: number; - reason: "control" | "unpaired-surrogate"; - codePoint: number; -} - -/** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */ -export function normalizeBody(body: string): string { - return body.replace(/\r\n?/g, "\n").replace(/\t/g, " "); -} - -/** First offending scalar, or null. Run AFTER normalizeBody. */ -export function findInvalidCharacter(body: string): CharacterFinding | null { - let position = 0; - for (let i = 0; i < body.length; ) { - const code = body.codePointAt(i)!; - const unit = body.charCodeAt(i); - const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff; - const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff; - // codePointAt only combines a well-formed pair, so a surviving surrogate - // code point here is unpaired by construction. - if ((isHighSurrogate || isLowSurrogate) && code === unit) { - return { position, reason: "unpaired-surrogate", codePoint: code }; - } - const isNewline = code === 0x0a; - const isC0 = code < 0x20 && !isNewline; - const isDel = code === 0x7f; - const isC1 = code >= 0x80 && code <= 0x9f; - if (isC0 || isDel || isC1) { - return { position, reason: "control", codePoint: code }; - } - i += code > 0xffff ? 2 : 1; - position += 1; - } - return null; -} - -/** - * TOML basic-string encoding, total over the accepted set: three rules, none of - * them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear - * because normalizeBody removed it; control characters cannot appear because - * findInvalidCharacter rejected them. - */ -export function encodeBasicString(body: string): string { - return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`; -} - -/** - * Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three - * escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than - * guessed — decoding them correctly is exactly the ambiguity the restricted set - * exists to avoid. - */ -export function decodeBasicString(literal: string): string | null { - if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null; - const inner = literal.slice(1, -1); - let out = ""; - for (let i = 0; i < inner.length; i += 1) { - const ch = inner[i]!; - if (ch !== "\\") { - if (ch === '"') return null; // unescaped quote: not a single literal - out += ch; - continue; - } - const next = inner[i + 1]; - if (next === "\\") out += "\\"; - else if (next === '"') out += '"'; - else if (next === "n") out += "\n"; - else return null; // any other escape is outside what we will decode - i += 1; - } - return out; -} - -// --------------------------------------------------------------------------- -// Byte-level hashing. The revision covers COMPLETE file bytes plus existence, -// so removing the marker while leaving the value intact still changes it. -// --------------------------------------------------------------------------- - -function readFileOrNull(path: string): string | null { - try { - if (!existsSync(path)) return null; - return readFileSync(path, "utf8"); - } catch { - return null; - } -} - -export function computeRevision(configBytes: string | null, storeBytes: string | null): string { - const hash = createHash("sha256"); - // Length-framed for the reason given on updateFingerprintField: with a bare - // separator, config bytes ending in "\nstore:" shift the boundary and two - // different pairs hash alike. That matters twice over — this value is both the - // probe's admission input and the optimistic-concurrency token compared in - // commit(), where a collision would let a write built on stale bytes through. - updateFingerprintField(hash, "cfg", configBytes); - updateFingerprintField(hash, "store", storeBytes); - return `sha256:${hash.digest("hex")}`; -} - -export { readFileOrNull as readFileBytes }; - -// --------------------------------------------------------------------------- -// Scoped TOML scanning. Line-based like `features.ts:80-93`: booleans need no -// escaping, and line editing preserves the user's comments and formatting -// exactly where a re-serialize would not. -// --------------------------------------------------------------------------- - -const TABLE_HEADER = /^\s*\[/; - -/** Lines of the root scope: everything before the first `[table]` header. */ -function rootLines(content: string): string[] { - const lines = content.split("\n"); - const first = lines.findIndex(l => TABLE_HEADER.test(l)); - return first === -1 ? lines : lines.slice(0, first); -} - -/** Lines of `[header]`'s body, up to the next table header. */ -function tableLines(content: string, header: string): string[] | null { - const lines = content.split("\n"); - const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l)); - if (start === -1) return null; - const rest = lines.slice(start + 1); - const end = rest.findIndex(l => TABLE_HEADER.test(l)); - return end === -1 ? rest : rest.slice(0, end); -} - -function boolInLines(lines: string[], key: string): boolean | null { - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(true|false)\\s*(?:#.*)?$`); - for (const line of lines) { - const m = pattern.exec(line); - if (m) return m[1] === "true"; - } - return null; -} - -// --------------------------------------------------------------------------- -// Ownership of the generated projection. -// -// Canonical physical form, always exactly two lines at the top of the document: -// -// # Auto-injected by opencodex -// developer_instructions = "" -// -// Replacement is "find the marker, replace the next line" — never a span search. -// Adjacency mirrors `injected-marker.ts:53-60`, tightened by a shape check. -// --------------------------------------------------------------------------- - -const DEV_INSTRUCTIONS_KEY = "developer_instructions"; -const CANONICAL_LINE = /^developer_instructions = "(?:[^"\\]|\\.)*"$/; -const ANY_DEV_INSTRUCTIONS = /^\s*(?:developer_instructions|"developer_instructions"|'developer_instructions')\s*=/; - -export type Ownership = - /** no such key anywhere in the root scope */ - | { state: "absent" } - /** marker-adjacent and canonically shaped: ours to rewrite */ - | { state: "owned"; line: number; literal: string } - /** marker-adjacent but reshaped: refuse, offer repair */ - | { state: "owned-malformed"; line: number; raw: string } - /** no marker: externally authored, refuse and offer adoption */ - | { state: "external"; line: number; raw: string }; - -export function inspectOwnership(configBytes: string | null): Ownership { - if (configBytes === null) return { state: "absent" }; - const lines = rootLines(configBytes); - for (let i = 0; i < lines.length; i += 1) { - const raw = lines[i]!; - if (!ANY_DEV_INSTRUCTIONS.test(raw)) continue; - const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER); - if (!marked) return { state: "external", line: i + 1, raw }; - if (!CANONICAL_LINE.test(raw)) return { state: "owned-malformed", line: i + 1, raw }; - const literal = raw.slice(`${DEV_INSTRUCTIONS_KEY} = `.length); - return { state: "owned", line: i + 1, literal }; - } - return { state: "absent" }; -} // --------------------------------------------------------------------------- // Store — the single source of truth for custom layers. @@ -996,151 +649,6 @@ export type WriteResult = | { ok: true; changed: boolean; snapshot: PromptLayerSnapshot } | { ok: false; error: WriteError; detail?: string }; -/** Line editing, not re-serialization: the user's comments and layout survive. */ -function dominantEol(content: string): "\r\n" | "\n" { - const crlf = (content.match(/\r\n/g) ?? []).length; - if (crlf === 0) return "\n"; - const bareLf = (content.match(/\n/g) ?? []).length - crlf; - return crlf >= bareLf ? "\r\n" : "\n"; -} - -function splitLines(content: string): string[] { - return content.replace(/\r\n/g, "\n").split("\n"); -} - -/** - * A leading UTF-8 BOM, split off so line editing never steps over it. - * - * Codex reads config.toml with Rust `toml_edit`, which accepts a BOM at byte 0 and - * nowhere else. Inserting the generated block at line index 0 pushed the BOM down - * to byte 58, the write reported success because our own byte comparison matched - * what we intended to write, and the next parse failed with - * "Expected a key but found (0xEF)" — a config file the user could no longer load, - * produced by a write that told them it worked. - * - * Editors on Windows write this byte routinely, so the file is not exotic. - */ -function splitBom(content: string): { bom: string; body: string } { - return content.startsWith("\ufeff") - ? { bom: "\ufeff", body: content.slice(1) } - : { bom: "", body: content }; -} - -function joinLines(lines: string[], eol: "\r\n" | "\n"): string { - const text = lines.join("\n"); - return eol === "\n" ? text : text.replace(/\n/g, "\r\n"); -} - -function firstTableIndex(lines: string[]): number { - const idx = lines.findIndex(l => TABLE_HEADER.test(l)); - return idx === -1 ? lines.length : idx; -} - -/** Set a root-scope boolean, inserting above the first table when absent. */ -function setRootBool(content: string, key: string, value: boolean): string { - const eol = dominantEol(content); - const { bom, body } = splitBom(content); - const lines = splitLines(body); - const limit = firstTableIndex(lines); - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`); - for (let i = 0; i < limit; i += 1) { - const m = pattern.exec(lines[i]!); - if (m) { - lines[i] = `${m[1]}${value}${m[2]}`; - return bom + joinLines(lines, eol); - } - } - lines.splice(limit, 0, `${key} = ${value}`); - return bom + joinLines(lines, eol); -} - -/** - * Set or REMOVE a root-scope basic string. `null` removes the key. - * - * Removal is what selecting the default variant does, and it has to be a real deletion - * rather than an empty string: `model_instructions_file = ""` is a path Codex would try - * to read, not an absent setting. - */ -function setRootString(content: string, key: string, value: string | null): string { - const eol = dominantEol(content); - const { bom, body } = splitBom(content); - const lines = splitLines(body); - const limit = firstTableIndex(lines); - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*"[^"]*"\\s*(?:#.*)?$`); - for (let i = 0; i < limit; i += 1) { - if (!pattern.test(lines[i]!)) continue; - if (value === null) lines.splice(i, 1); - else lines[i] = `${key} = ${encodeBasicString(value)}`; - return bom + joinLines(lines, eol); - } - if (value === null) return bom + joinLines(lines, eol); - lines.splice(limit, 0, `${key} = ${encodeBasicString(value)}`); - return bom + joinLines(lines, eol); -} - -/** Set a boolean inside `[table]`, appending the table when absent. */ -function setTableBool(content: string, table: string, key: string, value: boolean): string { - const eol = dominantEol(content); - const { bom, body } = splitBom(content); - const lines = splitLines(body); - const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l)); - if (start === -1) { - const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length; - lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`); - return bom + joinLines(lines, eol); - } - const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`); - let end = start + 1; - while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1; - for (let i = start + 1; i < end; i += 1) { - const m = pattern.exec(lines[i]!); - if (m) { - lines[i] = `${m[1]}${value}${m[2]}`; - return bom + joinLines(lines, eol); - } - } - lines.splice(end, 0, `${key} = ${value}`); - return bom + joinLines(lines, eol); -} - -/** - * Replace, insert, or remove the generated two-line block. Canonical form is - * marker + assignment at the top of the document; replacement is "find the - * marker, replace the next line" rather than a span search. - */ -function setProjection(content: string | null, projection: string | null): string { - const base = content ?? ""; - const eol = dominantEol(base); - // The BOM is held aside for the whole edit. This is the function that produced - // the corruption: the insert below is at index 0, which put the marker line - // ahead of a byte that is only legal at byte 0. - const { bom, body } = splitBom(base); - const lines = splitLines(body); - const limit = firstTableIndex(lines); - - let markerAt = -1; - for (let i = 0; i < limit; i += 1) { - if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) { - markerAt = i - 1; - break; - } - } - - if (markerAt !== -1) { - if (projection === null) lines.splice(markerAt, 2); - else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`; - return bom + joinLines(lines, eol); - } - - if (projection === null) return bom + joinLines(lines, eol); - lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`); - return bom + joinLines(lines, eol); -} - function serializeStore(layers: readonly CustomLayer[]): string { return `${JSON.stringify({ layers }, null, 2)}\n`; } @@ -1564,20 +1072,6 @@ export function adoptDeveloperInstructions(revision: string, opts?: Paths): Writ }); } -/** Remove an unowned or reshaped `developer_instructions` from the root scope. */ -function removeUnownedProjection(content: string): string { - const eol = dominantEol(content); - const lines = splitLines(content); - const limit = firstTableIndex(lines); - for (let i = 0; i < limit; i += 1) { - if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue; - const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER); - lines.splice(marked ? i - 1 : i, marked ? 2 : 1); - return joinLines(lines, eol); - } - return joinLines(lines, eol); -} - // --------------------------------------------------------------------------- // Salvage — the store is gone while a live projection remains. // diff --git a/src/codex/prompt-layers/encoding.ts b/src/codex/prompt-layers/encoding.ts new file mode 100644 index 0000000000..c4b39ac819 --- /dev/null +++ b/src/codex/prompt-layers/encoding.ts @@ -0,0 +1,80 @@ +// --------------------------------------------------------------------------- +// Character policy — see the header. Defined over Unicode SCALAR VALUES, not +// UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8 +// encoding would silently substitute U+FFFD. +// --------------------------------------------------------------------------- + +export interface CharacterFinding { + /** code-point index, consistent across module, route and editor */ + position: number; + reason: "control" | "unpaired-surrogate"; + codePoint: number; +} + +/** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */ +export function normalizeBody(body: string): string { + return body.replace(/\r\n?/g, "\n").replace(/\t/g, " "); +} + +/** First offending scalar, or null. Run AFTER normalizeBody. */ +export function findInvalidCharacter(body: string): CharacterFinding | null { + let position = 0; + for (let i = 0; i < body.length; ) { + const code = body.codePointAt(i)!; + const unit = body.charCodeAt(i); + const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff; + const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff; + // codePointAt only combines a well-formed pair, so a surviving surrogate + // code point here is unpaired by construction. + if ((isHighSurrogate || isLowSurrogate) && code === unit) { + return { position, reason: "unpaired-surrogate", codePoint: code }; + } + const isNewline = code === 0x0a; + const isC0 = code < 0x20 && !isNewline; + const isDel = code === 0x7f; + const isC1 = code >= 0x80 && code <= 0x9f; + if (isC0 || isDel || isC1) { + return { position, reason: "control", codePoint: code }; + } + i += code > 0xffff ? 2 : 1; + position += 1; + } + return null; +} + +/** + * TOML basic-string encoding, total over the accepted set: three rules, none of + * them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear + * because normalizeBody removed it; control characters cannot appear because + * findInvalidCharacter rejected them. + */ +export function encodeBasicString(body: string): string { + return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`; +} + +/** + * Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three + * escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than + * guessed — decoding them correctly is exactly the ambiguity the restricted set + * exists to avoid. + */ +export function decodeBasicString(literal: string): string | null { + if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null; + const inner = literal.slice(1, -1); + let out = ""; + for (let i = 0; i < inner.length; i += 1) { + const ch = inner[i]!; + if (ch !== "\\") { + if (ch === '"') return null; // unescaped quote: not a single literal + out += ch; + continue; + } + const next = inner[i + 1]; + if (next === "\\") out += "\\"; + else if (next === '"') out += '"'; + else if (next === "n") out += "\n"; + else return null; // any other escape is outside what we will decode + i += 1; + } + return out; +} diff --git a/src/codex/prompt-layers/paths.ts b/src/codex/prompt-layers/paths.ts new file mode 100644 index 0000000000..4cc3ab7af7 --- /dev/null +++ b/src/codex/prompt-layers/paths.ts @@ -0,0 +1,54 @@ +import { realpathSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { expandUserPath } from "../../config"; +import { CODEX_CONFIG_PATH } from "../paths"; + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + +export interface Paths { + configPath?: string; + storePath?: string; + baseVariantDir?: string; +} + +function activeCodexHome(): string { + const raw = process.env.CODEX_HOME?.trim(); + if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length); + const path = resolve(expandUserPath(raw)); + try { + return realpathSync.native(path); + } catch { + return path; + } +} + +export function activeConfigPath(opts?: Paths): string { + return opts?.configPath ?? join(activeCodexHome(), "config.toml"); +} + +export function activeStorePath(opts?: Paths): string { + return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json"); +} + +/** + * Where authored base-prompt variants live, one markdown file per variant. + * + * A directory of real files rather than another JSON store, because + * `model_instructions_file` points Codex at a path it reads directly. Embedding the + * bodies in `opencodex-prompt.json` would mean materialising a temp file at selection + * time, which is a second write path for no gain. + */ +export function activeBaseVariantDir(opts?: Paths): string { + return opts?.baseVariantDir ?? join(activeCodexHome(), "opencodex-prompt-base"); +} + + +export function journalPathFor(storePath: string): string { + return `${storePath.replace(/\.json$/, "")}.journal`; +} + +export function lockPathFor(storePath: string): string { + return `${storePath.replace(/\.json$/, "")}.lock`; +} diff --git a/src/codex/prompt-layers/revision.ts b/src/codex/prompt-layers/revision.ts new file mode 100644 index 0000000000..e3914f911f --- /dev/null +++ b/src/codex/prompt-layers/revision.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from "node:fs"; +import { createHash, type Hash } from "node:crypto"; + +/** + * Feed one named field into a fingerprint, framed so that no two distinct states + * can produce the same digest. + * + * Framing is the whole point. Concatenating `name + ":" + contents` is ambiguous: + * an adversarial review of the first version of this function showed that + * `{override: "left", agents: "right\nAGENTS.md:tail"}` and + * `{override: "left\nAGENTS.md:right", agents: "tail"}` hashed identically, because + * a file's own bytes can imitate the separator that follows it. That is exactly a + * missed invalidation: the fingerprint is the probe's admission key, so two + * different prompt states sharing a digest means one caller is served the other's + * stale text. + * + * A byte length cannot be forged by content, so each field carries one. Absence is + * a length of -1 rather than a sentinel string, because a sentinel is just more + * content: the same review found that `null` collided with a file whose bytes were + * literally NUL + "absent". + */ +export function updateFingerprintField(hash: Hash, name: string, contents: string | null): void { + const bytes = contents === null ? -1 : Buffer.byteLength(contents, "utf8"); + hash.update(`\n${name}:${bytes}:`); + if (contents !== null) hash.update(contents); +} + + +// --------------------------------------------------------------------------- +// Byte-level hashing. The revision covers COMPLETE file bytes plus existence, +// so removing the marker while leaving the value intact still changes it. +// --------------------------------------------------------------------------- + +export function readFileOrNull(path: string): string | null { + try { + if (!existsSync(path)) return null; + return readFileSync(path, "utf8"); + } catch { + return null; + } +} + +export function computeRevision(configBytes: string | null, storeBytes: string | null): string { + const hash = createHash("sha256"); + // Length-framed for the reason given on updateFingerprintField: with a bare + // separator, config bytes ending in "\nstore:" shift the boundary and two + // different pairs hash alike. That matters twice over — this value is both the + // probe's admission input and the optimistic-concurrency token compared in + // commit(), where a collision would let a write built on stale bytes through. + updateFingerprintField(hash, "cfg", configBytes); + updateFingerprintField(hash, "store", storeBytes); + return `sha256:${hash.digest("hex")}`; +} + +export { readFileOrNull as readFileBytes }; diff --git a/src/codex/prompt-layers/toml-edit.ts b/src/codex/prompt-layers/toml-edit.ts new file mode 100644 index 0000000000..4895e18a09 --- /dev/null +++ b/src/codex/prompt-layers/toml-edit.ts @@ -0,0 +1,163 @@ +import { OCX_SECTION_MARKER } from "../injected-marker"; +import { encodeBasicString } from "./encoding"; +import { TABLE_HEADER, ANY_DEV_INSTRUCTIONS, DEV_INSTRUCTIONS_KEY } from "./toml-read"; + +/** Line editing, not re-serialization: the user's comments and layout survive. */ +function dominantEol(content: string): "\r\n" | "\n" { + const crlf = (content.match(/\r\n/g) ?? []).length; + if (crlf === 0) return "\n"; + const bareLf = (content.match(/\n/g) ?? []).length - crlf; + return crlf >= bareLf ? "\r\n" : "\n"; +} + +function splitLines(content: string): string[] { + return content.replace(/\r\n/g, "\n").split("\n"); +} + +/** + * A leading UTF-8 BOM, split off so line editing never steps over it. + * + * Codex reads config.toml with Rust `toml_edit`, which accepts a BOM at byte 0 and + * nowhere else. Inserting the generated block at line index 0 pushed the BOM down + * to byte 58, the write reported success because our own byte comparison matched + * what we intended to write, and the next parse failed with + * "Expected a key but found (0xEF)" — a config file the user could no longer load, + * produced by a write that told them it worked. + * + * Editors on Windows write this byte routinely, so the file is not exotic. + */ +function splitBom(content: string): { bom: string; body: string } { + return content.startsWith("\ufeff") + ? { bom: "\ufeff", body: content.slice(1) } + : { bom: "", body: content }; +} + +function joinLines(lines: string[], eol: "\r\n" | "\n"): string { + const text = lines.join("\n"); + return eol === "\n" ? text : text.replace(/\n/g, "\r\n"); +} + +function firstTableIndex(lines: string[]): number { + const idx = lines.findIndex(l => TABLE_HEADER.test(l)); + return idx === -1 ? lines.length : idx; +} + +/** Set a root-scope boolean, inserting above the first table when absent. */ +export function setRootBool(content: string, key: string, value: boolean): string { + const eol = dominantEol(content); + const { bom, body } = splitBom(content); + const lines = splitLines(body); + const limit = firstTableIndex(lines); + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`); + for (let i = 0; i < limit; i += 1) { + const m = pattern.exec(lines[i]!); + if (m) { + lines[i] = `${m[1]}${value}${m[2]}`; + return bom + joinLines(lines, eol); + } + } + lines.splice(limit, 0, `${key} = ${value}`); + return bom + joinLines(lines, eol); +} + +/** + * Set or REMOVE a root-scope basic string. `null` removes the key. + * + * Removal is what selecting the default variant does, and it has to be a real deletion + * rather than an empty string: `model_instructions_file = ""` is a path Codex would try + * to read, not an absent setting. + */ +export function setRootString(content: string, key: string, value: string | null): string { + const eol = dominantEol(content); + const { bom, body } = splitBom(content); + const lines = splitLines(body); + const limit = firstTableIndex(lines); + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*"[^"]*"\\s*(?:#.*)?$`); + for (let i = 0; i < limit; i += 1) { + if (!pattern.test(lines[i]!)) continue; + if (value === null) lines.splice(i, 1); + else lines[i] = `${key} = ${encodeBasicString(value)}`; + return bom + joinLines(lines, eol); + } + if (value === null) return bom + joinLines(lines, eol); + lines.splice(limit, 0, `${key} = ${encodeBasicString(value)}`); + return bom + joinLines(lines, eol); +} + +/** Set a boolean inside `[table]`, appending the table when absent. */ +export function setTableBool(content: string, table: string, key: string, value: boolean): string { + const eol = dominantEol(content); + const { bom, body } = splitBom(content); + const lines = splitLines(body); + const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l)); + if (start === -1) { + const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length; + lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`); + return bom + joinLines(lines, eol); + } + const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`); + let end = start + 1; + while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1; + for (let i = start + 1; i < end; i += 1) { + const m = pattern.exec(lines[i]!); + if (m) { + lines[i] = `${m[1]}${value}${m[2]}`; + return bom + joinLines(lines, eol); + } + } + lines.splice(end, 0, `${key} = ${value}`); + return bom + joinLines(lines, eol); +} + +/** + * Replace, insert, or remove the generated two-line block. Canonical form is + * marker + assignment at the top of the document; replacement is "find the + * marker, replace the next line" rather than a span search. + */ +export function setProjection(content: string | null, projection: string | null): string { + const base = content ?? ""; + const eol = dominantEol(base); + // The BOM is held aside for the whole edit. This is the function that produced + // the corruption: the insert below is at index 0, which put the marker line + // ahead of a byte that is only legal at byte 0. + const { bom, body } = splitBom(base); + const lines = splitLines(body); + const limit = firstTableIndex(lines); + + let markerAt = -1; + for (let i = 0; i < limit; i += 1) { + if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) { + markerAt = i - 1; + break; + } + } + + if (markerAt !== -1) { + if (projection === null) lines.splice(markerAt, 2); + else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`; + return bom + joinLines(lines, eol); + } + + if (projection === null) return bom + joinLines(lines, eol); + lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`); + return bom + joinLines(lines, eol); +} + + +/** Remove an unowned or reshaped `developer_instructions` from the root scope. */ +export function removeUnownedProjection(content: string): string { + const eol = dominantEol(content); + const lines = splitLines(content); + const limit = firstTableIndex(lines); + for (let i = 0; i < limit; i += 1) { + if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue; + const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER); + lines.splice(marked ? i - 1 : i, marked ? 2 : 1); + return joinLines(lines, eol); + } + return joinLines(lines, eol); +} diff --git a/src/codex/prompt-layers/toml-read.ts b/src/codex/prompt-layers/toml-read.ts new file mode 100644 index 0000000000..b8d800b418 --- /dev/null +++ b/src/codex/prompt-layers/toml-read.ts @@ -0,0 +1,181 @@ +import { OCX_SECTION_MARKER } from "../injected-marker"; +import { decodeBasicString } from "./encoding"; + +/** + * Decoded string entries of a root-scope TOML array. + * + * Parsed, not pattern-matched. Three successive review rounds each found another + * valid spelling a hand-rolled reader missed — multi-line arrays, a comment after the + * opening bracket, a quoted key — and every miss was a rendered document whose edits + * moved no admission key. The pattern was the defect: TOML is not a line format, so + * no regex over lines can enumerate what a parser accepts. + * + * The module header's warning about JS TOML parsers does apply here, and a review + * round proved it against an earlier version of this comment that claimed otherwise. + * Bun rejects an entire document containing an integer outside JavaScript's safe + * range, such as `model_context_window = 9223372036854775807`, which Rust accepts as + * an ordinary `i64`. A whole-document parse turned that into BOTH arrays disappearing + * — a worse failure than any single missed spelling, and one the old regex did not + * have. + * + * So the parse is the preferred reader, not the only one. When it fails, the scan + * below runs, and it is deliberately loose: it accepts any spelling it recognises and + * over-reports rather than under-reports, because an extra hashed filename costs one + * redundant probe while a missing one costs stale text. + */ +export function rootArrayEntries(configBytes: string | null, key: string): string[] { + const value = rootValue(configBytes, key); + if (value === PARSE_FAILED) return scanRootArrayEntries(configBytes, key); + if (!Array.isArray(value)) return []; + return value.filter((entry): entry is string => typeof entry === "string"); +} + +/** + * Distinguishes "the parser could not read this file" from "the key is absent". + * Collapsing the two is what made an unrelated large integer silently empty the + * project-document set. + */ +const PARSE_FAILED = Symbol("toml-parse-failed"); + +/** A root-scope value, `undefined` when the key is absent, `PARSE_FAILED` when the file will not parse. */ +function rootValue(configBytes: string | null, key: string): unknown { + if (configBytes === null) return undefined; + let parsed: unknown; + try { + parsed = Bun.TOML.parse(configBytes); + } catch { + return PARSE_FAILED; + } + if (typeof parsed !== "object" || parsed === null) return PARSE_FAILED; + return (parsed as Record)[key]; +} + +/** + * Fallback reader for a config this parser will not accept but Codex will. + * + * Not a second attempt at being a TOML parser — that approach failed three review + * rounds. It is a deliberately over-eager scan: it takes the first bracketed group for + * the key under either spelling, spans lines, strips comments, and keeps anything that + * decodes. Over-reporting is the safe direction here. + */ +function scanRootArrayEntries(configBytes: string | null, key: string): string[] { + const lines = rootLines(configBytes ?? ""); + const opener = new RegExp(`^\\s*"?${key}"?\\s*=\\s*\\[(.*)$`); + for (let i = 0; i < lines.length; i += 1) { + const m = opener.exec(lines[i]!); + if (!m) continue; + let body = m[1]!.replace(/#.*$/, ""); + for (let j = i; !body.includes("]"); ) { + j += 1; + if (j >= lines.length) return []; + body += lines[j]!.replace(/#.*$/, ""); + } + const out: string[] = []; + for (const raw of body.slice(0, body.indexOf("]")).split(",")) { + const trimmed = raw.trim(); + if (trimmed === "") continue; + const decoded = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2 + ? trimmed.slice(1, -1) + : decodeBasicString(trimmed); + if (decoded !== null) out.push(decoded); + } + return out; + } + return []; +} + +/** + * Whether a root-scope key is present at all, regardless of what it holds. + * + * A parse failure is not an answer, so it falls through to the scan rather than + * counting as present: reading `PARSE_FAILED` as "present" would report an empty + * marker list and disable root detection on a config Codex reads fine. + */ +export function hasRootKey(configBytes: string | null, key: string): boolean { + const value = rootValue(configBytes, key); + if (value === PARSE_FAILED) return scanHasRootKey(configBytes, key); + return value !== undefined; +} + +/** Textual presence check, used only when the parser cannot read the file. */ +function scanHasRootKey(configBytes: string | null, key: string): boolean { + const probe = new RegExp(`^\\s*"?${key}"?\\s*=`); + return rootLines(configBytes ?? "").some(line => probe.test(line)); +} + +// --------------------------------------------------------------------------- +// Scoped TOML scanning. Line-based like `features.ts:80-93`: booleans need no +// escaping, and line editing preserves the user's comments and formatting +// exactly where a re-serialize would not. +// --------------------------------------------------------------------------- + +export const TABLE_HEADER = /^\s*\[/; + +/** Lines of the root scope: everything before the first `[table]` header. */ +export function rootLines(content: string): string[] { + const lines = content.split("\n"); + const first = lines.findIndex(l => TABLE_HEADER.test(l)); + return first === -1 ? lines : lines.slice(0, first); +} + +/** Lines of `[header]`'s body, up to the next table header. */ +export function tableLines(content: string, header: string): string[] | null { + const lines = content.split("\n"); + const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l)); + if (start === -1) return null; + const rest = lines.slice(start + 1); + const end = rest.findIndex(l => TABLE_HEADER.test(l)); + return end === -1 ? rest : rest.slice(0, end); +} + +export function boolInLines(lines: string[], key: string): boolean | null { + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(true|false)\\s*(?:#.*)?$`); + for (const line of lines) { + const m = pattern.exec(line); + if (m) return m[1] === "true"; + } + return null; +} + +// --------------------------------------------------------------------------- +// Ownership of the generated projection. +// +// Canonical physical form, always exactly two lines at the top of the document: +// +// # Auto-injected by opencodex +// developer_instructions = "" +// +// Replacement is "find the marker, replace the next line" — never a span search. +// Adjacency mirrors `injected-marker.ts:53-60`, tightened by a shape check. +// --------------------------------------------------------------------------- + +export const DEV_INSTRUCTIONS_KEY = "developer_instructions"; +const CANONICAL_LINE = /^developer_instructions = "(?:[^"\\]|\\.)*"$/; +export const ANY_DEV_INSTRUCTIONS = /^\s*(?:developer_instructions|"developer_instructions"|'developer_instructions')\s*=/; + +export type Ownership = + /** no such key anywhere in the root scope */ + | { state: "absent" } + /** marker-adjacent and canonically shaped: ours to rewrite */ + | { state: "owned"; line: number; literal: string } + /** marker-adjacent but reshaped: refuse, offer repair */ + | { state: "owned-malformed"; line: number; raw: string } + /** no marker: externally authored, refuse and offer adoption */ + | { state: "external"; line: number; raw: string }; + +export function inspectOwnership(configBytes: string | null): Ownership { + if (configBytes === null) return { state: "absent" }; + const lines = rootLines(configBytes); + for (let i = 0; i < lines.length; i += 1) { + const raw = lines[i]!; + if (!ANY_DEV_INSTRUCTIONS.test(raw)) continue; + const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER); + if (!marked) return { state: "external", line: i + 1, raw }; + if (!CANONICAL_LINE.test(raw)) return { state: "owned-malformed", line: i + 1, raw }; + const literal = raw.slice(`${DEV_INSTRUCTIONS_KEY} = `.length); + return { state: "owned", line: i + 1, literal }; + } + return { state: "absent" }; +} diff --git a/src/combos/identifiers.ts b/src/combos/identifiers.ts new file mode 100644 index 0000000000..ff2f231215 --- /dev/null +++ b/src/combos/identifiers.ts @@ -0,0 +1,89 @@ +import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models"; +import type { OcxComboConfig, OcxComboTarget, OcxConfig } from "../types"; + +export const COMBO_NAMESPACE = "combo"; + +export function preservesPhysicalComboProvider( + config: Pick, +): boolean { + return Object.hasOwn(config.providers, COMBO_NAMESPACE) + && Object.keys(config.combos ?? {}).length === 0; +} + +const COMBO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; + +/** True only for an explicitly opted-in bare native-family alias. */ +export function isNativeAliasCombo( + combo: { alias?: string | null; nativeAlias?: boolean }, +): boolean { + const alias = typeof combo.alias === "string" ? combo.alias.trim() : ""; + return combo.nativeAlias === true + && SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias); +} + +export function targetKey(target: Pick): string { + return `${target.provider}/${target.model}`; +} + +export function parseComboModelId(modelId: string): string | null { + const slash = modelId.indexOf("/"); + if (slash <= 0 || modelId.slice(0, slash) !== COMBO_NAMESPACE) return null; + const id = modelId.slice(slash + 1); + return id.length > 0 ? id : null; +} + +export function comboModelId(id: string): string { + return `${COMBO_NAMESPACE}/${id}`; +} + +/** Public model id clients request: the alias when set, else the default `combo/`. */ +export function comboPublicModelId(id: string, combo: { alias?: string | null }): string { + const alias = typeof combo.alias === "string" ? combo.alias.trim() : ""; + return alias || comboModelId(id); +} + +/** + * Persisted selector that hides a combo from discovery. Native aliases keep the canonical + * `combo/` selector because their bare public id remains the native OpenAI disable key. + */ +export function comboDisabledModelId( + id: string, + combo: { alias?: string | null; nativeAlias?: boolean }, +): string { + return isNativeAliasCombo(combo) ? comboModelId(id) : comboPublicModelId(id, combo); +} + +/** Every persisted selector that can refer to this combo in `disabledModels`. */ +export function comboDisabledModelSelectors( + id: string, + combo: { alias?: string | null; nativeAlias?: boolean }, +): string[] { + const canonical = comboModelId(id); + const preferred = comboDisabledModelId(id, combo); + return preferred === canonical ? [canonical] : [canonical, preferred]; +} + +/** + * Resolve a client-requested model id to a combo config key. The canonical `combo/` + * form wins first (back-compat); otherwise an exact alias match across configured combos. + */ +export function resolveComboId( + config: { combos?: Record }, + modelId: string, +): string | null { + const direct = parseComboModelId(modelId); + if (direct) return direct; + const combos = config.combos; + if (!combos) return null; + for (const [id, raw] of Object.entries(combos)) { + if (!raw || typeof raw !== "object") continue; + const alias = typeof raw.alias === "string" ? raw.alias.trim() : ""; + if (alias && alias === modelId) return id; + } + return null; +} + + +export function isValidComboId(id: string): boolean { + return COMBO_ID_PATTERN.test(id); +} diff --git a/src/combos/types.ts b/src/combos/types.ts index 0605530046..b5c5bf697c 100644 --- a/src/combos/types.ts +++ b/src/combos/types.ts @@ -1,26 +1,11 @@ import { isCodexReasoningEffort } from "../reasoning-effort"; import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models"; -import type { - OcxComboConfig, - OcxComboDefaultEffort, - OcxComboReasoningEffortMode, - OcxComboStrategy, - OcxComboTarget, - OcxConfig, - OcxProviderConfig, -} from "../types"; +import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types"; +import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers"; -export const COMBO_NAMESPACE = "combo"; export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0; +export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers"; -export function preservesPhysicalComboProvider( - config: Pick, -): boolean { - return Object.hasOwn(config.providers, COMBO_NAMESPACE) - && Object.keys(config.combos ?? {}).length === 0; -} - -const COMBO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; /** * Public alias shape: one optional "/" segment, each segment id-shaped. Bare aliases * (no "/") are the masquerade case — the combo answers to a mandated model id with no @@ -54,77 +39,6 @@ export interface NormalizedComboConfig { targets: Array>; } -/** True only for an explicitly opted-in bare native-family alias. */ -export function isNativeAliasCombo( - combo: { alias?: string | null; nativeAlias?: boolean }, -): boolean { - const alias = typeof combo.alias === "string" ? combo.alias.trim() : ""; - return combo.nativeAlias === true - && SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias); -} - -export function targetKey(target: Pick): string { - return `${target.provider}/${target.model}`; -} - -export function parseComboModelId(modelId: string): string | null { - const slash = modelId.indexOf("/"); - if (slash <= 0 || modelId.slice(0, slash) !== COMBO_NAMESPACE) return null; - const id = modelId.slice(slash + 1); - return id.length > 0 ? id : null; -} - -export function comboModelId(id: string): string { - return `${COMBO_NAMESPACE}/${id}`; -} - -/** Public model id clients request: the alias when set, else the default `combo/`. */ -export function comboPublicModelId(id: string, combo: { alias?: string | null }): string { - const alias = typeof combo.alias === "string" ? combo.alias.trim() : ""; - return alias || comboModelId(id); -} - -/** - * Persisted selector that hides a combo from discovery. Native aliases keep the canonical - * `combo/` selector because their bare public id remains the native OpenAI disable key. - */ -export function comboDisabledModelId( - id: string, - combo: { alias?: string | null; nativeAlias?: boolean }, -): string { - return isNativeAliasCombo(combo) ? comboModelId(id) : comboPublicModelId(id, combo); -} - -/** Every persisted selector that can refer to this combo in `disabledModels`. */ -export function comboDisabledModelSelectors( - id: string, - combo: { alias?: string | null; nativeAlias?: boolean }, -): string[] { - const canonical = comboModelId(id); - const preferred = comboDisabledModelId(id, combo); - return preferred === canonical ? [canonical] : [canonical, preferred]; -} - -/** - * Resolve a client-requested model id to a combo config key. The canonical `combo/` - * form wins first (back-compat); otherwise an exact alias match across configured combos. - */ -export function resolveComboId( - config: { combos?: Record }, - modelId: string, -): string | null { - const direct = parseComboModelId(modelId); - if (direct) return direct; - const combos = config.combos; - if (!combos) return null; - for (const [id, raw] of Object.entries(combos)) { - if (!raw || typeof raw !== "object") continue; - const alias = typeof raw.alias === "string" ? raw.alias.trim() : ""; - if (alias && alias === modelId) return id; - } - return null; -} - /** * Cross-combo alias checks that need the full combos map (uniqueness). Kept separate * from `comboConfigIssues` so config-file validation and the management API share it. @@ -410,10 +324,6 @@ export function comboDefaultEffort( : null; } -export function isValidComboId(id: string): boolean { - return COMBO_ID_PATTERN.test(id); -} - export function listComboIds(config: { combos?: Record }): string[] { return Object.keys(config.combos ?? {}).sort((a, b) => a.localeCompare(b)); } diff --git a/src/lib/redact-folding.ts b/src/lib/redact-folding.ts new file mode 100644 index 0000000000..9f5c1a70e4 --- /dev/null +++ b/src/lib/redact-folding.ts @@ -0,0 +1,176 @@ +/** + * Characters that render as a colon separator. Folded to `:` in the matching + * view so a look-alike cannot hide a header from the label pattern. + */ +const COLON_CONFUSABLES = new Set([ + "\uFF1A", "\uFE55", "\uFE13", "\uA789", "\u02D0", "\u2236", + "\u205A", "\u0589", "\u1361", "\u16EC", "\u1803", "\u2982", "\u2AF6", "\uFE30", +]); + +/** + * Characters dropped from the matching view: anything with no visible width + * that could split a label into pieces the pattern no longer recognizes. + * `\p{Default_Ignorable_Code_Point}` is the systematic answer — it covers the + * zero-width set, the bidi isolates and marks, the Mongolian vowel separator, + * and the variation selectors in one property instead of a list that review + * keeps finding another member of. `\p{Cf}` and combining marks are folded too. + */ +const INVISIBLE_FORMAT = /[\p{Default_Ignorable_Code_Point}\p{Cf}\p{Mn}\p{Me}]/u; + +/** + * HTML named character references. + * + * A hand-picked list is a coverage promise nobody can keep — review found + * `ⅈ`, `ⅇ`, and `ⅆ` decoding to compatibility letters that + * NFKD already maps onto `i`, `e`, and `d`, and the WHATWG table holds roughly + * 2200 entries. Neither Bun nor Node exposes that table, and pulling in a + * dependency to spell a header name is the wrong trade for this path. + * + * So names are not resolved at all. A named reference sitting inside a + * credential label is folded to a single placeholder character of unknown + * identity, and the label alternation accepts that placeholder wherever a + * letter may appear. Every named entity is covered, present and future, + * without claiming to know what any of them mean. + */ +const NAMED_ENTITY_PLACEHOLDER = "\u0001"; + +/** + * The handful of named references that spell a SEPARATOR rather than a letter. + * These have to resolve exactly, because the placeholder stands in for a letter + * position and a separator is structure, not a character of the name. + */ +const SEPARATOR_ENTITIES = new Map([ + ["colon", ":"], ["semi", ";"], ["equals", "="], ["quot", '"'], ["apos", "'"], + ["lt", "<"], ["gt", ">"], ["amp", "&"], ["sol", "/"], ["lowbar", "_"], + ["hyphen", "-"], ["dash", "-"], ["ndash", "-"], ["mdash", "-"], ["minus", "-"], + ["period", "."], ["comma", ","], ["num", "#"], ["nbsp", " "], +]); + +/** + * Latin look-alikes for the ASCII letters that appear in credential labels. + * Cyrillic `а`/`е`, Greek `ο`, fullwidth forms and the mathematical alphabets + * all render as the label to a human, so the matching view folds them back. + * NFKD handles the width/font variants; this table covers the cross-script + * homoglyphs NFKD deliberately leaves alone. + */ +const LETTER_CONFUSABLES = new Map([ + // Cyrillic + ["\u0430", "a"], ["\u0435", "e"], ["\u043E", "o"], ["\u0440", "p"], ["\u0441", "c"], + ["\u0445", "x"], ["\u0443", "y"], ["\u04BB", "h"], ["\u0455", "s"], ["\u0456", "i"], + ["\u0458", "j"], ["\u043A", "k"], ["\u0442", "t"], ["\u0432", "b"], ["\u043C", "m"], + ["\u043D", "h"], ["\u0501", "d"], ["\u0503", "g"], ["\u051B", "q"], ["\u051D", "w"], + ["\u04CF", "l"], ["\u0261", "g"], ["\u04AB", "c"], ["\u04BD", "e"], ["\u0459", "k"], + // Greek + ["\u03B1", "a"], ["\u03BF", "o"], ["\u03C1", "p"], ["\u03BD", "v"], ["\u03BA", "k"], + ["\u03B5", "e"], ["\u03C4", "t"], ["\u03B9", "i"], ["\u03C5", "u"], ["\u03C7", "x"], + ["\u03B7", "n"], ["\u03BC", "u"], ["\u03C3", "o"], ["\u03B2", "b"], ["\u03B3", "y"], + // Latin extended / other + ["\u0131", "i"], ["\u0269", "i"], ["\u1D0F", "o"], ["\u0280", "r"], ["\u01BF", "p"], + ["\u0578", "n"], ["\u057D", "u"], ["\u0585", "o"], ["\u0581", "g"], ["\u2044", "/"], +]); + +/** + * Build a folded copy plus an index map back to the original string, so the + * match runs on normalized text while the output keeps every byte the match did + * not cover. + */ +export function foldForMatching(value: string, decodeEscapes = true): { folded: string; map: number[] } { + let folded = ""; + const map: number[] = []; + // Serialization escapes are ALIASES for the label, not decoration: a JSON + // `\u0069`, a percent-encoded `%69`, and an XML `i` all spell the same + // field name to whatever parses the body, while spelling something else to a + // literal matcher. Decode them into the matching view (one folded character + // per escape, with the whole escape mapped back to its start) so + // `author\u0069zation`, `author%69zation`, and `authorization` are the + // label they claim to be. + const decodeEscape = (at: number): { ch: string; width: number } | null => { + // JSON `\uXXXX`, INCLUDING a surrogate pair. Decoding the halves + // independently left `\uD835\uDD69` as two lone surrogates, so the + // mathematical letter they spell was never normalized as one code point. + const json = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at, at + 6)); + if (json) { + const high = parseInt(json[1]!, 16); + if (high >= 0xd800 && high <= 0xdbff) { + const low = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at + 6, at + 12)); + const lowCode = low ? parseInt(low[1]!, 16) : NaN; + if (lowCode >= 0xdc00 && lowCode <= 0xdfff) { + return { ch: String.fromCharCode(high, lowCode), width: 12 }; + } + } + return { ch: String.fromCharCode(high), width: 6 }; + } + // Percent encoding is UTF-8: consecutive `%XX` bytes form ONE character. + // Decoding each byte on its own turned `%D0%B5` into two unrelated + // Latin-1 characters instead of the Cyrillic `е` the fold would have + // recognized. + const pct = /^(?:%[0-9a-fA-F]{2})+/.exec(value.slice(at, at + 24)); + if (pct) { + try { + const decoded = decodeURIComponent(pct[0]); + if (decoded.length >= 1) { + // Consume only the bytes that produced the FIRST character, so the + // rest of the sequence is decoded on the next iteration. + const first = String.fromCodePoint(decoded.codePointAt(0)!); + const bytes = new TextEncoder().encode(first).length; + return { ch: first, width: bytes * 3 }; + } + } catch { + const single = parseInt(pct[0].slice(1, 3), 16); + return { ch: String.fromCharCode(single), width: 3 }; + } + } + const xml = /^&#(x[0-9a-fA-F]{1,6}|[0-9]{1,7});/.exec(value.slice(at, at + 11)); + if (xml) { + const raw = xml[1]!; + const code = raw[0] === "x" || raw[0] === "X" + ? parseInt(raw.slice(1), 16) + : parseInt(raw, 10); + if (Number.isFinite(code) && code > 0 && code <= 0x10ffff) { + return { ch: String.fromCodePoint(code), width: xml[0].length }; + } + } + // HTML named references. `:` and the other separator names are + // resolved exactly; anything else folds to the opaque placeholder so the + // label still matches without pretending to know the character. + const named = /^&([A-Za-z][A-Za-z0-9]{1,31});/.exec(value.slice(at, at + 34)); + if (named) { + const separator = SEPARATOR_ENTITIES.get(named[1]!.toLowerCase()); + return { ch: separator ?? NAMED_ENTITY_PLACEHOLDER, width: named[0].length }; + } + return null; + }; + // Iterate by CODE POINT, not UTF-16 code unit: a supplementary character + // (mathematical letters, variation selectors above the BMP) is two units, so + // a per-unit loop hands each half to the property tests separately and + // neither half matches anything. `𝕩-api-key` and a U+E0100 inside a label + // both walked straight past the fold that way. + let i = 0; + while (i < value.length) { + const escaped = decodeEscapes ? decodeEscape(i) : null; + const ch = escaped ? escaped.ch : String.fromCodePoint(value.codePointAt(i)!); + const width = escaped ? escaped.width : ch.length; + if (INVISIBLE_FORMAT.test(ch)) { + i += width; + continue; + } + const mapped = COLON_CONFUSABLES.has(ch) + ? ":" + : LETTER_CONFUSABLES.get(ch.toLowerCase()) + // NFKD collapses fullwidth, circled, and mathematical letter variants + // onto their ASCII base. + ?? (ch.normalize("NFKD").length === 1 ? ch.normalize("NFKD") : ch); + // One folded unit per source code point keeps the offset map aligned; a + // multi-unit fold would desynchronize it, so those keep the original. + folded += mapped.length === 1 ? mapped : ch; + // One map entry per EMITTED UTF-16 unit. An escaped supplementary + // character emits two units, and giving it one entry desynchronized every + // later offset — the mask then landed mid-token and left part of the + // credential behind. + const emittedText = mapped.length === 1 ? mapped : ch; + for (let k = 0; k < emittedText.length; k += 1) map.push(i); + i += width; + } + map.push(value.length); + return { folded, map }; +} diff --git a/src/lib/redact.ts b/src/lib/redact.ts index ab82047aa3..f9e3e3ec6a 100644 --- a/src/lib/redact.ts +++ b/src/lib/redact.ts @@ -1,3 +1,5 @@ +import { foldForMatching } from "./redact-folding"; + export const REDACTED_SECRET = "[REDACTED]"; /** @@ -43,76 +45,6 @@ const CREDENTIAL_HEADER_LABEL_RAW = "x-api-key|x-goog-api-key|x-amz-security-tok const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW .replace(/(?([ - ["colon", ":"], ["semi", ";"], ["equals", "="], ["quot", '"'], ["apos", "'"], - ["lt", "<"], ["gt", ">"], ["amp", "&"], ["sol", "/"], ["lowbar", "_"], - ["hyphen", "-"], ["dash", "-"], ["ndash", "-"], ["mdash", "-"], ["minus", "-"], - ["period", "."], ["comma", ","], ["num", "#"], ["nbsp", " "], -]); - -/** - * Latin look-alikes for the ASCII letters that appear in credential labels. - * Cyrillic `а`/`е`, Greek `ο`, fullwidth forms and the mathematical alphabets - * all render as the label to a human, so the matching view folds them back. - * NFKD handles the width/font variants; this table covers the cross-script - * homoglyphs NFKD deliberately leaves alone. - */ -const LETTER_CONFUSABLES = new Map([ - // Cyrillic - ["\u0430", "a"], ["\u0435", "e"], ["\u043E", "o"], ["\u0440", "p"], ["\u0441", "c"], - ["\u0445", "x"], ["\u0443", "y"], ["\u04BB", "h"], ["\u0455", "s"], ["\u0456", "i"], - ["\u0458", "j"], ["\u043A", "k"], ["\u0442", "t"], ["\u0432", "b"], ["\u043C", "m"], - ["\u043D", "h"], ["\u0501", "d"], ["\u0503", "g"], ["\u051B", "q"], ["\u051D", "w"], - ["\u04CF", "l"], ["\u0261", "g"], ["\u04AB", "c"], ["\u04BD", "e"], ["\u0459", "k"], - // Greek - ["\u03B1", "a"], ["\u03BF", "o"], ["\u03C1", "p"], ["\u03BD", "v"], ["\u03BA", "k"], - ["\u03B5", "e"], ["\u03C4", "t"], ["\u03B9", "i"], ["\u03C5", "u"], ["\u03C7", "x"], - ["\u03B7", "n"], ["\u03BC", "u"], ["\u03C3", "o"], ["\u03B2", "b"], ["\u03B3", "y"], - // Latin extended / other - ["\u0131", "i"], ["\u0269", "i"], ["\u1D0F", "o"], ["\u0280", "r"], ["\u01BF", "p"], - ["\u0578", "n"], ["\u057D", "u"], ["\u0585", "o"], ["\u0581", "g"], ["\u2044", "/"], -]); // `\b` is the wrong left boundary for a header name: it matches after a `-` or // `_`, so `not-authorization:` and `internal_token:` were treated as the @@ -240,111 +172,6 @@ function maskOtherFramingsOnce(value: string, decodeEscapes: boolean): string { return current; } -/** - * Build a folded copy plus an index map back to the original string, so the - * match runs on normalized text while the output keeps every byte the match did - * not cover. - */ -function foldForMatching(value: string, decodeEscapes = true): { folded: string; map: number[] } { - let folded = ""; - const map: number[] = []; - // Serialization escapes are ALIASES for the label, not decoration: a JSON - // `\u0069`, a percent-encoded `%69`, and an XML `i` all spell the same - // field name to whatever parses the body, while spelling something else to a - // literal matcher. Decode them into the matching view (one folded character - // per escape, with the whole escape mapped back to its start) so - // `author\u0069zation`, `author%69zation`, and `authorization` are the - // label they claim to be. - const decodeEscape = (at: number): { ch: string; width: number } | null => { - // JSON `\uXXXX`, INCLUDING a surrogate pair. Decoding the halves - // independently left `\uD835\uDD69` as two lone surrogates, so the - // mathematical letter they spell was never normalized as one code point. - const json = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at, at + 6)); - if (json) { - const high = parseInt(json[1]!, 16); - if (high >= 0xd800 && high <= 0xdbff) { - const low = /^\\u([0-9a-fA-F]{4})/.exec(value.slice(at + 6, at + 12)); - const lowCode = low ? parseInt(low[1]!, 16) : NaN; - if (lowCode >= 0xdc00 && lowCode <= 0xdfff) { - return { ch: String.fromCharCode(high, lowCode), width: 12 }; - } - } - return { ch: String.fromCharCode(high), width: 6 }; - } - // Percent encoding is UTF-8: consecutive `%XX` bytes form ONE character. - // Decoding each byte on its own turned `%D0%B5` into two unrelated - // Latin-1 characters instead of the Cyrillic `е` the fold would have - // recognized. - const pct = /^(?:%[0-9a-fA-F]{2})+/.exec(value.slice(at, at + 24)); - if (pct) { - try { - const decoded = decodeURIComponent(pct[0]); - if (decoded.length >= 1) { - // Consume only the bytes that produced the FIRST character, so the - // rest of the sequence is decoded on the next iteration. - const first = String.fromCodePoint(decoded.codePointAt(0)!); - const bytes = new TextEncoder().encode(first).length; - return { ch: first, width: bytes * 3 }; - } - } catch { - const single = parseInt(pct[0].slice(1, 3), 16); - return { ch: String.fromCharCode(single), width: 3 }; - } - } - const xml = /^&#(x[0-9a-fA-F]{1,6}|[0-9]{1,7});/.exec(value.slice(at, at + 11)); - if (xml) { - const raw = xml[1]!; - const code = raw[0] === "x" || raw[0] === "X" - ? parseInt(raw.slice(1), 16) - : parseInt(raw, 10); - if (Number.isFinite(code) && code > 0 && code <= 0x10ffff) { - return { ch: String.fromCodePoint(code), width: xml[0].length }; - } - } - // HTML named references. `:` and the other separator names are - // resolved exactly; anything else folds to the opaque placeholder so the - // label still matches without pretending to know the character. - const named = /^&([A-Za-z][A-Za-z0-9]{1,31});/.exec(value.slice(at, at + 34)); - if (named) { - const separator = SEPARATOR_ENTITIES.get(named[1]!.toLowerCase()); - return { ch: separator ?? NAMED_ENTITY_PLACEHOLDER, width: named[0].length }; - } - return null; - }; - // Iterate by CODE POINT, not UTF-16 code unit: a supplementary character - // (mathematical letters, variation selectors above the BMP) is two units, so - // a per-unit loop hands each half to the property tests separately and - // neither half matches anything. `𝕩-api-key` and a U+E0100 inside a label - // both walked straight past the fold that way. - let i = 0; - while (i < value.length) { - const escaped = decodeEscapes ? decodeEscape(i) : null; - const ch = escaped ? escaped.ch : String.fromCodePoint(value.codePointAt(i)!); - const width = escaped ? escaped.width : ch.length; - if (INVISIBLE_FORMAT.test(ch)) { - i += width; - continue; - } - const mapped = COLON_CONFUSABLES.has(ch) - ? ":" - : LETTER_CONFUSABLES.get(ch.toLowerCase()) - // NFKD collapses fullwidth, circled, and mathematical letter variants - // onto their ASCII base. - ?? (ch.normalize("NFKD").length === 1 ? ch.normalize("NFKD") : ch); - // One folded unit per source code point keeps the offset map aligned; a - // multi-unit fold would desynchronize it, so those keep the original. - folded += mapped.length === 1 ? mapped : ch; - // One map entry per EMITTED UTF-16 unit. An escaped supplementary - // character emits two units, and giving it one entry desynchronized every - // later offset — the mask then landed mid-token and left part of the - // credential behind. - const emittedText = mapped.length === 1 ? mapped : ch; - for (let k = 0; k < emittedText.length; k += 1) map.push(i); - i += width; - } - map.push(value.length); - return { folded, map }; -} /** * Run the header rule over BOTH matching views and take the union. diff --git a/src/providers/openai-tiers-destination.ts b/src/providers/openai-tiers-destination.ts new file mode 100644 index 0000000000..5c6124f29d --- /dev/null +++ b/src/providers/openai-tiers-destination.ts @@ -0,0 +1,102 @@ +import type { OcxProviderConfig } from "../types"; +import { openaiResponsesUrl } from "../adapters/openai-responses-url"; + +export const OPENAI_CODEX_PROVIDER_ID = "openai"; +export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi"; +export const OPENAI_API_PROVIDER_ID = "openai-apikey"; +export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt"; + +export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex"; + +function normalizedBaseUrl(value: string): string | undefined { + try { + const url = new URL(value.trim()); + if (url.username || url.password || url.search || url.hash) return undefined; + const path = url.pathname.replace(/\/+$/, ""); + return `${url.origin}${path}`; + } catch { + return undefined; + } +} + +export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean { + return provider.adapter === "openai-responses" + && provider.authMode === "forward" + && normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL; +} + +const OPENAI_API_ORIGIN = "https://api.openai.com"; +const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`; +const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`; + +/** + * The Responses endpoint the adapter would actually POST key-auth traffic to, normalized. + * + * Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured + * `responsesPath` is appended to the base verbatim, and only the default branch runs the + * `/v1/responses` suffix normalization. Classifying on the base URL alone would call + * `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that + * request never reaches the official Responses endpoint. + */ +function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined { + try { + const raw = provider.responsesPath === undefined + ? openaiResponsesUrl(provider.baseUrl) + : `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`; + return normalizedBaseUrl(raw); + } catch { + return undefined; + } +} + +function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean { + // Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test` + // resolves to its own origin, never to the official one. + return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL; +} + +/** + * Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT + * backend can, and so can the official OpenAI API — but an arbitrary gateway that + * merely speaks the Responses wire cannot, and calling it there fails compaction + * with an unhelpful error instead of falling back to a routed summary (#422). + */ +export function supportsNativeResponsesCompactEndpoint( + providerName: string, + provider: OcxProviderConfig, +): boolean { + if (isCanonicalOpenAiForwardProvider(provider)) return true; + return providerName === OPENAI_API_PROVIDER_ID + && provider.adapter === "openai-responses" + && normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL; +} + +/** + * Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex + * surface or the official OpenAI API. + * + * Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not + * receive the caller's credentials (see the forward-header gate in the Responses adapter), so + * forward auth says nothing about which backend is on the other end. + */ +export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean { + if (isCanonicalOpenAiForwardProvider(provider)) return true; + return provider.adapter === "openai-responses" + && isOfficialOpenAiResponsesDestination(provider); +} + +/** + * Whether this destination can decode a native (non-`ocx1:`) compaction blob. + * + * Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal: + * the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a + * noncanonical forward provider receives no caller credentials and may point at any backend. + * + * Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in. + * Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the + * local config key a replay travels under. + */ +export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean { + return isOpenAiOperatedResponsesDestination(provider) + || provider.decodesNativeCompactionBlobs === true; +} diff --git a/src/providers/openai-tiers.ts b/src/providers/openai-tiers.ts index 5e99c89963..9b33d6e573 100644 --- a/src/providers/openai-tiers.ts +++ b/src/providers/openai-tiers.ts @@ -2,13 +2,9 @@ import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverla import { OPENAI_PROVIDER_TIER_VERSION } from "../types"; import { openaiResponsesUrl } from "../adapters/openai-responses-url"; import { MAX_COST4_RATE } from "../usage/expected-prices"; +import { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "./openai-tiers-destination"; +export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination"; -export const OPENAI_CODEX_PROVIDER_ID = "openai"; -export const LEGACY_OPENAI_MULTI_PROVIDER_ID = "openai-multi"; -export const OPENAI_API_PROVIDER_ID = "openai-apikey"; -export const LEGACY_CHATGPT_PROVIDER_ID = "chatgpt"; - -export const CODEX_FORWARD_BASE_URL = "https://chatgpt.com/backend-api/codex"; const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`; function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfig { @@ -20,99 +16,6 @@ function canonicalCodexForwardProvider(mode: CodexAccountMode): OcxProviderConfi }; } -function normalizedBaseUrl(value: string): string | undefined { - try { - const url = new URL(value.trim()); - if (url.username || url.password || url.search || url.hash) return undefined; - const path = url.pathname.replace(/\/+$/, ""); - return `${url.origin}${path}`; - } catch { - return undefined; - } -} - -export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): boolean { - return provider.adapter === "openai-responses" - && provider.authMode === "forward" - && normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL; -} - -const OPENAI_API_ORIGIN = "https://api.openai.com"; -const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`; -const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`; - -/** - * The Responses endpoint the adapter would actually POST key-auth traffic to, normalized. - * - * Mirrors the adapter's own construction (`src/adapters/openai-responses.ts`): a configured - * `responsesPath` is appended to the base verbatim, and only the default branch runs the - * `/v1/responses` suffix normalization. Classifying on the base URL alone would call - * `baseUrl: "https://api.openai.com"` with `responsesPath: "/other"` official even though that - * request never reaches the official Responses endpoint. - */ -function resolvedResponsesEndpoint(provider: OcxProviderConfig): string | undefined { - try { - const raw = provider.responsesPath === undefined - ? openaiResponsesUrl(provider.baseUrl) - : `${provider.baseUrl.replace(/\/$/, "")}${provider.responsesPath}`; - return normalizedBaseUrl(raw); - } catch { - return undefined; - } -} - -function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): boolean { - // Exact normalized URL keeps lookalike/suffix hosts out of this set: `api.openai.com.evil.test` - // resolves to its own origin, never to the official one. - return resolvedResponsesEndpoint(provider) === OPENAI_API_RESPONSES_URL; -} - -/** - * Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT - * backend can, and so can the official OpenAI API — but an arbitrary gateway that - * merely speaks the Responses wire cannot, and calling it there fails compaction - * with an unhelpful error instead of falling back to a routed summary (#422). - */ -export function supportsNativeResponsesCompactEndpoint( - providerName: string, - provider: OcxProviderConfig, -): boolean { - if (isCanonicalOpenAiForwardProvider(provider)) return true; - return providerName === OPENAI_API_PROVIDER_ID - && provider.adapter === "openai-responses" - && normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL; -} - -/** - * Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex - * surface or the official OpenAI API. - * - * Deliberately not keyed on `authMode === "forward"`: a noncanonical forward provider does not - * receive the caller's credentials (see the forward-header gate in the Responses adapter), so - * forward auth says nothing about which backend is on the other end. - */ -export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig): boolean { - if (isCanonicalOpenAiForwardProvider(provider)) return true; - return provider.adapter === "openai-responses" - && isOfficialOpenAiResponsesDestination(provider); -} - -/** - * Whether this destination can decode a native (non-`ocx1:`) compaction blob. - * - * Only the backend that minted a blob can decode it. `authMode: "forward"` alone is not a signal: - * the adapter forwards caller credentials only to the canonical ChatGPT Codex surface, while a - * noncanonical forward provider receives no caller credentials and may point at any backend. - * - * Relay only to an OpenAI-operated destination or a destination whose operator explicitly opts in. - * Keyed by destination rather than provider id: a blob's issuer is the URL that produced it, not the - * local config key a replay travels under. - */ -export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean { - return isOpenAiOperatedResponsesDestination(provider) - || provider.decodesNativeCompactionBlobs === true; -} - export interface OpenAiTierMigrationProjection { config: OcxConfig; changed: boolean; diff --git a/src/responses/parser-content.ts b/src/responses/parser-content.ts new file mode 100644 index 0000000000..4e29e6e03e --- /dev/null +++ b/src/responses/parser-content.ts @@ -0,0 +1,133 @@ +import type { OcxContentPart, OcxTextContent } from "../types"; + +export function isObj(v: unknown): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v); +} + +type InputBlock = + | { type: "input_text"; text: string } + | { type: "text"; text: string } + | { type: "input_image"; image_url?: string; file_id?: string; detail?: string } + | { type: "input_video"; video_url?: string } + | { type: "input_file"; file_id?: string; filename?: string; file_data?: string }; + +/** A usable reference string, or undefined. Empty strings and non-strings are not references. */ +function nonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +export function inputContentParts(blocks: unknown): string | OcxContentPart[] { + if (typeof blocks === "string") return blocks; + // The catch-all can also hand back a non-array `content` (an object, a number), which would + // throw at the loop below before any per-block guard runs. + if (!Array.isArray(blocks)) return []; + const parts: OcxContentPart[] = []; + for (const raw of blocks) { + // A malformed message item fails its strict schema and falls through to inputItemSchema's + // permissive catch-all, so blocks reaching here are NOT guaranteed to match the declared + // shape. Validate each field before use, as outputToToolResultContent already does. + if (!isObj(raw)) continue; + const block = raw as InputBlock; + if (block.type === "input_text" || block.type === "text") { + if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text }); + } else if (block.type === "input_image") { + const b = block as { image_url?: string; file_id?: string; detail?: string }; + const imageUrl = nonEmptyString(b.image_url); + const fileId = nonEmptyString(b.file_id); + const detail = nonEmptyString(b.detail); + if (imageUrl) { + // Preserve the image as a structured part — adapters send it as a native image block. + // NEVER inline the (often base64 data-URL) image_url as text: that explodes the token count. + parts.push({ type: "image", imageUrl, ...(detail ? { detail: normalizeImageDetail(detail) } : {}) }); + } else if (fileId) { + parts.push({ type: "text", text: `[image: ${fileId}]` }); // file_id ref → no inline data + } + // No usable reference: omit the block. A "[image: ?]" marker would claim an attachment + // the request never carried, which is worse than dropping malformed input. + } else if (block.type === "input_video") { + const videoUrl = nonEmptyString(block.video_url); + if (videoUrl) parts.push({ type: "video", videoUrl }); + } else if (block.type === "input_file") { + const b = block as { file_id?: string; filename?: string; file_data?: string }; + const fileId = nonEmptyString(b.file_id); + const fileData = nonEmptyString(b.file_data); + const filename = nonEmptyString(b.filename); + if (fileId) { + parts.push({ type: "text", text: `[file: ${fileId}]` }); + } else if (fileData) { + // Inline file_data is often large base64. Preserve only its presence and name, never bytes. + parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" }); + } + // A bare filename is not a file resource in the Responses schema, so omit it rather than + // fabricating a "[file: ...]" marker for an attachment that was never sent. + } + } + // Collapse to a plain string only for a single TEXT part; images must stay structured. + if (parts.length === 1 && parts[0].type === "text") return parts[0].text; + return parts; +} + +type OutputBlock = { type: "output_text"; text: string } | { type: "text"; text: string } | { type: "refusal"; refusal: string }; + +export function outputTextOf(blocks: unknown): OcxTextContent[] { + if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : []; + if (!Array.isArray(blocks)) return []; + const out: OcxTextContent[] = []; + for (const raw of blocks) { + // Same catch-all caveat as inputContentParts: validate before use. + if (!isObj(raw)) continue; + const b = raw as OutputBlock; + if (b.type === "output_text" || b.type === "text") { + if (typeof raw.text === "string") out.push({ type: "text", text: raw.text }); + } else if (b.type === "refusal") { + if (typeof raw.refusal === "string") out.push({ type: "text", text: `[refusal: ${raw.refusal}]` }); + } + } + return out; +} + +/** + * Tool-call output content. Preserves images (e.g. Codex `view_image` returns + * `input_image` items): returns content parts when any image is present, else a plain joined string. + * Never inlines an image_url as text (that would explode the token count). + */ +export function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] { + if (typeof output === "string") return output; + if (!Array.isArray(output)) return ""; + const parts: OcxContentPart[] = []; + let hasImage = false; + for (const raw of output) { + if (!isObj(raw)) continue; + if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") { + if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text }); + } else if (raw.type === "refusal" && typeof raw.refusal === "string") { + parts.push({ type: "text", text: `[refusal: ${raw.refusal}]` }); + } else if (raw.type === "input_image") { + const imageUrl = nonEmptyString(raw.image_url); + const fileId = nonEmptyString(raw.file_id); + if (imageUrl) { + parts.push({ type: "image", imageUrl, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) }); + hasImage = true; + } else if (fileId) { + parts.push({ type: "text", text: `[image: ${fileId}]` }); + } + } else if (raw.type === "encrypted_content") { + // codex-rs FunctionCallOutputContentItem::EncryptedContent — opaque to routed models. + parts.push({ type: "text", text: "[encrypted content omitted]" }); + } + } + if (!hasImage) return parts.map(p => (p.type === "text" ? p.text : "")).join(""); + return parts; +} + +export function toolOutputContainsEncryptedContent(output: string | unknown[] | undefined): boolean { + return Array.isArray(output) && output.some(raw => isObj(raw) && raw.type === "encrypted_content"); +} + +/** + * codex-rs ImageDetail allows "original", but chat-completions providers only accept + * auto|low|high on image_url.detail — degrade "original" to "high" (the codex default). + */ +function normalizeImageDetail(detail: string): string { + return detail === "original" ? "high" : detail; +} diff --git a/src/responses/parser-text-format.ts b/src/responses/parser-text-format.ts new file mode 100644 index 0000000000..08a27cdc36 --- /dev/null +++ b/src/responses/parser-text-format.ts @@ -0,0 +1,24 @@ +import type { OcxRequestOptions } from "../types"; +import { isObj } from "./parser-content"; + +/** + * The Responses `text.format` object when it requests structured output (json_schema or + * json_object), undefined otherwise. Acceptance is identical to the boolean detector this + * replaces; unknown or malformed formats are ignored, never rejected, so the native + * passthrough keeps forwarding whatever the caller sent via `_rawBody`. + */ +export function parseTextFormat(text: unknown): OcxRequestOptions["textFormat"] { + if (!isObj(text)) return undefined; + const format = (text as { format?: unknown }).format; + if (!isObj(format)) return undefined; + const f = format as { type?: unknown; name?: unknown; description?: unknown; schema?: unknown; strict?: unknown }; + if (f.type === "json_object") return { type: "json_object" }; + if (f.type !== "json_schema") return undefined; + return { + type: "json_schema", + ...(typeof f.name === "string" ? { name: f.name } : {}), + ...(typeof f.description === "string" ? { description: f.description } : {}), + ...(isObj(f.schema) ? { schema: f.schema as Record } : {}), + ...(typeof f.strict === "boolean" ? { strict: f.strict } : {}), + }; +} diff --git a/src/responses/parser-tools.ts b/src/responses/parser-tools.ts new file mode 100644 index 0000000000..8812b0bbe6 --- /dev/null +++ b/src/responses/parser-tools.ts @@ -0,0 +1,188 @@ +import type { OcxRequestOptions, OcxTool } from "../types"; +import { isObj } from "./parser-content"; +import { WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool"; +import { buildImageTool, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool"; +import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat"; + +export function mapToolChoice(value: unknown): OcxRequestOptions["toolChoice"] { + if (value === undefined || value === null) return undefined; + if (value === "auto" || value === "none" || value === "required") return value; + if (isObj(value) && "type" in value) { + const t = (value as { type: string }).type; + if ((t === "function" || t === "custom") && "name" in value) { + return { name: (value as { name: string }).name }; + } + // Hosted image tool types (with or without a name) map to the synthetic image_gen wire name. + if (t === "image_generation" || t === "image_gen") { + return { name: IMAGE_GEN_TOOL_NAME }; + } + if (t === "allowed_tools" && Array.isArray(value.tools)) { + const names = value.tools + .map(allowedToolName) + .filter((name): name is string => Boolean(name)); + return names.length > 0 + ? { allowedTools: [...new Set(names)], mode: value.mode === "required" ? "required" : "auto" } + : "none"; + } + return "auto"; + } + return undefined; +} + +function allowedToolName(tool: unknown): string | undefined { + if (!isObj(tool)) return undefined; + if (typeof tool.name === "string" && tool.name.length > 0) return tool.name; + if (tool.type === "web_search" || tool.type === "web_search_preview") return WEB_SEARCH_TOOL_NAME; + if (tool.type === "image_generation" || tool.type === "image_gen") return IMAGE_GEN_TOOL_NAME; + if (tool.type === "tool_search") return "tool_search"; + return undefined; +} + +export function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined { + if (!tools) return undefined; + const out: OcxTool[] = []; + const normalizeParameters = (raw: unknown): Record => { + if (isObj(raw) && raw.type === "object") return raw; + return { ...(isObj(raw) ? raw : {}), type: "object" }; + }; + const pushFn = (t: Record, namespace?: string) => { + // Hosted image_generation already installed the synthetic root tool. A later + // ordinary root `image_gen` must not create a second un-namespaced identity. + if ( + !namespace + && t.name === IMAGE_GEN_TOOL_NAME + && out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration) + ) { + return; + } + const tool: OcxTool = { + name: t.name as string, + description: (t.description as string) ?? "", + parameters: normalizeParameters(t.parameters), + }; + if (t.strict !== undefined) tool.strict = t.strict as boolean; + if (namespace) tool.namespace = namespace; + out.push(tool); + }; + const pushCustom = (t: Record, namespace?: string) => { + // Hosted image_generation already installed the synthetic root tool. A later + // root custom `image_gen` would collide on the same wire name with a different + // `freeform` flag and throw `ambiguous tool catalog`. + if ( + !namespace + && t.name === IMAGE_GEN_TOOL_NAME + && out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration) + ) { + return; + } + // Freeform custom tools are lowered to a single string `input` because chat models cannot + // emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the + // tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches + // routed models that the nested helper name is itself a callable top-level tool. + const inputDescription = t.name === "apply_patch" + ? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope." + : "Raw freeform input for this tool."; + const tool: OcxTool = { + name: t.name as string, + description: (t.description as string) ?? "", + parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] }, + freeform: true, + }; + if (namespace) tool.namespace = namespace; + out.push(tool); + }; + for (const t of tools) { + if (!isObj(t)) continue; + if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) { + pushFn(t.function as Record); + continue; + } + if (t.type === "function" && typeof t.name === "string") { + pushFn(t); + } else if (t.type === "namespace" && Array.isArray(t.tools)) { + // Codex 0.147 groups its ordinary client tools under the reserved `functions` namespace, + // including freeform custom tools such as code-mode `exec`. Those children are still + // top-level Responses tools, so flatten them without a namespace. Other namespace groups + // are MCP-style and keep their namespace for round-trip routing. + const builtinFunctions = t.name === "functions"; + const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined; + for (const inner of t.tools as unknown[]) { + if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns); + else if (isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner, ns); + } + } + else if (t.type === "custom" && typeof t.name === "string") { + pushCustom(t); + } + else if (t.type === "tool_search") { + // Client-executed tool discovery — the gateway to deferred tools (subagents, extra MCP tools). + // Expose as a function so chat models can call it; the bridge relays it as a tool_search_call. + out.push({ + name: "tool_search", + description: toolSearchDescription(t), + parameters: normalizeParameters(toolSearchParameters(t)), + toolSearch: true, + }); + } + else if (t.type === "image_generation" || t.type === "image_gen") { + // Keep Codex's image_gen visible to routed chat models. The hosted OpenAI tool + // cannot execute on Grok; the model still has to see a callable image_gen so + // Codex's client-side /v1/images request can fire and be relayed to xAI. + // Identity is the un-namespaced synthetic root (`imageGeneration: true`), not + // the bare name: a namespaced ordinary `image_gen` must not suppress it. + const synthetic = buildImageTool(); + // Every un-namespaced `image_gen` collides on one wire name, so removing only + // the first leaves a second root behind and the catalog stays ambiguous. + // Drop all root collisions, keep namespaced entries, then insert exactly one + // synthetic root — at the earliest colliding position so declaration order is + // preserved for models that read the catalog positionally. + let insertAt = -1; + for (let i = out.length - 1; i >= 0; i -= 1) { + const tool = out[i]!; + if (tool.name !== IMAGE_GEN_TOOL_NAME || tool.namespace) continue; + out.splice(i, 1); + insertAt = i; + } + if (insertAt >= 0) out.splice(insertAt, 0, synthetic); + else out.push(synthetic); + } + else if (typeof t.name === "string" && t.type !== "web_search" && t.type !== "image_generation") { + // Any OTHER named tool (e.g. a native/computer-use tool type opencodex doesn't explicitly + // model) is client-executed — pass it through as a function so the routed model can read and + // call it naturally; the bridge relays its call as a function_call. Previously such tools were + // silently dropped, so the model never saw them. + pushFn(t); + } + // Hosted web_search is still dropped here — the web-search sidecar re-injects it. + } + return out.length > 0 ? out : undefined; +} + +/** + * Namespace a custom tool was declared under, by its bare name. + * + * A `custom_tool_call` echoed back by the client carries only the bare name — the bridge + * emits `{"type":"custom_tool_call","name":"exec"}` even when the tool was declared as + * `mcp__functions__exec`. Without this lookup the namespace is lost on the return trip, + * and the adapters replay history through `namespacedToolName(namespace, name)`, which + * then produces a bare `exec` the provider may not have. Ordinary `function_call` items + * do not need this: they carry `namespace` on the wire. + */ +export function customToolNamespaces(tools: unknown): Map { + const out = new Map(); + if (!Array.isArray(tools)) return out; + for (const spec of tools) { + if (!isObj(spec) || spec.type !== "namespace" || !Array.isArray(spec.tools)) continue; + const namespace = typeof spec.name === "string" ? spec.name : undefined; + // Codex 0.147 groups ordinary client tools under the reserved `functions` namespace and + // buildTools deliberately flattens those without a namespace. Mirror that here, or the + // reconstruction would invent a namespace the request never advertised. + if (!namespace || namespace === "functions") continue; + for (const inner of spec.tools) { + if (!isObj(inner) || inner.type !== "custom" || typeof inner.name !== "string") continue; + // Ambiguous bare names are already rejected upstream, so first declaration wins. + if (!out.has(inner.name)) out.set(inner.name, namespace); + } + } + return out; +} diff --git a/src/responses/parser.ts b/src/responses/parser.ts index 6aab9f6028..c60f441406 100644 --- a/src/responses/parser.ts +++ b/src/responses/parser.ts @@ -22,9 +22,9 @@ import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synt import { buildImageTool, extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool"; import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat"; -function isObj(v: unknown): v is Record { - return typeof v === "object" && v !== null && !Array.isArray(v); -} +import { isObj, inputContentParts, outputTextOf, outputToToolResultContent, toolOutputContainsEncryptedContent } from "./parser-content"; +import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools"; +import { parseTextFormat } from "./parser-text-format"; /** * Wrap a remembered proxy-side signature as provider metadata for a replayed tool call. @@ -41,241 +41,7 @@ function replayThoughtSignatureMetadata( return signature ? { google: { thoughtSignature: signature } } : undefined; } -type InputBlock = - | { type: "input_text"; text: string } - | { type: "text"; text: string } - | { type: "input_image"; image_url?: string; file_id?: string; detail?: string } - | { type: "input_video"; video_url?: string } - | { type: "input_file"; file_id?: string; filename?: string; file_data?: string }; - -/** A usable reference string, or undefined. Empty strings and non-strings are not references. */ -function nonEmptyString(value: unknown): string | undefined { - return typeof value === "string" && value.length > 0 ? value : undefined; -} - -function inputContentParts(blocks: unknown): string | OcxContentPart[] { - if (typeof blocks === "string") return blocks; - // The catch-all can also hand back a non-array `content` (an object, a number), which would - // throw at the loop below before any per-block guard runs. - if (!Array.isArray(blocks)) return []; - const parts: OcxContentPart[] = []; - for (const raw of blocks) { - // A malformed message item fails its strict schema and falls through to inputItemSchema's - // permissive catch-all, so blocks reaching here are NOT guaranteed to match the declared - // shape. Validate each field before use, as outputToToolResultContent already does. - if (!isObj(raw)) continue; - const block = raw as InputBlock; - if (block.type === "input_text" || block.type === "text") { - if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text }); - } else if (block.type === "input_image") { - const b = block as { image_url?: string; file_id?: string; detail?: string }; - const imageUrl = nonEmptyString(b.image_url); - const fileId = nonEmptyString(b.file_id); - const detail = nonEmptyString(b.detail); - if (imageUrl) { - // Preserve the image as a structured part — adapters send it as a native image block. - // NEVER inline the (often base64 data-URL) image_url as text: that explodes the token count. - parts.push({ type: "image", imageUrl, ...(detail ? { detail: normalizeImageDetail(detail) } : {}) }); - } else if (fileId) { - parts.push({ type: "text", text: `[image: ${fileId}]` }); // file_id ref → no inline data - } - // No usable reference: omit the block. A "[image: ?]" marker would claim an attachment - // the request never carried, which is worse than dropping malformed input. - } else if (block.type === "input_video") { - const videoUrl = nonEmptyString(block.video_url); - if (videoUrl) parts.push({ type: "video", videoUrl }); - } else if (block.type === "input_file") { - const b = block as { file_id?: string; filename?: string; file_data?: string }; - const fileId = nonEmptyString(b.file_id); - const fileData = nonEmptyString(b.file_data); - const filename = nonEmptyString(b.filename); - if (fileId) { - parts.push({ type: "text", text: `[file: ${fileId}]` }); - } else if (fileData) { - // Inline file_data is often large base64. Preserve only its presence and name, never bytes. - parts.push({ type: "text", text: filename ? `[file: ${filename}]` : "[file: inline data]" }); - } - // A bare filename is not a file resource in the Responses schema, so omit it rather than - // fabricating a "[file: ...]" marker for an attachment that was never sent. - } - } - // Collapse to a plain string only for a single TEXT part; images must stay structured. - if (parts.length === 1 && parts[0].type === "text") return parts[0].text; - return parts; -} - -type OutputBlock = { type: "output_text"; text: string } | { type: "text"; text: string } | { type: "refusal"; refusal: string }; - -function outputTextOf(blocks: unknown): OcxTextContent[] { - if (typeof blocks === "string") return blocks.length > 0 ? [{ type: "text", text: blocks }] : []; - if (!Array.isArray(blocks)) return []; - const out: OcxTextContent[] = []; - for (const raw of blocks) { - // Same catch-all caveat as inputContentParts: validate before use. - if (!isObj(raw)) continue; - const b = raw as OutputBlock; - if (b.type === "output_text" || b.type === "text") { - if (typeof raw.text === "string") out.push({ type: "text", text: raw.text }); - } else if (b.type === "refusal") { - if (typeof raw.refusal === "string") out.push({ type: "text", text: `[refusal: ${raw.refusal}]` }); - } - } - return out; -} - -function mapToolChoice(value: unknown): OcxRequestOptions["toolChoice"] { - if (value === undefined || value === null) return undefined; - if (value === "auto" || value === "none" || value === "required") return value; - if (isObj(value) && "type" in value) { - const t = (value as { type: string }).type; - if ((t === "function" || t === "custom") && "name" in value) { - return { name: (value as { name: string }).name }; - } - // Hosted image tool types (with or without a name) map to the synthetic image_gen wire name. - if (t === "image_generation" || t === "image_gen") { - return { name: IMAGE_GEN_TOOL_NAME }; - } - if (t === "allowed_tools" && Array.isArray(value.tools)) { - const names = value.tools - .map(allowedToolName) - .filter((name): name is string => Boolean(name)); - return names.length > 0 - ? { allowedTools: [...new Set(names)], mode: value.mode === "required" ? "required" : "auto" } - : "none"; - } - return "auto"; - } - return undefined; -} - -function allowedToolName(tool: unknown): string | undefined { - if (!isObj(tool)) return undefined; - if (typeof tool.name === "string" && tool.name.length > 0) return tool.name; - if (tool.type === "web_search" || tool.type === "web_search_preview") return WEB_SEARCH_TOOL_NAME; - if (tool.type === "image_generation" || tool.type === "image_gen") return IMAGE_GEN_TOOL_NAME; - if (tool.type === "tool_search") return "tool_search"; - return undefined; -} -function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined { - if (!tools) return undefined; - const out: OcxTool[] = []; - const normalizeParameters = (raw: unknown): Record => { - if (isObj(raw) && raw.type === "object") return raw; - return { ...(isObj(raw) ? raw : {}), type: "object" }; - }; - const pushFn = (t: Record, namespace?: string) => { - // Hosted image_generation already installed the synthetic root tool. A later - // ordinary root `image_gen` must not create a second un-namespaced identity. - if ( - !namespace - && t.name === IMAGE_GEN_TOOL_NAME - && out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration) - ) { - return; - } - const tool: OcxTool = { - name: t.name as string, - description: (t.description as string) ?? "", - parameters: normalizeParameters(t.parameters), - }; - if (t.strict !== undefined) tool.strict = t.strict as boolean; - if (namespace) tool.namespace = namespace; - out.push(tool); - }; - const pushCustom = (t: Record, namespace?: string) => { - // Hosted image_generation already installed the synthetic root tool. A later - // root custom `image_gen` would collide on the same wire name with a different - // `freeform` flag and throw `ambiguous tool catalog`. - if ( - !namespace - && t.name === IMAGE_GEN_TOOL_NAME - && out.some(tool => tool.name === IMAGE_GEN_TOOL_NAME && !tool.namespace && tool.imageGeneration) - ) { - return; - } - // Freeform custom tools are lowered to a single string `input` because chat models cannot - // emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the - // tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches - // routed models that the nested helper name is itself a callable top-level tool. - const inputDescription = t.name === "apply_patch" - ? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope." - : "Raw freeform input for this tool."; - const tool: OcxTool = { - name: t.name as string, - description: (t.description as string) ?? "", - parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] }, - freeform: true, - }; - if (namespace) tool.namespace = namespace; - out.push(tool); - }; - for (const t of tools) { - if (!isObj(t)) continue; - if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) { - pushFn(t.function as Record); - continue; - } - if (t.type === "function" && typeof t.name === "string") { - pushFn(t); - } else if (t.type === "namespace" && Array.isArray(t.tools)) { - // Codex 0.147 groups its ordinary client tools under the reserved `functions` namespace, - // including freeform custom tools such as code-mode `exec`. Those children are still - // top-level Responses tools, so flatten them without a namespace. Other namespace groups - // are MCP-style and keep their namespace for round-trip routing. - const builtinFunctions = t.name === "functions"; - const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined; - for (const inner of t.tools as unknown[]) { - if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns); - else if (isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner, ns); - } - } - else if (t.type === "custom" && typeof t.name === "string") { - pushCustom(t); - } - else if (t.type === "tool_search") { - // Client-executed tool discovery — the gateway to deferred tools (subagents, extra MCP tools). - // Expose as a function so chat models can call it; the bridge relays it as a tool_search_call. - out.push({ - name: "tool_search", - description: toolSearchDescription(t), - parameters: normalizeParameters(toolSearchParameters(t)), - toolSearch: true, - }); - } - else if (t.type === "image_generation" || t.type === "image_gen") { - // Keep Codex's image_gen visible to routed chat models. The hosted OpenAI tool - // cannot execute on Grok; the model still has to see a callable image_gen so - // Codex's client-side /v1/images request can fire and be relayed to xAI. - // Identity is the un-namespaced synthetic root (`imageGeneration: true`), not - // the bare name: a namespaced ordinary `image_gen` must not suppress it. - const synthetic = buildImageTool(); - // Every un-namespaced `image_gen` collides on one wire name, so removing only - // the first leaves a second root behind and the catalog stays ambiguous. - // Drop all root collisions, keep namespaced entries, then insert exactly one - // synthetic root — at the earliest colliding position so declaration order is - // preserved for models that read the catalog positionally. - let insertAt = -1; - for (let i = out.length - 1; i >= 0; i -= 1) { - const tool = out[i]!; - if (tool.name !== IMAGE_GEN_TOOL_NAME || tool.namespace) continue; - out.splice(i, 1); - insertAt = i; - } - if (insertAt >= 0) out.splice(insertAt, 0, synthetic); - else out.push(synthetic); - } - else if (typeof t.name === "string" && t.type !== "web_search" && t.type !== "image_generation") { - // Any OTHER named tool (e.g. a native/computer-use tool type opencodex doesn't explicitly - // model) is client-executed — pass it through as a function so the routed model can read and - // call it naturally; the bridge relays its call as a function_call. Previously such tools were - // silently dropped, so the model never saw them. - pushFn(t); - } - // Hosted web_search is still dropped here — the web-search sidecar re-injects it. - } - return out.length > 0 ? out : undefined; -} function ensureAssistantPlaceholder(messages: OcxMessage[], modelId: string, now: number): OcxAssistantMessage { const last = messages[messages.length - 1]; @@ -285,51 +51,6 @@ function ensureAssistantPlaceholder(messages: OcxMessage[], modelId: string, now return placeholder; } -/** - * Tool-call output content. Preserves images (e.g. Codex `view_image` returns - * `input_image` items): returns content parts when any image is present, else a plain joined string. - * Never inlines an image_url as text (that would explode the token count). - */ -function outputToToolResultContent(output: string | unknown[] | undefined): string | OcxContentPart[] { - if (typeof output === "string") return output; - if (!Array.isArray(output)) return ""; - const parts: OcxContentPart[] = []; - let hasImage = false; - for (const raw of output) { - if (!isObj(raw)) continue; - if (raw.type === "output_text" || raw.type === "text" || raw.type === "input_text") { - if (typeof raw.text === "string") parts.push({ type: "text", text: raw.text }); - } else if (raw.type === "refusal" && typeof raw.refusal === "string") { - parts.push({ type: "text", text: `[refusal: ${raw.refusal}]` }); - } else if (raw.type === "input_image") { - const imageUrl = nonEmptyString(raw.image_url); - const fileId = nonEmptyString(raw.file_id); - if (imageUrl) { - parts.push({ type: "image", imageUrl, ...(typeof raw.detail === "string" ? { detail: normalizeImageDetail(raw.detail) } : {}) }); - hasImage = true; - } else if (fileId) { - parts.push({ type: "text", text: `[image: ${fileId}]` }); - } - } else if (raw.type === "encrypted_content") { - // codex-rs FunctionCallOutputContentItem::EncryptedContent — opaque to routed models. - parts.push({ type: "text", text: "[encrypted content omitted]" }); - } - } - if (!hasImage) return parts.map(p => (p.type === "text" ? p.text : "")).join(""); - return parts; -} - -function toolOutputContainsEncryptedContent(output: string | unknown[] | undefined): boolean { - return Array.isArray(output) && output.some(raw => isObj(raw) && raw.type === "encrypted_content"); -} - -/** - * codex-rs ImageDetail allows "original", but chat-completions providers only accept - * auto|low|high on image_url.detail — degrade "original" to "high" (the codex default). - */ -function normalizeImageDetail(detail: string): string { - return detail === "original" ? "high" : detail; -} function findToolById(messages: OcxMessage[], callId: string): { name: string; namespace?: string } { for (let i = messages.length - 1; i >= 0; i--) { @@ -372,34 +93,6 @@ function attachPendingReasoningToCallOwner( const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]); -/** - * Namespace a custom tool was declared under, by its bare name. - * - * A `custom_tool_call` echoed back by the client carries only the bare name — the bridge - * emits `{"type":"custom_tool_call","name":"exec"}` even when the tool was declared as - * `mcp__functions__exec`. Without this lookup the namespace is lost on the return trip, - * and the adapters replay history through `namespacedToolName(namespace, name)`, which - * then produces a bare `exec` the provider may not have. Ordinary `function_call` items - * do not need this: they carry `namespace` on the wire. - */ -function customToolNamespaces(tools: unknown): Map { - const out = new Map(); - if (!Array.isArray(tools)) return out; - for (const spec of tools) { - if (!isObj(spec) || spec.type !== "namespace" || !Array.isArray(spec.tools)) continue; - const namespace = typeof spec.name === "string" ? spec.name : undefined; - // Codex 0.147 groups ordinary client tools under the reserved `functions` namespace and - // buildTools deliberately flattens those without a namespace. Mirror that here, or the - // reconstruction would invent a namespace the request never advertised. - if (!namespace || namespace === "functions") continue; - for (const inner of spec.tools) { - if (!isObj(inner) || inner.type !== "custom" || typeof inner.name !== "string") continue; - // Ambiguous bare names are already rejected upstream, so first declaration wins. - if (!out.has(inner.name)) out.set(inner.name, namespace); - } - } - return out; -} export function parseRequest( body: unknown, @@ -865,25 +558,3 @@ export function parseRequest( ...(contextCompactionBoundary ? { _contextCompactionBoundary: true } : {}), }; } - -/** - * The Responses `text.format` object when it requests structured output (json_schema or - * json_object), undefined otherwise. Acceptance is identical to the boolean detector this - * replaces; unknown or malformed formats are ignored, never rejected, so the native - * passthrough keeps forwarding whatever the caller sent via `_rawBody`. - */ -function parseTextFormat(text: unknown): OcxRequestOptions["textFormat"] { - if (!isObj(text)) return undefined; - const format = (text as { format?: unknown }).format; - if (!isObj(format)) return undefined; - const f = format as { type?: unknown; name?: unknown; description?: unknown; schema?: unknown; strict?: unknown }; - if (f.type === "json_object") return { type: "json_object" }; - if (f.type !== "json_schema") return undefined; - return { - type: "json_schema", - ...(typeof f.name === "string" ? { name: f.name } : {}), - ...(typeof f.description === "string" ? { description: f.description } : {}), - ...(isObj(f.schema) ? { schema: f.schema as Record } : {}), - ...(typeof f.strict === "boolean" ? { strict: f.strict } : {}), - }; -} diff --git a/src/server/system-env-shell.ts b/src/server/system-env-shell.ts new file mode 100644 index 0000000000..35954f035e --- /dev/null +++ b/src/server/system-env-shell.ts @@ -0,0 +1,238 @@ +import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs"; +import { delimiter, join } from "node:path"; +import { getConfigDir } from "../config"; +import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows"; +import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect"; +import { resolveClaudeAuthMode } from "../claude/auth-mode"; +import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context"; +import type { OcxConfig } from "../types"; +import { recordOwnedConfigPath } from "../lib/config-ownership"; + +/** + * Does the opencodex dummy marker belong in the system environment? + * + * Keyed on the SAME resolver `ocx claude` uses, so an auto config with no Claude auth + * also reaches plain `claude` launches — before this, auto-absent users got nothing + * from auto-connect and the feature looked broken for exactly the people it helps + * (devlog 260726_claude_auth_auto/035). + * + * NOTE this is a SNAPSHOT: the file only changes when this runs (proxy start, `ocx + * ensure`, or a settings save). `ocx claude` re-resolves live on every launch. + */ +export type SystemEnvDeps = { + /** Test seam; production uses the authenticated Node-launcher context. */ + preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null; + /** Test seam for auth sources; `env` and `ownTokens` stay bound below. */ + authDetect?: Omit, "env" | "ownTokens">; +}; + +/** + * Bun may synthesize Anthropic variables from a project `.env` before this module runs. + * Only values recorded by the plain-Node launcher are trusted as parent exports. Direct + * Bun/service launches have no proof-bound slot list, so they fail closed and let the + * file/keychain auth sources decide instead of allowing dotenv to select subscription mode. + */ +function systemEnvAnthropicEnv( + env: NodeJS.ProcessEnv, + preBunAnthropicSlots: readonly AnthropicParentEnvSlot[] | null | undefined, +): NodeJS.ProcessEnv { + const trustedSlots = preBunAnthropicSlots === undefined + ? trustedNodeLauncherContext()?.anthropicEnvSlots ?? [] + : preBunAnthropicSlots ?? []; + const exported = new Set(trustedSlots); + const sanitized = { ...env }; + for (const name of ANTHROPIC_PARENT_ENV_SLOTS) { + if (sanitized[name] !== undefined && !exported.has(name)) delete sanitized[name]; + } + return sanitized; +} + +export function systemEnvMarkerMode(config: OcxConfig, deps: SystemEnvDeps = {}): "proxy" | "subscription" { + const env = systemEnvAnthropicEnv(process.env, deps.preBunAnthropicSlots); + const ownTokens = ownAdmissionTokens(config); + return resolveClaudeAuthMode(config, detectClaudeAuth({ + ...defaultAuthDetectDeps(env, ownTokens), + ...(deps.authDetect ?? {}), + env: () => env, + ownTokens, + })).markerMode; +} + +// --------------------------------------------------------------------------- +// Shell-hook env file: written on inject, sourced by the shell hook in .zshrc. +// This works for ALL new shells immediately, unlike launchctl setenv which only +// reaches processes launched directly by launchd (not Terminal.app children). +// --------------------------------------------------------------------------- + +export function getShellEnvFilePath(): string { + return join(getConfigDir(), "claude-env.sh"); +} + +function shellValue(value: string): string { + return `'${value.replaceAll("'", `'\\''`)}'`; +} + +export function writeShellEnvFile( + port: number, + config: OcxConfig, + modelEnv: Record = {}, + auto?: AutoContextMode, + deps: SystemEnvDeps = {}, +): void { + const lines = [ + `# Generated by opencodex — do not edit manually`, + `export ANTHROPIC_BASE_URL=${shellValue(`http://127.0.0.1:${port}`)}`, + `export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`, + ]; + // New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already + // exported in their shell wins even though launchctl knows nothing about it. + const conditional = (name: string, value: string) => + `[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`; + if (systemEnvMarkerMode(config, deps) === "proxy") { + if (config.apiKeys?.length) { + lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`); + } else { + lines.push(conditional("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER)); + } + } + // Model slots (default + tiers + legacy small-fast) with [1m] applied (devlog 260712 B2). + if (modelEnv.ANTHROPIC_MODEL) { + lines.push(`export ANTHROPIC_MODEL=${shellValue(modelEnv.ANTHROPIC_MODEL)}`); + } else if (config.claudeCode?.model) { + lines.push(`export ANTHROPIC_MODEL=${shellValue(config.claudeCode.model)}`); + } + for (const [name, value] of Object.entries(modelEnv)) { + if (name === "ANTHROPIC_MODEL") continue; + lines.push(conditional(name, value)); + } + const maxCtx = config.claudeCode?.maxContextTokens; + if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) { + lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)))); + lines.push(conditional("DISABLE_COMPACT", "1")); + } + // Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl. + const autoShell = auto ?? resolveAutoContext(config.claudeCode); + if (autoShell.enabled) lines.push(conditional("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(autoShell.compactWindow))); + if (config.claudeCode?.alwaysEnableEffort === true) { + lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1")); + } + const shellEnvPath = getShellEnvFilePath(); + recordOwnedConfigPath(getConfigDir(), shellEnvPath); + mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 }); + writeFileSync(shellEnvPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 }); +} + +export function removeShellEnvFile(): void { + try { unlinkSync(getShellEnvFilePath()); } catch { /* already gone */ } +} + +// --------------------------------------------------------------------------- +// .zshrc hook auto-install: adds a one-liner that sources claude-env.sh. +// Idempotent — skips if the hook line already exists. +// --------------------------------------------------------------------------- + +const SHELL_HOOK_MARKER = "# opencodex claude-env hook"; +const SHELL_HOOK_LINE = `${SHELL_HOOK_MARKER}\n[ -f ~/.opencodex/claude-env.sh ] && source ~/.opencodex/claude-env.sh`; + +export function installShellHook(): { installed: boolean; reason?: string } { + if (process.platform !== "darwin") return { installed: false, reason: "not macOS" }; + const home = process.env.HOME; + if (!home) return { installed: false, reason: "no HOME" }; + const zshrcPath = join(home, ".zshrc"); + try { + let content = ""; + try { content = readFileSync(zshrcPath, "utf8"); } catch { /* file doesn't exist yet */ } + if (content.includes(SHELL_HOOK_MARKER)) return { installed: false, reason: "already installed" }; + const addition = `\n${SHELL_HOOK_LINE}\n`; + writeFileSync(zshrcPath, content + addition, { encoding: "utf8", mode: 0o644 }); + return { installed: true }; + } catch (err) { + return { installed: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` }; + } +} + +export function uninstallShellHook(): { removed: boolean; reason?: string } { + if (process.platform !== "darwin") return { removed: false, reason: "not macOS" }; + const home = process.env.HOME; + if (!home) return { removed: false, reason: "no HOME" }; + const zshrcPath = join(home, ".zshrc"); + try { + const content = readFileSync(zshrcPath, "utf8"); + if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" }; + // Match CR?LF, not LF alone. A .zshrc with CRLF line endings — ordinary on a home + // directory an editor or another OS has touched — did not match, so the file was + // rewritten unchanged and the caller was told the hook was removed. Reporting success + // while the hook still sources on every new shell is the worse of the two failures. + const cleaned = content.replace(/\r?\n?# opencodex claude-env hook\r?\n\[.*claude-env\.sh.*(?:\r?\n)?/g, "\n"); + // Verify instead of assuming: if the marker survives, the block is shaped in a way this + // pattern does not own, and the honest answer is failure rather than a silent no-op. + if (cleaned.includes(SHELL_HOOK_MARKER)) { + return { removed: false, reason: "hook block present but not in the expected shape; remove it manually" }; + } + writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 }); + return { removed: true }; + } catch (error) { + if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") { + return { removed: false, reason: "not installed" }; + } + return { removed: false, reason: "read/write failed" }; + } +} + +/** Whether a real `claude` executable is discoverable from this process's PATH. */ +export function claudeCodeCliInstalled(pathValue = process.env.PATH): boolean { + if (!pathValue) return false; + for (const directory of pathValue.split(delimiter)) { + // An empty PATH segment means the current directory. Do not let the proxy treat a + // workspace-local file as a durable user installation. + if (!directory) continue; + const candidate = join(directory, "claude"); + try { + if (!statSync(candidate).isFile()) continue; + accessSync(candidate, constants.X_OK); + return true; + } catch { + // Keep scanning PATH after missing, non-file, and non-executable entries. + } + } + return false; +} + +/** + * Keep the shell hook aligned with the integration that can actually consume it. + * Claude Desktop uses its own profile and does not source `.zshrc`; this hook exists + * only for plain Claude Code CLI launches. + * + * Reconciliation is PATH-sensitive by construction: "Claude Code is installed" is answered + * from the PATH of whichever process calls this. A launchd/service context with a stripped + * PATH can therefore fail to see a `claude` the user's interactive shell finds, and this will + * remove the hook. That is the intended failure direction — removing an OpenCodex-owned block + * is reversible on the next foreground `ocx start`, whereas leaving a hook pointing at an + * uninstalled CLI is the stale state this reconciliation exists to clear. Only the block + * carrying our own marker is ever touched; user lines are preserved. + */ +export function reconcileShellHook(systemEnvInjected: boolean): { + changed: boolean; + state: "installed" | "absent" | "failed"; + reason?: string; +} { + if (process.platform !== "darwin") return { changed: false, state: "absent", reason: "not macOS" }; + if (systemEnvInjected && claudeCodeCliInstalled()) { + const result = installShellHook(); + if (result.installed) return { changed: true, state: "installed" }; + if (result.reason === "already installed") { + return { changed: false, state: "installed", reason: result.reason }; + } + return { changed: false, state: "failed", reason: result.reason ?? "install failed" }; + } + + const result = uninstallShellHook(); + if (!result.removed && result.reason !== "not installed") { + return { changed: false, state: "failed", reason: result.reason ?? "remove failed" }; + } + return { + changed: result.removed, + state: "absent", + reason: systemEnvInjected ? "Claude Code not installed" : "system environment inactive", + }; +} diff --git a/src/server/system-env.ts b/src/server/system-env.ts index 777fdd5828..5825b2a2a1 100644 --- a/src/server/system-env.ts +++ b/src/server/system-env.ts @@ -1,245 +1,18 @@ import { execFileSync } from "node:child_process"; -import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs"; -import { delimiter, join } from "node:path"; +import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; +import { join } from "node:path"; import { getConfigDir } from "../config"; import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows"; -import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect"; -import { resolveClaudeAuthMode } from "../claude/auth-mode"; -import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context"; +import { PROXY_MARKER } from "../claude/auth-detect"; import { isProxyAdmissionSecret } from "./auth-cors"; import type { OcxConfig } from "../types"; import { recordOwnedConfigPath } from "../lib/config-ownership"; import { providerContextCap } from "../providers/context-cap"; import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; - -/** - * Does the opencodex dummy marker belong in the system environment? - * - * Keyed on the SAME resolver `ocx claude` uses, so an auto config with no Claude auth - * also reaches plain `claude` launches — before this, auto-absent users got nothing - * from auto-connect and the feature looked broken for exactly the people it helps - * (devlog 260726_claude_auth_auto/035). - * - * NOTE this is a SNAPSHOT: the file only changes when this runs (proxy start, `ocx - * ensure`, or a settings save). `ocx claude` re-resolves live on every launch. - */ -export type SystemEnvDeps = { - /** Test seam; production uses the authenticated Node-launcher context. */ - preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null; - /** Test seam for auth sources; `env` and `ownTokens` stay bound below. */ - authDetect?: Omit, "env" | "ownTokens">; -}; - -/** - * Bun may synthesize Anthropic variables from a project `.env` before this module runs. - * Only values recorded by the plain-Node launcher are trusted as parent exports. Direct - * Bun/service launches have no proof-bound slot list, so they fail closed and let the - * file/keychain auth sources decide instead of allowing dotenv to select subscription mode. - */ -function systemEnvAnthropicEnv( - env: NodeJS.ProcessEnv, - preBunAnthropicSlots: readonly AnthropicParentEnvSlot[] | null | undefined, -): NodeJS.ProcessEnv { - const trustedSlots = preBunAnthropicSlots === undefined - ? trustedNodeLauncherContext()?.anthropicEnvSlots ?? [] - : preBunAnthropicSlots ?? []; - const exported = new Set(trustedSlots); - const sanitized = { ...env }; - for (const name of ANTHROPIC_PARENT_ENV_SLOTS) { - if (sanitized[name] !== undefined && !exported.has(name)) delete sanitized[name]; - } - return sanitized; -} - -function systemEnvMarkerMode(config: OcxConfig, deps: SystemEnvDeps = {}): "proxy" | "subscription" { - const env = systemEnvAnthropicEnv(process.env, deps.preBunAnthropicSlots); - const ownTokens = ownAdmissionTokens(config); - return resolveClaudeAuthMode(config, detectClaudeAuth({ - ...defaultAuthDetectDeps(env, ownTokens), - ...(deps.authDetect ?? {}), - env: () => env, - ownTokens, - })).markerMode; -} - -// --------------------------------------------------------------------------- -// Shell-hook env file: written on inject, sourced by the shell hook in .zshrc. -// This works for ALL new shells immediately, unlike launchctl setenv which only -// reaches processes launched directly by launchd (not Terminal.app children). -// --------------------------------------------------------------------------- - -export function getShellEnvFilePath(): string { - return join(getConfigDir(), "claude-env.sh"); -} - -function shellValue(value: string): string { - return `'${value.replaceAll("'", `'\\''`)}'`; -} - -function writeShellEnvFile( - port: number, - config: OcxConfig, - modelEnv: Record = {}, - auto?: AutoContextMode, - deps: SystemEnvDeps = {}, -): void { - const lines = [ - `# Generated by opencodex — do not edit manually`, - `export ANTHROPIC_BASE_URL=${shellValue(`http://127.0.0.1:${port}`)}`, - `export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`, - ]; - // New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already - // exported in their shell wins even though launchctl knows nothing about it. - const conditional = (name: string, value: string) => - `[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`; - if (systemEnvMarkerMode(config, deps) === "proxy") { - if (config.apiKeys?.length) { - lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`); - } else { - lines.push(conditional("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER)); - } - } - // Model slots (default + tiers + legacy small-fast) with [1m] applied (devlog 260712 B2). - if (modelEnv.ANTHROPIC_MODEL) { - lines.push(`export ANTHROPIC_MODEL=${shellValue(modelEnv.ANTHROPIC_MODEL)}`); - } else if (config.claudeCode?.model) { - lines.push(`export ANTHROPIC_MODEL=${shellValue(config.claudeCode.model)}`); - } - for (const [name, value] of Object.entries(modelEnv)) { - if (name === "ANTHROPIC_MODEL") continue; - lines.push(conditional(name, value)); - } - const maxCtx = config.claudeCode?.maxContextTokens; - if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) { - lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)))); - lines.push(conditional("DISABLE_COMPACT", "1")); - } - // Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl. - const autoShell = auto ?? resolveAutoContext(config.claudeCode); - if (autoShell.enabled) lines.push(conditional("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(autoShell.compactWindow))); - if (config.claudeCode?.alwaysEnableEffort === true) { - lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1")); - } - const shellEnvPath = getShellEnvFilePath(); - recordOwnedConfigPath(getConfigDir(), shellEnvPath); - mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 }); - writeFileSync(shellEnvPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 }); -} - -function removeShellEnvFile(): void { - try { unlinkSync(getShellEnvFilePath()); } catch { /* already gone */ } -} - -// --------------------------------------------------------------------------- -// .zshrc hook auto-install: adds a one-liner that sources claude-env.sh. -// Idempotent — skips if the hook line already exists. -// --------------------------------------------------------------------------- - -const SHELL_HOOK_MARKER = "# opencodex claude-env hook"; -const SHELL_HOOK_LINE = `${SHELL_HOOK_MARKER}\n[ -f ~/.opencodex/claude-env.sh ] && source ~/.opencodex/claude-env.sh`; - -export function installShellHook(): { installed: boolean; reason?: string } { - if (process.platform !== "darwin") return { installed: false, reason: "not macOS" }; - const home = process.env.HOME; - if (!home) return { installed: false, reason: "no HOME" }; - const zshrcPath = join(home, ".zshrc"); - try { - let content = ""; - try { content = readFileSync(zshrcPath, "utf8"); } catch { /* file doesn't exist yet */ } - if (content.includes(SHELL_HOOK_MARKER)) return { installed: false, reason: "already installed" }; - const addition = `\n${SHELL_HOOK_LINE}\n`; - writeFileSync(zshrcPath, content + addition, { encoding: "utf8", mode: 0o644 }); - return { installed: true }; - } catch (err) { - return { installed: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` }; - } -} - -export function uninstallShellHook(): { removed: boolean; reason?: string } { - if (process.platform !== "darwin") return { removed: false, reason: "not macOS" }; - const home = process.env.HOME; - if (!home) return { removed: false, reason: "no HOME" }; - const zshrcPath = join(home, ".zshrc"); - try { - const content = readFileSync(zshrcPath, "utf8"); - if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" }; - // Match CR?LF, not LF alone. A .zshrc with CRLF line endings — ordinary on a home - // directory an editor or another OS has touched — did not match, so the file was - // rewritten unchanged and the caller was told the hook was removed. Reporting success - // while the hook still sources on every new shell is the worse of the two failures. - const cleaned = content.replace(/\r?\n?# opencodex claude-env hook\r?\n\[.*claude-env\.sh.*(?:\r?\n)?/g, "\n"); - // Verify instead of assuming: if the marker survives, the block is shaped in a way this - // pattern does not own, and the honest answer is failure rather than a silent no-op. - if (cleaned.includes(SHELL_HOOK_MARKER)) { - return { removed: false, reason: "hook block present but not in the expected shape; remove it manually" }; - } - writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 }); - return { removed: true }; - } catch (error) { - if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") { - return { removed: false, reason: "not installed" }; - } - return { removed: false, reason: "read/write failed" }; - } -} - -/** Whether a real `claude` executable is discoverable from this process's PATH. */ -export function claudeCodeCliInstalled(pathValue = process.env.PATH): boolean { - if (!pathValue) return false; - for (const directory of pathValue.split(delimiter)) { - // An empty PATH segment means the current directory. Do not let the proxy treat a - // workspace-local file as a durable user installation. - if (!directory) continue; - const candidate = join(directory, "claude"); - try { - if (!statSync(candidate).isFile()) continue; - accessSync(candidate, constants.X_OK); - return true; - } catch { - // Keep scanning PATH after missing, non-file, and non-executable entries. - } - } - return false; -} - -/** - * Keep the shell hook aligned with the integration that can actually consume it. - * Claude Desktop uses its own profile and does not source `.zshrc`; this hook exists - * only for plain Claude Code CLI launches. - * - * Reconciliation is PATH-sensitive by construction: "Claude Code is installed" is answered - * from the PATH of whichever process calls this. A launchd/service context with a stripped - * PATH can therefore fail to see a `claude` the user's interactive shell finds, and this will - * remove the hook. That is the intended failure direction — removing an OpenCodex-owned block - * is reversible on the next foreground `ocx start`, whereas leaving a hook pointing at an - * uninstalled CLI is the stale state this reconciliation exists to clear. Only the block - * carrying our own marker is ever touched; user lines are preserved. - */ -export function reconcileShellHook(systemEnvInjected: boolean): { - changed: boolean; - state: "installed" | "absent" | "failed"; - reason?: string; -} { - if (process.platform !== "darwin") return { changed: false, state: "absent", reason: "not macOS" }; - if (systemEnvInjected && claudeCodeCliInstalled()) { - const result = installShellHook(); - if (result.installed) return { changed: true, state: "installed" }; - if (result.reason === "already installed") { - return { changed: false, state: "installed", reason: result.reason }; - } - return { changed: false, state: "failed", reason: result.reason ?? "install failed" }; - } - - const result = uninstallShellHook(); - if (!result.removed && result.reason !== "not installed") { - return { changed: false, state: "failed", reason: result.reason ?? "remove failed" }; - } - return { - changed: result.removed, - state: "absent", - reason: systemEnvInjected ? "Claude Code not installed" : "system environment inactive", - }; -} +export { getShellEnvFilePath, installShellHook, uninstallShellHook, claudeCodeCliInstalled, reconcileShellHook } from "./system-env-shell"; +export type { SystemEnvDeps } from "./system-env-shell"; +import { systemEnvMarkerMode, writeShellEnvFile, removeShellEnvFile } from "./system-env-shell"; +import type { SystemEnvDeps } from "./system-env-shell"; const SYSTEM_ENV_NAMES = [ "ANTHROPIC_BASE_URL", diff --git a/src/types/provider.ts b/src/types/provider.ts index 79651e0012..b1304f454c 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -723,7 +723,7 @@ export interface OcxProviderConfig { * headless and cannot control a screen itself; provide commands here only when running on a host * that can. With no executor, these tools honestly report "not supported". */ - desktopExecutor?: import("../adapters/cursor/native-exec-desktop").DesktopExecutorConfig; + desktopExecutor?: import("../adapters/cursor/desktop-executor-contract").DesktopExecutorConfig; /** * Cursor adapter only: unsafe opt-in escape hatch for Cursor server-driven built-in local * read/write/delete/ls/grep/shell/fetch execution. Prefer `nativeLocalExec: "on"` for new diff --git a/src/vision/image-rewrite.ts b/src/vision/image-rewrite.ts new file mode 100644 index 0000000000..9dcaee49e5 --- /dev/null +++ b/src/vision/image-rewrite.ts @@ -0,0 +1,108 @@ +import type { OcxContentPart, OcxParsedRequest, OcxTextContent } from "../types"; +import type { TranslatorBudget } from "../lib/translator-budget"; + +export const descriptionEncoder = new TextEncoder(); + +/** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */ +export function carriesImages(role: string): boolean { + return role === "user" || role === "developer" || role === "toolResult"; +} + + +const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]"; + +function isPlainRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Keep the native Responses passthrough body aligned with image replacements made in the parsed + * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize + * `context.messages`; updating only the latter would send the original pixels to a text-only + * Responses upstream even after the vision sidecar produced a caption. + * + * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses + * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally + * untouched. + */ +export function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void { + const rawBody = parsed._rawBody; + if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return; + + let nextDescription = 0; + const rewriteImages = (value: unknown): unknown => { + if (Array.isArray(value)) { + let changed = false; + const rewritten = value.map(entry => { + const next = rewriteImages(entry); + if (next !== entry) changed = true; + return next; + }); + return changed ? rewritten : value; + } + if (!isPlainRecord(value)) return value; + if (value.type === "input_image" && typeof value.image_url === "string") { + // Both message and tool-output parsers exclude empty URLs from caption jobs. + if (value.image_url.length === 0) { + const fileId = typeof value.file_id === "string" && value.file_id.length > 0 ? value.file_id : undefined; + return { type: "input_text", text: fileId ? `[image: ${fileId}]` : IMAGE_OMITTED_TEXT }; + } + const description = descriptions[nextDescription++]; + return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT }; + } + return value; + }; + + let changed = false; + const input = rawBody.input.map(item => { + if (!isPlainRecord(item)) return item; + const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : ""); + const role = typeof item.role === "string" ? item.role : ""; + const isMessageContent = ( + (type === "message" && (role === "user" || role === "developer")) + || type === "agent_message" + ); + const field = isMessageContent + ? "content" + : (type === "function_call_output" || type === "custom_tool_call_output") + ? "output" + : undefined; + if (!field) return item; + const rewritten = rewriteImages(item[field]); + if (rewritten === item[field]) return item; + changed = true; + return { ...item, [field]: rewritten }; + }); + + if (changed) rawBody.input = input; +} + +/** + * Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward + * provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding + * raw images would 400 or silently confuse it. Replace each image with an explicit marker so the + * model (and the user, via its reply) knows the image was dropped rather than ignored. + */ +export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean { + let stripped = false; + const descriptions: string[] = []; + for (const msg of parsed.context.messages) { + if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue; + const parts = msg.content as OcxContentPart[]; + if (!parts.some(p => p.type === "image")) continue; + msg.content = parts.map(p => { + if (p.type !== "image") return p; + const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart; + descriptions.push((replacement as OcxTextContent).text); + const reservation = translatorBudget?.reserveTransient( + descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength, + { kind: "request_copies" }, + ); + reservation?.commitRetained(); + return replacement; + }); + stripped = true; + } + syncRawBodyImageDescriptions(parsed, descriptions); + return stripped; +} diff --git a/src/vision/index.ts b/src/vision/index.ts index a4f8525cfd..633e2217c3 100644 --- a/src/vision/index.ts +++ b/src/vision/index.ts @@ -1,28 +1,16 @@ import { createHash } from "node:crypto"; -import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types"; -import type { VisionReasoningEffort } from "../reasoning-effort"; -import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe"; +import type { OcxContentPart, OcxMessage, OcxParsedRequest, OcxTextContent } from "../types"; +import { describeImage, type DescribeOutcome } from "./describe"; import { describeImageAnthropic } from "./anthropic-describe"; import { describeImageRouted } from "./routed-describe"; -import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility"; -import { normalizeVisionReasoningForModel } from "./reasoning"; -import type { CodexAuthContext, CodexAuthPolicyConfig } from "../codex/auth-context"; -import { isCodexReserveRequestEligible } from "../codex/loopback-target"; -import type { DataPlaneAdmission } from "../server/auth-cors"; -import { resolveSidecarAuth } from "../sidecar/auth"; -import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar"; +import type { CodexAuthContext } from "../codex/auth-context"; import type { SidecarOutcomeRecorder } from "../web-search/executor"; import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory"; import type { TranslatorBudget } from "../lib/translator-budget"; -import { - DEFAULT_VISION_TIMEOUT_MS, - MAX_VISION_TIMEOUT_MS, - MIN_VISION_TIMEOUT_MS, -} from "./timeout-bounds"; +import type { VisionPlan } from "./plan"; +import { carriesImages, descriptionEncoder, syncRawBodyImageDescriptions } from "./image-rewrite"; export { describeImage } from "./describe"; - -/** Backward-compatible request-time name for the shared vision-sidecar consumer predicate. */ export { isModelVisionSidecarConsumer as isModelTextOnly } from "./eligibility"; export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe"; export { @@ -35,19 +23,25 @@ export { visionEligibleModelOptions, } from "./eligibility"; export type { VisionCandidateModel, VisionModelOption, VisionSidecarBackend } from "./eligibility"; +export { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds"; export { - DEFAULT_VISION_TIMEOUT_MS, - MAX_VISION_TIMEOUT_MS, - MIN_VISION_TIMEOUT_MS, -}; + DEFAULT_MAX_DESCRIPTIONS_PER_TURN, + resolveMaxDescriptionsPerTurn, + isValidVisionTimeoutMs, + resolveVisionTimeoutMs, + findAnthropicVisionProvider, + resolveVisionBackend, + resolveOpenAiVisionModel, + resolveEffectiveVisionModel, + shouldResolveOpenAiVisionSidecar, + planVisionSidecar, +} from "./plan"; +export type { AnthropicVisionProvider, VisionPlan } from "./plan"; +export { stripImagesInPlace } from "./image-rewrite"; + -const DEFAULT_VISION_MODEL = "gpt-5.4-mini"; -const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5"; -const DEFAULT_REASONING: VisionReasoningEffort = "low"; -export const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8; const DESCRIPTION_CACHE_MAX_ENTRIES = 256; export const VISION_DESCRIPTION_CACHE_MAX_BYTES = 1024 * 1024; -const descriptionEncoder = new TextEncoder(); /** Max images described in parallel — keeps first-token latency bounded without flooding the backend. */ const VISION_CONCURRENCY = 3; /** Per-image description hard cap (chars) so multi-image turns can't blow the main model's context. */ @@ -162,25 +156,6 @@ export function evictOldestVisionDescriptionForBudget(): number { return descriptionCache.evictOldest?.() ?? 0; } -/** Runtime config is permissive: zero is intentional; malformed values fall back to the bounded default. */ -export function resolveMaxDescriptionsPerTurn(value: unknown): number { - if (value === 0) return 0; - return typeof value === "number" && Number.isInteger(value) && value > 0 - ? value - : DEFAULT_MAX_DESCRIPTIONS_PER_TURN; -} - -export function isValidVisionTimeoutMs(value: unknown): value is number { - return typeof value === "number" - && Number.isInteger(value) - && value >= MIN_VISION_TIMEOUT_MS - && value <= MAX_VISION_TIMEOUT_MS; -} - -/** Runtime config is permissive: malformed or out-of-range values fall back to the default. */ -export function resolveVisionTimeoutMs(value: unknown): number { - return isValidVisionTimeoutMs(value) ? value : DEFAULT_VISION_TIMEOUT_MS; -} /** Run `worker` over `items` with bounded concurrency, preserving input order in the result array. */ async function runBounded(items: T[], limit: number, worker: (item: T) => Promise): Promise { @@ -200,178 +175,7 @@ function clamp(s: string, max: number): string { return s.length <= max ? s : `${s.slice(0, max)}\n…[description truncated]`; } -export interface AnthropicVisionProvider { - providerName: string; - provider: OcxProviderConfig; -} - -/** - * First enabled Anthropic OAuth provider whose active stored account is not marked for reauth. - * Delegates to the shared sidecar auth module (#2188) — same predicate as web-search. - */ -export function findAnthropicVisionProvider(config: OcxConfig): AnthropicVisionProvider | undefined { - const auth = resolveSidecarAuth(config); - if (!auth.isAnthropicAuth || !auth.anthropicProviderName || !auth.anthropicProvider) return undefined; - return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider }; -} - -export function resolveVisionBackend( - explicit: "openai" | "anthropic" | "routed" | undefined, - anthropicSidecar: AnthropicVisionProvider | undefined, -): "openai" | "anthropic" { - if (explicit === "openai" || explicit === "anthropic") return explicit; - // "routed" collapses to the legacy default order until its describe executor - // lands (roadmap 170 → 180 revised): a persisted routed backend without a - // dispatchable arm degrades exactly like unset rather than crashing. wp3 - // replaces this collapse with the real routed arm in planVisionSidecar. - return anthropicSidecar ? "anthropic" : "openai"; -} - -/** Native model used by the OpenAI vision helper, including its bounded default. */ -export function resolveOpenAiVisionModel(config: Pick): string { - const configured = config.visionSidecar?.model; - // Namespaced routed ids never reach the forward executor (see - // resolveEffectiveVisionModel). - return configured && !configured.includes("/") ? configured : DEFAULT_VISION_MODEL; -} - -/** Effective describer model for the backend `planVisionSidecar` selected. */ -export function resolveEffectiveVisionModel( - config: Pick, - backend: "openai" | "anthropic", -): string { - const configured = config.visionSidecar?.model; - // A namespaced "provider/model" id belongs to the routed backend only; the - // forward/OAuth executors POST the model string verbatim, so it falls back - // to the side's default here (PUT coherence rejects new writes of this - // shape, but a legacy or hand-edited config must not break the executor). - const usable = configured && !configured.includes("/") ? configured : undefined; - return backend === "anthropic" - ? usable || DEFAULT_ANTHROPIC_VISION_MODEL - : usable || DEFAULT_VISION_MODEL; -} - -/** A user/developer/toolResult message can carry images (toolResult: e.g. Codex view_image output). */ -function carriesImages(role: string): boolean { - return role === "user" || role === "developer" || role === "toolResult"; -} - -function messagesHaveImage(parsed: OcxParsedRequest): boolean { - return parsed.context.messages.some(m => - carriesImages(m.role) && Array.isArray(m.content) && (m.content as OcxContentPart[]).some(p => p.type === "image")); -} - -export function shouldResolveOpenAiVisionSidecar( - config: OcxConfig, - provider: OcxProviderConfig, - modelId: string, - parsed: OcxParsedRequest, -): boolean { - if (!isModelTextOnly(provider, modelId) || !messagesHaveImage(parsed)) return false; - const cfg = config.visionSidecar ?? {}; - if (cfg.enabled === false) return false; - return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai"; -} - -export interface VisionPlan { - backend: "openai" | "anthropic" | "routed"; - forwardSidecar?: ResolvedOpenAiForwardSidecar; - anthropicSidecar?: AnthropicVisionProvider; - /** Namespaced "provider/model" describer for the routed backend (roadmap 180). */ - routedModel?: string; - /** Loopback dispatch inputs for the routed backend. */ - routedConfig?: Pick; - settings: VisionSettings; - maxDescriptionsPerTurn: number; -} - -/** - * Decide whether the vision sidecar should pre-describe images for this request, returning the plan - * if so. Active when: the routed model is in `provider.noVisionModels`, the request actually carries - * an image, the sidecar isn't disabled, and the selected backend has usable auth. Returns undefined - * otherwise (the caller strips images before sending to a text-only model). - */ -export function planVisionSidecar( - config: OcxConfig, - provider: OcxProviderConfig, - modelId: string, - parsed: OcxParsedRequest, - openAiSidecar?: ResolvedOpenAiForwardSidecar, - options: { admission?: Pick; codexAuthPolicy?: CodexAuthPolicyConfig } = {}, -): VisionPlan | undefined { - if (!isModelTextOnly(provider, modelId)) return undefined; - if (!messagesHaveImage(parsed)) return undefined; - const cfg = config.visionSidecar ?? {}; - if (cfg.enabled === false) return undefined; - - // Routed arm (roadmap 180 revised): explicit backend + NAMESPACED explicit - // model only — never inferred from credential availability. Plan-time - // fence: the target must not be provably blind, and must not itself be a - // model this planner would re-enter for (belt; the terminal marker on the - // loopback request is the braces). - if (cfg.backend === "routed") { - const routedModel = cfg.model; - const sep = routedModel ? routedModel.indexOf("/") : -1; - if (routedModel && sep > 0) { - const targetProvider = routedModel.slice(0, sep); - const targetId = routedModel.slice(sep + 1); - const targetProviderConfig = config.providers?.[targetProvider]; - const targetVisible = modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false - && !(targetProviderConfig && isModelTextOnly(targetProviderConfig, targetId)); - if (targetVisible) { - return { - backend: "routed", - routedModel, - routedConfig: { port: config.port, ...(config.apiKeys ? { apiKeys: config.apiKeys } : {}) }, - settings: { - model: routedModel, - reasoning: DEFAULT_REASONING, - timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs), - }, - maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn), - }; - } - } - // Misconfigured routed backend (bare id, unknown provider, or provably - // blind target): fall through to the legacy default order below rather - // than dispatching a describe that cannot work. - } - - const anthropicSidecar = findAnthropicVisionProvider(config); - const backend = resolveVisionBackend(cfg.backend, anthropicSidecar); - // A namespaced routed model must never reach the forward/OAuth executors - // (they POST the string verbatim); the effective-model resolver falls back - // to each side's default in that case. - const model = resolveEffectiveVisionModel(config, backend); - const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn); - if (backend === "anthropic") { - if (!anthropicSidecar) return undefined; - return { - backend, - anthropicSidecar, - settings: { - model, - reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING, - timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs), - }, - maxDescriptionsPerTurn, - }; - } - - if (!openAiSidecar) return undefined; - return { - backend, - forwardSidecar: openAiSidecar, - settings: { - ...(isCodexReserveRequestEligible(options.codexAuthPolicy ?? config, options.admission) ? { reserveCompatibility: true } : {}), - model, - reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING, - timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs), - }, - maxDescriptionsPerTurn, - }; -} interface ImageJob { imageUrl: string; @@ -389,73 +193,6 @@ function renderDescription(out: { text: string; error?: string }): OcxTextConten }; } -const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]"; - -function isPlainRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -/** - * Keep the native Responses passthrough body aligned with image replacements made in the parsed - * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize - * `context.messages`; updating only the latter would send the original pixels to a text-only - * Responses upstream even after the vision sidecar produced a caption. - * - * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses - * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally - * untouched. - */ -function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void { - const rawBody = parsed._rawBody; - if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return; - - let nextDescription = 0; - const rewriteImages = (value: unknown): unknown => { - if (Array.isArray(value)) { - let changed = false; - const rewritten = value.map(entry => { - const next = rewriteImages(entry); - if (next !== entry) changed = true; - return next; - }); - return changed ? rewritten : value; - } - if (!isPlainRecord(value)) return value; - if (value.type === "input_image" && typeof value.image_url === "string") { - // Both message and tool-output parsers exclude empty URLs from caption jobs. - if (value.image_url.length === 0) { - const fileId = typeof value.file_id === "string" && value.file_id.length > 0 ? value.file_id : undefined; - return { type: "input_text", text: fileId ? `[image: ${fileId}]` : IMAGE_OMITTED_TEXT }; - } - const description = descriptions[nextDescription++]; - return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT }; - } - return value; - }; - - let changed = false; - const input = rawBody.input.map(item => { - if (!isPlainRecord(item)) return item; - const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : ""); - const role = typeof item.role === "string" ? item.role : ""; - const isMessageContent = ( - (type === "message" && (role === "user" || role === "developer")) - || type === "agent_message" - ); - const field = isMessageContent - ? "content" - : (type === "function_call_output" || type === "custom_tool_call_output") - ? "output" - : undefined; - if (!field) return item; - const rewritten = rewriteImages(item[field]); - if (rewritten === item[field]) return item; - changed = true; - return { ...item, [field]: rewritten }; - }); - - if (changed) rawBody.input = input; -} function sha256(value: string | Uint8Array): string { return createHash("sha256").update(value).digest("hex"); @@ -641,33 +378,3 @@ export async function describeImagesInPlace( } syncRawBodyImageDescriptions(parsed, descriptions); } - -/** - * Fail-closed image strip for sidecar-covered models when NO sidecar plan exists (no forward - * provider / missing forwarded auth / sidecar disabled): the upstream is text-only, so forwarding - * raw images would 400 or silently confuse it. Replace each image with an explicit marker so the - * model (and the user, via its reply) knows the image was dropped rather than ignored. - */ -export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean { - let stripped = false; - const descriptions: string[] = []; - for (const msg of parsed.context.messages) { - if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue; - const parts = msg.content as OcxContentPart[]; - if (!parts.some(p => p.type === "image")) continue; - msg.content = parts.map(p => { - if (p.type !== "image") return p; - const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart; - descriptions.push((replacement as OcxTextContent).text); - const reservation = translatorBudget?.reserveTransient( - descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength, - { kind: "request_copies" }, - ); - reservation?.commitRetained(); - return replacement; - }); - stripped = true; - } - syncRawBodyImageDescriptions(parsed, descriptions); - return stripped; -} diff --git a/src/vision/plan.ts b/src/vision/plan.ts new file mode 100644 index 0000000000..cbcccf3d49 --- /dev/null +++ b/src/vision/plan.ts @@ -0,0 +1,205 @@ +import type { OcxConfig, OcxContentPart, OcxParsedRequest, OcxProviderConfig } from "../types"; +import type { VisionReasoningEffort } from "../reasoning-effort"; +import type { VisionSettings } from "./describe"; +import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar"; +import type { CodexAuthPolicyConfig } from "../codex/auth-context"; +import { isCodexReserveRequestEligible } from "../codex/loopback-target"; +import type { DataPlaneAdmission } from "../server/auth-cors"; +import { isModelVisionSidecarConsumer as isModelTextOnly, modelAcceptsImageInput } from "./eligibility"; +import { normalizeVisionReasoningForModel } from "./reasoning"; +import { resolveSidecarAuth } from "../sidecar/auth"; +import { DEFAULT_VISION_TIMEOUT_MS, MAX_VISION_TIMEOUT_MS, MIN_VISION_TIMEOUT_MS } from "./timeout-bounds"; +import { carriesImages } from "./image-rewrite"; + +const DEFAULT_VISION_MODEL = "gpt-5.4-mini"; +const DEFAULT_ANTHROPIC_VISION_MODEL = "claude-sonnet-5"; +const DEFAULT_REASONING: VisionReasoningEffort = "low"; +export const DEFAULT_MAX_DESCRIPTIONS_PER_TURN = 8; + +/** Runtime config is permissive: zero is intentional; malformed values fall back to the bounded default. */ +export function resolveMaxDescriptionsPerTurn(value: unknown): number { + if (value === 0) return 0; + return typeof value === "number" && Number.isInteger(value) && value > 0 + ? value + : DEFAULT_MAX_DESCRIPTIONS_PER_TURN; +} + +export function isValidVisionTimeoutMs(value: unknown): value is number { + return typeof value === "number" + && Number.isInteger(value) + && value >= MIN_VISION_TIMEOUT_MS + && value <= MAX_VISION_TIMEOUT_MS; +} + +/** Runtime config is permissive: malformed or out-of-range values fall back to the default. */ +export function resolveVisionTimeoutMs(value: unknown): number { + return isValidVisionTimeoutMs(value) ? value : DEFAULT_VISION_TIMEOUT_MS; +} + +export interface AnthropicVisionProvider { + providerName: string; + provider: OcxProviderConfig; +} + +/** + * First enabled Anthropic OAuth provider whose active stored account is not marked for reauth. + * Delegates to the shared sidecar auth module (#2188) — same predicate as web-search. + */ +export function findAnthropicVisionProvider(config: OcxConfig): AnthropicVisionProvider | undefined { + const auth = resolveSidecarAuth(config); + if (!auth.isAnthropicAuth || !auth.anthropicProviderName || !auth.anthropicProvider) return undefined; + return { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider }; +} + +export function resolveVisionBackend( + explicit: "openai" | "anthropic" | "routed" | undefined, + anthropicSidecar: AnthropicVisionProvider | undefined, +): "openai" | "anthropic" { + if (explicit === "openai" || explicit === "anthropic") return explicit; + // "routed" collapses to the legacy default order until its describe executor + // lands (roadmap 170 → 180 revised): a persisted routed backend without a + // dispatchable arm degrades exactly like unset rather than crashing. wp3 + // replaces this collapse with the real routed arm in planVisionSidecar. + return anthropicSidecar ? "anthropic" : "openai"; +} + +/** Native model used by the OpenAI vision helper, including its bounded default. */ +export function resolveOpenAiVisionModel(config: Pick): string { + const configured = config.visionSidecar?.model; + // Namespaced routed ids never reach the forward executor (see + // resolveEffectiveVisionModel). + return configured && !configured.includes("/") ? configured : DEFAULT_VISION_MODEL; +} + +/** Effective describer model for the backend `planVisionSidecar` selected. */ +export function resolveEffectiveVisionModel( + config: Pick, + backend: "openai" | "anthropic", +): string { + const configured = config.visionSidecar?.model; + // A namespaced "provider/model" id belongs to the routed backend only; the + // forward/OAuth executors POST the model string verbatim, so it falls back + // to the side's default here (PUT coherence rejects new writes of this + // shape, but a legacy or hand-edited config must not break the executor). + const usable = configured && !configured.includes("/") ? configured : undefined; + return backend === "anthropic" + ? usable || DEFAULT_ANTHROPIC_VISION_MODEL + : usable || DEFAULT_VISION_MODEL; +} + +function messagesHaveImage(parsed: OcxParsedRequest): boolean { + return parsed.context.messages.some(m => + carriesImages(m.role) && Array.isArray(m.content) && (m.content as OcxContentPart[]).some(p => p.type === "image")); +} + +export function shouldResolveOpenAiVisionSidecar( + config: OcxConfig, + provider: OcxProviderConfig, + modelId: string, + parsed: OcxParsedRequest, +): boolean { + if (!isModelTextOnly(provider, modelId) || !messagesHaveImage(parsed)) return false; + const cfg = config.visionSidecar ?? {}; + if (cfg.enabled === false) return false; + return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai"; +} + +export interface VisionPlan { + backend: "openai" | "anthropic" | "routed"; + forwardSidecar?: ResolvedOpenAiForwardSidecar; + anthropicSidecar?: AnthropicVisionProvider; + /** Namespaced "provider/model" describer for the routed backend (roadmap 180). */ + routedModel?: string; + /** Loopback dispatch inputs for the routed backend. */ + routedConfig?: Pick; + settings: VisionSettings; + maxDescriptionsPerTurn: number; +} + +/** + * Decide whether the vision sidecar should pre-describe images for this request, returning the plan + * if so. Active when: the routed model is in `provider.noVisionModels`, the request actually carries + * an image, the sidecar isn't disabled, and the selected backend has usable auth. Returns undefined + * otherwise (the caller strips images before sending to a text-only model). + */ +export function planVisionSidecar( + config: OcxConfig, + provider: OcxProviderConfig, + modelId: string, + parsed: OcxParsedRequest, + openAiSidecar?: ResolvedOpenAiForwardSidecar, + options: { admission?: Pick; codexAuthPolicy?: CodexAuthPolicyConfig } = {}, +): VisionPlan | undefined { + if (!isModelTextOnly(provider, modelId)) return undefined; + if (!messagesHaveImage(parsed)) return undefined; + const cfg = config.visionSidecar ?? {}; + if (cfg.enabled === false) return undefined; + + // Routed arm (roadmap 180 revised): explicit backend + NAMESPACED explicit + // model only — never inferred from credential availability. Plan-time + // fence: the target must not be provably blind, and must not itself be a + // model this planner would re-enter for (belt; the terminal marker on the + // loopback request is the braces). + if (cfg.backend === "routed") { + const routedModel = cfg.model; + const sep = routedModel ? routedModel.indexOf("/") : -1; + if (routedModel && sep > 0) { + const targetProvider = routedModel.slice(0, sep); + const targetId = routedModel.slice(sep + 1); + const targetProviderConfig = config.providers?.[targetProvider]; + const targetVisible = modelAcceptsImageInput(config, { provider: targetProvider, id: targetId }) !== false + && !(targetProviderConfig && isModelTextOnly(targetProviderConfig, targetId)); + if (targetVisible) { + return { + backend: "routed", + routedModel, + routedConfig: { port: config.port, ...(config.apiKeys ? { apiKeys: config.apiKeys } : {}) }, + settings: { + model: routedModel, + reasoning: DEFAULT_REASONING, + timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs), + }, + maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn), + }; + } + } + // Misconfigured routed backend (bare id, unknown provider, or provably + // blind target): fall through to the legacy default order below rather + // than dispatching a describe that cannot work. + } + + const anthropicSidecar = findAnthropicVisionProvider(config); + const backend = resolveVisionBackend(cfg.backend, anthropicSidecar); + // A namespaced routed model must never reach the forward/OAuth executors + // (they POST the string verbatim); the effective-model resolver falls back + // to each side's default in that case. + const model = resolveEffectiveVisionModel(config, backend); + const maxDescriptionsPerTurn = resolveMaxDescriptionsPerTurn(cfg.maxDescriptionsPerTurn); + + if (backend === "anthropic") { + if (!anthropicSidecar) return undefined; + return { + backend, + anthropicSidecar, + settings: { + model, + reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING, + timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs), + }, + maxDescriptionsPerTurn, + }; + } + + if (!openAiSidecar) return undefined; + return { + backend, + forwardSidecar: openAiSidecar, + settings: { + ...(isCodexReserveRequestEligible(options.codexAuthPolicy ?? config, options.admission) ? { reserveCompatibility: true } : {}), + model, + reasoning: normalizeVisionReasoningForModel(model, cfg.reasoning) ?? DEFAULT_REASONING, + timeoutMs: resolveVisionTimeoutMs(cfg.timeoutMs), + }, + maxDescriptionsPerTurn, + }; +} diff --git a/tests/adapters/anthropic/anthropic-image-normalize.test.ts b/tests/adapters/anthropic/anthropic-image-normalize.test.ts index 350e322d9c..6ef58e0eac 100644 --- a/tests/adapters/anthropic/anthropic-image-normalize.test.ts +++ b/tests/adapters/anthropic/anthropic-image-normalize.test.ts @@ -565,3 +565,18 @@ describe("bounded parallel first pass (WP170)", () => { expect(dropped.sort()).toEqual([1, 2]); }); }); + +test("image codec seam preserves hook identity and owns normalization state", async () => { + const { + resetNormalizeStateForTests: resetCodecState, + getNormalizeStatsForTests: getCodecStats, + } = await import("../../../src/adapters/anthropic-image-codec"); + const { readFileSync } = await import("node:fs"); + const { repoPath } = await import("../../helpers/repo-root"); + + expect(resetNormalizeStateForTests).toBe(resetCodecState); + expect(getNormalizeStatsForTests).toBe(getCodecStats); + const source = readFileSync(repoPath("src/adapters/anthropic-image-normalize.ts"), "utf8"); + expect(source).not.toMatch(/^(?:const|let|var)\b[^\n]*\bnew Map { expect(getDefaultConfig().providers.openai).toMatchObject({ codexAccountMode: "pool" }); }); }); + +test("destination leaf preserves facade bindings without importing the facade", () => { + expect(destinationIsCanonicalOpenAiForwardProvider).toBe(isCanonicalOpenAiForwardProvider); + expect(DESTINATION_OPENAI_CODEX_PROVIDER_ID).toBe(OPENAI_CODEX_PROVIDER_ID); + const source = readFileSync(repoPath("src/providers/openai-tiers-destination.ts"), "utf8"); + expect(source).not.toMatch(/(?:from\s*|import\s*(?:\(\s*)?)["']\.\/openai-tiers(?:\.ts)?["']/); +}); diff --git a/tests/claude-integration/claude-inbound.test.ts b/tests/claude-integration/claude-inbound.test.ts index 9943f14ede..32207c9019 100644 --- a/tests/claude-integration/claude-inbound.test.ts +++ b/tests/claude-integration/claude-inbound.test.ts @@ -1,4 +1,7 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { AnthropicRequestError as LeafAnthropicRequestError } from "../../src/claude/inbound-records"; +import { repoPath } from "../helpers/repo-root"; import { AnthropicRequestError, anthropicToResponsesBody, anthropicToResponsesTranslation, effortForThinkingBudget, extractOcxEffortDirective, resolveInboundModel } from "../../src/claude/inbound"; import { parseRequest } from "../../src/responses/parser"; import { responsesRequestSchema } from "../../src/responses/schema"; @@ -630,3 +633,14 @@ describe("ocx-route directive (devlog 072)", () => { expect(extractOcxEffortDirective(null)).toBeNull(); }); }); + +test("inbound leaves preserve the tool_choice error identity and avoid facade back-edges", () => { + const base = { model: "m", max_tokens: 10, messages: [{ role: "user", content: "hi" }] }; + expect(() => anthropicToResponsesBody({ ...base, tool_choice: { type: "tool" } })) + .toThrow(AnthropicRequestError); + expect(AnthropicRequestError).toBe(LeafAnthropicRequestError); + for (const leaf of ["inbound-records.ts", "inbound-model-options.ts", "inbound-content-options.ts"]) { + expect(readFileSync(repoPath("src", "claude", leaf), "utf8")) + .not.toMatch(/from\s+["']\.\/inbound["']/); + } +}); diff --git a/tests/codex-integration/codex-log-guard-inspect.test.ts b/tests/codex-integration/codex-log-guard-inspect.test.ts index 063a64a605..b6a00069cd 100644 --- a/tests/codex-integration/codex-log-guard-inspect.test.ts +++ b/tests/codex-integration/codex-log-guard-inspect.test.ts @@ -493,3 +493,14 @@ describe("Codex Log Guard inspection", () => { expect(after.schema.state).not.toBe("compatible"); }); }); + +test("inspect-schema preserves the public predicate identity without a back-edge", async () => { + const { hasCurrentLogsSchema } = await import("../../src/codex/log-guard/inspect"); + const { hasCurrentLogsSchema: schemaPredicate } = await import("../../src/codex/log-guard/inspect-schema"); + const { readFileSync } = await import("node:fs"); + const { repoPath } = await import("../helpers/repo-root"); + + expect(hasCurrentLogsSchema).toBe(schemaPredicate); + const source = readFileSync(repoPath("src/codex/log-guard/inspect-schema.ts"), "utf8"); + expect(source.split("\n").some(line => /from\s+["']\.\/inspect["']/.test(line))).toBe(false); +}); diff --git a/tests/codex-integration/codex-prompt-layers.test.ts b/tests/codex-integration/codex-prompt-layers.test.ts index fa95b7c3df..7a289c9201 100644 --- a/tests/codex-integration/codex-prompt-layers.test.ts +++ b/tests/codex-integration/codex-prompt-layers.test.ts @@ -8,6 +8,11 @@ * what we emit, not what a JS parser makes of it. */ import { describe, expect, test } from "bun:test"; +import { readFileSync, readdirSync } from "node:fs"; +import { repoPath } from "../helpers/repo-root"; +import * as encoding from "../../src/codex/prompt-layers/encoding"; +import * as revision from "../../src/codex/prompt-layers/revision"; +import * as tomlRead from "../../src/codex/prompt-layers/toml-read"; import { LAYER_INVENTORY, TOGGLE_IDS, @@ -15,6 +20,7 @@ import { decodeBasicString, encodeBasicString, findInvalidCharacter, + inspectOwnership, isToggleId, normalizeBody, } from "../../src/codex/prompt-layers"; @@ -206,3 +212,21 @@ describe("revision", () => { expect(computeRevision("a", null)).not.toBe(computeRevision("a", "\u0000absent")); }); }); + +test("prompt-layers leaf seams preserve facade identity without back-imports", () => { + expect(computeRevision).toBe(revision.computeRevision); + expect(encodeBasicString).toBe(encoding.encodeBasicString); + expect(decodeBasicString).toBe(encoding.decodeBasicString); + expect(inspectOwnership).toBe(tomlRead.inspectOwnership); + + const body = 'line one\n"quoted" \\ path 😀'; + expect(encoding.decodeBasicString(encoding.encodeBasicString(body))).toBe(body); + + const leaves = readdirSync(repoPath("src", "codex", "prompt-layers")) + .filter(name => name.endsWith(".ts")); + expect(leaves.length).toBeGreaterThan(0); + for (const leaf of leaves) { + const source = readFileSync(repoPath("src", "codex", "prompt-layers", leaf), "utf8"); + expect(source).not.toMatch(/from\s+["']\.\.\/prompt-layers["']/); + } +}); diff --git a/tests/codex-integration/combos.test.ts b/tests/codex-integration/combos.test.ts index fc85e82782..1c4924d3de 100644 --- a/tests/codex-integration/combos.test.ts +++ b/tests/codex-integration/combos.test.ts @@ -64,6 +64,9 @@ import { } from "../../src/providers/quota-routing-cache"; import { catalogConvergenceFactory } from "../helpers/catalog-convergence"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import * as publicCombos from "../../src/combos"; +import * as comboIdentifiers from "../../src/combos/identifiers"; +import { repoPath } from "../helpers/repo-root"; const VALID_COMBO = { targets: [{ provider: "a", model: "m1" }] }; @@ -1526,3 +1529,25 @@ describe("combo generation reconciliation", () => { expect(pickComboTarget(original, "free")?.target.provider).toBe("b"); }); }); + +test("combo identifiers leaf preserves public export identity without facade imports", () => { + const names = [ + "COMBO_NAMESPACE", + "preservesPhysicalComboProvider", + "isNativeAliasCombo", + "targetKey", + "parseComboModelId", + "comboModelId", + "comboPublicModelId", + "comboDisabledModelId", + "comboDisabledModelSelectors", + "resolveComboId", + "isValidComboId", + ] as const; + for (const name of names) { + expect(publicCombos[name]).toBe(comboIdentifiers[name]); + } + const source = readFileSync(repoPath("src", "combos", "identifiers.ts"), "utf8"); + expect(source.split(/\r?\n/).some(line => /from\s+["']\.\/(types|index)["']/.test(line))) + .toBe(false); +}); diff --git a/tests/config/client-config-export.test.ts b/tests/config/client-config-export.test.ts index 19f1edc8da..ec969a8455 100644 --- a/tests/config/client-config-export.test.ts +++ b/tests/config/client-config-export.test.ts @@ -24,6 +24,13 @@ import { import { buildOpencodeProviderBlockFromCatalog, opencodeGlobalConfigPath } from "../../src/cli/opencode"; import type { OcxConfig } from "../../src/types"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import * as facade from "../../src/clients/config-export"; +import { OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG as leafDefaultConfig } from "../../src/clients/config-export/constants"; +import { normalizeExportModels as leafNormalizeExportModels } from "../../src/clients/config-export/model-metadata"; +import * as omp from "../../src/clients/config-export/omp"; +import * as dsh from "../../src/clients/config-export/dsh"; +import * as mcode from "../../src/clients/config-export/mcode"; +import * as zcode from "../../src/clients/config-export/zcode"; /** * Fixture covering the four rows that exercise every emission branch: native, @@ -82,6 +89,51 @@ function dshConfig(context: ExportContext = ctx()): DshGeneratedConfig { return buildClientConfig("dsh", context) as DshGeneratedConfig; } +describe("split config-export public facade", () => { + test("keeps canonical singleton and registry function identities", () => { + expect(facade.OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG).toBe(leafDefaultConfig); + expect(facade.normalizeExportModels).toBe(leafNormalizeExportModels); + const leaves = [ + ["omp", omp.buildOmpClientConfig, omp.summarizeOmp, omp.buildOmpContribution], + ["dsh", dsh.buildDshClientConfig, dsh.summarizeDsh, dsh.buildDshContribution], + ["mcode", mcode.buildMcodeClientConfig, mcode.summarizeMcode, mcode.buildMcodeContribution], + ["zcode", zcode.buildZcodeClientConfig, zcode.summarizeZcode, zcode.buildZcodeContribution], + ] as const; + for (const [id, build, summarize, contribute] of leaves) { + expect(EXPORT_CLIENTS[id].build).toBe(build); + expect(EXPORT_CLIENTS[id].summarize).toBe(summarize); + expect(EXPORT_CLIENTS[id].buildContribution).toBe(contribute); + } + }); + + test("preserves each moved format's serialized fields, order and owned fragment", () => { + const context = ctx({ models: [{ + namespaced: "test/known", provider: "test", id: "known", contextWindow: 8192, + inputModalities: ["text", "image"], reasoningEfforts: ["none", "high"], + }] }); + const cases = [ + ["omp", ["providers", "opencodex"], '{"providers":{"opencodex":{"baseUrl":"http://127.0.0.1:10100/v1","api":"openai-completions","apiKey":"opencodex-loopback","models":[{"id":"test/known","name":"known (test)","input":["text","image"],"contextWindow":8192,"maxTokens":8192,"reasoning":true,"thinking":{"mode":"effort","efforts":["high"]}}]}}}'], + ["dsh", ["llm-pi-ai", "providers", "opencodex"], '{"llm-pi-ai":{"providers":{"opencodex":{"displayName":"OpenCodex","api":"openai-responses","baseURL":"http://127.0.0.1:10100/v1","headers":{"Authorization":"Bearer ocx_data_dsh"},"models":[{"id":"test/known","name":"known (test)","input":["text","image"],"contextWindow":8192,"reasoningEfforts":{"high":"high"}}]}}}}'], + ["mcode", ["custom_provider", "opencodex"], '{"custom_provider":{"opencodex":{"name":"OpenCodex","kind":"custom","enabled":true,"api":"anthropic-messages","options":{"apiKey":"opencodex-loopback","baseURL":"http://127.0.0.1:10100","authMode":"api-key"},"models":{"test/known":{"limit":{"context":8192},"thinking":{"effortOptions":["high"]}}}}}}'], + ["zcode", ["provider", "opencodex"], '{"provider":{"opencodex":{"name":"OpenCodex","kind":"openai-compatible","enabled":true,"source":"custom","options":{"apiKey":"opencodex-loopback","baseURL":"http://127.0.0.1:10100/v1","apiKeyRequired":true},"models":{"test/known":{"name":"known (test)","modalities":{"input":["text","image"],"output":["text"]},"limit":{"context":8192}}}}}}'], + ] as const; + for (const [id, path, expectedBytes] of cases) { + const built = buildClientConfigText(id, context); + expect(JSON.stringify(built.document)).toBe(expectedBytes); + expect(EXPORT_CLIENTS[id].summarize(built.document)).toEqual({ modelCount: 1, modelsWithoutLimits: 0 }); + const expectedDocument = JSON.parse(expectedBytes); + const expectedValue = path.reduce((value, key) => value[key], expectedDocument); + expect(facade.buildClientContribution(id, context)).toEqual({ + clientId: id, fragments: [{ path, value: expectedValue }], + }); + if (id === "zcode") { + expect(built.format).toBe("json"); + expect(built.text).toBe(JSON.stringify(expectedDocument, null, 2) + "\n"); + } + } + }); +}); + describe("relocated OpenCode serializer (accept criterion 1)", () => { test("the moved builder reproduces the pre-refactor golden byte-for-byte", () => { diff --git a/tests/lib/redact.test.ts b/tests/lib/redact.test.ts index 13e912f34a..26a4912a27 100644 --- a/tests/lib/redact.test.ts +++ b/tests/lib/redact.test.ts @@ -1,4 +1,7 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { foldForMatching } from "../../src/lib/redact-folding"; +import { repoPath } from "../helpers/repo-root"; import { REDACTED_SECRET, redactHeaders, @@ -538,3 +541,12 @@ describe("redactUrlForLog", () => { expect(redactUrlForLog("not a url?refreshToken=refresh-secret")).toBe("not a url"); }); }); + +test("redact-folding folds colon confusables with aligned offsets and stays a zero-import leaf", () => { + const { folded, map } = foldForMatching("\u205A"); + expect(folded).toBe(":"); + expect(map).toHaveLength(2); + expect(map).toEqual([0, 1]); + const source = readFileSync(repoPath("src/lib/redact-folding.ts"), "utf8"); + expect(source).not.toMatch(/^import /m); +}); diff --git a/tests/providers/cursor/cursor-desktop-exec.test.ts b/tests/providers/cursor/cursor-desktop-exec.test.ts index 43023799ad..9bf0870b9f 100644 --- a/tests/providers/cursor/cursor-desktop-exec.test.ts +++ b/tests/providers/cursor/cursor-desktop-exec.test.ts @@ -7,8 +7,14 @@ import { RecordScreenArgsSchema, } from "../../../src/adapters/cursor/gen/agent_pb"; import { handleCursorNativeExec } from "../../../src/adapters/cursor/native-exec"; -import { desktopDepsFromConfig } from "../../../src/adapters/cursor/native-exec-desktop"; +import { + desktopDepsFromConfig, + type DesktopExecutorConfig as DesktopExecutorConfigViaImplementation, +} from "../../../src/adapters/cursor/native-exec-desktop"; +import type { DesktopExecutorConfig } from "../../../src/adapters/cursor/desktop-executor-contract"; import { shellInvocation } from "../../../src/lib/win-exec"; +import { readFileSync } from "node:fs"; +import { repoPath } from "../../helpers/repo-root"; function execMessage(message: Parameters>[1]["message"]) { return create(ExecServerMessageSchema, { id: 3, execId: "exec-test", message }); @@ -27,6 +33,22 @@ function echoJson(json: string, platform: NodeJS.Platform = process.platform): s } describe("Cursor desktop executor hooks", () => { + test("DesktopExecutorConfig is one contract reachable from both paths, and provider types no longer import the implementation", () => { + // Type-level parity: the historical export and the contract leaf must be the same shape. + const viaContract: DesktopExecutorConfig = { computerUseCommand: "x", timeoutMs: 1 }; + const viaImplementation: DesktopExecutorConfigViaImplementation = viaContract; + expect(desktopDepsFromConfig(viaImplementation)).toHaveProperty("computerUse"); + + // Graph guard: the contract is dependency-free and src/types/provider.ts points at it, + // not at native-exec-desktop.ts (that edge closed a type cycle through tool-definitions). + const contract = readFileSync(repoPath("src", "adapters", "cursor", "desktop-executor-contract.ts"), "utf8"); + expect(contract).not.toMatch(/^\s*import\s/m); + expect(contract).toMatch(/^export interface DesktopExecutorConfig \{/m); + const provider = readFileSync(repoPath("src", "types", "provider.ts"), "utf8"); + expect(provider).toContain('import("../adapters/cursor/desktop-executor-contract").DesktopExecutorConfig'); + expect(provider).not.toContain("native-exec-desktop"); + }); + test("desktopDepsFromConfig returns empty deps when nothing configured", () => { expect(desktopDepsFromConfig(undefined)).toEqual({}); expect(desktopDepsFromConfig({})).toEqual({}); diff --git a/tests/providers/cursor/cursor-tool-definitions.test.ts b/tests/providers/cursor/cursor-tool-definitions.test.ts index f21ae2fb27..a2bd4150fe 100644 --- a/tests/providers/cursor/cursor-tool-definitions.test.ts +++ b/tests/providers/cursor/cursor-tool-definitions.test.ts @@ -580,3 +580,17 @@ describe("Cursor code mode tool guidance", () => { expect(note).not.toContain("V8 isolate"); }); }); + +test("tool-definitions preserves leaf identities and naming stays the dependency root", async () => { + const { cursorToolWireName: leafWireName } = await import("../../../src/adapters/cursor/tool-naming"); + const { cursorToolInputSchema: leafInputSchema } = await import("../../../src/adapters/cursor/tool-schemas"); + const { buildCursorToolGuidanceSystemNote: leafGuidance } = await import("../../../src/adapters/cursor/tool-guidance"); + const { readFileSync } = await import("node:fs"); + const { repoPath } = await import("../../helpers/repo-root"); + + expect(cursorToolWireName).toBe(leafWireName); + expect(cursorToolInputSchema).toBe(leafInputSchema); + expect(buildCursorToolGuidanceSystemNote).toBe(leafGuidance); + // Quote-agnostic: the naming leaf is the DAG root and must not import any sibling tool-* leaf. + expect(readFileSync(repoPath("src/adapters/cursor/tool-naming.ts"), "utf8")).not.toMatch(/from\s+["']\.\/tool-/); +}); diff --git a/tests/providers/xai/xai-tool-schema.test.ts b/tests/providers/xai/xai-tool-schema.test.ts index 67edf07154..d6bea10ad9 100644 --- a/tests/providers/xai/xai-tool-schema.test.ts +++ b/tests/providers/xai/xai-tool-schema.test.ts @@ -1,4 +1,11 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { lookupLocalJsonPointer } from "../../../src/adapters/xai-tool-schema"; +import { + lookupLocalJsonPointer as lookupLocalJsonPointerFromAnalysis, + xaiSchemasArePairwiseDisjoint, +} from "../../../src/adapters/xai-schema-analysis"; +import { repoPath } from "../../helpers/repo-root"; import { createOpenAIChatAdapter as createOpenAIChatAdapterProduction, } from "../../../src/adapters/openai-chat"; @@ -400,3 +407,10 @@ describe("xAI Grok CLI tool schema normalization", () => { expect(body.tools).toBeUndefined(); }); }); + +test("schema-analysis leaf preserves pointer identity, disjointness, and import isolation", () => { + expect(lookupLocalJsonPointer).toBe(lookupLocalJsonPointerFromAnalysis); + expect(xaiSchemasArePairwiseDisjoint([{ type: "string" }, { const: "view" }])).toBe(false); + expect(xaiSchemasArePairwiseDisjoint([{ type: "string" }, { type: "number" }])).toBe(true); + expect(readFileSync(repoPath("src", "adapters", "xai-schema-analysis.ts"), "utf8")).not.toMatch(/^import\s/m); +}); diff --git a/tests/responses/responses-parser.test.ts b/tests/responses/responses-parser.test.ts index a15b104b6c..83e5687a0b 100644 --- a/tests/responses/responses-parser.test.ts +++ b/tests/responses/responses-parser.test.ts @@ -1,7 +1,11 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; import { buildResponseJSON } from "../../src/bridge"; import { parseRequest } from "../../src/responses/parser"; +import { buildTools } from "../../src/responses/parser-tools"; +import { parseTextFormat } from "../../src/responses/parser-text-format"; import { buildToolBridgeMaps } from "../../src/server/responses"; +import { repoPath } from "../helpers/repo-root"; describe("Responses parser", () => { test("normalizes function tool schemas to an object root without corrupting valid schemas (#745)", () => { @@ -931,3 +935,12 @@ describe("unpaired tool result boundary (#3259)", () => { }))).not.toThrow(); }); }); + +test("parser leaf seams preserve tool and format contracts without importing the request parser", () => { + const tools = buildTools([{ type: "function", name: "missing_parameters" }]); + expect(tools?.[0]?.name).toBe("missing_parameters"); + expect(parseTextFormat(undefined)).toBeUndefined(); + for (const leaf of ["parser-content.ts", "parser-tools.ts", "parser-text-format.ts"]) { + expect(readFileSync(repoPath("src", "responses", leaf), "utf8")).not.toMatch(/from\s+["\x27]\.\/parser["\x27]/); + } +}); diff --git a/tests/server/server-auth.test.ts b/tests/server/server-auth.test.ts index c03207d05b..11fddb9772 100644 --- a/tests/server/server-auth.test.ts +++ b/tests/server/server-auth.test.ts @@ -55,6 +55,7 @@ import { removeTreeWithRetry } from "../helpers/remove-tree"; const previousApiToken = process.env.OPENCODEX_API_AUTH_TOKEN; const previousOpencodexHome = process.env.OPENCODEX_HOME; const originalGlobalFetch = globalThis.fetch; +const originalGlobalWebSocket = globalThis.WebSocket; // A per-run directory, not a fixed path. This used to be // join(import.meta.dir, ".tmp-server-auth-test"), the exact same literal that // management-provider-validation.test.ts also declared, and both files delete and @@ -122,10 +123,24 @@ function poolProviders(): OcxConfig["providers"] { } function redirectCanonicalCodexTo(baseUrl: string): void { + const prefix = "/backend-api/codex"; + const currentWebSocket = globalThis.WebSocket; + // These fixtures serve HTTP/SSE only. Refuse the native upstream upgrade + // deterministically so its existing SSE fallback stays on the mocked fetch; + // downstream loopback WebSockets and other destinations remain real. + globalThis.WebSocket = new Proxy(currentWebSocket, { + construct(target, args, newTarget) { + const url = new URL(String(args[0])); + if (url.protocol === "wss:" && url.hostname === "chatgpt.com" + && (url.pathname === prefix || url.pathname.startsWith(`${prefix}/`))) { + throw new Error("HTTP-only Codex fixture rejects native upstream WebSocket"); + } + return Reflect.construct(target, args, newTarget); + }, + }); globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { const requestUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const url = new URL(requestUrl); - const prefix = "/backend-api/codex"; if (url.hostname === "chatgpt.com" && url.pathname.startsWith(prefix)) { const target = new URL(`${url.pathname.slice(prefix.length)}${url.search}`, baseUrl); return originalGlobalFetch(target, init); @@ -152,6 +167,7 @@ beforeEach(() => { afterEach(() => { globalThis.fetch = originalGlobalFetch; + globalThis.WebSocket = originalGlobalWebSocket; if (previousApiToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; else process.env.OPENCODEX_API_AUTH_TOKEN = previousApiToken; if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; @@ -536,6 +552,32 @@ describe("Responses request identity handoff", () => { }); describe("server local API auth", () => { + test("canonical HTTP fixture refuses only its native upstream WebSocket", () => { + const delegated: unknown[][] = []; + globalThis.WebSocket = new Proxy(originalGlobalWebSocket, { + construct(_target, args) { + delegated.push(args); + return {}; + }, + }); + redirectCanonicalCodexTo("http://127.0.0.1:1"); + + for (const path of ["/backend-api/codex", "/backend-api/codex/responses"]) { + expect(() => new WebSocket(`wss://chatgpt.com${path}`)) + .toThrow("HTTP-only Codex fixture rejects native upstream WebSocket"); + } + const protocols = ["fixture"]; + const urls = [ + "ws://127.0.0.1:1/v1/responses", + "ws://chatgpt.com/backend-api/codex/responses", + "wss://chatgpt.com/backend-api/codex-other", + "wss://other.example/backend-api/codex/responses", + ]; + for (const url of urls) new WebSocket(url, protocols); + expect(delegated).toEqual(urls.map(url => [url, protocols])); + expect(WebSocket.OPEN).toBe(originalGlobalWebSocket.OPEN); + }); + test("responses timeout helper disables Bun request timeout when available", () => { const req = new Request("http://localhost/v1/responses", { method: "POST" }); const calls: Array<[Request, number]> = []; diff --git a/tests/server/system-env.test.ts b/tests/server/system-env.test.ts index e44ca7b43e..a8c4175198 100644 --- a/tests/server/system-env.test.ts +++ b/tests/server/system-env.test.ts @@ -1,12 +1,19 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"; import * as childProcess from "node:child_process"; import * as fs from "node:fs"; +import { repoPath } from "../helpers/repo-root"; import type { OcxConfig } from "../../src/types"; import { cleanStaleSystemEnv, + getShellEnvFilePath, injectSystemEnv, + installShellHook, revertSystemEnv, } from "../../src/server/system-env"; +import { + getShellEnvFilePath as shellEnvFilePath, + installShellHook as shellInstallHook, +} from "../../src/server/system-env-shell"; const originalFetch = globalThis.fetch; const originalPlatform = process.platform; @@ -474,3 +481,12 @@ describe("systemEnv lever keys (devlog 136 B6)", () => { expect(shellWrite!.data).toContain('[ -z "${ANTHROPIC_DEFAULT_OPUS_MODEL+x}" ] && export ANTHROPIC_DEFAULT_OPUS_MODEL='); }); }); + +test("system-env preserves the shell seam without a back-import", () => { + readSpy.mockRestore(); + expect(installShellHook).toBe(shellInstallHook); + expect(getShellEnvFilePath).toBe(shellEnvFilePath); + const shellSource = fs.readFileSync(repoPath("src/server/system-env-shell.ts"), "utf8"); + expect(shellSource.split("\n").some(line => /from\s+["']\.\/system-env["']/.test(line))).toBe(false); + expect(fs.readFileSync(repoPath("src/server/system-env.ts"), "utf8")).toContain("catalog_busy"); +}); diff --git a/tests/vision/vision-cache.test.ts b/tests/vision/vision-cache.test.ts index 7ceb3f2338..bb6323425d 100644 --- a/tests/vision/vision-cache.test.ts +++ b/tests/vision/vision-cache.test.ts @@ -391,3 +391,16 @@ describe("vision description cache and per-turn cap", () => { expect(visionDescriptionRetainedStoreSnapshot().bytes).toBe(before.bytes - released); }); }); + +test("vision planning and image-rewrite seams preserve boundary identity and dependency direction", async () => { + const boundary = await import("../../src/vision"); + const planning = await import("../../src/vision/plan"); + const rewrite = await import("../../src/vision/image-rewrite"); + const { readFileSync } = await import("node:fs"); + const { repoPath } = await import("../helpers/repo-root"); + + expect(boundary.resolveMaxDescriptionsPerTurn).toBe(planning.resolveMaxDescriptionsPerTurn); + expect(boundary.stripImagesInPlace).toBe(rewrite.stripImagesInPlace); + expect(readFileSync(repoPath("src/vision/image-rewrite.ts"), "utf8")).not.toMatch(/from\s+["']\.\/(plan|index)["']/); + expect(readFileSync(repoPath("src/vision/plan.ts"), "utf8")).not.toMatch(/from\s+["']\.\/index["']/); +});