Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Forgeyard Core Domain & Foundation System Architecture

Document type: Foundational System & Architecture
Project: Forgeyard CI/CD
Architecture style: Modular monolith, single Rust workspace
Scope: Core domain primitives, typed identities, invariants, time, digests, errors, capabilities, configuration contracts, versioning, foundational dependency rules, and composition boundaries
Status: Target production architecture for the new Forgeyard repository
Relationship to prior work: This document assumes the already-defined workspace structure, VCS-neutral source model, Change Proposal model, hermetic/reproducible build system, and ecosystem architectures. It does not redefine those systems; it defines the foundation they depend on.


1. Purpose

The new Forgeyard codebase must begin with a small, stable, strongly typed foundation.

Most architectural failures in large systems begin when:

IDs become strings
states become strings
timestamps become integers
errors become anyhow everywhere
configuration leaks into domain models
database rows become business models
transport DTOs become domain types
platform-specific code leaks into core

Forgeyard should prevent this from the first commit.

The foundation must provide:

  • typed identifiers;
  • typed digests;
  • typed timestamps/durations/deadlines;
  • stable capability models;
  • clear domain invariants;
  • explicit execution/application modes;
  • domain-safe errors;
  • versioned foundational serialization;
  • configuration boundaries;
  • strongly enforced dependency direction;
  • test utilities;
  • deterministic canonical primitives.

The central rule is:

Forgeyard core defines semantic truth but knows nothing about SQL, Git, Axum, Dioxus, Postgres, Stoolap, S3, operating systems, or language ecosystems.


2. Architectural Position

                     Applications
                         │
                         ▼
                Generic Services
                         │
                         ▼
                Domain Models / APIs
                         │
                         ▼
               Forgeyard Core Layer
              ┌──────────┼──────────┐
              ▼          ▼          ▼
            IDs       Digests      Time
              │          │          │
              └──────────┼──────────┘
                         ▼
                     Invariants

The core layer is the bottom of the dependency graph.

Nothing foundational depends on higher-level adapters.


3. New Repository Foundation

Recommended initial tree:

forgeyard/
├── Cargo.toml
├── Cargo.lock
├── rust-toolchain.toml
├── architecture.ron
│
└── crates/
    ├── core/
    │   ├── forgeyard-core/
    │   └── forgeyard-core-test/
    ├── ids/
    │   └── forgeyard-ids/
    ├── digest/
    │   └── forgeyard-digest/
    ├── time/
    │   └── forgeyard-time/
    ├── error/
    │   └── forgeyard-error/
    ├── config/
    │   ├── forgeyard-config/
    │   ├── forgeyard-config-loader/
    │   ├── forgeyard-config-schema/
    │   └── forgeyard-config-policy/
    └── protocol/
        ├── forgeyard-envelope/
        ├── forgeyard-wire/
        ├── forgeyard-api-model/
        └── forgeyard-version/

This should be the first substantial implementation milestone in the new repository.


4. Foundational Crates

4.1 forgeyard-core

Purpose:

system modes
tenancy primitives
project identity references
capability concepts
invariants
generic state principles
small foundational enums

Must remain small.

It must not become a dumping ground.


4.2 forgeyard-ids

Owns every strongly typed identifier.

Examples:

#![allow(unused)]
fn main() {
pub struct ProjectId(Ulid);
pub struct PipelineId(Ulid);
pub struct RunId(Ulid);
pub struct JobId(Ulid);
pub struct RunnerId(Ulid);
pub struct LeaseId(Ulid);
pub struct ArtifactId(Ulid);
pub struct ReleaseId(Ulid);
pub struct DeploymentId(Ulid);
pub struct RepositoryId(Digest);
pub struct SourceSnapshotId(Digest);
pub struct ChangeProposalId(Ulid);
pub struct PrincipalId(Ulid);
}

No subsystem should define ad-hoc ID aliases like:

#![allow(unused)]
fn main() {
type ProjectId = String;
}

5. ID Categories

Forgeyard uses three broad identity classes.

5.1 Entity IDs

Mutable lifecycle entities:

ProjectId
RunId
JobId
RunnerId
ReleaseId
DeploymentId
ChangeProposalId

Use opaque sortable/random identities such as ULID-like identifiers.


5.2 Content IDs

Immutable content:

SourceSnapshotId
BlobId
TreeObjectId
ArtifactDigest
DerivationId
ToolchainId

Use digest-backed identities.


