hecks

HECKS Architecture & Implementation Guide

Status: dated roadmap, last reconciled 2026-08-07. This is a plan, not a record of what is built. Phases it marks as new or research may have shipped, changed shape or never started since. It stays in docs/ because the README and CONTRIBUTING link it as the roadmap. For the current state read the README’s “Project status” section, the commit history, docs/implemented/ and future-features.md.

This document is the canonical engineering specification for Hecks. It combines architectural invariants, execution model, implementation guidance, and the feature roadmap into a single reference intended for humans and coding agents.

Restored 2026-08-07, corrected same day. This document was drafted, committed (ddb1ce2), and then reset away — all within the same session — before any of §8’s work actually started; a differently-architected Rust effort began fresh afterward on feat/rust-projection and is now substantially complete. §8/§9 below are rewritten to describe what was actually built, which diverges from this document’s own original plan in ways worth reading (it does the opposite of one of §8’s stated non-goals). Everything else in this document — Phases 1, 3–10 — was never started in the interim and is restored as originally written; spot-checked against the current codebase, not exhaustively re-audited section by section. docs/decisions/000{1,2,7,8,9,10} and hecks check_conformance, both named throughout, are restored alongside this file and confirmed still running against the current codebase.

How to use this document

Read the sections in order:

  1. Architectural Philosophy
  2. Architectural Invariants
  3. Execution Model
  4. Intermediate Representations
  5. Semantic & Capability Graphs
  6. Verification & Explainability
  7. Core Language
  8. Runtime
  9. Supporting Bluebooks
  10. Projections
  11. Canonical Corpus
  12. Feature Roadmap

Part I — Architectural Foundation

HECKS_IMPLEMENTATION_PLAN

Addendum: Foundational Architecture

Intent as the Primary Artifact

Hecks models organizational intent rather than software implementation. Bluebooks are the canonical description of why an organization behaves the way it does. Runtimes, APIs, databases, user interfaces, documentation, reports, AI assistants, and language projections are all derived artifacts. Every new feature should first ask whether it belongs in the intent layer before introducing implementation-specific concepts.

Execution Pipeline

Every command should move through an explicit deterministic pipeline:

  1. Intent resolution
  2. Runtime enrichment (UUIDs, clock, caller, external facts)
  3. Authorization
  4. Validation / invariants
  5. Policy evaluation
  6. Lifecycle evaluation
  7. Mutation planning
  8. Event planning
  9. Adapter execution
  10. Persistence
  11. Projection refresh
  12. Evidence generation

Each stage should document: - Inputs - Outputs - Replay behavior - Determinism guarantees - Failure semantics

Three Intermediate Representations

Authoring IR

Represents the canonical Bluebook independent of Ruby syntax. This is the durable semantic representation consumed by projectors.

Execution IR

Represents a fully bound command ready for execution after enrichment. It contains resolved identities, caller context, generated identifiers, timestamps, resolved references, and effective role information.

Evidence IR

Represents the outcome of execution. It records accepted/refused decisions, evaluated grounds, authority chain, mutations, emitted events, adapter interactions, evidence hashes, and provenance.

These three IRs should remain independent.

Capability Graph

The runtime should expose a graph of required capabilities and their fulfillments.

Examples:

Fulfillments may come from Bluebooks or adapters. The graph should power deployment analysis, onboarding, dependency visualization, and AI reasoning.

Semantic Graph

Canonical IR should expose a navigable semantic graph.

Examples:

Command -> Role Command -> Event Event -> Policy Policy -> Command Command -> Aggregate Aggregate -> Record Record -> Value Object

AI, projectors, verification, documentation, and visualization should operate on this graph instead of parsing Ruby.

Constraint Engine

Rules are constraints rather than simple validations.

The runtime should eventually explain blocked work by traversing dependency chains and identifying the next satisfiable organizational action.

Explainability

Every runtime decision should produce an explanation tree.

Examples:

Accepted because… Refused because… Blocked because…

Different projections should render explanations for developers, auditors, regulators, and business users without changing evidence.

Verification

Add a verification subsystem capable of identifying:

Target command:

hecks verify

Status note (2026-08-07 restore): substantially real already, under a different name — hecks model_check (built before this document was first drafted, survived the reset that took the rest of this plan) covers unreachable lifecycle states, transitions nothing can ever fire, saga states no handler chain reaches, unreachable compensation from_states, dispatches to nonexistent commands, and handlers listening for events nothing emits — all as static analysis over the same canonical IR the runtime dispatches against, the lightweight formal-methods leg this section describes. given/ensures (Design by Contract pre/postconditions) also exist and are load-bearing throughout the corpus and the Rust projection (§8) both. Not yet covered here specifically: dead commands/events/policies, duplicate rules, unused roles/value objects — the dead-code-shaped half of this list, distinct from model_check’s reachability/consistency half.

Visualization

Generate diagrams directly from canonical IR:

No hand-maintained diagrams should be required.

AI Layer

AI should operate as an assistant rather than an execution engine.

Responsibilities include:

Runtime behavior always remains deterministic.

Architecture Decision Records

Every architectural invariant should be captured as an ADR. The implementation plan and ADRs together become the durable engineering memory of Hecks.


These sections should be merged into the existing HECKS_IMPLEMENTATION_PLAN.md. They elevate the execution model, semantic graph, verification, and explainability to first-class architectural concepts while preserving the small core language.


Part II — Implementation Roadmap

Hecks Implementation Plan

Agent-ready engineering briefs for the next phase of Hecks.

This document is intentionally implementation-oriented. Each feature states the current Hecks correlation, the target behavior, likely code seams, Ruby DSL examples, acceptance criteria, dependencies, and explicit non-goals. The goal is that an engineering agent can pick up one section and implement it without re-deriving the architecture from the original design conversation.

Scope note (2026-08-07): this roadmap is pruned to unimplemented work only — supporting Bluebooks (formerly a separate item) is removed as already implemented. The Rust/WASM/host-language projection sections were briefly removed as obsolete after the Rust runtime retirement (docs/implemented/rust-experiment.md), then restored, then narrowed by decision: there is one Rust runtime target in this roadmap (§8, plus its WASM compile output in §9), not a family of separate host-language projections — the Ruby-hosts-Rust, Python, Go, and Java sections that used to describe a second, generic, IR-interpreting Rust core were removed rather than kept as speculative alternatives. §8 itself is a code generator, not an interpreter: it consumes canonical IR at build time and emits native Rust source, one module per held era as an initial implementation (a scaling concern, not a permanent architecture — see §8). The governing invariant is that no canonical IR is parsed or interpreted at runtime; generated static metadata for introspection/diagnostics is fine as long as it’s read, never interpreted as instructions. §8 starts early (Phase 2, right after core hardening) rather than waiting for later IR-shaping work to finish — per docs/decisions/0010-ruby-is-the-reference-implementation.md, Ruby serves as both the implementer’s guide and, through a continuously-run differential harness, the correctness oracle, so IR/behavior changes from later phases become fast-following, harness-verified extensions rather than a reason to have waited. This is explicitly framed as a second attempt that must not repeat the retired architecture (a hand-written second interpreter maintained through differential parity). Sections are renumbered sequentially; numbers no longer match any prior draft or external references to this document.

Working rules for agents

  1. Preserve the existing Bluebook grammar unless a feature explicitly requires syntax.
  2. Prefer changes to IR, runtime, projections, adapters, or supporting Bluebooks over new keywords.
  3. Preserve replay and era behavior.
  4. Preserve existing Bluebooks unless a feature explicitly calls for a migration.
  5. Every new semantic feature must round-trip through to_h / Bluebook::Assembly.
  6. Every new projection should consume canonical IR rather than Ruby implementation objects where practical.
  7. Every new imperative capability must enter through a port/adapter.
  8. Do not introduce a second hand-written interpretation of Bluebook semantics unless explicitly approved.
  9. Add focused specs next to the subsystem being changed, then add corpus/golden coverage where the wire representation changes.
  10. Treat lib/hecks/language/ and the self-hosted language declarations as load-bearing specifications, not documentation.

1. One command → one role

Status: Partially present
Priority: P0
Complexity: S

Current correlation

Commands already carry a single role field through Bluebook::DSL::CommandBuilder and Bluebook::IR::Command. Runtime authorization already compares the current caller role against that field in Runtime::CommandRules::Authorization. The current language still allows commands with no role, and comments in the authorization code note that a meaningful portion of Banking currently declares none.

Likely files:

Target behavior

A command may never declare multiple roles. The DSL already structurally satisfies this because role is a scalar assignment, but the invariant should become explicit in the language model and documentation. Whether every externally invokable command must eventually have a role should be a separate migration, because current role-less commands may represent system reactions or intentionally unguarded operations.

The immediate feature is therefore: role cardinality is zero-or-one, with a roadmap toward exactly one for externally proposed commands.

Ruby DSL

command "Withdraw" do
  role "Teller"
  goal "Give cash to an account holder"

  reference_to Account
  attribute :amount, Money

  given("the account has enough available funds") {
    balance.cents >= amount.cents
  }

  then_set :balance, decrement: :amount
  emits "CashWithdrawn"
end

Do not add this:

command "Withdraw" do
  role "Teller"
  role "Branch manager" # invalid modeling; second assignment must not mean OR
end

Implementation hint

Add a language-level statement to the self-hosted behavior grammar expressing that a command has at most one role. If the builder currently silently overwrites a second role call, make duplicate calls raise Malformed so authoring mistakes cannot disappear.

A simple builder guard is sufficient:

def role(value)
  raise Malformed, "#{@name} declares role twice" if defined?(@role) && @role
  @role = value
end

Keep IR shape scalar:

role: "Teller"

Do not change it to an array.

Acceptance criteria

Non-goals


2. act_as authority transition

Status: Done (2026-08-07) — on a smaller shape than this section originally sketched, and deliberately so. Re-reading the “Runtime execution” sketch below closely showed Governance.authorize_transition! called by APPLICATION code, before Runtime::Caller.as(role:) wraps a nested dispatch — not woven into CommandInterpreter/Dispatcher/ CommandRules::Authorization at all. So none of those three changed. lib/hecks/framework/bluebook/governance.bluebook gained a second aggregate, RoleTransition (from_role/to_role, Grant/Revoke, an Allowed query) — the fact “role X may act as role Y” that RoleAssignment alone could not answer. spec/act_as_spec.rb demonstrates the full pattern against two separate registries (Governance’s own, and a real Pizzas boot): an application checks Allowed, then wraps the downstream dispatch in Hecks.as_caller(role: ...) ; the unauthorized-refusal path (no Allowed row → the app-level check itself refuses, before any dispatch) and the role-restoration proof (a THIRD dispatch, right after the nested one, still authorized under the ORIGINAL role) are both covered. Also confirmed and left alone on purpose: “replay does not invoke live Governance decisions” was already true before this section existed — Fuzzing::Replay never binds a Caller, and refuse_role_mismatch’s first line is return unless caller. A facade-level calls "...", as: "..." sugar (this section’s own DSL sketch below) is real, later work — see “Non-goals”.
Priority: P0
Complexity: M

