# FixKnownGaps — known alias / dialect / image-scheme gaps, red-first Lane: `FixKnownGaps` (production) · worktree `C:/Users/dwgx1/.dwgx/ops/artifacts/windsurfapi-autonomous-20261022/w103-aliasfix` Branch: `fix/known-alias-dialect-gaps` · base: `b00b3b9` (master, v3.10.1) · no push, no tag, no merge. Every gap below was reproduced on `b00b3b9` first (probe or failing test), fixed, then re-measured on the same probe. The probe is `../probe-alias-dialect.mjs` (read-only: it imports the tree's `src/` and stubs `http(s).get` in memory; no socket, no DNS, no writes). "before" = `D:/Project/WindsurfAPI` at `b00b3b9`, "after" = this worktree. --- ## 0. Probe, before → after | probe | before (`b00b3b9`) | after | gap | |---|---|---|---| | `resolveConnectSelector('swe-2-0-medium')`, cold | `{selector:'swe-1-6-slow',mapped:false}` | `{selector:'swe-2-medium',mapped:true}` | 1 | | `resolveConnectSelector('swe-2-0-high')`, cold | `{selector:'swe-1-6-slow',mapped:false}` | `{selector:'swe-2-high',mapped:true}` | 1 | | `resolveConnectSelector('swe-2-0-max')`, cold | `{selector:'swe-1-6-slow',mapped:false}` | `{selector:'swe-2-max',mapped:true}` | 1 | | same three, live catalog (targets present, `swe-1-6-slow` absent) | `{selector:null,mapped:false}` | `mapped:true` on the right tier | 1 | | `resolveConnectSelector('swe-2-0')` (control, row already existed) | `{selector:'swe-2-medium',mapped:true}` | unchanged | 1 | | `resolveConnectSelector('gpt5-6-luna')`, cold | `{selector:'swe-1-6-slow',mapped:false}` | `{selector:'gpt-5-6-luna-medium',mapped:true}` | 2 | | `resolveConnectSelector('gpt5.6-luna')` (control, row already existed) | `{selector:'gpt-5-6-luna-medium',mapped:true}` | unchanged | 2 | | `pickToolDialect('zhipu/glm-5.1','zhipu')` | `glm47` | `gpt_native` | 3 | | `pickToolDialect('zhipu/glm-5.1',null)` | `openai_json_xml` | `gpt_native` | 3 | | `pickToolDialect('moonshot/kimi-k2',null)` | `openai_json_xml` | `kimi_k2` | 3 | | `pickToolDialect('anthropic/claude-sonnet-4.6',null)` | `openai_json_xml` | unchanged | 3 (no Claude family rule) | | `pickToolDialect('openai/gpt-5-4-medium',null)` | `openai_json_xml` | `gpt_native` | 3 (matches bare `gpt-5-4-medium`) | | `pickToolDialect('x/y/glm-5.1',null)` | `openai_json_xml` | unchanged (one strip only) | 3 | | `pickToolDialect('foo/bar/baz',null)` | `openai_json_xml` | unchanged | 3 | | `extractImages([{type:'image_url',url:'HTTPS://…'}])` | `dialed:0, images:0` (silent skip) | `dialed:1` (fetch) | 4 | | `extractImages([{type:'image_url',url:'HTTP://…'}])` | `dialed:0`, **no warning** (silent skip) | `dialed:0` + `Image URL rejected: plaintext http (img.example.test)` | 4 | Raw probe output is quoted per gap below. --- ## 1. `swe-2-0-medium` / `-high` / `-max` — FIXED **Defect.** `SELECTOR_MAP` carried `swe-2-0` (→ `swe-2-medium`) and the dotted `swe-2.0-{medium,high,max}`, but not the dashed effort spellings. Measured on base: cold start degrades to the free selector; a live catalog with the real `swe-2-*` rows and no `swe-1-6-slow` returns `{selector:null,mapped:false}` → handler answers `model_not_found` for a model the account can call. ``` resolveConnectSelector(swe-2-0-medium) {"selector":"swe-1-6-slow","mapped":false} live resolveConnectSelector(swe-2-0-high){"selector":null,"mapped":false} ``` **Change.** `src/devin-connect-models.js:149-151` — three rows onto `swe-2-medium` / `swe-2-high` / `swe-2-max`. Targets verified present in `src/data/devin-catalog-snapshot.json` before writing (`swe-2-medium PRESENT / swe-2-high PRESENT / swe-2-max PRESENT`, 233-model snapshot); the unsuffixed `swe-2-0` target was already the same. No target was absent, so nothing was left out. **Red-first.** `9034d50` (test only): ``` ℹ tests 27 ℹ pass 26 ℹ fail 1 AssertionError [ERR_ASSERTION]: swe-2-0-medium actual: { mapped: false, selector: 'swe-1-6-slow' } expected: { mapped: true, selector: 'swe-2-medium' } ``` **Fix.** `81753eb` → same command `ℹ tests 27 ℹ pass 27 ℹ fail 0`. --- ## 2. `gpt5-6-luna` (compact prefix, no dash after `gpt`) — FIXED **Defect.** Three Luna spellings were mapped (`gpt-5.6-luna`, `gpt-5-6-luna`, `gpt5.6-luna`); the fourth — compact prefix *and* dashed minor — missed every lookup. ``` resolveConnectSelector(gpt5-6-luna) {"selector":"swe-1-6-slow","mapped":false} live resolveConnectSelector(gpt5-6-luna) {"selector":null,"mapped":false} ``` **Target check.** `gpt-5-6-luna-medium` is `PRESENT` in the snapshot; the bare `gpt-5-6-luna` and `gpt-5.6-luna` are not selectors (they are family aliases), so the alias points at the `-medium` tier, exactly like its three siblings. The brief's "check whether `gpt-5-6-luna` / `gpt-5.6-luna` exist as selectors" answer: **they do not exist as selectors; they exist as aliases already in the map**, and the real target `gpt-5-6-luna-medium` does exist — so the alias was added. **Map invariant (whole table, after the change).** Every one of the 121 `SELECTOR_MAP` rows resolves to a selector that is either in the 233-model snapshot or catalog-independent: ``` map rows: 121 snapshot: 233 aliases whose target is absent from the snapshot: 0 ``` **Change.** `src/devin-connect-models.js:163`. **Red-first.** `9eea1d6`: ``` ℹ tests 28 ℹ pass 27 ℹ fail 1 AssertionError [ERR_ASSERTION]: gpt5-6-luna actual: { selector: 'swe-1-6-slow', mapped: false } expected: { selector: 'gpt-5-6-luna-medium', mapped: true } ``` **Fix.** `d18fe0c` → `ℹ tests 28 ℹ pass 28 ℹ fail 0`. --- ## 3. Provider-prefixed dialect — FIXED **Defect.** `resolveConnectSelector` strips one leading `provider/` (`src/devin-connect-models.js:492` `.replace(/^[a-z]+\//, '')`), so `zhipu/glm-5.1` routes to `glm-5-2`; `pickToolDialect` matched the RAW name, so the same request was taught `glm47` markup — the markup the v2.0.72 probe proved GLM 5.x ignores. The consequence is the silent one: plain-text answer, no tool call, client waits. **Reachability (matters for severity).** On the DEVIN_CONNECT path the prompt preamble is built from `reqModelName` (raw) — `src/handlers/chat.js:3716` and `:3736` — while the response parser is constructed from `params.model`, which is the RESOLVED selector (`src/handlers/chat.js:3888`, `src/devin-connect-openai.js:661`, `:884`). So a prefixed name produced a **prompt/parser dialect mismatch**, not just a suboptimal prompt. After this fix both sides agree: ``` pickToolDialect('zhipu/glm-5.1', …) = gpt_native # prompt, raw name pickToolDialect('glm-5-2', …) = gpt_native # parser, resolved selector ``` **Change.** `src/handlers/tool-emulation.js:364` — strip `^[a-z][a-z0-9_-]*/` once, after lowercasing, before every family check. Provider-argument precedence is untouched (the `normalizedProvider` tests still come first and still win). **Red-first.** `b0d013d`: ``` ℹ tests 20 ℹ pass 19 ℹ fail 1 AssertionError [ERR_ASSERTION]: prefixed GLM with the provider argument still gets the GLM family dialect actual: 'glm47' expected: 'gpt_native' ``` **Fix.** `c322c9f` → `ℹ tests 109 ℹ pass 109 ℹ fail 0` over the three tool-emulation suites (`tool-emulation-gpt-native`, `tool-emulation`, `swe-streaming-toolcall`). --- ## 4. Uppercase `HTTP://` in `extractImages` — FIXED **Defect.** `url.startsWith('https://') || url.startsWith('http://')` on the `image_url` branch. Schemes are case-insensitive (RFC 3986 §3.1) and `new URL()` normalizes them anyway, so the only effect of the case-sensitive test was to SKIP the block: no fetch, no warning, no trace. ``` before: extractImages(image_url HTTPS://…) {"dialed":0,"result":"images=0"} extractImages(image_url HTTP://…) {"dialed":0,"result":"images=0"} (no warn line) after: extractImages(image_url HTTPS://…) {"dialed":1,"result":"still-pending"} # fetch started extractImages(image_url HTTP://…) {"dialed":0,"result":"images=0"} [WARN] Image URL rejected: plaintext http (img.example.test) — set WINDSURFAPI_ALLOW_HTTP_IMAGES=1 to allow it [WARN] Image fetch failed: Image URL must use https (plaintext http is rejected; …) ``` **Change.** `src/image.js:753` — `/^https?:\/\//i`. The plaintext gate is unchanged (`validateImageUrl`, `src/image.js:505`): uppercase HTTP is now *rejected* by the https-only rule instead of being skipped, which is the semantics the gate is supposed to have. **Red-first.** `6de1a02`: ``` ℹ tests 11 ℹ pass 9 ℹ fail 2 ✖ fetches an uppercase-scheme HTTPS image_url instead of skipping the block AssertionError: the scheme test must be case-insensitive actual: 0 expected: 1 ✖ rejects an uppercase-scheme HTTP image_url through the plaintext gate AssertionError: uppercase HTTP must hit the https-only rejection, not be silently skipped actual: '' expected: /plaintext http/i ``` **Fix.** `190d3d0` → `ℹ tests 42 ℹ pass 42 ℹ fail 0` over the four image/remote-image suites. **Note on the brief's "two call sites".** There is exactly ONE scheme test in `extractImages` at `b00b3b9`; repo-wide there is only one `startsWith('https://')` in `src/`: ``` $ grep -rn "startsWith('https://')|startsWith(\"https://\")|startsWith('http://')" --include=*.js . .\src\image.js:753: } else if (url.startsWith('https://') || url.startsWith('http://')) { ``` The second URL shape in `extractImages` — `{type:'image', source:{type:'url'}}` — has no scheme gate at all (it calls `fetchImageUrl` directly), so uppercase works there both before and after. Both shapes and both cases are covered by the new tests; the `image` shape is the control that the fix does not double-gate. --- ## 5. Mutation coverage — FIXED **Defect.** `test/mutations/remote-image-inline.json` ran only `test/connect-remote-images.test.js` (baseline 17), so the https-only plaintext gate and the `extractImages` scheme handling that this fetch path depends on could regress without failing the mutation run. **Change.** `test/mutations/remote-image-inline.json:2-6` — added `test/image-http-gate.test.js`, `expectBaselinePass` 17 → **28** (17 + 11), measured by running the suite: ``` $ node --import ./test/setup-env.mjs --test test/connect-remote-images.test.js test/image-http-gate.test.js ℹ tests 28 ℹ pass 28 ℹ fail 0 ℹ skipped 0 ℹ todo 0 ``` **Run (WSL, `Ubuntu-24.04`, node v24.21.0 at `/opt/node24/bin/node`), commit `a5e628c`, clean tree:** ``` baseline: test/connect-remote-images.test.js test/image-http-gate.test.js baseline 28 pass / 0 fail CAUGHT 4 a failed fetch is treated as 'nothing changed' … (pass=24 fail=4) CAUGHT 1 the result-side content-type check is dropped … (pass=27 fail=1) CAUGHT 1 streamChat stops resolving remote images … (pass=27 fail=1) all 3 mutation(s) behaved as expected; tree clean EXIT=0 ``` **Why a bundle instead of the worktree path.** Windows git writes `gitdir: D:/Project/…` into the worktree's `.git` file; under WSL that resolves relative to the cwd, so `git` reports `fatal: not a git repository: …/D:/Project/WindsurfAPI/.git/worktrees/w103-aliasfix` and the harness refuses (dirty-tree preflight crashes). The harness already materializes its own disposable clone, so the branch was exported with `git bundle create … HEAD` and cloned inside WSL; HEAD there is `a5e628c`, identical to this worktree, and `git status --porcelain` was empty before the run. --- ## 6. Acceptance suite ``` $ node --import ./test/setup-env.mjs --test test/devin-connect-models.test.js \ test/tool-emulation-gpt-native.test.js test/image-img1-base64.test.js \ test/image-img2-bound.test.js test/image-http-gate.test.js test/connect-remote-images.test.js ℹ tests 90 ℹ suites 15 ℹ pass 90 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ``` Every run used the single-file form (`node --import ./test/setup-env.mjs --test `), never a bare `node --test`. ## 7. Commits (base `b00b3b9`) | commit | what | |---|---| | `9034d50` | test: dashed swe-2.0 effort spellings (red) | | `81753eb` | fix: `swe-2-0-{medium,high,max}` → their swe-2 tiers | | `9eea1d6` | test: compact `gpt5-6-luna` (red) | | `d18fe0c` | fix: compact `gpt5-6-luna` | | `b0d013d` | test: provider-prefixed dialect (red) | | `c322c9f` | fix: strip one provider prefix in `pickToolDialect` | | `6de1a02` | test: uppercase scheme in `extractImages` (red) | | `190d3d0` | fix: `/^https?:\/\//i` in `extractImages` | | `a5e628c` | test(mutations): cover the image http gate, baseline 17 → 28 | The four red commits are intentionally non-green commits in the branch history (that is the red-first evidence); each is immediately followed by its fix. ## 8. Non-goals honoured - `glm-5-10` boundary untouched (`src/handlers/tool-emulation.js:389` `/^glm-5-(?:1|[2-9])(?:-|$)/` unchanged; `glm-5-12` / `glm-5-1x` still `glm47`, pinned by the existing test). Known limitation, not a regression: a hypothetical `glm-5-10` minor stays on `glm47`. - `src/dashboard/model-access.js` untouched. - No push, no tag, no merge. ## 9. Residuals found, NOT fixed here (wave-2 candidates) 1. **`resolveConnectSelector` strips a narrower provider class than `pickToolDialect` now does.** Resolver: `/^[a-z]+\//` (`src/devin-connect-models.js:492`); dialect: `/^[a-z][a-z0-9_-]*\//`. A hyphenated provider id (`z-ai/glm-5.1`, OpenRouter style) is therefore stripped by the dialect but not by the resolver → the name still degrades, now with a `gpt_native` prompt. Same function family, different file, outside this brief's named scope. P2. 2. **`data:` scheme test in `extractImages` is still case-sensitive** (`src/image.js:734`, `url.startsWith('data:')`). `DATA:image/png;base64,…` is silently skipped, the same defect class as gap 4, two lines above it. Not named in the brief; left alone deliberately. P3 (clients emit lowercase data URLs). 3. **Compact `gpt5*` spellings get an `openai_json_xml` prompt while their resolved selector gets `gpt_native`** (prompt from the raw name at `chat.js:3716`, parser from `selector` at `chat.js:3888`). Pre-existing for `gpt5.6-luna` since #244, now also for `gpt5-6-luna`. Measured impact is low, not a silent loss: the `gpt_native` parser salvages the JSON body the XML wrapper teaches — ``` parseToolCallsFromText('{"name":"bash","arguments":{"command":"ls"}}', {modelKey:'gpt-5-6-luna-medium', route:'devin_connect'}) → toolCalls ["bash"] ``` Left as-is (P3) rather than widening the GPT family regex in the same commit as the alias fix.