Files
stack/docs/native-kanban-sot/SHARED-CONTRACT.md
jason.woltje 49e8a54105
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
docs(#751): Publish native Kanban/SOT canon (#752)
2026-07-14 17:08:09 +00:00

32 KiB
Raw Blame History

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 (1128 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.

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-001016. Mos alone releases waves and serializes integration roots.