diff --git a/docs/native-kanban-sot/P0-MAP-CURRENCY-2026-08-29.md b/docs/native-kanban-sot/P0-MAP-CURRENCY-2026-08-29.md index b06f5746..c50b4f30 100644 --- a/docs/native-kanban-sot/P0-MAP-CURRENCY-2026-08-29.md +++ b/docs/native-kanban-sot/P0-MAP-CURRENCY-2026-08-29.md @@ -73,12 +73,22 @@ SHARED-CONTRACT.md §5.1 phase 1 requires an N-1 patch that stops `mission_tasks.status` as a write source, plus a writer inventory, before any expand DDL. -- **Sole status write path:** `packages/brain/src/mission-tasks.ts` +- **Sole authoring write path:** `packages/brain/src/mission-tasks.ts` `create`/`update` (Drizzle insert/update on `mission_tasks`), invoked by `apps/gateway/src/missions/missions.controller.ts`. `update` accepts `Partial`, so `status` is writable through both DTOs today. The same module also exposes `remove`/`removeByMission` DELETE paths — immaterial to `status` writes, listed for inventory completeness. +- **Storage-layer surfaces that touch the column without authoring it** + (added 2026-08-29 after independent review of the phase-1 patch): + `packages/storage/src/migrate-tier.ts` copies whole `mission_tasks` rows + between storage tiers and must preserve the stored `status` verbatim — row + transport, exempt from the write prohibition (stripping there would corrupt + data inside the N-1 window). The generic table-keyed storage adapters + (`adapters/postgres.ts`, `adapters/pglite.ts`) register `mission_tasks` in + their table maps but have no caller that targets it: measured at this head, + every runtime adapter caller passes a fixed collection constant + (preferences/insights). Neither surface authors a new `status` value. - **Read-only consumers of `mission_tasks`:** federation verb services (`get-query.service.ts`, `list-query.service.ts`) select only. The MCP `brain_*` tools do not touch `mission_tasks` at all; `brain_create_task` / diff --git a/packages/brain/src/mission-tasks.spec.ts b/packages/brain/src/mission-tasks.spec.ts index fb1ab0f4..805207c7 100644 --- a/packages/brain/src/mission-tasks.spec.ts +++ b/packages/brain/src/mission-tasks.spec.ts @@ -2,11 +2,13 @@ import { describe, it, expect, vi } from 'vitest'; import { createMissionTasksRepo } from './mission-tasks.js'; /** - * SHARED-CONTRACT §5.5 "mission_tasks.status write prohibition": the repo is - * the sole write path, and it must never forward a caller-supplied status to - * the database on create or update. Callers keep working (the field is - * accepted and ignored), so these tests assert on what reaches the Drizzle - * chain, not on rejection. + * SHARED-CONTRACT §5.5 "mission_tasks.status write prohibition": this repo is + * the sole path that authors mission_tasks.status from caller input (storage + * tier migration is row transport and preserves stored values; the generic + * storage adapters have no mission_tasks caller), and it must never forward a + * caller-supplied status to the database on create or update. Callers keep + * working (the field is accepted and ignored), so these tests assert on what + * reaches the Drizzle chain, not on rejection. */ function makeInsertDb(returned: unknown[]) { diff --git a/packages/brain/src/mission-tasks.ts b/packages/brain/src/mission-tasks.ts index d08baeed..22b21ba2 100644 --- a/packages/brain/src/mission-tasks.ts +++ b/packages/brain/src/mission-tasks.ts @@ -4,11 +4,17 @@ export type MissionTask = typeof missionTasks.$inferSelect; export type NewMissionTask = typeof missionTasks.$inferInsert; // SHARED-CONTRACT §5.1 phase 1 / §5.4: mission_tasks.status is prohibited as a -// write source through the N-1 window. This repo is the sole write path, so the -// field is stripped here — accepted and ignored rather than rejected, because -// the legacy surface is frozen with existing consumers kept working -// (tool-gateway-mapping.md §3.2). The column keeps its DB default, stays -// declared and readable, and is retired only after no readers remain. +// write source through the N-1 window. This repo is the sole path that authors +// status from caller input, so the field is stripped here — accepted and +// ignored rather than rejected, because the legacy surface is frozen with +// existing consumers kept working (tool-gateway-mapping.md §3.2). Two other +// surfaces touch the column and are deliberately NOT stripped: +// packages/storage/migrate-tier.ts copies whole rows between storage tiers and +// must preserve the stored value verbatim, and the generic table-keyed storage +// adapters register mission_tasks but have no caller that targets it (runtime +// callers use fixed collection constants). Neither authors a new status. The +// column keeps its DB default, stays declared and readable, and is retired +// only after no readers remain. function stripStatus(data: T): Omit { const rest = { ...data }; delete rest.status;