From 728d205b3ea2fe976c90943e8ed842706fa50cbd Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 14 Jul 2026 12:00:52 -0500 Subject: [PATCH] docs(#751): publish native Kanban SOT canon --- docs/MISSION-MANIFEST.md | 15 +- docs/SITEMAP.md | 11 + docs/TASKS.md | 9 +- .../DOCUMENTATION-CHECKLIST.md | 36 + docs/native-kanban-sot/INDEX.md | 64 + docs/native-kanban-sot/MISSION-MANIFEST.md | 195 +++ docs/native-kanban-sot/SHARED-CONTRACT.md | 219 +++ docs/native-kanban-sot/TASKS.md | 261 ++++ .../contracts/health-state.v1.ts | 206 +++ .../contracts/kanban-schema.v1.ts | 1294 +++++++++++++++++ .../contracts/mechanical-coordinator.v1.ts | 419 ++++++ .../contracts/recovery-posture.v1.ts | 369 +++++ docs/native-kanban-sot/tsconfig.json | 16 + .../canon-final-rereview-go.md | 59 + .../canon-initial-review-no-go.md | 357 +++++ .../native-kanban-sot/ultron-final-go.md | 38 + docs/requirements/native-kanban-sot.md | 368 +++++ docs/scratchpads/751-native-kanban-canon.md | 152 ++ 18 files changed, 4077 insertions(+), 11 deletions(-) create mode 100644 docs/native-kanban-sot/DOCUMENTATION-CHECKLIST.md create mode 100644 docs/native-kanban-sot/INDEX.md create mode 100644 docs/native-kanban-sot/MISSION-MANIFEST.md create mode 100644 docs/native-kanban-sot/SHARED-CONTRACT.md create mode 100644 docs/native-kanban-sot/TASKS.md create mode 100644 docs/native-kanban-sot/contracts/health-state.v1.ts create mode 100644 docs/native-kanban-sot/contracts/kanban-schema.v1.ts create mode 100644 docs/native-kanban-sot/contracts/mechanical-coordinator.v1.ts create mode 100644 docs/native-kanban-sot/contracts/recovery-posture.v1.ts create mode 100644 docs/native-kanban-sot/tsconfig.json create mode 100644 docs/reports/native-kanban-sot/canon-final-rereview-go.md create mode 100644 docs/reports/native-kanban-sot/canon-initial-review-no-go.md create mode 100644 docs/reports/native-kanban-sot/ultron-final-go.md create mode 100644 docs/requirements/native-kanban-sot.md create mode 100644 docs/scratchpads/751-native-kanban-canon.md diff --git a/docs/MISSION-MANIFEST.md b/docs/MISSION-MANIFEST.md index ce80c48..8baa41e 100644 --- a/docs/MISSION-MANIFEST.md +++ b/docs/MISSION-MANIFEST.md @@ -10,9 +10,9 @@ **Statement:** Ship a self-hosted, multi-user AI agent platform that consolidates the user's disparate jarvis-brain usage across home and USC workstations into a single coherent system reachable via three first-class surfaces — webUI, TUI, and CLI — with federation as the data-layer mechanism that makes cross-host agent sessions work in real time without copying user data across the boundary. **Phase:** Execution (workstream W1 in planning-complete state) **Current Workstream:** W1 — Federation v1 -**Progress:** 0 / 1 declared workstreams complete (more workstreams will be declared as scope is refined) +**Progress:** 0 / 3 declared workstreams complete (more workstreams will be declared as scope is refined) **Status:** active (continuous since 2026-03-13) -**Last Updated:** 2026-04-19 (manifest authored at the rollup level; install-ux-v2 archived; W1 federation planning landed via PR #468) +**Last Updated:** 2026-07-14 (W3 Native Kanban/SOT canon independently approved under issue #751) **Source PRD:** [docs/PRD.md](./PRD.md) — Mosaic Stack v0.1.0 **Scratchpad:** [docs/scratchpads/mvp-20260312.md](./scratchpads/mvp-20260312.md) (active since 2026-03-13; 14 prior sessions of phase-based execution) @@ -67,11 +67,12 @@ The MVP is complete when ALL declared workstreams are complete AND every cross-c ## Workstreams -| # | ID | Name | Status | Manifest | Notes | -| --- | ---- | ------------------------------------------- | ----------------- | ----------------------------------------------------------------------- | --------------------------------------------------- | -| W1 | FED | Federation v1 | planning-complete | [docs/federation/MISSION-MANIFEST.md](./federation/MISSION-MANIFEST.md) | 7 milestones, ~175K tokens, issues #460–#466 filed | -| W2 | TESS | Tess interaction agent | planning-complete | [docs/tess/MISSION-MANIFEST.md](./tess/MISSION-MANIFEST.md) | 5 milestones; issue #706; M1 issue #707 ready | -| W3+ | TBD | (additional workstreams declared as scoped) | — | — | Scope creep is expected and explicitly accommodated | +| # | ID | Name | Status | Manifest | Notes | +| --- | ---- | ------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| W1 | FED | Federation v1 | planning-complete | [docs/federation/MISSION-MANIFEST.md](./federation/MISSION-MANIFEST.md) | 7 milestones, ~175K tokens, issues #460–#466 filed | +| W2 | TESS | Tess interaction agent | planning-complete | [docs/tess/MISSION-MANIFEST.md](./tess/MISSION-MANIFEST.md) | 5 milestones; issue #706; M1 issue #707 ready | +| W3 | KBN | Native Kanban and canonical task SOT | planning-complete | [docs/native-kanban-sot/MISSION-MANIFEST.md](./native-kanban-sot/MISSION-MANIFEST.md) | P0–P3; issue #751; implementation held until canon merge | +| W4+ | TBD | (additional workstreams declared as scoped) | — | — | Scope creep is expected and explicitly accommodated | ### Likely Additional Workstreams (Not Yet Declared) diff --git a/docs/SITEMAP.md b/docs/SITEMAP.md index cab3d2c..d228cf1 100644 --- a/docs/SITEMAP.md +++ b/docs/SITEMAP.md @@ -1,5 +1,16 @@ # Documentation Sitemap +## Native Kanban and canonical task SOT + +- [Canonical requirements](requirements/native-kanban-sot.md) — ratified P0–P3 requirements and acceptance criteria. +- [Workstream index](native-kanban-sot/INDEX.md) — artifact map, lane partition, and delivery order. +- [Mission manifest](native-kanban-sot/MISSION-MANIFEST.md) — scope, authority, invariants, and gate model. +- [Task decomposition](native-kanban-sot/TASKS.md) — dependency-ordered implementation slices and ownership boundaries. +- [Frozen shared contract](native-kanban-sot/SHARED-CONTRACT.md) — schema, API, Coordinator, health, recovery, and migration contracts. +- [Initial independent review](reports/native-kanban-sot/canon-initial-review-no-go.md) — KCR-001–016 findings that blocked the first draft. +- [Final independent re-review](reports/native-kanban-sot/canon-final-rereview-go.md) — closure evidence and GO verdict. +- [Ultron final gate](reports/native-kanban-sot/ultron-final-go.md) — final requirements, authority, schema, migration, recovery, and evidence review. + ## Tess interaction agent ### Operator guides diff --git a/docs/TASKS.md b/docs/TASKS.md index abe7fe0..4a4e7b8 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -14,10 +14,11 @@ ## Workstream Rollup -| id | status | workstream | progress | tasks file | notes | -| --- | ----------------- | ---------------------- | ---------------- | ------------------------------------------------- | --------------------------------------------------------------- | -| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning | -| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready | +| id | status | workstream | progress | tasks file | notes | +| --- | ----------------- | ---------------------- | ---------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| W1 | planning-complete | Federation v1 (FED) | 0 / 7 milestones | [docs/federation/TASKS.md](./federation/TASKS.md) | M1 task breakdown populated; M2–M7 deferred to mission planning | +| W2 | planning-complete | Tess interaction agent | 0 / 5 milestones | [docs/tess/TASKS.md](./tess/TASKS.md) | Issue #706; independent planning gate PASS; M1 issue #707 ready | +| W3 | planning-complete | Native Kanban/SOT | 0 / 4 phases | [docs/native-kanban-sot/TASKS.md](./native-kanban-sot/TASKS.md) | Issue #751; canon independently approved; implementation held until canon merges | ## Cross-Cutting Tracking diff --git a/docs/native-kanban-sot/DOCUMENTATION-CHECKLIST.md b/docs/native-kanban-sot/DOCUMENTATION-CHECKLIST.md new file mode 100644 index 0000000..017237f --- /dev/null +++ b/docs/native-kanban-sot/DOCUMENTATION-CHECKLIST.md @@ -0,0 +1,36 @@ +# Documentation Completion Checklist — Native Kanban/SOT Canon + +**Tracking:** Mosaic Stack issue #751 +**Scope:** Requirements and contract publication only; runtime implementation follows in separate slices. + +## Required artifacts + +- [x] Project `docs/PRD.md` exists; the workstream requirements refine its task/project-management scope. +- [x] Canonical workstream requirements published at `docs/requirements/native-kanban-sot.md`. +- [x] Mission manifest, task decomposition, frozen shared contract, and typed contract declarations included. +- [x] `docs/SITEMAP.md` updated. +- [x] Independent initial review and final GO report stored under `docs/reports/native-kanban-sot/`. +- [x] Task scratchpad stored under `docs/scratchpads/`. +- [ ] User/Admin/Developer guides — N/A for canon-only publication; required in implementation slices that change behavior or operations. +- [ ] OpenAPI and endpoint index — N/A until KBN-105 freezes implementation-ready endpoint contracts. + +## Structural and root hygiene + +- [x] Canonical requirements are under `docs/requirements/`. +- [x] Workstream artifacts are under `docs/native-kanban-sot/`. +- [x] Review reports are under `docs/reports/native-kanban-sot/`. +- [x] No new unscoped document was added to the `docs/` root. +- [x] Root mission/task rollups link to the workstream. + +## Review gate + +- [x] Author and independent reviewer are different agents. +- [x] KCR-001–016 closure was independently verified. +- [x] Ultron final gate returned GO with zero BLOCKER/HIGH findings. +- [x] Formatter, lint, typecheck, strict contract TypeScript, link, scope, and invariant publication validation passed in the current Stack toolchain. +- [ ] PR review, CI, squash merge, and issue closure remain required before publication completion. + +## Publishing + +- [x] Canonical source remains in-repository. +- [x] No external publishing platform is required for this internal architecture contract. diff --git a/docs/native-kanban-sot/INDEX.md b/docs/native-kanban-sot/INDEX.md new file mode 100644 index 0000000..2f12599 --- /dev/null +++ b/docs/native-kanban-sot/INDEX.md @@ -0,0 +1,64 @@ +# Native Kanban/SOT Canon + +**Status:** KCR-001–016 independently cleared; canonical publication is in progress under issue [#751](https://git.mosaicstack.dev/mosaicstack/stack/issues/751) +**Date:** 2026-07-14 +**Implementation hold:** no feature implementation starts until this canon is squash-merged to `main` with terminal-green CI; after merge, every slice remains held until its KBN prerequisite graph is satisfied. + +## Artifacts + +| Artifact | Purpose | +| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Canonical requirements](../requirements/native-kanban-sot.md) | Canonical P0–P3 requirements, all seven ratified decisions, fixed invariants, thin MVP, recovery tiers, non-goals, and per-requirement acceptance criteria | +| [`MISSION-MANIFEST.md`](./MISSION-MANIFEST.md) | Mission/authority boundaries, exact role chain, gate model, mandatory SecReview triggers, Certifier final/no-merge rule, and collision-free slice ownership | +| [`TASKS.md`](./TASKS.md) | Dependency-ordered, bounded P0–P3 slices with IN/OUT scope, dependencies, shared contracts, file ownership, evidence, and USC coder2/3/4/5 parallelization | +| [`SHARED-CONTRACT.md`](./SHARED-CONTRACT.md) | Remediated v1 integration contract: proof authority, exact failures/routes/DTOs/MCP ownership, concrete current-main field migration map, relational invariants, Coordinator split, recovery delivery | +| [`contracts/kanban-schema.v1.ts`](./contracts/kanban-schema.v1.ts) | Drizzle target declarations including exact owner/principal membership, project congruence, tags/archive, proposals, persisted assignments, monotonic fences, durable retry, immutable evidence/audit | +| [`contracts/mechanical-coordinator.v1.ts`](./contracts/mechanical-coordinator.v1.ts) | Pure snapshot decision engine separated from persistence/service adapter; ID-bound approvals, bigint-safe fences, durable retry/quarantine, artifact-backed checkpoints, exact failures | +| [`contracts/health-state.v1.ts`](./contracts/health-state.v1.ts) | Discriminated public health, separate branded transaction-local write proof, and non-overlapping denial/transport/version-conflict mappings | +| [`contracts/recovery-posture.v1.ts`](./contracts/recovery-posture.v1.ts) | Provider-neutral shape schema plus normative runtime refinement, cross-field constraints, and Lite/Standard/High-assurance defaults | +| [`tsconfig.json`](./tsconfig.json) | Strict no-emit project scope for linting and compiling the four frozen TypeScript contracts against the current Stack Drizzle declarations | +| [`DOCUMENTATION-CHECKLIST.md`](./DOCUMENTATION-CHECKLIST.md) | Publication documentation gate and implementation-slice deferrals | +| [Initial independent review](../reports/native-kanban-sot/canon-initial-review-no-go.md) | KCR-001–016 findings that blocked the first draft | +| [Final independent re-review](../reports/native-kanban-sot/canon-final-rereview-go.md) | Closure matrix, reproducible validation evidence, and GO verdict | +| [Ultron final gate](../reports/native-kanban-sot/ultron-final-go.md) | Final requirements, authority, schema, migration, recovery, decomposition, and evidence review GO | + +## Recommended USC lane partition + +| Lane | Natural seam | Exclusive ownership | +| ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **coder2** | Schema + migrations + recovery slice | Unified Drizzle schema, migration SQL/meta/journal/tests, then recovery parser/mechanism/runbook files | +| **coder3** | Domain + Gateway + MCP server | Workspace-safe repositories, DTOs/controllers/services, exact `apps/gateway/src/mcp/**` files, health proof, proposals, Coordinator persistence adapter | +| **coder4** | Pure Coordinator + tooling | `packages/coord` mechanical engine, CLI/MCP consumers, generated projection, one-way importer and cutover tooling; lane-serialized internally | +| **coder5** | Web | Tasks/Projects Kanban/List/detail and later Coordinator/migration-review UI | +| **Mos** | Serialized integration | Canon publication, frozen-contract changes, shared-root/exports, integration gates, merge authority | + +The safe order is KBN-010 → KBN-100 → KBN-105, then coder3 Gateway/MCP server, coder4 CLI/projection, coder5 web, and coder2 recovery can proceed on disjoint files. coder4 then runs pure Coordinator → importer → cutover tooling serially. No two active slices edit the same files. + +## Recovery defaults + +| Tier | RPO / RTO | WAL / PITR | Base backup | Restore / break-glass | Off-cluster | +| -------------- | ------------ | ------------------- | ----------- | ----------------------- | ----------------------------------------------- | +| Lite | 24h / 24h | disabled / disabled | daily | quarterly / annual | encrypted separate target | +| Standard | 1h / 8h | q15m / 14d | daily | quarterly / semiannual | encrypted separate object storage | +| High-assurance | **15m / 4h** | **q5m / 35d** | **daily** | **monthly / quarterly** | **encrypted base+WAL, separate failure domain** | + +These knobs affect recovery posture only. PostgreSQL remains the sole writable SOT in every tier. Fail-closed writes, generated-file non-authority, attributable post-recovery proposals, non-LLM Coordinator limits, and Certifier final-gate/no-merge authority are fixed for every tier. + +## Non-blocking implementation sub-decisions for Mos + +The source plan and ratified seven decisions resolve all build-blocking product choices. The following implementation-local selections remain for the owning slices/Mos and must not weaken v1: + +1. Exact PostgreSQL write-health probe SQL and bounded proof lifetime; authority and failures are frozen. +2. Dependency-cycle serialization mechanism (recursive CTE plus transaction/advisory lock or equivalent); required behavior is frozen. +3. Whether RLS lands in the first migration or immediately after the tested session-context pattern; workspace constraints/repository authorization are required from migration one. +4. Concrete off-cluster backup provider/bucket and selected production recovery tier; High-assurance minima are frozen if selected. +5. Cutover reconciliation thresholds and stabilization duration, to be owner-approved before P3 execution. + +None authorizes a second writer, dual sync, LLM scheduling, Coordinator gate waiver/merge, or Certifier merge authority. + +## Publication validation evidence + +- Concrete TypeScript contracts are formatted with repository Prettier. +- All four contracts pass strict TypeScript no-emit checking against the current Stack Drizzle toolchain. +- Contract remediation and KCR-001–016 traceability are recorded in the issue scratchpad and linked review reports. +- Independent re-review returned GO with KCR-001–016 closed; implementation remains held until canon merge and the dependency-ordered KBN prerequisites complete. diff --git a/docs/native-kanban-sot/MISSION-MANIFEST.md b/docs/native-kanban-sot/MISSION-MANIFEST.md new file mode 100644 index 0000000..d103643 --- /dev/null +++ b/docs/native-kanban-sot/MISSION-MANIFEST.md @@ -0,0 +1,195 @@ +# Mission Manifest — Mosaic Native Kanban and Canonical Task SOT P0–P3 + +**Mission status:** CANON INDEPENDENTLY APPROVED; publication in progress under issue [#751](https://git.mosaicstack.dev/mosaicstack/stack/issues/751) +**Date:** 2026-07-14 +**Human decision owner:** Jason +**Orchestrator/publication owner:** web1 control plane (`mos-claude`; `mosaic-100` acting during Claude quota outage) +**Execution topology:** USC web1, partitioned across collision-free GPT coder2/3/4/5 lanes +**Canonical requirements:** [`../requirements/native-kanban-sot.md`](../requirements/native-kanban-sot.md) +**Frozen integration contract:** `SHARED-CONTRACT.md` and `contracts/*.v1.ts` + +## 1. Mission statement + +Extend current `mosaicstack/stack` main into the sole native control plane for workspace-scoped project, mission, milestone, task, dependency, assignment, lease, approval, evidence, and audit state. First deliver a thin writable Kanban/List vertical slice; then add deterministic mechanical coordination and execute a one-way migration/cutover from jarvis-brain/Vikunja project/task stores. + +Success means every user, agent, orchestrator, specialist, and UI sees and mutates the same PostgreSQL aggregate revisions through typed Gateway commands, with no writable fallback and no hidden second authority. + +## 2. Scope boundaries + +### In scope + +- Current Drizzle/PostgreSQL schema extension and migrations. +- Workspace tenancy and authorization from the first migration. +- Projects, missions, milestones, tasks, normalized tags, dependencies, assignments, durable execution/quarantine state, links, immutable artifacts/evidence joins, outage change proposals, events, approvals, leases, checkpoints, and transactional outbox. +- NestJS Gateway queries and explicit lifecycle commands. +- MCP/CLI agent surfaces and generated read-only projections. +- Thin writable Next.js Tasks Kanban/List, task detail, minimal Projects CRUD, filters, dependency readiness, ownership/lease separation, and audit timeline. +- Non-LLM Mechanical Coordinator eligibility, proposal, approval-policy, lease/fence, heartbeat, retry, expiry, quarantine, and restart recovery. +- Planning, Enhance, Coder, Review, SecReview, PR-Monitor, and Certifier role/gate representation. +- One-way shadow importer, reconciliation, write freeze, final delta, cutover, rollback package, and legacy read-only stabilization. +- Recovery-posture configuration and health-state/fail-closed contract. + +### Out of scope + +- Greenfield services, Prisma runtime revival, or jarvis-brain flat files as runtime storage. +- Writable Markdown/JSON/Valkey/browser/provider fallback. +- Gitea issue/PR replacement or generic bidirectional provider sync. +- Calendar, email, GLPI cache, CRM, billing, time tracking, personal-brain migration. +- LLM scheduling or scope interpretation by the Coordinator. +- Autonomous gate waiver, certification, merge, release, deployment, or issue closure by Coordinator. +- Merge authority for Certifier. +- P4 full portfolio/mission designer and P5 fleet-scale policy unless separately released. + +## 3. Fixed invariants + +Every deployment MUST preserve all of the following: + +1. PostgreSQL is the sole writable SOT. +2. Drizzle on current stack main is the only persistence foundation. +3. Mutations fail closed when DB write-health cannot be proven `healthy`. +4. No file, Valkey, browser, queue, provider, or human note becomes a fallback writer. +5. `TASKS.md`, `mission.json`, and every file export are generated, read-only, non-authoritative, and never import sources. +6. Human outage notes become attributable post-recovery proposals only. +7. Workspace is the hard tenant; Team is intra-workspace authorization. +8. Valkey is expendable; PostgreSQL owns state, leases, fencing, audit, and outbox. +9. Mechanical Coordinator is deterministic/non-LLM and cannot invent scope, waive gates, certify, or merge. +10. Certifier is the final independent quality gate and has no merge authority. +11. Mutations use idempotency and optimistic aggregate versions; worker commands also require a current fencing token. +12. Recovery tier changes only backup/recovery posture, never authority or gate semantics. + +## 4. Configurable recovery posture + +Deployments select Lite, Standard, or High-assurance defaults from [`../requirements/native-kanban-sot.md`](../requirements/native-kanban-sot.md) and `contracts/recovery-posture.v1.ts`. Configurable fields are limited to: + +- backup/base-backup cadence; +- RPO and RTO targets; +- PITR retention; +- WAL archive cadence; +- restore-test frequency; +- break-glass drill frequency; +- encrypted off-cluster storage. + +High-assurance defaults are fixed reference values: RPO 15 minutes, RTO 4 hours, encrypted off-cluster WAL every 5 minutes with 35-day PITR, daily base backup, monthly restore test, and quarterly break-glass drill. + +## 5. Canonical role map + +```text +User + ↓ objectives, constraints, ratified decisions +Interaction Layer + ↓ workspace/project context; no scheduling authority +Portfolio Orchestrator + ↓ approved mission, cross-project priority/capacity +Project Sub-Orchestrator + ↓ decomposition, DAG, acceptance, release, routing policy, overrides +Gateway + ↓ authenticated/authorized typed commands +Project/Task Domain Services + ↓ transactional state + semantic event + outbox +Mechanical Coordinator + ↓ deterministic eligibility/proposal/lease/fence/retry/quarantine +Specialists + Planning → Enhance → Coder → Review → conditional SecReview → remediation + ↓ complete evidence bundle +Certifier + ↓ final pass/reject/escalate; NO merge authority +Project Sub-Orchestrator / control plane + ↓ merge authority after all gates +Post-merge validation +``` + +### Authority table + +| Role/layer | Owns | Explicitly cannot do | +| ------------------------ | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| User | Objectives, constraints, Jason-owned decisions | Direct DB/file authority bypass | +| Interaction | Conversation and context resolution | Schedule, approve, lease, certify | +| Portfolio Orchestrator | Mission approval, cross-project priority/capacity/global holds | Implement or self-certify specialist work | +| Project Sub-Orchestrator | Task decomposition/DAG/acceptance, release to ready, routing policy, overrides, remediation, merge go-ahead | Bypass required independent gates | +| Gateway | Identity, tenancy, DTO validation, commands, state-machine enforcement | Accept file edits or client SQL as mutations | +| Domain services | Transactional business invariants, semantic events/outbox | Depend on Valkey/files for committed truth | +| Mechanical Coordinator | Eligibility, dependencies, proposal, approved routing, lease/fence, heartbeat, retry/quarantine | Invent/alter scope, waive gates, certify, merge | +| Specialists | Bounded planning/implementation/review artifacts under a task lease | Modify another lane's owned files or self-approve | +| Certifier | Final independent evidence/traceability/gate decision | Merge, close provider issue, release, waive policy | + +## 6. Gate model + +### Mandatory gates + +1. Requirements/contract freeze before parallel implementation. +2. P0 schema/authority threat model and tenant isolation review. +3. Author and reviewer MUST be different principals/sessions. +4. Functional review validates requirements, endpoint registry, concurrency, and negative paths. +5. **Mandatory SecReview (`secrev`)** for any auth, authorization, tenant, service-token, secret, database schema/migration, data-integrity, import/cutover, audit, lease/fencing, recovery, or destructive-retirement surface. +6. Review findings enter bounded remediation owned by the implementation lane. +7. Raising reviewer re-verifies remediation. +8. Certifier performs the final independent evidence and traceability gate. +9. Merge authority remains with `mos-claude`/Project Sub-Orchestrator control plane after gates pass. +10. Post-merge CI and situational validation must be terminal green before closure. + +### Gate outcomes + +- **PASS:** evidence complete; next authority may proceed. +- **REJECT:** findings are explicit and route to remediation. +- **ESCALATE:** policy/owner decision required; no implicit waiver. + +No role can transform a missing gate into a warning by changing status, editing a projection, or writing Valkey. + +## 7. Slice ownership rules + +1. USC web1 is the sole execution environment; coder2/3/4/5 are independent bounded lanes under Mos. +2. Every slice has one named file-tree owner and an explicit IN/OUT boundary in `TASKS.md`. +3. Two active slices MUST NOT edit the same source file, migration file, generated snapshot, lockfile, or API contract. +4. coder2 exclusively owns `packages/db/src/schema.ts`, `packages/db/drizzle/**`, migration journal/meta/tests, then its disjoint recovery-parser/runbook slice. All schema requests serialize through coder2. +5. Frozen `contracts/*.v1.ts` are read-only inputs during implementation. Contract changes require Mos approval, a version bump/amendment, and coordinated rebase before work resumes. +6. coder3 exclusively owns Gateway DTO/controllers/services and the enumerated `apps/gateway/src/mcp/**` server files. coder4 owns CLI/projection clients and never edits MCP server files. Web consumers use the exact KBN-105 endpoint/DTO freeze. +7. coder4 executes one lane order: CLI/projection → pure Coordinator → importer → cutover. The pure Coordinator under `packages/coord` does not load IDs or access DB, Gateway, Valkey, recovery I/O, or web files; coder3 owns the persistence/service adapter. +8. Migration/import tooling calls Gateway/migration-only approved ports and does not add a second database model. +9. Each lane commits only its owned files and reports any needed cross-slice change as a contract-change request instead of editing another lane's tree. +10. Cross-review is mandatory: no lane reviews its own changes. Recommended ring is coder2 ← coder5, coder3 ← coder2, coder4 ← coder3, coder5 ← coder4, followed by independent SecReview where triggered and Certifier final. +11. Integration-only edits are a separate serialized slice after component lanes are green; no opportunistic merge-conflict resolution may alter semantics. + +## 8. Delivery phases and exit gates + +### P0 — Canon and authority foundation + +- Publish this canon, frozen schema/ports/health/recovery contracts, threat model, authorization matrix, exact endpoint/DTO registry, concrete current-main field-by-field migration map, and standards amendment. +- Build hold remains active until independent author≠reviewer re-review returns GO on health proof/failures, approval binding, fencing, tenant relationships, proposals, migration map, slice ordering/API freeze, recovery validation, and vocabulary alignment. +- Exit: no unresolved second writer or contract blocker, tenant boundary frozen, all seven decisions traceable, and independent re-review GO recorded. + +### P1 — Thin native MVP + +- Schema/migration, tenant-safe Gateway, CLI/MCP/projection, writable Kanban/List/Projects, dependencies/readiness/audit. +- Exit: same revision across web/CLI/MCP/projection; cross-workspace tests fail closed; generated files cannot mutate state. + +### P2 — Mechanical coordination + +- Agent/session registry, deterministic engine, approval queue, PostgreSQL leases/fencing/checkpoints/outbox, retry/quarantine, operations UI. +- Exit: one lease winner, stale tokens rejected, dependencies/approvals enforced, DB/Valkey fault semantics proven, Certifier gate has no merge authority. + +### P3 — Shadow migration and cutover + +- Importer, lineage, reconciliation, reviewer UI, write freeze, final delta, Gateway switch, legacy read-only, stabilization and rollback package. +- Exit: signed reconciliation, zero active legacy writers, scoped Gateway identities, imported backlog cannot dispatch accidentally. + +## 9. Evidence required for mission closure + +- Requirement-to-test/evidence matrix. +- Schema/migration and N-1 rolling-deploy proof. +- Cross-workspace API/repository/import/Coordinator negative tests. +- Health-state and fail-closed fault injection. +- Valkey-loss/outbox replay and Coordinator restart tests. +- Concurrent lease and stale fencing tests. +- Endpoint-registry alignment across web/CLI/MCP/Gateway. +- Accessible real-Gateway Kanban journeys. +- Generated projection tamper/no-import proof. +- One-way migration dry-run/apply/verify and field reconciliation. +- Author-independent functional review and required SecReview. +- Certifier final decision and evidence bundle. +- Merged main SHA, terminal green CI, closed linked task/issue, and post-merge situational validation under orchestrator ownership. + +## 10. Change control + +This manifest is derived from the ratified source plan. Any change to SOT authority, workspace tenancy, fixed statuses, Coordinator/Certifier authority, health-state semantics, schema v1, migration direction, or recovery-tier field set is a contract change. Contract changes require Jason/Mos authorization and cannot be inferred by an implementation lane. + +No coder lane may start while the build hold is active. KBN-010 must complete before KBN-100; KBN-105 exact endpoint/DTO freeze must complete before any API consumer implementation. diff --git a/docs/native-kanban-sot/SHARED-CONTRACT.md b/docs/native-kanban-sot/SHARED-CONTRACT.md new file mode 100644 index 0000000..43e44c3 --- /dev/null +++ b/docs/native-kanban-sot/SHARED-CONTRACT.md @@ -0,0 +1,219 @@ +# 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=`, `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=`. 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 { + contractVersion: '1.0.0'; + data: T; + aggregateRevision: string; + correlationId: string; +} +interface ListEnvelopeV1 extends SuccessEnvelopeV1 { + 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. diff --git a/docs/native-kanban-sot/TASKS.md b/docs/native-kanban-sot/TASKS.md new file mode 100644 index 0000000..c0e10ef --- /dev/null +++ b/docs/native-kanban-sot/TASKS.md @@ -0,0 +1,261 @@ +# Native Kanban/SOT P0–P3 — Dependency-Ordered Build Slices + +**Status:** CANON INDEPENDENTLY APPROVED; PUBLICATION IN PROGRESS +**Tracking:** [Mosaic Stack issue #751](https://git.mosaicstack.dev/mosaicstack/stack/issues/751) +**Execution:** USC web1 only; collision-free GPT coder2/3/4/5 lanes +**Contract:** `SHARED-CONTRACT.md` + four `contracts/*.v1.ts` files +**Implementation hold:** no feature slice starts until the canon PR is merged to `main` with terminal-green CI; after merge, each slice remains held until every declared KBN prerequisite is complete. + +> This publication file is not a runtime task authority. After cutover, repository `TASKS.md` is generated read-only and never imported. + +## Execution invariants + +- PostgreSQL is the sole writable SOT; current-main Drizzle is the persistence foundation. +- Mutations require fresh internal PostgreSQL transaction-local write proof and fail closed otherwise. +- Public health DTOs, Valkey, files, browser state, providers, and outage notes cannot authorize writes. +- Outage notes return only through attributable `change_proposals`; proposal acceptance executes the normal command. +- Mechanical Coordinator is non-LLM and cannot invent scope, waive gates, certify, or merge. +- Certifier is final independent gate with no merge authority. +- Workspace is the hard tenant. Project hierarchy is project-congruent. Assignment, approval, lease, fence, checkpoint, and evidence are relationally bound. +- Recovery tiers change recovery posture only. + +## 1. Collision-free ownership + +| USC lane | Exclusive ownership | Must not edit | +| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| **coder2 — schema/recovery** | `packages/db/src/schema.ts`; `packages/db/drizzle/**`; DB tests; `packages/config/src/recovery-posture.ts`; `packages/config/src/recovery-posture.spec.ts`; `docs/runbooks/kanban-postgres-recovery.md` | Gateway, Brain repositories, Coordinator, web, CLI/importer | +| **coder3 — domain/Gateway/MCP server** | Kanban repositories under `packages/brain/src/`; Gateway workspace/project/mission/milestone/task/kanban/health/coord modules; **exact MCP files:** `apps/gateway/src/mcp/mcp.dto.ts`, `mcp.controller.ts`, `mcp.service.ts`, `mcp.module.ts`, `mcp.tokens.ts`, `mcp.service.spec.ts`; Gateway root wiring/tests | DB schema/migrations, `packages/coord`, web, CLI/importer | +| **coder4 — CLI → pure Coordinator → migration tooling** | In this one fixed lane order: KBN-120 (`packages/mosaic` CLI/projection) → KBN-200 (`packages/coord/src/mechanical/**`) → KBN-300/320 (`scripts/kanban-migration/**`) | DB, Gateway/MCP server, web | +| **coder5 — web** | `apps/web/src/app/(dashboard)/{tasks,projects}/**`; `apps/web/src/components/{tasks,projects}/**`; Kanban web API/types; later Coordinator/migration-review routes | DB, Gateway, Coordinator, CLI/importer | +| **Mos — publication/integration** | Contract amendments, exact endpoint registry publication, serialized root exports/manifests/lockfiles, integration gates | Active lane feature files | + +Shared roots, package exports/manifests, lockfiles, and generated artifacts are integration-serialized. Contract changes stop affected lanes and require Mos approval. + +## 2. Parallelization legend + +- **SERIAL:** prerequisite must be complete and reviewed. +- **PARALLEL-GROUP:** disjoint files and exact frozen contract permit concurrent work. +- **LANE-SERIAL:** one lane's stated order cannot change. +- **INTEGRATION-SERIAL:** component heads green first; semantic findings return to owner. + +## 3. Corrected dependency graph + +```text +KBN-000 canon remediation + -> KBN-010 threat/auth/constraint-impact gate (MUST COMPLETE) + -> KBN-100 schema + concrete N-1 migration implementation + ├─ KBN-105 exact endpoint/DTO/error/registry freeze (SERIAL) + │ ├─ KBN-110 domain + Gateway + MCP server implementation + │ ├─ KBN-120 CLI/projection implementation [coder4 first] + │ └─ KBN-130 web MVP implementation + └─ KBN-115 recovery parser/mechanism slice [coder2 lane-serial] +KBN-110 + KBN-120 + KBN-130 + KBN-115 + -> KBN-140 P1 integration/SIT + -> KBN-200 pure decision engine [coder4 after KBN-120] + -> KBN-210 persistence/service adapter + approval/lease binding + -> KBN-220 Coordinator operations UI + -> KBN-230 P2 concurrency/fault/gate integration +KBN-230 + -> KBN-300 importer dry-run/apply/verify [coder4 after KBN-200] + ├─ KBN-310 migration reviewer UI + └─ KBN-320 cutover/rollback tooling [coder4 after KBN-300] +KBN-310 + KBN-320 + -> KBN-330 rehearsal/reconciliation + -> KBN-340 owner-gated cutover/stabilization +``` + +No consumer implementation begins before KBN-105. No schema work begins before KBN-010 completes. The coder4 order is always KBN-120 → KBN-200 → KBN-300 → KBN-320. + +## 4. P0 — Canon, threat gate, schema, and exact API freeze + +### KBN-000 — Remediate and publish canon + +- **Owner:** Mos / publication control plane. +- **Mode:** SERIAL; publication gate in progress. +- **IN:** Resolve KCR-001–016 in requirements, schema, health, Coordinator, recovery, migration map, and slices; independent re-review. +- **OUT:** Feature implementation. +- **Depends on:** none. +- **Contract surfaces:** all canon. +- **Evidence:** strict TS; Prettier; per-finding traceability; independent author≠reviewer GO. + +### KBN-010 — Threat, authorization, and constraint-impact gate + +- **Owner:** coder3; independent `secrev`. +- **Mode:** SERIAL prerequisite of KBN-100. +- **Exclusive files:** Mos-selected threat/auth docs only. +- **IN:** Cross-workspace owners/principals/evidence; active membership; stale/forged health; approval forgery; fence monotonicity; audit retention; proposal target/audit-event forgery; service tokens; DB/Valkey outage. +- **OUT:** Runtime/schema edits. +- **Depends on:** KBN-000 independent re-review GO. +- **Contract surfaces:** schema constraints, health proof, exact errors, command-family authorization. +- **Evidence:** signed constraint-impact matrix; no unresolved schema-impact finding; SecReview pass. + +### KBN-100 — Unified Drizzle schema and concrete N-1 migration + +- **Owner:** **coder2**. +- **Mode:** SERIAL. +- **Exclusive files:** `packages/db/src/schema.ts`, `packages/db/drizzle/**`, DB tests. +- **IN:** All frozen tables/joins/enums; workspace/project-congruent constraints; owners/principals; tags/archive; change proposals with both workspace-aware task-event composite FKs and frozen event-before-proposal DDL order; assignment approvals; durable execution/quarantine; monotonic bigint fence; exact checkpoint/evidence joins; RESTRICT/immutability; concrete current-main expand/backfill/switch/contract map. +- **OUT:** Repositories, Gateway, Coordinator behavior, UI, importer. +- **Depends on:** **KBN-010 completed**. +- **Contract surfaces:** `kanban-schema.v1.ts`; SHARED-CONTRACT current-main delta map. +- **Evidence:** reviewed SQL; empty/prod-shape/partial-resume/rollback tests; N-1 app safety; legacy columns remain declared; workspace/project mismatch negatives; proposal event-FK missing/foreign-workspace tests; one active lease; monotonic fence; parent-delete RESTRICT; immutability privileges; SecReview. + +### KBN-105 — Exact Gateway/MCP endpoint, DTO, and error freeze + +- **Owner:** Mos + coder3 contract author; independent endpoint-alignment reviewer. +- **Mode:** SERIAL after KBN-100; prerequisite for KBN-110/120/130. +- **Exclusive files:** canonical endpoint-registry/DTO contract docs; no implementation. +- **IN:** Exact routes and methods from SHARED-CONTRACT §8; request/success/error fields; status codes; pagination/filter/revision envelopes; idempotency/expected-version headers/fields; proposal commands; health proof exclusion from public DTOs; MCP tool-to-route map. +- **OUT:** Controller/service/client implementation. +- **Depends on:** KBN-100. +- **Contract surfaces:** health/error unions; schema IDs/statuses; Gateway DTO freeze. +- **Evidence:** every FE/CLI/MCP call maps 1:1 to a route; 503/502-504/409 non-cross-map fixtures; contract digest published. + +### KBN-115 — Recovery posture parser, mechanisms, and evidence + +- **Owner:** **coder2**, lane-serial after KBN-100. +- **Mode:** PARALLEL with KBN-110/120/130 after KBN-105. +- **Exclusive files:** `packages/config/src/recovery-posture.ts`, `.spec.ts`, `docs/runbooks/kanban-postgres-recovery.md`; deployment-specific backup manifest changes are a separately enumerated Mos integration patch. +- **IN:** Wire normative `validateRecoveryPostureV1`; override audit; backup/WAL/PITR mechanism assertions; off-cluster encryption/failure-domain checks; restore and break-glass evidence procedure. +- **OUT:** SOT/gate/Coordinator policy knobs; DB business schema. +- **Depends on:** KBN-100, KBN-105. +- **Contract surfaces:** `recovery-posture.v1.ts` only. +- **Evidence:** impossible-combination tests; High-assurance weakening tests; selected-tier mechanism verification; restore and break-glass evidence; SecReview. + +## 5. P1 — Thin native MVP + +### KBN-110 — Workspace-safe domain, Gateway, MCP server, and proposal commands + +- **Owner:** **coder3**. +- **Mode:** PARALLEL-GROUP P1-A after KBN-105. +- **Exclusive files:** ownership map, including all exact MCP server files listed there. +- **IN:** Workspace-safe repositories; project/task/dependency/tag/archive CRUD; transitions; exact owners; assignment/approval/link/artifact queries; submit/query/accept/reject change proposals; health endpoint; internal write-proof mint/revalidation; event/outbox atomicity; frozen DTOs/routes. +- **OUT:** Scheduling algorithm, web, CLI, DB schema. +- **Depends on:** KBN-100, KBN-105. +- **Contract surfaces:** all four TypeScript contracts and exact registry. +- **Evidence:** DTO/service/controller/integration tests; active-membership and no-oracle negatives; proposal cannot mutate directly; submission event is the new proposal's exact `change_proposal.submitted` event; acceptance links the executed normal command for the locked proposal and same workspace/target; missing, foreign-workspace, unrelated-proposal/target/command event negatives; exact failure mapping; endpoint registry; SecReview. + +### KBN-120 — CLI, MCP client mapping, and generated projection + +- **Owner:** **coder4**; first coder4 slice. +- **Mode:** PARALLEL-GROUP P1-A after KBN-105. +- **Exclusive files:** `packages/mosaic/src/commands/{kanban,tasks,projects}.ts`; `packages/mosaic/src/projections/**`; tests. **No `apps/gateway/src/mcp/**` edits.\*\* +- **IN:** Frozen query/mutation routes; proposal commands; compact context; generated `TASKS.md`; deliberate denial/transport/conflict handling. +- **OUT:** Gateway/MCP server, file importer, raw SQL/Valkey, Coordinator. +- **Depends on:** KBN-105; runtime integration later requires KBN-110. +- **Evidence:** contract fixtures; same revision; no import parser; same idempotency key on transport retry; 503 never auto-retried. + +### KBN-130 — Writable Kanban/List and minimal Projects UI + +- **Owner:** **coder5**. +- **Mode:** PARALLEL-GROUP P1-A after KBN-105. +- **Exclusive files:** web ownership map. +- **IN:** Workspace context; projects; tasks; tags; explicit archive; detail; accessible move/reorder; filters; dependency/readiness; owner/assignment/lease; audit; proposal visibility; conflict/loading/error/reconnect. +- **OUT:** Gateway/schema, Coordinator operations UI, migration UI. +- **Depends on:** KBN-105; runtime integration later requires KBN-110. +- **Evidence:** frozen contract mocks; real-Gateway journeys; keyboard/non-drag; tags/archive semantics; no-oracle tenant negatives; 503/transport/409 distinct UI. + +### KBN-140 — P1 integration and situational gate + +- **Owner:** Mos integration; independent reviewer/SecReview/Certifier. +- **Mode:** INTEGRATION-SERIAL. +- **IN:** KBN-110/120/130/115; unavoidable root exports only. +- **OUT:** P2 behavior. +- **Depends on:** KBN-110, KBN-120, KBN-130, KBN-115. +- **Evidence:** clean migration; web/CLI/MCP/projection revision parity; forged/expired health negatives; change-proposal event-chain success plus missing/foreign/unrelated-event negatives; tag/archive; tenant negatives; endpoint registry; author-independent review; Certifier pass. + +## 6. P2 — Mechanical Coordinator + +### KBN-200 — Pure deterministic decision engine + +- **Owner:** **coder4**; second coder4 slice, strictly after KBN-120. +- **Mode:** SERIAL in coder4 lane. +- **Exclusive files:** `packages/coord/src/mechanical/**` and pure tests. +- **IN:** `MechanicalCoordinatorDecisionEngineV1`; complete immutable snapshots; eligibility/explanation; fairness/order; capability matching; expiry/retry/quarantine decisions. +- **OUT:** ID loading, PostgreSQL, Drizzle, Gateway, Valkey, health-proof minting, persistence, `recoverFromPostgres`, LLM calls. +- **Depends on:** KBN-140 (or Mos may release after KBN-120 + frozen types if no P1 semantic risk remains). +- **Evidence:** deterministic/property tests; snapshot completeness; no I/O/model imports; no authority methods. + +### KBN-210 — Coordinator persistence/service adapter and approval-bound leases + +- **Owner:** **coder3**. +- **Mode:** SERIAL after KBN-200. +- **Exclusive files:** Gateway `coord` and repositories. +- **IN:** `MechanicalCoordinatorServicePortV1`; snapshot loading; proposal persistence; manual/versioned policy approval; acquire by IDs; reload+lock task/assignment/approval/session; fresh txn-local write proof; atomic task fence increment; lease/ack/heartbeat/checkpoint/submit; durable retry/quarantine; outbox/Valkey wake; restart recovery. +- **OUT:** Pure algorithm, UI, DB schema. +- **Depends on:** KBN-110, KBN-200. +- **Evidence:** forged/stale approval rejection; target/session/version/expiry/policy checks; concurrent monotonic fences; same-workspace mismatch negatives; bigint precision; stale worker rejection; DB/Valkey faults; SecReview. + +### KBN-220 — Coordinator operations UI + +- **Owner:** **coder5**. +- **Mode:** after KBN-210 exact DTO freeze. +- **IN:** Roster; eligibility; persisted assignment state; approvals/overrides; exact lease/fence; durable retry/quarantine; role/gate/Certifier visibility. +- **OUT:** Scheduling decisions, schema, merge control for Certifier. +- **Depends on:** KBN-210. +- **Evidence:** authorized journeys; reason required; stale refresh; no Certifier merge; endpoint alignment/accessibility. + +### KBN-230 — P2 concurrency/fault/gate integration + +- **Owner:** Mos integration; independent reviewer/SecReview/Certifier. +- **Mode:** INTEGRATION-SERIAL. +- **Depends on:** KBN-200, KBN-210, KBN-220. +- **Evidence:** one lease; monotonic fences; exact relational mismatches rejected; expired proof; forged healthy; approval binding; restart; durable quarantine; outbox recovery; author≠reviewer; Certifier final/no merge. + +## 7. P3 — Shadow migration and cutover + +### KBN-300 — One-way importer dry-run/apply/verify + +- **Owner:** **coder4**; third coder4 slice. +- **Mode:** after KBN-230. +- **Exclusive files:** `scripts/kanban-migration/import/**`. +- **IN:** Immutable jarvis-brain/Vikunja snapshots; deterministic mapping; source digest/lineage; Gateway writes; rejects; no dispatch. +- **OUT:** Bidirectional sync, direct DB/file canonical writes, unrelated brain data. +- **Depends on:** KBN-230. +- **Evidence:** idempotency; counts/fields; malformed/foreign rejects; no dispatch; SecReview. + +### KBN-310 — Shadow reviewer UI + +- **Owner:** **coder5**. +- **Mode:** PARALLEL-GROUP P3-A after KBN-300 report freeze. +- **IN:** Read-only counts/diffs/rejects/lineage/sign-off. +- **OUT:** Apply/cutover mutations. +- **Depends on:** KBN-300. +- **Evidence:** read-only and tenant tests; pagination/accessibility. + +### KBN-320 — Cutover/rollback tooling + +- **Owner:** **coder4**; fourth coder4 slice, after KBN-300. +- **Mode:** PARALLEL-GROUP P3-A with KBN-310. +- **Exclusive files:** `scripts/kanban-migration/cutover/**`. +- **IN:** Freeze assertion; backup/checksum; final delta; client switch; legacy writer/credential shutdown; rollback delta; stabilization. +- **OUT:** Destructive deletion, reverse sync, ungated production execution. +- **Depends on:** KBN-300. +- **Evidence:** fail-safe rehearsal; no dual writer; rollback authority; SecReview. + +### KBN-330 — Migration rehearsal/reconciliation + +- **Owner:** Mos + coder4 support + independent data reviewer. +- **Mode:** INTEGRATION-SERIAL. +- **Depends on:** KBN-310, KBN-320. +- **Evidence:** signed exceptions; selected-tier restore; backlog hold; no legacy changes; Certifier readiness. + +### KBN-340 — Final cutover/stabilization + +- **Owner:** Mos/control plane; owner-gated operation. +- **Mode:** SERIAL. +- **Depends on:** KBN-330 PASS and Jason authorization. +- **Evidence:** no legacy writer; scoped Gateway identities; no accidental dispatch; terminal green health/CI; Certifier evidence; owner retirement approval. + +## 8. Consistent USC wave schedule + +| Wave | coder2 | coder3 | coder4 | coder5 | +| ---- | ------------------------- | -------------------------------------- | ------------------------------ | ------------------------------ | +| 0 | Wait | **KBN-010** | Wait | Wait | +| 1 | **KBN-100** | Review constraint implementation | Wait | Wait | +| 2 | **KBN-115** after KBN-100 | **KBN-105** exact freeze, then KBN-110 | **KBN-120** only after KBN-105 | **KBN-130** only after KBN-105 | +| 3 | Review support | Finish KBN-110 | **KBN-200 after KBN-120** | Finish KBN-130 | +| 4 | — | **KBN-210 after KBN-200** | Review/support | **KBN-220 after KBN-210 DTOs** | +| 5 | — | P2 remediation | **KBN-300 then KBN-320** | **KBN-310** | + +Mos alone releases slices and lifts the build hold after independent re-review GO. diff --git a/docs/native-kanban-sot/contracts/health-state.v1.ts b/docs/native-kanban-sot/contracts/health-state.v1.ts new file mode 100644 index 0000000..2deaa0f --- /dev/null +++ b/docs/native-kanban-sot/contracts/health-state.v1.ts @@ -0,0 +1,206 @@ +/** + * Mosaic Native Kanban — frozen health/error contract v1. + * Publication contract only; no runtime implementation is included here. + * + * PostgreSQL is the sole writable SOT. Public health DTOs are observations, + * never write authority. Only an internal transaction-local proof produced by + * the PostgreSQL adapter may authorize a mutation. + */ + +export const KANBAN_CONTRACT_VERSION = '1.0.0' as const; + +export const kanbanHealthStates = ['healthy', 'read-only-degraded', 'write-unavailable'] as const; +export type KanbanHealthState = (typeof kanbanHealthStates)[number]; + +interface KanbanHealthBaseV1 { + contractVersion: typeof KANBAN_CONTRACT_VERSION; + checkedAt: string; + /** Observation expires at this RFC 3339 instant; it still never authorizes writes. */ + validUntil: string; + policyRevision: string; + reasons: string[]; +} + +export interface HealthyKanbanHealthResponseV1 extends KanbanHealthBaseV1 { + state: 'healthy'; + readHealthProven: true; + writeHealthProven: true; +} + +export interface ReadOnlyDegradedKanbanHealthResponseV1 extends KanbanHealthBaseV1 { + state: 'read-only-degraded'; + readHealthProven: true; + writeHealthProven: false; +} + +export interface WriteUnavailableKanbanHealthResponseV1 extends KanbanHealthBaseV1 { + state: 'write-unavailable'; + readHealthProven: false; + writeHealthProven: false; +} + +/** Public, discriminated observation. Contradictory combinations are unrepresentable. */ +export type KanbanHealthResponseV1 = + | HealthyKanbanHealthResponseV1 + | ReadOnlyDegradedKanbanHealthResponseV1 + | WriteUnavailableKanbanHealthResponseV1; + +/** Pure evaluation context. It cannot authorize a mutation. */ +export interface KanbanEvaluationContextV1 { + contractVersion: typeof KANBAN_CONTRACT_VERSION; + workspaceId: string; + correlationId: string; + now: string; + policyRevision: string; + observedHealth: KanbanHealthResponseV1; +} + +/** + * Non-exported brand: public DTO deserialization cannot construct this type. + * The PostgreSQL adapter mints it only after a fresh write probe inside the same + * transaction and validates checkedAt <= now < validUntil and policy revision. + */ +declare const postgresWriteHealthProofBrand: unique symbol; +export interface PostgresWriteHealthProofV1 { + readonly [postgresWriteHealthProofBrand]: true; + readonly source: 'postgres-transaction-local-write-probe'; + readonly transactionId: string; + readonly checkedAt: string; + readonly validUntil: string; + readonly policyRevision: string; +} + +/** Internal mutation context; MUST NOT appear in REST/MCP/CLI request DTOs. */ +export interface InternalKanbanMutationContextV1 { + contractVersion: typeof KANBAN_CONTRACT_VERSION; + workspaceId: string; + correlationId: string; + causationId?: string; + idempotencyKey: string; + now: string; + expectedPolicyRevision: string; + writeProof: PostgresWriteHealthProofV1; +} + +interface MutationFailureBaseV1 { + contractVersion: typeof KANBAN_CONTRACT_VERSION; + retryable: false; + requestOutcome: 'not_applied'; + idempotencyKey: string; + correlationId: string; + message: string; +} + +/** KCR-016: code/state pairing is exact and cannot cross-map. */ +export interface ReadOnlyWriteHealthDenialV1 extends MutationFailureBaseV1 { + kind: 'deliberate_fail_closed_denial'; + code: 'KANBAN_WRITE_HEALTH_UNPROVEN'; + healthState: 'read-only-degraded'; + checkedAt: string; +} + +export interface WriteUnavailableDenialV1 extends MutationFailureBaseV1 { + kind: 'deliberate_fail_closed_denial'; + code: 'KANBAN_WRITE_UNAVAILABLE'; + healthState: 'write-unavailable'; + checkedAt: string; +} + +export type DeliberateWriteDenialV1 = ReadOnlyWriteHealthDenialV1 | WriteUnavailableDenialV1; + +export const transportErrorCodes = [ + 'GATEWAY_UNREACHABLE', + 'GATEWAY_TIMEOUT', + 'UPSTREAM_BAD_GATEWAY', +] as const; +export type TransportErrorCode = (typeof transportErrorCodes)[number]; + +/** Client-normalized transport uncertainty; never an authoritative 503 body. */ +export interface RetryableTransportErrorV1 { + contractVersion: typeof KANBAN_CONTRACT_VERSION; + kind: 'retryable_transport_error'; + code: TransportErrorCode; + retryable: true; + requestOutcome: 'unknown'; + /** Retry MUST reuse this exact key. */ + idempotencyKey: string; + correlationId: string; + message: string; +} + +export interface VersionConflictV1 { + contractVersion: typeof KANBAN_CONTRACT_VERSION; + kind: 'version_conflict'; + code: 'AGGREGATE_VERSION_CONFLICT'; + retryable: false; + requestOutcome: 'not_applied'; + aggregateType: 'project' | 'mission' | 'milestone' | 'task' | 'change_proposal'; + aggregateId: string; + expectedVersion: number; + actualVersion: number; + idempotencyKey: string; + correlationId: string; + message: string; +} + +export type KanbanMutationFailureV1 = + | DeliberateWriteDenialV1 + | RetryableTransportErrorV1 + | VersionConflictV1; + +export const kanbanHealthCapabilities: Readonly< + Record +> = { + healthy: { canonicalReads: true, mutations: true }, + 'read-only-degraded': { canonicalReads: true, mutations: false }, + 'write-unavailable': { canonicalReads: false, mutations: false }, +}; + +/** Exact HTTP/error normalization freeze; 503, transport, and 409 cannot cross-map. */ +export const kanbanFailureHttpMapV1 = { + KANBAN_WRITE_HEALTH_UNPROVEN: { + httpStatus: 503, + kind: 'deliberate_fail_closed_denial', + requestOutcome: 'not_applied', + retryable: false, + }, + KANBAN_WRITE_UNAVAILABLE: { + httpStatus: 503, + kind: 'deliberate_fail_closed_denial', + requestOutcome: 'not_applied', + retryable: false, + }, + AGGREGATE_VERSION_CONFLICT: { + httpStatus: 409, + kind: 'version_conflict', + requestOutcome: 'not_applied', + retryable: false, + }, + GATEWAY_UNREACHABLE: { + httpStatus: 502, + kind: 'retryable_transport_error', + requestOutcome: 'unknown', + retryable: true, + }, + GATEWAY_TIMEOUT: { + httpStatus: 504, + kind: 'retryable_transport_error', + requestOutcome: 'unknown', + retryable: true, + }, + UPSTREAM_BAD_GATEWAY: { + httpStatus: 502, + kind: 'retryable_transport_error', + requestOutcome: 'unknown', + retryable: true, + }, +} as const; + +/** + * Required negative contract tests: + * - contradictory state/proof booleans fail type/schema validation; + * - expired internal proof and policy mismatch deny before mutation; + * - Valkey-only liveness cannot mint PostgresWriteHealthProofV1; + * - public/caller-forged `healthy` cannot enter InternalKanbanMutationContextV1; + * - authoritative 503, transport 502/504/timeout, and 409 mappings are exhaustive. + */ diff --git a/docs/native-kanban-sot/contracts/kanban-schema.v1.ts b/docs/native-kanban-sot/contracts/kanban-schema.v1.ts new file mode 100644 index 0000000..594a556 --- /dev/null +++ b/docs/native-kanban-sot/contracts/kanban-schema.v1.ts @@ -0,0 +1,1294 @@ +/** + * Mosaic Native Kanban — frozen Drizzle schema contract v1. + * + * This is the canonical target/compatibility declaration for integration into + * the ONE current-main packages/db/src/schema.ts. It MUST NOT be imported as a + * competing schema module. KBN-100 applies the field-by-field expand/backfill/ + * switch/contract map in SHARED-CONTRACT.md; legacy fields marked below remain + * declared throughout the expand and N-1 window. + * + * Existing Better Auth users.id is the identity parent. User references require + * active workspace membership checks in the same authoritative transaction. + */ + +import { sql } from 'drizzle-orm'; +import { + bigint, + boolean, + check, + foreignKey, + index, + integer, + jsonb, + numeric, + pgEnum, + pgTable, + primaryKey, + text, + timestamp, + uniqueIndex, + uuid, +} from 'drizzle-orm/pg-core'; + +// ─── Frozen vocabularies ───────────────────────────────────────────────────── + +export const workspaceLifecycleEnum = pgEnum('workspace_lifecycle', [ + 'active', + 'suspended', + 'archived', +]); +export const workspaceMemberRoleEnum = pgEnum('workspace_member_role', [ + 'owner', + 'admin', + 'member', + 'auditor', + 'service', +]); +export const teamMemberRoleEnum = pgEnum('team_member_role', ['manager', 'member']); +export const agentLifecycleEnum = pgEnum('agent_lifecycle', ['enabled', 'disabled']); +export const agentSessionStateEnum = pgEnum('agent_session_state', [ + 'starting', + 'available', + 'busy', + 'degraded', + 'offline', + 'ended', +]); +export const projectStatusEnum = pgEnum('project_status_v1', [ + 'planning', + 'active', + 'paused', + 'completed', + 'archived', +]); +export const missionStatusEnum = pgEnum('mission_status_v1', [ + 'draft', + 'awaiting_approval', + 'active', + 'paused', + 'certifying', + 'completed', + 'failed', + 'cancelled', +]); +export const milestoneStatusEnum = pgEnum('milestone_status_v1', [ + 'planned', + 'active', + 'at_risk', + 'completed', + 'cancelled', +]); +export const taskStatusEnum = pgEnum('task_status_v1', [ + 'backlog', + 'ready', + 'in_progress', + 'blocked', + 'in_review', + 'done', + 'cancelled', +]); +export const priorityEnum = pgEnum('work_priority_v1', ['critical', 'high', 'medium', 'low']); +export const specialistRoleEnum = pgEnum('specialist_role_v1', [ + 'planning', + 'enhance', + 'coder', + 'review', + 'security-review', + 'pr-monitor', + 'certifier', +]); +export const dependencyTypeEnum = pgEnum('task_dependency_type', [ + 'blocks', + 'review_gate', + 'certification_gate', +]); +/** Must match assignmentStates in mechanical-coordinator.v1.ts exactly. */ +export const assignmentStateEnum = pgEnum('task_assignment_state_v1', [ + 'awaiting_approval', + 'policy_pre_authorized', + 'approved', + 'rejected', + 'leased', + 'released', + 'expired', + 'superseded', +]); +export const leaseStateEnum = pgEnum('task_lease_state', [ + 'pending_ack', + 'active', + 'released', + 'expired', + 'revoked', +]); +export const actorKindEnum = pgEnum('actor_kind_v1', [ + 'user', + 'agent', + 'session', + 'service', + 'policy', + 'system', +]); +export const approvalDecisionEnum = pgEnum('approval_decision_v1', [ + 'requested', + 'approved', + 'rejected', + 'escalated', +]); +export const executionDispositionEnum = pgEnum('task_execution_disposition_v1', [ + 'available', + 'retry_delayed', + 'quarantined', + 'exhausted', +]); +export const changeProposalStateEnum = pgEnum('change_proposal_state_v1', [ + 'pending', + 'accepted', + 'rejected', +]); +export const outboxStateEnum = pgEnum('outbox_state_v1', [ + 'pending', + 'publishing', + 'published', + 'failed', +]); + +// ─── Tenant and identity ───────────────────────────────────────────────────── + +export const workspacesV1 = pgTable( + 'workspaces', + { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + slug: text('slug').notNull(), + settings: jsonb('settings').notNull().$type>().default({}), + lifecycle: workspaceLifecycleEnum('lifecycle').notNull().default('active'), + ownerId: text('owner_id').notNull(), // FK to existing users.id at integration + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('workspaces_slug_uidx').on(t.slug), + check('workspaces_version_positive_chk', sql`${t.version} > 0`), + ], +); + +export const workspaceMembersV1 = pgTable( + 'workspace_members', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + userId: text('user_id').notNull(), // FK to existing users.id at integration + role: workspaceMemberRoleEnum('role').notNull().default('member'), + active: boolean('active').notNull().default(true), + joinedAt: timestamp('joined_at', { withTimezone: true }).notNull().defaultNow(), + revokedAt: timestamp('revoked_at', { withTimezone: true }), + }, + (t) => [ + uniqueIndex('workspace_members_workspace_user_uidx').on(t.workspaceId, t.userId), + index('workspace_members_user_active_idx').on(t.userId, t.active), + ], +); + +export const teamsV1 = pgTable( + 'teams', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + name: text('name').notNull(), + slug: text('slug').notNull(), + ownerId: text('owner_id').notNull(), // legacy/current users.id field retained + managerId: text('manager_id').notNull(), // legacy/current users.id field retained + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('teams_workspace_slug_uidx').on(t.workspaceId, t.slug), + uniqueIndex('teams_workspace_id_uidx').on(t.workspaceId, t.id), + ], +); + +export const teamMembersV1 = pgTable( + 'team_members', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + teamId: uuid('team_id').notNull(), + userId: text('user_id').notNull(), + role: teamMemberRoleEnum('role').notNull().default('member'), + invitedBy: text('invited_by'), + joinedAt: timestamp('joined_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'team_members_workspace_team_fk', + columns: [t.workspaceId, t.teamId], + foreignColumns: [teamsV1.workspaceId, teamsV1.id], + }).onDelete('restrict'), + uniqueIndex('team_members_workspace_team_user_uidx').on(t.workspaceId, t.teamId, t.userId), + ], +); + +export const agentsV1 = pgTable( + 'agents', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + name: text('name').notNull(), + provider: text('provider').notNull(), // legacy retained + model: text('model').notNull(), // legacy retained + legacyStatus: text('status', { + enum: ['idle', 'active', 'error', 'offline'], + }) + .notNull() + .default('idle'), + projectId: uuid('project_id'), // legacy retained through N-1 + ownerId: text('owner_id'), // legacy retained; active membership required + systemPrompt: text('system_prompt'), // legacy retained + allowedTools: jsonb('allowed_tools').$type(), // legacy retained + skills: jsonb('skills').$type(), // legacy retained + isSystem: boolean('is_system').notNull().default(false), // legacy retained + config: jsonb('config'), // legacy retained + runtime: text('runtime').notNull(), + roles: jsonb('roles').notNull().$type().default([]), + capabilities: jsonb('capabilities').notNull().$type().default([]), + lifecycle: agentLifecycleEnum('lifecycle').notNull().default('enabled'), + metadata: jsonb('metadata').notNull().$type>().default({}), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('agents_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('agents_workspace_name_uidx').on(t.workspaceId, t.name), + index('agents_workspace_lifecycle_idx').on(t.workspaceId, t.lifecycle), + check('agents_version_positive_chk', sql`${t.version} > 0`), + ], +); + +export const agentSessionsV1 = pgTable( + 'agent_sessions', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + agentId: uuid('agent_id').notNull(), + harnessSessionKey: text('harness_session_key').notNull(), + host: text('host').notNull(), + state: agentSessionStateEnum('state').notNull().default('starting'), + declaredRoles: specialistRoleEnum('declared_primary_role'), + roleSet: jsonb('role_set').notNull().$type().default([]), + capabilities: jsonb('capabilities').notNull().$type().default([]), + capacity: integer('capacity').notNull().default(1), + contextUsagePercent: integer('context_usage_percent'), + startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(), + lastHeartbeatAt: timestamp('last_heartbeat_at', { withTimezone: true }), + endedAt: timestamp('ended_at', { withTimezone: true }), + metadata: jsonb('metadata').notNull().$type>().default({}), + }, + (t) => [ + foreignKey({ + name: 'agent_sessions_workspace_agent_fk', + columns: [t.workspaceId, t.agentId], + foreignColumns: [agentsV1.workspaceId, agentsV1.id], + }).onDelete('restrict'), + uniqueIndex('agent_sessions_workspace_harness_key_uidx').on(t.workspaceId, t.harnessSessionKey), + uniqueIndex('agent_sessions_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('agent_sessions_workspace_agent_id_uidx').on(t.workspaceId, t.agentId, t.id), + index('agent_sessions_workspace_state_heartbeat_idx').on( + t.workspaceId, + t.state, + t.lastHeartbeatAt, + ), + check('agent_sessions_capacity_positive_chk', sql`${t.capacity} > 0`), + check( + 'agent_sessions_context_percent_chk', + sql`${t.contextUsagePercent} is null or (${t.contextUsagePercent} >= 0 and ${t.contextUsagePercent} <= 100)`, + ), + ], +); + +// ─── Planning hierarchy ────────────────────────────────────────────────────── + +export const projectsV1 = pgTable( + 'projects', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + name: text('name').notNull(), + description: text('description'), + /** Legacy status remains declared/readable through the N-1 window. */ + legacyStatus: text('status', { + enum: ['active', 'paused', 'completed', 'archived'], + }) + .notNull() + .default('active'), + canonicalStatus: projectStatusEnum('canonical_status').notNull().default('planning'), + /** Legacy ownership fields retained until contract release. */ + legacyOwnerId: text('owner_id'), + legacyTeamId: uuid('team_id'), + legacyOwnerType: text('owner_type', { enum: ['user', 'team'] }) + .notNull() + .default('user'), + accountableUserId: text('accountable_user_id'), + accountableTeamId: uuid('accountable_team_id'), + priority: priorityEnum('priority').notNull().default('medium'), + repositoryUrl: text('repository_url'), + repositoryProvider: text('repository_provider'), + defaultBranch: text('default_branch'), + domain: text('domain'), + startDate: timestamp('start_date', { withTimezone: true }), + targetDate: timestamp('target_date', { withTimezone: true }), + blockerSummary: text('blocker_summary'), + progressPolicy: jsonb('progress_policy').notNull().$type>().default({}), + metadata: jsonb('metadata').notNull().$type>().default({}), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'projects_workspace_accountable_user_fk', + columns: [t.workspaceId, t.accountableUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'projects_workspace_accountable_team_fk', + columns: [t.workspaceId, t.accountableTeamId], + foreignColumns: [teamsV1.workspaceId, teamsV1.id], + }).onDelete('restrict'), + uniqueIndex('projects_workspace_id_uidx').on(t.workspaceId, t.id), + index('projects_workspace_status_idx').on(t.workspaceId, t.canonicalStatus), + check( + 'projects_exactly_one_accountable_owner_chk', + sql`num_nonnulls(${t.accountableUserId}, ${t.accountableTeamId}) = 1`, + ), + check('projects_version_positive_chk', sql`${t.version} > 0`), + ], +); + +export const milestonesV1 = pgTable( + 'milestones', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + projectId: uuid('project_id').notNull(), + name: text('name').notNull(), + description: text('description'), + status: milestoneStatusEnum('status').notNull().default('planned'), + sequence: integer('sequence').notNull(), + targetDate: timestamp('target_date', { withTimezone: true }), + completedAt: timestamp('completed_at', { withTimezone: true }), + providerMilestoneRef: text('provider_milestone_ref'), + acceptanceSummary: text('acceptance_summary'), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'milestones_workspace_project_fk', + columns: [t.workspaceId, t.projectId], + foreignColumns: [projectsV1.workspaceId, projectsV1.id], + }).onDelete('restrict'), + uniqueIndex('milestones_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id), + uniqueIndex('milestones_project_sequence_uidx').on(t.workspaceId, t.projectId, t.sequence), + index('milestones_workspace_project_status_idx').on(t.workspaceId, t.projectId, t.status), + check('milestones_sequence_positive_chk', sql`${t.sequence} > 0`), + check('milestones_version_positive_chk', sql`${t.version} > 0`), + ], +); + +/** Avoids an unsafe circular projects.current_milestone FK during expand. */ +export const projectCurrentMilestonesV1 = pgTable( + 'project_current_milestones', + { + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + projectId: uuid('project_id').notNull(), + milestoneId: uuid('milestone_id').notNull(), + setByUserId: text('set_by_user_id').notNull(), + setAt: timestamp('set_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + primaryKey({ name: 'project_current_milestones_pk', columns: [t.workspaceId, t.projectId] }), + foreignKey({ + name: 'project_current_milestones_project_fk', + columns: [t.workspaceId, t.projectId], + foreignColumns: [projectsV1.workspaceId, projectsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'project_current_milestones_milestone_fk', + columns: [t.workspaceId, t.projectId, t.milestoneId], + foreignColumns: [milestonesV1.workspaceId, milestonesV1.projectId, milestonesV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'project_current_milestones_set_by_user_fk', + columns: [t.workspaceId, t.setByUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + ], +); + +export const missionsV1 = pgTable( + 'missions', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + projectId: uuid('project_id').notNull(), + name: text('name').notNull(), + /** Legacy fields remain through N-1 and are mapped, never dropped on expand. */ + legacyDescription: text('description'), + legacyStatus: text('status', { + enum: ['planning', 'active', 'paused', 'completed', 'failed'], + }) + .notNull() + .default('planning'), + legacyUserId: text('user_id'), + legacyMilestones: jsonb('milestones').$type[]>(), + legacyConfig: jsonb('config'), + objective: text('objective').notNull(), + canonicalStatus: missionStatusEnum('canonical_status').notNull().default('draft'), + phase: text('phase'), + prdArtifactUri: text('prd_artifact_uri'), + prdRevision: text('prd_revision'), + portfolioOrchestratorId: text('portfolio_orchestrator_id'), + projectSubOrchestratorId: text('project_sub_orchestrator_id'), + approvalPolicy: jsonb('approval_policy').notNull().$type>().default({}), + startedAt: timestamp('started_at', { withTimezone: true }), + completedAt: timestamp('completed_at', { withTimezone: true }), + metadata: jsonb('metadata').notNull().$type>().default({}), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'missions_workspace_project_fk', + columns: [t.workspaceId, t.projectId], + foreignColumns: [projectsV1.workspaceId, projectsV1.id], + }).onDelete('restrict'), + uniqueIndex('missions_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id), + index('missions_workspace_project_status_idx').on( + t.workspaceId, + t.projectId, + t.canonicalStatus, + ), + check('missions_version_positive_chk', sql`${t.version} > 0`), + ], +); + +export const missionMilestonesV1 = pgTable( + 'mission_milestones', + { + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + projectId: uuid('project_id').notNull(), + missionId: uuid('mission_id').notNull(), + milestoneId: uuid('milestone_id').notNull(), + ordering: integer('ordering').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + primaryKey({ + name: 'mission_milestones_pk', + columns: [t.workspaceId, t.projectId, t.missionId, t.milestoneId], + }), + foreignKey({ + name: 'mission_milestones_project_mission_fk', + columns: [t.workspaceId, t.projectId, t.missionId], + foreignColumns: [missionsV1.workspaceId, missionsV1.projectId, missionsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'mission_milestones_project_milestone_fk', + columns: [t.workspaceId, t.projectId, t.milestoneId], + foreignColumns: [milestonesV1.workspaceId, milestonesV1.projectId, milestonesV1.id], + }).onDelete('restrict'), + uniqueIndex('mission_milestones_order_uidx').on( + t.workspaceId, + t.projectId, + t.missionId, + t.ordering, + ), + check('mission_milestones_order_positive_chk', sql`${t.ordering} > 0`), + ], +); + +// ─── Tasks, tags, dependencies ─────────────────────────────────────────────── + +export const tasksV1 = pgTable( + 'tasks', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + projectId: uuid('project_id').notNull(), + missionId: uuid('mission_id'), + milestoneId: uuid('milestone_id'), + parentTaskId: uuid('parent_task_id'), + title: text('title').notNull(), + description: text('description'), + /** Legacy/current-main fields retained throughout expand/N-1. */ + legacyStatus: text('status', { + enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'], + }) + .notNull() + .default('not-started'), + legacyAssignee: text('assignee'), + legacyTags: jsonb('tags').$type(), + legacyDueDate: timestamp('due_date', { withTimezone: true }), + acceptanceCriteria: jsonb('acceptance_criteria') + .notNull() + .$type | string[]>() + .default([]), + canonicalStatus: taskStatusEnum('canonical_status').notNull().default('backlog'), + priority: priorityEnum('priority').notNull().default('medium'), + boardRank: numeric('board_rank', { precision: 30, scale: 15 }).notNull().default('1000'), + accountableUserId: text('accountable_user_id'), + accountableTeamId: uuid('accountable_team_id'), + assignedSpecialistRole: specialistRoleEnum('assigned_specialist_role'), + dueAt: timestamp('due_at', { withTimezone: true }), + notBeforeAt: timestamp('not_before_at', { withTimezone: true }), + estimateMinutes: integer('estimate_minutes'), + progressPercent: integer('progress_percent').notNull().default(0), + blocker: text('blocker'), + retryPolicy: jsonb('retry_policy').notNull().$type>().default({}), + /** Atomically incremented under this task row lock for every new lease. */ + fencingCounter: bigint('fencing_counter', { mode: 'bigint' }) + .notNull() + .default(sql`0`), + archivedAt: timestamp('archived_at', { withTimezone: true }), + archivedByUserId: text('archived_by_user_id'), + archiveReason: text('archive_reason'), + metadata: jsonb('metadata').notNull().$type>().default({}), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + completedAt: timestamp('completed_at', { withTimezone: true }), + }, + (t) => [ + foreignKey({ + name: 'tasks_workspace_project_fk', + columns: [t.workspaceId, t.projectId], + foreignColumns: [projectsV1.workspaceId, projectsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'tasks_project_mission_fk', + columns: [t.workspaceId, t.projectId, t.missionId], + foreignColumns: [missionsV1.workspaceId, missionsV1.projectId, missionsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'tasks_project_milestone_fk', + columns: [t.workspaceId, t.projectId, t.milestoneId], + foreignColumns: [milestonesV1.workspaceId, milestonesV1.projectId, milestonesV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'tasks_project_parent_fk', + columns: [t.workspaceId, t.projectId, t.parentTaskId], + foreignColumns: [t.workspaceId, t.projectId, t.id], + }).onDelete('restrict'), + foreignKey({ + name: 'tasks_workspace_accountable_user_fk', + columns: [t.workspaceId, t.accountableUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'tasks_workspace_archived_by_user_fk', + columns: [t.workspaceId, t.archivedByUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'tasks_workspace_accountable_team_fk', + columns: [t.workspaceId, t.accountableTeamId], + foreignColumns: [teamsV1.workspaceId, teamsV1.id], + }).onDelete('restrict'), + uniqueIndex('tasks_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('tasks_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id), + index('tasks_workspace_project_status_rank_idx').on( + t.workspaceId, + t.projectId, + t.canonicalStatus, + t.boardRank, + ), + index('tasks_workspace_due_idx').on(t.workspaceId, t.dueAt), + check( + 'tasks_exactly_one_accountable_owner_chk', + sql`num_nonnulls(${t.accountableUserId}, ${t.accountableTeamId}) = 1`, + ), + check('tasks_version_positive_chk', sql`${t.version} > 0`), + check('tasks_fencing_counter_nonnegative_chk', sql`${t.fencingCounter} >= 0`), + check( + 'tasks_progress_percent_chk', + sql`${t.progressPercent} >= 0 and ${t.progressPercent} <= 100`, + ), + check( + 'tasks_archive_fields_chk', + sql`(${t.archivedAt} is null and ${t.archivedByUserId} is null and ${t.archiveReason} is null) or (${t.archivedAt} is not null and ${t.archivedByUserId} is not null and ${t.archiveReason} is not null)`, + ), + ], +); + +export const tagsV1 = pgTable( + 'tags', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + name: text('name').notNull(), + normalizedName: text('normalized_name').notNull(), + color: text('color'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('tags_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('tags_workspace_normalized_name_uidx').on(t.workspaceId, t.normalizedName), + ], +); + +export const taskTagsV1 = pgTable( + 'task_tags', + { + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id').notNull(), + tagId: uuid('tag_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + primaryKey({ name: 'task_tags_pk', columns: [t.workspaceId, t.taskId, t.tagId] }), + foreignKey({ + name: 'task_tags_workspace_task_fk', + columns: [t.workspaceId, t.taskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'task_tags_workspace_tag_fk', + columns: [t.workspaceId, t.tagId], + foreignColumns: [tagsV1.workspaceId, tagsV1.id], + }).onDelete('restrict'), + ], +); + +export const taskDependenciesV1 = pgTable( + 'task_dependencies', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + predecessorTaskId: uuid('predecessor_task_id').notNull(), + successorTaskId: uuid('successor_task_id').notNull(), + dependencyType: dependencyTypeEnum('dependency_type').notNull().default('blocks'), + completionCondition: jsonb('completion_condition').$type>(), + createdByActorKind: actorKindEnum('created_by_actor_kind').notNull(), + createdByActorId: text('created_by_actor_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'task_dependencies_predecessor_fk', + columns: [t.workspaceId, t.predecessorTaskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'task_dependencies_successor_fk', + columns: [t.workspaceId, t.successorTaskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + /** One directed pair only; dependency type is an attribute, not a second edge. */ + uniqueIndex('task_dependencies_directed_edge_uidx').on( + t.workspaceId, + t.predecessorTaskId, + t.successorTaskId, + ), + index('task_dependencies_successor_idx').on(t.workspaceId, t.successorTaskId), + check( + 'task_dependencies_no_self_edge_chk', + sql`${t.predecessorTaskId} <> ${t.successorTaskId}`, + ), + ], +); + +// ─── Links, immutable artifacts, audit events, outage proposals ────────────── + +export const externalLinksV1 = pgTable( + 'external_links', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + entityType: text('entity_type', { + enum: ['project', 'mission', 'milestone', 'task'], + }).notNull(), + entityId: uuid('entity_id').notNull(), + provider: text('provider').notNull(), + linkType: text('link_type', { + enum: ['issue', 'pr', 'ci', 'document', 'release', 'deployment'], + }).notNull(), + externalId: text('external_id').notNull(), + url: text('url').notNull(), + repository: text('repository'), + syncMetadata: jsonb('sync_metadata').notNull().$type>().default({}), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('external_links_entity_provider_type_external_uidx').on( + t.workspaceId, + t.entityType, + t.entityId, + t.provider, + t.linkType, + t.externalId, + ), + index('external_links_entity_idx').on(t.workspaceId, t.entityType, t.entityId), + ], +); + +export const artifactsV1 = pgTable( + 'artifacts', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id'), + missionId: uuid('mission_id'), + type: text('type').notNull(), + uri: text('uri').notNull(), + immutableRevision: text('immutable_revision').notNull(), + digest: text('digest').notNull(), + producerActorKind: actorKindEnum('producer_actor_kind').notNull(), + producerActorId: text('producer_actor_id').notNull(), + evidenceClassification: text('evidence_classification').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'artifacts_workspace_task_fk', + columns: [t.workspaceId, t.taskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'artifacts_workspace_mission_fk', + columns: [t.workspaceId, t.missionId], + foreignColumns: [missionsV1.workspaceId, missionsV1.id], + }).onDelete('restrict'), + uniqueIndex('artifacts_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('artifacts_workspace_digest_uidx').on(t.workspaceId, t.digest), + check('artifacts_exactly_one_owner_chk', sql`num_nonnulls(${t.taskId}, ${t.missionId}) = 1`), + ], +); + +export const taskEventsV1 = pgTable( + 'task_events', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + aggregateType: text('aggregate_type', { + enum: [ + 'workspace', + 'project', + 'mission', + 'milestone', + 'task', + 'assignment', + 'lease', + 'change_proposal', + ], + }).notNull(), + aggregateId: uuid('aggregate_id').notNull(), + eventType: text('event_type').notNull(), + actorKind: actorKindEnum('actor_kind').notNull(), + actorId: text('actor_id').notNull(), + correlationId: uuid('correlation_id').notNull(), + causationId: uuid('causation_id'), + idempotencyKey: text('idempotency_key').notNull(), + previousVersion: integer('previous_version'), + newVersion: integer('new_version'), + payload: jsonb('payload').notNull().$type>().default({}), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('task_events_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('task_events_workspace_idempotency_uidx').on(t.workspaceId, t.idempotencyKey), + index('task_events_aggregate_created_idx').on( + t.workspaceId, + t.aggregateType, + t.aggregateId, + t.createdAt, + ), + ], +); + +export const changeProposalsV1 = pgTable( + 'change_proposals', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + proposerUserId: text('proposer_user_id').notNull(), + sourceNoteDigest: text('source_note_digest').notNull(), + targetAggregateType: text('target_aggregate_type', { + enum: ['project', 'mission', 'milestone', 'task'], + }).notNull(), + targetAggregateId: uuid('target_aggregate_id').notNull(), + expectedAggregateVersion: integer('expected_aggregate_version').notNull(), + proposedCommand: text('proposed_command').notNull(), + proposedPayload: jsonb('proposed_payload').notNull().$type>(), + state: changeProposalStateEnum('state').notNull().default('pending'), + idempotencyKey: text('idempotency_key').notNull(), + submittedAuditEventId: uuid('submitted_audit_event_id').notNull(), + decisionActorUserId: text('decision_actor_user_id'), + decisionReason: text('decision_reason'), + decidedAt: timestamp('decided_at', { withTimezone: true }), + acceptedCommandAuditEventId: uuid('accepted_command_audit_event_id'), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'change_proposals_workspace_proposer_user_fk', + columns: [t.workspaceId, t.proposerUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'change_proposals_workspace_decision_actor_user_fk', + columns: [t.workspaceId, t.decisionActorUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'change_proposals_workspace_submitted_event_fk', + columns: [t.workspaceId, t.submittedAuditEventId], + foreignColumns: [taskEventsV1.workspaceId, taskEventsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'change_proposals_workspace_accepted_command_event_fk', + columns: [t.workspaceId, t.acceptedCommandAuditEventId], + foreignColumns: [taskEventsV1.workspaceId, taskEventsV1.id], + }).onDelete('restrict'), + uniqueIndex('change_proposals_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('change_proposals_workspace_idempotency_uidx').on(t.workspaceId, t.idempotencyKey), + index('change_proposals_workspace_target_state_idx').on( + t.workspaceId, + t.targetAggregateType, + t.targetAggregateId, + t.state, + ), + check('change_proposals_expected_version_positive_chk', sql`${t.expectedAggregateVersion} > 0`), + check('change_proposals_version_positive_chk', sql`${t.version} > 0`), + check( + 'change_proposals_decision_fields_chk', + sql`(${t.state} = 'pending' and ${t.decisionActorUserId} is null and ${t.decisionReason} is null and ${t.decidedAt} is null and ${t.acceptedCommandAuditEventId} is null) or (${t.state} = 'rejected' and ${t.decisionActorUserId} is not null and ${t.decisionReason} is not null and ${t.decidedAt} is not null and ${t.acceptedCommandAuditEventId} is null) or (${t.state} = 'accepted' and ${t.decisionActorUserId} is not null and ${t.decisionReason} is not null and ${t.decidedAt} is not null and ${t.acceptedCommandAuditEventId} is not null)`, + ), + ], +); + +// ─── Assignments, execution, fencing, checkpoints ──────────────────────────── + +export const taskAssignmentsV1 = pgTable( + 'task_assignments', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id').notNull(), + taskVersion: integer('task_version').notNull(), + targetUserId: text('target_user_id'), + targetTeamId: uuid('target_team_id'), + targetAgentId: uuid('target_agent_id'), + targetSessionId: uuid('target_session_id'), + specialistRole: specialistRoleEnum('specialist_role').notNull(), + state: assignmentStateEnum('state').notNull().default('awaiting_approval'), + policyRevision: text('policy_revision').notNull(), + proposedByUserId: text('proposed_by_user_id'), + proposedByAgentId: uuid('proposed_by_agent_id'), + reason: text('reason').notNull(), + proposedAt: timestamp('proposed_at', { withTimezone: true }).notNull().defaultNow(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + approvedAt: timestamp('approved_at', { withTimezone: true }), + endedAt: timestamp('ended_at', { withTimezone: true }), + }, + (t) => [ + foreignKey({ + name: 'task_assignments_workspace_task_fk', + columns: [t.workspaceId, t.taskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'task_assignments_workspace_user_fk', + columns: [t.workspaceId, t.targetUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'task_assignments_workspace_proposer_user_fk', + columns: [t.workspaceId, t.proposedByUserId], + foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId], + }).onDelete('restrict'), + foreignKey({ + name: 'task_assignments_workspace_proposer_agent_fk', + columns: [t.workspaceId, t.proposedByAgentId], + foreignColumns: [agentsV1.workspaceId, agentsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'task_assignments_workspace_team_fk', + columns: [t.workspaceId, t.targetTeamId], + foreignColumns: [teamsV1.workspaceId, teamsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'task_assignments_workspace_agent_fk', + columns: [t.workspaceId, t.targetAgentId], + foreignColumns: [agentsV1.workspaceId, agentsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'task_assignments_workspace_agent_session_fk', + columns: [t.workspaceId, t.targetAgentId, t.targetSessionId], + foreignColumns: [agentSessionsV1.workspaceId, agentSessionsV1.agentId, agentSessionsV1.id], + }).onDelete('restrict'), + uniqueIndex('task_assignments_workspace_id_uidx').on(t.workspaceId, t.id), + uniqueIndex('task_assignments_exact_target_uidx').on( + t.workspaceId, + t.taskId, + t.id, + t.targetAgentId, + t.targetSessionId, + ), + index('task_assignments_workspace_task_state_idx').on(t.workspaceId, t.taskId, t.state), + check( + 'task_assignments_exactly_one_principal_chk', + sql`num_nonnulls(${t.targetUserId}, ${t.targetTeamId}, ${t.targetAgentId}) = 1`, + ), + check( + 'task_assignments_exactly_one_proposer_chk', + sql`num_nonnulls(${t.proposedByUserId}, ${t.proposedByAgentId}) = 1`, + ), + check( + 'task_assignments_agent_session_pair_chk', + sql`(${t.targetAgentId} is null and ${t.targetSessionId} is null) or (${t.targetAgentId} is not null and ${t.targetSessionId} is not null)`, + ), + check('task_assignments_task_version_positive_chk', sql`${t.taskVersion} > 0`), + ], +); + +export const taskExecutionStatesV1 = pgTable( + 'task_execution_states', + { + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id').notNull(), + disposition: executionDispositionEnum('disposition').notNull().default('available'), + attemptCount: integer('attempt_count').notNull().default(0), + maxAttempts: integer('max_attempts').notNull(), + nextEligibleAt: timestamp('next_eligible_at', { withTimezone: true }), + terminalReason: text('terminal_reason'), + updatedByActorKind: actorKindEnum('updated_by_actor_kind').notNull(), + updatedByActorId: text('updated_by_actor_id').notNull(), + policyRevision: text('policy_revision').notNull(), + version: integer('version').notNull().default(1), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + primaryKey({ name: 'task_execution_states_pk', columns: [t.workspaceId, t.taskId] }), + foreignKey({ + name: 'task_execution_states_workspace_task_fk', + columns: [t.workspaceId, t.taskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + index('task_execution_states_disposition_next_idx').on( + t.workspaceId, + t.disposition, + t.nextEligibleAt, + ), + check('task_execution_states_attempt_nonnegative_chk', sql`${t.attemptCount} >= 0`), + check('task_execution_states_max_positive_chk', sql`${t.maxAttempts} > 0`), + check('task_execution_states_attempt_bound_chk', sql`${t.attemptCount} <= ${t.maxAttempts}`), + check('task_execution_states_version_positive_chk', sql`${t.version} > 0`), + ], +); + +export const taskLeasesV1 = pgTable( + 'task_leases', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id').notNull(), + assignmentId: uuid('assignment_id').notNull(), + agentId: uuid('agent_id').notNull(), + agentSessionId: uuid('agent_session_id').notNull(), + state: leaseStateEnum('state').notNull().default('pending_ack'), + acquiredAt: timestamp('acquired_at', { withTimezone: true }).notNull().defaultNow(), + acknowledgedAt: timestamp('acknowledged_at', { withTimezone: true }), + acknowledgeBy: timestamp('acknowledge_by', { withTimezone: true }).notNull(), + lastHeartbeatAt: timestamp('last_heartbeat_at', { withTimezone: true }), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + /** Exact value atomically returned from tasks.fencing_counter. */ + fencingToken: bigint('fencing_token', { mode: 'bigint' }).notNull(), + attemptNumber: integer('attempt_number').notNull(), + releaseReason: text('release_reason'), + releasedAt: timestamp('released_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'task_leases_exact_assignment_target_fk', + columns: [t.workspaceId, t.taskId, t.assignmentId, t.agentId, t.agentSessionId], + foreignColumns: [ + taskAssignmentsV1.workspaceId, + taskAssignmentsV1.taskId, + taskAssignmentsV1.id, + taskAssignmentsV1.targetAgentId, + taskAssignmentsV1.targetSessionId, + ], + }).onDelete('restrict'), + uniqueIndex('task_leases_active_task_uidx') + .on(t.workspaceId, t.taskId) + .where(sql`${t.state} in ('pending_ack', 'active')`), + uniqueIndex('task_leases_exact_fence_uidx').on(t.workspaceId, t.taskId, t.id, t.fencingToken), + uniqueIndex('task_leases_task_fence_uidx').on(t.workspaceId, t.taskId, t.fencingToken), + index('task_leases_workspace_state_expiry_idx').on(t.workspaceId, t.state, t.expiresAt), + check('task_leases_fence_positive_chk', sql`${t.fencingToken} > 0`), + check('task_leases_attempt_positive_chk', sql`${t.attemptNumber} > 0`), + ], +); + +export const taskCheckpointsV1 = pgTable( + 'task_checkpoints', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id').notNull(), + leaseId: uuid('lease_id').notNull(), + fencingToken: bigint('fencing_token', { mode: 'bigint' }).notNull(), + sequence: integer('sequence').notNull(), + resumableSummary: text('resumable_summary').notNull(), + contextUsagePercent: integer('context_usage_percent'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + foreignKey({ + name: 'task_checkpoints_exact_lease_fence_fk', + columns: [t.workspaceId, t.taskId, t.leaseId, t.fencingToken], + foreignColumns: [ + taskLeasesV1.workspaceId, + taskLeasesV1.taskId, + taskLeasesV1.id, + taskLeasesV1.fencingToken, + ], + }).onDelete('restrict'), + uniqueIndex('task_checkpoints_workspace_task_id_uidx').on(t.workspaceId, t.taskId, t.id), + uniqueIndex('task_checkpoints_lease_sequence_uidx').on(t.workspaceId, t.leaseId, t.sequence), + index('task_checkpoints_task_created_idx').on(t.workspaceId, t.taskId, t.createdAt), + check('task_checkpoints_sequence_positive_chk', sql`${t.sequence} > 0`), + check('task_checkpoints_fence_positive_chk', sql`${t.fencingToken} > 0`), + check( + 'task_checkpoints_context_percent_chk', + sql`${t.contextUsagePercent} is null or (${t.contextUsagePercent} >= 0 and ${t.contextUsagePercent} <= 100)`, + ), + ], +); + +export const checkpointArtifactsV1 = pgTable( + 'task_checkpoint_artifacts', + { + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id').notNull(), + checkpointId: uuid('checkpoint_id').notNull(), + artifactId: uuid('artifact_id').notNull(), + }, + (t) => [ + primaryKey({ + name: 'task_checkpoint_artifacts_pk', + columns: [t.workspaceId, t.checkpointId, t.artifactId], + }), + foreignKey({ + name: 'task_checkpoint_artifacts_checkpoint_fk', + columns: [t.workspaceId, t.taskId, t.checkpointId], + foreignColumns: [ + taskCheckpointsV1.workspaceId, + taskCheckpointsV1.taskId, + taskCheckpointsV1.id, + ], + }).onDelete('restrict'), + foreignKey({ + name: 'task_checkpoint_artifacts_artifact_fk', + columns: [t.workspaceId, t.artifactId], + foreignColumns: [artifactsV1.workspaceId, artifactsV1.id], + }).onDelete('restrict'), + ], +); + +// ─── Approvals, evidence, outbox ───────────────────────────────────────────── + +export const approvalDecisionsV1 = pgTable( + 'approval_decisions', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + taskId: uuid('task_id'), + missionId: uuid('mission_id'), + assignmentId: uuid('assignment_id'), + gateType: text('gate_type').notNull(), + requestedFromRole: specialistRoleEnum('requested_from_role'), + decision: approvalDecisionEnum('decision').notNull().default('requested'), + conditions: jsonb('conditions').notNull().$type>().default({}), + actorKind: actorKindEnum('actor_kind'), + actorId: text('actor_id'), + policyRevision: text('policy_revision').notNull(), + reason: text('reason'), + requestedAt: timestamp('requested_at', { withTimezone: true }).notNull().defaultNow(), + decidedAt: timestamp('decided_at', { withTimezone: true }), + }, + (t) => [ + foreignKey({ + name: 'approval_decisions_workspace_task_fk', + columns: [t.workspaceId, t.taskId], + foreignColumns: [tasksV1.workspaceId, tasksV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'approval_decisions_workspace_mission_fk', + columns: [t.workspaceId, t.missionId], + foreignColumns: [missionsV1.workspaceId, missionsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'approval_decisions_workspace_assignment_fk', + columns: [t.workspaceId, t.assignmentId], + foreignColumns: [taskAssignmentsV1.workspaceId, taskAssignmentsV1.id], + }).onDelete('restrict'), + uniqueIndex('approval_decisions_workspace_id_uidx').on(t.workspaceId, t.id), + index('approval_decisions_assignment_idx').on(t.workspaceId, t.assignmentId, t.decision), + check( + 'approval_decisions_one_target_chk', + sql`num_nonnulls(${t.taskId}, ${t.missionId}, ${t.assignmentId}) = 1`, + ), + ], +); + +export const approvalDecisionArtifactsV1 = pgTable( + 'approval_decision_artifacts', + { + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + approvalDecisionId: uuid('approval_decision_id').notNull(), + artifactId: uuid('artifact_id').notNull(), + }, + (t) => [ + primaryKey({ + name: 'approval_decision_artifacts_pk', + columns: [t.workspaceId, t.approvalDecisionId, t.artifactId], + }), + foreignKey({ + name: 'approval_decision_artifacts_decision_fk', + columns: [t.workspaceId, t.approvalDecisionId], + foreignColumns: [approvalDecisionsV1.workspaceId, approvalDecisionsV1.id], + }).onDelete('restrict'), + foreignKey({ + name: 'approval_decision_artifacts_artifact_fk', + columns: [t.workspaceId, t.artifactId], + foreignColumns: [artifactsV1.workspaceId, artifactsV1.id], + }).onDelete('restrict'), + ], +); + +export const outboxEventsV1 = pgTable( + 'outbox_events', + { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id') + .notNull() + .references(() => workspacesV1.id, { onDelete: 'restrict' }), + aggregateType: text('aggregate_type').notNull(), + aggregateId: uuid('aggregate_id').notNull(), + aggregateRevision: integer('aggregate_revision').notNull(), + eventType: text('event_type').notNull(), + payload: jsonb('payload').notNull().$type>(), + state: outboxStateEnum('state').notNull().default('pending'), + attempts: integer('attempts').notNull().default(0), + nextAttemptAt: timestamp('next_attempt_at', { withTimezone: true }), + publishedAt: timestamp('published_at', { withTimezone: true }), + lastError: text('last_error'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('outbox_events_aggregate_revision_type_uidx').on( + t.workspaceId, + t.aggregateType, + t.aggregateId, + t.aggregateRevision, + t.eventType, + ), + index('outbox_events_state_next_attempt_idx').on(t.state, t.nextAttemptAt), + check('outbox_events_revision_positive_chk', sql`${t.aggregateRevision} > 0`), + check('outbox_events_attempts_nonnegative_chk', sql`${t.attempts} >= 0`), + ], +); + +/** + * Mandatory transaction/privilege rules frozen with this schema: + * + * 1. Every user principal/owner/proposer/decision actor must have ACTIVE + * workspace_members membership; denials expose no foreign-ID existence. + * 2. roleSet/roles JSON values are validated against specialist_role_v1 until + * normalized role joins are introduced; the primary/assigned role is enum. + * 3. Dependency cycles are rejected under a serialized recursive check. + * 4. task_events is declared before change_proposals so migration DDL creates + * task_events_workspace_id_uidx before both workspace-aware proposal event + * FKs. Submission preallocates the proposal ID and, in one transaction, + * inserts `change_proposal.submitted` with aggregate_type=change_proposal, + * aggregate_id=proposal.id, previous_version=NULL, new_version=1, then the + * proposal referencing that event. Missing or foreign-workspace events fail. + * 5. change_proposals never mutate targets directly. Acceptance locks proposal + * and target, proves fresh PG write health, checks expected version, invokes + * the NORMAL typed command, and in that same transaction binds its emitted + * event. The event workspace/aggregate type/aggregate ID must equal the + * proposal workspace/target, causation_id must equal submittedAuditEventId, + * and payload.changeProposalId must equal proposal.id; unrelated events fail. + * 6. Lease acquisition locks task+assignment+approval+session, increments + * tasks.fencing_counter atomically, and uses RETURNING bigint as the token. + * 7. task_events, task_checkpoints, checkpoint/artifact evidence, approval + * evidence, and immutable artifacts grant application roles INSERT/SELECT + * only. Canonical parents are archived, not hard-deleted; all parent FKs use + * RESTRICT. Purge requires an audited retention/break-glass procedure. + * 8. Polymorphic external_links and change_proposals targets are validated in a + * workspace-scoped transaction before insert; no existence oracle. + * 9. Legacy fields remain in the unified Drizzle declaration throughout expand + * and N-1. New Gateway commands never accept mission_tasks.status as a write + * source; its concrete retirement is specified in SHARED-CONTRACT.md. + */ diff --git a/docs/native-kanban-sot/contracts/mechanical-coordinator.v1.ts b/docs/native-kanban-sot/contracts/mechanical-coordinator.v1.ts new file mode 100644 index 0000000..b75f4ce --- /dev/null +++ b/docs/native-kanban-sot/contracts/mechanical-coordinator.v1.ts @@ -0,0 +1,419 @@ +/** + * Mosaic Native Kanban — frozen Mechanical Coordinator contracts v1. + * + * The pure decision engine and persistence/orchestration service are separate. + * Neither surface can create scope, edit acceptance, waive gates, certify, + * merge, release a deployment, or close a provider issue. + */ + +import type { + DeliberateWriteDenialV1, + InternalKanbanMutationContextV1, + KanbanEvaluationContextV1, + KanbanMutationFailureV1, + RetryableTransportErrorV1, + VersionConflictV1, +} from './health-state.v1.js'; + +export const COORDINATOR_CONTRACT_VERSION = '1.0.0' as const; +export type Uuid = string; +export type IsoTimestamp = string; +/** PostgreSQL bigint-safe decimal string; never a JavaScript number. */ +export type FencingTokenV1 = string; + +export const specialistRoles = [ + 'planning', + 'enhance', + 'coder', + 'review', + 'security-review', + 'pr-monitor', + 'certifier', +] as const; +export type SpecialistRole = (typeof specialistRoles)[number]; + +/** One vocabulary shared with task_assignment_state_v1 in the Drizzle schema. */ +export const assignmentStates = [ + 'awaiting_approval', + 'policy_pre_authorized', + 'approved', + 'rejected', + 'leased', + 'released', + 'expired', + 'superseded', +] as const; +export type AssignmentStateV1 = (typeof assignmentStates)[number]; + +export const readinessStates = [ + 'dependency-gated', + 'schedule-gated', + 'policy-gated', + 'lease-available', + 'leased', + 'retry-delayed', + 'exhausted', + 'quarantined', +] as const; +export type ReadinessState = (typeof readinessStates)[number]; + +export interface RetryStateSnapshotV1 { + disposition: 'available' | 'retry_delayed' | 'quarantined' | 'exhausted'; + attemptCount: number; + maxAttempts: number; + nextEligibleAt: IsoTimestamp | null; + idempotent: boolean; + terminalReason: string | null; + version: number; +} + +export interface TaskEligibilitySnapshotV1 { + workspaceId: Uuid; + taskId: Uuid; + taskVersion: number; + projectId: Uuid; + projectActive: boolean; + missionId: Uuid | null; + missionActive: boolean; + status: 'ready'; + priority: 'critical' | 'high' | 'medium' | 'low'; + boardRank: string; + dueAt: IsoTimestamp | null; + notBeforeAt: IsoTimestamp | null; + createdAt: IsoTimestamp; + requiredRole: SpecialistRole; + requiredCapabilities: readonly string[]; + blockingDependencies: readonly { + taskId: Uuid; + done: boolean; + completionConditionSatisfied: boolean; + }[]; + releaseApproval: { + decisionId: Uuid; + approved: boolean; + policyRevision: string; + } | null; + activeLeaseId: Uuid | null; + retry: RetryStateSnapshotV1; +} + +export interface AgentSessionSnapshotV1 { + workspaceId: Uuid; + agentId: Uuid; + sessionId: Uuid; + state: 'available' | 'busy'; + roles: readonly SpecialistRole[]; + capabilities: readonly string[]; + capacity: number; + activeLeaseCount: number; + heartbeatAt: IsoTimestamp; +} + +export interface EligibilityExplanationV1 { + taskId: Uuid; + eligible: boolean; + readiness: ReadinessState; + reasons: readonly { + gate: + | 'status' + | 'project' + | 'mission' + | 'dependency' + | 'schedule' + | 'retry' + | 'approval' + | 'lease' + | 'capability' + | 'capacity' + | 'health'; + satisfied: boolean; + code: string; + detail: string; + }[]; + policyRevision: string; + evaluatedAt: IsoTimestamp; +} + +export interface AssignmentProposalDecisionV1 { + workspaceId: Uuid; + taskId: Uuid; + taskVersion: number; + targetAgentId: Uuid; + targetSessionId: Uuid; + specialistRole: SpecialistRole; + initialState: 'awaiting_approval' | 'policy_pre_authorized'; + policyRevision: string; + explanation: EligibilityExplanationV1; + expiresAt: IsoTimestamp; +} + +export interface AssignmentCycleSnapshotV1 { + context: KanbanEvaluationContextV1; + tasks: readonly TaskEligibilitySnapshotV1[]; + sessions: readonly AgentSessionSnapshotV1[]; + workspaceFairness: Readonly>; + limit: number; +} + +export interface AssignmentCycleDecisionV1 { + evaluatedTaskCount: number; + proposals: readonly AssignmentProposalDecisionV1[]; + explanations: readonly EligibilityExplanationV1[]; +} + +export interface LeaseExpirySnapshotV1 { + workspaceId: Uuid; + taskId: Uuid; + taskVersion: number; + leaseId: Uuid; + assignmentId: Uuid; + sessionId: Uuid; + fencingToken: FencingTokenV1; + state: 'pending_ack' | 'active'; + acknowledgeBy: IsoTimestamp; + expiresAt: IsoTimestamp; + lastHeartbeatAt: IsoTimestamp | null; + retry: RetryStateSnapshotV1; +} + +export interface LeaseExpiryDecisionV1 { + leaseId: Uuid; + action: 'retain' | 'release' | 'retry' | 'quarantine' | 'exhaust'; + reason: string; + nextEligibleAt: IsoTimestamp | null; +} + +/** Pure package owned by KBN-200. It receives complete immutable snapshots. */ +export interface MechanicalCoordinatorDecisionEngineV1 { + evaluateAssignmentCycle(snapshot: AssignmentCycleSnapshotV1): AssignmentCycleDecisionV1; + explainEligibility( + context: KanbanEvaluationContextV1, + task: TaskEligibilitySnapshotV1, + sessions: readonly AgentSessionSnapshotV1[], + ): EligibilityExplanationV1; + decideLeaseExpiry( + context: KanbanEvaluationContextV1, + lease: LeaseExpirySnapshotV1, + ): LeaseExpiryDecisionV1; +} + +export interface PersistedAssignmentV1 { + assignmentId: Uuid; + workspaceId: Uuid; + taskId: Uuid; + taskVersion: number; + targetAgentId: Uuid; + targetSessionId: Uuid; + specialistRole: SpecialistRole; + state: AssignmentStateV1; + policyRevision: string; + proposedBy: { kind: 'user' | 'agent'; id: Uuid }; + reason: string; + createdAt: IsoTimestamp; + expiresAt: IsoTimestamp; +} + +export interface TaskLeaseV1 { + leaseId: Uuid; + workspaceId: Uuid; + taskId: Uuid; + taskVersion: number; + assignmentId: Uuid; + agentId: Uuid; + sessionId: Uuid; + state: 'pending_ack' | 'active'; + fencingToken: FencingTokenV1; + attempt: number; + acquiredAt: IsoTimestamp; + acknowledgeBy: IsoTimestamp; + lastHeartbeatAt: IsoTimestamp | null; + expiresAt: IsoTimestamp; +} + +interface ServiceCommandBaseV1 { + context: InternalKanbanMutationContextV1; + taskId: Uuid; + expectedTaskVersion: number; +} + +export interface AcquireApprovedLeaseCommandV1 extends ServiceCommandBaseV1 { + assignmentId: Uuid; + approvalDecisionId: Uuid; + targetSessionId: Uuid; + leaseTtlSeconds: number; +} + +export interface LeaseCommandV1 extends ServiceCommandBaseV1 { + leaseId: Uuid; + sessionId: Uuid; + fencingToken: FencingTokenV1; +} + +export interface HeartbeatLeaseCommandV1 extends LeaseCommandV1 { + extendSeconds: number; +} + +export interface CheckpointCommandV1 extends LeaseCommandV1 { + sequence: number; + resumableSummary: string; + artifactIds: readonly Uuid[]; + contextUsagePercent: number; +} + +export interface SubmitForReviewCommandV1 extends LeaseCommandV1 { + artifactIds: readonly Uuid[]; + summary: string; +} + +export interface ReleaseLeaseCommandV1 extends LeaseCommandV1 { + reason: + | 'worker_requested' + | 'ack_timeout' + | 'heartbeat_timeout' + | 'task_submitted' + | 'policy_revoked' + | 'shutdown'; +} + +export interface AssignmentCycleCommandV1 { + context: InternalKanbanMutationContextV1; + limit: number; +} + +export interface ExpirySweepCommandV1 { + context: InternalKanbanMutationContextV1; + limit: number; +} + +export interface RecoverCoordinatorCommandV1 { + context: InternalKanbanMutationContextV1; +} + +interface CoordinatorRejectionBaseV1 { + kind: 'coordinator_rejection'; + retryable: false; + requestOutcome: 'not_applied'; + correlationId: Uuid; + idempotencyKey: string; + message: string; +} + +export type CoordinatorPolicyRejectionV1 = + | (CoordinatorRejectionBaseV1 & { code: 'WORKSPACE_MISMATCH' }) + | (CoordinatorRejectionBaseV1 & { + code: 'TASK_NOT_ELIGIBLE'; + explanation: EligibilityExplanationV1; + }) + | (CoordinatorRejectionBaseV1 & { code: 'APPROVAL_REQUIRED' }) + | (CoordinatorRejectionBaseV1 & { code: 'APPROVAL_STALE' }) + | (CoordinatorRejectionBaseV1 & { code: 'ASSIGNMENT_STALE' }) + | (CoordinatorRejectionBaseV1 & { code: 'ASSIGNMENT_TARGET_MISMATCH' }) + | (CoordinatorRejectionBaseV1 & { code: 'POLICY_REVISION_MISMATCH' }) + | (CoordinatorRejectionBaseV1 & { code: 'ARTIFACT_WORKSPACE_MISMATCH' }) + | (CoordinatorRejectionBaseV1 & { code: 'LEASE_ALREADY_ACTIVE' }) + | (CoordinatorRejectionBaseV1 & { code: 'LEASE_NOT_FOUND' }) + | (CoordinatorRejectionBaseV1 & { code: 'LEASE_NOT_ACTIVE' }) + | (CoordinatorRejectionBaseV1 & { + code: 'ACK_DEADLINE_EXPIRED'; + expiredAt: IsoTimestamp; + }) + | (CoordinatorRejectionBaseV1 & { + code: 'FENCING_TOKEN_STALE'; + currentFencingToken: FencingTokenV1; + }) + | (CoordinatorRejectionBaseV1 & { code: 'SESSION_MISMATCH' }) + | (CoordinatorRejectionBaseV1 & { + code: 'HEARTBEAT_EXPIRED'; + expiredAt: IsoTimestamp; + }) + | (CoordinatorRejectionBaseV1 & { + code: 'CHECKPOINT_SEQUENCE_CONFLICT'; + currentSequence: number; + }) + | (CoordinatorRejectionBaseV1 & { code: 'RETRY_EXHAUSTED' }) + | (CoordinatorRejectionBaseV1 & { + code: 'NON_IDEMPOTENT_RETRY_REQUIRES_ORCHESTRATOR'; + }); + +/** Explicit mapping to the Gateway mutation failure union; no arbitrary booleans. */ +export type CoordinatorFailureV1 = + | DeliberateWriteDenialV1 + | VersionConflictV1 + | RetryableTransportErrorV1 + | CoordinatorPolicyRejectionV1; + +export interface CoordinatorSuccessV1 { + ok: true; + value: T; + correlationId: Uuid; +} +export interface CoordinatorFailureResultV1 { + ok: false; + failure: CoordinatorFailureV1; +} +export type CoordinatorResultV1 = CoordinatorSuccessV1 | CoordinatorFailureResultV1; + +export interface ExpirySweepResultV1 { + examined: number; + released: readonly Uuid[]; + retryScheduled: readonly Uuid[]; + quarantined: readonly Uuid[]; + exhausted: readonly Uuid[]; +} + +export interface RestartRecoveryResultV1 { + activeLeaseIds: readonly Uuid[]; + expiredLeaseIds: readonly Uuid[]; + pendingAssignmentIds: readonly Uuid[]; + pendingOutboxEventIds: readonly Uuid[]; +} + +/** Persistence/Gateway adapter owned by KBN-210. */ +export interface MechanicalCoordinatorServicePortV1 { + /** Loads immutable snapshots, invokes pure engine, and persists proposals atomically. */ + runAssignmentCycle( + command: AssignmentCycleCommandV1, + ): Promise>; + + /** Query path loads by ID; public health observation cannot authorize mutation. */ + getEligibilityExplanation( + context: KanbanEvaluationContextV1, + taskId: Uuid, + ): Promise>; + + /** + * Accepts IDs only. Implementation reloads and locks assignment + approval + + * task + target session in PostgreSQL, then verifies workspace, task version, + * target agent/session, state, expiry, policy revision, and current approval. + */ + acquireApprovedLease( + command: AcquireApprovedLeaseCommandV1, + ): Promise>; + + acknowledgeLease(command: LeaseCommandV1): Promise>; + heartbeatLease(command: HeartbeatLeaseCommandV1): Promise>; + appendCheckpoint( + command: CheckpointCommandV1, + ): Promise>; + submitForReview( + command: SubmitForReviewCommandV1, + ): Promise>; + releaseLease(command: ReleaseLeaseCommandV1): Promise>; + expireAndRecover( + command: ExpirySweepCommandV1, + ): Promise>; + recoverFromPostgres( + command: RecoverCoordinatorCommandV1, + ): Promise>; +} + +/** Compile-time mapping guarantee: Coordinator Gateway failures are Kanban failures or exact policy rejections. */ +export function isKanbanMutationFailureV1( + failure: CoordinatorFailureV1, +): failure is KanbanMutationFailureV1 { + return ( + failure.kind === 'deliberate_fail_closed_denial' || + failure.kind === 'retryable_transport_error' || + failure.kind === 'version_conflict' + ); +} diff --git a/docs/native-kanban-sot/contracts/recovery-posture.v1.ts b/docs/native-kanban-sot/contracts/recovery-posture.v1.ts new file mode 100644 index 0000000..3604519 --- /dev/null +++ b/docs/native-kanban-sot/contracts/recovery-posture.v1.ts @@ -0,0 +1,369 @@ +/** + * Mosaic Native Kanban — frozen recovery-posture contract v1. + * Recovery posture is configurable; SOT, write-health, Coordinator authority, + * and gate semantics are not fields and cannot be overridden. + */ + +export const RECOVERY_POSTURE_CONTRACT_VERSION = '1.0.0' as const; +export const recoveryTiers = ['lite', 'standard', 'high-assurance'] as const; +export type RecoveryTier = (typeof recoveryTiers)[number]; + +export interface OffClusterStorageV1 { + required: true; + encrypted: true; + separateFailureDomain: true; + minimumCopies: number; + storageClass: 'encrypted-object-storage' | 'encrypted-backup-target'; +} + +export interface RecoveryPostureV1 { + contractVersion: typeof RECOVERY_POSTURE_CONTRACT_VERSION; + tier: RecoveryTier; + targetRpoMinutes: number; + targetRtoMinutes: number; + baseBackupIntervalHours: number; + /** null means WAL archival/PITR is disabled. */ + walArchiveIntervalMinutes: number | null; + /** 0 means PITR is disabled. */ + pitrRetentionDays: number; + restoreTestIntervalDays: number; + breakGlassDrillIntervalDays: number; + offClusterStorage: OffClusterStorageV1; +} + +export const recoveryPostureDefaults: Readonly> = { + lite: { + contractVersion: RECOVERY_POSTURE_CONTRACT_VERSION, + tier: 'lite', + targetRpoMinutes: 24 * 60, + targetRtoMinutes: 24 * 60, + baseBackupIntervalHours: 24, + walArchiveIntervalMinutes: null, + pitrRetentionDays: 0, + restoreTestIntervalDays: 90, + breakGlassDrillIntervalDays: 365, + offClusterStorage: { + required: true, + encrypted: true, + separateFailureDomain: true, + minimumCopies: 1, + storageClass: 'encrypted-backup-target', + }, + }, + standard: { + contractVersion: RECOVERY_POSTURE_CONTRACT_VERSION, + tier: 'standard', + targetRpoMinutes: 60, + targetRtoMinutes: 8 * 60, + baseBackupIntervalHours: 24, + walArchiveIntervalMinutes: 15, + pitrRetentionDays: 14, + restoreTestIntervalDays: 90, + breakGlassDrillIntervalDays: 180, + offClusterStorage: { + required: true, + encrypted: true, + separateFailureDomain: true, + minimumCopies: 1, + storageClass: 'encrypted-object-storage', + }, + }, + 'high-assurance': { + contractVersion: RECOVERY_POSTURE_CONTRACT_VERSION, + tier: 'high-assurance', + targetRpoMinutes: 15, + targetRtoMinutes: 4 * 60, + baseBackupIntervalHours: 24, + walArchiveIntervalMinutes: 5, + pitrRetentionDays: 35, + restoreTestIntervalDays: 30, + breakGlassDrillIntervalDays: 90, + offClusterStorage: { + required: true, + encrypted: true, + separateFailureDomain: true, + minimumCopies: 1, + storageClass: 'encrypted-object-storage', + }, + }, +}; + +/** Shape schema. Normative cross-field semantics are enforced by validateRecoveryPostureV1. */ +export const recoveryPostureJsonSchemaV1 = { + $id: 'https://mosaicstack.dev/contracts/recovery-posture.v1.schema.json', + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + additionalProperties: false, + required: [ + 'contractVersion', + 'tier', + 'targetRpoMinutes', + 'targetRtoMinutes', + 'baseBackupIntervalHours', + 'walArchiveIntervalMinutes', + 'pitrRetentionDays', + 'restoreTestIntervalDays', + 'breakGlassDrillIntervalDays', + 'offClusterStorage', + ], + properties: { + contractVersion: { const: RECOVERY_POSTURE_CONTRACT_VERSION }, + tier: { enum: recoveryTiers }, + targetRpoMinutes: { type: 'integer', minimum: 1 }, + targetRtoMinutes: { type: 'integer', minimum: 1 }, + baseBackupIntervalHours: { type: 'integer', minimum: 1 }, + walArchiveIntervalMinutes: { + anyOf: [{ type: 'integer', minimum: 1 }, { type: 'null' }], + }, + pitrRetentionDays: { type: 'integer', minimum: 0 }, + restoreTestIntervalDays: { type: 'integer', minimum: 1 }, + breakGlassDrillIntervalDays: { type: 'integer', minimum: 1 }, + offClusterStorage: { + type: 'object', + additionalProperties: false, + required: ['required', 'encrypted', 'separateFailureDomain', 'minimumCopies', 'storageClass'], + properties: { + required: { const: true }, + encrypted: { const: true }, + separateFailureDomain: { const: true }, + minimumCopies: { type: 'integer', minimum: 1 }, + storageClass: { + enum: ['encrypted-object-storage', 'encrypted-backup-target'], + }, + }, + }, + }, +} as const; + +export const recoveryValidationCodes = [ + 'INVALID_SHAPE', + 'UNKNOWN_FIELD', + 'PITR_REQUIRES_WAL', + 'WAL_REQUIRES_PITR', + 'RPO_BETTER_THAN_MECHANISM', + 'OFF_CLUSTER_REQUIRED', + 'HIGH_ASSURANCE_WEAKENED', +] as const; +export type RecoveryValidationCode = (typeof recoveryValidationCodes)[number]; + +export interface RecoveryValidationIssueV1 { + code: RecoveryValidationCode; + path: string; + message: string; +} +export type RecoveryValidationResultV1 = + | { ok: true; value: RecoveryPostureV1 } + | { ok: false; issues: RecoveryValidationIssueV1[] }; + +const topLevelFields = new Set([ + 'contractVersion', + 'tier', + 'targetRpoMinutes', + 'targetRtoMinutes', + 'baseBackupIntervalHours', + 'walArchiveIntervalMinutes', + 'pitrRetentionDays', + 'restoreTestIntervalDays', + 'breakGlassDrillIntervalDays', + 'offClusterStorage', +]); +const storageFields = new Set([ + 'required', + 'encrypted', + 'separateFailureDomain', + 'minimumCopies', + 'storageClass', +]); + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} +function isPositiveInteger(value: unknown): value is number { + return Number.isInteger(value) && Number(value) > 0; +} +function isNonnegativeInteger(value: unknown): value is number { + return Number.isInteger(value) && Number(value) >= 0; +} + +/** + * Normative parser/refinement. Deployment code MUST call this function (or a + * byte-for-byte behaviorally equivalent generated validator), not JSON Schema + * shape validation alone. + */ +export function validateRecoveryPostureV1(input: unknown): RecoveryValidationResultV1 { + const issues: RecoveryValidationIssueV1[] = []; + if (!isRecord(input)) { + return { + ok: false, + issues: [{ code: 'INVALID_SHAPE', path: '$', message: 'posture must be an object' }], + }; + } + + for (const key of Object.keys(input)) { + if (!topLevelFields.has(key)) { + issues.push({ code: 'UNKNOWN_FIELD', path: `$.${key}`, message: 'unknown field' }); + } + } + + const tier = input['tier']; + const storage = input['offClusterStorage']; + const integerFields = [ + 'targetRpoMinutes', + 'targetRtoMinutes', + 'baseBackupIntervalHours', + 'restoreTestIntervalDays', + 'breakGlassDrillIntervalDays', + ] as const; + + if (input['contractVersion'] !== RECOVERY_POSTURE_CONTRACT_VERSION) { + issues.push({ + code: 'INVALID_SHAPE', + path: '$.contractVersion', + message: `must equal ${RECOVERY_POSTURE_CONTRACT_VERSION}`, + }); + } + if (!recoveryTiers.includes(tier as RecoveryTier)) { + issues.push({ code: 'INVALID_SHAPE', path: '$.tier', message: 'unknown recovery tier' }); + } + for (const field of integerFields) { + if (!isPositiveInteger(input[field])) { + issues.push({ + code: 'INVALID_SHAPE', + path: `$.${field}`, + message: 'must be a positive integer', + }); + } + } + if (!isNonnegativeInteger(input['pitrRetentionDays'])) { + issues.push({ + code: 'INVALID_SHAPE', + path: '$.pitrRetentionDays', + message: 'must be a nonnegative integer', + }); + } + if ( + input['walArchiveIntervalMinutes'] !== null && + !isPositiveInteger(input['walArchiveIntervalMinutes']) + ) { + issues.push({ + code: 'INVALID_SHAPE', + path: '$.walArchiveIntervalMinutes', + message: 'must be null or a positive integer', + }); + } + + if (!isRecord(storage)) { + issues.push({ + code: 'INVALID_SHAPE', + path: '$.offClusterStorage', + message: 'must be an object', + }); + } else { + for (const key of Object.keys(storage)) { + if (!storageFields.has(key)) { + issues.push({ + code: 'UNKNOWN_FIELD', + path: `$.offClusterStorage.${key}`, + message: 'unknown field', + }); + } + } + if ( + storage['required'] !== true || + storage['encrypted'] !== true || + storage['separateFailureDomain'] !== true + ) { + issues.push({ + code: 'OFF_CLUSTER_REQUIRED', + path: '$.offClusterStorage', + message: 'storage must be required, encrypted, and in a separate failure domain', + }); + } + if (!isPositiveInteger(storage['minimumCopies'])) { + issues.push({ + code: 'INVALID_SHAPE', + path: '$.offClusterStorage.minimumCopies', + message: 'must be a positive integer', + }); + } + if ( + storage['storageClass'] !== 'encrypted-object-storage' && + storage['storageClass'] !== 'encrypted-backup-target' + ) { + issues.push({ + code: 'INVALID_SHAPE', + path: '$.offClusterStorage.storageClass', + message: 'unsupported storage class', + }); + } + } + + const wal = input['walArchiveIntervalMinutes']; + const pitr = input['pitrRetentionDays']; + if (pitr !== 0 && wal === null) { + issues.push({ + code: 'PITR_REQUIRES_WAL', + path: '$.pitrRetentionDays', + message: 'PITR retention requires WAL archival', + }); + } + if (wal !== null && pitr === 0) { + issues.push({ + code: 'WAL_REQUIRES_PITR', + path: '$.walArchiveIntervalMinutes', + message: 'WAL archival requires positive PITR retention', + }); + } + + if ( + isPositiveInteger(input['targetRpoMinutes']) && + isPositiveInteger(input['baseBackupIntervalHours']) && + (wal === null || isPositiveInteger(wal)) + ) { + const mechanismMinutes = wal === null ? input['baseBackupIntervalHours'] * 60 : wal; + if (mechanismMinutes > input['targetRpoMinutes']) { + issues.push({ + code: 'RPO_BETTER_THAN_MECHANISM', + path: '$.targetRpoMinutes', + message: `configured mechanism can only support ${mechanismMinutes} minutes`, + }); + } + } + + if (tier === 'high-assurance') { + const weakened = + !isPositiveInteger(input['targetRpoMinutes']) || + input['targetRpoMinutes'] > 15 || + !isPositiveInteger(input['targetRtoMinutes']) || + input['targetRtoMinutes'] > 4 * 60 || + !isPositiveInteger(input['baseBackupIntervalHours']) || + input['baseBackupIntervalHours'] > 24 || + !isPositiveInteger(wal) || + wal > 5 || + !isNonnegativeInteger(pitr) || + pitr < 35 || + !isPositiveInteger(input['restoreTestIntervalDays']) || + input['restoreTestIntervalDays'] > 30 || + !isPositiveInteger(input['breakGlassDrillIntervalDays']) || + input['breakGlassDrillIntervalDays'] > 90; + if (weakened) { + issues.push({ + code: 'HIGH_ASSURANCE_WEAKENED', + path: '$', + message: 'high-assurance posture may be strengthened but not weakened', + }); + } + } + + if (issues.length > 0) return { ok: false, issues }; + return { ok: true, value: input as unknown as RecoveryPostureV1 }; +} + +export interface RecoveryPostureOverrideAuditV1 { + actorId: string; + reason: string; + effectiveAt: string; + policyRevision: string; + previous: RecoveryPostureV1; + next: RecoveryPostureV1; +} diff --git a/docs/native-kanban-sot/tsconfig.json b/docs/native-kanban-sot/tsconfig.json new file mode 100644 index 0000000..8ab269f --- /dev/null +++ b/docs/native-kanban-sot/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "incremental": false, + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "baseUrl": ".", + "paths": { + "drizzle-orm": ["../../packages/db/node_modules/drizzle-orm/index.d.ts"], + "drizzle-orm/pg-core": ["../../packages/db/node_modules/drizzle-orm/pg-core/index.d.ts"] + } + }, + "include": ["contracts/*.ts"] +} diff --git a/docs/reports/native-kanban-sot/canon-final-rereview-go.md b/docs/reports/native-kanban-sot/canon-final-rereview-go.md new file mode 100644 index 0000000..f48b42e --- /dev/null +++ b/docs/reports/native-kanban-sot/canon-final-rereview-go.md @@ -0,0 +1,59 @@ +VERDICT: GO + +# Native Kanban/SOT canon independent re-review 2 + +Independent read-only re-review of the complete updated staged canon. Prior proposal-audit blocker is closed; no KCR-001–016 regression or new blocker found. + +## Prior blocker closure + +- `contracts/kanban-schema.v1.ts:836-837` declares the required unique `task_events(workspace_id,id)` key before proposal declaration. +- `contracts/kanban-schema.v1.ts:885-894` adds both composite proposal audit FKs—submission and accepted-command event—to that exact workspace-aware key with `RESTRICT`. +- Declaration/migration order is executable and explicit in `SHARED-CONTRACT.md:79-91`: events/key first, proposal table second, both FKs third/fourth, then command enablement. This avoids forward-reference/circular-DDL ambiguity. +- Submission/acceptance semantics are frozen in `SHARED-CONTRACT.md:87-91`: preallocate proposal ID; create exact `change_proposal.submitted` event and proposal in one transaction; on acceptance lock proposal/target, execute the normal command, and bind only a same-workspace/target event with submission causation and `payload.changeProposalId` equal to the locked proposal. +- Required missing, foreign-workspace, unrelated-proposal, unrelated-target, and unrelated-command negatives are explicit in `REQUIREMENTS.md` REQ-SOT-004 and `SHARED-CONTRACT.md:121`; KBN-100/110/140 own migration, service, and integration evidence. + +## KCR closure matrix + +| KCR | Status | +| ------------------------------------------------------ | ------ | +| 001 health/proof | CLOSED | +| 002 error discrimination | CLOSED | +| 003 approval/assignment binding | CLOSED | +| 004 monotonic fencing/composites | CLOSED | +| 005 tenant-safe relations | CLOSED | +| 006 outage proposal persistence/commands/audit binding | CLOSED | +| 007 dependency/API freeze sequencing | CLOSED | +| 008 concrete N-1 map | CLOSED | +| 009 dependency uniqueness | CLOSED | +| 010 project congruence | CLOSED | +| 011 immutable audit retention | CLOSED | +| 012 retry/quarantine/vocabulary | CLOSED | +| 013 archive/tags target semantics | CLOSED | +| 014 recovery validator/owner slice | CLOSED | +| 015 pure Coordinator split | CLOSED | +| 016 health code/state pairing | CLOSED | + +Fixed invariants remain consistent: PostgreSQL is sole writable SOT; writes require transaction-local proof and fail closed; exports never import sources; notes are attributable proposals only; Coordinator has no scope/gate/certify/merge authority; Certifier has no merge authority. + +## Reproducible validation evidence + +Executed read-only with current-stack config/toolchain `/src/mosaic-mono-v1`: + +```text +./node_modules/.bin/prettier --config /src/mosaic-mono-v1/.prettierrc --check +PASS: All matched files use Prettier code style. + +strict TypeScript --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext +PASS + +cascade/TODO/TBD/stale-hold grep plus composite-FK/semantic-marker invariant checks +PASS +``` + +The TypeScript check used a disposable copy under `/home/hermes/agent-work` solely to provide external-file NodeNext dependency resolution; the reviewed staging artifacts were not modified. + +## Residual findings + +None blocking. Implementation must execute the frozen KBN-100/KBN-110/KBN-140 proposal-event-chain tests and SecReview evidence before feature release, as already required by the canon. + +No artifact source repository, branch, PR, or provider state was modified. diff --git a/docs/reports/native-kanban-sot/canon-initial-review-no-go.md b/docs/reports/native-kanban-sot/canon-initial-review-no-go.md new file mode 100644 index 0000000..6a45358 --- /dev/null +++ b/docs/reports/native-kanban-sot/canon-initial-review-no-go.md @@ -0,0 +1,357 @@ +# Independent Review — Native Kanban/SOT Canon + +**Reviewer:** `enhance-sol` (independent of author `planner-sol`) +**Date:** 2026-07-13 +**Review mode:** design/contract only; read-only against the staged canon +**Source plan:** `/home/hermes/agent-work/planning/mosaic-native-kanban-sot-plan.md` (`sha256:96ea4fb91436ec9a53f371d27276e27f62ecf817662599ff9152df0db55296e5`) +**Canon reviewed:** every listed artifact under `/home/hermes/agent-work/planning/kanban-canon/`, including the four TypeScript contracts; the author scratchpad was also read as validation context. + +## Executive verdict + +# NO-GO + +The canon is not freeze-ready. I found **8 BLOCKERs**, **7 MAJORs**, and **1 MINOR**. The prose preserves the ratified authority model well, but the frozen types/schema leave concrete fail-closed, approval, fencing, tenant, outage-proposal, migration, and parallelization gaps. Those gaps would force implementation lanes either to invent contract semantics or to ship paths that violate fixed invariants. + +### Blocking findings + +1. Health/write authorization can be represented as contradictory, stale, or caller-asserted state. +2. Coordinator failures collapse authoritative denial, unknown transport outcome, and version conflict into one permissive shape. +3. Assignment proposals and approval proofs have no authoritative relational binding; lease acquisition accepts a forgeable proof DTO. +4. Fencing uniqueness is present, but monotonic fencing and same-task lease/checkpoint binding are not. +5. Workspace-safe accountable-owner, assignment-principal, and evidence/artifact relationships are not frozen. +6. Attributable post-recovery proposals have neither a canonical table nor command contract. +7. The slice graph starts schema/UI work before prerequisite threat and exact API/DTO freezes and contradicts coder4 lane order. +8. P0 claims a migration map while publishing only generic rules; the concrete N-1 transition from current `origin/main` is absent. + +--- + +## Findings + +### KCR-001 — BLOCKER — “Healthy” is not a proof and can be contradictory or stale + +**Location** + +- `contracts/health-state.v1.ts:21-31` — `KanbanHealthResponseV1` permits every combination of `state`, `readHealthProven`, and `writeHealthProven`. +- `contracts/mechanical-coordinator.v1.ts:40-49` — `CoordinatorContextV1` accepts a caller-supplied `healthState` enum only. +- `contracts/mechanical-coordinator.v1.ts:255-293` — every Coordinator operation, including mutating operations, accepts that context. +- `SHARED-CONTRACT.md:171-184` — mutations are allowed only after live PostgreSQL read/write probes. + +**Violation** + +Fixed invariant 2 / `REQ-SOT-002`: mutations must fail closed unless write health is positively proven. The current type permits `{ state: 'healthy', writeHealthProven: false }`, and the Coordinator mutation boundary can be invoked with a stale or fabricated `{ healthState: 'healthy' }`. A Valkey/client-derived enum could therefore be mistaken for write authorization. + +**Minimal fix** + +1. Make `KanbanHealthResponseV1` a discriminated union with only these legal combinations: `healthy => read=true/write=true`, `read-only-degraded => read=true/write=false`, and `write-unavailable => read=false/write=false`. +2. Do not accept write authority from a public DTO. Require Gateway/domain code to obtain and revalidate a fresh internal PostgreSQL write-health proof at mutation time (including `checkedAt`, bounded validity/policy revision, and transaction-local enforcement). +3. Split pure evaluation context from mutation context; mutation methods must accept only an unforgeable/internal healthy context or perform the probe themselves. +4. Add negative contract tests for contradictory state, expired proof, Valkey-only liveness, and caller-forged `healthy`. + +### KCR-002 — BLOCKER — Coordinator error shape can conflate denial, unknown outcome, and conflict + +**Location** + +- `contracts/mechanical-coordinator.v1.ts:184-216` — one `CoordinatorFailureV1` allows every code to pair with arbitrary `retryable` and either `requestOutcome` value. +- `contracts/health-state.v1.ts:51-106` — the Gateway health contract correctly distinguishes deliberate denial, transport uncertainty, and version conflict. +- `SHARED-CONTRACT.md:177-216` — frozen client semantics require those cases not to be conflated. + +**Violation** + +Charter E and `REQ-SOT-002`. The current Coordinator result can legally encode `WRITE_HEALTH_UNPROVEN` as `retryable: true, requestOutcome: 'unknown'`, or `VERSION_CONFLICT` as retryable. That permits blind retry or a false “unknown” outcome after an authoritative fail-closed denial. + +**Minimal fix** + +Replace `CoordinatorFailureV1` with a discriminated union keyed by code/kind: + +- deliberate health denial: `not_applied`, `retryable:false`; +- version conflict: `not_applied`, `retryable:false`, current version; +- stale fence/session/eligibility/approval failures: exact non-retry semantics; +- transport failure: a separate `retryable_transport_error`, `unknown`, same idempotency key. + +Reuse or map explicitly to `KanbanMutationFailureV1`, and add exhaustive client tests proving 503 authoritative bodies, 502/504/timeouts, and 409 cannot cross-map. + +### KCR-003 — BLOCKER — Approval proof is forgeable and is not linked to the persisted proposal + +**Location** + +- `contracts/mechanical-coordinator.v1.ts:107-137` — proposal and approval DTOs. +- `contracts/mechanical-coordinator.v1.ts:265-270` — `acquireApprovedLease` accepts the entire `ApprovalProofV1` by value. +- `contracts/kanban-schema.v1.ts:650-688` — `task_assignments` has no proposal expiry, task version, session binding, or proposal/approval FK. +- `contracts/kanban-schema.v1.ts:823-863` — `approval_decisions` can target only a task or mission and has no proposal/assignment relation. +- `SHARED-CONTRACT.md:128-137` — lease acquisition requires authoritative approval under the exact policy revision. + +**Violation** + +Fixed invariant 5 and `REQ-COORD-002/003`. A caller can construct an `ApprovalProofV1`; the schema cannot prove that it belongs to the proposal, workspace, task version, agent/session, unexpired policy revision, or still-current approval. The DTO state vocabulary (`awaiting_approval | policy_pre_authorized`) also does not map directly to the persisted assignment states (`proposed | approved | ...`). + +**Minimal fix** + +Persist one authoritative proposal/assignment identity with task version, target agent/session, expiry, state, and policy revision. Add a workspace-aware approval relation to that identity. Change lease acquisition to accept IDs, then reload and lock proposal + approval + task inside PostgreSQL and verify workspace, current version, target session, state, expiry, and policy revision before creating the lease. Freeze one state vocabulary across schema and DTOs. + +### KCR-004 — BLOCKER — Fencing is unique but not monotonically increasing; relational binding is incomplete + +**Location** + +- `contracts/kanban-schema.v1.ts:694-743` — `task_leases` has positive/unique fencing tokens but no monotonic per-task counter. +- `contracts/kanban-schema.v1.ts:748-784` — checkpoints independently carry task, lease, and fencing token. +- `contracts/kanban-schema.v1.ts:905-910` — token equality is deferred to prose; same-task lease binding is not stated. +- `contracts/mechanical-coordinator.v1.ts:140-175` — worker commands depend on fencing safety. + +**Violation** + +Fixed invariant 12 / `REQ-COORD-003`. Uniqueness permits token 10 followed by token 9. A lease can reference assignment A while naming task B in the same workspace, and a checkpoint can reference lease A while naming task B. `bigint(..., { mode: 'number' })` also eventually loses integer precision in JavaScript. + +**Minimal fix** + +Add a durable per-task fencing counter (or equivalent PostgreSQL sequence row) incremented atomically under task lock and use the returned value for every new lease. Add workspace-aware composite constraints tying lease to its exact task+assignment and checkpoint to exact task+lease+fence. Use bigint-safe representation (`bigint`/serialized decimal), and test monotonicity, concurrent claims, stale lower tokens, and mismatched same-workspace IDs. + +### KCR-005 — BLOCKER — Hard tenant boundary is not frozen for several polymorphic relationships + +**Location** + +- `contracts/kanban-schema.v1.ts:315-318` and `475-478` — project/task accountable owners are unvalidated `(kind, text id)` pairs. +- `contracts/kanban-schema.v1.ts:659-663` — assignment principals are unvalidated `(kind, text id)` pairs. +- `contracts/kanban-schema.v1.ts:758` and `841` — checkpoint/evidence artifact relationships are JSON arrays without workspace-aware FKs. +- `SHARED-CONTRACT.md:89-96` — only selected polymorphic checks are delegated to domain transactions; owner/principal/evidence checks are not included. +- `REQUIREMENTS.md:101-108` — every relationship must reject cross-workspace IDs. + +**Violation** + +Fixed invariant 7 / `REQ-TEN-001` and `REQ-ID-001`. The frozen schema can name a team or agent from another workspace as owner/assignee, and can embed foreign-workspace artifact IDs in checkpoint or approval evidence arrays. A global user ID is also insufficient without active workspace membership validation. + +**Minimal fix** + +Use workspace-aware owner/assignment join tables or separate nullable user/team/agent columns with exactly-one checks and composite FKs where possible. Model checkpoint/evidence artifact links as workspace-scoped join rows, or freeze explicit transaction checks for every ID. Require active workspace membership for user principals and workspace-agent/session consistency for agent principals. Add DB/repository/API/Coordinator cross-workspace negative tests without existence oracles. + +### KCR-006 — BLOCKER — Post-recovery outage proposals have no canonical persistence or command surface + +**Location** + +- `REQUIREMENTS.md:93-99` — proposal submission and authorized accept/reject are required. +- `SHARED-CONTRACT.md:26-29` — outage notes may return only as authenticated proposals. +- `SHARED-CONTRACT.md:243-267` — the thin command/query contract contains no proposal submit/get/accept/reject operations. +- `contracts/kanban-schema.v1.ts:1-916` — no proposal table captures proposed command, target/version, attribution, lifecycle, or decision. +- `TASKS.md:99-108` — KBN-110 does not own an outage-proposal command path. + +**Violation** + +Fixed invariant 4 / `REQ-SOT-004`. An implementation lane would have to invent storage or misuse artifacts/approval gates. Either path risks silently applying an outage note or creating shadow state. + +**Minimal fix** + +Add a workspace-scoped `change_proposals`/`outage_proposals` contract with authenticated proposer, source note digest, target aggregate, expected version, proposed typed command/payload, pending/accepted/rejected state, decision actor/reason/time, idempotency key, and audit linkage. Add explicit submit/query/accept/reject Gateway commands. Acceptance must execute the normal command in a healthy transaction; a proposal itself can never claim, order, satisfy a gate, or mutate the target. + +### KCR-007 — BLOCKER — Parallel slice ordering is not freeze-safe and contains a direct lane-order contradiction + +**Location** + +- `TASKS.md:43-60` — dependency graph makes KBN-010 and KBN-100 siblings. +- `TASKS.md:88-97` — KBN-100 nevertheless depends on KBN-010 threat findings that alter constraints. +- `SHARED-CONTRACT.md:243` and `INDEX.md:44-50` — exact route names/DTO placement remain unresolved. +- `TASKS.md:110-130` — KBN-120/130 depend on a frozen endpoint/DTO contract, while mocks may begin before KBN-110 lands. +- `TASKS.md:145-153` — KBN-200 says lane-serial after KBN-120. +- `TASKS.md:248-254` — wave table runs KBN-200 before KBN-120. + +**Violation** + +Charter C and the mandatory freeze-before-parallelize gate. Schema can begin before tenant/threat findings are complete; web/CLI consumers have only semantic operations, not exact DTO/endpoint contracts; coder4 has two opposite legal orders. This does not create same-file edits immediately, but it guarantees contract invention or rework across active lanes. + +**Minimal fix** + +1. Make KBN-010 (or an explicit constraint-impact gate from it) a completed prerequisite of KBN-100. +2. Add a small serialized KBN-105 endpoint/DTO/endpoint-registry freeze, with exact request/response/error DTOs, before KBN-120 and KBN-130 implementation. +3. Choose one coder4 lane order and use it consistently in slice text, graph, and wave table. +4. Name the exact MCP-owned files or assign their Gateway changes to coder3 before coder4 starts. + +### KCR-008 — BLOCKER — Claimed P0 migration map is absent; concrete N-1 hazards remain unresolved + +**Location** + +- `MISSION-MANIFEST.md:153-157` — P0 says to publish a migration map and states the build hold is lifted at line 3. +- `SHARED-CONTRACT.md:101-121` — only generic expand/backfill/contract rules are supplied. +- `contracts/kanban-schema.v1.ts:1-916` — target-state declarations reuse live table names and make target fields required. +- Current foundation evidence: `origin/main:packages/db/src/schema.ts:120-301` has no workspace keys, nullable project/mission links, legacy text status vocabularies, `tasks.tags`, `tasks.assignee`, `tasks.due_date`, mission JSON milestones/config, `mission_tasks.status`, and legacy agent fields. + +**Violation** + +Charter D / `REQ-MIG-001` and the P0 exit claim. The generic rule is correct, but coder2 lacks the required field-by-field transition map. A direct Drizzle reconciliation could attempt type narrowing/status conversion, add required workspace/project/owner columns too early, or drop legacy columns before N-1 readers and writers are retired. + +**Minimal fix** + +Publish a concrete current-main delta map before lifting the hold. For each existing table/column, specify expand, backfill, compatibility read/write, switch, and contract release. At minimum cover: + +- nullable-first `workspace_id`, required project/owner fields, and workspace backfill; +- legacy task/project/mission status aliases or shadow columns before v1 emission; +- `mission_tasks.status` read retirement and write-source prohibition; +- mapping/retention for tags, assignee, due date, mission description/config/milestones, and agent fields; +- current milestone circular FK ordering; +- empty, production-shape, partial-resume, and rollback/downgrade tests already named in §4. + +Explicitly require legacy columns to remain in the unified Drizzle declaration during the expand/N-1 window. + +### KCR-009 — MAJOR — Dependency uniqueness permits parallel duplicate edges + +**Location** + +- `contracts/kanban-schema.v1.ts:561-567` — unique key includes `dependencyType`. +- `SHARED-CONTRACT.md:47-49` — calls for a unique directed edge. +- `REQUIREMENTS.md:142-149` — duplicate edge attempts must fail. + +**Violation** + +`REQ-DEP-001`. The same predecessor/successor pair can be inserted three times, once per dependency type. That is not a unique directed edge and complicates readiness semantics. + +**Minimal fix** + +Make `(workspace_id, predecessor_task_id, successor_task_id)` unique independent of type, or explicitly redefine the requirement as one edge per type and freeze deterministic multi-edge completion semantics. The source plan says unique directed edge, so the former is the minimal faithful fix. + +### KCR-010 — MAJOR — Same-workspace planning relationships can contradict the project hierarchy + +**Location** + +- `contracts/kanban-schema.v1.ts:325` — `projects.currentMilestoneId` has no FK in the declaration. +- `contracts/kanban-schema.v1.ts:427-448` — mission/milestone association checks workspace but not common project. +- `contracts/kanban-schema.v1.ts:490-516` — a task’s project, mission, milestone, and parent only need share a workspace, not a project. +- `contracts/kanban-schema.v1.ts:905-908` — only current milestone is mentioned as a deferred invariant. + +**Violation** + +`REQ-PLAN-001` and schema correctness. A task in project A can point to a mission/milestone/parent task from project B in the same workspace. A mission can associate a milestone from another project despite having one required project. + +**Minimal fix** + +Add project-congruent composite keys/FKs (or freeze mandatory transaction checks) for task→mission, task→milestone, task→parent, mission→milestone, and project→current milestone. Add same-workspace/same-project negative tests. + +### KCR-011 — MAJOR — Immutable/append-only records can be erased by parent cascades + +**Location** + +- `contracts/kanban-schema.v1.ts:798-818` — `task_events` is described as append-only but remains under a workspace cascade. +- `contracts/kanban-schema.v1.ts:911` — only application-role UPDATE/DELETE privilege removal is stated. +- Numerous canonical relationships use `onDelete('cascade')`, including workspace roots and artifact/checkpoint/event owners. +- `REQUIREMENTS.md:41-43` and `154-170` — audit must be append-only, attributable, and reconstructable. + +**Violation** + +`REQ-AUD-001`. Revoking direct DELETE on `task_events` does not prevent a parent delete from cascading into the audit log. Checkpoints and immutable artifacts also lack explicit append-only privilege/retention semantics. + +**Minimal fix** + +Use lifecycle/archive states and `RESTRICT` for canonical parent deletion during normal operation. Freeze a separate, audited retention/break-glass purge procedure. Apply INSERT/SELECT-only or equivalent immutability controls to task events, checkpoints, and immutable artifacts, and test that parent deletion cannot silently erase them. + +### KCR-012 — MAJOR — Coordinator persistence lacks durable quarantine/retry state and DTO/schema alignment + +**Location** + +- `contracts/mechanical-coordinator.v1.ts:239-244` — expiry returns `quarantined` IDs. +- `contracts/kanban-schema.v1.ts:457-490` — task has only untyped `retryPolicy` metadata and no quarantine/execution disposition. +- `contracts/mechanical-coordinator.v1.ts:173` — `evidenceIds` has no corresponding evidence table/type; schema has artifacts. +- `contracts/kanban-schema.v1.ts:479`, `663`, and agent role JSON — specialist roles are free text despite the frozen role vocabulary in `mechanical-coordinator.v1.ts:19-29`. + +**Violation** + +`REQ-COORD-004` and internal consistency. PostgreSQL cannot deterministically reconstruct why/when a task was quarantined, its bounded retry state, or which typed evidence was submitted. Free-text roles allow the schema and engine to disagree. + +**Minimal fix** + +Freeze a durable execution/retry/quarantine record (attempt count, next eligibility, terminal reason, actor/policy, timestamps, version) or typed task columns with events. Align `evidenceIds` to artifact IDs or add a real evidence entity. Use one specialist-role enum/check across tasks, assignments, agents/sessions, DTOs, and Coordinator. + +### KCR-013 — MAJOR — Thin MVP promises task archive and tag filtering without target-state semantics + +**Location** + +- `REQUIREMENTS.md:182-193` — users must archive tasks and filter by tags. +- `SHARED-CONTRACT.md:252-267` — mutations include cancel but not archive task. +- `contracts/kanban-schema.v1.ts:457-490` — no task archive field and no typed tags field/table. +- Current `origin/main` already has `tasks.tags`, making omission from the target declaration a migration-loss hazard. + +**Violation** + +`REQ-UI-001/002` and internal acceptance consistency. “Archive” cannot be implemented without inventing whether it means cancelled, hidden, or soft-deleted; tag filtering has no frozen storage/query contract. + +**Minimal fix** + +Either remove task archive/tag acceptance from P1, or add explicit non-lifecycle archival semantics (`archived_at/by/reason`) and a workspace-safe tags model/query contract. Preserve/migrate the current tags column until the selected model is live. + +### KCR-014 — MAJOR — Recovery contract states critical rules only in comments and has no owning implementation slice + +**Location** + +- `contracts/recovery-posture.v1.ts:97-147` — exported JSON Schema validates only local field shapes. +- `contracts/recovery-posture.v1.ts:150-156` — PITR/WAL, effective RPO, off-cluster, high-assurance minima, and audit rules are comments only. +- `REQUIREMENTS.md:270-277` — parser rejection of impossible combinations is acceptance-critical. +- `TASKS.md:75-244` — no bounded slice owns recovery config parsing, override audit, backup/WAL setup, or restore/break-glass evidence. + +**Violation** + +`REQ-REC-001`. A consumer using the advertised JSON Schema can accept weakened high-assurance values, PITR without WAL, or an impossible RPO. The task plan has no lane accountable for closing that acceptance criterion. + +**Minimal fix** + +Export a normative `validateRecoveryPostureV1`/schema refinement with machine-testable cross-field checks and add a bounded Infra/recovery slice (serialized if it touches shared config) owning config parsing, override audit, mechanism verification, restore test, and break-glass evidence. Recovery config must continue to expose no authority/gate knobs. + +### KCR-015 — MAJOR — Pure Coordinator slice cannot implement two frozen methods without persistence access + +**Location** + +- `contracts/mechanical-coordinator.v1.ts:259-263` — `explainEligibility` receives only `taskId`, not a structured snapshot. +- `contracts/mechanical-coordinator.v1.ts:289-293` — `recoverFromPostgres` explicitly reads PostgreSQL. +- `TASKS.md:145-153` — KBN-200 is a pure engine with no SQL, Drizzle, Gateway, or Valkey. +- `TASKS.md:157-164` — persistence belongs to coder3/KBN-210. + +**Violation** + +Charter C and internal consistency. coder4 cannot implement the frozen port in a pure package without crossing coder3’s persistence boundary. If coder3 implements the port instead, KBN-200’s acceptance and ownership are misassigned. + +**Minimal fix** + +Split the contract into a pure decision engine that receives complete immutable snapshots and a persistence/orchestration service port implemented by KBN-210. Move `recoverFromPostgres` and ID-based loading to the adapter/service; make pure explanation accept a snapshot. + +### KCR-016 — MINOR — Health denial code/state pairs are not correlated by type + +**Location** + +- `contracts/health-state.v1.ts:35-61` — either denial code can pair with either degraded state. +- `SHARED-CONTRACT.md:188-190` — prose defines `KANBAN_WRITE_UNAVAILABLE` specifically for `write-unavailable`. + +**Violation** + +Health contract precision. A client can receive a semantically inconsistent authoritative body even after KCR-001’s broader state fix. + +**Minimal fix** + +Make deliberate denial a two-variant union with exact code/state pairing. + +--- + +## Clean checks / invariants that do hold + +The review did **not** find a gap in these areas: + +- The canon consistently selects current `mosaicstack/stack` + Drizzle/PostgreSQL and rejects greenfield/Prisma revival. +- Every artifact states PostgreSQL is the sole writable SOT and Valkey/files are non-authoritative. +- Generated `TASKS.md`, `mission.json`, and exports are consistently declared read-only and never import sources. KBN-300’s importer is scoped to immutable legacy JSON/Vikunja snapshots, not generated projections. +- Recovery config exposes recovery fields only; it contains no direct fail-open, SOT, Coordinator-authority, or gate-waiver knob. +- The Coordinator interface contains no `createTask`, acceptance-edit, gate-waive, certify, merge, release, or provider-close method. `submitForReview` is type-limited to `in_review`, not `done` or `certified`. +- Certifier is consistently final independent gate with no merge authority. +- The seven canonical task status values match across requirements, shared prose, schema, and Coordinator’s ready/in-review surfaces. +- One-active-lease partial uniqueness, no-self-edge, outbox aggregate-revision/event-type uniqueness, optimistic task/project/mission/milestone versions, and N-1 test categories are explicitly present. +- The file-tree partition is mostly well separated once the ordering/freeze defects in KCR-007 are corrected. + +## Required re-review scope + +After remediation, re-review at minimum: + +1. health/coordinator discriminated unions and mutation-time health proof; +2. proposal/approval/assignment/lease relational model; +3. monotonic fencing and composite bindings; +4. tenant-safe polymorphic relationships; +5. outage-proposal persistence and commands; +6. concrete current-main migration map; +7. corrected dependency graph and exact API/DTO freeze; +8. recovery validator/owner slice; +9. all schema and DTO vocabulary alignment. + +## Overall verdict + +**NO-GO — 8 BLOCKERs must be resolved before the v1 contract is frozen or parallel implementation begins.** diff --git a/docs/reports/native-kanban-sot/ultron-final-go.md b/docs/reports/native-kanban-sot/ultron-final-go.md new file mode 100644 index 0000000..92604f7 --- /dev/null +++ b/docs/reports/native-kanban-sot/ultron-final-go.md @@ -0,0 +1,38 @@ +# #751 Native Kanban/SOT canonical publication — Ultron final gate + +**Verdict: GO** — zero BLOCKER/HIGH findings. + +## Scope / integrity + +- Reviewed `/home/hermes/agent-work/stack-kanban-canon` staged delta only: exactly 16 documentation/contract artifacts; no unstaged delta; `git diff --cached --check` passes. +- This is a publication canon, not a runtime implementation. The explicit implementation hold prevents feature work until canon merge and prerequisite release (`docs/requirements/native-kanban-sot.md:8-9`; `docs/native-kanban-sot/TASKS.md:45-67`). + +## Acceptance mapping and findings + +| Requirement area | Final evidence / result | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Sole PostgreSQL SOT, generated projections, outage proposals | Requirements D3/D4 and fixed invariants prohibit alternate writers and import (`docs/requirements/native-kanban-sot.md:22-23,32-44`). Health contract keeps public observation separate from branded transaction-local proof (`contracts/health-state.v1.ts:44-84`) and freezes 503/409/502/504 mappings (`:91-184`). Proposal table uses workspace-aware event FKs (`contracts/kanban-schema.v1.ts:847-908`); exact submission/acceptance transaction semantics are specified (`SHARED-CONTRACT.md:81-89`). PASS. | +| Workspace tenancy, planning, assignments, evidence | Workspace-composite task and proposal relations plus active-member rules are explicit (`SHARED-CONTRACT.md:40-48`; `kanban-schema.v1.ts:587-637,875-908`). Lease/checkpoint relations bind workspace/task/assignment/session/fence, with one active lease and bigint fencing (`:1062-1114`). PASS. | +| Coordinator, gates, concurrency/recovery | Pure Coordinator has snapshot-only decision methods (`mechanical-coordinator.v1.ts:186-198`); persistence port owns locked ID validation and recovery (`:371-407`). Requirements forbid Coordinator scope/gate/certification/merge authority and Certifier merge authority (`requirements:39-40`; `MISSION-MANIFEST.md` authority table). Recovery validator rejects unknown fields, PITR/WAL/RPO/storage/high-assurance violations (`recovery-posture.v1.ts:193-369`). PASS. | +| Migration/N-1/API/task decomposition | N-1 expand/backfill/compatibility/switch/contract order and proposal DDL sequence are concrete (`SHARED-CONTRACT.md:69-115`). Frozen exact Gateway/DTO registry and non-overlapping lane ownership/prerequisites are present (`SHARED-CONTRACT.md:244-282`; `TASKS.md:45-67,81-259`). PASS. | +| Documentation / seven owner decisions / evidence | D1–D7 are all explicitly ratified (`requirements:20-26`); all 26 REQ sections contain acceptance criteria. Index/manifest/task graph link requirements, frozen contracts, ownership, and evidence. Relative-link audit passes. PASS. | + +## Independent verification performed + +```text +git diff --cached --check PASS +./node_modules/.bin/prettier --check PASS +./node_modules/.bin/tsc --noEmit --strict PASS +Python relative Markdown link audit PASS (0 errors) +Python requirement acceptance audit PASS (26 requirements; 0 missing acceptance sections) +Static staged scope/status check PASS (16 staged docs-only; no unstaged delta) +``` + +The full schema-contract strict type check cannot resolve `drizzle-orm` from this docs-only worktree; this is an environment dependency-resolution limitation, not a contract diagnostic. Independent external publication validation and final re-review record the strict all-four-contract check against the current Stack Drizzle toolchain as PASS. + +## Residual items + +- **LOW:** implementation must deliver the declared KBN-100/KBN-110/KBN-140 proposal-event-chain, tenant, failure-mapping, and SecReview evidence before P0/P1 release. This is a forward implementation obligation already frozen in the canon, not a publication defect. +- **LOW:** selected infrastructure backup provider/recovery tier and migration/cutover thresholds remain owner-controlled implementation decisions, bounded by the normative recovery contract and change control. + +No source, staging, commit, provider, CI, or deployment state was mutated. diff --git a/docs/requirements/native-kanban-sot.md b/docs/requirements/native-kanban-sot.md new file mode 100644 index 0000000..291f579 --- /dev/null +++ b/docs/requirements/native-kanban-sot.md @@ -0,0 +1,368 @@ +# Native Kanban and Canonical Task SOT — Canonical Requirements + +**Status:** RATIFIED and independently approved for canonical publication under issue [#751](https://git.mosaicstack.dev/mosaicstack/stack/issues/751) +**Date:** 2026-07-14 +**Decision owner:** Jason +**Publication owner:** web1 control plane (`mos-claude`; `mosaic-100` acting during Claude quota outage) +**Implementation foundation:** current `mosaicstack/stack` main only +**Implementation hold:** no feature implementation begins until this canon is squash-merged to `main` with terminal-green CI. + +## 1. Purpose + +Deliver Mosaic Stack's native project/task control plane and thin writable Kanban on one authoritative PostgreSQL model. This document formalizes the ratified source plan; it does not create a parallel design. + +Normative terms **MUST**, **MUST NOT**, **SHOULD**, and **MAY** are binding as used here. + +## 2. Ratified decisions + +| # | Ratified decision | Canonical result | +| --- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| D1 | Foundation | Extend current `mosaicstack/stack` main with its existing Drizzle/PostgreSQL, NestJS Gateway, Next.js, Better Auth, and Valkey/BullMQ conventions. No greenfield service and no Prisma revival. | +| D2 | Tenant boundary | `workspace_id` is the hard tenant boundary from the first migration. Teams are authorization groups inside a workspace, never tenant substitutes. | +| D3 | Outage authority — Option A with amendment | PostgreSQL is the sole writable SOT and mutations fail closed whenever DB write-health cannot be proven. The amendment permits deployment-specific **recovery posture only**; it does not permit an alternate writer. Human outage notes are attributable post-recovery proposals, never shadow state. | +| D4 | Generated files | `TASKS.md`, `mission.json`, and any file export are generated, read-only, non-authoritative, and never import sources. Generate on demand; commit only where repository review policy requires a snapshot. | +| D5 | Status model | Task statuses are `backlog`, `ready`, `in_progress`, `blocked`, `in_review`, `done`, `cancelled`. Runtime readiness is orthogonal and computed. | +| D6 | Coordinator approval | Hybrid: manual Project Sub-Orchestrator approval by default; automatic routing only under an explicit, approved, versioned low-risk policy. | +| D7 | Initial migration scope | Project, mission, milestone, task, tags/archive, dependency, assignment, outage proposal, evidence/link, and orchestration state only. Calendar, email, GLPI cache, and personal-brain features remain out of scope. | + +## 3. Fixed invariants — every deployment + +These are not tier settings and cannot be weakened by deployment configuration. + +1. PostgreSQL is the **sole writable source of truth**. +2. The implementation uses Drizzle on current stack main. +3. Kanban and orchestration mutations **fail closed** unless DB write-health is positively proven `healthy`. +4. No failed mutation is redirected to Markdown, JSON, browser storage, Valkey, queue payloads, scratchpads, or provider issues. +5. `TASKS.md` and all file exports are generated, read-only, non-authoritative, and never parsed for import. +6. Human notes created during an outage become attributable proposals only after recovery. They do not reserve work, change status, satisfy a gate, or establish ordering. +7. Valkey is derived, expendable coordination infrastructure. PostgreSQL retains task truth, leases, fencing, audit, and the transactional outbox. +8. The Mechanical Coordinator is non-LLM and deterministic. It may evaluate eligibility, dependencies, approval policy, leases, fencing, heartbeat, retry, expiry, and quarantine. It cannot invent scope, alter acceptance criteria, waive gates, certify, or merge. +9. **Certifier** is the final independent quality-gate role. Certifier may pass, reject, or escalate with evidence; it has no merge authority. +10. Every business and orchestration record is workspace-scoped; cross-workspace relationships are rejected. +11. Every mutation is idempotent and expected-version checked where it changes an aggregate. +12. Stale worker mutations are rejected by monotonically increasing fencing tokens. +13. Audit events are append-only and attributable; authoritative state is reconstructable from PostgreSQL without Valkey or files. + +## 4. Configurable recovery posture only + +Deployment tiers configure durability and operational recovery targets. They never configure SOT authority, fail-open writes, or gate bypass. + +### 4.1 Tier defaults + +| Setting | Lite | Standard | High-assurance | +| --------------------------- | --------------------------------------: | ------------------------------------------------------------: | ----------------------------------------------------------------------: | +| Target RPO | 24 hours | 1 hour | **15 minutes** | +| Target RTO | 24 hours | 8 hours | **4 hours** | +| Base backup cadence | Daily | Daily | **Daily** | +| WAL archive cadence | Disabled | Every 15 minutes | **Every 5 minutes** | +| PITR retention | 0 days / disabled | 14 days | **35 days** | +| Restore test frequency | Quarterly | Quarterly | **Monthly** | +| Break-glass drill frequency | Annually | Semiannually | **Quarterly** | +| Off-cluster storage | One encrypted off-cluster backup target | Encrypted off-cluster object storage, separate failure domain | **Encrypted off-cluster base backups and WAL, separate failure domain** | + +A deployment MAY override defaults only through the validated recovery-posture contract. An override MUST record actor, reason, effective time, and policy revision. A claimed RPO MUST be no smaller than the actual backup/WAL mechanism can support. Enabling PITR requires WAL archival and off-cluster storage. + +## 5. Functional requirements and acceptance criteria + +### REQ-SOT-001 — Sole writable PostgreSQL authority + +**Requirement:** All project, mission, milestone, task/tag/archive, dependency, assignment, execution/quarantine, lease, checkpoint, approval, outage proposal, event, link, artifact, and outbox mutations MUST commit through Gateway domain services into PostgreSQL. + +**Acceptance:** + +- Mutation journey tests show web, CLI, MCP, and agents invoke typed Gateway commands. +- Static/process inventory finds no file, Valkey, browser, or provider issue writer acting as canonical state. +- PostgreSQL state survives Valkey loss and reconstructs the same aggregate revisions. + +### REQ-SOT-002 — Fail-closed mutation health + +**Requirement:** A mutation MUST execute only while health state is `healthy`. `read-only-degraded` and `write-unavailable` MUST return the frozen deliberate-denial error contract and MUST NOT enqueue a hidden write. + +**Acceptance:** + +- Public health response is a discriminated union; contradictory state/proof combinations fail contract validation. +- Mutation methods accept only a fresh internal PostgreSQL transaction-local write proof, never caller-asserted/public health state. +- Negative tests reject expired proofs, policy-revision mismatch, Valkey-only liveness, and caller-forged `healthy`. +- Fault tests force both degraded states and prove row counts, outbox, files, and Valkey remain unchanged. +- Exact failure mapping proves authoritative 503 denial, retryable 502/504/timeout uncertainty, and 409 version conflict cannot cross-map. +- Replaying the same idempotency key after recovery returns one canonical result. + +### REQ-SOT-003 — Generated projections + +**Requirement:** `TASKS.md`, `mission.json`, and other exports MUST contain a non-authoritative header, workspace/project IDs, generated time, and source revision. No production parser may mutate DB from an export. + +**Acceptance:** + +- Generated output matches the API snapshot revision. +- Hand editing a projection fails CI validation or is overwritten by regeneration. +- Repository search finds no import path from generated projections. + +### REQ-SOT-004 — Attributable outage proposals + +**Requirement:** Human outage notes MAY be captured outside the system but, after recovery, can enter Mosaic only through workspace-scoped `change_proposals` attributed to an authenticated active member. A proposal stores source-note digest, target aggregate/version, typed command/payload, idempotency, lifecycle, decision actor/reason/time, and audit links. It MUST NOT silently change canonical state. + +**Acceptance:** + +- `(workspace_id, submitted_audit_event_id)` and `(workspace_id, accepted_command_audit_event_id)` are composite foreign keys to `task_events(workspace_id, id)`; missing and foreign-workspace event IDs fail before commit. +- Submission preallocates the proposal ID and atomically inserts `change_proposal.submitted` for that exact workspace/proposal with the new proposal referencing it. +- Accept locks proposal and target, obtains fresh write proof, checks expected version, executes the normal typed command, and atomically links that command's event for the same workspace/target and proposal causation. +- Negative tests reject missing submission events, foreign-workspace submission/acceptance events, and same-workspace events for an unrelated proposal, aggregate, target, or command. +- Tests prove a pending/rejected proposal cannot claim/order work, satisfy a dependency/gate, or mutate any target directly. + +### REQ-TEN-001 — Workspace hard tenancy + +**Requirement:** Every canonical business/orchestration row MUST carry `workspace_id`. Workspace-aware constraints and authorization MUST prevent cross-tenant relationships and reads/writes. + +**Acceptance:** + +- API, repository, import, WebSocket, and Coordinator negative tests reject foreign-workspace IDs without existence oracles. +- Project/task owners use exactly-one user/team references; assignment principals use exactly-one user/team/agent reference; agent/session targets are workspace-consistent. +- User owners, principals, proposers, and decision actors require ACTIVE workspace membership in the authoritative transaction. +- Dependency, project hierarchy, assignment, lease, checkpoint, approval-evidence, link, artifact, proposal target, and both proposal-audit-event composite relationships reject mixed workspaces. +- Tenant context is derived from authenticated authority, never accepted blindly from request data. + +### REQ-ID-001 — Workspace identity and service scope + +**Requirement:** Users, teams, agents, and agent sessions MUST be bound to a workspace with explicit role/capability scope. Agents MUST NOT receive raw DB credentials. + +**Acceptance:** + +- Workspace membership and service-identity tests enforce command-family scope. +- Revoked/disabled agents and ended sessions cannot claim, heartbeat, or submit. + +### REQ-PLAN-001 — Normalized planning hierarchy + +**Requirement:** Canonical planning entities are projects, milestones, missions, mission-milestone associations, and tasks. A task belongs to one required project and at most one mission/milestone/parent task. + +**Acceptance:** + +- CRUD tests preserve workspace, hierarchy, versions, and lifecycle constraints. +- Mission membership does not duplicate task status. +- Composite project-congruent constraints reject task→mission, task→milestone, task→parent, mission→milestone, and project→current-milestone mismatches. +- Parent and association constraints reject cycles/orphans where applicable. + +### REQ-TASK-001 — Canonical task fields + +**Requirement:** Tasks MUST support title, description, structured acceptance criteria, canonical status, priority, fractional board rank, accountable owner, assigned specialist role, due/not-before dates, estimate, progress, explicit blocker, retry policy, normalized workspace tags, non-lifecycle archival (`archived_at/by/reason`), metadata, monotonic fencing counter, and optimistic version. + +**Acceptance:** + +- API and UI round-trip every field without silent loss. +- Current `tasks.tags`, `assignee`, and `due_date` remain declared/preserved during N-1 and backfill to the canonical model without loss. +- Archive hides work without changing its canonical lifecycle status and requires actor/reason/time. +- Invalid status, rank, progress, date, owner, tag, archive, or retry data is rejected. +- Concurrent expected-version updates produce a visible conflict. + +### REQ-TASK-002 — Fixed lifecycle and computed readiness + +**Requirement:** Human workflow status MUST use the seven ratified values. Dependency/schedule/policy/lease/retry conditions MUST be exposed as computed readiness, not hidden status rewrites. + +**Acceptance:** + +- A dependency becoming incomplete changes readiness but does not silently rewrite the Kanban column. +- Readiness explanation identifies all active gates. +- State-machine tests reject illegal transitions and require reasons for blocked/cancelled paths. + +### REQ-DEP-001 — Dependency DAG + +**Requirement:** Workspace-local directed dependencies MUST be unique and acyclic. A task is dependency-eligible only after every blocking predecessor is `done` and completion conditions pass. + +**Acceptance:** + +- `(workspace_id, predecessor_task_id, successor_task_id)` is unique independent of dependency type. +- Cycle, duplicate, self-edge, and cross-workspace attempts fail before commit. +- Property/concurrency tests prove all blocking predecessors are evaluated. +- UI displays dependency and readiness errors accessibly. + +### REQ-ASN-001 — Assignment is not a lease + +**Requirement:** Assignment history and execution leases MUST be separate records. One persisted assignment identity freezes task version, exact target agent/session (or exactly-one non-agent principal), specialist role, expiry, state, policy revision, proposer, reason, and timestamps. Approval decisions relate to that assignment with workspace-aware constraints. + +**Acceptance:** + +- One assignment-state vocabulary is identical across schema, DTO, and engine. +- Lease acquisition accepts IDs only, then reloads and locks assignment, approval, task, and target session to verify workspace, current task version, exact target, state, expiry, and policy revision. +- Reassignment preserves history; assignment may exist without a lease; lease expiry does not erase ownership/evidence. + +### REQ-AUD-001 — Semantic audit and outbox + +**Requirement:** Mutating commands MUST append semantic `task_events` with actor, correlation, causation, idempotency key, and aggregate versions in the same transaction as state. Notifications MUST flow from a transactional outbox. + +**Acceptance:** + +- Atomicity tests prove state/event/outbox commit or roll back together. +- Proposal submission and acceptance tests prove their workspace-bound event links identify the exact submission and executed normal command, not merely an existing event UUID. +- Duplicate idempotency keys return the prior result without duplicate events. +- `task_events`, checkpoints, immutable artifacts, and evidence joins are INSERT/SELECT-only for application roles; parent hard deletes are RESTRICTed. +- Normal lifecycle uses archive/cancel, never hard delete; retention purge requires audited break-glass authority and evidence. +- Valkey outage leaves outbox pending and later replayable. + +### REQ-API-001 — Typed Gateway command boundary + +**Requirement:** Gateway MUST expose workspace-safe project/task/dependency/assignment/link/artifact/change-proposal queries and explicit lifecycle commands. Generic patching MUST NOT bypass claim, heartbeat, review, certify, proposal acceptance, or completion invariants. + +**Acceptance:** + +- KBN-105 freezes exact route, request, success, denial, conflict, and transport-normalization DTOs before CLI/web implementation. +- DTO validation, authorization, contract, and integration tests cover each command. +- Exact MCP-owned Gateway files are coder3-owned; coder4 consumes only frozen Gateway contracts. +- Endpoint registry aligns web, CLI, MCP, and generated client paths. +- Direct SQL and raw Valkey writes are absent from clients. + +### REQ-UI-001 — Writable thin Kanban/List MVP + +**Requirement:** Existing Tasks and Projects surfaces MUST become a real-data writable MVP with one shared query contract. + +**Acceptance:** + +- Users can create/edit/cancel/archive tasks, open task detail, and move cards within/across columns. +- Server validates transition and persists fractional board rank. +- Refresh, reconnect, CLI, MCP, and generated projection show the same revision. + +### REQ-UI-002 — Tenant and work context + +**Requirement:** UI MUST show workspace context and support filters for project, mission, milestone, status, priority, owner/specialist, due state, and tags. + +**Acceptance:** + +- Context is visible on every mutation surface. +- Filter tests cannot expose foreign-workspace data. +- Empty/loading/error states are explicit. + +### REQ-UI-003 — Dependency, ownership, lease, and audit visibility + +**Requirement:** Task detail MUST separate accountable owner, specialist assignment, active session/lease expiry, dependencies/readiness, acceptance criteria, blocker, external links, and audit timeline. + +**Acceptance:** + +- Each concept renders from its canonical endpoint. +- A lease is never displayed as ownership or completion. +- Conflict and stale-reconnect states require refresh rather than silent overwrite. + +### REQ-UI-004 — Accessible interaction + +**Requirement:** Kanban MUST support keyboard-accessible moves, non-drag alternatives, responsive layout, and semantic status/error announcements. + +**Acceptance:** + +- Keyboard journey performs every card transition available by drag. +- Automated accessibility checks and manual responsive checks pass. + +### REQ-COORD-001 — Non-LLM Mechanical Coordinator + +**Requirement:** Coordinator decisions MUST be deterministic from structured data and versioned policy. It MUST NOT invoke an LLM to interpret scope or acceptance criteria. + +**Acceptance:** + +- Pure decision engine receives complete immutable snapshots and performs no ID loading, SQL, Gateway, Valkey, or recovery I/O. +- Persistence/service adapter owns ID loading, transaction-local write proof, locking, persistence, and `recoverFromPostgres`. +- Same snapshot and policy revision produce the same eligibility/order explanation. +- Dependency, schedule, durable retry/quarantine, approval, role, and capacity inputs are auditable. +- Code/config inspection finds no model/provider dependency in the scheduling engine. + +### REQ-COORD-002 — Eligibility and approval routing + +**Requirement:** Only `ready` tasks under active project/mission, passed dependencies/schedule/retry/release policy, and without active lease may be proposed. Manual approval is default; auto-route requires an explicit approved policy revision. + +**Acceptance:** + +- Unapproved or gated tasks are never leased. +- Every persisted assignment proposal includes task version, exact target agent/session, expiry, state, deterministic reasons, and policy revision. +- Approval is relationally bound to the assignment identity and cannot be supplied as a forgeable proof-by-value DTO. +- Override/reject/reassign requires an attributable reason. + +### REQ-COORD-003 — Atomic lease, heartbeat, fencing, and recovery + +**Requirement:** Lease acquisition MUST be atomic in PostgreSQL, permit at most one active lease per task, atomically increment the durable per-task fencing counter under task lock, use bigint-safe tokens, require timely acknowledgement/heartbeat, and reject stale workers. Lease and checkpoint relations MUST bind the exact workspace+task+assignment/session+fence. + +**Acceptance:** + +- Concurrent claim tests yield one winner and strictly increasing fencing tokens. +- Lower/expired tokens and mismatched same-workspace task/assignment/lease/checkpoint IDs fail. +- Token values round-trip as bigint/decimal strings without JavaScript precision loss. +- Coordinator restart reconstructs lease/retry/quarantine state from PostgreSQL alone. + +### REQ-COORD-004 — Retry and quarantine + +**Requirement:** Missing acknowledgement, agent loss, or execution failure MUST produce a deterministic release, bounded backoff retry, or quarantine outcome according to retry policy. Ambiguous/non-idempotent work requires Sub-Orchestrator action. + +**Acceptance:** + +- Durable execution state records disposition, attempt/max, next eligibility, terminal reason, actor/policy, timestamps, and version. +- Retry budget/backoff are bounded and tested. +- Exhausted or non-idempotent failures quarantine with workspace-scoped artifact evidence. +- One specialist-role vocabulary is enforced across schema, sessions, assignments, DTOs, and engine. +- No task loops indefinitely or silently returns to ready. + +### REQ-GATE-001 — Role and authority chain + +**Requirement:** Canonical flow is User → Interaction → Portfolio Orchestrator → Project Sub-Orchestrator → Gateway → domain services → Mechanical Coordinator → specialists → Certifier. + +**Acceptance:** + +- Role bindings and approvals are queryable and audited. +- Coordinator cannot create scope or waive gates. +- Certifier cannot merge or close provider artifacts. + +### REQ-GATE-002 — Independent review and certification + +**Requirement:** Author and reviewer MUST differ. Auth, security, tenant, secrets, and data-integrity surfaces MUST receive mandatory SecReview. Certifier is the final quality gate after remediation. + +**Acceptance:** + +- Gate tests reject author self-review and missing required SecReview. +- Certifier receives complete traceability/evidence and returns pass/reject/escalate. +- A Certifier pass does not grant merge authority. + +### REQ-REC-001 — Recovery posture validation + +**Requirement:** A deployment MUST select a validated Lite, Standard, or High-assurance posture and MAY override only recovery knobs. + +**Acceptance:** + +- Runtime invokes normative `validateRecoveryPostureV1`, not shape-only JSON Schema validation. +- Validator rejects PITR/WAL mismatch, impossible RPO, unknown fields, non-encrypted/non-separated storage, and weakened High-assurance values. +- A bounded recovery/infra slice owns parser wiring, override audit, mechanism verification, restore test, and break-glass evidence. +- High-assurance defaults equal RPO 15m/RTO 4h, encrypted off-cluster WAL every 5m, 35d PITR, daily base backup, monthly restore test, and quarterly break-glass. + +### REQ-MIG-001 — One-way shadow migration + +**Requirement:** Migration from jarvis-brain/Vikunja MUST use inventory, immutable source snapshots/checksums, one-way shadow import, read reconciliation, write freeze, final delta, cutover, and read-only stabilization. Dual writes are forbidden. + +**Acceptance:** + +- P0 publishes the current `origin/main` field-by-field expand/backfill/compatibility/switch/contract map before any schema lane starts. +- Legacy columns remain in the unified Drizzle declaration for the entire expand/N-1 window. +- Dry-run/apply/verify modes are idempotent and workspace-safe. +- Import lineage preserves source system/key/file/checksum/batch and rejected-record reports. +- Empty DB, production-shape, partial-resume, downgrade/rollback, status-shadow, workspace-backfill, and `mission_tasks.status` retirement tests pass. +- Shadow records cannot auto-dispatch. + +### REQ-MIG-002 — Cutover and rollback safety + +**Requirement:** Cutover MUST disable legacy writers and switch all clients to Gateway. Before first DB mutation rollback may switch authority back; afterward rollback requires freeze, DB-delta export/reconciliation, and owner decision. + +**Acceptance:** + +- Process inventory proves no active jarvis-brain/Vikunja project/task writer. +- Cutover rehearsal meets signed reconciliation thresholds. +- No reverse and forward sync run concurrently. + +## 6. Explicit non-goals + +The P0–P3 canon does not authorize: + +- replacing Gitea issue/PR storage; +- calendar, email, GLPI cache, CRM, billing, time tracking, or personal-brain migration; +- arbitrary custom workflows/statuses/fields; +- a writable offline/file/Valkey/browser fallback; +- direct client database access; +- LLM scheduling or autonomous scope invention; +- Coordinator gate waiver, certification, merge, release, or provider issue closure; +- Certifier merge authority; +- full mission designer, portfolio analytics, critical-path UX, or advanced board customization in the thin MVP; +- P4/P5 features unless separately released. + +## 7. Global release evidence + +P0–P3 may close only when requirements traceability maps every requirement above to automated and situational evidence, including cross-workspace denials, DB/Valkey fault injection, concurrent leases, stale fencing, generated-file immutability, UI conflict/reconnect behavior, migration reconciliation, independent review, mandatory SecReview, and final Certifier evidence. diff --git a/docs/scratchpads/751-native-kanban-canon.md b/docs/scratchpads/751-native-kanban-canon.md new file mode 100644 index 0000000..d041535 --- /dev/null +++ b/docs/scratchpads/751-native-kanban-canon.md @@ -0,0 +1,152 @@ +# Issue #751 — Native Kanban/SOT canonical publication + +## Objective + +Publish the owner-ratified P0–P3 requirements, mission manifest, task decomposition, and frozen shared contracts before feature implementation. + +## Authority and decisions + +- Owner: Jason +- Plan owner/orchestrator: web1 control plane; takeover by mosaic-100 during Claude quota outage +- Tracking: Mosaic Stack issue #751 +- Foundation: current Stack main + Drizzle/PostgreSQL +- Fixed invariants: PostgreSQL sole writable SOT; writes fail closed; exports never import; outage notes become attributable proposals; mechanical Coordinator has no scope/gate/certify/merge authority; Certifier has no merge authority. +- Recovery posture only is configurable through Lite, Standard, and High-assurance profiles. + +## Execution log + +- 2026-07-14: Existing planner-sol canon remediation reviewed from staging. KCR-001–016 claimed resolved; static checks passed. +- 2026-07-14: Independent GPT/Terra re-review dispatched to rev1. +- 2026-07-14: Re-review returned NO-GO: proposal audit-event IDs were not workspace-bound, leaving attribution forgeable; formatter evidence was not reproducible. Focused remediation round 2 routed to planner-sol. +- 2026-07-14: Remediation bound proposal audit links to `task_events(workspace_id,id)`, froze same-transaction semantic validation and negative tests, and made formatter/type/static checks reproducible. +- 2026-07-14: Independent rev1 re-review returned GO with KCR-001–016 closed and no new blocker. Canon copied into the issue #751 publication worktree; feature implementation remains held until merge. +- 2026-07-14: Independent publication validation returned FAIL on formatting/trailing whitespace, stale staging wording, ignored review evidence, and missing worktree dependencies. Bounded publication remediation routed to planner-sol; no runtime source change authorized. +- 2026-07-14: Publication remediation installed locked dependencies outside the repository cache, fixed formatting and wording, and preserved docs-only scope. Independent gaterun revalidation returned PASS across staged scope, formatting, lint, typecheck, strict contract compile, links, rollups, review artifacts, and fixed invariants. +- 2026-07-14: Ultron final gate returned GO with zero BLOCKER/HIGH findings; residual LOW items remain explicit implementation obligations. +- 2026-07-14: First commit attempt was correctly blocked by the lint-staged hook because docs contract `.ts` files were outside TypeScript project-service scope. Added a strict no-emit workstream `tsconfig.json` with exact Drizzle declaration paths; targeted contract TSC, contract ESLint, format, full lint/typecheck, strictness, and docs-only scope independently passed. + +## Verification evidence + +- Initial review: `docs/reports/native-kanban-sot/canon-initial-review-no-go.md` +- Final GO: `docs/reports/native-kanban-sot/canon-final-rereview-go.md` +- Ultron GO: `docs/reports/native-kanban-sot/ultron-final-go.md` +- Pending: PR gates, squash merge, terminal-green CI, and issue closure. + +## Publication remediation session — planner-sol + +**Assignment:** Remediate only issue #751 publication blockers from `/home/hermes/agent-work/reviews/751-kanban-canon-publication-validation.md`; no source, package, lock, provider, CI, build, deploy, commit, or push action. + +**Plan:** + +1. Install the locked pnpm dependencies without modifying package metadata. +2. Replace only stale publication/staging wording while retaining the canon-merge and KBN prerequisite implementation hold. +3. Format all changed candidate Markdown and contract TypeScript; remove trailing whitespace. +4. Run worktree `format:check`, `lint`, `typecheck`, strict no-emit contract compilation, tracked/untracked whitespace checks, and static invariants. +5. Verify package/lock/source paths are unchanged and append exact evidence here. + +**Budget:** No explicit token cap; bounded docs-only remediation, no exploratory/runtime work. +**TDD:** Not applicable—documentation/contract-publication formatting and wording only; strict/static validation is the primary gate. + +## Publication remediation results + +### Changes + +- `docs/native-kanban-sot/INDEX.md`: replaced staging/pending-GO wording with current publication and independent-GO wording; retained the merge hold and dependency-ordered KBN prerequisite hold. +- `docs/native-kanban-sot/TASKS.md`: replaced “Mos using this staging set” with “Mos / publication control plane”; made the post-merge KBN prerequisite hold explicit. +- Formatted all changed candidate Markdown and four contract TypeScript files with current-worktree Prettier 3.8.1. +- Removed trailing whitespace from candidate Markdown, including both linked review reports. +- Preserved both review reports and their links; they remain ignored by `.gitignore:11` for coordinator force-tracking. + +### Dependency installation + +The first target-worktree install attempt used the environment's default root-owned pnpm store and failed without changing package metadata: + +```text +cd /home/hermes/agent-work/stack-kanban-canon && pnpm install --frozen-lockfile +EACCES: permission denied, open '/root/.local/share/pnpm/store/v10/server/server.json' +``` + +Successful locked install using an authorized cache outside the repository: + +```bash +cd /home/hermes/agent-work/stack-kanban-canon +pnpm install --frozen-lockfile --store-dir /home/hermes/agent-work/pnpm-store +``` + +Result: PASS, 1,240 packages installed; lockfile resolution skipped as up to date. `node_modules` remains ignored. Tool versions: pnpm 10.6.2, Prettier 3.8.1, TypeScript 5.9.3, Drizzle ORM 0.45.1, Turbo 2.8.16. + +### Exact quality-gate results + +```text +pnpm format:check +PASS — All matched files use Prettier code style. + +pnpm lint +PASS — 23 successful lint tasks. + +pnpm typecheck +PASS — 42 successful tasks. Turbo invoked configured dependency build prerequisites as part of the repository's exact typecheck graph; no standalone build command was run. +``` + +Candidate formatting commands: + +```bash +pnpm exec prettier --write <3 tracked rollups + 9 native-kanban artifacts + requirements + scratchpad> +pnpm exec prettier --check +pnpm exec prettier --ignore-path /dev/null --write \ + docs/reports/native-kanban-sot/canon-initial-review-no-go.md \ + docs/reports/native-kanban-sot/canon-final-rereview-go.md +pnpm exec prettier --ignore-path /dev/null --check \ + docs/reports/native-kanban-sot/canon-initial-review-no-go.md \ + docs/reports/native-kanban-sot/canon-final-rereview-go.md +``` + +Result: PASS. The explicit `/dev/null` ignore path is required because `docs/reports/` is intentionally ignored pending coordinator force-tracking. + +Tracked and untracked whitespace checks: + +```text +git diff --check +PASS + +git diff --no-index --check /dev/null +PASS for all candidates +``` + +Strict contract compilation initially could not resolve pnpm-isolated `drizzle-orm` from the external docs directory. A temporary, removed dependency-context symlink made current-worktree resolution explicit: + +```bash +LINK=docs/native-kanban-sot/node_modules +ln -s ../../packages/db/node_modules "$LINK" +trap 'unlink "$LINK"' EXIT +pnpm exec tsc \ + --noEmit \ + --strict \ + --skipLibCheck \ + --target ES2022 \ + --module NodeNext \ + --moduleResolution NodeNext \ + docs/native-kanban-sot/contracts/*.ts +``` + +Result: `strict-contract-noemit=PASS`; temporary link removed. + +Static result: + +```text +proposal-audit-links=PASS +kcr-invariant-regression=PASS +publication-wording=PASS +vocabulary-alignment=PASS +``` + +### Scope-integrity evidence + +Baseline and final hashes are identical: + +```text +package.json 93a50eaefc7a0446a56234e427df03f6a2256f8da17c0bede17c22206928c8c0 +pnpm-lock.yaml 8b6448d51ac7797c8f782af52a080c0e38ab8bf364f32624f94e636bf5743229 +``` + +`tracked-package-lock-source-unchanged=PASS`: every tracked/untracked nonignored change remains under `docs/`; no package, lock, application source, plugin source, configuration, CI trigger, standalone build/deploy, container, provider, commit, or push action occurred.