Code Smell / Tech-Debt Tracking
Tracking issue for maintainability findings from a structured code-smell audit of the extension source (extension/src, ~35k LOC, 272 files), 2026-06-02.
Context / scope:
knip runs clean (no dead code, no unused exports) and ESLint is wired, so this issue deliberately excludes anything tooling already catches. It covers design/maintainability smells only.
- The codebase is healthy overall (good DI seams, careful concurrency/lifecycle handling). Most items below are "grew too large" / copy-paste debt, not bugs. The exceptions are the four Real risks at the top.
- Items are grouped by severity. Severity is a triage hint, not a promise that everything is urgent. Check off as addressed; split into dedicated issues/PRs where useful.
- Update 2026-06-08 (re-verified): the source tree was reorganized since this audit. Everything now lives under
src/extension/ and src/webview/, so the paths below are stale by directory (findings re-verified against current code). All four Real risks remain fixed. No maintainability item is fully resolved yet, but two have a sub-part addressed (see the inline ⚠️ notes below): parseRecordedData (duplicate-schema) and extensionCommands (WS-status state machine). The show*Message count is now ~165 (was 144).
🔴 Real risks (latent correctness, do first)
🟠 God objects (oversized, mixed responsibilities)
🟠 Duplication
🟡 Scattered constants / magic numbers
🟡 Type-safety holes
🟢 Low / hygiene
Code Smell / Tech-Debt Tracking
Tracking issue for maintainability findings from a structured code-smell audit of the extension source (
extension/src, ~35k LOC, 272 files), 2026-06-02.Context / scope:
knipruns clean (no dead code, no unused exports) and ESLint is wired, so this issue deliberately excludes anything tooling already catches. It covers design/maintainability smells only.src/extension/andsrc/webview/, so the paths below are stale by directory (findings re-verified against current code). All four Real risks remain fixed. No maintainability item is fully resolved yet, but two have a sub-part addressed (see the inlineparseRecordedData(duplicate-schema) andextensionCommands(WS-status state machine). Theshow*Messagecount is now ~165 (was 144).🔴 Real risks (latent correctness, do first)
git status --porcelainparsing —services/workspace/workspaceFileChecker.ts:216.line.slice(3).trim()blindly slices; renames (R old -> new) and quoted/spaced paths get mangled and silently dropped from the file set sent to Iris. →git status --porcelain=v1 -z+ NUL-split, handle rename form. ✅ Fixed in fix(workspace): parse git status with -z to handle renames and spaced paths #258.api/artemisApi.ts:71(also:298,:392).makeRequest/authenticate/logoutFromServerhave noAbortController/timeout (git execs do); a stalled server hangs login/chat indefinitely. → thread anAbortSignalwith default timeout. ✅ Fixed in fix(api): add request timeouts to Artemis API calls #259. Follow-up fix(auth): keep stored credentials on transient startup failures #261 (don't clear stored credentials on a transient startup timeout). Deferred (LOW): body-read timeout —fetchWithTimeoutonly covers connect+headers; a full-body timeout needs an API-layer wrapper refactor.async dispose()on a synchronousvscode.Disposable—services/telemetry/recording/sessionRecorder.ts:491(alsostorageWriter.ts:255). Returns a Promise but the interface isvoid→ fire-and-forget disposal; the documented awaited-shutdown durability guarantee only holds on the one hand-wireddeactivate()path. → syncdispose()that schedules teardown + explicitawait drain()/shutdown(). ✅ Fixed in refactor(recording): harden recorder teardown contract, fix write-lane TOCTOU #260 (renameddispose()→shutdown()and droppedimplements vscode.Disposable, so the awaitable teardown can't be structurally mistaken for a sync disposable).await, masked by!—services/telemetry/recording/storageWriter.ts:190(also:211,:383). Guard on_snapshotsDir, then_snapshotsDir!inside a queued lambda;endSession/abortcan null it in between → runtime crash instead of a typed branch. → capture path into a localconstbefore enqueuing. ✅ Fixed in refactor(recording): harden recorder teardown contract, fix write-lane TOCTOU #260 (:190/:211captured into locals;:383was already safe — its guard runs inside the lambda).🟠 God objects (oversized, mixed responsibilities)
provider/chatWebviewProvider.ts(966 LOC) — largest file in the repo. Extract the command dispatcher (_handleCommand:513) + ~10_handle*methods into aChatWebviewMessageHandler(mirroring the sidebar'sWebViewMessageHandler); leave the provider as a thin coordinator.api/artemisApi.ts(587 LOC) — one class owns auth + submission/result + Iris + problem-statement endpoints. Split intoAuthApi/ProgrammingApi/IrisApiover a sharedmakeRequestcore.services/telemetry/telemetryManager.ts(536 LOC) — orchestrates 13 sub-services, 8 listeners, config loading, debug lifecycle, and decision dispatch. Extract event-listener wiring and the config/debug lifecycle into collaborators.activation/extensionCommands.ts(707 LOC) +extension.tsactivate()(~235 LOC) — command file contains a ~110-line WS-status state machine;activate()has a load-bearing but implicit construction order. Move the WS-status logic to its own module; splitactivate()into phase helpers.collectWebSocketStatus/buildStatusReport/decideStatusActions/buildStatusHeadline/handleStatusAction). The file is still 707 LOC andactivate()(~236 LOC) is unchanged.views/IrisChat/IrisChatView.tsx(646 LOC) — 12-case message reducer + send/retry/feedback + 5 layers of nested render-gating ternaries (:561-598). Extract auseIrisChatMessageshook + a purederiveChatUiStateselector.views/ExerciseDetail/ExerciseDetailView.tsx(623 LOC) — ~250-line derivation engine in the render body that reruns on every WebSocket re-render. Extract auseExerciseDetailViewModelselector; move time-remaining into existing utils.services/iris/chat/chatSessionService.ts(692 LOC) — availability + concurrency + settings probing + formatting + create/switch/reset/reload, with repeatedisCurrentContextguard blocks. Extract availability classification + a shared guard helper.services/telemetry/recording/parseRecordedData.ts(786 LOC) — 36 hand-written validators, each event schema declared twice (here +types.ts) with redundantascasts. Replace with a schema lib (zod/valibot) or a small field-spec DSL.EVENT_PARSERStable (satisfies Record<RecordedEvent['type'], EventParser>; a missing parser fails compilation, and the runtime type set is derived from its keys, per Schema source of truth: deduplicate event-type lists between parser and validator #215). The 40 hand-written validators (was 36) remain.🟠 Duplication
withErrorBoundaryfor command handlers —controller/commands/*.try { … } catch { logger.X; showErrorMessage }is copy-pasted across dozens of handlers (144show*Messagecalls in scope) and already drifts. → a singlewithErrorBoundary(label, fn)decorator.useViewInit+<ViewScaffold>— ~13 webview views each re-wire init listener,reload/retry/back,getState/setStatepersistence, and loading/error/empty scaffolding; they already diverge (localisLoadedvs storeisLoading; only some handleViewInitError).showNotificationEQ/showProactiveHelpEQ—services/telemetry/interventionService.ts:157vs:201. Near-identical incl. a copy-pasted accept/dismiss block. → shared_showModalIntervention+_handleResult.action/level/triggerType/dismissReasonspelled out three times (types.ts+ parserisOneOf+ recorder API). Adding an enum value silently drops events in replay if one copy is missed. → exportconst-asserted arrays and derive unions + guards. Note (2026-06-08):blockedReasonis now only 2× (the recorder references the derivedInterventionEvent['blockedReason']type).action/level/triggerType/dismissReasonremain 3×.recording/observation/observationRegistry.ts:103); repo-match block 4× (workspace/workspaceDetectionService.ts:216); Git-extension API access reimplemented twice with opposite type rigor (workspaceFileChecker.ts:127vsfileMonitorService.ts:65); logout sequence 2× (extensionCommands.ts:27vsauthCommands.ts:59); chat-retry payload rebuilt (artemisApi.ts:478).🟡 Scattered constants / magic numbers
interventionService.ts:42(0.45/0.80),decision/interventionDecisionEngine.ts:27(0.15/0.35/0.60),eventPipeline/compileEquivalentEmitter.ts:210, despitetypes.tsbeing the advertised config home. Also:LOOKAHEAD_WINDOW_MS=500(replay) must stay in lock-step with the live emitter's 500ms timer but nothing links them → silent desync risk.'iris.chatView.focus',getConfiguration('artemis'), etc. hard-coded inline despiteVSCODE_CONFIGconstants existing;'artemis.trustedDomains'global-state key duplicated (utilityCommands.ts+extensionCommands.ts:522).views/StruggleDetection/StruggleDetectionView.tsxhard-codes EQ color hex values that duplicate the decision-engine threshold semantics; dozens of inlinestyle={{}}bypass the CSS-module convention used elsewhere.🟡 Type-safety holes
decision.levelcasts + tighten API unions —activation/sessionRecorderWiring.ts:92-118castsdecision.level5×;shared/types/apiResponses.ts:67/78/99typestate/type/initializationStateas barestringinstead of literal unions, forcing runtime narrowing in every view.typeof x === 'function'checks —controller/commands/navigationCommands.ts:90/95,irisCommands.ts:68/92guard methods that theIChatWebviewProviderinterface declares as required; dead defensive branches. A plain null-check suffices.🟢 Low / hygiene
JSON.stringify(payload)oninfo-level hot paths (iris/chat/chatMessageService.ts). → demote step tracing to debug/trace, drop emoji from log lines.id(CourseListView.tsx:270/325; Dashboard tracks expanded state by array index); internal"Block F/AB/K"provenance comments inrecording/types.tsreference an unresolvable plan;errorCountdiverges between the two replay snapshot builders (replay/snapshotReconstructor.ts:51raw count vs:78family count) — currently not an active bug since EQ uses onlyhasErrors/errorFamilies, but a latent trap iferrorCountis ever consumed.