Current correlation

Runtime::Caller already provides:

Hecks::Runtime::Caller.as(role: "Teller") { ... }

and authorization reads that ambient role. Dispatcher reactions explicitly clear caller context because system reactions should not inherit the initiating human’s authority.

That is very close to the execution primitive required for act_as, but today it is merely a runtime caller binding rather than a governed, declared cross-command transition.

Likely files:

Target behavior

When command A invokes command B across a domain or aggregate boundary, A may declare that the downstream invocation is performed as B’s required role. Governance decides whether that transition is permitted.

This preserves the invariant that each command has one role while supporting workflows where the initiating actor’s organizational role differs from the downstream domain role.

Ruby DSL direction

Prefer declaration on the cross-command call rather than global role inheritance.

Illustrative syntax:

command "RegisterCustomerAccount" do
  role "Customer administrator"
  goal "Open the customer's operating relationship"

  calls "Customers::Customer.Register",
        as: "Customer registrar"
end

Or if cross-command execution remains policy-oriented:

policy "RegisterCustomerAfterIdentity" do
  on "IdentityRegistered"

  trigger "Customers::Customer.Register",
          act_as: "Customer registrar"
end

The exact surface spelling can be chosen after inspecting the existing trigger / orchestration DSL. The IR should care about the semantics, not the spelling.

Suggested IR

A call edge should contain:

{
  command: "Customers::Customer.Register",
  act_as: "Customer registrar"
}

Do not change the downstream command:

command "Register" do
  role "Customer registrar"
end

Runtime execution

Conceptually:

Governance.authorize_transition!(
  actor: current_actor,
  from_command: "Identity::RegisterCustomerAccount",
  from_role: "Customer administrator",
  to_command: "Customers::Customer.Register",
  to_role: "Customer registrar"
)

Runtime::Caller.as(role: "Customer registrar") do
  dispatcher.dispatch_flat("Customers::Customer.Register", **args)
end

The existing Caller.as should be reused rather than replaced.

Audit envelope

Preserve:

{
  actor: "identity-123",
  originating_role: "Customer administrator",
  effective_role: "Customer registrar",
  originating_command: "Identity::RegisterCustomerAccount",
  command: "Customers::Customer.Register"
}

The domain event itself may remain business-focused; the execution/evidence envelope can retain the authority chain.

Acceptance criteria

Non-goals


3. Governance Bluebook

