Files
stack/docs/native-kanban-sot/SHARED-CONTRACT.md
2026-07-14 15:11:28 -05:00

39 KiB
Raw Blame History

Native Kanban/SOT — Remediated Shared Contract v1

Status: CONTROL-PLANE SI-001 AMENDMENT AUTHORIZED; prior KCR-001016 independent-review GO retained; rc.4 requires independent schema/SecReview before KBN-100 Version: 1.0.0-rc.4 Date: 2026-07-14 Change authority: Mosaic control plane/Jason only SI-001 amendment authority: web1:mosaic-100 control-plane decision under issue #753

Amendment record

1.0.0-rc.4 — KBN010-SI-001

  • Choice: add the explicitly named, non-partial unique candidate key missions_workspace_id_uidx on missions(workspace_id, id) and retain missions_workspace_project_id_uidx on (workspace_id, project_id, id).
  • Rationale: mission id remains globally unique, while the composite candidate key makes the frozen tenant-safe generic mission relations valid. artifacts and approval_decisions are polymorphic exactly-one-target records and do not consistently carry project_id; widening both children would unnecessarily broaden v1 and its target semantics.
  • Exact effect: artifacts_workspace_mission_fk and approval_decisions_workspace_mission_fk continue to reference the exact ordered columns missions(workspace_id, id) with RESTRICT deletion, now backed by a matching candidate key.
  • Non-effect: no SOT, tenancy, project-congruence, proposal-audit, approval, fencing, immutability, no-cascade, API, or wire-version invariant changes. The SuccessEnvelopeV1.contractVersion remains 1.0.0.
  • Historical evidence boundary: KBN-010-THREAT-AUTH-CONSTRAINT-GATE.md intentionally remains the immutable rc.3 blocker verdict that detected SI-001; this rc.4 record and the #753 scratchpad append are the authorized disposition. Rewriting the gate verdict is outside this amendment's exclusive scope.
  • Gate: this amendment resolves the DDL defect identified by KBN010-SI-001 but does not itself lift KBN-100; independent schema/SecReview remains required.

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.
  • Mission id remains globally unique. The additional non-partial missions_workspace_id_uidx candidate key on (workspace_id, id) exists only to support the frozen workspace-safe polymorphic artifact and approval-decision mission relations; the project-congruent (workspace_id, project_id, id) key remains authoritative wherever project_id is present.
  • 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 Mission candidate-key and dependent-FK DDL order

KBN-100 migration DDL must execute the SI-001 portion in this order:

  1. expand/backfill missions.workspace_id and missions.project_id while preserving the global missions.id primary key and the project-congruent missions_workspace_project_id_uidx key;
  2. prove duplicate-key feasibility on the production-shape dataset: (workspace_id, id) has no duplicate groups and global id uniqueness remains intact;
  3. create the non-partial unique index missions_workspace_id_uidx on exact ordered columns (workspace_id, id);
  4. only after step 3, create/alter artifacts and add artifacts_workspace_mission_fk from (workspace_id, mission_id) to exact missions(workspace_id, id) with ON DELETE RESTRICT;
  5. only after step 3, create/alter approval_decisions and add approval_decisions_workspace_mission_fk from (workspace_id, mission_id) to exact missions(workspace_id, id) with ON DELETE RESTRICT;
  6. validate both constraints and prove a mission ID paired with a foreign workspace is rejected for each child.

The candidate key is intentionally redundant with globally unique missions.id, but PostgreSQL requires a matching unique candidate key for the exact two-column FK target. It is additive and N-1-safe. Pre-switch rollback drops the two dependent FKs/tables before dropping this candidate key, preserves the global primary key and project-congruent key, and follows the existing freeze/reconciliation rule after the first canonical mutation.

5.3 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.4 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 relational candidate keys retain global id PK and project-congruent key; add non-partial (workspace_id,id) key before artifact/approval FKs additive key is ignored safely by N-1 readers/writers retain both composite keys; generic mission children use exact workspace+ID target
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.5 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.

SI-001 adds frozen future executable evidence: empty and production-shape migrations create missions_workspace_id_uidx before either dependent FK; duplicate-key feasibility preflight returns no (workspace_id,id) duplicate groups without weakening global id uniqueness; N-1 startup/read/write behavior is unchanged; pre-switch rollback removes dependents before the candidate key; both exact FK column lists reconcile to the candidate key; and foreign-workspace mission references fail for both artifacts and approval decisions. TDD is not applicable to this design-only amendment; KBN-100 must implement these negative migration tests before runtime schema 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.

9.1 SI-001 amendment gate and #757 boundary

  • KBN-100 must provide the §5.2 candidate-key ordering, duplicate-feasibility, exact-FK reconciliation, empty/prod/N-1/rollback, and two-child foreign-workspace evidence before SI-001 can be certified closed.
  • All prior KCR-001016 decisions and fixed SOT/tenant/authority, proposal-audit, approval, task-fencing, immutability, and no-cascade invariants remain unchanged.
  • Read-only PR #757 cross-check: its logical-agent connector lease/CAS fencing uses separate runtime tables/contracts and lease_epoch; rc.4 changes only the frozen missions candidate key. There is no shared table, index, FK, identity, fence, or authority semantic to consume or reconcile, and #757 remains owned by its existing lane.

The build hold remains active until independent re-review reports GO for KCR-001016 and the rc.4 SI-001 amendment. Mos alone releases waves and serializes integration roots.