5.3 External IDs

External systems:

Git revision
GitHub PR number
Postgres row key
SCM delivery ID

Never reuse as internal identity directly.

Wrap them:

#![allow(unused)]
fn main() {
pub struct ExternalRevisionId(String);
pub struct ProviderProposalId(String);
}

6. ID Rules

  1. IDs are never plain String.
  2. Entity IDs are globally unique.
  3. Content IDs derive from canonical content.
  4. Display representation is not internal representation.
  5. Parsing is fallible.
  6. Serialization is version-safe.
  7. Equality semantics are obvious from type.
  8. IDs from different domains cannot be accidentally compared.

7. Typed Digest System

Crate:

crates/digest/forgeyard-digest/

Tree:

src/
├── lib.rs
├── digest.rs
├── blake3.rs
├── sha256.rs
├── alias.rs
├── encoding.rs
├── canonical.rs
└── error.rs

8. Digest Model

#![allow(unused)]
fn main() {
pub enum DigestAlgorithm {
    Blake3,
    Sha256,
}
}
#![allow(unused)]
fn main() {
pub struct Digest {
    pub algorithm: DigestAlgorithm,
    pub bytes: DigestBytes,
}
}

But content-specific types should wrap Digest.


9. Default Hashing Rule

Forgeyard internal content addressing:

BLAKE3

Interop:

SHA-256

Examples:

OCI
Bazel RBE
external provenance standards
vendor checksums

10. Digest Aliases

Same bytes may have:

BLAKE3 identity
SHA-256 alias
backend-native hash

Model explicitly:

#![allow(unused)]
fn main() {
pub struct DigestAlias {
    pub primary: Digest,
    pub alias: Digest,
}
}

11. Canonical Hashing Rule

Never hash raw Rust memory layout.

Canonical hashing must use stable schema encoding:

schema version
+
canonical fields
+
deterministic ordering

12. Schema Version in Hash

Any ID whose semantics depend on encoding must include:

schema_version

Example:

SourceSnapshotId
DerivationId
TreeObjectId
PolicyDigest

This prevents silent identity breakage.


13. Time Architecture

Crate:

crates/time/forgeyard-time/

Tree:

src/
├── lib.rs
├── timestamp.rs
├── duration.rs
├── deadline.rs
├── expiry.rs
├── clock.rs
├── monotonic.rs
└── test_clock.rs

14. Time Types

#![allow(unused)]
fn main() {
pub struct Timestamp(...);
pub struct Duration(...);
pub struct Deadline(...);
pub struct LeaseExpiry(...);
}

Do not use raw i64 milliseconds throughout domain code.


15. Wall Clock vs Monotonic Time

Wall clock:

audit timestamp
created_at
release time
provider event time

Monotonic:

timeouts
lease local countdown
performance duration

Do not mix them.


16. Clock Trait

#![allow(unused)]
fn main() {
pub trait Clock: Send + Sync {
    fn now(&self) -> Timestamp;
    fn monotonic_now(&self) -> MonotonicInstant;
}
}

Production:

SystemClock

Tests:

TestClock

17. Why Inject Clock

Avoid:

#![allow(unused)]
fn main() {
SystemTime::now()
}

inside domain services.

Benefits:

deterministic tests
expiry simulations
lease tests
approval expiry tests
release window tests

18. Core Error Architecture

Crate:

crates/error/forgeyard-error/

Tree:

src/
├── lib.rs
├── code.rs
├── category.rs
├── diagnostic.rs
├── retry.rs
├── user_message.rs
├── context.rs
└── source.rs

19. Error Philosophy

Do not create:

#![allow(unused)]
fn main() {
pub enum ForgeyardError {
    Everything...
}
}

Each subsystem owns typed errors.

Core provides common metadata.


20. Error Metadata

#![allow(unused)]
fn main() {
pub struct ErrorMetadata {
    pub code: ErrorCode,
    pub category: ErrorCategory,
    pub retry: RetryClass,
    pub user_safe: bool,
}
}

21. Error Categories

#![allow(unused)]
fn main() {
pub enum ErrorCategory {
    InvalidInput,
    Conflict,
    NotFound,
    PermissionDenied,
    Authentication,
    PolicyDenied,
    Infrastructure,
    DependencyUnavailable,
    Corruption,
    Timeout,
    Cancelled,
    InternalInvariant,
}
}

22. Retry Classification

