220 lines
32 KiB
Markdown
220 lines
32 KiB
Markdown
# Native Kanban/SOT — Remediated Shared Contract v1
|
||
|
||
**Status:** INDEPENDENT REVIEW GO; freezes as v1 when issue #751 canon merges to `main`
|
||
**Version:** 1.0.0-rc.3
|
||
**Date:** 2026-07-14
|
||
**Change authority:** Mosaic control plane/Jason only
|
||
|
||
## 1. Authority
|
||
|
||
Concrete contracts are the four `contracts/*.v1.ts` files. PostgreSQL/current-main Drizzle is the sole writable SOT. Public health, Valkey, files, exports, providers, browser state, and outage notes cannot authorize/reconstruct writes. Mechanical Coordinator is non-LLM with no scope/gate/certification/merge authority. Certifier is final independent gate with no merge authority. No feature lane starts until this canon merges and the KBN-010/KBN-105 prerequisites are satisfied.
|
||
|
||
## 2. Health proof and exact failures
|
||
|
||
`KanbanHealthResponseV1` is a discriminated union:
|
||
|
||
| State | read | write | Capability |
|
||
| -------------------- | ----: | ----: | --------------------------------------------------- |
|
||
| `healthy` | true | true | reads; public state still cannot authorize mutation |
|
||
| `read-only-degraded` | true | false | reads only |
|
||
| `write-unavailable` | false | false | diagnostics only |
|
||
|
||
Every response has `checkedAt`, `validUntil`, `policyRevision`; contradictory booleans fail validation.
|
||
|
||
For a mutation, Gateway opens the PostgreSQL transaction, executes the live write probe on that transaction/connection, mints the internal branded `PostgresWriteHealthProofV1`, and revalidates time/policy/transaction identity immediately before mutation. Public REST/MCP/CLI DTOs never accept health/proof fields. Valkey/caller assertions cannot mint proof. Pure Coordinator takes `KanbanEvaluationContextV1`; persistence takes `InternalKanbanMutationContextV1` or probes internally.
|
||
|
||
| Case | HTTP | Frozen result | Retry |
|
||
| ---------------------------- | --------------------------: | ------------------------------------------------------------------- | -------------------- |
|
||
| degraded write | 503 | `KANBAN_WRITE_HEALTH_UNPROVEN`, `read-only-degraded`, `not_applied` | false |
|
||
| write unavailable | 503 | `KANBAN_WRITE_UNAVAILABLE`, `write-unavailable`, `not_applied` | false |
|
||
| version conflict | 409 | `AGGREGATE_VERSION_CONFLICT`, actual version, `not_applied` | false |
|
||
| timeout/unreachable | timeout/502/504 | `retryable_transport_error`, `unknown` | same idempotency key |
|
||
| stale fence/session/approval | coordinator rejection union | `not_applied` | false |
|
||
|
||
Required negatives: contradictory state, expired/policy-mismatched/wrong-transaction proof, Valkey-only health, forged healthy, and exhaustive non-cross-mapping of 503 vs 502/504/timeout vs 409.
|
||
|
||
## 3. Canonical schema invariants
|
||
|
||
Complete declaration: `contracts/kanban-schema.v1.ts`.
|
||
|
||
- Tables: tenant/identity (`workspaces`, members, teams/members, agents/sessions); planning (`projects`, `milestones`, current-milestone join, `missions`, mission-milestones, `tasks`, normalized tags, dependencies); orchestration (`task_assignments`, durable execution state, leases, checkpoints/evidence); governance (`change_proposals`, immutable artifacts/evidence, events, approvals, outbox, external links).
|
||
- Task statuses: `backlog | ready | in_progress | blocked | in_review | done | cancelled`.
|
||
- Assignment states everywhere: `awaiting_approval | policy_pre_authorized | approved | rejected | leased | released | expired | superseded`.
|
||
- Specialist roles everywhere: `planning | enhance | coder | review | security-review | pr-monitor | certifier`.
|
||
- Owner uses exactly-one user/team; assignment principal exactly-one user/team/agent; users require active membership; agent/session and all evidence are workspace-bound.
|
||
- Task→mission/milestone/parent, mission→milestone, and project→current-milestone are project-congruent composite relations.
|
||
- Dependency identity is workspace+predecessor+successor independent of type.
|
||
- Approval evidence and checkpoint evidence are workspace-scoped joins to immutable artifacts, never JSON ID arrays.
|
||
- Proposal audit links are composite relations: `(workspace_id, submitted_audit_event_id)` and `(workspace_id, accepted_command_audit_event_id)` reference `task_events(workspace_id, id)` with RESTRICT deletion.
|
||
- Assignment is persisted with task/version, exact target/session, expiry/state/policy/proposer/reason. Approval relates to assignment. Lease acquisition accepts IDs, then reloads/locks and validates every relation.
|
||
- `tasks.fencing_counter` is bigint; locked atomic increment/RETURNING creates a decimal-string lease token. Lease/checkpoint composites bind exact workspace+task+assignment/session+fence.
|
||
- `task_execution_states` durably records retry/quarantine/exhaustion.
|
||
- Tags are normalized; legacy `tasks.tags` remains through N-1. Archive is explicit actor/reason/time and does not change lifecycle.
|
||
- Canonical parents use RESTRICT. Events/checkpoints/artifacts/evidence are INSERT/SELECT-only for application roles. Normal flow archives/cancels; purge is audited break-glass retention work.
|
||
|
||
## 4. Outage proposal contract
|
||
|
||
`change_proposals` stores workspace, active-member proposer, source-note digest, target/version, typed command/payload, idempotency, lifecycle, decision actor/reason/time, proposal version, and submit/accepted event IDs. Both event IDs are workspace-aware composite foreign keys to `task_events(workspace_id, id)`; a bare UUID is never sufficient.
|
||
|
||
Submission preallocates the proposal ID. One transaction inserts `change_proposal.submitted` with the proposal workspace, `aggregate_type='change_proposal'`, `aggregate_id=<new proposal ID>`, `previous_version=NULL`, and `new_version=1`, then inserts the proposal referencing that event. Missing, foreign-workspace, wrong-type, or unrelated-proposal events abort the transaction.
|
||
|
||
Submit/list/get/accept/reject are explicit Gateway commands. Pending/rejected proposals are inert: no scheduling, dependency/gate satisfaction, or direct target mutation. Acceptance locks proposal+target, obtains fresh transaction-local proof, verifies pending/expected version, invokes the normal command handler, and atomically stores the emitted normal-command event ID. That event must share the proposal workspace, match `target_aggregate_type` and `target_aggregate_id`, use `causation_id=submitted_audit_event_id`, and carry `payload.changeProposalId=<locked proposal ID>`. Missing, foreign-workspace, unrelated-target, unrelated-proposal, or unrelated-command events abort acceptance.
|
||
|
||
## 5. Concrete current-main N-1 migration delta
|
||
|
||
**Inspected:** `origin/main:packages/db/src/schema.ts` at `e72388b2cbfe400842fe940fa6cabf984ed43711` (2026-07-13). It has global teams/no workspace keys, legacy project/mission/task statuses, nullable task project/mission, `tasks.assignee/tags/due_date`, mission JSON/config, duplicated `mission_tasks.status`, legacy agent fields, and separate fleet `backlog` claims.
|
||
|
||
Legacy columns remain declared in unified `schema.ts` for expand + full N-1/rollback window. Generation must not infer early drops.
|
||
|
||
### 5.1 Ordered phases
|
||
|
||
1. **Pre-expand:** N-1 patch stops `mission_tasks.status` as write source; inventory writers; backup/checksum.
|
||
2. **Expand:** add enums/tables and nullable-first columns; retain legacy declarations/uniques; emit no v1-only status.
|
||
3. **Backfill:** bootstrap workspace; bounded idempotent cursor/checksum batches; quarantine ambiguous rows.
|
||
4. **Validate:** no null tenant, cross-project link, ambiguous owner; status/tag/date/config retention; then constraints/NOT NULL.
|
||
5. **Compatibility:** N-1 reads legacy; same-DB transaction mirrors only unavoidable fields; never file/Valkey dual write.
|
||
6. **Switch:** stop N-1 writers; Gateway sole command boundary; enable canonical statuses.
|
||
7. **Contract release:** later release after rollback/N-1; remove compatibility/global uniques/legacy fields.
|
||
|
||
### 5.2 New audit/proposal DDL order
|
||
|
||
KBN-100 migration DDL must execute in this order:
|
||
|
||
1. create `task_events` and its unique `(workspace_id, id)` key;
|
||
2. create `change_proposals` with nullable acceptance-event ID and required submission-event ID;
|
||
3. add `change_proposals_workspace_submitted_event_fk` from `(workspace_id, submitted_audit_event_id)` to `task_events(workspace_id, id)` with `ON DELETE RESTRICT`;
|
||
4. add `change_proposals_workspace_accepted_command_event_fk` from `(workspace_id, accepted_command_audit_event_id)` to the same composite key with `ON DELETE RESTRICT`;
|
||
5. install application-role immutability privileges and same-transaction semantic validation before enabling proposal commands.
|
||
|
||
The submission transaction inserts the event first using a preallocated proposal UUID, then the proposal. Acceptance inserts the normal command event before updating the locked proposal. Neither FK is omitted or replaced by a bare UUID/index check.
|
||
|
||
### 5.3 Field map
|
||
|
||
| Current | Expand/backfill | N-1 compatibility | Switch/contract |
|
||
| ---------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- |
|
||
| global `teams`, `team_members` | add workspace nullable; bootstrap; validate active owners | retain global slug/FKs | workspace composites; global unique contracts later |
|
||
| `projects.status` | add `canonical_status`; map active/paused/completed/archived | mirror representable values; no `planning` | canonical authority; legacy contracts later |
|
||
| project `owner_id/team_id/owner_type` | add exact accountable user/team; deterministic map or quarantine | preserve old reads and compare drift | canonical exact-one; remove legacy after parity |
|
||
| current milestone | create milestones then join table (no circular DDL) | absent to N-1 | join is authority |
|
||
| nullable `missions.project_id` | derive workspace/project; null/orphan exception, never guess | keep nullable legacy read | canonical required; validate/set NOT NULL later |
|
||
| mission `description` | add objective; preserve description; reviewed nonblank mapping | N-1 description | objective authority; retain until signed review |
|
||
| `missions.status` | add canonical; planning→draft, active/paused/completed/failed same | no new-only statuses emitted | canonical authority |
|
||
| mission `milestones` JSON | normalize with source digest; preserve malformed/original | N-1 reads JSON; no reverse sync | normalized authority; JSON removed after checksum sign-off |
|
||
| mission config/metadata/phase/user | retain all; map known typed policy only | all remain declared | remove only by signed consumer inventory |
|
||
| nullable `tasks.project_id` | derive explicit/mission project; orphan quarantine | retain nullable read/write during compatibility | canonical required; NOT NULL later |
|
||
| `tasks.mission_id` | add project-congruent composite | old relation readable | composite authority |
|
||
| `tasks.status` | canonical: not-started→backlog, in-progress→in_progress, others same | no ready/in_review emission | canonical authority |
|
||
| `tasks.assignee` | deterministic active user/team/agent assignment; raw value preserved if ambiguous | mirror text only if unambiguous | canonical owner/assignment; remove after no-loss sign-off |
|
||
| `tasks.tags` JSON | normalize trim/case/dedupe with original digest | transactionally mirror normalized rows | normalized authority; JSON later removed |
|
||
| `tasks.due_date` | copy exactly to `due_at` | mirror | due_at authority; legacy later |
|
||
| task common fields | preserve metadata byte-for-byte; add criteria/rank/retry/archive/version/fence | old reads valid | new fields canonical |
|
||
| `mission_tasks.status` | keep; prohibit as write source; linked status ignored; unlinked becomes task or reject | read-only compatibility value | membership uses task mission; status dropped after no readers |
|
||
| mission-task notes/PR/user | map to metadata/artifact/event/link/attribution; preserve | read-only | remove after parity |
|
||
| `agents.status` | add workspace/lifecycle/runtime/roles; status remains presence | retain all legacy fields | lifecycle/roles authority; status may remain telemetry |
|
||
| agent project/owner/prompt/tools/skills/config | preserve; validate tenant; derive typed capabilities without loss | N-1 reads | removal only by separate inventory |
|
||
| fleet `backlog` | map to designated-project tasks; edges; claimed rows quarantine | freeze claims before switch; read-only compare | task/lease authority; retire after stabilization |
|
||
|
||
### 5.4 Required migration tests
|
||
|
||
Empty DB; exact production-shape snapshot; crash/resume; rollback before switch; N-1 startup/read/write; workspace/member negatives; status-shadow/no premature new status; `mission_tasks.status` write prohibition; tags/assignee/date/mission JSON/config/description/agent checksum; project congruence/current-milestone order; backlog freeze/no dispatch; and proof legacy declarations persist until contract release.
|
||
|
||
Proposal-specific negatives must attempt: missing submission event, foreign-workspace submission event, foreign-workspace acceptance event, same-workspace event for another proposal, event for another target aggregate, and unrelated normal-command event. Every attempt must fail atomically with no accepted proposal and no target mutation.
|
||
|
||
## 6. Ownership and Coordinator split
|
||
|
||
coder2 solely owns `packages/db/src/schema.ts`, `packages/db/drizzle/**`, journal/metadata, and migration tests. No other lane generates migrations. Expand is additive; no drop/rename/narrow; constraints validate before NOT NULL; compatibility is same-DB only; contract is later.
|
||
|
||
KBN-200/coder4 owns pure `MechanicalCoordinatorDecisionEngineV1`: complete immutable snapshots in, deterministic eligibility/proposal/retry decisions out; no ID loading, SQL, Gateway, Valkey, proof, persistence, restart I/O, or LLM.
|
||
|
||
KBN-210/coder3 owns `MechanicalCoordinatorServicePortV1`: ID loading, locks, fresh proof, assignment/approval persistence, atomic fencing, lease/checkpoint/outbox, Valkey wakes, durable retry/quarantine, and `recoverFromPostgres`. Cycle: load snapshots → pure decision → persist assignment → authoritative approval/policy → acquire by IDs/locks → increment fence → lease → ack/heartbeat/checkpoint → submit to review or durable retry/quarantine. No completion/certification/merge method exists.
|
||
|
||
## 7. Exact Gateway/DTO freeze for KBN-105
|
||
|
||
### 7.1 Common wire rules
|
||
|
||
Base is `/api/v1/workspaces/:workspaceId`. Mutations require header `Idempotency-Key` (1–128 chars). Existing-aggregate mutations also require `If-Match-Version` (positive integer); create and privileged assignment-cycle requests are the only exceptions, while proposal submission carries `expectedTargetVersion` in its body. Body workspace fields are forbidden. Tenant denial follows one 404/403 policy without foreign existence detail.
|
||
|
||
```ts
|
||
interface SuccessEnvelopeV1<T> {
|
||
contractVersion: '1.0.0';
|
||
data: T;
|
||
aggregateRevision: string;
|
||
correlationId: string;
|
||
}
|
||
interface ListEnvelopeV1<T> extends SuccessEnvelopeV1<T[]> {
|
||
page: { cursor: string | null; nextCursor: string | null; limit: number };
|
||
}
|
||
```
|
||
|
||
Errors are the exact health/transport/version unions in §2 plus validation/auth/not-found. Public DTOs never expose/accept internal write proof.
|
||
|
||
### 7.2 Exact route registry
|
||
|
||
| Method/path | Request body/query | Success data |
|
||
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
|
||
| `GET /kanban-health` | none | `KanbanHealthResponseV1` |
|
||
| `GET /projects` | `status,ownerUserId,ownerTeamId,cursor,limit` | project list |
|
||
| `POST /projects` | `name,key,description,status,priority,ownerUserId XOR ownerTeamId,metadata` | project |
|
||
| `GET /projects/:projectId` | none | project |
|
||
| `PATCH /projects/:projectId` | editable create fields + expected header | project |
|
||
| `POST /projects/:projectId/archive` | `reason` | project |
|
||
| `GET /tasks` | `projectId,missionId,milestoneId,status,priority,ownerUserId,ownerTeamId,specialistRole,tag,dueState,archived,cursor,limit` | task summary list |
|
||
| `POST /tasks` | `projectId,missionId?,milestoneId?,parentTaskId?,title,description?,acceptanceCriteria[],status,priority,rank,ownerUserId XOR ownerTeamId,specialistRole?,dueAt?,notBeforeAt?,estimateMinutes?,retryPolicy?,tagIds[],metadata` | task detail |
|
||
| `GET /tasks/:taskId` | none | task detail including readiness/dependencies/assignment/lease/events |
|
||
| `PATCH /tasks/:taskId` | editable non-transition fields | task detail |
|
||
| `POST /tasks/:taskId/transition` | `toStatus,reason?` | task detail |
|
||
| `POST /tasks/:taskId/move` | `toStatus?,beforeTaskId?,afterTaskId?` | task detail with persisted rank |
|
||
| `POST /tasks/:taskId/archive` | `reason` | task detail |
|
||
| `PUT /tasks/:taskId/tags` | `tagIds[]` | task detail |
|
||
| `POST /tasks/:taskId/dependencies` | `predecessorTaskId,type` | dependency |
|
||
| `DELETE /tasks/:taskId/dependencies/:predecessorTaskId` | no body | deleted dependency ID |
|
||
| `GET /tasks/:taskId/events` | `cursor,limit` | event list |
|
||
| `GET /tags` | `query,cursor,limit` | tag list |
|
||
| `POST /tags` | `name,color?` | tag |
|
||
| `GET /change-proposals` | `state,targetType,targetId,cursor,limit` | proposal list |
|
||
| `POST /change-proposals` | `sourceNoteDigest,targetType,targetId,expectedTargetVersion,commandType,commandPayload` | inert proposal |
|
||
| `GET /change-proposals/:proposalId` | none | proposal |
|
||
| `POST /change-proposals/:proposalId/accept` | `reason` | proposal + normal command result |
|
||
| `POST /change-proposals/:proposalId/reject` | `reason` | proposal |
|
||
| `GET /coordinator/eligibility` | `projectId?,missionId?,cursor,limit` | `EligibilityDecisionV1[]` |
|
||
| `POST /coordinator/assignment-cycles` | `limit` | assignment proposals; privileged internal |
|
||
| `POST /coordinator/assignments/:assignmentId/approve` | `decision,reason,policyRevision,artifactIds[]` | approval decision |
|
||
| `POST /coordinator/leases/acquire` | `taskId,assignmentId,approvalDecisionId,targetSessionId,leaseTtlSeconds` | lease with decimal-string fence |
|
||
| `POST /coordinator/leases/:leaseId/ack` | `taskId,sessionId,fencingToken` | lease |
|
||
| `POST /coordinator/leases/:leaseId/heartbeat` | `taskId,sessionId,fencingToken,extendSeconds` | lease |
|
||
| `POST /coordinator/leases/:leaseId/checkpoints` | `taskId,sessionId,fencingToken,sequence,resumableSummary,artifactIds[],contextUsagePercent` | checkpoint |
|
||
| `POST /coordinator/leases/:leaseId/submit-review` | `taskId,sessionId,fencingToken,artifactIds[],summary` | task in `in_review` |
|
||
|
||
All Coordinator mutations except human approval are service-identity-only. Generic task PATCH cannot perform claim/heartbeat/checkpoint/review/certification/completion shortcuts. Completion after certification uses a separately gated lifecycle command owned by the Portfolio/Sub-Orchestrator flow, not the Coordinator.
|
||
|
||
### 7.3 DTO invariants
|
||
|
||
Task summary/detail use exact schema vocabularies, owner union, `version: number`, `fencingCounter: string`, explicit `archivedAt/by/reason`, normalized tags, computed readiness, and separate assignment/lease. Assignment DTO includes one persisted ID, task/version, exact principal/agent/session, role, state, expiry, policy, proposer/reason. Lease/checkpoint DTOs serialize every fence as decimal string. Proposal DTO exposes no hidden write authority.
|
||
|
||
### 7.4 MCP ownership and mapping
|
||
|
||
coder3 exclusively owns:
|
||
|
||
- `apps/gateway/src/mcp/mcp.dto.ts`
|
||
- `mcp.controller.ts`
|
||
- `mcp.service.ts`
|
||
- `mcp.module.ts`
|
||
- `mcp.tokens.ts`
|
||
- `mcp.service.spec.ts`
|
||
|
||
MCP tools are thin maps: `mosaic_projects_{list,get,create,update,archive}`, `mosaic_tasks_{list,get,create,update,transition,move,archive,set_tags,add_dependency,remove_dependency}`, and `mosaic_change_proposals_{list,get,submit,accept,reject}` to the exact routes above. coder4 owns CLI/projection clients only and must not edit Gateway MCP files.
|
||
|
||
KBN-105 publishes route+DTO fixture digest before KBN-110/120/130. Every web/CLI/MCP call must match this registry and the generated client.
|
||
|
||
## 8. Recovery contract and bounded delivery slice
|
||
|
||
Runtime must invoke normative `validateRecoveryPostureV1`; JSON Schema alone is insufficient. It rejects unknown fields, PITR/WAL mismatch, RPO better than mechanism, unsafe storage, and weakened High-assurance. High-assurance is RPO 15m/RTO 4h, WAL ≤5m, PITR ≥35d, base ≤24h, restore test ≤30d, break-glass ≤90d, encrypted separate-failure-domain storage.
|
||
|
||
KBN-115/coder2 owns `packages/config/src/recovery-posture.ts`, tests, and recovery runbook. It wires parser/refinement, override audit, mechanism assertions, restore test, and break-glass evidence. Any deployment manifest is separately enumerated and Mos-serialized. Recovery config has no SOT/gate/Coordinator authority fields.
|
||
|
||
## 9. Integration, security, and hold
|
||
|
||
Required release evidence includes empty/prod/partial/rollback/N-1 migration tests; cross-workspace and same-workspace wrong-project negatives; active-membership owners/principals; proposal inertness/normal acceptance; exact failure mapping; concurrent monotonic bigint fences; relational lease/checkpoint/evidence mismatch; immutability privileges/RESTRICT; recovery validation/mechanism evidence; endpoint registry alignment; accessible web journeys; author≠reviewer; mandatory SecReview; final Certifier pass/no merge authority.
|
||
|
||
The build hold remains active until independent re-review reports GO for KCR-001–016. Mos alone releases waves and serializes integration roots.
|