hecks 0.3.0 → 1.0.1
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/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/behaviour/value_object.rb +13 -7
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +144 -3
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +49 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/ast_json.rb +149 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +147 -6
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/project_register.rb +25 -0
- data/lib/hecks/bluebook/value_object.rb +19 -2
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +3 -2
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/hecksagon/domain_port.bluebook +25 -1
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +165 -34
- data/lib/hecks/runtime/command_rules/admissibility.rb +101 -16
- data/lib/hecks/runtime/command_rules/arithmetic.rb +40 -4
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +66 -5
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +92 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +89 -5
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +28 -3
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +49 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
|
-
require_relative "sql_query_builder"
|
|
3
|
+
require_relative "../../../../adapters/driven/sql_query_builder"
|
|
4
4
|
require_relative "postgres_era/lineage"
|
|
5
5
|
require_relative "postgres_era/lineage_manager"
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
9
|
-
require_relative "
|
|
10
|
-
require_relative "
|
|
11
|
-
require_relative "
|
|
6
|
+
require_relative "../../../../ports/persistence/append_only"
|
|
7
|
+
require_relative "../../../../query_specification/common/order_by"
|
|
8
|
+
require_relative "../../../../runtime/errors"
|
|
9
|
+
require_relative "../../../../runtime/event"
|
|
10
|
+
require_relative "../../../../runtime/instance"
|
|
11
|
+
require_relative "../../../../runtime/registry"
|
|
12
12
|
|
|
13
13
|
module Hecks
|
|
14
14
|
module Adapters
|
|
@@ -67,12 +67,30 @@ module Hecks
|
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# BOTH SPELLINGS OF A SETTING ARE HONORED — a world's settings hash
|
|
71
|
+
# may arrive symbol-keyed (built straight in Ruby) or string-keyed
|
|
72
|
+
# (round-tripped through JSON), and a domain that names one is not
|
|
73
|
+
# obligated to also skip the other. `key?` decides which spelling
|
|
74
|
+
# actually exists, never `||` — `||` cannot tell a genuinely stored
|
|
75
|
+
# `false` apart from an absent key, and would silently prefer the
|
|
76
|
+
# OTHER spelling (or `default`) instead of returning the real, held
|
|
77
|
+
# answer. See Hecks::QuerySpecification::FieldPath#read for the same
|
|
78
|
+
# discipline applied to stored state instead of settings.
|
|
79
|
+
def self.setting(settings, key, default: nil)
|
|
80
|
+
return settings[key] if settings.key?(key)
|
|
81
|
+
|
|
82
|
+
str_key = key.to_s
|
|
83
|
+
return settings[str_key] if settings.key?(str_key)
|
|
84
|
+
|
|
85
|
+
default
|
|
86
|
+
end
|
|
87
|
+
|
|
70
88
|
def self.connect_for(name, settings)
|
|
71
89
|
# LAZY, ON PURPOSE — same reasoning as Sqlite's own initialize:
|
|
72
90
|
# a domain that never wires PostgresEra should never need the gem.
|
|
73
91
|
require "pg"
|
|
74
92
|
|
|
75
|
-
declared = settings
|
|
93
|
+
declared = setting(settings, :database)
|
|
76
94
|
if declared.to_s.empty?
|
|
77
95
|
raise Runtime::WiringError,
|
|
78
96
|
"#{name} binds PostgresEra, which needs a database connection, " \
|
|
@@ -94,7 +112,7 @@ module Hecks
|
|
|
94
112
|
# TABLE ... SET SCHEMA migrations transparent to the rest of the
|
|
95
113
|
# adapter. A domain with no `schema` setting keeps Postgres's
|
|
96
114
|
# own default search_path (public), same as before this existed.
|
|
97
|
-
schema = settings
|
|
115
|
+
schema = setting(settings, :schema)
|
|
98
116
|
if schema.to_s != ""
|
|
99
117
|
# THE SCHEMA ITSELF, IDEMPOTENTLY — a domain naming a `schema:`
|
|
100
118
|
# nobody has created yet used to fail on its FIRST table-
|
|
@@ -132,14 +150,29 @@ module Hecks
|
|
|
132
150
|
# The domain names the journal (one journal per lineage). The
|
|
133
151
|
# factory injects it; a directly-instantiated adapter (specs,
|
|
134
152
|
# consoles) journals under the aggregate's own name.
|
|
135
|
-
@domain = (settings
|
|
153
|
+
@domain = self.class.setting(settings, :domain, default: aggregate.storage_name).to_s
|
|
136
154
|
@lineage = Lineage.new(@db, @domain)
|
|
137
155
|
@lineage.ensure_base!
|
|
138
156
|
# The era gate resolves which era this boot IS (an old checkout
|
|
139
157
|
# boots a held-but-superseded era and keeps writing its own
|
|
140
158
|
# partition); a directly-instantiated adapter defaults to the
|
|
141
159
|
# newest.
|
|
142
|
-
|
|
160
|
+
#
|
|
161
|
+
# NOT `self.class.setting(...)` here — RepositoryFactory#build
|
|
162
|
+
# always merges `era: registry.resolved_eras[domain]` into
|
|
163
|
+
# settings, so the key is genuinely PRESENT (not absent) for
|
|
164
|
+
# any domain the era boot gate hasn't resolved yet (or that
|
|
165
|
+
# doesn't have one at all), just holding `nil`. `setting`'s own
|
|
166
|
+
# presence-over-truthiness discipline (correct for a field like
|
|
167
|
+
# `:role`, where a stored `false` is a real, distinct answer
|
|
168
|
+
# from "unset") does the wrong thing for `:era` specifically:
|
|
169
|
+
# `nil` can never be a meaningful era override — only a real
|
|
170
|
+
# ordinal or "not resolved, self-resolve" ever apply — so
|
|
171
|
+
# coalescing here, not `setting`'s presence check, is what
|
|
172
|
+
# actually honors this comment's own "defaults to the newest"
|
|
173
|
+
# promise.
|
|
174
|
+
@era = settings.key?(:era) ? settings[:era] : settings["era"]
|
|
175
|
+
@era ||= @lineage.current_era
|
|
143
176
|
# Unconditional and idempotent, regardless of era — belt-and-
|
|
144
177
|
# suspenders self-healing (compile_head! already ensures this for
|
|
145
178
|
# a freshly-minted era's own name; ensure_first_head! for era 1's)
|
|
@@ -309,8 +342,31 @@ module Hecks
|
|
|
309
342
|
end
|
|
310
343
|
end
|
|
311
344
|
|
|
345
|
+
# The journal carries FORCE ROW LEVEL SECURITY with exactly two
|
|
346
|
+
# policies — hecks_current_era's INSERT and hecks_read_all's
|
|
347
|
+
# SELECT (advance_era! above) — and no DELETE policy at all, for
|
|
348
|
+
# anyone. FORCE means even the table's own owner is fenced by
|
|
349
|
+
# that (only an actual Postgres superuser or a role granted
|
|
350
|
+
# BYPASSRLS sits above it — see lineage.rb's own header), so a
|
|
351
|
+
# plain `DELETE ... WHERE aggregate = $1` from an ordinary
|
|
352
|
+
# connection silently matches zero rows: no privilege error, no
|
|
353
|
+
# exception, just a no-op that looks like success. Counting
|
|
354
|
+
# before and comparing to what the DELETE itself reports is what
|
|
355
|
+
# tells "nothing to delete" apart from "RLS silently ate the
|
|
356
|
+
# delete" — the same row count, from the same statement, either
|
|
357
|
+
# way, with no separate query racing the DELETE for an answer.
|
|
312
358
|
def reset!
|
|
313
|
-
@db.exec_params(
|
|
359
|
+
before = @db.exec_params(
|
|
360
|
+
"SELECT count(*) FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table]
|
|
361
|
+
)[0]["count"].to_i
|
|
362
|
+
result = @db.exec_params("DELETE FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table])
|
|
363
|
+
if before.positive? && result.cmd_tuples.zero?
|
|
364
|
+
raise Runtime::WiringError,
|
|
365
|
+
"reset! deleted 0 of #{before} row(s) for #{table} in #{@lineage.quoted_journal} — " \
|
|
366
|
+
"FORCE ROW LEVEL SECURITY admits no DELETE policy on the journal, so this connection's " \
|
|
367
|
+
"DELETE silently matched nothing. reset! only works connected as an actual Postgres " \
|
|
368
|
+
"superuser or a role granted BYPASSRLS, not as the provisioner or an app role."
|
|
369
|
+
end
|
|
314
370
|
self
|
|
315
371
|
end
|
|
316
372
|
|
|
@@ -355,13 +411,14 @@ module Hecks
|
|
|
355
411
|
# `SagaInterpreter`'s own mutex (§7) serializing IN-PROCESS writers,
|
|
356
412
|
# and gets the SAME cross-process safety an aggregate's own writes
|
|
357
413
|
# get from this adapter — no better, no worse.
|
|
358
|
-
def save_saga(process_manager:, correlation:, state:, memory:)
|
|
414
|
+
def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
|
|
359
415
|
@db.exec_params(
|
|
360
|
-
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
|
|
361
|
-
"VALUES ($1, $2, $3, $4, $5) " \
|
|
416
|
+
"INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
|
|
417
|
+
"VALUES ($1, $2, $3, $4, $5, $6) " \
|
|
362
418
|
"ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
|
|
363
|
-
"SET state = EXCLUDED.state, memory = EXCLUDED.memory,
|
|
364
|
-
|
|
419
|
+
"SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
|
|
420
|
+
"completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
|
|
421
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
|
|
365
422
|
)
|
|
366
423
|
end
|
|
367
424
|
|
|
@@ -376,11 +433,12 @@ module Hecks
|
|
|
376
433
|
return enum_for(:each_saga) unless block_given?
|
|
377
434
|
|
|
378
435
|
@db.exec_params(
|
|
379
|
-
"SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = $1",
|
|
436
|
+
"SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = $1",
|
|
380
437
|
[@domain]
|
|
381
438
|
).each do |row|
|
|
382
439
|
yield row["process_manager"], row["correlation"], row["state"],
|
|
383
|
-
JSON.parse(row["memory"], symbolize_names: true)
|
|
440
|
+
JSON.parse(row["memory"], symbolize_names: true),
|
|
441
|
+
JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true)
|
|
384
442
|
end
|
|
385
443
|
end
|
|
386
444
|
|
|
@@ -404,9 +462,9 @@ module Hecks
|
|
|
404
462
|
|
|
405
463
|
if entry.save?
|
|
406
464
|
@db.exec_params(
|
|
407
|
-
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, state) VALUES ($1, $2, $3) " \
|
|
408
|
-
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal,
|
|
409
|
-
"WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
465
|
+
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
|
|
466
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
467
|
+
"state = EXCLUDED.state WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
410
468
|
[entry.id, ordinal, state_json]
|
|
411
469
|
)
|
|
412
470
|
# SAME TRANSACTION, SAME ORDINAL — every field cache stays
|
|
@@ -419,7 +477,30 @@ module Hecks
|
|
|
419
477
|
@lineage.upsert_field_cache_row!(cache_table, entry.id, ordinal, state_json, query_expression(field))
|
|
420
478
|
end
|
|
421
479
|
else
|
|
422
|
-
|
|
480
|
+
# A TOMBSTONE ROW, NOT A BARE DELETE — H3 (docs/audits/2026-08-
|
|
481
|
+
# 10-main-bug-audit.md). `DELETE FROM head_snapshot` used to be
|
|
482
|
+
# the whole story here, which is correct in isolation but wrong
|
|
483
|
+
# once an ancestor era is in the picture: for a record carried
|
|
484
|
+
# into this era from an ancestor, removing this era's row left
|
|
485
|
+
# NOTHING on the current-era side of `compile_head!`'s union to
|
|
486
|
+
# outrank the ancestor matview's own (still-present, still
|
|
487
|
+
# `save`) row, so `DISTINCT ON` picked the ancestor's row and
|
|
488
|
+
# the "deleted" record kept reading back forever. Upserting a
|
|
489
|
+
# tombstone (`operation = 'delete'`, `state` NULL) instead
|
|
490
|
+
# means this era always has ITS OWN newest-ordinal row for the
|
|
491
|
+
# id, exactly like a real re-save already did ("re-saves are
|
|
492
|
+
# masked correctly" — the audit's own phrasing for why that
|
|
493
|
+
# half of this was never broken) — it just carries `operation
|
|
494
|
+
# = 'delete'` instead of `'save'`, so `head_view`'s own `WHERE
|
|
495
|
+
# operation = 'save'` still correctly hides it. Ordinal-guarded
|
|
496
|
+
# the same as every other upsert here, so an out-of-order
|
|
497
|
+
# replay can never let a stale delete clobber a newer save.
|
|
498
|
+
@db.exec_params(
|
|
499
|
+
"INSERT INTO #{quoted_head_snapshot} (id, ordinal, operation, state) VALUES ($1, $2, 'delete', NULL) " \
|
|
500
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
501
|
+
"state = EXCLUDED.state WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
|
|
502
|
+
[entry.id, ordinal]
|
|
503
|
+
)
|
|
423
504
|
@field_caches.each_value { |cache_table| @lineage.delete_field_cache_row!(cache_table, entry.id) }
|
|
424
505
|
end
|
|
425
506
|
|
|
@@ -656,15 +737,21 @@ module Hecks
|
|
|
656
737
|
def create_saga_table!
|
|
657
738
|
@db.exec(<<~SQL)
|
|
658
739
|
CREATE TABLE IF NOT EXISTS hecks_saga_instances (
|
|
659
|
-
domain
|
|
660
|
-
process_manager
|
|
661
|
-
correlation
|
|
662
|
-
state
|
|
663
|
-
memory
|
|
664
|
-
|
|
740
|
+
domain text NOT NULL,
|
|
741
|
+
process_manager text NOT NULL,
|
|
742
|
+
correlation text NOT NULL,
|
|
743
|
+
state text NOT NULL,
|
|
744
|
+
memory jsonb NOT NULL,
|
|
745
|
+
completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
746
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
665
747
|
PRIMARY KEY (domain, process_manager, correlation)
|
|
666
748
|
)
|
|
667
749
|
SQL
|
|
750
|
+
# `CREATE TABLE IF NOT EXISTS` above is a no-op against a table
|
|
751
|
+
# this same domain already created before this column existed —
|
|
752
|
+
# the same idiom `rust/host/src/journal.rs`'s own
|
|
753
|
+
# `sagas_backfilled` column addition already uses.
|
|
754
|
+
@db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb")
|
|
668
755
|
end
|
|
669
756
|
end
|
|
670
757
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../runtime/errors"
|
|
2
|
+
require_relative "../../../../../../runtime/instance"
|
|
3
|
+
require_relative "../../../../../../runtime/value/invariant_violation"
|
|
4
|
+
require_relative "../../../../../../bluebook/model_check"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module Translation
|
|
@@ -21,8 +22,16 @@ module Hecks
|
|
|
21
22
|
next unless lifecycle
|
|
22
23
|
|
|
23
24
|
held = instance[lifecycle.field]
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# `Lifecycle#states` answers default+targets only — a state
|
|
26
|
+
# legitimately declared just as a `from:` (a terminal
|
|
27
|
+
# transition's source, never anyone's target) is real and
|
|
28
|
+
# reachable but invisible to it. `ModelCheck.full_states`
|
|
29
|
+
# is the full declared set (default, every target, AND
|
|
30
|
+
# every from) that `fuzzing/properties.rb`'s own replay
|
|
31
|
+
# check already uses for this identical question — see its
|
|
32
|
+
# comment on this same hole.
|
|
33
|
+
allowed = Bluebook::ModelCheck.full_states(lifecycle)
|
|
34
|
+
unless held.nil? || allowed.include?(held.to_s)
|
|
26
35
|
violations << "#{aggregate.name}##{id}: #{lifecycle.field} is #{held.inspect}, " \
|
|
27
36
|
"a state this era's lifecycle never reaches"
|
|
28
37
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "../../
|
|
1
|
+
require_relative "../../../../../../ports/persistence/append_only"
|
|
2
|
+
require_relative "../../lineage"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Translation
|
|
@@ -53,15 +53,14 @@ module Hecks
|
|
|
53
53
|
return if rekeyed
|
|
54
54
|
|
|
55
55
|
rules = Ports::Persistence::Lineage.from_declared(declared, aggregate.name)
|
|
56
|
-
|
|
57
|
-
.map { |path| path.to_s.split(".").first }
|
|
56
|
+
compute_paths = declared.computes.flat_map { |compute| [compute.from, compute.to] }.map(&:to_s)
|
|
58
57
|
|
|
59
58
|
before.each do |id, state|
|
|
60
59
|
next unless after.key?(id)
|
|
61
60
|
|
|
62
61
|
entry = Ports::Persistence::Entry.new(operation: "save", id: id, state: state.transform_keys(&:to_sym))
|
|
63
|
-
expected = normalize(rules.translate(entry).state)
|
|
64
|
-
actual = normalize(after[id])
|
|
62
|
+
expected = strip_compute_paths(normalize(rules.translate(entry).state), compute_paths)
|
|
63
|
+
actual = strip_compute_paths(normalize(after[id]), compute_paths)
|
|
65
64
|
next if expected == actual
|
|
66
65
|
|
|
67
66
|
diverged = (expected.keys | actual.keys).select { |key| expected[key] != actual[key] }
|
|
@@ -71,6 +70,32 @@ module Hecks
|
|
|
71
70
|
end
|
|
72
71
|
|
|
73
72
|
def normalize(state) = JSON.parse(JSON.generate(state))
|
|
73
|
+
|
|
74
|
+
# Exempts exactly the paths a compute owns, not the whole
|
|
75
|
+
# top-level attribute it happens to live under. A bare path
|
|
76
|
+
# ("price_cents") is itself the compute's entire value — dropping
|
|
77
|
+
# the whole top-level key is correct, there's nothing else there
|
|
78
|
+
# to check. A DOTTED path ("price.cents") only owns that one
|
|
79
|
+
# member of the value object it reaches into; every sibling
|
|
80
|
+
# member (e.g. "price.currency") is untouched by the compute and
|
|
81
|
+
# must stay subject to the equivalence check below. Blanket-
|
|
82
|
+
# dropping the whole top-level key for a dotted compute used to
|
|
83
|
+
# exempt the entire attribute — silent data loss elsewhere in the
|
|
84
|
+
# same value object (a migration that nulls or drops a sibling
|
|
85
|
+
# field) produced zero violations, defeating the one gate whose
|
|
86
|
+
# entire purpose is to catch exactly that.
|
|
87
|
+
def strip_compute_paths(state, paths)
|
|
88
|
+
paths.each do |path|
|
|
89
|
+
segments = path.split(".")
|
|
90
|
+
if segments.length == 1
|
|
91
|
+
state.delete(segments.first)
|
|
92
|
+
else
|
|
93
|
+
parent = segments[0..-2].reduce(state) { |node, segment| node.is_a?(Hash) ? node[segment] : nil }
|
|
94
|
+
parent.delete(segments.last) if parent.is_a?(Hash)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
state
|
|
98
|
+
end
|
|
74
99
|
end
|
|
75
100
|
end
|
|
76
101
|
end
|
data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb
RENAMED
|
@@ -33,7 +33,11 @@ module Hecks
|
|
|
33
33
|
path.to_s.split(".").reduce(state) do |node, segment|
|
|
34
34
|
break nil unless node.is_a?(Hash)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
# `key?` decides which spelling answers, never `||` — a
|
|
37
|
+
# `false` genuinely held at this path must not fall through
|
|
38
|
+
# to the other spelling (usually absent) and read as `nil`,
|
|
39
|
+
# which would wrongly mark a `false`-valued attribute "unfed".
|
|
40
|
+
node.key?(segment) ? node[segment] : node[segment.to_sym]
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "tempfile"
|
|
2
|
-
require_relative "../
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "../
|
|
2
|
+
require_relative "../era_guard"
|
|
3
|
+
require_relative "../../../../../runtime/registry"
|
|
4
|
+
require_relative "../storage_shape"
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Translation
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative "../plugin"
|
|
2
|
+
require_relative "era/era_check"
|
|
3
|
+
require_relative "era/era_guard"
|
|
4
|
+
require_relative "era/era_tamper"
|
|
5
|
+
require_relative "era/storage_shape"
|
|
6
|
+
require_relative "era/lineage"
|
|
7
|
+
require_relative "era/postgres_era"
|
|
8
|
+
require_relative "era/translation"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
module Ports
|
|
12
|
+
module Persistence
|
|
13
|
+
module Plugins
|
|
14
|
+
# ADR 0033 — requiring THIS FILE is installing the plugin. Nothing in
|
|
15
|
+
# Hecks core requires it; an app that binds `PostgresEra`, or wants
|
|
16
|
+
# schema-translation support at all, requires it explicitly — the
|
|
17
|
+
# same shape every adapter-specific spec fixture already uses to
|
|
18
|
+
# load one particular `.adapter` file rather than all of them.
|
|
19
|
+
module Era
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# `Runtime::Loader.run_boot_gates!` asks every loaded persistence
|
|
23
|
+
# plugin to contribute here, generically — it never mentions
|
|
24
|
+
# `EraCheck` or "era" by name. Two gates, both `:pre_verify`:
|
|
25
|
+
#
|
|
26
|
+
# `:era_compute_rules` — unconditional, whenever this plugin is
|
|
27
|
+
# loaded at all. A `compute`/`rekey` rule requires Postgres
|
|
28
|
+
# whatever adapter is actually bound (ADR 0031's own reasoning,
|
|
29
|
+
# unchanged) — this is the rich, adapter-aware version of that
|
|
30
|
+
# check; `Runtime::Loader`'s own structural backstop (plain
|
|
31
|
+
# `Bluebook::Translation` data, no plugin-specific class) only
|
|
32
|
+
# ever fires when NO persistence plugin is loaded at all.
|
|
33
|
+
#
|
|
34
|
+
# `:era_check` — conditional, exactly ADR 0031's own gate,
|
|
35
|
+
# unchanged: registered only when this registry has an aggregate
|
|
36
|
+
# actually bound to a lineage-capable adapter.
|
|
37
|
+
def contribute_boot_gates(registry, gates)
|
|
38
|
+
gates.register(:era_compute_rules, ->(reg, _dir) { Runtime::EraCheck.check_compute_rules_for_registry!(reg) }, phase: :pre_verify)
|
|
39
|
+
gates.register(:era_check, Runtime::EraCheck.method(:check_lineage!), phase: :pre_verify) if
|
|
40
|
+
Runtime::EraCheck.lineage_capable_registry?(registry)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Hecks::Ports::Persistence.register_plugin(:era, Hecks::Ports::Persistence::Plugins::Era)
|
|
@@ -9,7 +9,7 @@ module Hecks
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
require_relative "persistence/binding_policy"
|
|
12
|
-
require_relative "persistence/
|
|
12
|
+
require_relative "persistence/plugin"
|
|
13
13
|
require_relative "persistence/repository_factory"
|
|
14
14
|
require_relative "persistence/append_only"
|
|
15
15
|
require_relative "persistence/execution"
|
|
@@ -29,10 +29,30 @@ module Hecks
|
|
|
29
29
|
class Worker
|
|
30
30
|
attr_reader :projection
|
|
31
31
|
|
|
32
|
+
# The only two policies anything in this codebase ever passes
|
|
33
|
+
# (`bin/project`, every spec) — there is no third, legitimate
|
|
34
|
+
# "lenient append" policy on record anywhere. Before this, any
|
|
35
|
+
# value OTHER than the exact symbol `:strict` silently fell
|
|
36
|
+
# through the `consistent?` check below and appended onto
|
|
37
|
+
# divergent history without a word — not just a real typo like
|
|
38
|
+
# `:strikt`, but a caller-supplied String `"strict"` too (this
|
|
39
|
+
# duck-typed fine via `policy.to_sym`, but that was luck, not a
|
|
40
|
+
# contract: nothing here declared what a valid policy even was).
|
|
41
|
+
# Refusing loudly at construction, once, for anything outside
|
|
42
|
+
# this list turns a silent no-op into an immediate, named error
|
|
43
|
+
# — the "refuse rather than silently skip" reading of L1, since
|
|
44
|
+
# `:strict` really is meant to be the only enforcing contract.
|
|
45
|
+
VALID_POLICIES = %i[refresh strict].freeze
|
|
46
|
+
|
|
32
47
|
def initialize(authoritative, projection, policy: :refresh)
|
|
33
48
|
@authoritative = authoritative
|
|
34
49
|
@projection = projection
|
|
35
50
|
@policy = policy.to_sym
|
|
51
|
+
unless VALID_POLICIES.include?(@policy)
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
"unknown projection catch_up! policy #{@policy.inspect} — expected one of " \
|
|
54
|
+
"#{VALID_POLICIES.map(&:inspect).join(' or ')}"
|
|
55
|
+
end
|
|
36
56
|
end
|
|
37
57
|
|
|
38
58
|
# Invoke from a separate process or scheduler. The command-side write
|