#![allow(unused)]
fn main() {
pub enum RetryClass {
    Never,
    Immediate,
    Backoff,
    Reconcile,
    HumanActionRequired,
}
}

This is essential for distributed reliability.


23. Error Source Separation

Internal diagnostic:

full technical context

User/API message:

safe explanation

Audit/log:

structured context

Never expose secrets through error formatting.


24. anyhow Policy

anyhow may be used:

CLI composition
one-off admin tools
top-level application bootstrap

Do not use anyhow::Error as domain API.


25. thiserror / Typed Error Policy

Subsystem crates should expose:

#![allow(unused)]
fn main() {
#[derive(thiserror::Error)]
pub enum VcsError { ... }
}

or equivalent typed errors.


26. Forgeyard Modes

Core:

#![allow(unused)]
fn main() {
pub enum ForgeyardMode {
    Standalone,
    DistributedServer,
    DistributedAgent,
    ClientOnly,
}
}

Do not encode mode as booleans:

is_server
is_local
is_distributed

27. Capability Architecture

Forgeyard should reason using capabilities.

#![allow(unused)]
fn main() {
pub trait Capability {
    type Id;
}
}

Examples:

CanExecuteLinux
CanBuildRust
CanAccessAndroidDevice
CanSignWindows
CanUseXcode
CanRunConfidential

28. Capability Categories

#![allow(unused)]
fn main() {
pub enum CapabilityKind {
    Platform,
    Toolchain,
    Runtime,
    Device,
    Security,
    Network,
    Storage,
    Signing,
    Execution,
}
}

29. Capability IDs

Capabilities must be typed and stable.

Example:

#![allow(unused)]
fn main() {
pub struct CapabilityId(Digest);
}

A capability that includes versioned identity:

Xcode 18.0 SDK X

should not collapse into:

"macos"

30. Core Invariants

Crate module:

forgeyard-core/src/invariant.rs

Provides utilities for invariant enforcement.

Examples:

job terminal state cannot transition back to Running
expired lease cannot complete job
snapshot digest cannot change
candidate cannot be submitted against wrong target

Subsystems define actual rules.

Core provides pattern.


31. Invariant Violation

#![allow(unused)]
fn main() {
pub struct InvariantViolation {
    pub code: InvariantCode,
    pub entity: Option<EntityRef>,
    pub description: String,
}
}

Invariant violations are serious internal errors.

They should not be silently retried.


32. State Modeling Principle

Persisted distributed state:

enum + transition validation

Do not use typestate as primary persisted-state architecture.

Example:

#![allow(unused)]
fn main() {
pub enum JobState {
    Pending,
    Eligible,
    Leased,
    Preparing,
    Running,
    UploadingOutputs,
    Succeeded,
    Failed,
    Cancelled,
    TimedOut,
    Lost,
}
}

Transition logic lives separately.


33. State Transition Pattern

Recommended module:

state.rs
transition.rs
invariant.rs
reason.rs

Example:

#![allow(unused)]
fn main() {
pub fn transition(
    from: JobState,
    event: JobEvent,
) -> Result<JobState, JobTransitionError>
}

34. Local Typestate Usage

Typestate is useful for:

validated configuration builders
resource ownership
transaction builders
protocol handshake builders
candidate construction

Avoid across asynchronous persisted entity lifecycle.


35. Tenant Model

Core type:

#![allow(unused)]
fn main() {
pub struct TenantId(Ulid);
}

Optional standalone configuration may use:

LocalTenant

but internal domain should remain tenant-aware enough for distributed/enterprise evolution.


36. Organization / Project Model

Recommended:

Tenant
  ↓
Organization
  ↓
Project
  ↓
Repository / Pipelines / Runs

Do not require organization hierarchy in standalone UI.

But domain can support it.


37. ProjectRef

#![allow(unused)]
fn main() {
pub struct ProjectRef {
    pub tenant: TenantId,
    pub project: ProjectId,
}
}

Useful for isolation and audit.


38. Principal Model Boundary

Core only knows:

#![allow(unused)]
fn main() {
pub struct PrincipalId(Ulid);
}

Identity providers live elsewhere.


39. Principal Kinds

Higher identity model may represent:

human
service
runner
bot
automation

Core should only define minimal neutral kind if widely needed.


40. Actor Model

Audit/events need actor:

#![allow(unused)]
fn main() {
pub enum ActorRef {
    Principal(PrincipalId),
    Runner(RunnerId),
    System(SystemActor),
}
}

