MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-2108

The ARCHIVED list read never projects `type`, yet its row type declares it — every archived row carries `type: undefined`

Done
Description

Found while running MOTIR-2098 (the hasDescription list-row projection), logged rather than absorbed per notes.html #27 — a drive-by fix gets its own card and its own PR.

The defect

workItemRepository.findArchivedByProject (lib/repositories/workItemRepository.ts) SELECTs id, kind, key, identifier, title, status, priority, assigneeId, reporterId, dueDate, estimateMinutes, storyPoints, updatedAt plus the archive bits — it never projects w."type".

But its row type declares it:

export interface ArchivedWorkItemRow extends WorkItemListRow { … }   // WorkItemListRow.type: WorkItemType | null

and toArchivedWorkItemDto spreads toWorkItemListItemDto(row), which reads row.type. So ArchivedWorkItemDto.type is undefined on every archived row — not null, which is what the DTO's own contract promises for a container.

Why it compiles

$queryRaw<ArchivedWorkItemRow[]> is an unchecked cast: TypeScript takes the row shape on faith, so a column the SELECT omits is a runtime hole, never a compile error. Exactly the class MOTIR-2098 had to guard against by hand when it added hasDescription to the same base row (it projects the new column in all five reads for this reason).

Blast radius today: LATENT

app/(authed)/items/archived/_components/archivedRows.ts shapes archived rows off kind only and never reads .type, so nothing renders wrong right now. The bug is the TRAP: the next consumer to read type on an archived row — an archived-view Type chip mirroring the /items one (8.8.9), a filter, an export — gets undefined and a silently blank cell, and its unit test will pass because the fixtures set type by hand (tests/components/archived-work-items.test.tsx does).

What to fix

  • Project w."type"::text AS "type" in findArchivedByProject, mirroring how findProjectIssuesFlat does it (the ::text cast is what makes $queryRaw return the plain enum label).
  • Assert it in tests/integration/work-items/archived-list.test.ts: an archived LEAF comes back with its work type, an archived container with null — the same pair issue-list-view.test.ts pins for the active read.

Worth considering in the same PR (not required)

Audit the other $queryRaw reads whose row type EXTENDS WorkItemListRow for the same omission, since the cast hides them all: findProjectIssuesKeyset, findProjectTreeLevel, findArchivedByProject. If a cheap compile-time guard exists (e.g. a satisfies-style column checklist next to each SELECT), name it as a follow-up rather than building it here.

Acceptance criteria

  • findArchivedByProject projects type; an archived leaf's DTO carries its work type and an archived epic/story carries null — never undefined.
  • An integration test pins both, against real Postgres.
  • No change to the archived view's rendering (it does not read the field yet) — this closes the contract hole, it does not add UI.