Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Domain primitive identities**: Newly created audiences, audience pains, components, decisions, dependencies, goals, guidelines, invariants, projects, relations, sessions, value propositions, and workers now receive domain-owned UUID identities. Existing persisted identifiers remain supported during replay and through CLI operations without migration.

## [3.16.1] - 2026-07-10

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { AddAudiencePainCommand } from "./AddAudiencePainCommand.js";
import { IAudiencePainAddedEventWriter } from "./IAudiencePainAddedEventWriter.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
import { AudiencePain } from "../../../../domain/audience-pains/AudiencePain.js";
import { IdGenerator } from "../../../identity/IdGenerator.js";

export class AddAudiencePainCommandHandler {
constructor(
Expand All @@ -21,11 +20,9 @@ export class AddAudiencePainCommandHandler {
) {}

async execute(command: AddAudiencePainCommand): Promise<{ painId: string }> {
// Generate unique ID for new pain
const painId = IdGenerator.generate();

// 1. Create new aggregate
const pain = AudiencePain.create(painId);
const pain = AudiencePain.create();
const painId = pain.snapshot.id;

// 2. Domain logic produces event
const event = pain.add(command.title, command.description);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { AddAudienceCommand } from "./AddAudienceCommand.js";
import { IAudienceAddedEventWriter } from "./IAudienceAddedEventWriter.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
import { Audience } from "../../../../domain/audiences/Audience.js";
import { IdGenerator } from "../../../identity/IdGenerator.js";

export class AddAudienceCommandHandler {
constructor(
Expand All @@ -21,11 +20,9 @@ export class AddAudienceCommandHandler {
) {}

async execute(command: AddAudienceCommand): Promise<{ audienceId: string }> {
// Generate unique ID for new audience
const audienceId = IdGenerator.generate();

// 1. Create new aggregate
const audience = Audience.create(audienceId);
const audience = Audience.create();
const audienceId = audience.snapshot.id;

// 2. Domain logic produces event
const event = audience.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { IComponentAddReader } from "./IComponentAddReader.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
import { Component } from "../../../../domain/components/Component.js";
import { ComponentEvent } from "../../../../domain/components/EventIndex.js";
import { IdGenerator } from "../../../identity/IdGenerator.js";

export class AddComponentCommandHandler {
constructor(
Expand Down Expand Up @@ -34,8 +33,8 @@ export class AddComponentCommandHandler {
);
} else {
// Create new component
componentId = IdGenerator.generate();
const component = Component.create(componentId);
const component = Component.create();
componentId = component.snapshot.id;

event = component.add(
command.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IdGenerator } from "../../../identity/IdGenerator.js";
import { AddDecisionCommand } from "./AddDecisionCommand.js";
import { IDecisionAddedEventWriter } from "./IDecisionAddedEventWriter.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
Expand All @@ -17,8 +16,8 @@ export class AddDecisionCommandHandler {

async execute(command: AddDecisionCommand): Promise<{ decisionId: string }> {
// 1. Create new aggregate with unique ID
const decisionId = IdGenerator.generate();
const decision = Decision.create(decisionId);
const decision = Decision.create();
const decisionId = decision.snapshot.id;

// 2. Domain logic produces event
const event = decision.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ export class AddDependencyCommandHandler {

async execute(command: AddDependencyCommand): Promise<{ dependencyId: string }> {
const identity = this.resolveIdentity(command);
const dependencyId = `dep_${identity.ecosystem}_${identity.packageName}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');

// Check if dependency already exists (idempotent behavior)
const existingDependency = await this.dependencyReader.findById(dependencyId);
const existingDependency = await this.dependencyReader.findByIdentity(
identity.ecosystem,
identity.packageName,
);
if (existingDependency) {
// Silently succeed - idempotent operation
return { dependencyId };
return { dependencyId: existingDependency.dependencyId };
}

// 1. Create new aggregate
const dependency = Dependency.create(dependencyId);
const dependency = Dependency.create();
const dependencyId = dependency.snapshot.id;

// 2. Domain logic produces event
const event = dependency.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ import { DependencyView } from "../DependencyView.js";
*/
export interface IDependencyAddReader {
findById(dependencyId: string): Promise<DependencyView | null>;
findByIdentity(
ecosystem: string,
packageName: string,
): Promise<DependencyView | null>;
}
7 changes: 2 additions & 5 deletions src/application/context/goals/add/AddGoalCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IdGenerator } from "../../../identity/IdGenerator.js";
import { AddGoalCommand } from "./AddGoalCommand.js";
import { IGoalAddedEventWriter } from "./IGoalAddedEventWriter.js";
import { IGoalUpdatedEventWriter } from "../update/IGoalUpdatedEventWriter.js";
Expand Down Expand Up @@ -26,11 +25,9 @@ export class AddGoalCommandHandler {
) {}

async execute(command: AddGoalCommand): Promise<{ goalId: string }> {
// Generate new goal ID (handler owns ID generation)
const goalId = IdGenerator.generate();

// Create new aggregate
const goal = Goal.create(goalId);
const goal = Goal.create();
const goalId = goal.snapshot.id;

// Domain logic produces event
const event = goal.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { AddGuidelineCommand } from "./AddGuidelineCommand.js";
import { IGuidelineAddedEventWriter } from "./IGuidelineAddedEventWriter.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
import { Guideline } from "../../../../domain/guidelines/Guideline.js";
import { IdGenerator } from "../../../identity/IdGenerator.js";

export class AddGuidelineCommandHandler {
constructor(
Expand All @@ -19,8 +18,8 @@ export class AddGuidelineCommandHandler {

async execute(command: AddGuidelineCommand): Promise<{ guidelineId: string }> {
// 1. Create new aggregate
const guidelineId = IdGenerator.generate();
const guideline = Guideline.create(guidelineId);
const guideline = Guideline.create();
const guidelineId = guideline.snapshot.id;

// 2. Domain logic produces event
const event = guideline.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { IInvariantAddedEventWriter } from "./IInvariantAddedEventWriter.js";
import { IInvariantAddReader } from "./IInvariantAddReader.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
import { Invariant } from "../../../../domain/invariants/Invariant.js";
import { IdGenerator } from "../../../identity/IdGenerator.js";

export class AddInvariantCommandHandler {
constructor(
Expand All @@ -31,8 +30,8 @@ export class AddInvariantCommandHandler {
}

// 1. Create new aggregate
const invariantId = IdGenerator.generate();
const invariant = Invariant.create(invariantId);
const invariant = Invariant.create();
const invariantId = invariant.snapshot.id;

// 2. Domain logic produces event
const event = invariant.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export class InitializeProjectCommandHandler {
}

// 1. Create new aggregate
const projectId = this.projectIdentityResolver.generateProjectId();
const project = Project.create(projectId);
const project = Project.create();
const projectId = project.snapshot.id;

// 2. Domain logic produces event
const event = project.initialize(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IdGenerator } from "../../../identity/IdGenerator.js";
import { AddRelationCommand } from "./AddRelationCommand.js";
import { IRelationAddedEventWriter } from "./IRelationAddedEventWriter.js";
import { IRelationAddedReader } from "./IRelationAddedReader.js";
Expand Down Expand Up @@ -32,11 +31,9 @@ export class AddRelationCommandHandler {
return { relationId: existingRelation.relationId };
}

// Generate new relation ID
const relationId = IdGenerator.generate();

// Create new aggregate
const relation = Relation.create(relationId);
const relation = Relation.create();
const relationId = relation.snapshot.id;

// Domain logic produces event
const event = relation.add(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IdGenerator } from "../../../identity/IdGenerator.js";
import { StartSessionCommand } from "./StartSessionCommand.js";
import { ISessionStartedEventWriter } from "./ISessionStartedEventWriter.js";
import { IEventBus } from "../../../messaging/IEventBus.js";
Expand All @@ -15,13 +14,11 @@
) {}

async execute(
command: StartSessionCommand

Check warning on line 17 in src/application/context/sessions/start/StartSessionCommandHandler.ts

View workflow job for this annotation

GitHub Actions / lint

'command' is defined but never used. Allowed unused args must match /^_/u
): Promise<{ sessionId: string }> {
// Generate new session ID
const sessionId = IdGenerator.generate();

// Create new aggregate
const session = Session.create(sessionId);
const session = Session.create();
const sessionId = session.snapshot.id;

// Domain logic produces event
const event = session.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { IValuePropositionAddedEventWriter } from "./IValuePropositionAddedEvent
import { IEventBus } from "../../../messaging/IEventBus.js";
import { ValueProposition } from "../../../../domain/value-propositions/ValueProposition.js";
import { ValuePropositionAddedEvent } from "../../../../domain/value-propositions/add/ValuePropositionAddedEvent.js";
import { IdGenerator } from "../../../identity/IdGenerator.js";

/**
* Handler for adding a new value proposition
Expand All @@ -19,8 +18,8 @@ export class AddValuePropositionCommandHandler {
command: AddValuePropositionCommand
): Promise<{ valuePropositionId: string }> {
// 1. Create new aggregate with generated ID
const valuePropositionId = IdGenerator.generate();
const valueProposition = ValueProposition.create(valuePropositionId);
const valueProposition = ValueProposition.create();
const valuePropositionId = valueProposition.snapshot.id;

// 2. Domain logic produces event
const event = valueProposition.add(
Expand Down
18 changes: 6 additions & 12 deletions src/application/host/workers/WorkerId.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
/**
* WorkerId - Branded type for worker identity
*
* Represents a unique identifier for a worker (LLM agent session).
* Each terminal/IDE session gets a unique workerId that persists
* for the lifetime of that session.
*
* This is a branded type to ensure type safety and prevent
* accidental mixing with regular strings.
*/
export type WorkerId = string & { readonly __brand: "WorkerId" };
import {
WorkerId,
} from "../../../domain/workers/WorkerId.js";

export type { WorkerId } from "../../../domain/workers/WorkerId.js";

/**
* Creates a WorkerId from a string value.
Expand All @@ -17,5 +11,5 @@ export type WorkerId = string & { readonly __brand: "WorkerId" };
* @returns The branded WorkerId
*/
export function createWorkerId(value: string): WorkerId {
return value as WorkerId;
return WorkerId.fromLegacy(value);
}
7 changes: 4 additions & 3 deletions src/domain/audience-pains/AudiencePain.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { BaseAggregate, AggregateState } from "../BaseAggregate.js";
import { UUID } from "../BaseEvent.js";
import { AudiencePainId } from "./AudiencePainId.js";
import { ValidationRuleSet } from "../validation/ValidationRule.js";
import { AudiencePainEvent, AudiencePainAddedEvent, AudiencePainUpdatedEvent } from "./EventIndex.js";
import { AudiencePainEventType, AudiencePainErrorMessages, AudiencePainStatus, AudiencePainStatusType } from "./Constants.js";
import { TITLE_RULES } from "./rules/TitleRules.js";
import { DESCRIPTION_RULES } from "./rules/DescriptionRules.js";

export interface AudiencePainState extends AggregateState {
id: UUID;
id: AudiencePainId;
title: string;
description: string;
status: AudiencePainStatusType;
Expand Down Expand Up @@ -47,7 +48,7 @@ export class AudiencePain extends BaseAggregate<AudiencePainState, AudiencePainE
}
}

static create(id: UUID): AudiencePain {
static create(id: AudiencePainId = AudiencePainId.create()): AudiencePain {
const state: AudiencePainState = {
id,
title: "",
Expand All @@ -64,7 +65,7 @@ export class AudiencePain extends BaseAggregate<AudiencePainState, AudiencePainE
*/
static rehydrate(id: UUID, history: AudiencePainEvent[]): AudiencePain {
const state: AudiencePainState = {
id,
id: AudiencePainId.fromLegacy(id),
title: "",
description: "",
status: AudiencePainStatus.ACTIVE,
Expand Down
4 changes: 4 additions & 0 deletions src/domain/audience-pains/AudiencePainId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineDomainIdentity, DomainIdentity } from "../identity/DomainIdentity.js";

export type AudiencePainId = DomainIdentity<"AudiencePainId">;
export const AudiencePainId = defineDomainIdentity("AudiencePainId");
7 changes: 4 additions & 3 deletions src/domain/audiences/Audience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
BaseAggregate,
AggregateState,
} from "../BaseAggregate.js";
import { UUID, BaseEvent } from "../BaseEvent.js";

Check warning on line 12 in src/domain/audiences/Audience.ts

View workflow job for this annotation

GitHub Actions / lint

'BaseEvent' is defined but never used. Allowed unused vars must match /^_/u
import { AudienceId } from "./AudienceId.js";
import { ValidationRuleSet } from "../validation/ValidationRule.js";
import {
AudienceEvent,
Expand All @@ -30,7 +31,7 @@
* Domain state: business properties + aggregate metadata
*/
export interface AudienceState extends AggregateState {
id: UUID; // Aggregate identity
id: AudienceId; // Aggregate identity
name: string; // Required: audience name
description: string; // Required: who they are
priority: AudiencePriorityType; // Required: primary/secondary/tertiary
Expand Down Expand Up @@ -80,7 +81,7 @@
* Creates a new Audience aggregate.
* Use this when starting a new aggregate that will emit its first event.
*/
static create(id: UUID): Audience {
static create(id: AudienceId = AudienceId.create()): Audience {
const state: AudienceState = {
id,
name: "",
Expand All @@ -98,7 +99,7 @@
*/
static rehydrate(id: UUID, history: AudienceEvent[]): Audience {
const state: AudienceState = {
id,
id: AudienceId.fromLegacy(id),
name: "",
description: "",
priority: "primary",
Expand Down
4 changes: 4 additions & 0 deletions src/domain/audiences/AudienceId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineDomainIdentity, DomainIdentity } from "../identity/DomainIdentity.js";

export type AudienceId = DomainIdentity<"AudienceId">;
export const AudienceId = defineDomainIdentity("AudienceId");
7 changes: 4 additions & 3 deletions src/domain/components/Component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BaseAggregate, AggregateState } from "../BaseAggregate.js";
import { UUID, ISO8601 } from "../BaseEvent.js";
import { ComponentId } from "./ComponentId.js";
import { ValidationRuleSet } from "../validation/ValidationRule.js";
import {
ComponentEvent,
Expand All @@ -25,7 +26,7 @@ import { PATH_RULES } from "./rules/PathRules.js";
import { DEPRECATION_REASON_RULES } from "./rules/DeprecationReasonRules.js";

export interface ComponentState extends AggregateState {
id: UUID;
id: ComponentId;
name: string;
type: ComponentTypeValue;
description: string;
Expand Down Expand Up @@ -96,7 +97,7 @@ export class Component extends BaseAggregate<ComponentState, ComponentEvent> {
}
}

static create(id: UUID): Component {
static create(id: ComponentId = ComponentId.create()): Component {
const state: ComponentState = {
id,
name: "",
Expand All @@ -117,7 +118,7 @@ export class Component extends BaseAggregate<ComponentState, ComponentEvent> {
*/
static rehydrate(id: UUID, history: ComponentEvent[]): Component {
const state: ComponentState = {
id,
id: ComponentId.fromLegacy(id),
name: "",
type: "service" as ComponentTypeValue,
description: "",
Expand Down
4 changes: 4 additions & 0 deletions src/domain/components/ComponentId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineDomainIdentity, DomainIdentity } from "../identity/DomainIdentity.js";

export type ComponentId = DomainIdentity<"ComponentId">;
export const ComponentId = defineDomainIdentity("ComponentId");
Loading
Loading