Avoid fake users for system operations.


41. Core References

Avoid cross-crate object embedding.

Prefer identifiers:

#![allow(unused)]
fn main() {
pub struct ArtifactRef {
    pub id: ArtifactId,
}
}

not:

#![allow(unused)]
fn main() {
struct Run {
    artifact: FullArtifactObject,
}
}

for persisted distributed domains.


42. Version Types

Crate:

crates/protocol/forgeyard-version/

Model:

#![allow(unused)]
fn main() {
pub struct ProtocolVersion {
    pub major: u16,
    pub minor: u16,
}
}

Also:

ConfigSchemaVersion
EventSchemaVersion
SnapshotSchemaVersion
DerivationSchemaVersion

Do not reuse one global version for everything.


43. Versioning Domains

Forgeyard should version independently:

wire protocol
REST API
config schema
event schema
database schema
source snapshot schema
derivation schema
lock schema
plugin API

44. Compatibility Rule

For internal protocol:

N supports N and N-1 where practical

rolling upgrade strategy.

Major incompatibility:

explicit handshake rejection

45. Envelope Architecture

Crate:

forgeyard-envelope

Model:

#![allow(unused)]
fn main() {
pub struct Envelope<T> {
    pub version: ProtocolVersion,
    pub message_id: MessageId,
    pub correlation_id: Option<CorrelationId>,
    pub sent_at: Timestamp,
    pub payload: T,
}
}

46. Correlation IDs

#![allow(unused)]
fn main() {
pub struct CorrelationId(Ulid);
}

Used for:

request/response
logs
tracing
reconciliation
audit context

47. Message IDs

At-least-once systems require stable message identity.

#![allow(unused)]
fn main() {
pub struct MessageId(Ulid);
}

48. Configuration Architecture

crates/config/
├── forgeyard-config/
├── forgeyard-config-loader/
├── forgeyard-config-schema/
└── forgeyard-config-policy/

49. Config Separation

Three layers:

RawConfig
    ↓ parse
ParsedConfig
    ↓ validate
ValidatedConfig

Do not let arbitrary raw RON become runtime authority.


50. Config Sources

Priority may include:

compiled defaults
system config
project config
user config
environment
CLI overrides

Exact precedence must be deterministic.


51. Config Provenance

Each resolved config value may optionally expose source:

#![allow(unused)]
fn main() {
pub struct ConfigValue<T> {
    pub value: T,
    pub origin: ConfigOrigin,
}
}

Useful for forgeyard doctor and explainability.


52. Secret Separation

Configuration contains:

SecretRef

not secret value.

Example:

database_password: Secret("prod/postgres")

53. Environment Variables

Environment is an input boundary.

Rules:

explicit allowlist
typed parse
no hidden behavior

Avoid business logic directly calling:

#![allow(unused)]
fn main() {
std::env::var(...)
}

throughout code.


54. Config Validation

Examples:

Postgres selected but connection config missing
OIDC enabled without issuer
mTLS enabled without trust root
Iroh selected as sole authoritative CAS -> reject

55. Config Policy

Some combinations are syntactically valid but architecturally unsafe.

forgeyard-config-policy handles these.


56. Config Schema Migration

Config schema versions:

v1
v2
...

Migration can be:

automatic safe migration
warning
manual required

57. Public API Model

Crate:

forgeyard-api-model

Public DTOs must be separate from domain models.

Example:

#![allow(unused)]
fn main() {
pub struct RunResponse { ... }
}

Convert:

domain -> DTO
DTO -> validated command

58. Internal Wire Model

Crate:

forgeyard-wire

Contains versioned Postcard messages.

Do not serialize arbitrary internal structs directly.


59. Why Separate Domain / Wire / API Types

Allows:

internal refactor
without API break

and:

wire evolution
without DB/domain corruption

60. Serialization Policy

Domain types:

not automatically serialized everywhere

Internal DTOs:

serde + Postcard

Public DTOs:

serde + JSON

Human config:

serde + RON

61. Deterministic Serialization

For canonical identity:

BTreeMap
sorted vectors
explicit enum representation
schema version

Avoid nondeterministic map order.


62. Path Types

Do not use raw PathBuf for remote/canonical domain paths.

Define:

#![allow(unused)]
fn main() {
pub struct CanonicalRepoPath(...);
pub struct SandboxPath(...);
pub struct HostPath(PathBuf);
}

