Class: Hecks::Bluebook::Assembly
- Inherits:
-
Object
- Object
- Hecks::Bluebook::Assembly
- Defined in:
- lib/hecks/bluebook/assembly.rb,
lib/hecks/bluebook/assembly/build.rb,
lib/hecks/bluebook/assembly/marks.rb,
lib/hecks/bluebook/assembly/contract.rb,
lib/hecks/bluebook/assembly/contracts.rb,
lib/hecks/bluebook/assembly/specializer.rb,
lib/hecks/bluebook/assembly/aggregate_assembly.rb
Overview
A GRAPH BUILT FROM DECLARATIONS, rather than from DSL calls.
This is the half that lets the language orchestrate. Reconstruction reads a
chapter back out of the meta-domain, in declaration order, as plain
declarations — the same shape to_h spells. This turns those declarations
into the graph the runtime runs: IR aggregates with their verbs, value
objects, entities and asks, owned by the chapter that declares them.
It takes a HASH, not a runtime, on purpose. That makes it a pure inverse of
to_h and testable without the meta-domain in the picture at all:
Assembly.call(built.to_h).to_h == built.to_h
which is the check spec/assembly_spec makes for every chapter in the tree.
Feed it the reconstruction instead and the same code assembles what the
LANGUAGE holds — the only difference being where the declarations came from.
EVERY FIELD IS READ FROM ONE TABLE. There is no method per category here:
Contracts names what the language cannot say about a construct, Build
reads it, and the coverage gate holds the table to the language. The first
draft of this file did have a method each, which is the shape the judge used
to have — and the price of that shape was fourteen verbs the language declared
and nothing ever offered.
What stays hand-written is the CONTAINMENT: which construct holds which. That is not a field table. (The runtime SURFACE is no longer built here at all — the door is a per-boot projection, facade/surface.rb.)
Defined Under Namespace
Modules: Build, Marks, Specializer Classes: AggregateAssembly, Contract
Constant Summary collapse
- PARENT_POINTERS =
The fields that hold a parent's id without being spelled
*_id. An entity is declared in an aggregate, a member on a value object, a dispatch inside a handler, and each names its parent with a word rather than a suffix. %i[owner shape handler].freeze
- ELSEWHERE =
The only fields allowed to claim they describe something other than the construct they hang off, each with the reason spelled out.
normalisations the canonical-form table belongs to the expression grammar. A chapter's rules are canonicalised on the way in, so the language models the table — but no chapter STORES one, and `Bluebook#to_h` splices it in from Expression. { Bluebook: %i[normalisations] }.freeze
- CONTRACTS =
{ "Bluebook" => Contract.new( holder: Chapter, make: :new, fields: { name: [:name, :plain], version: [:version, :plain], vision: [:vision, :plain], classification: [:classification, :plain], formerly_known_as: [:formerly_known_as, :plain], attaches_to: [:attaches_to, :plain] }, rows: { normalisations: :normalisation_table }, derived: { normalisations: :elsewhere } ), "Aggregate" => Contract.new( holder: Aggregate, make: :new, fields: { name: [:name, :plain], description: [:description, :plain], identified_by: [:identified_by, :plain], attributes: [:attributes, [:each, :attribute]], # THE AGGREGATE BOUNDARY, and the precondition a command may # reference by name (S10, ADR 0025 — "Rules"). Same reader # shapes ValueObject's own `invariants`/Command's own # `givens` already use — `invariant` builds an Invariant, # `given` a Given, the same struct AggregateBuilder#given # hands a referencing command's own resolved list. invariants: [:invariants, [:each, :invariant]], preconditions: [:preconditions, [:each, :given]], # S12, ADR 0025 — the local half of "projects :name, from: # :\"reference.remote_field\"", read the same way # invariants/preconditions above are: a synthetic command # (Aggregate.Projects) the judge dispatches once per # declaration, folded into a list on the owning aggregate. projected_fields: [:projected_fields, [:each, :projected_field]], provenance: [:provenance, :plain] }, rows: { transitions: :transition_rows, value_objects: :value_object_names, identified_by: :identity_rows }, reads: { identified_by: [:each, :identity_path], attributes: [:each_with_id, :attribute], invariants: [:each, :rule], preconditions: [:each, :rule], projected_fields: [:each, :projected_field] }, derived: { position: :walk, state_field: [:folded, :lifecycle, :field], state_start: [:folded, :lifecycle, :default], transitions: [:folded, :lifecycle, :transitions], value_objects: :children } ), "Command" => Contract.new( holder: Command, make: :declare, fields: { name: [:name, :plain], role: [:role, :plain], goal: [:goal, :plain], references: [:references, :plain], attributes: [:attributes, [:each, :shape_field]], givens: [:givens, [:each, :given]], ensures: [:ensures, [:each, :given]], mutations: [:mutations, [:each, :mutation]], emits: [:emits, :plain], # LIFECYCLE STATE AS A COMMAND GUARD (S10, ADR 0025) — a # literal the same way `provenance`/`default:` already are; # one state or an array of them, or nil for a command with # no such guard. from: [:from, :plain], provenance: [:provenance, :plain] }, rows: { mutations: :mutation_rows }, reads: { attributes: [:each, :shape_field], givens: [:each, :rule], ensures: [:each, :rule], mutations: [:call, :mutations], emits: :names, provenance: :provenance, from: :from }, derived: { position: :walk } ), "ValueObject" => Contract.new( holder: ValueObject, make: :declare, fields: { name: [:name, :plain], attributes: [:attributes, [:each, :shape_field]], invariants: [:invariants, [:each, :invariant]], members: [:members, [:each, :member]], closed_set: [:closed_set, :flag] }, # The language counts the admitted rows ; the IR keeps a flag and the rows. reads: { attributes: [:each, :shape_field], invariants: [:each, :rule], closed_set: [:call, :closed_set_of], members: [:call, :members_row] }, derived: { position: :walk, rows: [:folded, %i[closed_set members], nil] } ), "Query" => Contract.new( holder: Query, make: :new, fields: { name: [:name, :plain], description: [:description, :plain], attributes: [:attributes, [:each, :shape_field]], wheres: [:wheres, [:each, :where_clause]], order_by: [:order_by, :order_by], limit: [:limit, :limit], # Held by the language as an OPEN MAP, so every one of these reads the # same way and a ninth option needs no new field on either side. offset: [:offset, [:option, :offset]], cursor: [:cursor, [:option, :cursor]], null_semantics: [:null_semantics, [:option, :null_semantics]], authorization: [:authorization, [:option, :authorization]], inspection: [:inspection, [:option, :inspection]] }, rows: { wheres: :where_rows, options: :option_rows }, reads: { attributes: [:each, :shape_field], wheres: [:each, :where_clause], order_by: [:call, :order_by], limit: [:call, :limit] }, derived: { position: :walk, order_field: [:folded, :order_by, :field], order_way: [:folded, :order_by, :direction], options: [:folded, %i[offset cursor null_semantics authorization inspection], nil] } ), "Entity" => Contract.new( holder: Entity, make: :declare, fields: { name: [:name, :plain], description: [:description, :plain], # A LIST OF PATHS, exactly as an aggregate's is. The two used to differ # — a Symbol here and a String there, which byte equality with `to_h` # COULD NOT SEE because both render as a string, so the assembled graph # got a String and `element_of` looked up `args["sequence"]` in a # symbol-keyed payload and found nothing: "Reverse acts on one # LedgerEntry — pass sequence:", while passing sequence. There is one # spelling now, and no room left for that difference. identified_by: [:identified_by, :plain], attributes: [:attributes, [:each, :shape_field]], # ADR 0028 — the SAME shape Aggregate's own `preconditions` # already carries, one level down: a piece's own named # `given`, referenced back by one of its own commands. preconditions: [:preconditions, [:each, :given]], # Round 7 — the SAME shape Aggregate's own `invariants` # already carries, one level down: checked against every # instance of this piece, not the aggregate's own flat state. invariants: [:invariants, [:each, :invariant]] }, rows: { transitions: :transition_rows, identified_by: :identity_rows }, reads: { identified_by: [:each, :identity_path], attributes: [:each, :shape_field], preconditions: [:each, :rule], invariants: [:each, :rule] }, derived: { position: :walk, owner: :parent, state_field: [:folded, :lifecycle, :field], state_start: [:folded, :lifecycle, :default], transitions: [:folded, :lifecycle, :transitions] } ), "Policy" => Contract.new( holder: Policy, make: :new, fields: { name: [:name, :plain], aggregate: [:aggregate, :plain], on_event: [:on_event, :plain], trigger_command: [:trigger_command, :plain], target_domain: [:target_domain, :plain], where: [:where, :plain], for_each: [:for_each, :plain], with_spec: [:with_spec, :bindings] }, rows: { with_spec: :with_spec_rows }, reads: { with_spec: [:from, :with_spec] }, derived: { position: :walk } ), "ProcessManager" => Contract.new( holder: ProcessManager, make: :new, fields: { name: [:name, :plain], # A SYMBOL. `SagaInterpreter` does `event.payload[pm.correlates_by]` — a # hash lookup on a symbol-keyed payload — and `value == pm.correlates_by` # when resolving a leg s bindings. A String there finds nothing and # resolves to nothing, so the wire never advanced and a drawer that # should have held 2500 held 0. to_h could not show it: it stringifies. correlates_by: [:correlates_by, :identity], starts_on: [:starts_on, :plain], ends_on: [:ends_on, :plain], states: [:states, :plain] }, reads: { states: :names }, # S17, ADR 0026 — `handlers` is a REAL feature of the LANGUAGE's # own "ProcessManager" declaration now (`attribute :handlers, # list_of(Handler)`, reaction.bluebook), consumed by the judge # walking `Plan`'s own containment tree (`Handler`'s own # `.parent == "ProcessManager"`) rather than by any field this # contract itself reads — the same `:children` shape Aggregate's # own `value_objects` claim already uses, one level in. derived: { position: :walk, handlers: :children } ), # S17, ADR 0026 — Handler is a genuine entity now, nested under # ProcessManager (`entity "Handler"`, reaction.bluebook). Neither # `position` nor `handler` is a stored field any more: a saga # answers each event ONCE, so `event_type` is Handler's own real, # non-positional identity (no walk-minted `position` to derive), # and the process manager it belongs to is structural now — which # list this element sits in, not a stored field to fold a parent # pointer out of. "Handler" => Contract.new( holder: ProcessManagerHandler, make: :new, fields: { event_type: [:event_type, :plain], from_state: [:from_state, :plain], to_state: [:to_state, :plain] }, # `dispatches` — same reason ProcessManager's own `handlers` # claim, above, is `:children` : a real feature of the # LANGUAGE's own "Handler" declaration (`attribute :dispatches, # list_of(Dispatch)`), consumed by the judge walking `Plan`'s # own containment tree rather than by any field this contract # reads. derived: { dispatches: :children } ), # S17, ADR 0026 — Dispatch is a genuine entity now, nested under # Handler (`entity "Dispatch"`, reaction.bluebook) — two levels # deep, "no life outside its Handler" (the ADR's own words). # `command_name` is Dispatch's own real, non-positional identity # — neither `position` nor `handler` is a stored field any more, # the same reason Handler's own contract, above, dropped them. "Dispatch" => Contract.new( holder: DispatchSpec, make: :new, fields: { command_name: [:command_name, :plain], with_spec: [:with_spec, :bindings] }, rows: { with_spec: :with_spec_rows }, reads: { with_spec: [:from, :with_spec] }, derived: {} ), # S14, ADR 0026 — Syntax/Keyword/Argument are never built via # `Build`/`Reconstruction`'s own generic path — their own data # lives in a dedicated repository `SyntaxBoot` (meta_validator/ # syntax_boot.rb) reads directly, never through the meta- # domain's own reconstruction, the same reason `Member`'s own # entry (above) carries `holder: nil, make: nil` too. These # entries exist purely so the introspection specs # (assembly_spec.rb) can hold them to the same "every field # claimed" discipline every other category answers to. "Syntax" => Contract.new( holder: nil, make: nil, fields: { name: [:name, :plain] }, derived: { keywords: :children, arguments: :children } ), "Keyword" => Contract.new( holder: nil, make: nil, fields: { word: [:word, :plain], context: [:context, :plain], body: [:body, :plain], inner: [:inner, :plain], opens: [:opens, :plain], fills: [:fills, :plain], was: [:was, :plain], resolves_via: [:resolves_via, :plain], disambiguator: [:disambiguator, :plain], calls: [:calls, :plain] }, derived: { position: :walk } ), "Argument" => Contract.new( holder: nil, make: nil, fields: { keyword: [:keyword, :plain], context: [:context, :plain], at: [:at, :plain], named: [:named, :plain], kind: [:kind, :plain], required: [:required, :plain], fills: [:fills, :plain], selects: [:selects, :plain], pair_key_fills: [:pair_key_fills, :plain], pair_value_fills: [:pair_value_fills, :plain], pairs_shape: [:pairs_shape, :plain], variadic: [:variadic, :plain], minimum: [:minimum, :plain], coerce: [:coerce, :plain], blank_message: [:blank_message, :plain] }, derived: { position: :walk } ), "ReadModel" => Contract.new( holder: ReadModel, make: :new, fields: { name: [:name, :plain], description: [:description, :plain], reference_name: [:reference_name, :identity], reference_target: [:reference_target, :plain], aggregate_heads: [:aggregate_heads, [:each, :head]], group_by: [:group_by, [:each, :group_by_field]], # `count`/`median_field` are `group_by`'s own two siblings — # scalars, not lists, so no `[:each, ...]` shape ; `:plain` # is what `Assembly::Build` needs (fed the native `to_h` # value directly, already `true`/`nil`/a String), and the # `reads:` entry below is what `Reconstruction` needs # instead (fed the STRINGIFIED meta-domain row). count: [:count, :plain], median_field: [:median_field, :plain], # A read model inherits every option an ask has, so it reads them the # same way — see Query. wheres: [:wheres, [:each, :where_clause]], order_by: [:order_by, :order_by], limit: [:limit, :limit], offset: [:offset, [:option, :offset]], cursor: [:cursor, [:option, :cursor]], null_semantics: [:null_semantics, [:option, :null_semantics]], authorization: [:authorization, [:option, :authorization]], inspection: [:inspection, [:option, :inspection]] }, rows: { options: :read_model_option_rows }, # `wheres` needs its own reader for the same reason Query's does — a # list defaults to `[]`, not the generic `text(row[key])` cell reader's # `nil` — even though a read model's row never carries `wheres` as a # native field the way Query's does (`[:each, :where_clause]` over an # absent `row[:wheres]` is `Array(nil).map { ... }`, i.e. `[]`, always). # The REAL values, when a read model declares any, arrive through # `options_of(row)`'s merge in `read_model` below (dispatched as # generic Option rows, `read_model_option_rows`/`filter_options` — not # as dedicated where-clause rows), which overrides this default. Until # `ReadModel#to_h` spelled `wheres`/`order_by`/`limit` # unconditionally, `round_trip_spec`'s own "SOURCE KEYS ONLY" +compare # never asked about this key at all, so the `nil`-vs-`[]` gap between # this contract's default and `to_h`'s own `[]` default went unnoticed. # `order_by`/`limit` need no matching entry — the generic reader's # `nil` already agrees with `to_h`'s own nil-when-undeclared default # for those two. `group_by` needs its own reader the same way — a # rootless read model's grouping keys, added independently on main. reads: { reference_name: :symbol, aggregate_heads: [:each, :head], group_by: [:each, :group_by_field], wheres: [:each, :where_clause], # `count` needs the boolean coercion `Shapes#read_model_count` # gives it (a stringified "true"/absent on the wire, a real # `true`/`nil` in `to_h`) — `median_field` needs none: the # default `text(row[key])` cell reader already returns the # same String-or-nil `ReadModel#to_h`'s own `&.to_s` does. count: :read_model_count }, derived: { position: :walk, query_name: [:computed, :query_name], options: [:folded, %i[offset cursor null_semantics authorization inspection], nil] } ), # A member's pairs are an OPEN MAP, which is why Member is its own root in # the language. The IR keeps them as a plain hash on the value object, so # they are assembled with their shape rather than as a construct. # S17, ADR 0026 — Member is a genuine entity now, nested under # ValueObject (`entity "Member"`, shape.bluebook). It no longer # holds a `shape` field at all (that was the free-text, un-parsed # spelling a standalone root once needed ; an entity's element is # never serialized as text) — only `position` (walk-minted, see # `entity_own_identity`, judge.rb) and `pairs` (still an open map, # still one row per entry, still why a value object cannot hold it # directly). "Member" => Contract.new( holder: nil, make: nil, fields: {}, rows: { pairs: :pair_rows }, derived: { position: :walk, pairs: [:folded, %i[members], nil] } ) }.freeze
Class Method Summary collapse
- .call(declaration) ⇒ Object
-
.contract(category) ⇒ Object
ONE TABLE, WHERE THERE WERE FIVE HAND-WRITTEN MIRRORS OF IT.
- .elsewhere?(category, field) ⇒ Boolean
Instance Method Summary collapse
- #bluebook ⇒ Object
-
#initialize(declaration) ⇒ Assembly
constructor
A new instance of Assembly.
Constructor Details
#initialize(declaration) ⇒ Assembly
Returns a new instance of Assembly.
33 34 35 |
# File 'lib/hecks/bluebook/assembly.rb', line 33 def initialize(declaration) @declaration = declaration end |
Class Method Details
.call(declaration) ⇒ Object
31 |
# File 'lib/hecks/bluebook/assembly.rb', line 31 def self.call(declaration) = new(declaration).bluebook |
.contract(category) ⇒ Object
ONE TABLE, WHERE THERE WERE FIVE HAND-WRITTEN MIRRORS OF IT.
bluebook.bluebook already declares what every construct is made of, and
Plan already reads it — parent, fields, lists, setters. What the language
cannot say is the three things Ruby needs to rebuild one, so those are here
and ONLY those:
holder which class holds it
make :declare for a construct that became a class, :new for an
instance — the boundary `Query` sits on, since its body is
inherited instance methods
fields each keyword the constructor takes, as
keyword => [key in the declaration, how to read it]
A READER is a symbol naming a method on Marks, or :plain for a value that needs no decoding, or [:each, reader] for a list. Nothing here is a rule about whether a declaration is admissible — the language settled that on the way in. This is only how a spelling becomes an object again.
WHY A TABLE AND NOT A METHOD PER CATEGORY. The judge used to carry one hand-written branch per category, and the price was fourteen verbs the language declared and the walk never offered — every rule hanging off them decoration, and nothing red, because a branch that does not exist cannot fail. An assembler with a method per category is the same shape. So the table is checked AGAINST the language by spec/assembly_spec: a field the language declares that no contract consumes is a failure, not a silence.
The derived list is how a field says it needs no assembling — a parent
pointer the containment tree already knows, or something computed from what
is here (query_name is Naming.snake(name), creates? is whether a verb
names a root). Naming one is a claim, and the coverage gate holds it.
35 |
# File 'lib/hecks/bluebook/assembly/contracts.rb', line 35 def self.contract(category) = CONTRACTS.fetch(category.to_s) |
.elsewhere?(category, field) ⇒ Boolean
107 108 109 |
# File 'lib/hecks/bluebook/assembly/contract.rb', line 107 def self.elsewhere?(category, field) Array(ELSEWHERE[category.to_sym]).include?(field) end |
Instance Method Details
#bluebook ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/hecks/bluebook/assembly.rb', line 37 def bluebook aggregates = Array(@declaration[:aggregates]).map { |row| AggregateAssembly.new(row).aggregate } models = Array(@declaration[:read_models]).map { |row| Build.call("ReadModel", row) } chapter = Build.call( "Bluebook", @declaration, aggregates: aggregates, read_models: models, policies: reactions(aggregates), process_managers: Array(@declaration[:process_managers]).map { |row| process_manager(row) } ) chapter end |