Status: Done (2026-08-07) — lib/hecks/framework/bluebook/governance.bluebook (+ .hecksagon, Memory-persisted). RoleAssignment aggregate: composite identity (actor_id, role_name, starts_at) rather than the sketch’s synthetic assignment_id — no UUID adapter exists yet (§5), and a real assignment event is genuinely unique by actor+role+when it started, so none is needed. Assign/Revoke commands (Revoke sets ends_at, never deletes), one query (AssignmentsForActor, does not filter by ends_at — a caller decision, not baked in). The original sketch’s Hecks.port "Authorization" do operation "CanActAs" ... returns ... end doesn’t match the real port DSL (PortOperation has no returns — it’s attributes + required emits, an anti-corruption boundary, not a query); AssignmentsForActor is an ordinary query, the construct that actually answers “who holds what” — no new DSL surface needed anywhere in this build. spec/corpus_spec.rb/spec/model_check_spec.rb extended with a FRAMEWORK_MEMBERS glob (mirrors GRAMMAR_CHAPTERS’s flat-file pattern) so lib/hecks/framework/bluebook/*.bluebook gets the same must-load/must-have-a-corpus-script/no-unnamed-model-check-finding guarantees examples/* domains get. spec/governance_spec.rb (4 examples) + spec/corpus/governance.json, full suite green (1004 examples). Explicitly NOT done here: §2 (act_as wired to a real Governance.authorize_transition! — there is still no hook point in CommandRules::Authorization for this at all, confirmed by reading it; refuse_role_mismatch is a hard-coded ==), any port/query surface for OTHER domains to actually call into Governance at dispatch time, and §4/§5 (Identities, UUID adapter) — IdentityId here is Governance’s own owned value object, not a cross-domain reference.
Priority: P0
Complexity: L

Target

Create a canonical governance.bluebook that models organizational authority independently of business domains.

Governance should answer questions such as:

Ruby example

Hecks.bluebook "Governance" do
  vision "Organizational authority is explicit, reviewable, and historically explainable."
  supporting

  aggregate "RoleAssignment" do
    identified_by { assignment_id.value }

    attribute :assignment_id, AssignmentId
    attribute :actor_id, IdentityId
    attribute :role_name, RoleName
    attribute :scope, Scope
    attribute :starts_at, Timestamp
    attribute :ends_at, Timestamp, optional: true

    command "Assign" do
      role "Governance administrator"
      goal "Authorize an actor to perform a responsibility"

      attribute :assignment_id, AssignmentId
      attribute :actor_id, IdentityId
      attribute :role_name, RoleName
      attribute :scope, Scope
      attribute :starts_at, Timestamp
      attribute :ends_at, Timestamp, optional: true

      emits "RoleAssigned"
    end

    command "Revoke" do
      role "Governance administrator"
      goal "Remove organizational authority"

      reference_to RoleAssignment
      emits "RoleRevoked"
    end
  end

  query "AssignmentsForActor" do
    where(actor_id: :actor_id)
  end
end

The exact modeling should be driven by current Bluebook capabilities; avoid adding language solely for Governance.

Implementation hint

Build Governance first as an ordinary Bluebook using current records/aggregates, commands, queries, lifecycles, policies, and ports. Only promote something into the language if Governance cannot express a universal concept without distortion.

Expose Governance to runtime authorization through a port, not a direct dependency on the Ruby implementation.

Example runtime port concept:

Hecks.port "Authorization" do
  operation "CanActAs" do
    attribute :actor_id, String
    attribute :from_role, String
    attribute :to_role, String
    attribute :command, String
  end
end

Acceptance criteria

Non-goals


4. Identities Bluebook

Status: Done (2026-08-07) — lib/hecks/framework/bluebook/identity.bluebook (+ .hecksagon, Memory-persisted; chapter renamed from Identities to Identity on 2026-08-08, matching Governance’s own singular naming — the aggregate inside is ALSO named Identity, so its FQN is the slightly repetitive but unambiguous Identity::Identity). Attached to Banking for real via attaches "Identity" in examples/banking/bluebook/banking.hecksagon, the same mechanism Governance uses — see §31’s own note on Hecks::Framework. Identity (identity_id minted via §5’s Ports::IdentityGeneration.uuid — no natural key exists for “a newly recognized identity,” and this is the first real consumer of that port) and ExternalIdentifier (issuer/subject pair, natural key key.value). One correction to the sketch: ExternalIdentifier uses a real reference_to Identity rather than a bare owned value object — both aggregates share one bluebook (unlike Governance’s actor_id, which deliberately avoided a cross-domain reference to this exact domain before it existed), so there’s no reason to give up resolve_references’s real existence-checking; spec/identity_spec.rb confirms Link refuses against a nonexistent identity, not just that it succeeds against a real one. key’s derivation (SHA256 of issuer+subject, or any other scheme) stays a caller-side concern, per the original sketch’s own framing — no new port for it. spec/corpus_spec.rb/spec/model_check_spec.rb needed no changes — §3’s FRAMEWORK_MEMBERS glob picked this domain up automatically. Full suite green (1019 examples). Explicitly not done: any OIDC verification (§11); Governance’s actor_id was not retrofitted to reference this domain.
Priority: P0
Complexity: M

Target

Create identities.bluebook as the domain that relates authenticated identifiers to stable organizational identities. It should not become a junk-drawer User model.

The domain should support external identities such as OIDC issuer/subject pairs and locally minted identity IDs where there is no derivable identity.

Ruby example

Hecks.bluebook "Identities" do
  vision "Authentication proves an identifier; the organization decides what that identity means."
  supporting

  aggregate "Identity" do
    identified_by { identity_id.value }

    attribute :identity_id, IdentityId

    value_object "IdentityId" do
      attribute :value, String
      invariant("an identity id is present") { !value.to_s.empty? }
    end

    command "Register" do
      role "Identity registrar"
      goal "Recognize an identity"

      attribute :identity_id, IdentityId
      emits "IdentityRegistered"
    end
  end

  aggregate "ExternalIdentifier" do
    identified_by { key.value }

    attribute :key, ExternalIdentifierKey
    attribute :identity_id, IdentityId
    attribute :issuer, Issuer
    attribute :subject, Subject

    command "Link" do
      role "Identity registrar"
      goal "Associate an external authenticated subject with an identity"

      attribute :key, ExternalIdentifierKey
      attribute :identity_id, IdentityId
      attribute :issuer, Issuer
      attribute :subject, Subject

      emits "ExternalIdentifierLinked"
    end
  end
end

Derived IDs

For OIDC, the external identifier key can be derived deterministically from (issuer, subject).

Example conceptual adapter-side derivation:

key = Digest::SHA256.hexdigest("#{issuer}\0#{subject}")

The Bluebook need not know how it is derived if the adapter supplies it.

Acceptance criteria

Non-goals


5. UUID adapter and imperative enrichment

Status: Done (2026-08-07) — lib/hecks/ports/identity_generation.{port,rb}, modeled on Ports::Extraction (one adapter registry-wide implements the port; zero/many both refuse), not on Persistence’s heavier per-aggregate binding — different aggregates have no real reason to want different id-generation strategies. Two adapters: SecureRandomIdentity (real) and SequentialIdentity (deterministic, for specs — satisfies the “replaceable by deterministic test adapter” criterion directly). The sketch’s operation "UUID" do returns String end doesn’t match the real port DSL (no returns anywhere — same correction §3 already needed for its own port sketch); the real precedent for “call something and get a plain value back” is extraction.port’s shape, not DomainPort/PortOperation’s. The “replay never invokes it” half needed no new mechanism at all — confirmed by reading the real replay path (Fuzzing::Replay re-dispatches recorded args through the identical CommandInterpreter pipeline; nothing ever adds a key to args between a caller and hydrate), so a UUID minted for a creating command’s identity is just an ordinary string baked into that step’s own recorded args from the moment it’s generated — replaying those args never touches the adapter again, for the same reason Event#occurred_at’s own Time.now call (the one existing “environmental fact” precedent) needs no suppression either. Verified directly, not just architecturally: spec/ports/identity_generation_spec.rb mints an id, dispatches a real Pizzas::Order.CreatePizza with it, then replays the same recorded args through a fresh boot and confirms both the identical id and that SequentialIdentity’s own counter did not advance a second time. No CommandInterpreter/Dispatcher changes, no new .hecksagon binding syntax. Not done: facade-level automatic enrichment (omitting an identity argument and having a generated command method fill it in) — demonstrated as an explicit caller-side call, deliberately not built as new AggregateDoor codegen; nothing in the existing corpus was retrofitted to use this (natural keys remain preferred wherever one exists).
Priority: P0
Complexity: S–M

Current correlation

Hecks already has named ports and adapters and a deterministic replay model. DeclarationSnapshot demonstrates the strong separation between authoring and replay: structured IR can be reconstructed without rerunning Ruby source.

Target

When a creating command has no natural/derived reference, the runtime obtains a UUID through an adapter on first execution and freezes it into durable history. Replay never invokes the UUID adapter.

Bluebook remains declarative. UUID generation is an imperative environmental fact.

Ruby authoring example

Do not add a create keyword:

command "Register" do
  role "Identity registrar"
  goal "Recognize a new identity"

  attribute :identity_id, IdentityId
  emits "IdentityRegistered"
end

A façade may allow omission:

identities.identity.register

and the runtime enriches the command:

dispatcher.dispatch_flat(
  "Identities::Identity.Register",
  identity_id: { value: uuid_adapter.generate }
)

Port sketch

Hecks.port "IdentityGeneration" do
  operation "UUID" do
    returns String
  end
end

If the existing port DSL does not support returns, use the existing operation-output convention instead of expanding syntax.

Runtime rule

first execution:
intent → enrich imperative facts → execute → record facts/events

replay:
recorded facts/events → rebuild state

Acceptance criteria

Non-goals


6. report as business-facing read model

Status: Existing query + read_model; naming change only
Priority: P0
Complexity: XS–S

Current correlation

Hecks already has both query and read_model. The query/read-model guide explicitly describes read models as report-like cross-aggregate projections.

Target

Introduce report as the SME-facing spelling for read_model, preserving read_model as a permanent compatibility alias.

Do not replace query. A query is a request/question; a report is a declared view.

Ruby example

Current:

read_model "ComplianceDashboard" do
  reference_to Account
  include Account
  include CardPayment
  where(status: "disputed")
  order_by :amount, :desc
  limit 5
end

Preferred:

report "ComplianceDashboard" do
  description "One account and the disputed payments requiring review."

  reference_to Account
  include Account
  include CardPayment

  where(status: "disputed")
  order_by :amount, :desc
  limit 5
end

Keep:

query "SuspendedCustomers" do
  where(status: "suspended")
end

Implementation hint

Make the DSL builder dispatch both words to the same IR type initially. Update self-hosted syntax vocabulary using the existing rename/was: mechanism if appropriate so old eras remain valid.

Acceptance criteria

Non-goals


7. Canonical IR as durable source format

Status: Done (2026-08-07) — IR::Bluebook::IR_VERSION (currently 1) is now the first key to_h emits, distinct from a domain’s own version:; golden IR (all 9 fixtures) and running-a-runtime.md updated; Assembly needed no change (an unrecognized key is simply not read back). Otherwise strongly present already through to_h, Projector::Exporter, DeclarationSnapshot, Assembly.
Priority: P0
Complexity: M

Current correlation

Projector::Exporter serializes booted Bluebooks to hashes/JSON. Runtime::DeclarationSnapshot reconstructs a real Bluebook from JSON through Bluebook::Assembly. Era replay explicitly no longer depends on reparsing Ruby source.

This is the foundation for language-independent projections.

Target

Formalize canonical IR as a versioned contract that can be consumed without Ruby authoring. Ruby remains a high-quality authoring surface, but the IR is sufficient to reconstruct and project a Bluebook.

Ruby generation example

registry = Hecks.registry
json = Hecks::Projector::Exporter.json(registry)
File.write("payments.ir.json", json)

Round-trip invariant

original = registry.bluebook("Payments")

json = Hecks::Runtime::DeclarationSnapshot.dump(original)
restored = Hecks::Runtime::DeclarationSnapshot.load(json)

expect(restored.to_h).to eq(original.to_h)

Implementation hint

Add an explicit top-level IR metadata envelope if one does not already exist:

{
  ir_version: 1,
  bluebook: { ... }
}

Avoid changing the semantic tree merely to satisfy a projection language. Language projection layers should transform canonical IR into target-specific IR.

Acceptance criteria

Non-goals


8. Rust projection — built, on a different architecture than planned below

Status: Substantially done for Pizzas and Banking (zero generation skips on either, as of feat/rust-projection’s current tip), on a DIFFERENT architecture than this section originally specified — see “What actually got built” below before reading anything past it as current. No differential-harness integration, no era support, no WASM.
Priority: P0 (was; largely satisfied)
Complexity: XL (was)

What actually got built

Recorded formally as docs/implemented/decisions/0011-rust-compiles-types-interprets-dispatch.md, which supersedes 0007’s central claim — read it for the fuller argument and consequences; this section is the summary.

Everything below this point in §8, up through the original “Non-goals for milestone 1,” was written before work started and describes a design that was not the one implemented. The as-built system inverts one of that design’s own stated non-goals — worth reading as a real correction, not a footnote:

Location. rust/ at the repo root (a Cargo crate — renamed twice in review, first from the pre-existing rust_runner/, then settled here specifically so Ruby generator code doesn’t live under lib/hecks/) holds both the Rust crate (rust/src/) and its Ruby generator (rust/project.rb + rust/project/*.rb, module RustProjection, no Hecks:: namespace — it’s tooling for hecks, not part of the library). hecks project_rust <domain> is the thin driver.

Architecture actually built: compile types, interpret dispatch DATA. rust/src/kernel/{expr.rs,dispatch.rs} is hand-written once, and is a small, fully generic interpreter: interpret(expr, ctx) walks an Expr AST (a direct structural port of Evaluator/Resolver’s node kinds — Or/And/Not/Compare/Include/Lookup/arithmetic/sign-test/etc.), and dispatch(...) walks the real DISPATCH_ORDER steps generically, driven by whatever GivenSpec/EnsuresSpec/mutation-closure a generated function hands it. This is the opposite of the non-goal this section originally stated (“No generic kernel that parses or interprets canonical IR at runtime”) — given/ensures/invariant text genuinely is walked as data, by one hand-written interpreter, at the compiled binary’s own runtime. What IS compiled ahead of time, by hecks project_rust, is narrower than originally planned: just the type shapes — real Rust structs/enums per value object/entity/record, plus a Fielded trait impl per type so the generic interpreter can do name-based field lookup ("balance.cents") against otherwise fully static, typed data. The generator itself parses real IR objects via Ruby’s own Evaluator.parse, then emits Expr::Compare { ... }-shaped Rust data literals, not compiled boolean expressions.

Why this diverged: mid-build, generating one bespoke Rust function per command shape (a plain creator vs. an acting command with a given vs. one with a lifecycle transition vs. one with ensures…) didn’t converge — every new shape needed a new hand-written case, the same failure mode this section already correctly diagnosed for the retired Rust runtime, just one level down (per-shape codegen instead of a whole second interpreter). Interpreting dispatch data generically, once, was the fix — see docs/implemented/guides/running-a-runtime.md’s “Interpret data, don’t compile source” section for the fuller argument.

What generates correctly today, for both Pizzas and Banking, zero skips: every then_set op (set/append/increment/decrement, including cross-VO-type coercion by matching field names — Value::Coercion#fields_for’s real rule, not a guess), given/ensures (any expression the real grammar admits — no Unsupported case, because an interpreter needs none of the static machinery a compiler would), lifecycle transitions, closed sets (including multi-field ones, as a data table rather than a tag enum), composite identity (dotted, bare-declared, and bare-undeclared components), and entities — a capability this section didn’t originally scope at all: list_of(Entity) attributes (Account.ledger, a Vec<LedgerEntry>) are a valid append target, with the entity’s own identity and lifecycle field auto-filled at append time the way MutationApplier#entity_element fills them. The self-hosted grammar itself (lib/hecks/language/bluebook/) also compiles through the identical pipeline, alongside whichever business domain is the build target — another capability not in the original plan.

What’s still real and unbuilt, same as before: an entity’s OWN commands (LedgerEntry.Amend/.Reverse — addressing one list element by identity, a materially different and larger feature) — done (0013); role checking — done (0019); reference-existence checking (resolve_references) — done (0017); attribute-level admits:/pattern: constraint checks — done (0018); the CardPayment.tags nil-vs-[] list representation gap — done (0020, full corpus parity: 35/35 matching instances); era selection / historical-era support — split, partially done: reading a lineage-capable aggregate’s current, already-translated state and writing a new mutation from Rust — done (rust/host’s journal::read_lineage_head_all/_by_id/append_lineage_mutation, generic over any aggregate Projector::Exporter.lineage names, tracked per domain by hecks rust_coverage’s own lineage_aggregate findings); minting an era, detecting shape drift, or compiling a translation rule to SQL from Rust — still not done, and not planned, by architecture rather than by gap (that machinery parses bluebook/translation-rule DSL source at runtime, which ADR 0007 rules out for any Rust target permanently — rust/project.rb’s own header carries the full, current account, including why this generator’s own WASM kernel deliberately still never blends a lineage-capable aggregate into its InMemoryRepository replay path); WASM (§9, fully unstarted, and now depends on a different underlying design than §9 assumes — see §9’s own note).

Correction (2026-08-10): “building a Rust Postgres adapter FIRST” is now stale — one exists, built afterward (rust/host/src/journal.rs, rust/host/src/main.rs), and is live in production (the deployed pizzas Lambda). It’s real, tested against a genuine Postgres instance including Ruby’s own RLS fence (journal::lineage_tests::a_stale_era_write_is_refused_by_postgres_rls_not_this_crate, using real Ruby LineageManager mint code, not a stand-in), and writes each accepted command’s mutations into Ruby’s OWN per-aggregate, era-partitioned schema (hecks_journal_<domain>, <aggregate>_head_snapshot_<era> — same table names Ruby’s postgres.rb uses) so a human or Ruby tooling reading that schema sees Rust’s writes exactly the way it sees Ruby’s own. As of the boot-gate hardening in this same investigation, rust/host also refuses to boot against a stale era (one that exists in hecks_eras but has been superseded by a later mint), not merely a never-minted one — closing the gap where a bare existence check would have let a stale checkout start and rely on RLS alone to catch it, and only for the subset of commands touching a lineage-capable aggregate. What’s still genuinely, entirely missing — the actual remaining scope of this line item — is everything on the reading and transition side: rust/host writes into exactly the ONE era it’s deployed against and never reads the era-partitioned tables back (its own command-replay state comes from a separate, flat, era-oblivious journal); there is no Rust equivalent of LineageManager’s edge-chain resolution, shadow-parsing, shape-hashing, translation-rule execution, mint transaction, tamper-evidence, or coverage/approval checks (lib/hecks/adapters/driven/postgres/lineage_manager/*.rb, lineage/*.rb) — all of that remains Ruby-only, deliberately, per ADR 0007 (Rust doesn’t carry the bluebook parser or translation DSL). So: narrower than this section originally claimed in the “Postgres adapter” dimension (done), unchanged in the “shape-diff/migration/multi-era-read subsystem” dimension (still not started, still genuinely large — see the era/lineage investigation’s own scoping notes for the phased plan a full port would need).

Verification, as actually done: hecks check_conformance (restored 2026-08-07 alongside this document, confirmed still running against the current codebase — spec/corpus/pizzas.json and banking.json both still work) was not wired into the Rust generator’s workflow at any point. Everything above was verified instead by direct cargo run smoke tests against real scenarios (Account.Open → Credit → Debit → over-limit refusal, ScheduledPayment.Schedule → Fail → Retry ×3 → exhausted, the full Pizzas walkthrough), diffed by eye against known-correct Ruby behavior, plus spec/port_shape_coverage_spec.rb (a Ruby-side gate ensuring Banking keeps exercising every IR shape the generator branches on) and full-suite regression on the Ruby side after every change. Wiring hecks check_conformance into an automated per-change check against the Rust output remains real, valuable, un-started work — see “If this is picked up again,” below.

Everything from here to the end of §8 is the original plan, kept verbatim as a record of the intended design — read it as history, not as a spec to implement, except where the notes above say otherwise.

Original target (superseded — see above)

Rust is a projection, not an interpreter, and specifically a code generator, not a data embedder: a build-time tool reads canonical IR and emits native Rust source — types for records/value objects, a dispatch table, and per-command argument gates, role checks, given evaluation, mutation application, and event emission, all as real generated Rust code. That generated code is what gets compiled. A small hand-written kernel supplies only genuinely domain-independent infrastructure (the repository trait, evaluator primitives the generated given code calls into, the dispatch entry point) and is compiled alongside the generated code. Ruby is not required at runtime, and neither is the IR file; both are authoring/build-time-only.

The governing invariant is narrower than “nothing IR-shaped exists in the binary”: no canonical IR is parsed or interpreted at runtime — runtime behavior is compiled into native code. That leaves room for the generator to also emit compact static metadata tables (for introspection, evidence, diagnostics, docs, or explaining a refusal) without reopening the interpreter architecture, as long as those tables are consulted for description, not walked by a generic kernel to decide behavior. Behavior comes from generated code paths; metadata is only ever read, never interpreted.

The semantic requirement is that the compiled artifact can execute or query any held era, not a snapshot of current semantics only — matching Ruby’s era-replay capability (§25), not a reduced “current era only” subset. That requirement is separate from how eras get generated, and shouldn’t be read as committing to a specific implementation forever.

Initial implementation: one generated module per held era. The codegen step reads every held declaration for the Bluebook (the same lineage DeclarationSnapshot/era_check machinery Ruby’s history tooling already uses) and generates a module per era — its own types, dispatch table, and command logic — with a single dispatch entry point that takes an era selector (defaulting to the latest) and routes into the matching module. This is the simplest thing that satisfies the semantic requirement, and it’s an explicit scaling concern, not a permanent architecture: a long-lived domain with many eras where most rules are unchanged from one era to the next would generate and compile a full duplicate module per era regardless. Don’t let the wording here — or the milestone-1 acceptance criteria below — foreclose a later optimization that deduplicates unchanged behavior across eras (e.g. sharing generated functions/types where two eras’ declarations produce identical output, and only forking where they diverge); that’s a code-generator-internals improvement, not a semantic change, and shouldn’t require touching this section’s acceptance criteria to land.

The real constraint regardless of implementation: minting a new era still requires rerunning codegen and recompiling — the compiled artifact does not ingest a new era at runtime, by the same no-runtime-IR design as everything else in this section.

Minimum path:

Ruby DSL (authoring only)
      ↓
canonical Bluebook IR for every held era (JSON, build-time artifact — consumed once, then discarded)
      ↓
Rust projection (code generator; initial implementation: one module per held era)
      ↓
generated Rust source — native types & per-command logic derived from the IR, per era
      ↓
compiled together with a small hand-written shared kernel + an era-dispatching entry point
      ↓
native binary / library / WASM — every held era's semantics compiled in as code, not carried as embedded IR data

Ruby authoring remains unchanged

Hecks.bluebook "Payments" do
  aggregate "Payment" do
    command "Open" do
      role "Merchant"
      goal "Start tracking a payment"
      emits "PaymentOpened"
    end
  end
end

No Rust syntax appears in the Bluebook.

Original acceptance criteria (superseded — see “What actually got built”)

Original non-goals (superseded — the as-built system violates the interpreter one on purpose; see above)

If this is picked up again

Real, scoped next increments, roughly in order of how self-contained each is: (1) hecks check_conformance’s native/<path.wasm> subprocess mode (0012) is now wired into CI as an automated regression gate (spec/rust_conformance_spec.rb, run by the same bundle exec rspec CI already runs) — done; (2) entity commands (Amend/Reverse-shaped — addressing one list element by its own identity) — done (0013), along with policies and process managers/sagas, neither of which this section originally scoped at all; (3) the JSON-boundary refuse_unknown_arguments/refuse_absent_arguments equivalent 0013 flagged — done (0014) — and the optional: true/Option<T> modeling gap 0014 found and deferred — done (0016, skips loudly the one shape it can’t honestly represent: an optional argument feeding a non-optional VO/entity field); reference-existence checking — done (0017); investigating it surfaced three distinct, previously-masked gaps — a closed-set-subset admits: membership check and a scalar attribute’s pattern: regex constraint, both done (0018, the pattern: half needing a hand-rolled zero-Cargo-dependency regex matcher bounded to exactly the dialect PatternSubset admits), and an Integer-typed JSON value arriving as a non-numeric string, which turned out to be a real bug in 0014’s own default: fallback rather than a missing check — also done, fixed directly. Role checking — done (0019, needed a wire-contract extension first — a step’s optional role: key — since Ruby’s own caller is ambient thread-local state never reachable through the existing corpus-script format at all). 0014’s original two-item pair is now fully closed; the CardPayment.tags nil-vs-[] representation gap 0014 also flagged — done (0020: a list-typed record field is Option-wrapped exactly when a creating command’s own :set mutation can leave it genuinely unset, traced to Instance.defaults’/apply_mutations’ real mechanics rather than guessed at). Full corpus parity reached: 35/35 matching instances — every corpus-level mismatch this arc has been closing since 0013 is now resolved. The two remaining COSMETIC gaps 0012/0013 named (event payload default-fill, GivenNotMet wording) — also done (0021; spec/rust_conformance_spec.rb now compares events and full refusal wording, not just instances/verbs). Running the FULL corpus through the differential harness during that pass found a much longer tail of OTHER refusal-wording templates that still don’t match Ruby byte-for-byte (LifecycleRefused, general VO-invariant messages, one_of membership, entity-element-missing) — none affect instances, all deliberately left open as a separately-scoped future slice, not silently skipped; (4) era support — investigated and confirmed genuinely out of scope for this arc: not a parity bug in current output but a missing subsystem requiring a Rust Postgres adapter before any lineage/shape-diff work could even start; update (2026-08-10): that adapter now exists (rust/host, see the correction on this same era-support sentence just above) — single-era read/write against Ruby’s own schema, boot-gated on era currency, RLS-verified — but the shape-diff/translation/multi-era-read subsystem it was gating stays exactly as out-of-scope as described, since rust/host never grew the bluebook parser or translation DSL that subsystem needs; (5) §9 WASM — a first, non-browser slice shipped (0012, stdin/stdout over WASI, not the JS API), plus a browser wasm-bindgen slice (0015); the original browser-facing design in §9 remains only partially scoped against the as-built (no-era) architecture.


9. WASM embedded Bluebook

Status: A first WASM slice is built, on a DIFFERENT shape than this section originally specified — see decision 0012 and “What actually shipped” below. Everything else in this section (the browser JS API, per-era dispatch, the host-adapter import boundary) remains unstarted and was written against the ORIGINAL §8 design; re-scope against the as-built architecture before picking it up, particularly the per-era claims in the acceptance criteria.
Priority: P1
Complexity: L

What actually shipped (WASI-stdio slice, 0012)

Not the browser JS API below — a stdin/stdout JSON CLI contract ({"steps": [...]} in, {"instances", "events", "refusals"} out — hecks check_conformance’s own pre-existing shape), compiled unchanged for both native (aarch64-apple-darwin) and wasm32-wasip1 (WASI preview 1). hecks build_wasm <domain> regenerates and cross-compiles; hecks check_conformance <domain> <script.json> native|<path.wasm> runs either artifact as a real subprocess and diffs it against the Ruby oracle, finally wiring the subprocess hook that tool’s own header comment used to say didn’t exist. Verified on Banking (10 aggregates, composite identities, closed-set data tables): instances matches byte-for-byte across Ruby, native Rust, and WASM-under-wasmtime. Two pre-existing, unrelated fidelity gaps surfaced by actually running the harness live rather than fixed by this slice — events[].payload’s debug-string-vs-structured-JSON shape, and a refusal-wording prefix Ruby adds and Rust doesn’t — see 0012’s own Consequences for both.

No era selection (the generator still has no era concept — §8’s own still-open gap, untouched here), no browser JS API, no host-adapter import boundary. The rest of this section — Target through Non-goals — is the ORIGINAL, still-unbuilt browser-facing design; 0012’s “Rejected alternatives” explains why it wasn’t what got built first, and that a browser layer remains a plausible follow-on built ON TOP of the WASI-stdio contract, not a replacement for it.

Target

Compile the artifact §8 generates to WASM so browser applications can call the domain directly as their internal command bus. This is a compile target of §8’s single runtime, not a separate implementation.

JS-facing API target

The Bluebook’s command logic — every held era of it — is already compiled into parcelpro.wasm by §8’s code generator, so loading the module takes no separate IR argument — there is nothing left to load into it:

const domain = await Hecks.load("parcelpro.wasm")

await domain.dispatch("LandPlanning::Road.MoveVertex", {
  road_id: "r-1",
  vertex: 3,
  x: 12.2,
  y: 4.8
})
// domain.dispatch(name, args, { era: 12 }) — optional, defaults to latest

const report = await domain.report("LandPlanning::ProjectView", {
  project_id: "p-1"
})

Ruby source remains normal Bluebook

command "MoveVertex" do
  role "Designer"
  goal "Move a road vertex"

  reference_to Road
  attribute :vertex, Integer
  attribute :x, Float
  attribute :y, Float

  emits "RoadVertexMoved"
end

Host adapter boundary

Browser-only capabilities remain outside WASM:

Define narrow imported host operations rather than embedding JS application code in the runtime.

Acceptance criteria

Non-goals


10. SQL projection vs SQL runtime

Status: Persistence projections already exist implicitly in adapters; full runtime is research
Priority: P2
Complexity: L–XL

Target A — SQL artifact projection

Separate schema/migration generation conceptually from persistence execution.

Bluebook IR → Postgres DDL / SQLite DDL

Target B — executable Postgres projection

Research compiling transactional command semantics into stored procedures, constraints, triggers, and views.

Ruby Bluebook:

command "CloseAccount" do
  role "Branch clerk"
  reference_to Account

  given("the balance is zero") { balance.cents == 0 }

  emits "AccountClosed"
end

Possible generated PostgreSQL surface:

select * from banking_close_account(
  actor_id => 'a-1',
  account_number => '1234'
);

Keep outside SQL

Use outbox/ports for those.

Acceptance criteria for research spike


11. OIDC client projection

Status: Integration layer done (2026-08-08) — the real provider- facing half (redirect, code exchange, JWT/JWKS signature verification against a live provider) is deliberately NOT built: security-critical, needs real external dependencies and provider credentials neither available nor meaningfully testable here, scoped out on purpose rather than stubbed. What IS built and proven against real Banking: a new identity_resolution port (lib/hecks/ports/identity_resolution.{port,rb}), resolved zero/one/many exactly like the authorization port Ports::Authorization built for §2, fulfilled by Hecks::Adapters::IdentityRegistry (lib/hecks/adapters/driven/identity_registry.rb) — a same-registry dispatch against Identity::ExternalIdentifier.ResolvedBy, mirroring GovernanceAuthorization’s own shape exactly. spec/oidc_projection_spec.rb composes it with Ports::Authorization.holds_role? and Hecks.as_caller — the plan’s own sketch, just spelled out — booted through the real Hecks.boot("examples/banking") path (both Governance and Identity are already attached there via attaches, see §4/§5’s own notes). Every acceptance criterion below is proven, not merely asserted: no password field exists on Identity/ExternalIdentifier (checked against the real declared attributes), (issuer, subject) uniquely resolves (checked against Link’s own natural key), two different external identifiers link to one identity and both authenticate (dispatched for real, against real Banking commands), and the unauthorized/unresolved paths refuse BEFORE any dispatch — command authorization stays exactly Governance’s job, identity_resolution never touches CommandInterpreter/Dispatcher/CommandRules::Authorization.
Priority: P1
Complexity: M

Target

Allow applications to authenticate through Google/Microsoft/etc. as OIDC clients while mapping (issuer, subject) into identities.bluebook.

Flow

OIDC provider
→ verified issuer/subject
→ identities.bluebook lookup/link
→ stable Identity
→ Governance roles
→ command dispatch

Ruby application integration sketch

identity = identities.lookup_external(
  issuer: claims["iss"],
  subject: claims["sub"]
)

Hecks.as_caller(role: governance.role_for(identity)) do
  runtime.dispatch_flat("Billing::Invoice.Refund", **args)
end

This is illustrative; final runtime should carry actor separately from role.

Acceptance criteria


12. OIDC provider projection

Status: New executable protocol projection
Priority: P2
Complexity: XL

Target

Project Identity + Governance into a deployable OIDC provider that exposes standard endpoints while Bluebook remains the organizational source.

Expected generated/hosted surfaces:

/.well-known/openid-configuration
/authorize
/token
/jwks.json
/userinfo

Bluebook source idea

Hecks.bluebook "IdentityProvider" do
  # business configuration represented with normal Bluebook concepts
end

Protocol machinery should be projection/runtime code, not DSL keywords.

Runtime configuration

Issuer URL, signing keys, upstream providers, registered clients, redirect URIs, token lifetimes, and storage remain deployment configuration/adapters.

Acceptance criteria


13. Email OTP adapter

Status: New auth adapter
Priority: P1
Complexity: M

Target

Provide passwordless email sign-in as an alternative authenticator feeding identities.bluebook.

Domain flow

command "RequestLoginCode" do
  role "Anonymous"
  goal "Prove control of an email address"
  attribute :email, EmailAddress
  emits "LoginCodeRequested"
end

The actual random code generation and email send are adapters. The durable domain should store only appropriate digests/state required to verify the challenge.

Acceptance criteria


14. People / Customers / Users decomposition

Status: Canonical corpus design
Priority: P1
Complexity: M

Target

Keep technical identity separate from business identity.

Potential canonical Bluebooks:

identities.bluebook — who can be authenticated
people.bluebook     — people/teams/employment
customers.bluebook  — commercial relationships
users.bluebook      — app-specific preferences, only if needed
governance.bluebook — authority

Ruby relationship example

Hecks.bluebook "Customers" do
  aggregate "Customer" do
    identified_by { customer_id.value }

    reference_to "Identities::Identity", as: :identity_id, optional: true

    attribute :customer_id, CustomerId
    attribute :identity_id, IdentityId, optional: true
  end
end

Use the actual cross-domain reference mechanism supported by Hecks; the syntax above is illustrative if FQN references require adjustment.

Acceptance criteria


15. UL Projection

Status: New projection class
Priority: P0
Complexity: L

Target

Adapt a canonical Bluebook into an organization’s ubiquitous language while preserving executable semantics.

Example canonical source:

command "Refund" do
  role "Refund agent"
end

UL mapping:

Hecks.ul_projection "Acme Payments" do
  role "Refund agent", as: "Customer success representative"
  command "Refund", as: "Reverse charge"
end

Projected Bluebook:

command "Reverse charge" do
  role "Customer success representative"
end

The exact mapping DSL can live in a supporting Bluebook or projection configuration rather than core Bluebook grammar.

Critical invariant

The projection changes vocabulary, not behavior.

given, ensures, lifecycle transitions, mutation semantics, types, and event relationships remain equivalent unless onboarding explicitly records a semantic customization.

Implementation hint

Start by operating directly on canonical IR hashes:

projected = ULProjector.call(ir, mapping)

Do not parse/rewrite Ruby source.

Then render projected IR back to Bluebook source if human-editable output is desired.

Acceptance criteria


16. UL adoption recipe

Status: New durable supporting artifact
Priority: P0
Complexity: M

Target

Keep the UL projection used during onboarding so future canonical releases can be adapted again without repeating all mappings.

Example recipe:

Hecks.ul_projection "Acme Payments" do
  from "Registry::Payments", version: "1.2"

  role "Refund agent", as: "Customer success representative"
  role "Payment processor", as: "Finance administrator"

  command "Cancel refund" do
    role "Finance manager"
  end
end

This recipe is not required at runtime.

Acceptance criteria


17. Onboarding Bluebook

Status: New supporting Bluebook
Priority: P0
Complexity: L

Target

Model ontology adoption as a business process using Hecks itself.

Suggested aggregates:

Suggested commands:

Suggested reports:

Ruby sketch

Hecks.bluebook "Onboarding" do
  supporting

  aggregate "Adoption" do
    identified_by { adoption_id.value }

    command "MapRole" do
      role "Domain owner"
      goal "Express a canonical responsibility in our language"

      reference_to Adoption
      attribute :canonical_role, String
      attribute :organization_role, String

      emits "RoleMapped"
    end
  end

  report "UnmappedRoles" do
    # driven from stored adoption state
  end
end

Adapter needs

Onboarding must inspect external Bluebook IR, so define a projection/catalog port rather than letting the Bluebook read files directly.

Acceptance criteria


18. Role mapping review

Status: New onboarding feature
Priority: P0
Complexity: M

Target

When the organization maps a canonical role, show every command that will inherit the mapping before writing the projected Bluebook.

Example:

Refund agent → Customer success representative

Affected:
- Refund payment
- Review refund
- Cancel refund
- View refund history

SME may then override one:

Cancel refund → Finance manager

Representation

Store mapping at two levels:

role_mapping "Refund agent" => "Customer success representative"

command_override "Payments::Refund.Cancel",
                 role: "Finance manager"

Acceptance criteria


19. Existing-role discovery

Status: New onboarding projection/query
Priority: P1
Complexity: M

Target

Onboarding should inspect organization Bluebooks and offer existing roles as mapping choices.

Potential sources:

Example output

[
  "Customer success representative",
  "Finance administrator",
  "Controller",
  "Operations manager"
]

Acceptance criteria


20. Semantic mapping suggestions

Status: New AI-assisted tool; optional
Priority: P2
Complexity: L

Target

Rank likely organization-role matches based on command/responsibility shape rather than string similarity alone.

Example:

Canonical Refund Agent:
  Refund payment
  Review refund
  View payment

Matches:
  Customer Success Representative  0.92
  Finance Administrator            0.71

Implementation hint

Create semantic feature vectors from canonical IR:

AI proposes; SME approves.

Non-goal

Never silently apply authority mappings based on AI confidence.


21. Semantic drift detection

Status: Existing era/translation/diff machinery is a foundation; broaden scope
Priority: P1
Complexity: L

Current correlation

Hecks already has:

Target

Define drift as disagreement between semantic representations, not textual differences.

Drift pairs:

Canonical Bluebook ↔ Organization Bluebook
ISO mapping        ↔ Bluebook
Bluebook           ↔ projected artifacts
Era N              ↔ Era N+1

Agent API direction

diff = Hecks::SemanticDiff.call(old_ir, new_ir)

diff.commands.changed
diff.roles.renamed
diff.rules.changed
diff.references.changed

Acceptance criteria


22. Canonical ontology upgrades

Status: New workflow built on UL Projection + drift
Priority: P1
Complexity: XL

Target

Upgrade an organization from canonical v1 to canonical v2 without overwriting organization-specific evolution.

Conceptual three-way merge:

canonical v1
      ↓ UL recipe
projected org baseline v1
      ↓ org edits
organization current

canonical v2
      ↓ same UL recipe
projected org candidate v2

semantic merge(candidate v2, organization current, baseline v1)

Acceptance criteria


23. ISO discovery and projection

Status: New adjacent workflow
Priority: P2
Complexity: XL

Target

Use ISO standards as discovery inputs for executable quality/compliance Bluebooks. Initially humans interpret requirements into Bluebook; later an ISO-facing projection provides clause coverage and documentation.

Do not make ISO a new Bluebook dialect.

Ruby example

Hecks.bluebook "QualityManagement" do
  aggregate "Nonconformity" do
    command "Report" do
      role "Quality representative"
      goal "Record a failure to meet a requirement"
      emits "NonconformityReported"
    end
  end

  aggregate "CorrectiveAction" do
    command "VerifyEffectiveness" do
      role "Quality manager"
      goal "Confirm the corrective action solved the problem"
      emits "CorrectiveActionVerified"
    end
  end
end

Traceability metadata

Prefer external mapping initially:

map "ISO9001:2015/10.2",
    to: [
      "QualityManagement::Nonconformity.Report",
      "QualityManagement::CorrectiveAction.VerifyEffectiveness"
    ]

Acceptance criteria


24. Standards conformance drift

Status: New
Priority: P2
Complexity: L

Target

If a Bluebook changes in a way that invalidates its ISO mapping, create a reconciliation task instead of silently generating stale documentation.

Example:

Changed:
QualityManagement::ReleaseProduct

Potentially affected:
ISO 9001 §8.6

Reason:
approval evidence removed

Resolution required:
- restore behavior
- update mapping
- record justified deviation

Acceptance criteria


25. Historical runtime query by Era

Status: Much of the storage foundation already exists
Priority: P1
Complexity: M

Current correlation

The codebase already stores held era declarations, structured declaration snapshots, resolved era ordinals, shape checks, translations, and replay tooling.

Target

Expose a simple first-class API to boot/query a historical Era without Git or source checkout.

Ruby target:

history = Hecks.at(era: 12, domain: "Banking")

history.query(
  "Banking::Customer.Suspended"
)

or:

Hecks.query(
  "Banking::Customer.Suspended",
  era: 12
)

Implementation hint

Use held structured declarations (DeclarationSnapshot) and the lineage adapter’s era data directly. Do not parse historical Ruby source unless a repair/reattest tool explicitly requires it.

Acceptance criteria


26. Historical analytics / reports by Era

Status: New surface over existing era storage + query/read_model
Priority: P1
Complexity: M

Target

Let analysts run reports against the semantics of the era in which data was produced, or intentionally restate through current semantics.

Two explicit modes:

report.run(era: 12, interpretation: :historical)
report.run(era: 12, interpretation: :current)

Historical means “what did this mean then?” Current means “translate old facts forward and answer using today’s definition.”

Acceptance criteria


27. Data engineering projections from reports

Status: Existing query/read-model infrastructure is foundation
Priority: P2
Complexity: L

Target

Treat analytical artifacts as projections of Bluebook reports instead of standalone warehouse semantics.

Potential targets:

Ruby source:

report "MonthlyRevenue" do
  include Invoice
  where(status: "paid")
  order_by :paid_at
end

Projection command:

hecks refresh_projections MonthlyRevenue --target postgres-view

Acceptance criteria


28. Canonical corpus

Status: New product layer; existing spec/corpus proves corpus tooling patterns
Priority: P1
Complexity: Ongoing

Target

Build a curated repository of canonical Bluebooks that capture reusable organizational knowledge.

Initial candidates:

Quality bar

A canonical Bluebook should include:

Non-goal

Do not canonicalize arbitrary application implementation details.


29. GitHub/documentation pattern discovery

Status: Research/product tooling
Priority: P3
Complexity: XL

Target

Use public source/documentation as evidence for discovering recurring business patterns, then author canonical Bluebooks from those findings.

Pipeline:

sources
→ pattern extraction
→ semantic clustering
→ candidate ontology
→ human/AI review
→ canonical Bluebook
→ verification

Do not mechanically translate source code line-by-line.

Acceptance criteria


30. Projector architecture

Status: Done (2026-08-08) — Hecks::Projector.register(name, projector) / .call(name, bluebook:, options:) now exist for real (lib/hecks/projector.rb), proven against a genuine registered target, :ir (lib/hecks/projector/ir_projector.rb), whose output matches spec/golden/ir/Pizzas.json byte-for-byte once sorted and carries ir_version: as target-version metadata for free (§7). Exporter itself is UNTOUCHED and still exactly what hecks ir, hecks project_rust, and translation/audit’s approval digest read — it operates on a whole registry (many bluebooks), a different unit than this framework’s per-bluebook call, so it was deliberately not folded in rather than forced to fit. Retrofitting the real existing projectors — :rust (hecks project_rust/rust/project.rb, a whole separate toolchain under a confusingly-identical RustProjection::Projector name) and the not-yet-built :ul/:openid — is real, separate work this pass didn’t attempt; spec/projector_spec.rb covers register/call/re-register/ unknown-target/golden-determinism.
Priority: P0
Complexity: M

Target

Turn Hecks::Projector into a stable projection framework rather than one exporter module.

Suggested interface:

Hecks::Projector.register(:rust, RustProjector)
Hecks::Projector.register(:ul, ULProjector)
Hecks::Projector.register(:openid, OIDCProjector)

Hecks::Projector.call(
  :rust,
  bluebook: ir,
  options: {}
)

RustProjector here is §8’s code generator: this .call is the build-time step that emits the native Rust source described there (one module per held era), not a runtime call — its output is source files for the Rust toolchain to compile, the same way :ul’s output is a projected Bluebook and :openid’s is provider configuration.

Layers

Canonical IR
→ target normalization
→ target IR
→ renderer/artifact emitter

This prevents target formatting logic from polluting canonical semantics.

Acceptance criteria


31. Port fulfillment graph

Status: Done (2026-08-07) — lib/hecks/runtime/capability_graph.rb, Hecks::Runtime::CapabilityGraph, exposing exactly the sketched registry.capability_graph.{fulfillments,unfulfilled,cycles}. Built off what the registry already holds: registry.ports/registry.adapters, and the same adapter.port == port.name match Ports::Extraction and Ports::IdentityGeneration already make for themselves, one port at a time — this asks it once, for every port at once. cycles answers [] always, and honestly: nothing in this port model lets one port depend on another, so there is no edge for a cycle to be made of. Registry #capability_graph is a memoized wrapper, matching #repository’s own pattern one line above it. spec/runtime/capability_graph_spec.rb proves fulfilled/unfulfilled/multi-adapter cases against a real registry (Memory on persistence, Prism on extraction, identity_generation declared and left unbound on purpose, to exercise the gap). The onboarding-UI framing above (“Found: ✓ Acme Governance ○ …”) is not built — this section only covers the data the UI would read.
Priority: P1
Complexity: M

Target

Expose the dependency tree that already emerges from port fulfillment.

Example:

Payments
├── Identity → Identities.bluebook
├── Governance → Governance.bluebook
└── PaymentGateway → Stripe.adapter

Agent API

graph = registry.capability_graph

graph.unfulfilled
graph.fulfillments
graph.cycles

Onboarding use

When canonical Payments requires a port:

Governance capability required.

Found:
✓ Acme Governance
○ Core Governance
○ External adapter

Acceptance criteria


32. Business-language audit of DSL

Status: Audited (2026-08-07) against the current public keyword list (spec/dsl_coverage_spec.rb’s own maintained inventory). Conclusion: read_model → report (§6) was the one concrete, ready-to-execute rename, and it’s done. Everything else this section names is a “retain” or an explicitly open “consider,” not a decided rename — aggregate in particular is real DDD jargon an SME wouldn’t naturally reach for, but renaming it would be structural (it’s the core keyword, not a leaf verb like report was) and the plan itself only ever says “consider,” not “do” — left alone on purpose, per “do not rename merely for novelty,” not overlooked. sets/then_set (Command level) already went through this exact same treatment earlier, via the LOP roadmap work, before this section was written.
Priority: P2
Complexity: M

Target

Review every public DSL keyword against the rule:

Would an SME plausibly understand this word without learning a software architecture pattern?

Likely candidates:

Do not rename merely for novelty.

Acceptance criteria


33. Semantic identity / provenance

Status: Done (2026-08-07), through the full self-hosted grammar — not skipped the way §7’s ir_version was (that one is a computed constant; provenance is author-facing DSL, so it belongs in the load-bearing language description). provenance from: { source:, source_id:, source_version: } on both aggregate and command, captured as a raw Hash untouched — the exact shape sketched below, one level up from attribute ..., default: { ... }’s own literal-Hash precedent, which it reuses end to end: AggregateBuilder#provenance/CommandBuilder #provenance, IR::Aggregate#provenance/IR::Command#provenance (in to_h), new rows in syntax.bluebook, and — the part that makes it real rather than decorative — aggregate.bluebook/behavior.bluebook (the language’s OWN grammar) gained a matching attribute :provenance, LiteralText/CommandText on both the root record and the Declare command, Assembly::CONTRACTS/Shapes#provenance/Reconstruction# aggregate round-trip it, and all 9 golden IR fixtures were regenerated. One real usage: Banking’s Account aggregate now carries provenance from: { source: "HecksCanonical", source_id: "aggregate:account", source_version: "1.0" }, proving the path against the real corpus, not just a synthetic fixture — spec/round_trip_spec.rb catches any future drift here directly. Confirmed and left alone on purpose: readings.rb needed no change (the judge’s generic field_value fallback already offers any field with no shape-differing exception); description was confirmed NOT a usable precedent for Command specifically (CommandBuilder has no description at all — the asymmetry is real). Provenance never touches dispatch or identity, exactly as the acceptance criteria below require.
Priority: P1
Complexity: M

Current correlation

IR::Command comments explicitly state that identity is (KIND, FQN), because names may collide across construct kinds.

UL projection and ontology upgrades complicate name-based identity because vocabulary intentionally changes.

Target

Introduce provenance identifiers for adopted canonical concepts without replacing Hecks runtime identity.

For example:

{
  kind: "command",
  fqn: "AcmePayments::Payment.ReverseCharge",
  provenance: {
    source: "Registry::Payments",
    source_id: "command:payment.refund",
    source_version: "1.2"
  }
}

The organization FQN remains runtime identity. Provenance supports upgrades and semantic correspondence.

Acceptance criteria


34. Architecture Decision Records

Status: Started — docs/decisions/0001, 0002, 0007, 0008, 0009, 0010 exist (restored 2026-08-07 alongside this document); 0011-rust-compiles-types-interprets-dispatch.md (new, same day) records the real architecture §8 was actually built on and formally supersedes 0007’s central claim (0007’s own Status line now points at it). 0003–0006 below were never written. 0009/0010 hold up as written — read 0007 as historical record of the original reasoning, same as §8’s own superseded sections, not current architecture.
Priority: P0
Complexity: XS

Target

Record irreversible or expensive design decisions in docs/decisions/.

Initial ADRs:

0001-canonical-ir-is-runtime-independent.md
0002-one-command-one-role.md
0003-act-as-cross-domain-authority.md
0004-bluebooks-never-own-entropy.md
0005-ul-projection-preserves-ubiquitous-language.md
0006-ports-not-packages-drive-composition.md
0007-rust-generates-code-not-ruby-source.md
0008-reports-are-business-facing-read-models.md

Template

# Decision

## Context
...

## Decision
...

## Consequences
...

## Rejected alternatives
...

Acceptance criteria

An agent implementing a feature can find the relevant decision without re-reading chat history.


Recommended implementation order

Reordered 2026-08-07 from a dependency/risk analysis of all 34 sections. The Rust/WASM/host-language sections were briefly pruned as obsolete after the Rust retirement, then restored at the user’s request, then narrowed to a single runtime target: Rust (§8) plus its WASM compile output (§9) are the only runtime-projection sections in this roadmap — Ruby-hosts-Rust, Python, Go, and Java host bindings were removed rather than kept as separate speculative sections; OIDC and the other non-runtime projections were untouched by that decision and remain as before. supporting Bluebooks stays removed as already implemented. Each phase either has no unmet dependency on later work, or exists specifically to unblock the phase after it. Section numbers below (§N) refer to the numbered headings above.

Rust/WASM moved to Phase 2 (2026-08-07, second revision) per docs/decisions/0010-ruby-is-the-reference-implementation.md: it no longer waits for authority/ontology/canonical-domain work to finish. Ruby serves as both the implementer’s guide and, through a continuously-run differential harness (§8’s Milestone 0), the correctness oracle — so IR/behavior changes from later phases become fast-following, harness-verified extensions to the generator and kernel, not a reason to have waited. See §8 and 0010/0007 for the full reasoning; 0007’s original “wait for Phases 2–4” entry criterion is superseded, kept in that ADR only as the reasoning this project moved past.

Phases 1, 3, 4, 5 still read as one arc for the authority/ontology track specifically: harden the language (1), establish authority/identity semantics (3), establish ontology/provenance semantics (4), prove those semantics against real domains and a real corpus (5). Phase 2 (Rust/WASM) runs concurrently alongside that whole arc rather than waiting at its end, chasing each phase’s IR/behavior additions as they land. Phase 6 onward project the same settled semantics outward into external ecosystems (auth protocols, history/analytics, persistence, standards, research).

Phase 1 — Core hardening

§1 duplicate-role guard · §6 report alias · §32 business-language DSL audit · §7 IR version envelope · §34 ADRs (start now, apply continuously)

All five are small, have zero dependencies on anything else in this plan, and directly reduce risk for every later phase: §1 closes an authoring footgun before more commands get written against Governance/Identities; §6+§32 fix vocabulary before it’s baked into onboarding-facing tooling; §7 gives every later projector (§8, §10, §11, §15, §27) a stable contract to target instead of an implicit one; §34 should be adopted as a habit starting here, not treated as a single deliverable — every phase below changes an architectural invariant somewhere.

Status note (2026-08-07 restore): none of §1, §6, §7, §32 were implemented — verified directly against the current codebase, not assumed. §8 (Phase 2) proceeded anyway, out of the order this section recommends, and reached substantial completion without any of Phase 1 landing first; nothing about §8’s actual build depended on core hardening happening first. Worth noting as evidence either way: Phase 1 may matter less as a hard prerequisite than this ordering implied, or §8’s architecture change (interpret dispatch data, rather than the originally-planned pure codegen) is precisely what let it route around needing §7’s IR-version contract, §1’s role-cardinality guard, etc. Re-evaluate whether Phase 1 still belongs first before resuming this roadmap, rather than restarting it by default because it’s numbered first.

Phase 2 — Rust/WASM runtime

§8 Rust projection, second attempt · §9 WASM embedded Bluebook

Status note (2026-08-07 restore): §8 is done, substantially, per its own “What actually got built” section — but not via this paragraph’s plan. Read the note there before treating anything below as current.

Starts right after core hardening, not after authority/ontology/canonical-domain work — see the scope note above and docs/decisions/0010-ruby-is-the-reference-implementation.md. §8’s Milestone 0 (the differential harness against Ruby) is the actual gate from here on, not a phase boundary: build the harness first, then generate against Pizzas and Banking as they exist today. As Phase 3 (Governance, act_as), Phase 4 (UL projection/provenance), and Phase 5 (canonical domain expansion) land, each extends canonical IR and sometimes dispatch-pipeline behavior — §8’s “chase Ruby” section is the standing obligation that follows: extend the generator/kernel, re-run the harness, done when it’s green, not accumulated as debt. §9 is simply §8’s output compiled to a WASM target and depends directly on §8’s generator existing — there is exactly one runtime here, not a family of host bindings. Hold to §8’s own non-goals throughout: no second hand-written parser, no ported DSL builder, no full parity corpus, and no generic IR-interpreting kernel — Rust code is generated from canonical IR at build time, and no canonical IR is parsed or interpreted at runtime.

Phase 3 — Authority & identity core

§3 Governance Bluebook · §4 Identities Bluebook · §5 UUID adapter · §2 act_as (wired to real Governance.authorize_transition!) · §31 port fulfillment graph · §33 semantic identity/provenance

Status note (2026-08-07): Every task in this phase is done — §3, §5, §4, §2, §31, and §33 — see their own Status lines. §2 landed with no dispatch-pipeline change at all (CommandInterpreter/Dispatcher/CommandRules::Authorization untouched) — the “Runtime execution” sketch below turned out to describe an application-level pattern composing Governance.RoleTransition + Runtime::Caller.as, not new pipeline plumbing, so the “first workload for Phase 2’s chase-Ruby obligation” framing two paragraphs down did not end up applying to §2 — nothing here changed Rust-relevant dispatch behavior.

This is the load-bearing phase for the authority/ontology arc: almost everything in Phase 4/5 assumes Governance and Identities exist. It’s also the first real workload for Phase 2’s “chase Ruby” obligation — act_as in particular adds dispatch-pipeline behavior, not just IR shape, so it’s the first place the Rust kernel (not just the generator) needs a follow-up change. Order within the phase matters — §5 (UUID adapter) should land before or alongside §4, since Identity.Register needs a UUID source for identity IDs with no natural key; §2 can only be fully accepted (not just stubbed) once §3 exists to make the authorization decision. §31 and §33 are cheap additions best made here, while the object model is still small: §31 becomes meaningful the moment Governance/Identities exist as real fulfillments to graph, and §33 (provenance on IR nodes) is far cheaper to add now than to retrofit once Onboarding (§17) and ontology upgrades (§22) depend on it.

At the end of this phase, command-level authorization is organizational rather than merely ambient-role matching.

Phase 4 — Ontology adoption

§30 Projector framework · §15 UL Projection · §16 UL adoption recipe · §17 Onboarding Bluebook · §18 role mapping review · §19 existing-role discovery · §21 semantic drift detection · §22 canonical ontology upgrades · §20 semantic mapping suggestions (AI, optional tail)

Build §30 first — it’s the general registration framework, and §15 is its first real target, so building the framework in a vacuum before this phase would be guessing at an interface. §15 → §16 → §17 → §18 → §19 follow the natural onboarding flow (project a canonical Bluebook into org vocabulary → keep the recipe → give onboarding a home → let SMEs review the mapping → let them discover existing roles to map onto). §21 (drift detection) should land before §22 (ontology upgrades), since upgrades are explicitly a three-way merge built on top of drift detection. §20 is genuinely optional and lowest-value here — AI-ranked suggestions only matter once there’s a real mapping backlog to rank, so it can trail or slip to a later pass without blocking anything.

Status note (2026-08-08): §30 is done — see its own Status line. Nothing else in this phase has started; §15 (UL Projection) is next, now that the framework it registers against is real.

At the end of this phase, a canonical Bluebook can be shipped and a customer can adopt it into its own language. UL projection/onboarding should not be allowed to destabilize runtime IR any more than necessary: vocabulary and provenance changes should land as a clean, bounded addition to the IR contract, not as ongoing churn — Phase 2’s Rust generator extends to cover whatever this phase adds, verified by its harness, rather than treating onboarding as something Rust needs to wait on or stay in lockstep with.

Phase 5 — Canonical domain expansion

§14 People/Customers/Users decomposition · §28 canonical corpus (ongoing)

§14 depends on Identities/Governance (Phase 3) and is the first real test of the UL/onboarding machinery from Phase 4. §28 isn’t a single task — it’s an ongoing practice of writing more canonical domains at the Phase 4 quality bar — but it can’t produce anything credible until Governance, Identities, and People/Customers exist as its first real examples.

This phase still doubles as a stress test for the IR’s expressiveness — writing real domains against Phase 3/4’s semantics is how gaps in them surface — but per 0010, Rust (Phase 2) no longer waits for that proof before starting. Instead, whatever this phase finds and fixes about the IR flows into Phase 2 through the same “chase Ruby” mechanism as everything else, verified by the harness as it lands rather than gating Rust’s start.

Phase 6 — Auth projections

§11 OIDC client projection · §13 email OTP adapter · §12 OIDC provider projection

Both §11 and §13 only need Identities (Phase 3) and can run in parallel — neither depends on Phase 2’s Rust runtime. §12 is ordered last and is by far the biggest lift (XL, full protocol surface) — it should wait until the client-side flow (§11) has validated the (issuer, subject) → Identity mapping in practice.

Phase 7 — History & analytics

§25 era-addressable query API · §26 historical reports · §27 data-engineering projections from reports

§25 is mostly API surface over storage that already exists (DeclarationSnapshot, era_check, era_tamper) and unblocks §26’s historical/current interpretation modes. §27 depends on both §6 (the report keyword) and §30 (the projector framework), both already in place by this point.

Phase 8 — Persistence projection

§10 SQL projection vs SQL runtime

Deliberately isolated as its own phase: Target A (DDL-as-artifact) is a moderate, low-risk win, but Target B (transactional command semantics as stored procedures) is research-grade and shouldn’t gate anything else. Slot Target A wherever bandwidth allows once §30 exists; treat Target B as optional.

Phase 9 — Standards & compliance

§23 ISO discovery and projection · §24 standards conformance drift

Lower priority (P2) and largely independent of the rest of the roadmap, but §24 explicitly depends on §23 existing first (a mapping has to exist before its drift can be detected), and both benefit from §21’s drift-detection groundwork and §33’s provenance edges.

Phase 10 — Research / long tail

§29 GitHub/documentation pattern discovery

Lowest priority (P3) and least defined. Needs the canonical corpus (§28) to already be a going concern before a discovery pipeline for growing it is worth building.


In flight — identity, relationships, routing, and dependency planning

Status note (2026-08-20): this is not one of §1–§34, and it does not belong to a phase above. It is an out-of-band track: an agent-run implementation of docs/value-object-identity-and-relationships-plan.md and its ADR (0029), which supersede the Identity and References halves of ADR 0025 rather than any roadmap section. It is recorded here because it is the largest uncommitted change this checkout has ever carried — ~291 modified tracked files and ~48 new untracked ones, nothing committed — and because a reader arriving at “Recommended implementation order” would otherwise pick up a phase whose ground has already shifted under it. Everything below was measured against the working tree, not read off the implementing agent’s own summary: the suite, hecks model_check, hecks measure_doc_coverage, rubocop, and the Rust crates were all run on 2026-08-20.

What it is

Four language/runtime changes landed together, because each one is the other three’s prerequisite:

  1. Identity is a value concept, not a field pointer. identified_by AccountNumber, as: :number, an inline identified_by do attribute … end block that synthesizes a deterministic value object, and identified_by :branch_code, :box_number as an explicit two-or-more compound key. The wire shape is unchanged — still identified_by: [path, …], still joined by Naming.identity — so no storage rewrite was needed.
  2. Relationship words tell the truth about cardinality. has_many, has_one, and belongs_to are live again, with has_many a real reference list rather than the scalar it used to be under a plural name. Relationship kind survives into IR so a projector can tell belongs_to from a neutral reference_to.
  3. Routing is not payload. Receiver identity travels in to:; with: carries only new domain facts. Runtime::Routing, Runtime::ReactionInvocation, and Facade::CommandRequest are the new seams; policies, process managers, handles, CLI, Forms, and the JSON door all consume the same envelope, and reference_to inside a command/query/policy/port operation is now a hard Malformed refusal.
  4. Persistence is a planned byproduct. Runtime::DependencyPlanning::Analyzer derives a command’s read set, write set, and completeness from its own semantics, and CommandInterpreter#step_hydrate picks an execution strategy from it (ATOMIC_PUT vs. load/apply/validate/store) instead of branching on Command#creates?. Ports::Persistence::Execution/Outcome are the adapter contract; Memory is the oracle, SQLite the pilot, D1/Postgres/PostgresEra implement it.

Two structural reorganizations rode along and account for most of the file count: finished material moved from docs/{decisions,guides,reference,resolution-rules}/ into docs/implemented/… (moves plus link fixes only — no decision text was rewritten), and the monolithic sources split into concept files — examples/banking/bluebook/banking.bluebook into eight business-concept bluebooks, language/bluebook/{behavior,reaction}.bluebook into command/policy/process_manager/query, and syntax.bluebook’s 1083 lines of central tables distributed into each aggregate’s own file. Runtime::EraCheck.source_text_for was widened to digest a chapter spread across several files, which is what made that split safe.

Where it actually got to

Status note (2026-08-28): the “109” figure below, and “What’s next” item 1 right after it, both predate a fix that has since landed — legacy_ implicit_creation? already dropped plan.write_set.empty? by the time this note was written (ctx.route.nil? && ctx.command.creates?, unchanged since). Separately, and later: a full corpus audit (DependencyPlanning::Analyzer against every creates?-true command in all 5 example domains) found and fixed 4 commands whose plan was genuinely incomplete (Statement.Generate, CreatePizza, Chess::Game.Start, Roster.Open — each declared an attribute and never sets it) — real progress on item 5’s own recipe-by- recipe migration, from a different angle. CommandInterpreter#hydrate_ legacy_creation (the fallback this whole passage is about) is NOT deleted, though a first attempt tried to, on the strength of that one audit — running the full suite afterward (not merely trusting the audit’s own scope) found 218 unrelated failures: dozens of separate spec fixtures across the suite, well beyond the 5 example domains, declare their own creates?-true commands the same incomplete way and depend on this exact fallback to create anything at all. See command_interpreter.rb’s own current comment on legacy_implicit_creation? for the corrected account. Read the rest of this section, unedited below, as the historical record it is — the specific counts and named specs no longer describe the current tree, but the fallback itself is still live and still load-bearing.

The plan’s own Wave 0–6 checkpoints are substantially real; Wave 7 (corpus and prose migration) barely started, and Wave 8 (regeneration, then the first full-suite run the plan permits) never ran at all. The implementing agent stopped mid-Wave-6 on an external usage limit, not at a checkpoint — so the tree is a genuine mid-wave snapshot, not an abandoned one.

Measured, with the ordinary fuzzing/io exclusions:

Gate Result
bundle exec rspec 1691 examples, 146 failures
cargo test — parser / codegen / build / root lib green (65+10, 3, 21, 22+4); rust/host will not build — the aws-* crates want rustc 1.94.1 and this machine has 1.94.0, unrelated to anything here
hecks model_check clean
hecks measure_doc_coverage fails — reference pages stale, and Entity’s has_many/has_one/belongs_to carry no prose and no running example
bundle exec rubocop 257 offenses across 43 files, every one of them a file this change touched; 249 autocorrectable, 199 of those a single Layout/HashAlignment pass

The 146 failures are not 146 problems. They are five causes:

Worth reading as evidence rather than noise: docs/implemented/guides/aggregates-and-value-objects.md:101 fails because a second SafeDepositBox.rent! now succeeds where the guide says it refuses. That is not a stale doc. hydrate_complete_state mints a fresh Instance and hands it to atomic_put without consulting repository.find, so the AlreadyExists refusal — whose removed comment records a real shipped bug, “the tenant a box was rented to, gone without a refusal” — now exists only on the legacy path.

What’s next

In order. The first two are gates on everything after them.

  1. Widen the legacy hydration fallback. Drop plan.write_set.empty? from legacy_implicit_creation? so command.creates? alone carries un-routed legacy source. The plan’s own wave rule — “Do not leave the main branch in a state where the grammar refuses source that has not yet migrated” — makes this the fix, not the corpus sweep; step 5 is what eventually deletes the fallback. Expect the 109 to close, the Wave 2B exemplar among them. Check Bluebook::Entity.Seal and Bluebook::Aggregate.Seal alongside it: those two reach the legacy branch and then refuse for a different reason — no identity facts to derive — and may want the same decision.
  2. Decide the duplicate-creation question deliberately, and write it down. The plan says complete commands are PUTs that may insert or replace; the deleted hydrate comment says a silent replace has already cost this project real data. Either re-introduce an existence check before atomic_put for the creating case, or amend docs/implemented/guides/aggregates-and-value-objects.md and ADR 0029 to say the refusal is gone on purpose. Do not leave it as an accident of which hydration branch a command happens to take.
  3. Fix reaction target resolution. ReactionInvocation.resolve_target assumes registry.bluebook(domain) answers #aggregate, and raises UnknownVerb when the target aggregate is not loaded. spec/runtime/reaction_defects_spec.rb catches both consequences: a domain refusal (“a cross-domain target not loaded in this deployment” — that spec’s own words) is now recorded as a runtime defect, which is exactly the distinction that spec exists to hold.
  4. Fix era/shadow parsing of value objects. spec/oidc_manifest_spec.rb fails with NameError: undefined local variable or method 'name' for ValueObjectBuilder out of WordGate#method_missing while shadow-parsing a held era. Frozen history must stay readable; this is the compatibility boundary the plan calls non-negotiable.
  5. Finish Wave 7 corpus migration, in the plan’s order and against its recipes. safe_deposit_boxes.bluebook is the resolved exemplar; deposit_accounts (13), transfers_and_payments (21), payment_cards (18), new_customer_onboarding (5), customer_and_compliance_views (5), customer_relationships (3), statements (3), and pizzas (2) still carry reference_to. The self-hosted language is down from 89 to 55, with aggregate (13), command (10), and query (8) the remaining bulk.
  6. Update the specs that still assert the retired forms. spec/dsl_spec.rb expects the old one-symbol identity refusals; note that identified_by :name, as: :other now raises nothing at all, which is a real hole rather than only a stale expectation. spec/evolve_spec.rb expects a deprecated keyword row and there are none left — the lifecycle machinery has lost its live example. spec/word_coverage_spec.rb wants the now-outgrown belongs_to (Aggregate) exemption deleted. spec/runtime/saga_durability_spec.rb’s saga_mutex race gets born_count 0 and should be re-checked after step 1 rather than assumed collateral.
  7. Write the missing prose. Entity’s has_many, has_one, and belongs_to need a section and a running example each; hecks measure_doc_coverage names them precisely. Then Wave 7D’s remaining guide/ADR prose.
  8. Run rubocop -a over the 43 touched files — 249 of the 257 offenses are autocorrectable and 199 are one hash-alignment pass — then hand-fix the residue (Lint/StructNewOverride, Metrics/*, Naming/Predicate*).
  9. Then Wave 8: regenerate parser keywords, IR goldens, reference pages, README indexes, and the Banking/Pizzas Rust output exactly once, through the plan’s exemplar-first regeneration recipe — inspect one projection’s diff before rewriting all of them.
  10. Then, and only then, the first full-suite run the plan permits, plus Ruby/Rust parity and the adapter agreement matrix in CI. Bump the local toolchain to 1.94.1 first so rust/host is actually in that matrix.

Nothing here needs re-deriving: the wave structure, exemplar-first rule, and per-wave checkpoints in docs/value-object-identity-and-relationships-plan.md still hold, and its own “Implementation checkpoint — 2026-08-18” list is an accurate account of what was resolved before the work stopped.


Definition of done for any new Hecks feature

A feature is not complete merely because the Ruby DSL accepts it.

An agent should check every applicable box:


Central architectural summary

Hecks should remain a small language for executable organizational knowledge. The canonical IR is the durable semantic representation. Ruby is an excellent authoring/runtime environment but not a mandatory deployment dependency — a second hand-written interpreter maintained through differential parity is a cost this project has already paid once and retired (see docs/implemented/rust-experiment.md), so any non-Ruby runtime must consume canonical IR through a thin projection with a small handwritten kernel, not reimplement semantics from scratch. Ports define required capabilities; Bluebooks or adapters fulfill them. Imperative facts such as UUIDs, time, authentication results, and external API responses enter through adapters and become durable history. Commands carry a single responsibility role; Governance maps real organizational authority onto those roles and controls act_as transitions. Reports expose the analytical face of the same organizational model. Canonical Bluebooks can be adopted through UL Projection so organizations gain reusable ontology without giving up their own language. Eras preserve both facts and the interpreter that gave those facts meaning. Projections then carry the same organizational knowledge into Rust, WASM, OIDC, SQL, documentation, standards, and future ecosystems without forcing the core language to grow.


Appendix — Future Direction

The roadmap should evolve alongside the codebase.

When implementing any feature:

This document is intended to become the long-term architectural memory of Hecks.