Prevent mixing host path with repository path.


63. URI / URL Types

Typed:

#![allow(unused)]
fn main() {
pub struct RepositoryUrl(...);
pub struct ApiUrl(...);
pub struct SecretSafeUrl(...);
}

Avoid accidentally logging credentials.


64. Byte Size Types

#![allow(unused)]
fn main() {
pub struct ByteSize(u64);
}

Do not mix:

bytes
KiB
MiB

through raw integers.


65. Resource Types

#![allow(unused)]
fn main() {
pub struct CpuCount(...);
pub struct MemoryBytes(...);
pub struct DiskBytes(...);
}

Scheduler will depend on these.


66. Percentage / Ratio Types

Use bounded types for:

cache probability
resource utilization
retry jitter

Avoid arbitrary f64.


67. Retry Policy Types

#![allow(unused)]
fn main() {
pub struct RetryPolicy {
    pub max_attempts: NonZeroU16,
    pub backoff: BackoffPolicy,
}
}

68. Backoff Policy

#![allow(unused)]
fn main() {
pub enum BackoffPolicy {
    Fixed(Duration),
    Exponential {
        initial: Duration,
        max: Duration,
        jitter: JitterPolicy,
    },
}
}

69. Cancellation Token Boundary

Cancellation is operational, not persisted state.

Use Tokio cancellation primitives behind service interfaces.

Persist final cancellation intent/state separately.


70. Domain Command Pattern

Commands represent intent:

#![allow(unused)]
fn main() {
pub struct StartRun { ... }
pub struct CancelJob { ... }
pub struct RegisterRunner { ... }
}

Do not directly mutate domain structs from API handlers.


71. Query Pattern

Queries:

#![allow(unused)]
fn main() {
pub struct GetRun { ... }
pub struct ListJobs { ... }
}

Avoid forcing full CQRS complexity, but separate read intent when useful.


72. Domain Event Pattern

#![allow(unused)]
fn main() {
pub struct JobStarted { ... }
pub struct RunnerRegistered { ... }
}

Events describe facts, not commands.


73. Event ID

#![allow(unused)]
fn main() {
pub struct EventId(Ulid);
}

All durable events must have unique identity.


74. Event Envelope

#![allow(unused)]
fn main() {
pub struct EventEnvelope<E> {
    pub schema_version: EventSchemaVersion,
    pub event_id: EventId,
    pub occurred_at: Timestamp,
    pub actor: ActorRef,
    pub correlation_id: Option<CorrelationId>,
    pub event: E,
}
}

75. At-Least-Once Foundation

Core assumptions:

messages may repeat
events may repeat
responses may be lost
workers may crash

Therefore:

idempotency
leases
reconciliation

are first-class architectural principles.


76. Idempotency Key

#![allow(unused)]
fn main() {
pub struct IdempotencyKey(String);
}

Strong limits:

non-empty
max length
safe character policy

77. Idempotency Scope

#![allow(unused)]
fn main() {
pub struct IdempotencyScope {
    pub tenant: TenantId,
    pub operation: OperationId,
}
}

78. Operation IDs

Typed operation identifiers help audit/retry:

#![allow(unused)]
fn main() {
pub struct OperationId(Ulid);
}

79. Capability Registry Boundary

Do not use global singleton registries.

Construct registries at application bootstrap.

#![allow(unused)]
fn main() {
pub struct CapabilityRegistry<T> {
    entries: ...
}
}

Owned by application/service context.


80. Registry Invariants

  1. duplicate IDs rejected;
  2. deterministic lookup;
  3. immutable after bootstrap where possible;
  4. explicit dynamic registration only where plugin architecture requires it.

81. Application Context

Potential crate:

crates/app-context/forgeyard-app-context/

Contains application-layer references to services.

It is not a foundational dependency.

Domain crates must not depend on application context.


82. Logging Context

Core types for safe structured fields:

TenantId
ProjectId
RunId
JobId
CorrelationId

Do not log entire arbitrary domain structs.


83. Secret Redaction

Implement redaction wrappers:

#![allow(unused)]
fn main() {
pub struct Secret<T>(T);
}

Debug output:

<redacted>

But real secret storage belongs in secrets subsystem.


84. Sensitive String

For tokens/URLs that may contain secrets:

#![allow(unused)]
fn main() {
pub struct SensitiveString(...);
}

No Display unless explicitly safe.


85. Debug / Display Rules

IDs:

