Skip to content

Quality hardening: real bug fixes + SOLID/DRY refactors + honest coverage + tooling spine#46

Open
alichherawalla wants to merge 196 commits into
mainfrom
chore/quality-hardening
Open

Quality hardening: real bug fixes + SOLID/DRY refactors + honest coverage + tooling spine#46
alichherawalla wants to merge 196 commits into
mainfrom
chore/quality-hardening

Conversation

@alichherawalla

@alichherawalla alichherawalla commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What this is

The chore/quality-hardening branch: fix the real bugs, land the SOLID/DRY refactor
backlog, bring coverage to an honest floor, and stand up the code-quality tooling spine.
One PR per repo (pro is a separate repo — companion PR in desktop-pro, same branch name).

🐞 Real bugs fixed (each with a regression test)

  • 3 divergent ext→MIME maps unified into src/main/mime.ts — fixed a .webp attachment
    mislabelled image/jpeg (vision model may reject) and gifimage/png.
  • Upload picker vs router drift — the file-dialog allowlist now derives from the router's
    classify sets (was omitting gif/bmp/heic/opus/avi the router handles).
  • Double image-intent decision (root of the image-gen-as-tool bug) — the renderer no longer
    pre-decides when the agentic tool path owns the turn.
  • (from the earlier coverage work on this branch) image-gen params single-source, reasoning
    survives reload, KV-cache choice survives restart.

Refactors (SOLID/DRY — behavior-neutral, tested)

ToolDef.run uniform dispatch (DIP); one mimeForExt; appNameLikeClause (7 rag:chat sites);
likeMatch/LIKE_COLUMNS (facet counts == results); epochMsSql; shared isValidGguf,
artifact-kind/model-kind label maps, CHAT_JOB/IMAGE_JOB, ffmpeg-decode, streamCompletion
(one SSE transport), parseSqliteUtc, parseSessionId; ChatList link cyan→emerald token.

Coverage — honest floor, enforced

all:true + the true testable-surface include/exclude (native/IPC/spawn shells excluded, each
covered by e2e/smoke/test:db). Ratchet floor in vitest.config.ts, blocked on pre-push AND CI.
Now 97.1 / 92.1 / 96.0 / 98.1 (stmts/branches/funcs/lines). README carries the badges.

Quality-tooling spine (new)

  • dependency-cruiser (blocking CI) — open-core boundary (core ↛ pro) + pure/IO isolation +
    circular/unresolvable/dev-dep/orphans. 0 violations.
  • SonarCloud Automatic Analysis on core (content: bugs/smells/duplication).
  • tsc dead-code gates (both configs) — noUnusedLocals/noUnusedParameters/allowUnreachableCode:false.
  • knip (npm run knip) — module-graph dead code; first pass deleted 27 unused template UI files.
  • typed no-unnecessary-condition (warn-ratchet) — dead-branch detection.

Evidence

  • npx tsc --noEmit — both tsconfig.node.json and tsconfig.web.json: 0 errors.
  • npm run test:coverage2110 passed, 1 skipped; floor held (97.1/92.1/96.0/98.1).
  • npm run depcruise0 violations.
  • Diffstat: 139 files, +7689 / −4654.

Honest gaps (not blocking this PR)

  • Functional/on-device verification not yet run this cycle — the gates prove the tested surface
    (regression safety) + structural health; they do NOT verify end-to-end functionality (real model
    chat/image-gen, connectors, capture). Recommend the Playwright e2e tour + a live golden-path smoke
    before release. Screenshots therefore pending the e2e run.
  • Deferred backlog tracked in docs/CODE_AUDIT.md (now a clean "what's left" list): ImageRuntime,
    imagegen/ipc/database SRP splits, tabler→Phosphor, and the warn-ratchet lint backlog
    (no-unnecessary-condition 289, sonarjs-on-pro 295, knip deps/exports). Each lands as its own
    verified change — not this PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Image generation now keeps per-model steps/size overrides across model changes and restarts.
    • “Thinking” reasoning is persisted and restored after reloads.
    • Upload routing supports more file formats and keeps file-picker allowlists in sync.
    • Tool streaming now returns more structured citations/sources and defers image generation via requests.
    • Artifact/model/connectors labels are more consistent across the UI.
  • Bug Fixes
    • Correct MIME handling for WebP and other image types; improved Content-Type for served/captured content.
    • Image intent routing better respects active tools/connectors and avoids unwanted auto-routing.
    • Pressing Enter in the identity panel now saves changes.
  • Tests
    • Expanded automated coverage across uploads, search, licensing, streaming, image generation, and persistence.

