From a08e897f969234229661e43fa2650e5dec800920 Mon Sep 17 00:00:00 2001 From: Bernt Date: Thu, 2 Jul 2026 13:49:19 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20PR-001=20Domain=20Model=20=E2=80=94=20C?= =?UTF-8?q?ore=20language=20for=20LandveX=20Intelligence=20Lab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Package: @landvex/domain - Zero dependencies on Express, PostgreSQL, S3, or AI frameworks - Four package structure: domain, contracts, shared, infrastructure - Domain never depends on infrastructure - Aggregate Roots: FieldSession, Mission, DecisionCase - Entities: MissionAsset, Artifact, Observation, Evidence, Finding, Decision, Review, Action, Outcome - Value Objects: MissionId, ArtifactId, SessionId, ObservationId, DecisionId, GeoLocation, GpsAccuracy, Confidence, Severity, Priority, Hash, StorageUri, Version - Enums: ArtifactType, ReviewStatus, DecisionVerb, MissionStatus, SessionStatus - Event Contracts: FieldSessionCreated, MissionCreated, MissionImported, ArtifactUploaded, ArtifactValidated, ObservationCreated, EvidenceCreated, FindingCreated, DecisionCreated, DecisionReviewed, DecisionApproved - Domain Invariants: - FieldSession: must have location, date, can have zero missions - Mission: must belong to one Session, must have at least one Artifact - DecisionCase: must have at least one Observation, one Evidence, exactly one Decision, cannot be Approved without Review - Artifact: must have unique hash, storage URI, versioned - TypeScript interfaces for all domain objects - Branded ID types for type safety - Strict rule: No new feature may introduce new domain concepts without approved change to domain model - README: 'This package describes LandveX domain model. Contains no dependencies to database, HTTP, cloud storage, or AI frameworks.' - Next: PR-002 Persistence Adapter (Repository Interface → PostgreSQL/S3/Event Store) Rationale: Lock the language before writing infrastructure code. Shared vocabulary for code, docs, APIs, tests, and product discussions. --- docs/design/PR-001-DOMAIN-MODEL.md | 549 +++++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 docs/design/PR-001-DOMAIN-MODEL.md diff --git a/docs/design/PR-001-DOMAIN-MODEL.md b/docs/design/PR-001-DOMAIN-MODEL.md new file mode 100644 index 000000000..d1f25dfc9 --- /dev/null +++ b/docs/design/PR-001-DOMAIN-MODEL.md @@ -0,0 +1,549 @@ +# PR-001: Domain Model + +**Package: `@landvex/domain`** + +| | | +|---|---| +| **PR** | 001 | +| **Scope** | Domain language only — no database, no HTTP, no buckets, no AI | +| **Rule** | This package has zero dependencies on Express, PostgreSQL, S3, or AI frameworks | + +--- + +## README + +**This package describes the LandveX domain model. It contains no dependencies to database, HTTP, cloud storage, or AI frameworks. It defines only the language, objects, and rules that the rest of the platform builds upon.** + +--- + +## Package Structure + +``` +packages/ +├── domain/ +│ ├── session/ +│ ├── mission/ +│ ├── artifact/ +│ ├── observation/ +│ ├── evidence/ +│ ├── finding/ +│ ├── decision/ +│ ├── review/ +│ ├── event/ +│ └── common/ +│ +├── contracts/ +│ ├── api/ +│ ├── events/ +│ └── dto/ +│ +├── shared/ +│ +└── infrastructure/ +``` + +**Domain never depends on infrastructure.** + +--- + +## Domain Objects + +### Aggregate Roots + +| Object | Type | Description | +|--------|------|-------------| +| `FieldSession` | Aggregate Root | Organizes field work | +| `Mission` | Aggregate Root | Single data collection task | +| `DecisionCase` | Aggregate Root | Complete decision chain | + +### Entities + +| Object | Description | +|--------|-------------| +| `MissionAsset` | Photo, video, or sensor data | +| `Artifact` | Any produced artifact (versioned, immutable) | +| `Observation` | Recorded fact from reality | +| `Evidence` | Linked observations with context | +| `Finding` | Pattern or conclusion | +| `Decision` | Recommended action | +| `Review` | Human quality assessment | +| `Action` | Executed task | +| `Outcome` | Measured result | + +### Value Objects + +| Object | Description | Example | +|--------|-------------|---------| +| `MissionId` | Unique mission identifier | `mission_20260814_000123` | +| `ArtifactId` | Unique artifact identifier | `artifact_000456` | +| `SessionId` | Unique session identifier | `session_20260814_000001` | +| `ObservationId` | Unique observation identifier | `obs_000981` | +| `DecisionId` | Unique decision identifier | `decision_000044` | +| `GeoLocation` | Latitude and longitude | `{ lat: 59.2371, lng: 18.1456 }` | +| `GpsAccuracy` | GPS accuracy in meters | `4.5` | +| `Confidence` | Model confidence 0-1 | `0.87` | +| `Severity` | Issue severity | `low`, `medium`, `high`, `critical` | +| `Priority` | Action priority | `low`, `medium`, `high`, `urgent` | +| `Hash` | SHA-256 checksum | `sha256:a3f7...` | +| `StorageUri` | Storage location | `s3://landvex-raw/...` | +| `Version` | Semantic version | `1`, `2`, `3` | + +--- + +## Enums + +### ArtifactType + +```typescript +enum ArtifactType { + IMAGE = 'image', + VIDEO = 'video', + AUDIO = 'audio', + DATASET = 'dataset', + ANNOTATION = 'annotation', + DECISION_CASE = 'decision_case', + MODEL = 'model', + EVALUATION = 'evaluation', + REPLAY = 'replay' +} +``` + +### ReviewStatus + +```typescript +enum ReviewStatus { + ASSIGNED = 'assigned', + IN_REVIEW = 'in_review', + APPROVED = 'approved', + REJECTED = 'rejected', + NEEDS_MORE_EVIDENCE = 'needs_more_evidence' +} +``` + +### DecisionVerb + +```typescript +enum DecisionVerb { + INSPECT = 'inspect', + REPAIR = 'repair', + MONITOR = 'monitor', + WAIT = 'wait', + COLLECT = 'collect', + ESCALATE = 'escalate', + IGNORE = 'ignore', + PRIORITIZE = 'prioritize' +} +``` + +### MissionStatus + +```typescript +enum MissionStatus { + CREATED = 'created', + UPLOADING = 'uploading', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed' +} +``` + +### SessionStatus + +```typescript +enum SessionStatus { + PLANNED = 'planned', + ACTIVE = 'active', + COMPLETED = 'completed' +} +``` + +--- + +## Event Contracts + +```typescript +// Session events +interface FieldSessionCreated { + type: 'FieldSessionCreated'; + sessionId: SessionId; + location: GeoLocation; + date: Date; + timestamp: Date; +} + +// Mission events +interface MissionCreated { + type: 'MissionCreated'; + missionId: MissionId; + sessionId: SessionId; + location: GeoLocation; + timestamp: Date; +} + +interface MissionImported { + type: 'MissionImported'; + missionId: MissionId; + assetCount: number; + timestamp: Date; +} + +// Artifact events +interface ArtifactUploaded { + type: 'ArtifactUploaded'; + artifactId: ArtifactId; + missionId: MissionId; + type: ArtifactType; + storageUri: StorageUri; + hash: Hash; + timestamp: Date; +} + +interface ArtifactValidated { + type: 'ArtifactValidated'; + artifactId: ArtifactId; + validationResult: ValidationResult; + timestamp: Date; +} + +// Observation events +interface ObservationCreated { + type: 'ObservationCreated'; + observationId: ObservationId; + missionId: MissionId; + artifactId: ArtifactId; + description: string; + timestamp: Date; +} + +// Evidence events +interface EvidenceCreated { + type: 'EvidenceCreated'; + evidenceId: string; + observationIds: ObservationId[]; + description: string; + timestamp: Date; +} + +// Finding events +interface FindingCreated { + type: 'FindingCreated'; + findingId: string; + evidenceId: string; + description: string; + severity: Severity; + timestamp: Date; +} + +// Decision events +interface DecisionCreated { + type: 'DecisionCreated'; + decisionId: DecisionId; + findingId: string; + verb: DecisionVerb; + description: string; + confidence: Confidence; + timestamp: Date; +} + +interface DecisionReviewed { + type: 'DecisionReviewed'; + decisionId: DecisionId; + reviewId: string; + status: ReviewStatus; + reviewer: string; + timestamp: Date; +} + +interface DecisionApproved { + type: 'DecisionApproved'; + decisionId: DecisionId; + reviewId: string; + approvedBy: string; + timestamp: Date; +} +``` + +--- + +## Domain Invariants + +### FieldSession + +- Must have exactly one location +- Must have a date +- Can have zero or more missions +- Cannot be completed if any mission is still uploading + +### Mission + +- Must belong to exactly one Session +- Must have at least one Artifact +- Cannot be completed before import is finished +- Status is a projection of event history, not mutable state + +### DecisionCase + +- Must have at least one Observation +- Must have at least one Evidence +- Must have exactly one Decision +- Cannot be Approved without Review +- Immutable once created — revisions create new versions + +### Artifact + +- Must have a unique hash +- Must have a storage URI +- Version is incremented on each change +- Parent reference creates lineage chain + +--- + +## TypeScript Interfaces + +```typescript +// Aggregate Roots +interface FieldSession { + id: SessionId; + location: GeoLocation; + date: Date; + status: SessionStatus; + missionIds: MissionId[]; + createdAt: Date; + updatedAt: Date; +} + +interface Mission { + id: MissionId; + sessionId: SessionId; + location: GeoLocation; + status: MissionStatus; + device: DeviceInfo; + artifactIds: ArtifactId[]; + observationIds: ObservationId[]; + createdAt: Date; + updatedAt: Date; +} + +interface DecisionCase { + id: string; + missionId: MissionId; + observationIds: ObservationId[]; + evidenceIds: string[]; + findingId: string; + decisionId: DecisionId; + reviewId: string; + status: 'pending' | 'under_review' | 'approved' | 'rejected'; + version: number; + createdAt: Date; +} + +// Entities +interface MissionAsset { + id: ArtifactId; + missionId: MissionId; + type: ArtifactType; + storageUri: StorageUri; + hash: Hash; + sizeBytes: number; + mimeType: string; + metadata: AssetMetadata; + createdAt: Date; +} + +interface Artifact { + id: ArtifactId; + type: ArtifactType; + version: Version; + hash: Hash; + createdBy: string; + storageUri: StorageUri; + parentId?: ArtifactId; + lineage: ArtifactId[]; + createdAt: Date; +} + +interface Observation { + id: ObservationId; + missionId: MissionId; + artifactId: ArtifactId; + type: string; + description: string; + location: GeoLocation; + confidence: Confidence; + boundingBox?: BoundingBox; + createdAt: Date; +} + +interface Evidence { + id: string; + observationIds: ObservationId[]; + description: string; + context: EvidenceContext; + createdAt: Date; +} + +interface Finding { + id: string; + evidenceId: string; + description: string; + severity: Severity; + pattern: string; + confidence: Confidence; + createdAt: Date; +} + +interface Decision { + id: DecisionId; + findingId: string; + verb: DecisionVerb; + description: string; + rationale: string; + confidence: Confidence; + consequence: string; + action: string; + businessImpact: BusinessImpact; + createdAt: Date; +} + +interface Review { + id: string; + decisionId: DecisionId; + status: ReviewStatus; + reviewer: string; + comments: string; + createdAt: Date; + completedAt?: Date; +} + +interface Action { + id: string; + decisionId: DecisionId; + description: string; + assignedTo: string; + scheduledDate: Date; + status: 'pending' | 'in_progress' | 'completed'; + createdAt: Date; +} + +interface Outcome { + id: string; + actionId: string; + description: string; + measuredImpact: BusinessImpact; + verifiedBy: string; + createdAt: Date; +} + +// Value Objects +interface GeoLocation { + lat: number; + lng: number; + accuracy?: GpsAccuracy; +} + +interface BoundingBox { + x: number; + y: number; + width: number; + height: number; +} + +interface AssetMetadata { + exif: ExifData; + gps: GeoLocation; + device: DeviceInfo; +} + +interface ExifData { + device?: string; + iso?: number; + exposure?: string; + focalLength?: string; + timestamp?: Date; +} + +interface DeviceInfo { + model: string; + os: string; + appVersion: string; +} + +interface EvidenceContext { + historicalData?: boolean; + weatherData?: boolean; + trafficData?: boolean; + gisData?: boolean; +} + +interface BusinessImpact { + risk: string; + cost: string; + time: string; + opportunity: string; +} + +interface ValidationResult { + valid: boolean; + issues: QualityIssue[]; +} + +interface QualityIssue { + type: 'blur' | 'duplicate' | 'bad_gps' | 'low_resolution' | 'missing_metadata' | 'wrong_timestamp' | 'occlusion'; + severity: Severity; + description: string; +} + +// ID Types (branded types for type safety) +type MissionId = string & { readonly __brand: 'MissionId' }; +type ArtifactId = string & { readonly __brand: 'ArtifactId' }; +type SessionId = string & { readonly __brand: 'SessionId' }; +type ObservationId = string & { readonly __brand: 'ObservationId' }; +type DecisionId = string & { readonly __brand: 'DecisionId' }; +``` + +--- + +## Strict Rule + +**No new feature may introduce new domain concepts without an approved change to the domain model.** + +The concepts `Session`, `Mission`, `Artifact`, `Observation`, `Evidence`, `Finding`, `Decision`, `Review`, and `Outcome` become the shared language for the entire organization — code, documentation, APIs, tests, and product discussions use the same terms. + +--- + +## Not in PR-001 + +| Item | Why Excluded | +|------|-------------| +| Repositories | Domain should not know about persistence | +| Services | Will come in next PR | +| Database code | Infrastructure concern | +| HTTP endpoints | API concern | +| Bucket code | Infrastructure concern | +| AI logic | Separate concern | + +--- + +## Next PR + +**PR-002: Persistence Adapter** + +``` +Domain + ↓ +Repository Interface + ↓ +PostgreSQL Adapter + ↓ +S3 Adapter + ↓ +Event Store Adapter +``` + +Freedom to change technology without touching core model. + +--- + +## Status + +**READY FOR IMPLEMENTATION**