Display allowed

Secrets:

Display denied/redacted

Large domain structs:

structured logging only

86. Validation Types

Use constructors that guarantee validity:

#![allow(unused)]
fn main() {
pub struct ProjectName(String);
}
#![allow(unused)]
fn main() {
impl ProjectName {
    pub fn new(value: String) -> Result<Self, ProjectNameError>;
}
}

No invalid value after construction.


87. Name Rules

Examples:

ProjectName
PipelineName
RunnerName
LabelName

Validation may include:

length
Unicode policy
reserved names
control character denial

88. Label Model

#![allow(unused)]
fn main() {
pub struct Label {
    pub key: LabelKey,
    pub value: Option<LabelValue>,
}
}

Useful across:

runners
projects
changes
artifacts
deployments

89. Metadata Rule

Avoid generic unbounded:

#![allow(unused)]
fn main() {
HashMap<String, String>
}

for critical domain semantics.

Use typed fields.

Generic metadata is allowed only for extension/non-authoritative contexts.


90. Extension Metadata

#![allow(unused)]
fn main() {
pub struct ExtensionMetadata {
    pub namespace: ExtensionNamespace,
    pub payload: BoundedBytes,
}
}

Versioned and size-limited.


91. Bounded Types

Prevent unbounded inputs:

BoundedString
BoundedBytes
BoundedVec

especially in:

protocols
API
logs
comments
labels
provider payload metadata

92. Size Limits

Foundational constants/config:

max message size
max label count
max config size
max metadata field size

Enforced before expensive processing.


93. Memory Safety Rule

No unsafe in foundational crates unless unavoidable and separately justified.

Prefer:

#![allow(unused)]
#![forbid(unsafe_code)]
fn main() {
}

for:

forgeyard-core
forgeyard-ids
forgeyard-time
forgeyard-config-model

94. Unsafe Boundary

If unsafe is required later:

native
platform
FFI
high-performance specialized crates

keep it isolated.


95. Lint Architecture

Workspace lints in root Cargo.toml.

Recommended classes:

warnings
unused
clippy correctness
suspicious
complexity
perf
pedantic selectively

Do not enable noisy lint rules blindly.


96. Missing Docs

Public foundational API should require documentation.

#![allow(unused)]
#![warn(missing_docs)]
fn main() {
}

where practical.


97. Dependency Policy

Foundation crates should have minimal dependencies.

Example:

forgeyard-ids:
  ulid
  serde optional
  forgeyard-digest

Avoid:

tokio
reqwest
sqlx
axum

in foundational crates.


98. Feature Policy

Foundation features must be small and semantic.

Bad:

full
everything

Good:

serde
postcard
test-util

99. Test Utility Feature

test-util may expose:

fake IDs
TestClock
deterministic digests
builders

but should not leak into release logic.


100. forgeyard-core-test

Dedicated test support crate:

src/
├── lib.rs
├── id.rs
├── clock.rs
├── builder.rs
├── fixtures.rs
└── assertions.rs

101. Deterministic Test IDs

Tests may use seeded IDs.

Never rely on random ordering in assertions.


102. Property Tests

Foundation properties:

ID round-trip
digest round-trip
canonical encoding stable
time ordering
bounded string validation
state transition invariants

103. Fuzz Targets

Initial fuzz:

ID parsing
digest parsing
RON config parsing
Postcard envelope decoding
bounded input decoding
canonical serialization

104. Miri

Core crates are excellent candidates for periodic Miri testing.


105. Documentation

Each foundational crate gets:

README.md
crate-level rustdoc

System-level docs:

docs/architecture/core-foundation.md

106. Root Architecture Manifest

architecture.ron should classify foundation crates.

Example:

(
    crates: {
        "forgeyard-ids": (
            layer: Primitive,
            area: "foundation",
        ),

        "forgeyard-core": (
            layer: Domain,
            area: "foundation",
            allowed_dependency_layers: [Primitive],
        ),

        "forgeyard-config-loader": (
            layer: Adapter,
            area: "config",
        ),
    },
)

107. Architecture Checker

Tool:

tools/forgeyard-architecture-check/

Checks:

forbidden dependency direction
forbidden external dependencies
cycle-risk patterns
platform leakage
adapter leakage into domain

108. Example Forbidden Imports

Foundation crates must not depend on:

sqlx
tokio-postgres
axum
dioxus
git libraries
cloud SDKs
Docker clients
Kubernetes clients

