hecks 1.0.2 → 1.1.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 +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- 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 +17 -1
- 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 +2 -1
- 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/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 +19 -472
- 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 +11 -3
- 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 +40 -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 +23 -13
- 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/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/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -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/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- 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 +29 -2
- 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 +26 -3
- 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 +207 -48
- 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 +14 -2
|
@@ -2,6 +2,12 @@ module Hecks
|
|
|
2
2
|
module Adapters
|
|
3
3
|
class PostgresEra
|
|
4
4
|
class Lineage
|
|
5
|
+
# Owner-only schema bootstrap: creates the lineage tables
|
|
6
|
+
# (`hecks_eras`, `hecks_era_texts`, `hecks_approvals`, the
|
|
7
|
+
# partitioned journal), sets up its RLS fence, bridges a renamed
|
|
8
|
+
# domain's history (`rename_domain!`), and attaches each era's
|
|
9
|
+
# journal partition (`ensure_partition!`) — the build-then-ATTACH
|
|
10
|
+
# dance that keeps a mint from stopping every writer.
|
|
5
11
|
module Provisioning
|
|
6
12
|
# Provisioning is the OWNER's job, and a deployment's app role is
|
|
7
13
|
# deliberately not the owner — it may append and read, and it
|
|
@@ -13,6 +19,15 @@ module Hecks
|
|
|
13
19
|
# grant_era! exists to constrain could never finish booting
|
|
14
20
|
# ("must be owner of table hecks_eras"). A fence nothing can
|
|
15
21
|
# reach is not a fence.
|
|
22
|
+
# One bootstrap sequence of DDL, order-dependent throughout —
|
|
23
|
+
# every guarded ALTER (RLS, REVOKE) is guarded and placed
|
|
24
|
+
# exactly where it is for measured, documented reasons (see the
|
|
25
|
+
# inline comments on each: catalog-lock avoidance, RLS timing,
|
|
26
|
+
# FORCE semantics). Splitting this into smaller methods would
|
|
27
|
+
# only hide that single ordered sequence behind several call
|
|
28
|
+
# sites, with nothing gained — each statement already reads as
|
|
29
|
+
# one step, and the length here is DDL, not branching logic.
|
|
30
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
16
31
|
def ensure_base!
|
|
17
32
|
rename_domain! if @formerly_known_as
|
|
18
33
|
return unless provisioner?
|
|
@@ -183,6 +198,16 @@ module Hecks
|
|
|
183
198
|
# Anything else (no rows under either name) falls through
|
|
184
199
|
# harmlessly — `formerly_known_as` pointing at a name with no
|
|
185
200
|
# held history is not an error, just inert.
|
|
201
|
+
# One transactional migration, whose own comment above spells
|
|
202
|
+
# out a three-way precheck and a FIXED lock-acquisition order
|
|
203
|
+
# (old before new, eras before ordinal) chosen specifically to
|
|
204
|
+
# avoid a deadlock against a concurrent rename/mint/write.
|
|
205
|
+
# Splitting this would separate the precheck from the locking
|
|
206
|
+
# from the renames from the commit/rollback handling — each a
|
|
207
|
+
# piece of ONE transaction that must stay in this exact order
|
|
208
|
+
# or the deadlock-avoidance guarantee stops holding.
|
|
209
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
210
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
186
211
|
def rename_domain!
|
|
187
212
|
return unless @db.exec_params("SELECT to_regclass($1)", ["hecks_eras"])[0]["to_regclass"]
|
|
188
213
|
return if @db.exec_params(
|
|
@@ -236,17 +261,30 @@ module Hecks
|
|
|
236
261
|
# ensure_base! at all — only lazily, on first reattest! — so a
|
|
237
262
|
# domain that never needed one must not be forced through an
|
|
238
263
|
# UPDATE against a table that doesn't exist.
|
|
239
|
-
|
|
264
|
+
if @db.exec_params(
|
|
265
|
+
"SELECT to_regclass($1)", ["hecks_attestations"]
|
|
266
|
+
)[0]["to_regclass"]
|
|
267
|
+
@db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2",
|
|
268
|
+
[@domain, @formerly_known_as])
|
|
269
|
+
end
|
|
240
270
|
|
|
241
271
|
@db.exec("COMMIT")
|
|
242
272
|
rescue PG::LockNotAvailable
|
|
243
|
-
|
|
273
|
+
begin
|
|
274
|
+
@db.exec("ROLLBACK")
|
|
275
|
+
rescue StandardError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
244
278
|
raise Runtime::WiringError,
|
|
245
279
|
"cannot rename #{@formerly_known_as} to #{@domain}: another rename, mint, or write holds " \
|
|
246
280
|
"one of the domain locks — waited 10s; try again shortly"
|
|
247
|
-
rescue PG::Error =>
|
|
248
|
-
|
|
249
|
-
|
|
281
|
+
rescue PG::Error => e
|
|
282
|
+
begin
|
|
283
|
+
@db.exec("ROLLBACK")
|
|
284
|
+
rescue StandardError
|
|
285
|
+
nil
|
|
286
|
+
end
|
|
287
|
+
raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{e.message.strip}"
|
|
250
288
|
end
|
|
251
289
|
|
|
252
290
|
# Nothing provisioned yet — build it. Provisioned and owned —
|
|
@@ -4,6 +4,10 @@ module Hecks
|
|
|
4
4
|
module Adapters
|
|
5
5
|
class PostgresEra
|
|
6
6
|
class Lineage
|
|
7
|
+
# The one deliberate, human-driven command that closes a fork:
|
|
8
|
+
# `merge_tail!` re-enters an old era's post-cut writes into the
|
|
9
|
+
# current era under named winners, and `diverged_count` observes how
|
|
10
|
+
# far the two worlds have drifted at any time before that.
|
|
7
11
|
module TailMerge
|
|
8
12
|
# ── fork observability ─────────────────────────────────────────
|
|
9
13
|
|
|
@@ -29,6 +33,14 @@ module Hecks
|
|
|
29
33
|
# winner; resolution itself is append-only (the winner's state
|
|
30
34
|
# re-enters as the newest row and wins structurally — originals
|
|
31
35
|
# stay immutable).
|
|
36
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
37
|
+
# One Postgres transaction (BEGIN…COMMIT) with a manual ROLLBACK
|
|
38
|
+
# at every early refusal, and a snapshot-before-mutation
|
|
39
|
+
# invariant (`new_states` captured before the head rebuild lets
|
|
40
|
+
# the tail interleave). Splitting the steps into separate
|
|
41
|
+
# methods would force each to independently know how to roll
|
|
42
|
+
# back the same shared transaction, and would separate the
|
|
43
|
+
# snapshot from the mutation it must precede.
|
|
32
44
|
def merge_tail!(aggregates:, edges:, winners: {}, audit: nil)
|
|
33
45
|
@db.exec("BEGIN")
|
|
34
46
|
@db.exec("SET LOCAL lock_timeout = '10s'")
|
|
@@ -129,14 +141,23 @@ module Hecks
|
|
|
129
141
|
@db.exec("COMMIT")
|
|
130
142
|
true
|
|
131
143
|
rescue PG::LockNotAvailable
|
|
132
|
-
|
|
144
|
+
begin
|
|
145
|
+
@db.exec("ROLLBACK")
|
|
146
|
+
rescue StandardError
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
133
149
|
raise Runtime::WiringError,
|
|
134
150
|
"cannot merge the tail of #{@domain}: another mint or merge holds the domain lock — " \
|
|
135
151
|
"waited 10s; try again shortly"
|
|
136
|
-
rescue PG::Error =>
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
rescue PG::Error => e
|
|
153
|
+
begin
|
|
154
|
+
@db.exec("ROLLBACK")
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
raise Runtime::WiringError, "cannot merge the tail of #{@domain}: #{e.message.strip}"
|
|
139
159
|
end
|
|
160
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
140
161
|
|
|
141
162
|
# Ids touched by BOTH worlds since the cut — the old world's
|
|
142
163
|
# post-cut tail INTERSECTed with the new world's own writes.
|
|
@@ -2,6 +2,12 @@ module Hecks
|
|
|
2
2
|
module Adapters
|
|
3
3
|
class PostgresEra
|
|
4
4
|
class Lineage
|
|
5
|
+
# Installs the shared, domain-independent `hecks_tr_*` Postgres
|
|
6
|
+
# functions (extract/insert/rename/move/convert/drop over jsonb)
|
|
7
|
+
# that a compiled translation rule set's SQL calls into — the SQL
|
|
8
|
+
# compilation target kept equal to the Ruby reference transform by
|
|
9
|
+
# the cross-execution equivalence spec, never a second source of
|
|
10
|
+
# truth.
|
|
5
11
|
module TransformInstaller
|
|
6
12
|
# The jsonb rule transforms — installed once, idempotently. Kept
|
|
7
13
|
# equal to the port's reference entry-JSON transform by the
|
|
@@ -27,7 +33,26 @@ module Hecks
|
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
36
|
+
# Six independent CREATE OR REPLACE FUNCTION statements — each
|
|
37
|
+
# self-contained SQL, no shared Ruby state, and (per this
|
|
38
|
+
# module's own header comment) safe in any install order since
|
|
39
|
+
# plpgsql bodies aren't resolved against each other until
|
|
40
|
+
# called, not at CREATE time. Split one-per-function below
|
|
41
|
+
# purely so each has its own name and (where relevant) its own
|
|
42
|
+
# comment to sit next to, not because the six have any
|
|
43
|
+
# sequencing dependency on one another.
|
|
30
44
|
def install_transform_functions!
|
|
45
|
+
install_hecks_tr_extract!
|
|
46
|
+
install_hecks_tr_insert!
|
|
47
|
+
install_hecks_tr_rename!
|
|
48
|
+
install_hecks_tr_move!
|
|
49
|
+
install_hecks_tr_convert!
|
|
50
|
+
install_hecks_tr_drop!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def install_hecks_tr_extract!
|
|
31
56
|
@db.exec(<<~SQL)
|
|
32
57
|
CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
|
|
33
58
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -57,15 +82,18 @@ module Hecks
|
|
|
57
82
|
END IF;
|
|
58
83
|
END $fn$
|
|
59
84
|
SQL
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# ADVERSARIAL FINDING: a destination whose top segment already
|
|
88
|
+
# holds a value — most commonly a reference, a bare scalar id
|
|
89
|
+
# — used to be silently overwritten with an empty object the
|
|
90
|
+
# moment a dotted destination needed to nest under it. That is
|
|
91
|
+
# a drop that never declared itself, the one thing this
|
|
92
|
+
# language exists to make explicit (see hecks_tr_convert's own
|
|
93
|
+
# refusal below, the same shape) — refused here instead, with
|
|
94
|
+
# the Ruby reference transform (ports/persistence/lineage.rb's
|
|
95
|
+
# `insert`) raising the identical wording.
|
|
96
|
+
def install_hecks_tr_insert!
|
|
69
97
|
@db.exec(<<~SQL)
|
|
70
98
|
CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
|
|
71
99
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -83,6 +111,9 @@ module Hecks
|
|
|
83
111
|
RETURN jsonb_set(state, path, value);
|
|
84
112
|
END $fn$
|
|
85
113
|
SQL
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def install_hecks_tr_rename!
|
|
86
117
|
@db.exec(<<~SQL)
|
|
87
118
|
CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
|
|
88
119
|
LANGUAGE sql IMMUTABLE AS $fn$
|
|
@@ -91,6 +122,9 @@ module Hecks
|
|
|
91
122
|
ELSE state END
|
|
92
123
|
$fn$
|
|
93
124
|
SQL
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def install_hecks_tr_move!
|
|
94
128
|
@db.exec(<<~SQL)
|
|
95
129
|
CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
|
|
96
130
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -101,6 +135,9 @@ module Hecks
|
|
|
101
135
|
RETURN hecks_tr_insert(extracted.remaining, to_path, extracted.value, rule_label);
|
|
102
136
|
END $fn$
|
|
103
137
|
SQL
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def install_hecks_tr_convert!
|
|
104
141
|
@db.exec(<<~SQL)
|
|
105
142
|
CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
|
|
106
143
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -117,6 +154,9 @@ module Hecks
|
|
|
117
154
|
from_label, extracted.value, extracted.value;
|
|
118
155
|
END $fn$
|
|
119
156
|
SQL
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def install_hecks_tr_drop!
|
|
120
160
|
@db.exec(<<~SQL)
|
|
121
161
|
CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
|
|
122
162
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -66,8 +66,8 @@ module Hecks
|
|
|
66
66
|
declared = edge.for_aggregate(aggregate.name)
|
|
67
67
|
after = begin
|
|
68
68
|
lineage.translated_latest(aggregate, ordinal, chain)
|
|
69
|
-
rescue PG::Error =>
|
|
70
|
-
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{
|
|
69
|
+
rescue PG::Error => e
|
|
70
|
+
raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{e.message.strip}"
|
|
71
71
|
end
|
|
72
72
|
before = if chain.size > 1
|
|
73
73
|
lineage.translated_latest(aggregate, ordinal, chain[0..-2])
|
|
@@ -10,6 +10,15 @@ module Hecks
|
|
|
10
10
|
# held-but-superseded shape boots read-only-toward-the-fence; an
|
|
11
11
|
# unheld shape goes to the minter.
|
|
12
12
|
module EraResolver
|
|
13
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
14
|
+
# A single order-dependent dispatch over a closed set of four
|
|
15
|
+
# boot scenarios (first boot / quiet reboot / held-but-superseded
|
|
16
|
+
# / unheld-shape-goes-to-minter), each already explained by its
|
|
17
|
+
# own comment above, all sharing one `db` connection closed in
|
|
18
|
+
# `ensure`. Splitting the branches into separate methods would
|
|
19
|
+
# turn each `return` (which exits `check!`, closing `db`) into a
|
|
20
|
+
# sentinel value threaded back up, obscuring the mutual
|
|
21
|
+
# exclusivity that IS the method's whole point.
|
|
13
22
|
def check!(registry:, bluebook:, current_text:, settings:, directory: nil)
|
|
14
23
|
db = PostgresEra.connect_for(bluebook.name, settings)
|
|
15
24
|
lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
|
|
@@ -78,6 +87,7 @@ module Hecks
|
|
|
78
87
|
ensure
|
|
79
88
|
db&.close
|
|
80
89
|
end
|
|
90
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
end
|
|
@@ -20,10 +20,29 @@ module Hecks
|
|
|
20
20
|
label = hash[0, Runtime::StorageShape::LABEL_LENGTH]
|
|
21
21
|
ordinal = latest[:ordinal] + 1
|
|
22
22
|
|
|
23
|
+
edge = resolve_edge!(registry, bluebook, lineage, latest, label, ordinal, directory)
|
|
24
|
+
ensure_compute_rekey_approved!(bluebook, lineage, edge, ordinal)
|
|
25
|
+
check_coverage!(registry, bluebook, shadow(latest[:held_text]), edge)
|
|
26
|
+
|
|
27
|
+
chain = edge_chain(registry, bluebook, lineage.eras, label)
|
|
28
|
+
audit!(bluebook, lineage, chain, ordinal, edge)
|
|
29
|
+
lineage.mint_era!(
|
|
30
|
+
ordinal: ordinal, hash: hash, label: label, held_text: current_text,
|
|
31
|
+
aggregates: bluebook.aggregates, edges: chain, role: role,
|
|
32
|
+
projection: Runtime::StorageShape.project(bluebook)
|
|
33
|
+
)
|
|
34
|
+
ordinal
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Find the one translation edge that leaves the held era, and
|
|
38
|
+
# validate it: exactly one edge must leave (eras fork
|
|
39
|
+
# mechanically — a second edge from the same source is a wiring
|
|
40
|
+
# mistake, not a merge to resolve automatically), and it must
|
|
41
|
+
# target the current shape's label (otherwise the edge is stale
|
|
42
|
+
# and boot must not silently mint past it).
|
|
43
|
+
def resolve_edge!(registry, bluebook, lineage, latest, label, ordinal, directory)
|
|
23
44
|
edges = registry.translations.select { |t| t.domain == bluebook.name && t.from == latest[:label] }
|
|
24
|
-
if edges.empty?
|
|
25
|
-
refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory)
|
|
26
|
-
end
|
|
45
|
+
refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory) if edges.empty?
|
|
27
46
|
if edges.size > 1
|
|
28
47
|
raise Runtime::WiringError,
|
|
29
48
|
"cannot boot #{bluebook.name}: #{edges.size} translation edges leave era #{latest[:ordinal]} " \
|
|
@@ -36,43 +55,35 @@ module Hecks
|
|
|
36
55
|
"cannot boot #{bluebook.name}: the translation edge from #{latest[:label]} targets " \
|
|
37
56
|
"#{edge.to}, but the current shape is #{label} — the edge is stale; re-run bin/scaffold_translation"
|
|
38
57
|
end
|
|
58
|
+
edge
|
|
59
|
+
end
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
unless approval && approval[:edge_digest] == Translation::Audit.edge_digest(edge)
|
|
52
|
-
raise Runtime::WiringError,
|
|
53
|
-
"cannot mint era #{ordinal} of #{bluebook.name}: this edge carries a compute or rekey " \
|
|
54
|
-
"rule, and the audit's human-approved sample is its only verification — run " \
|
|
55
|
-
"bin/translation_audit with --approve, then boot again"
|
|
56
|
-
end
|
|
57
|
-
tip = lineage.last_ordinal
|
|
58
|
-
if approval[:reviewed_ordinal] != tip
|
|
59
|
-
raise Runtime::WiringError,
|
|
60
|
-
"cannot mint era #{ordinal} of #{bluebook.name}: the journal advanced past the approved " \
|
|
61
|
-
"review (ordinal #{approval[:reviewed_ordinal]} reviewed, #{tip} now) — the samples a " \
|
|
62
|
-
"human approved no longer cover the data; re-run bin/translation_audit with --approve"
|
|
63
|
-
end
|
|
64
|
-
end
|
|
61
|
+
# A compute's (and, the same way, a rekey's) only verification
|
|
62
|
+
# is the audit's human-approved sample — mint stays
|
|
63
|
+
# non-interactive by requiring the approval to already exist,
|
|
64
|
+
# recorded IN THIS DATABASE by `bin/translation_audit …
|
|
65
|
+
# --approve` and bound to what was actually reviewed: this
|
|
66
|
+
# edge's parsed content, and the journal as it stood when the
|
|
67
|
+
# samples were read. A journal that has advanced past the
|
|
68
|
+
# review invalidates it — the approved samples no longer cover
|
|
69
|
+
# the data.
|
|
70
|
+
def ensure_compute_rekey_approved!(bluebook, lineage, edge, ordinal)
|
|
71
|
+
return unless edge.aggregates.any? { |declared| !declared.computes.empty? || !declared.rekeys.empty? }
|
|
65
72
|
|
|
66
|
-
|
|
73
|
+
approval = lineage.approval_for(from: edge.from, to: edge.to)
|
|
74
|
+
unless approval && approval[:edge_digest] == Translation::Audit.edge_digest(edge)
|
|
75
|
+
raise Runtime::WiringError,
|
|
76
|
+
"cannot mint era #{ordinal} of #{bluebook.name}: this edge carries a compute or rekey " \
|
|
77
|
+
"rule, and the audit's human-approved sample is its only verification — run " \
|
|
78
|
+
"bin/translation_audit with --approve, then boot again"
|
|
79
|
+
end
|
|
80
|
+
tip = lineage.last_ordinal
|
|
81
|
+
return unless approval[:reviewed_ordinal] != tip
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
aggregates: bluebook.aggregates, edges: chain, role: role,
|
|
73
|
-
projection: Runtime::StorageShape.project(bluebook)
|
|
74
|
-
)
|
|
75
|
-
ordinal
|
|
83
|
+
raise Runtime::WiringError,
|
|
84
|
+
"cannot mint era #{ordinal} of #{bluebook.name}: the journal advanced past the approved " \
|
|
85
|
+
"review (ordinal #{approval[:reviewed_ordinal]} reviewed, #{tip} now) — the samples a " \
|
|
86
|
+
"human approved no longer cover the data; re-run bin/translation_audit with --approve"
|
|
76
87
|
end
|
|
77
88
|
|
|
78
89
|
# No edge yet: the boot refuses toward the authoring loop —
|
|
@@ -96,7 +107,7 @@ module Hecks
|
|
|
96
107
|
# Diff the held era against the current shape and write the edge
|
|
97
108
|
# file — confident rules inline, ambiguities as parse-refusing
|
|
98
109
|
# `unresolved` lines. Returns the file path.
|
|
99
|
-
def scaffold!(
|
|
110
|
+
def scaffold!(_registry, bluebook, lineage, latest, directory)
|
|
100
111
|
ensure_named!(lineage, latest)
|
|
101
112
|
latest = lineage.eras.last
|
|
102
113
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
require_relative "../../../../adapters/driven/sql_query_builder"
|
|
4
|
+
require_relative "../../../../adapters/driven/postgres/outbox"
|
|
4
5
|
require_relative "postgres_era/lineage"
|
|
5
6
|
require_relative "postgres_era/lineage_manager"
|
|
6
7
|
require_relative "../../../../ports/persistence/append_only"
|
|
@@ -40,6 +41,7 @@ module Hecks
|
|
|
40
41
|
# operator compiles fully into SQL, or the query refuses loudly.
|
|
41
42
|
class PostgresEra
|
|
42
43
|
include SqlQueryBuilder
|
|
44
|
+
include Adapters::PostgresOutbox
|
|
43
45
|
|
|
44
46
|
attr_reader :aggregate
|
|
45
47
|
|
|
@@ -139,9 +141,9 @@ module Hecks
|
|
|
139
141
|
# problems) still surface.
|
|
140
142
|
connection.exec("SET client_min_messages = warning")
|
|
141
143
|
connection
|
|
142
|
-
rescue PG::Error =>
|
|
144
|
+
rescue PG::Error => e
|
|
143
145
|
raise Runtime::WiringError,
|
|
144
|
-
"cannot bind PostgresEra at #{declared} for #{name}: #{
|
|
146
|
+
"cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip}"
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
@@ -192,6 +194,7 @@ module Hecks
|
|
|
192
194
|
@field_caches = ensure_field_caches!
|
|
193
195
|
create_event_table!
|
|
194
196
|
create_saga_table!
|
|
197
|
+
create_outbox_table!
|
|
195
198
|
end
|
|
196
199
|
|
|
197
200
|
def table = @aggregate.storage_name
|
|
@@ -216,7 +219,10 @@ module Hecks
|
|
|
216
219
|
return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
|
|
217
220
|
|
|
218
221
|
name = order_by.to_s.split(".").first
|
|
219
|
-
|
|
222
|
+
unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
|
|
223
|
+
raise Runtime::WiringError,
|
|
224
|
+
"#{@aggregate.name} has no attribute #{order_by.inspect} to order by"
|
|
225
|
+
end
|
|
220
226
|
|
|
221
227
|
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
222
228
|
@db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) }
|
|
@@ -286,7 +292,7 @@ module Hecks
|
|
|
286
292
|
# make that replay pay real DB cost for a snapshot that's already
|
|
287
293
|
# correct.
|
|
288
294
|
def append(entry)
|
|
289
|
-
|
|
295
|
+
transaction do
|
|
290
296
|
lock_writes!
|
|
291
297
|
append_and_project!(entry)
|
|
292
298
|
end
|
|
@@ -299,7 +305,7 @@ module Hecks
|
|
|
299
305
|
# occurs before entering this adapter-native operation.
|
|
300
306
|
def atomic_put(entry, insert_only: false)
|
|
301
307
|
status = nil
|
|
302
|
-
|
|
308
|
+
transaction do
|
|
303
309
|
lock_writes!
|
|
304
310
|
exists = !@db.exec_params(
|
|
305
311
|
"SELECT 1 FROM #{quoted_head} WHERE id = $1 LIMIT 1",
|
|
@@ -418,7 +424,8 @@ module Hecks
|
|
|
418
424
|
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
419
425
|
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
|
|
420
426
|
"completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
|
|
421
|
-
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory),
|
|
427
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory),
|
|
428
|
+
JSON.generate(completed_compensations)]
|
|
422
429
|
)
|
|
423
430
|
end
|
|
424
431
|
|
|
@@ -433,7 +440,8 @@ module Hecks
|
|
|
433
440
|
return enum_for(:each_saga) unless block_given?
|
|
434
441
|
|
|
435
442
|
@db.exec_params(
|
|
436
|
-
"SELECT process_manager, correlation, state, memory, completed_compensations
|
|
443
|
+
"SELECT process_manager, correlation, state, memory, completed_compensations " \
|
|
444
|
+
"FROM hecks_saga_instances WHERE domain = $1",
|
|
437
445
|
[@domain]
|
|
438
446
|
).each do |row|
|
|
439
447
|
yield row["process_manager"], row["correlation"], row["state"],
|
|
@@ -624,11 +632,12 @@ module Hecks
|
|
|
624
632
|
end
|
|
625
633
|
|
|
626
634
|
def cached_where_fields
|
|
627
|
-
declared_queries.flat_map
|
|
628
|
-
q.wheres.map
|
|
635
|
+
fields = declared_queries.flat_map do |q|
|
|
636
|
+
q.wheres.map do |clause|
|
|
629
637
|
clause.field.to_s
|
|
630
|
-
|
|
631
|
-
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
fields.uniq.select { |field| cacheable_field?(field) }
|
|
632
641
|
end
|
|
633
642
|
|
|
634
643
|
def declared_queries
|
|
@@ -709,7 +718,7 @@ module Hecks
|
|
|
709
718
|
clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
|
|
710
719
|
end
|
|
711
720
|
|
|
712
|
-
sql =
|
|
721
|
+
sql = "SELECT #{select_list} FROM #{from_relation} WHERE #{clauses.join(' AND ')}"
|
|
713
722
|
sql << if declared.order_by
|
|
714
723
|
" ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
|
|
715
724
|
else
|
|
@@ -751,7 +760,8 @@ module Hecks
|
|
|
751
760
|
# this same domain already created before this column existed —
|
|
752
761
|
# the same idiom `rust/host/src/journal.rs`'s own
|
|
753
762
|
# `sagas_backfilled` column addition already uses.
|
|
754
|
-
@db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb
|
|
763
|
+
@db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb " \
|
|
764
|
+
"NOT NULL DEFAULT '[]'::jsonb")
|
|
755
765
|
end
|
|
756
766
|
end
|
|
757
767
|
end
|
|
@@ -15,29 +15,27 @@ module Hecks
|
|
|
15
15
|
module LayerOne
|
|
16
16
|
def layer_one!(violations, aggregate, after)
|
|
17
17
|
after.each do |id, state|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
next unless lifecycle
|
|
18
|
+
symbolized = JSON.parse(JSON.generate(state), symbolize_names: true)
|
|
19
|
+
instance = Runtime::Instance.new(aggregate: aggregate, id: id, state: symbolized)
|
|
20
|
+
lifecycle = aggregate.lifecycle
|
|
21
|
+
next unless lifecycle
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
rescue Runtime::InvariantViolation, Runtime::TypeMismatch => error
|
|
39
|
-
violations << "#{aggregate.name}##{id}: #{error.message}"
|
|
23
|
+
held = instance[lifecycle.field]
|
|
24
|
+
# `Lifecycle#states` answers default+targets only — a state
|
|
25
|
+
# legitimately declared just as a `from:` (a terminal
|
|
26
|
+
# transition's source, never anyone's target) is real and
|
|
27
|
+
# reachable but invisible to it. `ModelCheck.full_states`
|
|
28
|
+
# is the full declared set (default, every target, AND
|
|
29
|
+
# every from) that `fuzzing/properties.rb`'s own replay
|
|
30
|
+
# check already uses for this identical question — see its
|
|
31
|
+
# comment on this same hole.
|
|
32
|
+
allowed = Bluebook::ModelCheck.full_states(lifecycle)
|
|
33
|
+
unless held.nil? || allowed.include?(held.to_s)
|
|
34
|
+
violations << "#{aggregate.name}##{id}: #{lifecycle.field} is #{held.inspect}, " \
|
|
35
|
+
"a state this era's lifecycle never reaches"
|
|
40
36
|
end
|
|
37
|
+
rescue Runtime::InvariantViolation, Runtime::TypeMismatch => e
|
|
38
|
+
violations << "#{aggregate.name}##{id}: #{e.message}"
|
|
41
39
|
end
|
|
42
40
|
end
|
|
43
41
|
end
|
|
@@ -24,6 +24,26 @@ module Hecks
|
|
|
24
24
|
def layer_two!(violations, aggregate, declared, before, after)
|
|
25
25
|
rekeyed = declared && !declared.rekeys.empty?
|
|
26
26
|
|
|
27
|
+
check_id_conservation!(violations, aggregate, before, after, rekeyed: rekeyed)
|
|
28
|
+
|
|
29
|
+
return unless declared
|
|
30
|
+
# No correspondence between an old id and its rekeyed row exists
|
|
31
|
+
# in Ruby (the rekey's SQL is its only implementation, same as
|
|
32
|
+
# compute's) — exempt from the per-record equivalence gate for
|
|
33
|
+
# the same reason compute paths already are, extended to the
|
|
34
|
+
# whole record since an old-id lookup into `after` can never
|
|
35
|
+
# succeed once the id itself has changed.
|
|
36
|
+
return if rekeyed
|
|
37
|
+
|
|
38
|
+
check_value_preservation!(violations, aggregate, declared, before, after)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Record-count / id-set conservation — one of the two properties
|
|
42
|
+
# this module's own header names, and independent of the other
|
|
43
|
+
# (per-rule value preservation, below): it needs only `before`,
|
|
44
|
+
# `after`, and whether the edge rekeyed, never the declared rules
|
|
45
|
+
# themselves.
|
|
46
|
+
def check_id_conservation!(violations, aggregate, before, after, rekeyed:)
|
|
27
47
|
# A rekey legitimately changes the id SET (that's the entire
|
|
28
48
|
# point) — set-equality would flag every honest rekey as data
|
|
29
49
|
# loss. What must still hold is RECORD COUNT: a botched rekey
|
|
@@ -43,15 +63,13 @@ module Hecks
|
|
|
43
63
|
violations << "#{aggregate.name}: the id set changed across the edge " \
|
|
44
64
|
"(lost #{lost.sort.inspect}, gained #{gained.sort.inspect})"
|
|
45
65
|
end
|
|
46
|
-
|
|
47
|
-
# No correspondence between an old id and its rekeyed row exists
|
|
48
|
-
# in Ruby (the rekey's SQL is its only implementation, same as
|
|
49
|
-
# compute's) — exempt from the per-record equivalence gate for
|
|
50
|
-
# the same reason compute paths already are, extended to the
|
|
51
|
-
# whole record since an old-id lookup into `after` can never
|
|
52
|
-
# succeed once the id itself has changed.
|
|
53
|
-
return if rekeyed
|
|
66
|
+
end
|
|
54
67
|
|
|
68
|
+
# Per-rule value preservation — the other property this module's
|
|
69
|
+
# header names. Only reached once `layer_two!` has already ruled
|
|
70
|
+
# out "no declared edge" and "rekeyed" (see its own comment on
|
|
71
|
+
# that second guard); this method assumes both are false.
|
|
72
|
+
def check_value_preservation!(violations, aggregate, declared, before, after)
|
|
55
73
|
rules = Ports::Persistence::Lineage.from_declared(declared, aggregate.name)
|
|
56
74
|
compute_paths = declared.computes.flat_map { |compute| [compute.from, compute.to] }.map(&:to_s)
|
|
57
75
|
|
|
@@ -63,7 +81,7 @@ module Hecks
|
|
|
63
81
|
actual = strip_compute_paths(normalize(after[id]), compute_paths)
|
|
64
82
|
next if expected == actual
|
|
65
83
|
|
|
66
|
-
diverged = (expected.keys | actual.keys).
|
|
84
|
+
diverged = (expected.keys | actual.keys).reject { |key| expected[key] == actual[key] }
|
|
67
85
|
violations << "#{aggregate.name}##{id}: the translated state diverges from the reference " \
|
|
68
86
|
"transform at #{diverged.sort.join(', ')}"
|
|
69
87
|
end
|
|
@@ -7,6 +7,15 @@ module Hecks
|
|
|
7
7
|
# attribute, and the loud refusal for a REQUIRED one comes from
|
|
8
8
|
# Layer 1 the moment an invariant reads it.
|
|
9
9
|
module UnfedReport
|
|
10
|
+
# `fed` build-up plus one ordered guard chain per attribute
|
|
11
|
+
# (already fed, has a default, actually present in some record, no
|
|
12
|
+
# records to check at all) answering a single question — "is
|
|
13
|
+
# anything, ever, going to feed this field" — where each `next`
|
|
14
|
+
# rules out one way the answer is "not unfed after all" before the
|
|
15
|
+
# next is even worth asking. Splitting the guards out would just
|
|
16
|
+
# turn each into a same-shaped one-line predicate call.
|
|
17
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
18
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
10
19
|
def unfed(aggregate, declared, after)
|
|
11
20
|
fed = if declared
|
|
12
21
|
(declared.renames.values.map(&:to_s) +
|