/** * 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. */