Summary by Gitar

  • Tooling & Infrastructure:
    • Integrated dependency-cruiser for architectural boundary enforcement and knip for dead-code elimination.
    • Added comprehensive automated test coverage for persistence, streaming, and tool interaction logic.
  • Core Logic Refactors:
    • Unified MIME type handling for image attachments and standardized session ID parsing logic.
    • Extracted IPC query logic into ipc-query-logic.ts and refined tool dispatch using a uniform ToolResult structure.
  • LLM Engine Hardening:
    • Implemented a fresh-connection-per-request policy (agent: false) for http.request to prevent ECONNRESET in multi-round tool loops.
    • Added an userExplicit pin-set for LLM settings to ensure user-selected performance modes and cache configurations persist across restarts.
  • Composer & State Persistence:
    • Enabled persistence for reasoning blocks, per-model image parameter overrides, and global image generation settings.
    • Fixed image-gen state drift by ensuring the composer and model-picker share a single source of truth for active model selection.
  • Type Safety:
    • Enabled noUncheckedIndexedAccess across the project, resulting in 14/14 module migrations and associated guard logic.

This will update automatically on new commits.

User and others added 30 commits July 10, 2026 08:56
Correct the coverage measurement to the true testable surface: all:true +
.ts include (was all:false, which hid ~63% of the surface behind a flattering
77%), exclude native/IPC/spawn shells (logic extracted to measured siblings)
and e2e-covered .tsx components. Add a React render harness (jsdom +
@testing-library/react + resolve aliases + .tsx test glob) so renderer
components get real mount tests. Floors ratcheted to measured (95/90/93/96).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…merge)

An explicit kvCacheType (q8_0) silently reverted to f16 because a
performanceMode patch applied MODE_PRESETS and unconditionally overwrote the
user's kvCacheType/flashAttn/ctxSize, persisting the clobber — re-triggered on
every mode (re)pick. Fix: a pure applyModePreset() that MERGES — a preset only
fills fields the user hasn't explicitly pinned (userExplicit set, persisted +
reloaded). Extract buildLaunchArgs() as the single source for the launch argv.
Terminal-artifact test drives the real persist->restart->launch-args round-trip
(q8_0 still in --cache-type-k after a balanced pick + restart), red-proven 3 ways.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two data-layer/presentation drift bugs (§A). (1) Image composer kept local
copies of the active model + steps: the dropdown never wrote through to
setActiveModalModel (diverged from the Active-models panel) and a model-change
effect stomped the user's steps ("showed 10, generated 28"). Now the dropdown
binds to the owning source and steps/size/seed/negative persist per-model
(override ?? default resolver). (2) Reasoning/thinking showed live but was
never persisted, so it vanished on conversation remap — now carried through the
message context blob and restored by mapRagMessages. Terminal-artifact tests:
a real <MemoryChat/> mount asserts the generateImage payload carries the user's
steps + model; a real DB round-trip asserts reasoning restores. Red-proven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tral)

Pull the decision logic out of I/O/IPC shells into zero-import siblings the
default runner can exercise, each unit-tested; originals re-import (no
duplication, no behavior change): files-classify, tts-logic, vectors-predicates,
mcp-parse-data-url, rag/prompt, skills-parse, ipc-query-logic, tools-parsers,
+ export isActionTool from the MCP connector extension. Full suite stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isProActive/toInfo + the Keygen JSON:API parsers pulled out of the
Keychain/fetch shells (behavior-neutral) and unit-tested — the app-wide pro
gate now has real coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… theme, reasoning round-trip)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/CODE_AUDIT.md: ~40 evidence-based SOLID/DRY findings (4 real bugs + the
renderer-has-no-store-layer root cause), prioritized, with a scope note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three divergent ext->MIME maps (ogcapture protocol in index.ts, the media HTTP
server, image-attachment sniffing in data-url.ts) meant adding a format to one
served the wrong/absent type on the other paths. Collapse to a single
mimeForExt(ext, fallback) map in src/main/mime.ts; each caller keeps its own
fallback (file-serving -> octet-stream, image attach -> image/png) so every
existing path is byte-identical.