109. Example Dependency Flow

forgeyard-ids
      ↑
forgeyard-core
      ↑
forgeyard-vcs-model
      ↑
forgeyard-vcs
      ↑
forgeyard-vcs-git

Never:

forgeyard-core
      ↓
forgeyard-vcs-git

110. Configuration Dependency Flow

forgeyard-config
      ↑
forgeyard-config-schema
      ↑
forgeyard-config-policy
      ↑
forgeyard-config-loader

forgeyard-config should not know:

filesystem
environment
CLI

if those can be kept in loader/adapters.


111. Protocol Dependency Flow

forgeyard-version
      ↑
forgeyard-envelope
      ↑
forgeyard-wire

Public API model remains separate.


112. Database Boundary

Foundation has no DB structs.

Bad:

#![allow(unused)]
fn main() {
#[derive(sqlx::FromRow)]
pub struct Project { ... }
}

inside domain.

Correct:

PostgresRow
   ↓ convert
Project

113. UI Boundary

No Dioxus types in domain.

Correct:

domain state
   ↓ API DTO
UI view model

114. Axum Boundary

No Axum extractor types in domain services.

Routes translate:

HTTP request
  ↓
validated command
  ↓
service

115. Tokio Boundary

Async service traits may use async abstraction.

But simple value/model crates should not depend on Tokio.


116. Rayon Boundary

Rayon belongs in CPU-heavy implementation crates.

Never foundational domain.


117. File System Boundary

Core paths are semantic path types.

Host filesystem access belongs in adapters/services.


118. Environment Boundary

Core does not read process environment.

forgeyard-config-loader does.


119. Network Boundary

Core has no network clients.


120. Process Execution Boundary

Core never invokes commands.

All tool/process execution later goes through executor abstractions.


121. Serialization Upgrade Tests

Maintain golden encodings for important stable protocol/config objects.

Do not golden-test every internal Rust struct.


122. Canonical Encoding Tests

For hash-sensitive structures:

field order changes must not silently change identity

Canonical format should be explicit rather than relying on derive behavior.


123. Compatibility Test Directory

tests/compatibility/
├── protocol_n_minus_1.rs
├── config_upgrade.rs
├── snapshot_schema.rs
└── derivation_schema.rs

Foundation provides the version primitives these use.


124. New Forgeyard Bootstrap Order

Recommended implementation sequence:

1. root workspace
2. forgeyard-digest
3. forgeyard-ids
4. forgeyard-time
5. forgeyard-error
6. forgeyard-version
7. forgeyard-envelope
8. forgeyard-core
9. forgeyard-config
10. config schema/loader/policy
11. architecture checker
12. core testkit

Only after this should higher domains start.


125. Phase 1 — Root Workspace

Create:

Cargo.toml
Cargo.lock
rust-toolchain.toml
rustfmt.toml
clippy.toml
deny.toml
architecture.ron
README.md

Exit:

cargo check --workspace

works on empty foundational crates.


126. Phase 2 — Digest

Implement:

DigestAlgorithm
Digest
Blake3Digest
Sha256Digest
aliases
parsing/display
canonical hashing helpers

Acceptance:

roundtrip
stable display
invalid input rejection

127. Phase 3 — IDs

Implement initial IDs for:

tenant
project
pipeline
run
job
runner
lease
repository
snapshot
artifact
change
principal

128. Phase 4 — Time

Implement:

Timestamp
Duration
Deadline
Clock
SystemClock
TestClock

129. Phase 5 — Error Metadata

Implement:

ErrorCode
ErrorCategory
RetryClass
diagnostic metadata
safe user message

130. Phase 6 — Version / Envelope

Implement:

ProtocolVersion
SchemaVersion types
MessageId
CorrelationId
Envelope<T>

131. Phase 7 — Core

Implement:

ForgeyardMode
TenantRef
ProjectRef
ActorRef
Capability basics
InvariantViolation

Keep small.


132. Phase 8 — Config

Implement:

raw parse
schema version
validation
source precedence
config provenance

133. Phase 9 — Architecture Check

Parse Cargo metadata.

Reject:

domain -> adapter
primitive -> service
core -> platform

violations.


134. Phase 10 — Testkit

Add:

TestClock
deterministic ID factories
assertion helpers
canonical digest fixtures

