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
+96
View File
@@ -0,0 +1,96 @@
/**
* Review - Entity
* Human quality assessment of a decision
*
* Invariants:
* - Must have a reviewer
* - Must have a status
* - Cannot be approved without review
*/
import { ReviewId, DecisionId } from '../common/ids';
import { ReviewStatus } from '../common/enums';
import { InvariantViolationError } from '../common/errors';
export interface Review {
readonly id: ReviewId;
readonly decisionId: DecisionId;
readonly status: ReviewStatus;
readonly reviewer: string;
readonly comments: string;
readonly createdAt: Date;
readonly updatedAt?: Date;
readonly completedAt?: Date;
}
export interface CreateReviewParams {
readonly id: ReviewId;
readonly decisionId: DecisionId;
readonly reviewer: string;
}
export class ReviewFactory {
static create(params: CreateReviewParams): Review {
if (!params.reviewer) {
throw new InvariantViolationError('Review must have a reviewer');
}
return {
...params,
status: ReviewStatus.ASSIGNED,
comments: '',
createdAt: new Date()
};
}
static startReview(review: Review): Review {
if (review.status !== ReviewStatus.ASSIGNED) {
throw new InvariantViolationError('Cannot start review that is not assigned');
}
return {
...review,
status: ReviewStatus.IN_REVIEW,
updatedAt: new Date()
};
}
static approve(review: Review, comments: string): Review {
if (review.status !== ReviewStatus.IN_REVIEW) {
throw new InvariantViolationError('Cannot approve review that is not in review');
}
return {
...review,
status: ReviewStatus.APPROVED,
comments,
completedAt: new Date()
};
}
static reject(review: Review, comments: string): Review {
if (review.status !== ReviewStatus.IN_REVIEW) {
throw new InvariantViolationError('Cannot reject review that is not in review');
}
return {
...review,
status: ReviewStatus.REJECTED,
comments,
completedAt: new Date()
};
}
static requestMoreEvidence(review: Review, comments: string): Review {
if (review.status !== ReviewStatus.IN_REVIEW) {
throw new InvariantViolationError('Cannot request evidence for review that is not in review');
}
return {
...review,
status: ReviewStatus.NEEDS_MORE_EVIDENCE,
comments,
completedAt: new Date()
};
}
}