Two real bugs fixed by the consolidation:
- tools.ts inlined an endsWith(.png) ? png : jpeg guess, so a .webp attachment
  was labelled image/jpeg (vision model may reject). Now routed through
  mimeFromExt -> image/webp.
- gif attachments resolved to image/png (wrong); now image/gif.

Tests: mimeForExt map/dot-strip/case/fallback + a source-read guard that tools.ts
no longer re-inlines the png/jpeg ternary; data-url gif assertion corrected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The knowledge-base file picker (rag-ipc) hand-maintained its own extension
allowlist, separate from files-classify's IMAGE/AUDIO/VIDEO sets that the
processor routes on. They had already drifted: the picker omitted
gif/bmp/heic/opus/oga/aiff/aif/avi that the router handles, so a user could not
select files the processor was perfectly able to ingest.

Add DOC_EXT + uploadPickerExtensions() to files-classify (single source) and
build DOC_FILTERS from it. Purely additive to the picker (no format removed);
picker and router can no longer disagree.

Tests: every router-classified format is offered; every offered format resolves
to a real handler; deduped; source guard that rag-ipc builds from the helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n agentic mode

The renderer's keyword auto-route (looksLikeImageRequest) and the agent's own
tool decision both decided "is this an image request?" for the same turn and
could disagree. With tools/connectors on, a "draw ..." message tripped the
renderer's direct-generate fast-path and NEVER reached the generate_image tool —
the root of the image-gen-as-tool bug.

Extract the pre-decision into shouldAutoRouteImage() (pure, in image-intent) with
the fix baked in: when the agentic path owns the turn, the renderer returns false
and lets the model decide (image generation is a tool there). Behavior is
unchanged in plain chat — the keyword auto-route still fires.

Tests: shouldAutoRouteImage every branch (mode/availability/agentic/keyword); and
a render-harness wiring proof on the terminal artifact — tools ON + "draw a dog"
crosses on toolChat with NO direct generateImage; tools OFF auto-routes to
generateImage with the cleaned "a dog" prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ervice

The magic-byte + min-size integrity check was byte-identical in
models-manager.importLocalModel and LLMService.validateGguf — two hand-kept
copies. Extract to models/gguf.ts (isValidGgufHeader pure + isValidGgufFile with
injected fs); both call sites delegate. Behavior-neutral.

