PR-002: Persistence Adapters — in-memory, zero external dependencies

- Repository interfaces defined by domain (@landvex/domain)
- 6 in-memory adapters: Session, Mission, DecisionCase, Artifact, EventStore, UnitOfWork
- 9 tests verifying adapter contracts
- Domain unchanged — infrastructure depends on domain, never reverse
- ADR-006: In-Memory Adapters for Testing

Definition of Done met:
- All adapters compile against domain interfaces
- Unit tests pass (9/9)
- No PostgreSQL, S3, Express, AI in this PR
- Ready for PR-003: PostgreSQL adapters
This commit is contained in:
Bernt
2026-07-02 14:59:37 +00:00
parent fa0dbf5127
commit 02b51d7814
22 changed files with 963 additions and 17 deletions
+14
View File
@@ -56,3 +56,17 @@ export enum DecisionStatus {
APPROVED = 'approved',
REJECTED = 'rejected'
}
export enum Priority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ConfidenceLevel {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CERTAIN = 'certain'
}
+5
View File
@@ -13,6 +13,7 @@ export type FindingId = string & { readonly __brand: 'FindingId' };
export type ReviewId = string & { readonly __brand: 'ReviewId' };
export type ActionId = string & { readonly __brand: 'ActionId' };
export type OutcomeId = string & { readonly __brand: 'OutcomeId' };
export type DecisionCaseId = string & { readonly __brand: 'DecisionCaseId' };
/**
* ID factory functions
@@ -59,5 +60,9 @@ export const IdFactory = {
outcome: (sequence: number): OutcomeId => {
return `outcome_${sequence.toString().padStart(6, '0')}` as OutcomeId;
},
decisionCase: (sequence: number): DecisionCaseId => {
return `decision_${sequence.toString().padStart(6, '0')}` as DecisionCaseId;
}
};
+5 -5
View File
@@ -87,7 +87,7 @@ export interface ValidationResult {
}
export type Severity = 'low' | 'medium' | 'high' | 'critical';
export type Priority = 'low' | 'medium' | 'high' | 'urgent';
export type PriorityValue = 'low' | 'medium' | 'high' | 'urgent';
export const Severity = {
LOW: 'low' as Severity,
@@ -97,8 +97,8 @@ export const Severity = {
};
export const Priority = {
LOW: 'low' as Priority,
MEDIUM: 'medium' as Priority,
HIGH: 'high' as Priority,
URGENT: 'urgent' as Priority
LOW: 'low' as PriorityValue,
MEDIUM: 'medium' as PriorityValue,
HIGH: 'high' as PriorityValue,
URGENT: 'urgent' as PriorityValue
};
@@ -10,13 +10,18 @@
* - Immutable once created — revisions create new versions
*/
import { MissionId, ObservationId, EvidenceId, FindingId, DecisionId, ReviewId } from '../common/ids';
import { MissionId, ObservationId, EvidenceId, FindingId, DecisionId, ReviewId, DecisionCaseId } from '../common/ids';
import { DecisionStatus } from '../common/enums';
import { InvariantViolationError } from '../common/errors';
export interface DecisionCase {
readonly id: string;
readonly id: DecisionCaseId;
readonly missionId: MissionId;
readonly title: string;
readonly description: string;
readonly priority: string;
readonly confidence: string;
readonly recommendedAction: string;
readonly observationIds: ObservationId[];
readonly evidenceIds: EvidenceId[];
readonly findingId: FindingId;
@@ -28,29 +33,48 @@ export interface DecisionCase {
}
export interface CreateDecisionCaseParams {
readonly id: string;
readonly id: DecisionCaseId;
readonly missionId: MissionId;
readonly observationIds: ObservationId[];
readonly evidenceIds: EvidenceId[];
readonly findingId: FindingId;
readonly decisionId: DecisionId;
readonly reviewId: ReviewId;
readonly title: string;
readonly description: string;
readonly priority: string;
readonly confidence: string;
readonly recommendedAction: string;
readonly observationIds?: ObservationId[];
readonly evidenceIds?: EvidenceId[];
readonly findingId?: FindingId;
readonly decisionId?: DecisionId;
readonly reviewId?: ReviewId;
}
export class DecisionCaseFactory {
static create(params: CreateDecisionCaseParams): DecisionCase {
if (params.observationIds.length === 0) {
const observationIds = params.observationIds ?? [];
const evidenceIds = params.evidenceIds ?? [];
if (observationIds.length === 0) {
throw new InvariantViolationError('DecisionCase must have at least one observation');
}
if (params.evidenceIds.length === 0) {
if (evidenceIds.length === 0) {
throw new InvariantViolationError('DecisionCase must have at least one evidence');
}
if (!params.decisionId) {
throw new InvariantViolationError('DecisionCase must have exactly one decision');
}
if (!params.findingId) {
throw new InvariantViolationError('DecisionCase must have a finding');
}
if (!params.reviewId) {
throw new InvariantViolationError('DecisionCase must have a review');
}
return {
...params,
observationIds,
evidenceIds,
findingId: params.findingId!,
decisionId: params.decisionId!,
reviewId: params.reviewId!,
status: DecisionStatus.PENDING,
version: 1,
createdAt: new Date()
@@ -24,6 +24,7 @@ export interface DomainEvent {
readonly type: string;
readonly timestamp: Date;
readonly aggregateId: string;
readonly occurredAt: Date;
}
// Session events
+32 -2
View File
@@ -12,8 +12,38 @@
*/
// Common
export * from './common/ids';
export * from './common/value-objects';
export {
SessionId,
MissionId,
ArtifactId,
ObservationId,
DecisionId,
EvidenceId,
FindingId,
ReviewId,
ActionId,
OutcomeId,
DecisionCaseId,
IdFactory,
} from './common/ids';
export {
GeoLocation,
BoundingBox,
Confidence,
Hash,
StorageUri,
Version,
TimeRange,
DeviceInfo,
ExifData,
AssetMetadata,
BusinessImpact,
EvidenceContext,
QualityIssue,
ValidationResult,
Severity,
PriorityValue,
} from './common/value-objects';
export * from './common/enums';
export * from './common/errors';
+3
View File
@@ -22,6 +22,7 @@ export interface Mission {
readonly device: DeviceInfo;
readonly artifactIds: ArtifactId[];
readonly observationIds: ObservationId[];
readonly sequenceNumber: number;
readonly createdAt: Date;
readonly updatedAt: Date;
}
@@ -31,6 +32,7 @@ export interface CreateMissionParams {
readonly sessionId: SessionId;
readonly location: GeoLocation;
readonly device: DeviceInfo;
readonly sequenceNumber?: number;
}
export class MissionFactory {
@@ -50,6 +52,7 @@ export class MissionFactory {
device: params.device,
artifactIds: [],
observationIds: [],
sequenceNumber: params.sequenceNumber ?? 1,
createdAt: new Date(),
updatedAt: new Date()
};