PR-001: Domain Model v1.0 — compile-only, zero dependencies

- @landvex/domain package with TypeScript strict mode
- 3 Aggregate Roots: FieldSession, Mission, DecisionCase
- Branded IDs, Value Objects, Domain Events, Invariants
- 19 unit tests for IDs, FieldSession, DecisionCase
- Zero runtime dependencies (only TypeScript + jest for tests)
- Separates Entity / Value Object / Aggregate Root
- README documents Three Rules of the domain

Definition of Done met:
- Compiles without errors
- Exports all domain types
- Unit tests for invariants and value objects
- No PostgreSQL, S3, Express, AI models, queues
This commit is contained in:
Bernt
2026-07-02 14:26:29 +00:00
parent 118180e7ff
commit fa0dbf5127
26 changed files with 1599 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
/**
* Branded ID types for type safety
* Prevents mixing different ID types accidentally
*/
export type SessionId = string & { readonly __brand: 'SessionId' };
export type MissionId = string & { readonly __brand: 'MissionId' };
export type ArtifactId = string & { readonly __brand: 'ArtifactId' };
export type ObservationId = string & { readonly __brand: 'ObservationId' };
export type DecisionId = string & { readonly __brand: 'DecisionId' };
export type EvidenceId = string & { readonly __brand: 'EvidenceId' };
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' };
/**
* ID factory functions
* Ensures consistent ID format across the domain
*/
export const IdFactory = {
session: (date: Date, sequence: number): SessionId => {
const dateStr = date.toISOString().split('T')[0].replace(/-/g, '');
return `session_${dateStr}_${sequence.toString().padStart(6, '0')}` as SessionId;
},
mission: (date: Date, sequence: number): MissionId => {
const dateStr = date.toISOString().split('T')[0].replace(/-/g, '');
return `mission_${dateStr}_${sequence.toString().padStart(6, '0')}` as MissionId;
},
artifact: (sequence: number): ArtifactId => {
return `artifact_${sequence.toString().padStart(6, '0')}` as ArtifactId;
},
observation: (sequence: number): ObservationId => {
return `obs_${sequence.toString().padStart(6, '0')}` as ObservationId;
},
decision: (sequence: number): DecisionId => {
return `decision_${sequence.toString().padStart(6, '0')}` as DecisionId;
},
evidence: (sequence: number): EvidenceId => {
return `evidence_${sequence.toString().padStart(6, '0')}` as EvidenceId;
},
finding: (sequence: number): FindingId => {
return `finding_${sequence.toString().padStart(6, '0')}` as FindingId;
},
review: (sequence: number): ReviewId => {
return `review_${sequence.toString().padStart(6, '0')}` as ReviewId;
},
action: (sequence: number): ActionId => {
return `action_${sequence.toString().padStart(6, '0')}` as ActionId;
},
outcome: (sequence: number): OutcomeId => {
return `outcome_${sequence.toString().padStart(6, '0')}` as OutcomeId;
}
};