Tests: pure header judgement (size floor, magic match/mismatch) + isValidGgufFile
against real temp files (valid, truncated, wrong-magic, missing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two label maps were each defined twice in the renderer:
- artifact badge labels: identical in ArtifactCanvas and ProjectsScreen
- model-kind labels: ModelsScreen vs StoragePanel (already drifted — StoragePanel
  carried an extra `other` bucket the other lacked)

Collapse each to one source: lib/artifact-labels.ts (ARTIFACT_KIND_LABELS +
artifactKindLabel) and lib/model-kind-labels.ts (MODEL_KIND_LABELS superset +
modelKindLabel). The long-form fallback title in main/artifacts.ts (labelFor:
"HTML page"/"React component") is a separate concern and left as-is.
Behavior-neutral (model-kind gains `other` on the Models screen, previously an
identity fallback anyway).

Tests: exact label maps locked + fallback path for both helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e DIP/SRP tail

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The exact ffmpeg argv (-y -i <in> -vn -ar 16000 -ac 1 -f wav <out>) plus the
10-minute decode timeout were copy-pasted into whisper-cli, whisper-server, and
parakeet-cli — a change to the sample rate / channels / timeout meant editing
three sites in sync. Extract decodeToWavArgs + DECODE_TIMEOUT_MS to
transcription/ffmpeg-decode.ts; all three call it. Each keeps its own ffmpeg
resolution + tmp lifecycle. Behavior-neutral (byte-identical argv).

Tests: exact argv + the 16kHz/mono/wav/no-video contract + input/output
placement + the timeout constant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chatStream (message -> answer) and streamChat (raw messages + tool calls) each
carried a ~40-line copy of the same streaming Promise body: buffered newline
split, parseSseLine, reasoning/answer routing, tool-call accumulation, timeout,
cooperative abort, and error handling. Extract to llm/stream.ts streamCompletion
(returns { content, toolCalls }); chatStream takes only content (it sends no
tools), streamChat returns the pair. Behavior-neutral. Also drops the now-unused
http / parseSseLine / splitter imports from llm.ts.

Tests: NEW integration test drives streamCompletion against a real local SSE
server — content stream, reasoning channel, delta split across TCP boundaries,
tool-call reassembly, non-200, timeout, mid-stream abort. This covers transport
logic that previously lived (twice) in the coverage-excluded llm.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iterals

modalityQueue.run({ tier: 2, label: 'chat', evicts: ['image'] }) was inlined 4×
in ipc.ts, and the image counterpart once in imagegen. Export frozen CHAT_JOB /
IMAGE_JOB from the queue module; both callers use them. The mutual-eviction
contract (chat evicts image, image evicts llm — they can't co-reside on unified
memory) is now stated once. Behavior-neutral.

Tests: the two descriptors' shape, mutual eviction, and frozen-ness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chat-list preview's markdown link was text-cyan-300 — cyan is not in the Off
Grid palette (DESIGN.md: emerald is THE accent for links). Switch to the emerald
token (text-green-500), matching the composer's link style. The three per-surface
markdownComponents maps are intentionally styled per density (dense ChatDetail,
compact MemoryChat, preview ChatList) and NOT merged — that would change visuals,
not a behavior-neutral refactor.

Test: source guard — no cyan class in ChatList, link uses the emerald token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`dateStr.replace(' ', 'T') + 'Z'` (parse a zone-less SQLite UTC timestamp) was
inlined in ChatDetail (2×) and ChatList. Export parseSqliteUtc from lib/time
(with the tz guard timeAgo already used) and route all three through it; toDate
delegates to it too, so the module has one parse. Behavior-neutral — timeAgo's
suite stays green and the new parse matches the old inline exactly.

Tests: parseSqliteUtc UTC handling + equivalence to the old inline + no
double-zone; timeAgo's 12 existing cases still pass over the unified parse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ChatDetail and ChatList each parsed a session id ("<model>-<dashed-title>") into
model name / readable title / LLM label with identical inline code — a drift
risk on how titles render across the two views. Extract parseSessionId to
lib/session-id.ts; both consume it (ChatDetail's inline model-label replace now
uses the shared llmLabel too). Behavior-neutral.

Tests: model/title split, underscore->space label, no-dash fallback, empty-title
fallback to the raw id, leading-dash edge (indexOf > 0 guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ng()

findBinary hand-rolled the first-existing-path loop that bin-resolution.existing()
already owns (and whisper-cli/parakeet-cli use). Build the candidate paths and
delegate. Behavior-neutral (existing() adds a defensive try/catch around
existsSync; a bad path still resolves to "not found").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LoRA/checkpoint extension set /\.(safetensors|ckpt|gguf|pt)$/i was inlined 3×
in imagegen (the LoRA lister's include filter + display-name strip, and the
LoRA-path builder's default-extension). Extract CHECKPOINT_EXT + hasCheckpointExt
/ stripCheckpointExt / ensureCheckpointExt into imagegen/model-filter.ts (pure,
tested); all three sites use them. Behavior-neutral.

Tests: the four extensions (case-insensitive), strip, and safetensors default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…engine refactors

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ranching

The agentic loop branched on tool identity: `if (c.name === 'search_memory')`
(build citations) `else if (=== 'generate_image')` (record prompt) `else` generic.
A tool with a side channel meant a new branch, and search logic was written twice
(the tool's text-only run + the loop's citation version).

Push the decision behind the interface: ToolDef.run(args, ctx) returns
string | ToolResult { text, sources?, imageRequest? }; the loop normalizes and
merges uniformly (sources deduped into `unified` across rounds; last non-empty
imageRequest wins). search_memory now owns its citations + self-exclusion (via
ctx.conversationId); generate_image owns its deferred imageRequest. Adding a tool
with a side channel needs zero loop edits. execute() folded into one runTool()
dispatch (ext → built-in → error text). Behavior-neutral for every existing path;
one intentional hardening — a throwing extension tool now yields error text
instead of aborting the whole turn (matches built-in behavior).

Tests (real loop, only the model + search boundaries faked): search_memory
citations surface in r.unified with excludeChatId self-exclusion; dedup across two
rounds (dynamic per-round hits); empty-hits text; + a DIP guard asserting the loop
source no longer contains `c.name === 'search_memory'/'generate_image'`. Existing
18 loop cases (generate_image last-wins/empty, extension routing, calculator, step
order) stay green; MemoryChat render wiring unaffected (return contract unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ADME

CI ran `npm test` (no --coverage), so the coverage ratchet was only enforced on
pre-push — a regression could land via CI. Switch the CI test step to
`npm run test:coverage`, which runs the same suite AND fails on any drop below the
vitest.config.ts thresholds (same gate as pre-push).

Make the pro/** threshold group conditional on pro being checked out (existsSync
guard), so a core-only CI run (fork without the cross-repo token) measures + gates
core alone instead of erroring on an empty pro/** glob.

README: four coverage badges (statements/branches/functions/lines, emerald token)
reflecting the measured `npm run test:coverage` numbers; a comment notes the
CI-enforced floor and to refresh them as it rises.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The optional app-name filter — the `appName && appName !== 'All'` guard plus the
`%${appName}%` LIKE wildcarding — was hand-inlined SEVEN times across
db:get-memories and the rag:chat vector / FTS-fallback / message / summary /
entity / entity-fact queries, each with a different column, so the facet filter
could silently disagree between result sets. Extract appNameLikeClause(appName,
column) to ipc-query-logic (pure, tested); every site builds its filter from it
and keeps its own connector / subquery shape. Behavior-neutral.

Tests: null for All/empty/undefined; clause+param for a specific app; per-column;
+ a source guard that ipc.ts has NO remaining inline appName+LIKE guard and uses
the helper (the guard caught 3 sites beyond the first pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The no-FTS LIKE search built its WHERE clause AND its bound params by hand in TWO
places per source — the Sources-rail facet COUNT query and the hit builder — for
chat / doc / meeting (byte-identical) plus frame. If a column were added to one
copy, a source's facet count would silently disagree with its actual results.

Extract LIKE_COLUMNS (the per-source column sets, single source of truth) +
likeMatch(columns, terms) → { where, args } into search-ranking (pure, tested);
facet and hit both build from it, so clause and params can never drift apart.
Behavior-neutral (frame's single column gains harmless parens; all other SQL
byte-identical — asserted in tests).

Tests: OR-per-term/AND-joined clause, per-term wildcard params, placeholder count
always equals params length across every column set, single-column + empty-terms
edges, and the column sets locked to what search.ts used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-connector setup instructions (SETUP_HINTS) lived as a map inside the
ConnectorsScreen view — catalog DATA in the presentation layer (§A). Move it beside
CONNECTOR_CATALOG in connectorCatalog.ts as CONNECTOR_SETUP_HINTS + setupHintFor(id);
the view reads via the helper. Behavior-neutral.

Tests: every hint id is a real catalog connector (orphan-hint guard), setupHintFor
known/unknown, and a source guard that the hints are no longer inlined in the view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erified-only remainder

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The data-layer drift sweep's TIER-1 items were already fixed + tested this branch;
verified each against the shipped code and moved them to RESOLVED with evidence
(file:line + test names). Close the two remaining safe TIER-2 items:
- preload setLlmSettings type now carries kvCacheType/flashAttn/gpuLayers/threads/
  batchSize/performanceMode (was a type-check blind spot; runtime already passed them).
- Settings identity fields commit on Enter too, not just blur (no more lost edit if
  the screen closes without blurring).
ctxSize crash-recovery halving is documented as by-design; ephemeral view prefs are
by-design. GAPS_BACKLOG OPEN section is now empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…progress on session limits

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.3% Duplication on New Code (required ≤ 3%)
E Security Rating on New Code (required ≥ A)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant