207 lines
6.4 KiB
TypeScript
207 lines
6.4 KiB
TypeScript
/**
|
|
* 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<KanbanHealthState, { canonicalReads: boolean; mutations: boolean }>
|
|
> = {
|
|
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.
|
|
*/
|