hecks 2.6.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -97,9 +97,10 @@ module Hecks
|
|
|
97
97
|
include Hecks::IR
|
|
98
98
|
include Behaviour::World
|
|
99
99
|
|
|
100
|
-
emits_ir(domain: :domain, realm: :realm, latest: :latest, settings: :settings
|
|
100
|
+
emits_ir(domain: :domain, realm: :realm, latest: :latest, settings: :settings,
|
|
101
|
+
default_database: :default_database, default_adapter: :default_adapter)
|
|
101
102
|
|
|
102
|
-
attr_reader :domain, :realm, :latest, :settings
|
|
103
|
+
attr_reader :domain, :realm, :latest, :settings, :default_database, :default_adapter
|
|
103
104
|
|
|
104
105
|
# @param domain [String, Symbol] the domain this world configures
|
|
105
106
|
# @param realm [String, Symbol, nil] the declared realm/version marker, or `nil`
|
|
@@ -108,11 +109,18 @@ module Hecks
|
|
|
108
109
|
# if none is declared
|
|
109
110
|
# @param settings [Hash] the declared adapter bind settings, keyed by verb and,
|
|
110
111
|
# for a qualified entry, `"verb:adapter"`
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
# @param default_database [String, nil] the connection every chapter's
|
|
113
|
+
# database-taking persistence adapter uses unless its own settings name one,
|
|
114
|
+
# or `nil` if none is declared
|
|
115
|
+
# @param default_adapter [String, nil] the persistence adapter every aggregate
|
|
116
|
+
# binds to unless its chapter's hecksagon binds it, or `nil` if none is declared
|
|
117
|
+
def initialize(domain:, realm: nil, latest: nil, settings: {}, default_database: nil, default_adapter: nil)
|
|
118
|
+
@domain = domain.to_s
|
|
119
|
+
@realm = realm&.to_s
|
|
120
|
+
@latest = latest&.to_s
|
|
121
|
+
@settings = settings
|
|
122
|
+
@default_database = default_database&.to_s
|
|
123
|
+
@default_adapter = default_adapter&.to_s
|
|
116
124
|
end
|
|
117
125
|
end
|
|
118
126
|
end
|
|
@@ -302,7 +302,7 @@ module Hecks
|
|
|
302
302
|
end
|
|
303
303
|
|
|
304
304
|
# Entity-owned categories have no top-level aggregate for the runtime
|
|
305
|
-
# to route a bare verb into
|
|
305
|
+
# to route a bare verb into — `Member`'s own "Pair" reaches
|
|
306
306
|
# the runtime as `ValueObject.Member.Pair`, and a nested one
|
|
307
307
|
# (`Dispatch`, inside `Handler`) reaches it as `ProcessManager.
|
|
308
308
|
# Handler.Dispatch.Bind` — the dotted shape `EntityInterpreter#call`
|
|
@@ -567,8 +567,8 @@ module Hecks
|
|
|
567
567
|
# Reference its type is Reference<X>, another aggregate's head
|
|
568
568
|
# Holds its type names an entity this aggregate declares
|
|
569
569
|
#
|
|
570
|
-
# Nothing is skipped
|
|
571
|
-
#
|
|
570
|
+
# Nothing is skipped. Without Reference and Holds the walk would drop
|
|
571
|
+
# both kinds and the meta-domain would silently not contain
|
|
572
572
|
# Account#customer_id or Account#ledger.
|
|
573
573
|
# Each alternate carries its own map, read from the language. Borrowing the
|
|
574
574
|
# primary's map dispatched `type:` where Reference declares `points_at:`,
|
|
@@ -108,7 +108,7 @@ module Hecks
|
|
|
108
108
|
# S17, ADR 0026 — Member/Handler/Dispatch are entities now
|
|
109
109
|
# (`entity "Member" do ... end`, nested under `ValueObject`/
|
|
110
110
|
# `ProcessManager`/`Handler`), not their own top-level
|
|
111
|
-
# aggregates — so `meta.aggregates` alone
|
|
111
|
+
# aggregates — so `meta.aggregates` alone does not find
|
|
112
112
|
# them the way it always found a standalone `aggregate
|
|
113
113
|
# "Member"`. Each still needs its own named category here:
|
|
114
114
|
# `Assembly::Contracts` keeps a bespoke entry for each
|
|
@@ -125,8 +125,8 @@ module Hecks
|
|
|
125
125
|
# itself ever declares are exactly the three this ADR
|
|
126
126
|
# names. `entity_owned: true` records why this is a
|
|
127
127
|
# category at all — the real runtime has no top-level
|
|
128
|
-
# aggregate named "Member" to dispatch a bare verb into
|
|
129
|
-
#
|
|
128
|
+
# aggregate named "Member" to dispatch a bare verb into,
|
|
129
|
+
# so the judge has to build a dotted one instead
|
|
130
130
|
# (see `Judge#verb_for`).
|
|
131
131
|
#
|
|
132
132
|
# Recurses — `Dispatch` nests inside `Handler`, which nests
|
|
@@ -158,7 +158,7 @@ module Hecks
|
|
|
158
158
|
# Every verb the language declares, spelled as the judge would
|
|
159
159
|
# dispatch it. S17, ADR 0026 — an entity-owned category (its
|
|
160
160
|
# own `entity_owned` flag) has no real top-level aggregate the
|
|
161
|
-
# runtime can route a bare verb into
|
|
161
|
+
# runtime can route a bare verb into, so it is
|
|
162
162
|
# spelled dotted here too — `Bluebook::ValueObject.Member.
|
|
163
163
|
# Declare`, `Bluebook::ProcessManager.Handler.Dispatch.Bind` —
|
|
164
164
|
# matching `Judge#verb_for`/`#dotted_prefix`'s own build
|
|
@@ -364,7 +364,7 @@ module Hecks
|
|
|
364
364
|
# same way Shapes#provenance reads it back.
|
|
365
365
|
return encode_literal(node.provenance) if field == :provenance
|
|
366
366
|
|
|
367
|
-
# `identified_by` is
|
|
367
|
+
# `identified_by` is not a field of any declaration — it is a list,
|
|
368
368
|
# filled by Identify one part at a time, so it is read through `identity_rows`
|
|
369
369
|
# like every other list rather than special-cased here. What this branch
|
|
370
370
|
# existed to protect is now structural : a path cannot come back as its head,
|
|
@@ -228,7 +228,7 @@ module Hecks
|
|
|
228
228
|
# by its own dotted `ValueObject.Member.Pair`. Its data therefore
|
|
229
229
|
# lives inline on the value object's own dispatched state — same as
|
|
230
230
|
# any other entity list — not behind a separate `DeclaredIn` query:
|
|
231
|
-
# there is no such query
|
|
231
|
+
# there is no such query, because there is no top-level
|
|
232
232
|
# "Member" aggregate left to hold one. Pairs are still an open map,
|
|
233
233
|
# which no value object can hold, so they still come back one pair
|
|
234
234
|
# at a time.
|
|
@@ -330,7 +330,7 @@ module Hecks
|
|
|
330
330
|
# Handler.Dispatch`/`...Dispatch.Bind`. Its data therefore lives
|
|
331
331
|
# inline on the process manager's own dispatched state — same as
|
|
332
332
|
# any other entity list — not behind a separate `DeclaredIn`
|
|
333
|
-
# query : there is no such query
|
|
333
|
+
# query : there is no such query, the same fix
|
|
334
334
|
# `Reconstruction#members_of` already made for Member.
|
|
335
335
|
def process_manager(row)
|
|
336
336
|
declaration("ProcessManager", row,
|
|
@@ -32,7 +32,7 @@ module Hecks
|
|
|
32
32
|
default: decode_literal(text(field[:default])),
|
|
33
33
|
# Read back the same way `list` is — both are booleans about the
|
|
34
34
|
# attribute, held as text, and dropping either would rebuild a
|
|
35
|
-
# bluebook that
|
|
35
|
+
# bluebook that says something other than what was written.
|
|
36
36
|
optional: text(field[:optional]).to_s == "true",
|
|
37
37
|
pattern: presence(text(field[:pattern])),
|
|
38
38
|
# The round trip is the only way in. The grammar registry keeps the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "digest"
|
|
2
2
|
require "fileutils"
|
|
3
|
+
require_relative "../../cache_dir"
|
|
3
4
|
|
|
4
5
|
module Hecks
|
|
5
6
|
module Bluebook
|
|
@@ -145,13 +146,10 @@ module Hecks
|
|
|
145
146
|
#
|
|
146
147
|
# Fails toward a real boot, never toward a wrong table — same
|
|
147
148
|
# loud-not-silent discipline this codebase already holds CI to
|
|
148
|
-
# (`postgres_io_relevant_changed`'s own header). A missing file, a
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
# for exactly this kind of local, disposable-but-useful-while-it-
|
|
153
|
-
# lasts artifact (`Storehouse::LOG_ROOT`'s own header names the
|
|
154
|
-
# same convention).
|
|
149
|
+
# (`postgres_io_relevant_changed`'s own header). A missing file, a corrupt
|
|
150
|
+
# Marshal blob, a permission error or an unwritable cache directory degrades
|
|
151
|
+
# to "no disk cache today", never to a crash or a served-but-wrong table.
|
|
152
|
+
# The cache lives under `Hecks::CacheDir`, never the gem's read-only directory.
|
|
155
153
|
#
|
|
156
154
|
# **Atomic write, not a lock** — `qa_sweep --all` spawns up to 4
|
|
157
155
|
# children at once, any of which could reach a cold cache
|
|
@@ -166,7 +164,10 @@ module Hecks
|
|
|
166
164
|
# change, the same shape `Storehouse::BOOT_ROOT`'s own
|
|
167
165
|
# `HECKS_STOREHOUSE_ROOT` override uses, for the day this needs to
|
|
168
166
|
# be ruled out while debugging something else entirely.
|
|
169
|
-
|
|
167
|
+
#
|
|
168
|
+
# @return [String] the directory cache entries are written to:
|
|
169
|
+
# `hecks_syntax_boot_cache` under `Hecks::CacheDir`
|
|
170
|
+
def cache_dir = Hecks::CacheDir.path("hecks_syntax_boot_cache")
|
|
170
171
|
|
|
171
172
|
# Reports whether the cross-process disk cache is turned on.
|
|
172
173
|
#
|
|
@@ -203,7 +204,7 @@ module Hecks
|
|
|
203
204
|
return unless disk_cache_enabled?
|
|
204
205
|
|
|
205
206
|
path = disk_cache_path(chapters)
|
|
206
|
-
FileUtils.mkdir_p(
|
|
207
|
+
FileUtils.mkdir_p(cache_dir)
|
|
207
208
|
tmp_path = "#{path}.#{Process.pid}.tmp"
|
|
208
209
|
File.binwrite(tmp_path, Marshal.dump(result))
|
|
209
210
|
File.rename(tmp_path, path)
|
|
@@ -217,7 +218,7 @@ module Hecks
|
|
|
217
218
|
# chapter]` pairs to key the cache entry on
|
|
218
219
|
# @return [String] the disk cache file path for `chapters`
|
|
219
220
|
def disk_cache_path(chapters)
|
|
220
|
-
File.join(
|
|
221
|
+
File.join(cache_dir, "#{disk_cache_key(chapters)}.marshal")
|
|
221
222
|
end
|
|
222
223
|
|
|
223
224
|
# Builds the cache key for `chapters`.
|
|
@@ -40,7 +40,9 @@ module Hecks
|
|
|
40
40
|
def judge!
|
|
41
41
|
domain = @world.domain
|
|
42
42
|
send_to("World::World.Declare", domain, domain: v(domain),
|
|
43
|
-
realm: v(@world.realm), latest: v(@world.latest)
|
|
43
|
+
realm: v(@world.realm), latest: v(@world.latest),
|
|
44
|
+
default_database: v(@world.default_database),
|
|
45
|
+
default_adapter: v(@world.default_adapter))
|
|
44
46
|
|
|
45
47
|
Hash(@world.settings).each do |verb, values|
|
|
46
48
|
# the DSL records each binding twice — once by verb, once by
|
|
@@ -541,7 +541,7 @@ module Hecks
|
|
|
541
541
|
# every Paging-attached word (limit/offset/cursor/nulls) because
|
|
542
542
|
# something called it inside this exact window.
|
|
543
543
|
#
|
|
544
|
-
# SyntaxBoot.call
|
|
544
|
+
# SyntaxBoot.call does not use this. Gating its cache on "the
|
|
545
545
|
# whole registry is finished" meant nothing was cached for the
|
|
546
546
|
# entire window, and the window is not narrow — every word routed
|
|
547
547
|
# through `word_gate_dispatch` while the language judged itself
|
|
@@ -15,60 +15,29 @@ module Hecks
|
|
|
15
15
|
#
|
|
16
16
|
# ## The rules
|
|
17
17
|
#
|
|
18
|
-
# - `:client_group_by_row_drop` — a `group_by` whose fields do not include
|
|
19
|
-
# the grouped aggregate's whole identity. Rows sharing a key path are
|
|
20
|
-
# collapsed to the first, on every adapter (docs/decisions/0061).
|
|
21
18
|
# - `:client_native_read_model` — a read model the SQLite projection
|
|
22
19
|
# answers natively, where every other path answers it in process
|
|
23
20
|
# (docs/1.0-readiness.md, "Known gaps at 1.0", item 2).
|
|
24
|
-
# - `:client_dotted_compute_source` — an era translation `compute` whose
|
|
25
|
-
# source is a dotted path, which never fires
|
|
26
|
-
# (`Translation::RuleCompiler.compile_compute`).
|
|
27
21
|
module ClientProfile
|
|
28
22
|
# Adapters whose Ruby class implements `query_read_model`, which is what
|
|
29
23
|
# makes `ReadModelInterpreter#project` take the native path. The spec
|
|
30
24
|
# checks this list against the adapter classes themselves.
|
|
31
25
|
NATIVE_READ_MODEL_ADAPTERS = %w[SqliteProjection].freeze
|
|
32
26
|
|
|
33
|
-
GROUP_BY_TRACKER = "docs/decisions/0061-query-dsl-aggregation-count-sum-group-by.md".freeze
|
|
34
27
|
NATIVE_READ_MODEL_TRACKER = "docs/1.0-readiness.md (Known gaps at 1.0, item 2)".freeze
|
|
35
|
-
DOTTED_COMPUTE_TRACKER = "Translation::RuleCompiler.compile_compute, and the pending example " \
|
|
36
|
-
"\"applies the SQL of a compute whose source is a dotted member\" in " \
|
|
37
|
-
"spec/adapters/driven/postgres_era/migration_data_safety_spec.rb".freeze
|
|
38
28
|
|
|
39
29
|
module_function
|
|
40
30
|
|
|
41
|
-
# Runs every client-profile rule over one chapter
|
|
31
|
+
# Runs every client-profile rule over one chapter.
|
|
42
32
|
#
|
|
43
33
|
# @param bluebook [Bluebook::Chapter] the assembled chapter to check
|
|
44
34
|
# @param hecksagon [Bluebook::Hecksagon, nil] the chapter's sibling wiring file; without
|
|
45
35
|
# one no aggregate is projected, so the native-read-model rule has nothing to find
|
|
46
|
-
# @param translations [Array<Bluebook::Translation>] the data translations declared for
|
|
47
|
-
# this chapter's domain
|
|
48
36
|
# @return [Array<ModelCheck::Finding>] one error-severity finding per construct refused
|
|
49
|
-
def call(bluebook, hecksagon: nil
|
|
37
|
+
def call(bluebook, hecksagon: nil)
|
|
50
38
|
bluebook.read_models.flat_map do |model|
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Refuses a `group_by` that cannot prove each of its key paths names one row.
|
|
56
|
-
#
|
|
57
|
-
# The interpreter's `nest` keeps `stripped.first` per key path, whichever adapter
|
|
58
|
-
# supplied the rows, because a `group_by` model never reaches the native path. Grouping
|
|
59
|
-
# by the aggregate's whole identity is the one shape that cannot collide.
|
|
60
|
-
#
|
|
61
|
-
# @param bluebook [Bluebook::Chapter] the chapter the read model is declared in
|
|
62
|
-
# @param model [Bluebook::ReadModel] the read model to inspect
|
|
63
|
-
# @return [Array<ModelCheck::Finding>] one finding, or `[]` when `model` declares no
|
|
64
|
-
# `group_by` or groups by its aggregate's whole identity
|
|
65
|
-
def group_by_findings(bluebook, model)
|
|
66
|
-
return [] if model.group_by.empty? || groups_by_identity?(bluebook, model)
|
|
67
|
-
|
|
68
|
-
[finding(:client_group_by_row_drop, model.name,
|
|
69
|
-
"group_by #{model.group_by_fields.map(&:inspect).join(', ')} does not cover the grouped " \
|
|
70
|
-
"aggregate's whole identity, so rows sharing a key path are silently reduced to the " \
|
|
71
|
-
"first. Tracked in #{GROUP_BY_TRACKER}.")]
|
|
39
|
+
native_read_model_findings(model, hecksagon)
|
|
40
|
+
end
|
|
72
41
|
end
|
|
73
42
|
|
|
74
43
|
# Refuses a read model that the SQLite projection would answer natively.
|
|
@@ -95,26 +64,6 @@ module Hecks
|
|
|
95
64
|
"is not, and nothing checks the two agree. Tracked in #{NATIVE_READ_MODEL_TRACKER}.")]
|
|
96
65
|
end
|
|
97
66
|
|
|
98
|
-
# Refuses a translation `compute` whose source names a member of a value object.
|
|
99
|
-
#
|
|
100
|
-
# `compile_compute` guards the compute with `__s ? '<from>'`, a top-level key test, so a
|
|
101
|
-
# dotted source never matches, the mint succeeds and the record keeps its old value.
|
|
102
|
-
#
|
|
103
|
-
# @param translations [Array<Bluebook::Translation>] the translations to inspect
|
|
104
|
-
# @return [Array<ModelCheck::Finding>] one finding per dotted-source compute
|
|
105
|
-
def dotted_compute_findings(translations)
|
|
106
|
-
translations.flat_map do |translation|
|
|
107
|
-
translation.aggregates.flat_map do |aggregate|
|
|
108
|
-
aggregate.computes.select { |compute| compute.from.to_s.include?(".") }.map do |compute|
|
|
109
|
-
finding(:client_dotted_compute_source, "#{translation.domain}::#{aggregate.name}",
|
|
110
|
-
"compute #{compute.from.to_s.inspect} has a dotted source, which the compiled SQL " \
|
|
111
|
-
"never matches, so the mint succeeds and leaves the old value in place. " \
|
|
112
|
-
"Tracked in #{DOTTED_COMPUTE_TRACKER}.")
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
67
|
# Reports whether a model's shape sends it down the native path in `#project`.
|
|
119
68
|
#
|
|
120
69
|
# @param model [Bluebook::ReadModel] the read model to inspect
|
|
@@ -123,21 +72,6 @@ module Hecks
|
|
|
123
72
|
!model.reference_target.nil? && model.group_by.empty? && !model.count? && !model.median_field
|
|
124
73
|
end
|
|
125
74
|
|
|
126
|
-
# Reports whether a `group_by` names every identity path of the aggregate it groups.
|
|
127
|
-
#
|
|
128
|
-
# @param bluebook [Bluebook::Chapter] the chapter the read model is declared in
|
|
129
|
-
# @param model [Bluebook::ReadModel] a read model that declares a `group_by`
|
|
130
|
-
# @return [Boolean] false when the many-side aggregate is not a top-level aggregate or
|
|
131
|
-
# declares no identity, since uniqueness cannot then be shown
|
|
132
|
-
def groups_by_identity?(bluebook, model)
|
|
133
|
-
target = model.aggregate_heads.find { |head| head[:many] }
|
|
134
|
-
aggregate = target && bluebook.aggregate(target[:aggregate])
|
|
135
|
-
return false unless aggregate
|
|
136
|
-
|
|
137
|
-
identity = Array(aggregate.identity_heads).map(&:to_sym)
|
|
138
|
-
!identity.empty? && (identity - model.group_by_fields.map(&:to_sym)).empty?
|
|
139
|
-
end
|
|
140
|
-
|
|
141
75
|
# Builds one error-severity finding of this profile.
|
|
142
76
|
#
|
|
143
77
|
# @param kind [Symbol] the finding's kind, one of the `:client_*` rules above
|
|
@@ -25,7 +25,7 @@ module Hecks
|
|
|
25
25
|
# **A finding shipped, not silenced** — the coverage-gate idiom, empty
|
|
26
26
|
# allowlists enforced both directions (spec/model_check_spec.rb holds
|
|
27
27
|
# this exact table: an error the checker reports and this does not
|
|
28
|
-
# name is a regression, an entry the checker
|
|
28
|
+
# name is a regression, an entry the checker does not report is
|
|
29
29
|
# stale and must be deleted). bin/model_check reads this same
|
|
30
30
|
# constant, so the tool and the spec can never drift apart.
|
|
31
31
|
#
|
|
@@ -44,9 +44,9 @@ module Hecks
|
|
|
44
44
|
# was a `state "x"` line never named by any handler's own from:/to:, a
|
|
45
45
|
# pure declaration-drift artifact. States are derived from the
|
|
46
46
|
# transitions that name them now (ProcessManagerBuilder#derived_
|
|
47
|
-
# states), so a state nothing ever transitions into or out of
|
|
48
|
-
#
|
|
49
|
-
# cannot occur
|
|
47
|
+
# states), so a state nothing ever transitions into or out of does
|
|
48
|
+
# not exist to be unreachable — the finding that entry allowlisted
|
|
49
|
+
# cannot occur, by construction.
|
|
50
50
|
#
|
|
51
51
|
# "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
|
|
52
52
|
# to banking. `across "Notifications"` names a domain that does not
|
|
@@ -145,15 +145,11 @@ module Hecks
|
|
|
145
145
|
# findings for constructs known to answer wrongly without refusing;
|
|
146
146
|
# `nil` (the default) adds nothing and leaves every other finding as
|
|
147
147
|
# it was
|
|
148
|
-
# @param translations [Array<Bluebook::Translation>] the data
|
|
149
|
-
# translations declared for this chapter's domain, read only by the
|
|
150
|
-
# `:client` profile — they live on the registry, not the chapter, so
|
|
151
|
-
# the caller supplies them the way it supplies `hecksagon`
|
|
152
148
|
# @return [Array<Finding>] every finding this bluebook triggers,
|
|
153
149
|
# across its lifecycles, sagas, policies, and Rust-reserved names
|
|
154
150
|
# @raise [ArgumentError] if `profile` is neither `nil` nor `:client`
|
|
155
151
|
def call(bluebook, hecksagon: nil, known_domains: nil, global_emitted_events: nil, rust_target: false, strict: false,
|
|
156
|
-
profile: nil
|
|
152
|
+
profile: nil)
|
|
157
153
|
unless profile.nil? || PROFILES.include?(profile)
|
|
158
154
|
raise ArgumentError, "unknown profile #{profile.inspect} (known: #{PROFILES.inspect})"
|
|
159
155
|
end
|
|
@@ -170,7 +166,7 @@ module Hecks
|
|
|
170
166
|
findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
|
|
171
167
|
aggregate_names: bluebook.aggregates.map(&:hecks_name),
|
|
172
168
|
rust_target: rust_target, strict: strict))
|
|
173
|
-
findings.concat(ClientProfile.call(bluebook, hecksagon: hecksagon
|
|
169
|
+
findings.concat(ClientProfile.call(bluebook, hecksagon: hecksagon)) if profile == :client
|
|
174
170
|
findings
|
|
175
171
|
end
|
|
176
172
|
|
|
@@ -659,6 +655,9 @@ module Hecks
|
|
|
659
655
|
# sibling wiring file, if the caller loaded one
|
|
660
656
|
# @param known_domains [Set<String>, nil] every bluebook/hecksagon
|
|
661
657
|
# name the caller has booted anywhere in this corpus scan
|
|
658
|
+
# @param global_emitted_events [Set<String>, nil] every event name emitted anywhere in the
|
|
659
|
+
# corpus, consulted only as a fallback for a `translates` reaction; nil when the caller
|
|
660
|
+
# has no cross-domain set to offer
|
|
662
661
|
# @return [Array<Finding>] every `deaf_policy`/`unknown_trigger`
|
|
663
662
|
# finding this policy triggers, or `cross_domain_policy_findings`'s
|
|
664
663
|
# own return for a cross-domain policy
|
|
@@ -680,12 +679,12 @@ module Hecks
|
|
|
680
679
|
# (`across "Compliance"`), which exits this method one line above
|
|
681
680
|
# before the mismatch is ever reached.
|
|
682
681
|
# `global_emitted_events` — a `translates` (hecksagon-level)
|
|
683
|
-
# reaction is
|
|
682
|
+
# reaction is built as the exact same same-domain-target `Policy`
|
|
684
683
|
# an ordinary `policy` block is (no `target_domain`, so it never
|
|
685
684
|
# reaches `cross_domain_policy_findings` above), but its whole
|
|
686
|
-
# point is to react to a
|
|
685
|
+
# point is to react to a foreign domain's own event — this
|
|
687
686
|
# domain's own `emitted_events(bluebook)` was never going to
|
|
688
|
-
# contain it. Checked only as a
|
|
687
|
+
# contain it. Checked only as a fallback, after the local check
|
|
689
688
|
# already failed, so a real same-domain typo still gets flagged
|
|
690
689
|
# exactly as before whenever the caller has no cross-domain set
|
|
691
690
|
# to offer (nil — every existing call site, unchanged) or the
|
|
@@ -793,7 +792,7 @@ module Hecks
|
|
|
793
792
|
|
|
794
793
|
# **Typo detection, deliberately weaker** — `known_domains` can only
|
|
795
794
|
# ever be a monorepo-scoped heuristic: a real external hecks
|
|
796
|
-
# consumer's own domain (this repo's own embryonaut
|
|
795
|
+
# consumer's own domain (this repo's own embryonaut client-
|
|
797
796
|
# shaped case) lives in a genuinely separate repository this
|
|
798
797
|
# corpus scan can never see, so a target this check cannot find
|
|
799
798
|
# is "unknown to THIS corpus," never proof of a typo. A target
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "etc"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
# The one place the runtime's own scratch files live, outside the gem.
|
|
7
|
+
#
|
|
8
|
+
# The syntax-boot cache and the Storehouse audit log are written on a
|
|
9
|
+
# normal run, so they cannot sit under the gem's own directory: an
|
|
10
|
+
# installed gem is read-only, and a checkout's `tmp/` is not a place a
|
|
11
|
+
# library gets to assume. Both ask this module for a subdirectory instead.
|
|
12
|
+
#
|
|
13
|
+
# ## Where the root is
|
|
14
|
+
#
|
|
15
|
+
# The first of these that can be made a private directory wins:
|
|
16
|
+
#
|
|
17
|
+
# 1. `$XDG_CACHE_HOME/hecks`, when `XDG_CACHE_HOME` is set to an absolute path
|
|
18
|
+
# 2. `~/.cache/hecks`
|
|
19
|
+
# 3. `<system temp dir>/hecks-<uid>`
|
|
20
|
+
#
|
|
21
|
+
# ## Private, or not used
|
|
22
|
+
#
|
|
23
|
+
# The syntax-boot cache is read back with `Marshal.load`, so a directory
|
|
24
|
+
# another account can write to is a way to run code as this one. A
|
|
25
|
+
# candidate is accepted only if it is owned by the current user and
|
|
26
|
+
# writable by nobody else; the shared temp directory in particular is
|
|
27
|
+
# where a predictable name can be planted by someone else. When no candidate
|
|
28
|
+
# qualifies, a fresh per-process directory stands in: the cache still
|
|
29
|
+
# works within the process and is never shared with a stranger.
|
|
30
|
+
module CacheDir
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# The directory a named piece of scratch state lives in.
|
|
34
|
+
#
|
|
35
|
+
# @param name [String] the subdirectory, such as `"storehouse"`
|
|
36
|
+
# @return [String] the absolute path of `name` under the cache root; the
|
|
37
|
+
# directory itself is created by whoever writes to it
|
|
38
|
+
def path(name)
|
|
39
|
+
File.join(root, name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The cache root, resolved once per process.
|
|
43
|
+
#
|
|
44
|
+
# @return [String] the absolute path of the directory every `path` lives under
|
|
45
|
+
def root
|
|
46
|
+
@root ||= resolve
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Forgets the resolved root so the next call resolves again.
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
def reset!
|
|
53
|
+
@root = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Picks the cache root from the environment.
|
|
57
|
+
#
|
|
58
|
+
# @param env [Hash{String => String}] the environment to read
|
|
59
|
+
# `XDG_CACHE_HOME` and `HOME` from
|
|
60
|
+
# @param tmpdir [String] the system temp directory
|
|
61
|
+
# @param uid [Integer] the current user id, used in the temp fallback's name
|
|
62
|
+
# @return [String] the absolute path of the first candidate that is a
|
|
63
|
+
# private directory owned by this user, else a per-process directory
|
|
64
|
+
def resolve(env: ENV, tmpdir: Dir.tmpdir, uid: Process.uid)
|
|
65
|
+
candidates(env, tmpdir, uid).each do |candidate|
|
|
66
|
+
return candidate if private_dir?(candidate)
|
|
67
|
+
end
|
|
68
|
+
Dir.mktmpdir("hecks-", tmpdir)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
File.join(tmpdir, "hecks-#{uid}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The roots to try, most preferred first.
|
|
74
|
+
#
|
|
75
|
+
# @param env [Hash{String => String}] the environment
|
|
76
|
+
# @param tmpdir [String] the system temp directory
|
|
77
|
+
# @param uid [Integer] the current user id
|
|
78
|
+
# @return [Array<String>] absolute candidate paths
|
|
79
|
+
def candidates(env, tmpdir, uid)
|
|
80
|
+
xdg = env["XDG_CACHE_HOME"].to_s
|
|
81
|
+
home = env["HOME"].to_s
|
|
82
|
+
list = []
|
|
83
|
+
list << File.join(xdg, "hecks") if File.absolute_path?(xdg)
|
|
84
|
+
list << File.join(home, ".cache", "hecks") if File.absolute_path?(home)
|
|
85
|
+
list << File.join(tmpdir, "hecks-#{uid}")
|
|
86
|
+
end
|
|
87
|
+
private_class_method :candidates
|
|
88
|
+
|
|
89
|
+
# Creates `dir` if needed and says whether it is private to this user.
|
|
90
|
+
#
|
|
91
|
+
# @param dir [String] the candidate directory
|
|
92
|
+
# @return [Boolean] true when `dir` is a directory owned by this user that
|
|
93
|
+
# no other user can write to; false on any failure
|
|
94
|
+
def private_dir?(dir)
|
|
95
|
+
FileUtils.mkdir_p(dir, mode: 0o700)
|
|
96
|
+
stat = File.stat(dir)
|
|
97
|
+
stat.directory? && stat.owned? && stat.mode.nobits?(0o022) && File.writable?(dir)
|
|
98
|
+
rescue StandardError
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
private_class_method :private_dir?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require_relative "../../hecks"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module CLI
|
|
5
|
+
# The command behind `bin/docs`, `bin/narrate`, `hecks docs` and `hecks narrate`:
|
|
6
|
+
# one domain's document, projected from its own bluebook, to stdout.
|
|
7
|
+
#
|
|
8
|
+
# `:docs` answers the usage document a console's `<Domain>.docs` prints; `:narrate`
|
|
9
|
+
# answers the same chapter read back in English, for the subject-matter expert
|
|
10
|
+
# (`Projector::NarrateProjector`). Nothing is written for the caller: redirect it,
|
|
11
|
+
# so the caller decides where a generated file lands and git decides whether it
|
|
12
|
+
# drifted.
|
|
13
|
+
#
|
|
14
|
+
# ## Every failure is a sentence
|
|
15
|
+
#
|
|
16
|
+
# A misspelled aggregate refuses by name rather than printing nothing and exiting
|
|
17
|
+
# 0. A caller cannot tell an empty document from an empty domain, so this refuses
|
|
18
|
+
# instead and names what is actually there.
|
|
19
|
+
#
|
|
20
|
+
# ## Standing in a domain is enough
|
|
21
|
+
#
|
|
22
|
+
# `Adapters::Folder#domain_root` walks up for the nearest `.hecksagon`, which makes
|
|
23
|
+
# the first argument ambiguous. The rule: a directory is a domain, anything else
|
|
24
|
+
# is an aggregate name, and the two-argument form says which is which.
|
|
25
|
+
module Document
|
|
26
|
+
# Build output and other trees under a root that hold no domain a caller means.
|
|
27
|
+
# Matched against the path relative to the root, because a substring test
|
|
28
|
+
# against an absolute path tests wherever somebody happened to clone.
|
|
29
|
+
IGNORED = %r{\A(rust|deploy|tmp|coverage)/}
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Prints `projection` for the domain and optional aggregate `argv` names.
|
|
34
|
+
#
|
|
35
|
+
# @param argv [Array<String>] an optional domain directory, then an optional
|
|
36
|
+
# aggregate name
|
|
37
|
+
# @param projection [Symbol] `:docs` or `:narrate`
|
|
38
|
+
# @param program [String] the name the usage message calls this command by
|
|
39
|
+
# @param root [String] the directory whose domains are listed when no domain
|
|
40
|
+
# is found
|
|
41
|
+
# @return [void]
|
|
42
|
+
# @raise [SystemExit] when no domain is found, it cannot boot, it loads no
|
|
43
|
+
# bluebook, or the aggregate does not exist
|
|
44
|
+
def call(argv, projection:, program:, root:)
|
|
45
|
+
here = Adapters::Folder.new.domain_root
|
|
46
|
+
path, aggregate = target(argv, here)
|
|
47
|
+
refuse_without_domain(program, root) unless path
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
runtime = Hecks.boot(path, install_facade: false)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
abort "cannot read #{path}: #{e.message.lines.first.strip}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The domain's own chapter, not a framework member it attached: insertion
|
|
56
|
+
# order, the same distinction `Runtime::Loader.dispatcher_for` draws.
|
|
57
|
+
bluebook = runtime.registry.bluebooks.values.first
|
|
58
|
+
abort "#{path} loaded no bluebook" unless bluebook
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
puts Projector.call(projection, bluebook: bluebook, options: aggregate ? { aggregate: aggregate } : {})
|
|
62
|
+
rescue Runtime::NotFound => e
|
|
63
|
+
abort e.message
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Lists every directory under `root` that `Hecks.boot` would accept.
|
|
68
|
+
#
|
|
69
|
+
# Found by their wiring, not their chapters: a `.hecksagon` is what says "this
|
|
70
|
+
# is a domain, and here is how its aggregates are stored", while most
|
|
71
|
+
# `.bluebook` files (era translations, the language's own grammar,
|
|
72
|
+
# `spec/fixtures`) are not domains anybody boots.
|
|
73
|
+
#
|
|
74
|
+
# @param root [String] the directory to search under
|
|
75
|
+
# @return [Array<String>] domain directory paths relative to `root`,
|
|
76
|
+
# deduplicated and sorted
|
|
77
|
+
def domains(root)
|
|
78
|
+
Dir.glob(File.join(root, "**/*.hecksagon"))
|
|
79
|
+
.map { |path| File.basename(File.dirname(path)) == "bluebook" ? File.dirname(path, 2) : File.dirname(path) }
|
|
80
|
+
.map { |path| path.delete_prefix("#{root}/") }
|
|
81
|
+
.grep_v(IGNORED)
|
|
82
|
+
.uniq.sort
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @api private
|
|
86
|
+
def target(argv, here)
|
|
87
|
+
if argv.empty? then [here, nil]
|
|
88
|
+
elsif argv.length > 1 then [argv[0], argv[1]]
|
|
89
|
+
elsif File.directory?(argv[0]) then [argv[0], nil]
|
|
90
|
+
else [here, argv[0]]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api private
|
|
95
|
+
def refuse_without_domain(program, root)
|
|
96
|
+
warn "usage: #{program} [domain-path] [aggregate]"
|
|
97
|
+
warn ""
|
|
98
|
+
warn "No bluebook here — #{Dir.pwd} is not inside a domain. Name one:"
|
|
99
|
+
domains(root).each { |candidate| warn " #{candidate}" }
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/hecks/cli/ir.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative "../../hecks"
|
|
2
|
+
# ADR 0033 — a domain wired to PostgresEra needs this plugin loaded
|
|
3
|
+
# explicitly; the era/lineage subsystem does not load with core.
|
|
4
|
+
require_relative "../ports/persistence/plugins/era"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module CLI
|
|
8
|
+
# The command behind `bin/ir` and `hecks ir`: a booted domain's IR as JSON, the
|
|
9
|
+
# same `to_h` the golden specs pin and `StorageShape` hashes into an era, for
|
|
10
|
+
# reading rather than asserting on.
|
|
11
|
+
module Ir
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Prints the IR `argv` asks for.
|
|
15
|
+
#
|
|
16
|
+
# `--meta` prints the language's own IR, the chapters under
|
|
17
|
+
# `lib/hecks/language/bluebook/` merged into one. It is reached through
|
|
18
|
+
# `Bluebook::MetaValidator.grammar_registry`, the real boot sequence, so it
|
|
19
|
+
# cannot drift from what `MetaValidator` itself loads.
|
|
20
|
+
#
|
|
21
|
+
# @param argv [Array<String>] a domain path and `--translations`, or `--meta`
|
|
22
|
+
# @param program [String] the name the usage message calls this command by
|
|
23
|
+
# @param default_domain [String, nil] the domain booted when `argv` names none;
|
|
24
|
+
# nil makes a domain argument required
|
|
25
|
+
# @return [void]
|
|
26
|
+
# @raise [SystemExit] when no domain is named and there is no default
|
|
27
|
+
def call(argv, program:, default_domain: nil)
|
|
28
|
+
argv = argv.dup
|
|
29
|
+
if argv.delete("--meta")
|
|
30
|
+
require_relative "../bluebook/meta_validator"
|
|
31
|
+
puts Projector::Exporter.json(Bluebook::MetaValidator.grammar_registry)
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
translations_only = argv.delete("--translations")
|
|
36
|
+
domain = argv.shift || default_domain or abort "usage: #{program} <domain> [--translations] | #{program} --meta"
|
|
37
|
+
runtime = Hecks.boot(domain)
|
|
38
|
+
|
|
39
|
+
if translations_only
|
|
40
|
+
puts Projector::Exporter.translations_json(runtime.registry)
|
|
41
|
+
else
|
|
42
|
+
puts Projector::Exporter.json(runtime.registry)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|