135. Acceptance Tests

  1. ProjectId cannot be confused with RunId.
  2. malformed ID parsing fails.
  3. BLAKE3 digest round-trips.
  4. SHA-256 alias can reference same content.
  5. canonical hashing is deterministic.
  6. Timestamp and Duration cannot be accidentally mixed.
  7. TestClock controls expiry deterministically.
  8. retry classification survives error conversion.
  9. secrets are redacted in Debug/Display wrappers.
  10. RON config parses into raw config.
  11. invalid config never becomes ValidatedConfig.
  12. config source precedence is deterministic.
  13. protocol envelope rejects unsupported major version.
  14. message ID/correlation ID survive Postcard round-trip.
  15. architecture checker rejects core→Git adapter dependency.
  16. architecture checker rejects domain→Postgres adapter dependency.
  17. foundation crates build without network/database/UI dependencies.
  18. core crate can compile with #![forbid(unsafe_code)].
  19. canonical serialization stable across repeated runs.
  20. golden compatibility tests detect schema breaking changes.

136. Production Readiness Gates

Do not consider the foundation stable until:

typed IDs are established
digest schema is explicit
clock abstraction is used
errors are typed/retry-classified
config validation pipeline works
schema versions are independent
protocol envelope works
architecture checker prevents invalid edges
foundation crates have no adapter leakage

137. Architectural Invariants

  1. No core entity ID is a plain string.
  2. Content IDs are digest-based.
  3. External IDs never become internal authority directly.
  4. Core has no SQL dependency.
  5. Core has no Axum dependency.
  6. Core has no Dioxus dependency.
  7. Core has no VCS adapter dependency.
  8. Core has no cloud SDK dependency.
  9. Core never executes external processes.
  10. Domain code does not read environment variables directly.
  11. Domain code does not read wall clock directly.
  12. Canonical identity formats include schema version.
  13. Retry behavior is explicit.
  14. Secret-bearing values are not printable by default.
  15. Public API DTOs are separate from domain types.
  16. Wire DTOs are separate from domain types.
  17. Configuration is parsed then validated before use.
  18. Persisted distributed states use explicit enums + transition rules.
  19. Typestate is used only where it improves local correctness.
  20. Architecture rules are machine-enforced.
  21. Foundation crates remain small and stable.
  22. Generic metadata cannot replace typed domain fields.
  23. Platform-specific logic never leaks into foundation.
  24. One subsystem cannot bypass capability APIs by importing adapters directly unless it is an application composition crate.
  25. The new Forgeyard repository must build its architecture from this foundation upward.

138. Final Core Architecture

                     forgeyard-core
                          │
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
  forgeyard-ids     forgeyard-time   forgeyard-digest
        │                 │                 │
        └─────────────────┼─────────────────┘
                          ▼
                   forgeyard-error
                          │
                          ▼
                  version/envelope
                          │
                          ▼
                  config contracts
                          │
                          ▼
                    domain layers

And the global dependency rule begins here:

foundation
   ↑
domain
   ↑
capability APIs
   ↑
services
   ↑
adapters
   ↑
applications

Never invert this.


139. First Commit Recommendation

The first meaningful new Forgeyard commit should contain only:

root workspace
architecture.ron
forgeyard-digest
forgeyard-ids
forgeyard-time
forgeyard-error
forgeyard-version
forgeyard-envelope
forgeyard-core
basic config types
architecture-check skeleton
tests

Do not immediately copy the old Forgeyard daemon, scheduler, runner, or UI into the new repository.

Those should be reintroduced subsystem-by-subsystem against the new contracts.


140. Old Forgeyard Migration Rule

Keep the old repository:

forgeyard-legacy/

or archive it remotely as:

forgeyard-legacy

Use it only for:

working algorithms
test cases
protocol lessons
UI ideas
bug history
operational lessons

Do not mechanically copy its architecture.

Migration process:

legacy behavior
  ↓
identify required capability
  ↓
map to new architecture
  ↓
write tests
  ↓
reimplement/adapt cleanly

This minimizes accidental preservation of old coupling.


141. Final Recommendation

Start the new Forgeyard.

Do not delete the old repository.

Treat the old Forgeyard as:

reference implementation + historical knowledge

and the new Forgeyard as:

architecturally authoritative production codebase

The new repository should be built from the bottom upward, beginning with this Core Domain & Foundation architecture, then adding storage, CAS, pipeline, scheduler, runner, sandbox/executor, security/policy, observability, release/deployment, API/UI, and finally advanced distributed/enterprise capabilities.