hecks 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
- data/lib/hecks/adapters/driven/postgres.rb +45 -31
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +15 -4
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +51 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/glossary.rb +374 -0
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projections.rb +1 -0
- data/lib/hecks/projector/cli_projector.rb +78 -42
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +34 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/common/order_by.rb +9 -2
- data/lib/hecks/query_specification/common/where_clause.rb +15 -2
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +45 -9
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +271 -49
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +16 -2
|
@@ -3,7 +3,7 @@ require_relative "expression/ast_json"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Bluebook
|
|
6
|
-
Invariant = Struct.new(:description, :canonical, :predicate, keyword_init: true)
|
|
6
|
+
Invariant = Struct.new(:description, :canonical, :predicate, :ast, keyword_init: true)
|
|
7
7
|
|
|
8
8
|
# A value object — a DECLARATION HOLDER, never instantiated.
|
|
9
9
|
#
|
|
@@ -37,7 +37,7 @@ module Hecks
|
|
|
37
37
|
# `Expression::AstJson`'s own header — built for `rust/host`'s
|
|
38
38
|
# own mint-time invariant check (`reference_validate.rs`), which
|
|
39
39
|
# has no kernel crate to parse `canonical` with.
|
|
40
|
-
invariants: -> { invariants.map { |rule|
|
|
40
|
+
invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
41
41
|
closed_set: :closed_set?,
|
|
42
42
|
# THE FIELD NAME IS STRINGIFIED, NEVER THE VALUE. A `member` row can
|
|
43
43
|
# hold any of the scalar types an attribute declares — `Integer 84`
|
data/lib/hecks/codemod.rb
CHANGED
|
@@ -221,7 +221,11 @@ module Hecks
|
|
|
221
221
|
# loop for why it can't batch) — splitting it across methods just
|
|
222
222
|
# to satisfy the line count would scatter state four ways for no
|
|
223
223
|
# readability gain.
|
|
224
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
224
225
|
# rubocop:disable-next Metrics/BlockLength
|
|
226
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
227
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
228
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
225
229
|
def run_example_domains(results, dry_run)
|
|
226
230
|
Codemod::EXAMPLE_ROOTS.each do |domain_dir|
|
|
227
231
|
bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook"))
|
|
@@ -286,6 +290,15 @@ module Hecks
|
|
|
286
290
|
# changes the IR, so the running state's own export should always
|
|
287
291
|
# still equal the pristine original after each kept edit, by
|
|
288
292
|
# definition, no moving target needed.
|
|
293
|
+
# Same shape and same reason as run_example_domains just above (see
|
|
294
|
+
# its own comment) — the write/verify/revert sequence, PER
|
|
295
|
+
# CANDIDATE (this method's own comment explains why it cannot
|
|
296
|
+
# batch), is one coherent unit; splitting it would scatter
|
|
297
|
+
# before_meta/live_meta/applied_by_file state across methods for
|
|
298
|
+
# no gain.
|
|
299
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
300
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
301
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
289
302
|
def run_meta_domain(results, dry_run)
|
|
290
303
|
before_meta = Codemod.boot_meta
|
|
291
304
|
meta_candidates = @find_candidates.call(Codemod.meta_registry)
|
data/lib/hecks/doc/reference.rb
CHANGED
|
@@ -114,7 +114,7 @@ module Hecks
|
|
|
114
114
|
by `bin/reference` — do not edit inside the markers. The prose
|
|
115
115
|
between them is hand-written and survives regeneration.*
|
|
116
116
|
#{GENERATED_END}
|
|
117
|
-
#{
|
|
117
|
+
#{"\n#{preamble}\n" unless preamble.empty?}
|
|
118
118
|
#{sections.join("\n")}
|
|
119
119
|
PAGE
|
|
120
120
|
end
|
|
@@ -145,7 +145,7 @@ module Hecks
|
|
|
145
145
|
## #{row[:word]}
|
|
146
146
|
|
|
147
147
|
#{generated_begin(row[:word])}
|
|
148
|
-
#{spellings}#{
|
|
148
|
+
#{spellings}#{" — #{facts.join(', ')}" unless facts.empty?}
|
|
149
149
|
#{table}#{GENERATED_END}
|
|
150
150
|
|
|
151
151
|
#{prose_or_sentinel(prose)}
|
|
@@ -181,7 +181,7 @@ module Hecks
|
|
|
181
181
|
name = arg[:at].to_s.empty? ? "`#{arg[:named]}:`" : "positional #{arg[:at]}"
|
|
182
182
|
lines << "| #{name} | #{arg[:kind]} | #{arg[:required]} | #{arg[:fills]} |"
|
|
183
183
|
end
|
|
184
|
-
lines.join("\n")
|
|
184
|
+
"#{lines.join("\n")}\n"
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def render_index
|
|
@@ -210,6 +210,11 @@ module Hecks
|
|
|
210
210
|
# region existed has no generated marker ahead of its first `## `,
|
|
211
211
|
# so nothing is collecting when that heading arrives and no empty
|
|
212
212
|
# preamble is invented — the older shape reads back unchanged.
|
|
213
|
+
# A single-pass line-scanning state machine (current/collecting/
|
|
214
|
+
# buffer/in_fence) — each branch mutates shared local state that
|
|
215
|
+
# carries into the NEXT iteration, so splitting per branch would
|
|
216
|
+
# mean passing all four back and forth by reference every line.
|
|
217
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
213
218
|
def harvest(text)
|
|
214
219
|
prose = {}
|
|
215
220
|
current = PREAMBLE
|
|
@@ -263,7 +268,7 @@ module Hecks
|
|
|
263
268
|
end
|
|
264
269
|
|
|
265
270
|
def guide_index(root)
|
|
266
|
-
paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
|
|
271
|
+
paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
|
|
267
272
|
.reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
|
|
268
273
|
lines = paths.map do |path|
|
|
269
274
|
heading = File.foreach(path).find { |line| line.start_with?("# ") }
|
|
@@ -273,7 +278,7 @@ module Hecks
|
|
|
273
278
|
lines.join("\n")
|
|
274
279
|
end
|
|
275
280
|
|
|
276
|
-
def reference_index(
|
|
281
|
+
def reference_index(_root)
|
|
277
282
|
count = contexts.size
|
|
278
283
|
"[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
|
|
279
284
|
"the aggregate-local tables under `lib/hecks/language/` and held to them by " \
|
|
@@ -333,11 +338,11 @@ module Hecks
|
|
|
333
338
|
end
|
|
334
339
|
|
|
335
340
|
def corpus_roster(root)
|
|
336
|
-
dirs = Dir.glob(File.join(root, "examples/*/"))
|
|
341
|
+
dirs = Dir.glob(File.join(root, "examples/*/"))
|
|
337
342
|
lines = dirs.filter_map do |dir|
|
|
338
343
|
name = File.basename(dir.chomp("/"))
|
|
339
|
-
bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook"))
|
|
340
|
-
bluebooks = Dir.glob(File.join(dir, "*.bluebook"))
|
|
344
|
+
bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook"))
|
|
345
|
+
bluebooks = Dir.glob(File.join(dir, "*.bluebook")) if bluebooks.empty?
|
|
341
346
|
next if bluebooks.empty?
|
|
342
347
|
|
|
343
348
|
vision = bluebooks.filter_map { |bluebook| File.read(bluebook)[/vision\s+"([^"]*)"/, 1] }.first
|
|
@@ -364,7 +369,7 @@ module Hecks
|
|
|
364
369
|
# example — a word whose only "example" is invisible or inert is a
|
|
365
370
|
# word still shipping on its prose alone, which is the thing this
|
|
366
371
|
# gate exists to refuse.
|
|
367
|
-
EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]
|
|
372
|
+
EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/
|
|
368
373
|
|
|
369
374
|
def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
|
|
370
375
|
|
|
@@ -378,12 +383,13 @@ module Hecks
|
|
|
378
383
|
# `harvest` already rejects empty prose and the TODO sentinel, so a
|
|
379
384
|
# word with nothing written for it arrives here with a nil.
|
|
380
385
|
def live_words(directory)
|
|
381
|
-
contexts.flat_map
|
|
386
|
+
rows = contexts.flat_map do |context|
|
|
382
387
|
path = File.join(directory, page_name(context))
|
|
383
388
|
prose = File.exist?(path) ? harvest(File.read(path)) : {}
|
|
384
389
|
keywords.select { |row| row[:context] == context && live?(row) }
|
|
385
390
|
.map { |row| [row[:word], context, prose[row[:word]]] }
|
|
386
|
-
|
|
391
|
+
end
|
|
392
|
+
rows.uniq { |word, context, _| [word, context] }
|
|
387
393
|
end
|
|
388
394
|
|
|
389
395
|
def name_of(word, context) = "#{word} (#{context})"
|
|
@@ -79,7 +79,7 @@ module Hecks
|
|
|
79
79
|
rescue Runtime::NotFound, Runtime::TypeMismatch => e
|
|
80
80
|
# A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
|
|
81
81
|
# same next step: read what the verb actually takes.
|
|
82
|
-
["#{e.message}\n\n #{program} #{
|
|
82
|
+
["#{e.message}\n\n #{program} #{'ask ' if asking}#{name} --help", 1]
|
|
83
83
|
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
84
84
|
# THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
|
|
85
85
|
[e.message, 1]
|
|
@@ -177,7 +177,7 @@ module Hecks
|
|
|
177
177
|
|
|
178
178
|
lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
|
|
179
179
|
lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
|
|
180
|
-
lines += ["", " #{program}#{
|
|
180
|
+
lines += ["", " #{program}#{' ask' if asking} for the full list"]
|
|
181
181
|
lines.join("\n")
|
|
182
182
|
end
|
|
183
183
|
|
|
@@ -18,9 +18,7 @@ module Hecks
|
|
|
18
18
|
|
|
19
19
|
def normalize(input, receiver:, legacy_receiver: nil)
|
|
20
20
|
request = symbolize(input)
|
|
21
|
-
unless request.is_a?(Hash)
|
|
22
|
-
raise Runtime::TypeMismatch, "a command request must be a hash"
|
|
23
|
-
end
|
|
21
|
+
raise Runtime::TypeMismatch, "a command request must be a hash" unless request.is_a?(Hash)
|
|
24
22
|
|
|
25
23
|
route, facts = split(request, receiver: receiver, legacy_receiver: legacy_receiver)
|
|
26
24
|
validate_route!(route, receiver)
|
|
@@ -54,9 +52,7 @@ module Hecks
|
|
|
54
52
|
def take_legacy_route(flat, receiver, legacy_receiver)
|
|
55
53
|
return unless legacy_receiver
|
|
56
54
|
|
|
57
|
-
if receiver == :aggregate
|
|
58
|
-
return flat.delete(legacy_receiver.to_sym)
|
|
59
|
-
end
|
|
55
|
+
return flat.delete(legacy_receiver.to_sym) if receiver == :aggregate
|
|
60
56
|
|
|
61
57
|
return unless receiver == :entity && legacy_receiver.is_a?(Hash)
|
|
62
58
|
|
|
@@ -68,23 +64,27 @@ module Hecks
|
|
|
68
64
|
end
|
|
69
65
|
private_class_method :take_legacy_route
|
|
70
66
|
|
|
67
|
+
# A case dispatch over the three closed receiver kinds (nil,
|
|
68
|
+
# :aggregate, :entity) plus the impossible-kind backstop — each
|
|
69
|
+
# branch is its own self-contained validation for that one kind.
|
|
70
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
71
71
|
def validate_route!(route, receiver)
|
|
72
72
|
case receiver
|
|
73
73
|
when nil
|
|
74
74
|
raise Runtime::TypeMismatch, "this command does not take a receiver in to:" unless route.nil?
|
|
75
75
|
when :aggregate
|
|
76
76
|
if route.nil? || route.to_s.empty? || route.is_a?(Hash)
|
|
77
|
-
raise Runtime::TypeMismatch,
|
|
77
|
+
raise Runtime::TypeMismatch,
|
|
78
|
+
"to: must name the receiving aggregate identity"
|
|
78
79
|
end
|
|
79
80
|
when :entity
|
|
80
|
-
unless route.is_a?(Hash)
|
|
81
|
-
raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:"
|
|
82
|
-
end
|
|
81
|
+
raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:" unless route.is_a?(Hash)
|
|
83
82
|
|
|
84
83
|
extra = route.keys - [:aggregate, :entity]
|
|
85
84
|
missing = [:aggregate, :entity].select { |key| route[key].nil? || route[key].to_s.empty? }
|
|
86
85
|
unless extra.empty? && missing.empty?
|
|
87
|
-
raise Runtime::TypeMismatch,
|
|
86
|
+
raise Runtime::TypeMismatch,
|
|
87
|
+
"to: for an entity command must contain only aggregate: and entity:"
|
|
88
88
|
end
|
|
89
89
|
else
|
|
90
90
|
raise ArgumentError, "unknown receiver kind #{receiver.inspect}"
|
data/lib/hecks/facade/handle.rb
CHANGED
|
@@ -18,10 +18,10 @@ module Hecks
|
|
|
18
18
|
class Handle
|
|
19
19
|
attr_reader :id
|
|
20
20
|
|
|
21
|
-
def initialize(dispatcher:, domain:,
|
|
21
|
+
def initialize(dispatcher:, domain:, aggregate:, instance:)
|
|
22
22
|
@dispatcher = dispatcher
|
|
23
23
|
@domain = domain
|
|
24
|
-
@ir =
|
|
24
|
+
@ir = aggregate
|
|
25
25
|
@id = instance.id
|
|
26
26
|
@state = instance.state
|
|
27
27
|
define_reference_accessors
|
|
@@ -72,7 +72,7 @@ module Hecks
|
|
|
72
72
|
# A declared field not yet written arrives here too (nil, the way a
|
|
73
73
|
# defined reader answered). Verbs are NOT handled here — see
|
|
74
74
|
# `define_verb_methods` for why.
|
|
75
|
-
def method_missing(name, *args, **kwargs, &
|
|
75
|
+
def method_missing(name, *args, **kwargs, &)
|
|
76
76
|
return @state[name] if @state.key?(name) || reader?(name)
|
|
77
77
|
|
|
78
78
|
super
|
|
@@ -13,14 +13,25 @@ module Hecks
|
|
|
13
13
|
# `persisted_by`-style binding collector a `.hecksagon` lands on, and
|
|
14
14
|
# the aggregate-scoped `port` a `.hecksagon` lands on beside it.
|
|
15
15
|
module AggregateDoor
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# One method building one `door` module's ~20 singleton methods
|
|
17
|
+
# looks like it splits along each `define_singleton_method` call,
|
|
18
|
+
# but three of those blocks (`:port`, `:method_missing`,
|
|
19
|
+
# `:const_missing`) are a single cross-referencing essay on the
|
|
20
|
+
# stale-facade-across-boots hazard — each one's comment explicitly
|
|
21
|
+
# points at "below"/"above" as part of the SAME method. Splitting
|
|
22
|
+
# into helper methods wouldn't break anything at runtime (no
|
|
23
|
+
# shared mutable state beyond the closed-over args, which just
|
|
24
|
+
# become parameters), but it would sever that narrative across
|
|
25
|
+
# method boundaries for no functional gain.
|
|
26
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
27
|
+
def aggregate_module(dispatcher, domain, aggregate)
|
|
28
|
+
fqn = "#{domain}::#{aggregate.hecks_name}"
|
|
18
29
|
door = Module.new
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
aggregate.attributes.each do |attribute|
|
|
21
32
|
next unless RESERVED.include?(attribute.name.to_sym)
|
|
22
33
|
|
|
23
|
-
warn "[hecks] #{
|
|
34
|
+
warn "[hecks] #{aggregate.hecks_name}##{attribute.name} shadows a built-in — no reader defined"
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
# A creating verb is a MODULE method returning the new record in hand ;
|
|
@@ -33,9 +44,9 @@ module Hecks
|
|
|
33
44
|
# accounts, a real same-aggregate collision this corpus already
|
|
34
45
|
# has) — the suffix is what makes both nameable at all, not
|
|
35
46
|
# merely a style choice.
|
|
36
|
-
|
|
47
|
+
aggregate.commands.select(&:creates?).each do |command|
|
|
37
48
|
door.define_singleton_method("#{Naming.snake(command.hecks_name)}!") do |**args|
|
|
38
|
-
Handle.new(dispatcher: dispatcher, domain: domain,
|
|
49
|
+
Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate,
|
|
39
50
|
instance: dispatcher.dispatch("#{fqn}.#{command.hecks_name}", with: args).instance)
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -46,14 +57,14 @@ module Hecks
|
|
|
46
57
|
# about the way `!` does for a command. Answers the raw row
|
|
47
58
|
# array `dispatcher.query` itself answers, the same shape
|
|
48
59
|
# `runtime.query("#{fqn}.Name")` already gave.
|
|
49
|
-
|
|
60
|
+
aggregate.queries.each do |query|
|
|
50
61
|
door.define_singleton_method(Naming.snake(query.hecks_name)) do |**args|
|
|
51
62
|
dispatcher.query("#{fqn}.#{query.hecks_name}", **args)
|
|
52
63
|
end
|
|
53
64
|
end
|
|
54
65
|
|
|
55
66
|
door.define_singleton_method(:fqn) { fqn }
|
|
56
|
-
door.define_singleton_method(:ir) {
|
|
67
|
+
door.define_singleton_method(:ir) { aggregate }
|
|
57
68
|
|
|
58
69
|
# THE SAME VERB THE CHAPTER ANSWERS, one level down. Every
|
|
59
70
|
# construct emits its own IR (Hecks::IR), so an aggregate
|
|
@@ -67,39 +78,41 @@ module Hecks
|
|
|
67
78
|
# `{"aggregates" => []}` for an aggregate.
|
|
68
79
|
door.define_singleton_method(:project) do |target, out: nil, **options|
|
|
69
80
|
key = Projector.key_for(target)
|
|
70
|
-
artifact = Projector.call(key, bluebook:
|
|
81
|
+
artifact = Projector.call(key, bluebook: aggregate, options: options)
|
|
71
82
|
return artifact unless out
|
|
72
83
|
|
|
73
84
|
Projector.write(artifact, out, as: Projector.emits_for(key))
|
|
74
85
|
end
|
|
75
|
-
door.define_singleton_method(:repository) { dispatcher.registry.repository(domain,
|
|
76
|
-
door.define_singleton_method(:commands)
|
|
77
|
-
|
|
86
|
+
door.define_singleton_method(:repository) { dispatcher.registry.repository(domain, aggregate) }
|
|
87
|
+
door.define_singleton_method(:commands) do
|
|
88
|
+
aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort
|
|
89
|
+
end
|
|
90
|
+
door.define_singleton_method(:queries) { aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
|
|
78
91
|
# ONE AGGREGATE'S USAGE DOCUMENT — the same projection the chapter
|
|
79
92
|
# answers with, narrowed to this head. `commands` above already
|
|
80
93
|
# answers "what can I call"; this answers "and what does each one
|
|
81
94
|
# want, refuse, and guarantee", which is the rest of the question.
|
|
82
95
|
door.define_singleton_method(:docs) do |**options|
|
|
83
96
|
Projector.call(:docs, bluebook: dispatcher.registry.bluebook(domain),
|
|
84
|
-
options: options.merge(aggregate:
|
|
97
|
+
options: options.merge(aggregate: aggregate.hecks_name))
|
|
85
98
|
end
|
|
86
99
|
# ONE AGGREGATE, READ BACK IN ENGLISH — the same narrowing `:docs`
|
|
87
100
|
# takes, aimed at `:narrate` instead.
|
|
88
101
|
door.define_singleton_method(:narrate) do |**options|
|
|
89
102
|
Projector.call(:narrate, bluebook: dispatcher.registry.bluebook(domain),
|
|
90
|
-
options: options.merge(aggregate:
|
|
103
|
+
options: options.merge(aggregate: aggregate.hecks_name))
|
|
91
104
|
end
|
|
92
|
-
door.define_singleton_method(:count) { dispatcher.registry.repository(domain,
|
|
105
|
+
door.define_singleton_method(:count) { dispatcher.registry.repository(domain, aggregate).count }
|
|
93
106
|
door.define_singleton_method(:events) { dispatcher.events.select { |event| event.aggregate == fqn } }
|
|
94
107
|
|
|
95
108
|
door.define_singleton_method(:find) do |id|
|
|
96
|
-
found = dispatcher.registry.repository(domain,
|
|
97
|
-
found && Handle.new(dispatcher: dispatcher, domain: domain,
|
|
109
|
+
found = dispatcher.registry.repository(domain, aggregate).find(id)
|
|
110
|
+
found && Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate, instance: found)
|
|
98
111
|
end
|
|
99
112
|
|
|
100
113
|
door.define_singleton_method(:all) do |**opts|
|
|
101
|
-
dispatcher.registry.repository(domain,
|
|
102
|
-
Handle.new(dispatcher: dispatcher, domain: domain,
|
|
114
|
+
dispatcher.registry.repository(domain, aggregate).all(**opts).map do |instance|
|
|
115
|
+
Handle.new(dispatcher: dispatcher, domain: domain, aggregate: aggregate, instance: instance)
|
|
103
116
|
end
|
|
104
117
|
end
|
|
105
118
|
|
|
@@ -109,20 +122,20 @@ module Hecks
|
|
|
109
122
|
# `ConstShim`/`const_missing`, so `Pizzas::Pizza.port(...)` lands
|
|
110
123
|
# HERE instead once any boot has run before.
|
|
111
124
|
#
|
|
112
|
-
# RE-RESOLVED, NOT THE CLOSED-OVER `
|
|
113
|
-
# one, built by a boot from earlier in this same process,
|
|
114
|
-
# on the `Pizzas`/`Pizza` constants only because nothing
|
|
115
|
-
# re-installed them since. Attaching to this door's OWN
|
|
116
|
-
# attach the port to a discarded aggregate from
|
|
117
|
-
# invisible to the CURRENT one actually being
|
|
118
|
-
# the exact way `method_missing` below already
|
|
119
|
-
# a plain bind, via `HecksagonBuilder.collector`
|
|
120
|
-
# anything this door closes over. `Hecks.current_registry`
|
|
121
|
-
# the same "whichever boot is actually in progress" indirection,
|
|
125
|
+
# RE-RESOLVED, NOT THE CLOSED-OVER `aggregate` — this door can be a
|
|
126
|
+
# STALE one, built by a boot from earlier in this same process,
|
|
127
|
+
# sitting on the `Pizzas`/`Pizza` constants only because nothing
|
|
128
|
+
# has re-installed them since. Attaching to this door's OWN
|
|
129
|
+
# `aggregate` would attach the port to a discarded aggregate from
|
|
130
|
+
# that old boot, invisible to the CURRENT one actually being
|
|
131
|
+
# loaded — silently, the exact way `method_missing` below already
|
|
132
|
+
# has to avoid it for a plain bind, via `HecksagonBuilder.collector`
|
|
133
|
+
# rather than anything this door closes over. `Hecks.current_registry`
|
|
134
|
+
# is the same "whichever boot is actually in progress" indirection,
|
|
122
135
|
# and `BindingProxy#port` already re-resolves through it the same
|
|
123
136
|
# way — this is that door's fallback twin, not a shortcut past it.
|
|
124
137
|
door.define_singleton_method(:port) do |name, &block|
|
|
125
|
-
current = Hecks.current_registry&.bluebook(domain)&.aggregate(
|
|
138
|
+
current = Hecks.current_registry&.bluebook(domain)&.aggregate(aggregate.hecks_name) or
|
|
126
139
|
raise Bluebook::DSL::Malformed, "#{fqn}.port(#{name.inspect}) called outside a boot"
|
|
127
140
|
|
|
128
141
|
domain_port = Bluebook::DSL::ConstShim.with(->(const) { const }) do
|
|
@@ -163,16 +176,17 @@ module Hecks
|
|
|
163
176
|
# const_missing directly, never `Object.const_missing`/`ConstShim::
|
|
164
177
|
# Hook` at all: real modules resolve without ever calling that.
|
|
165
178
|
#
|
|
166
|
-
# `
|
|
167
|
-
# read the SAME either way, whether or not a stale
|
|
168
|
-
# sitting on this process from an earlier boot;
|
|
169
|
-
# would make `Account::Debit`'s own meaning
|
|
170
|
-
# process history, which is the exact
|
|
171
|
-
# remove (docs/dsl-work-slices.md — "two
|
|
172
|
-
# this file's own reason for being
|
|
179
|
+
# `aggregate.hecks_name`, NOT the qualified `fqn` — a scoped
|
|
180
|
+
# reference has to read the SAME either way, whether or not a stale
|
|
181
|
+
# door happens to be sitting on this process from an earlier boot;
|
|
182
|
+
# qualifying it here would make `Account::Debit`'s own meaning
|
|
183
|
+
# depend on incidental process history, which is the exact
|
|
184
|
+
# instability S0b exists to remove (docs/dsl-work-slices.md — "two
|
|
185
|
+
# domains in one registry" is this file's own reason for being
|
|
186
|
+
# owned by that slice).
|
|
173
187
|
door.define_singleton_method(:const_missing) do |name|
|
|
174
188
|
resolver = Bluebook::DSL::ConstShim.resolver
|
|
175
|
-
return resolver.call("#{
|
|
189
|
+
return resolver.call("#{aggregate.hecks_name}::#{name}") if resolver
|
|
176
190
|
|
|
177
191
|
super(name)
|
|
178
192
|
end
|
data/lib/hecks/facade.rb
CHANGED
|
@@ -15,6 +15,10 @@ module Hecks
|
|
|
15
15
|
# what GENERATED retains is one small facade module per name, not a graph
|
|
16
16
|
# per boot.
|
|
17
17
|
module Namespace
|
|
18
|
+
# NOT frozen — a real registry, mutated below
|
|
19
|
+
# (`GENERATED[[container, name]] = value`). False positive for
|
|
20
|
+
# Style/MutableConstant.
|
|
21
|
+
# rubocop:disable-next Style/MutableConstant
|
|
18
22
|
GENERATED = {}
|
|
19
23
|
|
|
20
24
|
module_function
|
data/lib/hecks/forms/app.rb
CHANGED
|
@@ -118,7 +118,8 @@ module Hecks
|
|
|
118
118
|
return respond(405, "text/plain", "GET only") unless request.get?
|
|
119
119
|
|
|
120
120
|
if format == "html"
|
|
121
|
-
html("#{chapter.name}::#{aggregate.hecks_name}",
|
|
121
|
+
html("#{chapter.name}::#{aggregate.hecks_name}",
|
|
122
|
+
RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
|
|
122
123
|
breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
|
|
123
124
|
else
|
|
124
125
|
instances = @registry.repository(chapter.name, aggregate).all
|
|
@@ -196,7 +197,7 @@ module Hecks
|
|
|
196
197
|
end
|
|
197
198
|
|
|
198
199
|
def command_json(request, domain, aggregate, command)
|
|
199
|
-
return json(200, command.to_h) if request.get? && request.GET.
|
|
200
|
+
return json(200, command.to_h) if request.get? && request.GET.except("format").empty?
|
|
200
201
|
return respond(405, "text/plain", "GET or POST only") unless request.post?
|
|
201
202
|
|
|
202
203
|
_, envelope = submitted_command(request, aggregate, command)
|
|
@@ -213,7 +214,7 @@ module Hecks
|
|
|
213
214
|
return respond(405, "text/plain", "GET only") unless request.get?
|
|
214
215
|
|
|
215
216
|
params = request.GET
|
|
216
|
-
asked = params.
|
|
217
|
+
asked = params.except("format")
|
|
217
218
|
return query_json(domain, aggregate, query, asked) if format != "html"
|
|
218
219
|
|
|
219
220
|
query_html(domain, aggregate, query, asked)
|
|
@@ -252,7 +253,7 @@ module Hecks
|
|
|
252
253
|
def run_query(domain, aggregate, query, fields, asked)
|
|
253
254
|
args = Params.extract(fields, asked)
|
|
254
255
|
rows = @dispatcher.query("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}", **args)
|
|
255
|
-
[rows.map { |row| Record.new(row[:id], row.
|
|
256
|
+
[rows.map { |row| Record.new(row[:id], row.except(:id)) }, nil]
|
|
256
257
|
# L10 (docs/audits/2026-08-10-main-bug-audit.md) — `Params.extract`
|
|
257
258
|
# (params.rb's `extract_list`) reads a list-of-value-object line as
|
|
258
259
|
# JSON (the honest fallback for a multi-attribute list element this
|
|
@@ -275,7 +276,8 @@ module Hecks
|
|
|
275
276
|
# sites in this file (see aggregate_route's own comment).
|
|
276
277
|
return json(200, instance.state.merge(id: instance.id)) if format != "html"
|
|
277
278
|
|
|
278
|
-
html("#{domain}::#{aggregate.hecks_name} #{id}",
|
|
279
|
+
html("#{domain}::#{aggregate.hecks_name} #{id}",
|
|
280
|
+
RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
|
|
279
281
|
breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"], [id, nil]])
|
|
280
282
|
end
|
|
281
283
|
|
|
@@ -283,7 +285,9 @@ module Hecks
|
|
|
283
285
|
return json(404, { error: "NotFound", message: "no #{aggregate.hecks_name} #{id}" }) if format != "html"
|
|
284
286
|
|
|
285
287
|
respond(404, "text/html; charset=utf-8",
|
|
286
|
-
Page.render(title: "not found",
|
|
288
|
+
Page.render(title: "not found",
|
|
289
|
+
body: "<h1>Not found</h1><p>No #{Escape.html(aggregate.hecks_name)} " \
|
|
290
|
+
"#{Escape.html(id)}.</p>"))
|
|
287
291
|
end
|
|
288
292
|
|
|
289
293
|
def find_aggregate(chapter, name)
|
|
@@ -57,9 +57,9 @@ module Hecks
|
|
|
57
57
|
def self.header(domain, aggregate, command)
|
|
58
58
|
<<~HTML
|
|
59
59
|
<h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
|
|
60
|
-
#{
|
|
61
|
-
#{
|
|
62
|
-
#{
|
|
60
|
+
#{%(<span class="badge role" title="Declared on the command; this prototype dispatches with no caller bound, so the check does not run.">role: #{Escape.html(command.role)} (not enforced here)</span>) if command.role}
|
|
61
|
+
#{%(<span class="badge">creates a new #{Escape.html(aggregate.hecks_name)}</span>) if command.creates?}
|
|
62
|
+
#{%(<p class="goal">#{Escape.html(command.goal)}</p>) if command.goal}
|
|
63
63
|
#{givens_callout(command)}
|
|
64
64
|
HTML
|
|
65
65
|
end
|
|
@@ -28,7 +28,7 @@ module Hecks
|
|
|
28
28
|
def self.group(field, values, errors, reference_options, tag:, css: nil)
|
|
29
29
|
inner = field.children.map { |child| render(child, values: values, errors: errors, reference_options: reference_options) }
|
|
30
30
|
<<~HTML
|
|
31
|
-
<#{tag}#{
|
|
31
|
+
<#{tag}#{%( class="#{css}") if css}>
|
|
32
32
|
<legend>#{Escape.html(field.label)}#{required_mark(field)}</legend>
|
|
33
33
|
#{inner.join("\n")}
|
|
34
34
|
</#{tag}>
|
|
@@ -43,7 +43,7 @@ module Hecks
|
|
|
43
43
|
<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>
|
|
44
44
|
<textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}"
|
|
45
45
|
#{aria(field)} placeholder="one #{Escape.attr(item.label.downcase)} per line">#{Escape.html(current)}</textarea>
|
|
46
|
-
<span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{
|
|
46
|
+
<span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{' — each line is one JSON object' unless item.leaf?}.</span>
|
|
47
47
|
</div>
|
|
48
48
|
HTML
|
|
49
49
|
end
|
|
@@ -64,11 +64,11 @@ module Hecks
|
|
|
64
64
|
|
|
65
65
|
def self.wrap(field, body, error)
|
|
66
66
|
<<~HTML
|
|
67
|
-
<div class="field#{
|
|
68
|
-
#{
|
|
67
|
+
<div class="field#{' has-error' if error}">
|
|
68
|
+
#{%(<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>) unless field.kind == :boolean}
|
|
69
69
|
#{body}
|
|
70
|
-
#{
|
|
71
|
-
#{
|
|
70
|
+
#{%(<span class="help" id="#{dom_id(field.path)}-help">#{Escape.html(field.help)}</span>) if field.help}
|
|
71
|
+
#{%(<span class="help" role="alert">#{Escape.html(error)}</span>) if error}
|
|
72
72
|
</div>
|
|
73
73
|
HTML
|
|
74
74
|
end
|
|
@@ -114,7 +114,8 @@ module Hecks
|
|
|
114
114
|
def self.select(field, value)
|
|
115
115
|
selected = value || field.default
|
|
116
116
|
options = field.options.map do |option_value, option_label|
|
|
117
|
-
%(<option value="#{Escape.attr(option_value)}"#{option_value.to_s == selected.to_s
|
|
117
|
+
%(<option value="#{Escape.attr(option_value)}"#{' selected' if option_value.to_s == selected.to_s}>) \
|
|
118
|
+
"#{Escape.html(option_label)}</option>"
|
|
118
119
|
end
|
|
119
120
|
blank = field.optional? ? %(<option value="">—</option>) : ""
|
|
120
121
|
%(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{options.join}</select>)
|
|
@@ -124,9 +125,13 @@ module Hecks
|
|
|
124
125
|
return input(field.tap { |f| f.html_type = "text" }, value) unless options && !options.empty?
|
|
125
126
|
|
|
126
127
|
rendered = options.map do |id, label|
|
|
127
|
-
%(<option value="#{Escape.attr(id)}"#{id.to_s == value.to_s
|
|
128
|
+
%(<option value="#{Escape.attr(id)}"#{' selected' if id.to_s == value.to_s}>#{Escape.html(label)}</option>)
|
|
128
129
|
end
|
|
129
|
-
blank = field.optional?
|
|
130
|
+
blank = if field.optional?
|
|
131
|
+
%(<option value="">—</option>)
|
|
132
|
+
else
|
|
133
|
+
%(<option value="" disabled#{' selected' unless value}>choose one…</option>)
|
|
134
|
+
end
|
|
130
135
|
%(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{rendered.join}</select>)
|
|
131
136
|
end
|
|
132
137
|
|
|
@@ -24,6 +24,8 @@ module Hecks
|
|
|
24
24
|
def required? = !optional
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Turns a wire-spelled field or path segment into the plain-English text
|
|
28
|
+
# `FieldShape` embeds as a `Field`'s label or a group's legend.
|
|
27
29
|
module Humanize
|
|
28
30
|
# "given" -> "Given", "daily_limit" -> "Daily limit", "end_to_end" ->
|
|
29
31
|
# "End to end" — the label a plain reader wants, not the wire spelling.
|
|
@@ -91,8 +93,11 @@ module Hecks
|
|
|
91
93
|
def self.reference_field(attribute, common)
|
|
92
94
|
target = attribute.type.resolve
|
|
93
95
|
Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
|
|
94
|
-
help:
|
|
95
|
-
|
|
96
|
+
help: if target
|
|
97
|
+
"References an existing #{target.hecks_name} by id."
|
|
98
|
+
else
|
|
99
|
+
"References an aggregate in another domain — enter its id."
|
|
100
|
+
end)
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
# `admits:` names a closed set declared ELSEWHERE (`"Account::
|
data/lib/hecks/forms/html.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Hecks
|
|
|
69
69
|
next if value.nil? || value == false
|
|
70
70
|
next name.to_s.tr("_", "-") if value == true
|
|
71
71
|
|
|
72
|
-
%(#{name.to_s.tr(
|
|
72
|
+
%(#{name.to_s.tr('_', '-')}="#{Escape.attr(value)}")
|
|
73
73
|
end.join(" ")
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -78,7 +78,10 @@ module Hecks
|
|
|
78
78
|
rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
# `self.open(...)`, not bare `open(...)` — this Html.open (an HTML tag
|
|
82
|
+
# renderer, right above) shadows Kernel#open safely either way, but the
|
|
83
|
+
# explicit receiver also settles Security/Open's static ambiguity.
|
|
84
|
+
def self.void(name, **pairs) = self.open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
end
|
|
@@ -26,7 +26,7 @@ module Hecks
|
|
|
26
26
|
HTML
|
|
27
27
|
end
|
|
28
28
|
<<~HTML
|
|
29
|
-
<h2>#{Escape.html(name)}#{
|
|
29
|
+
<h2>#{Escape.html(name)}#{%( <span class="badge">#{Escape.html(chapter.vision)}</span>) if chapter.vision}</h2>
|
|
30
30
|
<ul class="verb-list">#{items.join}</ul>
|
|
31
31
|
HTML
|
|
32
32
|
end
|
data/lib/hecks/forms/page.rb
CHANGED
|
@@ -40,12 +40,16 @@ module Hecks
|
|
|
40
40
|
return "" if crumbs.empty?
|
|
41
41
|
|
|
42
42
|
items = crumbs.map do |label, href|
|
|
43
|
-
|
|
43
|
+
if href
|
|
44
|
+
%(<a href="#{Escape.attr(href)}">#{Escape.html(label)}</a>)
|
|
45
|
+
else
|
|
46
|
+
%(<span aria-current="page">#{Escape.html(label)}</span>)
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
%(<nav class="breadcrumbs" aria-label="Breadcrumb">#{items.join(' <span class="sep">/</span> ')}</nav>)
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
STYLE = <<~CSS
|
|
52
|
+
STYLE = <<~CSS.freeze
|
|
49
53
|
:root {
|
|
50
54
|
--bg: #fbfaf8; --surface: #ffffff; --border: #e2ddd3; --text: #24211c;
|
|
51
55
|
--muted: #6b6355; --accent: #8a5a2b; --accent-contrast: #fff8ef;
|
|
@@ -131,7 +135,7 @@ module Hecks
|
|
|
131
135
|
# Decorative only — every form here already works with JS disabled
|
|
132
136
|
# (plain `<form method=post>`/`method=get`), matching this repo's own
|
|
133
137
|
# "no ERB, nothing scripted" bar for the domain layer itself.
|
|
134
|
-
SCRIPT = <<~JS
|
|
138
|
+
SCRIPT = <<~JS.freeze
|
|
135
139
|
document.addEventListener("click", (event) => {
|
|
136
140
|
const button = event.target.closest("[data-copy]");
|
|
137
141
|
if (!button) return;
|