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
- IDs are never plain
String. - Entity IDs are globally unique.
- Content IDs derive from canonical content.
- Display representation is not internal representation.
- Parsing is fallible.
- Serialization is version-safe.
- Equality semantics are obvious from type.
- 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
- duplicate IDs rejected;
- deterministic lookup;
- immutable after bootstrap where possible;
- 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
ProjectIdcannot be confused withRunId.- malformed ID parsing fails.
- BLAKE3 digest round-trips.
- SHA-256 alias can reference same content.
- canonical hashing is deterministic.
TimestampandDurationcannot be accidentally mixed.TestClockcontrols expiry deterministically.- retry classification survives error conversion.
- secrets are redacted in Debug/Display wrappers.
- RON config parses into raw config.
- invalid config never becomes
ValidatedConfig. - config source precedence is deterministic.
- protocol envelope rejects unsupported major version.
- message ID/correlation ID survive Postcard round-trip.
- architecture checker rejects core→Git adapter dependency.
- architecture checker rejects domain→Postgres adapter dependency.
- foundation crates build without network/database/UI dependencies.
- core crate can compile with
#![forbid(unsafe_code)]. - canonical serialization stable across repeated runs.
- 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
- No core entity ID is a plain string.
- Content IDs are digest-based.
- External IDs never become internal authority directly.
- Core has no SQL dependency.
- Core has no Axum dependency.
- Core has no Dioxus dependency.
- Core has no VCS adapter dependency.
- Core has no cloud SDK dependency.
- Core never executes external processes.
- Domain code does not read environment variables directly.
- Domain code does not read wall clock directly.
- Canonical identity formats include schema version.
- Retry behavior is explicit.
- Secret-bearing values are not printable by default.
- Public API DTOs are separate from domain types.
- Wire DTOs are separate from domain types.
- Configuration is parsed then validated before use.
- Persisted distributed states use explicit enums + transition rules.
- Typestate is used only where it improves local correctness.
- Architecture rules are machine-enforced.
- Foundation crates remain small and stable.
- Generic metadata cannot replace typed domain fields.
- Platform-specific logic never leaks into foundation.
- One subsystem cannot bypass capability APIs by importing adapters directly unless it is an application composition crate.
- 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.